mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-27 20:20:07 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d088a1298 | ||
|
|
7b626d39aa | ||
|
|
ade3014131 |
@@ -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
Reference in New Issue
Block a user