Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/editor/src/assets/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"add-annotation-layer": "Neue Annotation hinzufügen",
"no-layers": "Keine Ebenen geladen.",
"untitled-layer": "Unbenannte Ebene",
"untitled-layer-group": "Unbenannte Ebenengruppe",
"layer-settings": "Ebeneneinstellungen",
"opacity": "Deckkraft",
"background-color": "Hintergrundfarbe",
Expand All @@ -104,6 +105,8 @@
"rename-layer": "Ebene umbenennen",
"delete-layer": "Ebene löschen",
"delete-layer-confirmation": "Ebene \"{{layer}}\" löschen?",
"delete-layer-group": "Ebenengruppe löschen",
"delete-layer-group-confirmation": "Ebenengruppe \"{{layerGroup}}\" löschen?",

"view-settings": "Ansichtseinstellungen",
"transverse": "Transverse (1)",
Expand Down
3 changes: 3 additions & 0 deletions apps/editor/src/assets/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"add-annotation-layer": "Add new annotation",
"no-layers": "No layers loaded.",
"untitled-layer": "Untitled layer",
"untitled-layer-group": "Untitled layer group",
"layer-settings": "Layer Settings",
"opacity": "Opacity",
"background-color": "Background Color",
Expand All @@ -104,6 +105,8 @@
"rename-layer": "Rename Layer",
"delete-layer": "Delete Layer",
"delete-layer-confirmation": "Delete layer \"{{layer}}\"?",
"delete-layer-group": "Delete Layer Group",
"delete-layer-group-confirmation": "Delete layer group \"{{layerGroup}}\"?",

"view-settings": "View Settings",
"transverse": "Transverse (1)",
Expand Down
269 changes: 256 additions & 13 deletions apps/editor/src/components/editor/layers/layers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FloatingUIButton,
IImageLayer,
ILayer,
ILayerGroup,
InfoText,
List,
ListItem,
Expand Down Expand Up @@ -75,7 +76,8 @@ const LayerListItem = observer<{
index: number;
isActive?: boolean;
isLast?: boolean;
}>(({ layer, index, isActive, isLast }) => {
isChild?: boolean;
}>(({ layer, index, isActive, isLast, isChild }) => {
const store = useStore();

const toggleAnnotationVisibility = useCallback(() => {
Expand Down Expand Up @@ -251,6 +253,7 @@ const LayerListItem = observer<{
onTrailingIconPress={toggleAnnotationVisibility}
isActive={isActive}
isLast={isLast || snapshot.isDragging}
isChild={isChild}
onPointerDown={handlePointerDown}
onPointerUp={stopTap}
onContextMenu={handleContextMenu}
Expand Down Expand Up @@ -325,6 +328,229 @@ const LayerListItem = observer<{
);
});

const LayerGroupListItem = observer<{
layerGroup: ILayerGroup;
index: number;
isActive?: boolean;
isLast?: boolean;
}>(({ layerGroup, index, isActive, isLast }) => {
const store = useStore();

// TODO: Automatically adjust group visibility (icon) if all children (in)visible
const toggleAnnotationVisibility = useCallback(() => {
layerGroup.layers.forEach((layer) => {
layer.setIsVisible(!layerGroup.isVisible);
});
layerGroup.setIsVisible(!layerGroup.isVisible);
}, [layerGroup]);

// Color Modal Toggling
// const [areLayerSettingsOpen, setAreLayerSettingsOpen] = useState(false);
// const isOpeningRef = useRef(false);
// const resetOpeningRef = useCallback(() => {
// isOpeningRef.current = false;
// }, []);
// const [schedule, cancel] = useDelay(resetOpeningRef, 25);
// const openLayerSettings = useCallback(() => {
// setAreLayerSettingsOpen(true);
// isOpeningRef.current = true;
// schedule();
// }, [schedule]);
// const closeLayerSettings = useCallback(() => {
// if (!isOpeningRef.current) setAreLayerSettingsOpen(false);
// isOpeningRef.current = false;
// cancel();
// }, [cancel]);

// Color Modal Positioning
const [colorRef, setColorRef] = useState<
HTMLDivElement | SVGSVGElement | null
>(null);

const trailingIconRef = useRef<SVGSVGElement | null>(null);

const [isGroupOpen, setIsGroupOpen] = useState(true);

const [startTap1, stopTap] = useShortTap(
useCallback(() => {
// setIsGroupOpen(!isGroupOpen);
}, []),
);
const startTap2 = useDoubleTap(
useCallback((event: React.PointerEvent) => {
if (event.pointerType === "mouse") return;
setContextMenuPosition({ x: event.clientX, y: event.clientY });
}, []),
);
const startTap = useForwardEvent(startTap1, startTap2);

// Context Menu
const [contextMenuPosition, setContextMenuPosition] = useState<Pixel | null>(
null,
);
const closeContextMenu = useCallback(() => {
setContextMenuPosition(null);
}, []);
useEffect(() => {
setContextMenuPosition(null);
}, [store?.editor.activeDocument?.viewSettings.viewMode]);

const { t } = useTranslation();

// const exportLayer = useCallback(() => {
// if (layer.kind !== "image") return;
// (layer as ImageLayer).quickExport().then(() => {
// setContextMenuPosition(null);
// });
// }, [layer]);

const deleteLayerGroup = useCallback(() => {
if (
// eslint-disable-next-line no-alert
window.confirm(
t("delete-layer-group-confirmation", { layerGroup: layerGroup.title }),
)
) {
layerGroup.delete();
}
setContextMenuPosition(null);
}, [layerGroup, t]);

// Press Handler
const handlePointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (
colorRef?.contains(event.target as Node) ||
trailingIconRef.current?.contains(event.target as Node)
) {
return;
}

if (event.button === PointerButton.LMB) {
startTap(event);
} else if (event.button === PointerButton.RMB) {
setContextMenuPosition({ x: event.clientX, y: event.clientY });
}
},
[colorRef, startTap],
);

const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault();
};

// Layer Renaming Handling
const [isLayerGroupNameEditable, setIsLayerGroupNameEditable] = useState(
false,
);
const startEditingLayerGroupName = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
setIsLayerGroupNameEditable(true);
closeContextMenu();
},
[closeContextMenu],
);
const stopEditingLayerGroupName = useCallback(() => {
setIsLayerGroupNameEditable(false);
}, []);

const layers = store?.editor.activeDocument?.layers;
const layerCount = layers?.length;
const activeLayer = store?.editor.activeDocument?.activeLayer;
const activeLayerIndex = layers?.findIndex((layer) => layer === activeLayer);

const modalRootRef = useModalRoot();
return (
<>
<Draggable
draggableId={layerGroup.id}
index={index}
// isDragDisabled={areLayerSettingsOpen}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const node = (
<Observer>
{() => (
<ListItem
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
icon={{
color: layerGroup.color || "text",
icon: isGroupOpen ? "arrowUp" : "arrowDown",
}}
iconRef={setColorRef}
onIconPress={() => {
setIsGroupOpen(!isGroupOpen);
}}
labelTx={
layerGroup.title ? undefined : "untitled-layer-group"
}
label={layerGroup.title}
isLabelEditable={isLayerGroupNameEditable}
onChangeLabelText={layerGroup.setTitle}
onConfirmLabelText={stopEditingLayerGroupName}
trailingIcon={layerGroup.isVisible ? "eye" : "eyeCrossed"}
disableTrailingIcon={!layerGroup.isVisible}
trailingIconRef={trailingIconRef}
onTrailingIconPress={toggleAnnotationVisibility}
isActive={isActive}
isLast={isLast || snapshot.isDragging}
onPointerDown={handlePointerDown}
onPointerUp={stopTap}
onContextMenu={handleContextMenu}
/>
)}
</Observer>
);

return snapshot.isDragging && modalRootRef.current
? ReactDOM.createPortal(node, modalRootRef.current)
: node;
}}
</Draggable>
{isGroupOpen &&
layerGroup.layers.map((layer, layerIndex) => (
<LayerListItem
key={layer.id}
layer={layer}
index={index + layerIndex + 1}
isActive={layer === activeLayer}
isLast={
index + layerIndex + 1 === layerCount! - 1 ||
index + layerIndex + 1 + 1 === activeLayerIndex
}
isChild
/>
))}
{/* <LayerSettings
layer={layer}
isOpen={areLayerSettingsOpen}
anchor={colorRef}
position="right"
onOutsidePress={closeLayerSettings}
/> */}
<ContextMenu
anchor={contextMenuPosition}
isOpen={Boolean(contextMenuPosition)}
onOutsidePress={closeContextMenu}
>
{/* <ContextMenuItem labelTx="export-layer" onPointerDown={exportLayer} /> */}
<ContextMenuItem
labelTx="rename-layer"
onPointerDown={startEditingLayerGroupName}
/>
<ContextMenuItem
labelTx="delete-layer"
onPointerDown={deleteLayerGroup}
isLast
/>
</ContextMenu>
</>
);
});

const LayerModal = styled(Modal)`
padding-bottom: 0px;
width: 230px;
Expand Down Expand Up @@ -409,18 +635,35 @@ export const Layers: React.FC = observer(() => {
>
{layerCount ? (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
layers!.map((layer, index) => (
<LayerListItem
key={layer.id}
layer={layer}
index={index}
isActive={layer === activeLayer}
isLast={
index === layerCount - 1 ||
index + 1 === activeLayerIndex
}
/>
))
layers!.map((layer, index) =>
layer.kind === "group" ? (
<LayerGroupListItem
key={layer.id}
layerGroup={layer as ILayerGroup}
index={index}
isActive={layer === activeLayer}
isLast={
index === layerCount - 1 ||
index + 1 === activeLayerIndex
}
/>
) : (
<>
{layer.parent ? undefined : (
<LayerListItem
key={layer.id}
layer={layer}
index={index}
isActive={layer === activeLayer}
isLast={
index === layerCount - 1 ||
index + 1 === activeLayerIndex
}
/>
)}
</>
),
)
) : (
<ListItem isLast>
<SubtleText tx="no-layers" />
Expand Down
23 changes: 22 additions & 1 deletion apps/editor/src/models/editor/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TrackingLog,
ErrorNotification,
ValueType,
ILayerGroup,
PerformanceMode,
} from "@visian/ui-shared";
import {
Expand Down Expand Up @@ -182,6 +183,7 @@ export class Document
setActiveLayer: action,
setMeasurementDisplayLayer: action,
setMeasurementType: action,
reorderLayersForGrouping: action,
addLayer: action,
addNewAnnotationLayer: action,
moveLayer: action,
Expand Down Expand Up @@ -313,10 +315,28 @@ export class Document
this.measurementType = measurementType;
};

public reorderLayersForGrouping = (idOrLayer: string | ILayer): void => {
const layer =
typeof idOrLayer === "string" ? this.getLayer(idOrLayer) : idOrLayer;
if (!layer) return;
if (layer.kind === "group") {
(layer as ILayerGroup).layers.forEach((childLayer, index) => {
this.moveLayer(childLayer, this.layerIds.indexOf(layer.id) + index + 1);
});
}
if (layer.parent) {
// TODO: Move to beginning or end of group based on image/annotation
this.moveLayer(layer, this.layerIds.indexOf(layer.parent.id + 1));
}
};

public addLayer = (...newLayers: Layer[]): void => {
newLayers.forEach((layer) => {
this.layerMap[layer.id] = layer;
if (layer.isAnnotation) {
if (layer.kind === "group") {
this.layerIds.unshift(layer.id);
this.reorderLayersForGrouping(layer);
} else if (layer.isAnnotation) {
this.layerIds.unshift(layer.id);
} else {
// insert image layer after all annotation layers
Expand All @@ -329,6 +349,7 @@ export class Document
}
this.layerIds.splice(insertIndex, 0, layer.id);
}
if (layer.parent) this.reorderLayersForGrouping(layer);
});
};

Expand Down
Loading