Compare commits

...
5 changed files with 599 additions and 753 deletions
@@ -15,6 +15,7 @@ import { useCopyPasteHotkeys } from "./hooks/useCopyPasteHotkeys";
import Konva from "konva";
import { captureStageEditsBitmap } from "./hooks/useUpdateSnapshot";
import { ContextMenuContainer } from "./components/ui/ContextMenu";
import InpaintToolBar from "./components/ui/InpaintToolBar";
import { ImageModel } from "@storyteller/model-list";
import {
CANVAS_2D_PAGE_MODEL_LIST,
@@ -321,14 +322,18 @@ const PageDraw = () => {
() => (state: SceneState) => ({
baseImageInfo: state.baseImageInfo,
baseImageBitmap: state.baseImageBitmap,
nodes: state.nodes,
drawNodes: state.drawNodes,
inpaintLineNodes: state.inpaintLineNodes,
selectedNodeIds: state.selectedNodeIds,
lineNodes: state.lineNodes,
activeTool: state.activeTool,
currentShape: state.currentShape,
fillColor: state.fillColor,
brushColor: state.brushColor,
brushSize: state.brushSize,
inpaintOperation: state.inpaintOperation,
inpaintBrushSize: state.inpaintBrushSize,
setInpaintOperation: state.setInpaintOperation,
setInpaintBrushSize: state.setInpaintBrushSize,
historyImageBundles: state.historyImageBundles,
getAspectRatioDimensions: state.getAspectRatioDimensions,
finishRemoveBackground: state.finishRemoveBackground,
@@ -338,8 +343,6 @@ const PageDraw = () => {
updateNode: state.updateNode,
setBaseImageInfo: state.setBaseImageInfo,
RESET: state.RESET,
clearLineNodes: state.clearLineNodes,
setNodes: state.setNodes,
removeHistoryImage: state.removeHistoryImage,
addHistoryImageBundle: state.addHistoryImageBundle,
setAspectRatioType: state.setAspectRatioType,
@@ -368,14 +371,18 @@ const PageDraw = () => {
const {
baseImageInfo,
baseImageBitmap,
nodes,
drawNodes,
inpaintLineNodes,
selectedNodeIds,
lineNodes,
activeTool,
currentShape,
fillColor,
brushColor,
brushSize,
inpaintOperation,
inpaintBrushSize,
setInpaintOperation,
setInpaintBrushSize,
historyImageBundles,
getAspectRatioDimensions,
finishRemoveBackground,
@@ -385,8 +392,6 @@ const PageDraw = () => {
updateNode,
setBaseImageInfo,
RESET,
clearLineNodes,
setNodes,
removeHistoryImage,
addHistoryImageBundle,
setAspectRatioType,
@@ -616,9 +621,9 @@ const PageDraw = () => {
const displayNodes = useMemo(
() =>
editing3DNodeId
? nodes.filter((n) => n.id !== editing3DNodeId)
: nodes,
[nodes, editing3DNodeId],
? drawNodes.filter((n) => n.id !== editing3DNodeId)
: drawNodes,
[drawNodes, editing3DNodeId],
);
const handleImageUpload = useCallback(async (files: File[]): Promise<void> => {
@@ -1086,21 +1091,25 @@ const PageDraw = () => {
const isLocked = useMemo(
() =>
selectedNodeIds.some((id) => {
const node = nodes.find((n) => n.id === id);
const lineNode = lineNodes.find((n) => n.id === id);
return (node?.locked || lineNode?.locked) ?? false;
const node = drawNodes.find((n) => n.id === id);
return node?.locked ?? false;
}),
[selectedNodeIds, nodes, lineNodes],
[selectedNodeIds, drawNodes],
);
const selectedNodeWithModel = useMemo(() => {
if (selectedNodeIds.length !== 1) return null;
return nodes.find((n) => n.id === selectedNodeIds[0]) ?? null;
}, [selectedNodeIds, nodes]);
const n = drawNodes.find((n) => n.id === selectedNodeIds[0]);
return n?.type !== "line" ? n : null;
}, [selectedNodeIds, drawNodes]);
const editingNode = useMemo(
() => (editing3DNodeId ? (nodes.find((n) => n.id === editing3DNodeId) ?? null) : null),
[editing3DNodeId, nodes],
() => {
if (!editing3DNodeId) return null;
const n = drawNodes.find((n) => n.id === editing3DNodeId);
return n?.type !== "line" ? n : null;
},
[editing3DNodeId, drawNodes],
);
// Display image selector on launch, otherwise hide it
@@ -1196,6 +1205,14 @@ const PageDraw = () => {
activeToolId={activeTool}
currentShape={currentShape}
/>
{activeTool === "inpaint" && (
<InpaintToolBar
operation={inpaintOperation}
brushSize={inpaintBrushSize}
onOperationChange={setInpaintOperation}
onBrushSizeChange={setInpaintBrushSize}
/>
)}
<div className="relative z-0">
<ContextMenuContainer
onAction={(e, action) => {
@@ -1215,14 +1232,16 @@ const PageDraw = () => {
isLocked={isLocked}
>
<PaintSurface
nodes={displayNodes}
lineNodes={lineNodes}
drawNodes={displayNodes}
inpaintLineNodes={inpaintLineNodes}
selectedNodeIds={selectedNodeIds}
onCanvasSizeChange={handleCanvasSizeChange}
fillColor={fillColor}
activeTool={activeTool}
brushColor={brushColor}
brushSize={brushSize}
inpaintOperation={inpaintOperation}
inpaintBrushSize={inpaintBrushSize}
onSelectionChange={setIsSelecting}
stageRef={stageRef}
transformerRefs={transformerRefs}
@@ -31,8 +31,8 @@ import { checkerboard } from "@storyteller/common";
import { DragState } from "../PageEdit/EditPaintSurface";
export type MiraiProps = {
nodes: Node[];
lineNodes: LineNode[];
drawNodes: (Node | LineNode)[];
inpaintLineNodes: LineNode[];
selectedNodeIds: string[];
onCanvasSizeChange?: (width: number, height: number) => void;
fillColor?: string;
@@ -45,6 +45,8 @@ export type MiraiProps = {
| "inpaint";
brushColor?: string;
brushSize?: number;
inpaintOperation?: "add" | "minus";
inpaintBrushSize?: number;
onSelectionChange?: (isSelecting: boolean) => void;
stageRef: React.RefObject<Konva.Stage>;
transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>;
@@ -59,14 +61,16 @@ export const BG_LAYER_ID = "bg-layer";
export const DRAW_LAYER_ID = "draw-layer";
export const PaintSurface = ({
nodes,
lineNodes,
drawNodes,
inpaintLineNodes,
selectedNodeIds,
onCanvasSizeChange,
fillColor,
activeTool = "select",
brushColor = "#000000",
brushSize = 5,
inpaintOperation = "add",
inpaintBrushSize = 30,
onSelectionChange,
stageRef,
transformerRefs,
@@ -200,7 +204,7 @@ export const PaintSurface = ({
if (minX === Infinity) return null;
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedNodeIds, nodes, lineNodes, stageRef]);
}, [selectedNodeIds, drawNodes, inpaintLineNodes, stageRef]);
useLayoutEffect(() => {
const updateDimensions = () => {
@@ -348,31 +352,37 @@ export const PaintSurface = ({
) {
const lineId = `line-${activeTool}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const opacity = activeTool === "draw" ? brushOpacity : 1;
const composite =
activeTool === "eraser"
? "destination-out"
: activeTool === "inpaint" && inpaintOperation === "minus"
? "destination-out"
: "source-over";
const strokeColor =
activeTool === "eraser"
? "#FFFFFF"
: activeTool === "draw"
? brushColor
: InpaintingColor;
: inpaintOperation === "minus"
? "#FFFFFF"
: InpaintingColor;
const composite =
activeTool === "eraser" ? "destination-out" : "source-over";
console.log("Starting new line with composite:", composite);
const zindex =
activeTool === "eraser" ? 999 : activeTool === "inpaint" ? 998 : 1;
const strokeWidth =
activeTool === "inpaint"
? inpaintBrushSize / stage.scaleX()
: brushSize / stage.scaleX();
const newLineNode: LineNode = {
id: lineId,
type: "line",
points: [stagePoint.x, stagePoint.y],
stroke: strokeColor,
strokeWidth: brushSize / stage.scaleX(),
strokeWidth,
draggable: true,
opacity: opacity,
locked: false,
zIndex: zindex,
globalCompositeOperation: composite,
x: 0,
y: 0,
@@ -388,7 +398,6 @@ export const PaintSurface = ({
draggable: true,
opacity: opacity,
locked: false,
zIndex: zindex,
globalCompositeOperation: composite,
x: 0,
y: 0,
@@ -504,6 +513,14 @@ export const PaintSurface = ({
const cursorNode = cursorShapeRef.current;
const cursorLayer = cursorLayerRef.current;
if (cursorNode && cursorLayer) {
const stX = stage.x();
const stY = stage.y();
const scX = stage.scaleX();
const scY = stage.scaleY();
cursorLayer.x(-stX / scX);
cursorLayer.y(-stY / scY);
cursorLayer.scaleX(1 / scX);
cursorLayer.scaleY(1 / scY);
cursorNode.position(pointer);
cursorLayer.batchDraw();
}
@@ -656,8 +673,7 @@ export const PaintSurface = ({
isDrawingShape,
isSelecting,
selectionRect,
nodes,
lineNodes,
drawNodes,
],
);
@@ -680,29 +696,30 @@ export const PaintSurface = ({
const selectedIds: string[] = [];
nodes.forEach((node) => {
if (
node.x + node.width >= left &&
node.x <= right &&
node.y + node.height >= top &&
node.y <= bottom
) {
selectedIds.push(node.id);
}
});
lineNodes.forEach((node) => {
const isInSelection = node.points.some((point, index) => {
if (index % 2 === 0) {
const x = point;
const y = node.points[index + 1];
return x >= left && x <= right && y >= top && y <= bottom;
drawNodes.forEach((n) => {
if (n instanceof Node) {
if (
n.x + n.width >= left &&
n.x <= right &&
n.y + n.height >= top &&
n.y <= bottom
) {
selectedIds.push(n.id);
}
} else {
// LineNode
const ln = n as LineNode;
const isInSelection = ln.points.some((point, index) => {
if (index % 2 === 0) {
const x = point;
const y = ln.points[index + 1];
return x >= left && x <= right && y >= top && y <= bottom;
}
return false;
});
if (isInSelection) {
selectedIds.push(n.id);
}
return false;
});
if (isInSelection) {
selectedIds.push(node.id);
}
});
@@ -797,6 +814,17 @@ export const PaintSurface = ({
stage.scale({ x: newScale, y: newScale });
stage.position(newPos);
const cursorNode = cursorShapeRef.current;
const cursorLayer = cursorLayerRef.current;
if (cursorNode && cursorLayer && cursorPositionRef.current) {
cursorLayer.x(-newPos.x / newScale);
cursorLayer.y(-newPos.y / newScale);
cursorLayer.scaleX(1 / newScale);
cursorLayer.scaleY(1 / newScale);
cursorNode.position(cursorPositionRef.current);
cursorLayer.batchDraw();
}
}
};
@@ -838,6 +866,17 @@ export const PaintSurface = ({
stage.scale({ x: newScale, y: newScale });
stage.position(newPos);
const cursorNode = cursorShapeRef.current;
const cursorLayer = cursorLayerRef.current;
if (cursorNode && cursorLayer && cursorPositionRef.current) {
cursorLayer.x(-newPos.x / newScale);
cursorLayer.y(-newPos.y / newScale);
cursorLayer.scaleX(1 / newScale);
cursorLayer.scaleY(1 / newScale);
cursorNode.position(cursorPositionRef.current);
cursorLayer.batchDraw();
}
}
setLastPinchDistance(currentDistance);
@@ -901,6 +940,14 @@ export const PaintSurface = ({
const cursorNode = cursorShapeRef.current;
const cursorLayer = cursorLayerRef.current;
if (cursorNode && cursorLayer) {
const stX = stage.x();
const stY = stage.y();
const scX = stage.scaleX();
const scY = stage.scaleY();
cursorLayer.x(-stX / scX);
cursorLayer.y(-stY / scY);
cursorLayer.scaleX(1 / scX);
cursorLayer.scaleY(1 / scY);
cursorNode.position(pointer);
cursorLayer.batchDraw();
}
@@ -945,15 +992,20 @@ export const PaintSurface = ({
cursorNode.visible(true);
cursorNode.position(cursorPositionRef.current);
cursorNode.radius(brushSize / 2);
cursorNode.radius((activeTool === "inpaint" ? inpaintBrushSize : brushSize) / 2);
if (activeTool === "draw") {
cursorNode.fill(brushColor);
cursorNode.stroke("rgba(255, 255, 255, 0.7)");
cursorNode.strokeWidth(1);
} else if (activeTool === "inpaint") {
cursorNode.fill(InpaintingColor);
cursorNode.stroke("rgba(255, 255, 255, 0.7)");
if (inpaintOperation === "add") {
cursorNode.fill(InpaintingColor);
cursorNode.stroke("rgba(255, 255, 255, 0.7)");
} else {
cursorNode.fill("rgba(255, 255, 255, 0.3)");
cursorNode.stroke("rgba(0, 0, 0, 0.7)");
}
cursorNode.strokeWidth(1);
} else {
cursorNode.fill("rgba(255, 255, 255, 0.3)");
@@ -965,7 +1017,7 @@ export const PaintSurface = ({
}
cursorLayer.batchDraw();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cursorVisibleLocal, activeTool, brushColor, brushSize]);
}, [cursorVisibleLocal, activeTool, brushColor, brushSize, inpaintBrushSize, inpaintOperation]);
const renderNode = useCallback(
(node: Node | LineNode) => {
@@ -989,19 +1041,18 @@ export const PaintSurface = ({
}
if ("button" in e.evt && (e.evt as MouseEvent).button === 2) {
const node = nodes.find((n) => n.id === nodeId);
const lineNode = lineNodes.find((n) => n.id === nodeId);
const isLocked = (node?.locked || lineNode?.locked) ?? false;
if (isLocked) {
const found =
drawNodes.find((n) => n.id === nodeId) ||
inpaintLineNodes.find((n) => n.id === nodeId);
if (found?.locked) {
selectNode(nodeId);
return;
}
}
const draggedNode =
(nodes.find((n) => n.id === nodeId) as Node | LineNode | undefined) ||
lineNodes.find((ln) => ln.id === nodeId);
drawNodes.find((n) => n.id === nodeId) ||
inpaintLineNodes.find((n) => n.id === nodeId);
if (draggedNode?.locked) {
return;
}
@@ -1039,12 +1090,10 @@ export const PaintSurface = ({
let newX = targetNode.x();
let newY = targetNode.y();
const draggedNode =
(nodes.find((n) => n.id === nodeId) as Node | LineNode | undefined) ||
lineNodes.find((ln) => ln.id === nodeId);
const draggedNode = drawNodes.find((n) => n.id === nodeId);
if (draggedNode && draggedNode.type === "circle") {
newX = targetNode.x() - draggedNode.width / 2;
newY = targetNode.y() - draggedNode.height / 2;
newX = targetNode.x() - (draggedNode as Node).width / 2;
newY = targetNode.y() - (draggedNode as Node).height / 2;
}
moveNode(nodeId, newX, newY, 0, 0, false);
};
@@ -1066,12 +1115,10 @@ export const PaintSurface = ({
let newXMove = targetNode.x();
let newYMove = targetNode.y();
const movingNode =
(nodes.find((n) => n.id === nodeId) as Node | LineNode | undefined) ||
lineNodes.find((ln) => ln.id === nodeId);
const movingNode = drawNodes.find((n) => n.id === nodeId);
if (movingNode && movingNode.type === "circle") {
newXMove = targetNode.x() - movingNode.width / 2;
newYMove = targetNode.y() - movingNode.height / 2;
newXMove = targetNode.x() - (movingNode as Node).width / 2;
newYMove = targetNode.y() - (movingNode as Node).height / 2;
}
moveNode(nodeId, newXMove, newYMove, dx, dy, false);
@@ -1090,12 +1137,10 @@ export const PaintSurface = ({
let endX = targetNode.x();
let endY = targetNode.y();
const endNode =
(nodes.find((n) => n.id === nodeId) as Node | LineNode | undefined) ||
lineNodes.find((ln) => ln.id === nodeId);
const endNode = drawNodes.find((n) => n.id === nodeId);
if (endNode && endNode.type === "circle") {
endX = targetNode.x() - endNode.width / 2;
endY = targetNode.y() - endNode.height / 2;
endX = targetNode.x() - (endNode as Node).width / 2;
endY = targetNode.y() - (endNode as Node).height / 2;
}
moveNode(nodeId, endX, endY, 0, 0, true);
};
@@ -1137,21 +1182,11 @@ export const PaintSurface = ({
if (selectedNodeIds.length > 1 && selectedNodeIds.includes(nodeId)) {
selectedNodeIds.forEach((id) => {
if (id !== nodeId) {
const isLineNode = lineNodes.find((ln) => ln.id === id);
if (isLineNode) {
const dn = drawNodes.find((n) => n.id === id);
if (dn && dn.type === "line") {
moveLineNode(id, dx, dy);
} else {
const regularNode = nodes.find((n) => n.id === id);
if (regularNode) {
moveNode(
id,
regularNode.x + dx,
regularNode.y + dy,
dx,
dy,
false,
);
}
} else if (dn && dn instanceof Node) {
moveNode(id, dn.x + dx, dn.y + dy, dx, dy, false);
}
}
});
@@ -1407,7 +1442,7 @@ export const PaintSurface = ({
return null;
},
[activeTool, nodes, lineNodes, selectedNodeIds, transformerRefs],
[activeTool, drawNodes, inpaintLineNodes, selectedNodeIds, transformerRefs],
);
// Load the checkboard image for transparency background
@@ -1420,7 +1455,7 @@ export const PaintSurface = ({
// Don't render individual transformers when multiple nodes are selected
if (selectedNodeIds.length > 1) return null;
return [...nodes, ...lineNodes].map((node) => {
return [...drawNodes, ...inpaintLineNodes].map((node) => {
const isSelected = selectedNodeIds.includes(node.id);
if (!isSelected) return null;
@@ -1474,7 +1509,7 @@ export const PaintSurface = ({
const finalOffsetY = konvaNode.offsetY();
// Find the corresponding node in the store to check its type
const storeNode = nodes.find((n) => n.id === nodeId);
const storeNode = drawNodes.find((n) => n.id === nodeId);
// Calculate new dimensions based on the scale that the Transformer applied
const newWidth = konvaNode.width() * finalScaleX;
@@ -1502,7 +1537,7 @@ export const PaintSurface = ({
offsetY: finalOffsetY,
};
const isLineNode = lineNodes.find((ln) => ln.id === nodeId);
const isLineNode = storeNode?.type === "line";
if (isLineNode) {
updateLineNode(
@@ -1577,20 +1612,8 @@ export const PaintSurface = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedNodeIds]);
const inpaintingLineNodes: LineNode[] = [];
const drawLineNodes: LineNode[] = [];
lineNodes.forEach((line) => {
if (
line.id.startsWith("line-eraser") ||
line.id.startsWith("line-inpaint")
) {
inpaintingLineNodes.push(line);
}
if (line.id.startsWith("line-eraser") || line.id.startsWith("line-draw")) {
drawLineNodes.push(line);
}
});
// drawNodes is already ordered — render directly, no splitting needed.
// inpaintLineNodes is the separate inpaint layer.
return (
<SplitPane
@@ -1684,9 +1707,7 @@ export const PaintSurface = ({
}}
pixelRatio={1}
>
{nodes.map((node) => renderNode(node))}
{drawLineNodes.map((node) => renderNode(node))}
{drawNodes.map((node) => renderNode(node))}
{currentLineId &&
!currentLineId.startsWith("line-inpaint") &&
@@ -1754,7 +1775,7 @@ export const PaintSurface = ({
visible={showMaskLayer}
pixelRatio={1}
>
{inpaintingLineNodes.map((node) => renderNode(node))}
{inpaintLineNodes.map((node) => renderNode(node))}
{currentLineId &&
currentLineId.startsWith("line-inpaint") &&
currentLinePendingRef.current && (
@@ -1830,7 +1851,7 @@ export const PaintSurface = ({
const finalOffsetY = konvaNode.offsetY();
// Find the corresponding node in the store to check its type
const storeNode = nodes.find((n) => n.id === nodeId);
const storeNode = drawNodes.find((n) => n.id === nodeId);
// Calculate new dimensions based on the scale that the Transformer applied
const newWidth = konvaNode.width() * finalScaleX;
@@ -1858,8 +1879,8 @@ export const PaintSurface = ({
offsetY: finalOffsetY,
};
const isLineNode = lineNodes.find(
(ln) => ln.id === nodeId,
const isLineNode = drawNodes.find(
(ln) => ln.id === nodeId && ln.type === "line",
);
if (isLineNode) {
@@ -1914,11 +1935,11 @@ export const PaintSurface = ({
tgt.lastY = tgt.y();
selectedNodeIds.forEach((id) => {
const lineNode = lineNodes.find((ln) => ln.id === id);
const lineNode = drawNodes.find((ln) => ln.id === id && ln.type === "line");
if (lineNode) {
moveLineNode(id, dx, dy);
} else {
const currentNode = nodes.find((n) => n.id === id);
const currentNode = drawNodes.find((n) => n.id === id && n.type !== "line") as Node | undefined;
if (currentNode) {
// Move each shape individually without triggering extra multi-select propagation
moveNode(
@@ -0,0 +1,57 @@
import { SliderV2 } from "@storyteller/ui-sliderv2";
interface InpaintToolBarProps {
operation: "add" | "minus";
brushSize: number;
onOperationChange: (op: "add" | "minus") => void;
onBrushSizeChange: (size: number) => void;
}
const InpaintToolBar = ({
operation,
brushSize,
onOperationChange,
onBrushSizeChange,
}: InpaintToolBarProps) => {
return (
<div className="fixed left-1/2 top-20 z-20 flex -translate-x-1/2 flex-col gap-3">
<div className="glass flex w-[400px] items-center gap-2 rounded-xl p-2">
<div className="flex rounded-lg border border-white/10 bg-white/5">
<button
className={`rounded-l-lg px-4 py-2 text-sm font-medium transition-colors ${
operation === "add"
? "bg-white/20 text-white"
: "text-white/60 hover:bg-white/10 hover:text-white"
}`}
onClick={() => onOperationChange("add")}
>
Add
</button>
<button
className={`rounded-r-lg px-4 py-2 text-sm font-medium transition-colors ${
operation === "minus"
? "bg-white/20 text-white"
: "text-white/60 hover:bg-white/10 hover:text-white"
}`}
onClick={() => onOperationChange("minus")}
>
Erase
</button>
</div>
<SliderV2
min={1}
max={100}
value={brushSize}
onChange={onBrushSizeChange}
step={1}
innerLabel={"Size " + brushSize + "pt"}
showDecrement
showIncrement
/>
</div>
</div>
);
};
export default InpaintToolBar;
@@ -25,6 +25,7 @@ import { HsvaColorPicker, HsvaColor } from "react-colorful";
import { hsvaToHex } from "@uiw/color-convert";
import SliderWithIndicator from "./SliderWithIndicator";
import { ActiveTool, useSceneStore } from "../../stores/SceneState";
import { Node } from "../../Node";
import { Tooltip } from "@storyteller/ui-tooltip";
import {
showActionReminder,
@@ -88,13 +89,13 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
// Get selected nodes for shape color picker
const selectedNodes =
store.selectedNodeIds.length > 0
? store.nodes.filter((node) => store.selectedNodeIds.includes(node.id))
? store.drawNodes.filter((node) => store.selectedNodeIds.includes(node.id))
: [];
// Get only colorable nodes (shapes, not images)
// Get only colorable nodes (shapes, not images or lines)
const selectedColorableNodes = selectedNodes.filter(
(node) => node.type !== "image",
);
(node) => node.type !== "image" && node.type !== "line",
) as Node[];
// Get first selected colorable node for color preview (when multiple selected, show first one's color)
const firstSelectedColorableNode =
@@ -345,7 +346,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
primaryActionText: "Reset all",
onPrimaryAction: () => {
store.setNodes([]);
store.lineNodes.forEach((lineNode) => {
store.drawNodes.filter((n) => n.type === "line").forEach((lineNode) => {
store.removeLineNode(lineNode.id, false);
});
store.selectNode(null);
File diff suppressed because it is too large Load Diff