mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
backport test
This commit is contained in:
1 parent
2422ad48ea
commit
e00ea02ca7
4 files changed
+492
-143
No files matched your search
@@ -27,7 +27,7 @@ export type EditPaintSurfaceProps = {
|
||||
nodes: Node[];
|
||||
selectedNodeIds: string[];
|
||||
onCanvasSizeChange?: (width: number, height: number) => void;
|
||||
fillColor?: string;
|
||||
//fillColor?: string;
|
||||
activeTool?: ActiveEditTool;
|
||||
brushColor?: string;
|
||||
brushSize?: number;
|
||||
@@ -42,7 +42,7 @@ export const EditPaintSurface = ({
|
||||
nodes,
|
||||
selectedNodeIds,
|
||||
onCanvasSizeChange,
|
||||
fillColor,
|
||||
//fillColor,
|
||||
activeTool = "select",
|
||||
brushColor = "#000000",
|
||||
brushSize = 5,
|
||||
@@ -50,35 +50,34 @@ export const EditPaintSurface = ({
|
||||
stageRef,
|
||||
transformerRefs,
|
||||
leftPanelRef,
|
||||
baseImageRef
|
||||
baseImageRef,
|
||||
}: EditPaintSurfaceProps) => {
|
||||
// switch off to be preview panel mode.
|
||||
const singlePaneMode = true
|
||||
const singlePaneMode = true;
|
||||
|
||||
const store = useEditStore(); // Use store directly
|
||||
const imageRef = React.useRef<HTMLImageElement>(null);
|
||||
const [snapshotImage, setSnapshotImage] = useState<HTMLImageElement | null>(
|
||||
null,
|
||||
);
|
||||
const imageRef = React.useRef<Konva.Image>(null);
|
||||
// Snapshot image state used in other components
|
||||
const [snapshotImage] = useState<HTMLImageElement | null>(null);
|
||||
// TODO: Polish this by using canvas graphics instead of a bitmap
|
||||
// Or at least manually do the pattern fill to avoid image interpolation
|
||||
const [checkerImage, setCheckerImage] = useState<HTMLImageElement | null>(null);
|
||||
const [checkerImage, setCheckerImage] = useState<HTMLImageElement | null>(
|
||||
null,
|
||||
);
|
||||
const rightContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const cursorLayerRef = React.useRef<Konva.Layer>(null);
|
||||
const cursorShapeRef = React.useRef<Konva.Circle>(null);
|
||||
|
||||
// Layout 683 by 1024
|
||||
const [leftPanelWidth, setLeftPanelWidth] = useState(1024);
|
||||
const [leftPanelHeight, setLeftPanelHeight] = useState(1024);
|
||||
const [rightPanelWidth, setRightPanelWidth] = useState(1024);
|
||||
const [rightPanelHeight, setRightPanelHeight] = useState(1024);
|
||||
// Layout dimensions
|
||||
const leftPanelWidth = 1024;
|
||||
const leftPanelHeight = 1024;
|
||||
const rightPanelWidth = 1024;
|
||||
const rightPanelHeight = 1024;
|
||||
|
||||
/* 1️⃣ Track SplitPane percent so we can re-measure */
|
||||
const [leftPct, setLeftPct] = useState(singlePaneMode ? 100 : 50);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [lastPoint, setLastPoint] = useState<{ x: number; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
const [, setLastPoint] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
const selectionRectRef = React.useRef<Konva.Rect>(null);
|
||||
const [selectionRect, setSelectionRect] = useState<{
|
||||
@@ -140,7 +139,10 @@ export const EditPaintSurface = ({
|
||||
}): { x: number; y: number } => {
|
||||
return {
|
||||
x: Math.max(0, Math.min(point.x, store.getAspectRatioDimensions().width)), //leftPanelWidth)),
|
||||
y: Math.max(0, Math.min(point.y, store.getAspectRatioDimensions().height))//leftPanelHeight)),
|
||||
y: Math.max(
|
||||
0,
|
||||
Math.min(point.y, store.getAspectRatioDimensions().height),
|
||||
), //leftPanelHeight)),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -158,9 +160,12 @@ export const EditPaintSurface = ({
|
||||
activeTool: string,
|
||||
nodeDraggable: boolean,
|
||||
): boolean => {
|
||||
return activeTool !== "edit" && nodeDraggable;
|
||||
return activeTool !== "edit" && nodeDraggable && !isMiddleMousePressed;
|
||||
};
|
||||
|
||||
// Add state for middle mouse button
|
||||
const [isMiddleMousePressed, setIsMiddleMousePressed] = useState(false);
|
||||
|
||||
// Combined mouse handlers
|
||||
const handleStageMouseDown = (
|
||||
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||
@@ -175,27 +180,46 @@ export const EditPaintSurface = ({
|
||||
y: (point.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
// Only set isDragging if the point is outside the panel bounds first mouse button
|
||||
if (!isWithinLeftPanel(stagePoint)) {
|
||||
// Check if the target is a transformer or its children (handles)
|
||||
const isTransformerTarget =
|
||||
e.target.getClassName() === "Transformer" ||
|
||||
e.target.getParent()?.getClassName() === "Transformer" ||
|
||||
e.target.name()?.includes("_anchor") ||
|
||||
e.target.name()?.includes("rotater");
|
||||
|
||||
if (!isWithinLeftPanel(stagePoint) && !isTransformerTarget) {
|
||||
setIsDragging(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dragging and panning the stage given the middle mouse.
|
||||
if (e.evt.button === 1 || e.evt.button === 2) {
|
||||
setIsDragging(true);
|
||||
return;
|
||||
// Improved handling for mouse buttons
|
||||
if ("button" in e.evt) {
|
||||
const mouseEvent = e.evt as MouseEvent;
|
||||
|
||||
if (mouseEvent.button === 1) {
|
||||
// Middle mouse button
|
||||
setIsMiddleMousePressed(true);
|
||||
setIsDragging(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouseEvent.button === 2) {
|
||||
// Right mouse button
|
||||
if (!isTransformerTarget) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drawing tools - only start if within bounds
|
||||
if (
|
||||
(activeTool === "edit") &&
|
||||
isWithinLeftPanel(stagePoint)
|
||||
) {
|
||||
if (activeTool === "edit" && isWithinLeftPanel(stagePoint)) {
|
||||
const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const opacity = activeTool === "edit" ? store.brushOpacity : 1;
|
||||
const composite = store.editOperation === "add" ? "source-over" : "destination-out";
|
||||
const strokeColor = store.editOperation === "add" ? brushColor : "#FFFFFF"; // Eraser uses transparent stroke
|
||||
const composite =
|
||||
store.editOperation === "add" ? "source-over" : "destination-out";
|
||||
const strokeColor =
|
||||
store.editOperation === "add" ? brushColor : "#FFFFFF"; // Eraser uses transparent stroke
|
||||
|
||||
const newLineNode: LineNode = {
|
||||
id: lineId,
|
||||
@@ -210,7 +234,7 @@ export const EditPaintSurface = ({
|
||||
globalCompositeOperation: composite,
|
||||
};
|
||||
store.selectNode(null);
|
||||
store.addLineNode(newLineNode, false); // Don't save state when starting line
|
||||
store.addLineNode(newLineNode, false); // Don't save state when starting line
|
||||
setCurrentLineId(lineId);
|
||||
setIsDrawing(true);
|
||||
setLastPoint(stagePoint);
|
||||
@@ -250,35 +274,59 @@ export const EditPaintSurface = ({
|
||||
const stage = stageRef.current;
|
||||
if (!stage) return;
|
||||
|
||||
// Update cursor position for draw/eraser tools
|
||||
if (activeTool === "edit") {
|
||||
const pointer = stage.getPointerPosition();
|
||||
if (pointer) {
|
||||
store.setCursorPosition(pointer);
|
||||
store.setCursorVisible(true);
|
||||
const pointer = stage.getPointerPosition();
|
||||
if (pointer) {
|
||||
const stagePoint = {
|
||||
x: (pointer.x - stage.x()) / stage.scaleX(),
|
||||
y: (pointer.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
const isWithinCanvas = isWithinLeftPanel(stagePoint);
|
||||
|
||||
// Update cursor position and visibility based on tool and canvas position
|
||||
if (activeTool === "edit") {
|
||||
if (isWithinCanvas || isDrawing) {
|
||||
stage.container().style.cursor = "none";
|
||||
store.setCursorPosition(pointer);
|
||||
store.setCursorVisible(true);
|
||||
} else {
|
||||
stage.container().style.cursor = "grab";
|
||||
store.setCursorVisible(false);
|
||||
}
|
||||
} else {
|
||||
if (isWithinCanvas) {
|
||||
stage.container().style.cursor = "default";
|
||||
} else {
|
||||
stage.container().style.cursor = "grab";
|
||||
}
|
||||
store.setCursorVisible(false);
|
||||
}
|
||||
} else {
|
||||
store.setCursorVisible(false);
|
||||
}
|
||||
|
||||
// Only handle panning if we're actually dragging
|
||||
// Handle panning with improved dragging
|
||||
if (isDragging) {
|
||||
const currentStage = e.target.getStage();
|
||||
if (!currentStage) return;
|
||||
const newPos = {
|
||||
x: currentStage.x() + e.evt.movementX,
|
||||
y: currentStage.y() + e.evt.movementY,
|
||||
};
|
||||
|
||||
currentStage.container().style.cursor = "grabbing";
|
||||
|
||||
const dragSensitivity = 3.0;
|
||||
let newPos = { x: currentStage.x(), y: currentStage.y() };
|
||||
|
||||
if ("movementX" in e.evt && "movementY" in e.evt) {
|
||||
const mouseEvent = e.evt as MouseEvent;
|
||||
newPos = {
|
||||
x: currentStage.x() + mouseEvent.movementX * dragSensitivity,
|
||||
y: currentStage.y() + mouseEvent.movementY * dragSensitivity,
|
||||
};
|
||||
}
|
||||
|
||||
currentStage.position(newPos);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle drawing - only add points if within bounds
|
||||
if (
|
||||
isDrawing &&
|
||||
currentLineId &&
|
||||
(activeTool === "edit")
|
||||
) {
|
||||
if (isDrawing && currentLineId && activeTool === "edit") {
|
||||
const point = stage.getPointerPosition();
|
||||
if (!point) return;
|
||||
|
||||
@@ -287,20 +335,19 @@ export const EditPaintSurface = ({
|
||||
y: (point.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
if (isWithinLeftPanel(stagePoint)) {
|
||||
const currentLine = store.lineNodes.find(
|
||||
(line) => line.id === currentLineId,
|
||||
);
|
||||
if (currentLine) {
|
||||
const updatedPoints = [
|
||||
...currentLine.points,
|
||||
stagePoint.x,
|
||||
stagePoint.y,
|
||||
];
|
||||
store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing
|
||||
}
|
||||
setLastPoint(stagePoint);
|
||||
// Continue drawing regardless of canvas boundaries
|
||||
const currentLine = store.lineNodes.find(
|
||||
(line) => line.id === currentLineId,
|
||||
);
|
||||
if (currentLine) {
|
||||
const updatedPoints = [
|
||||
...currentLine.points,
|
||||
stagePoint.x,
|
||||
stagePoint.y,
|
||||
];
|
||||
store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing
|
||||
}
|
||||
setLastPoint(stagePoint);
|
||||
}
|
||||
|
||||
// Handle selection rectangle - Update directly through Konva
|
||||
@@ -329,11 +376,11 @@ export const EditPaintSurface = ({
|
||||
setSelectionRect((prev) =>
|
||||
prev
|
||||
? {
|
||||
// Ensure prev is not null
|
||||
...prev,
|
||||
endX: clampedPoint.x,
|
||||
endY: clampedPoint.y,
|
||||
}
|
||||
// Ensure prev is not null
|
||||
...prev,
|
||||
endX: clampedPoint.x,
|
||||
endY: clampedPoint.y,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
@@ -341,7 +388,7 @@ export const EditPaintSurface = ({
|
||||
|
||||
const handleStageMouseUp = () => {
|
||||
if (isDrawing) {
|
||||
store.saveState(); // Save state only when the stroke is complete
|
||||
store.saveState(); // Save state only when the stroke is complete
|
||||
}
|
||||
|
||||
if (isSelecting && selectionRect) {
|
||||
@@ -402,6 +449,7 @@ export const EditPaintSurface = ({
|
||||
onSelectionChange?.(false);
|
||||
setSelectionRect(null);
|
||||
setLastPoint(null);
|
||||
setIsMiddleMousePressed(false); // Reset middle mouse state
|
||||
};
|
||||
|
||||
const handleStageWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||
@@ -417,17 +465,38 @@ export const EditPaintSurface = ({
|
||||
y: (pointer.y - stage.y()) / oldScale,
|
||||
};
|
||||
|
||||
// Calculate new scale
|
||||
const newScale = e.evt.deltaY < 0 ? oldScale * 1.1 : oldScale / 1.1;
|
||||
// Improved zoom handling with better precision
|
||||
const isPinchGesture = e.evt.ctrlKey;
|
||||
const deltaY = e.evt.deltaY;
|
||||
const absDelta = Math.abs(deltaY);
|
||||
|
||||
// Calculate new position
|
||||
const newPos = {
|
||||
x: pointer.x - mousePointTo.x * newScale,
|
||||
y: pointer.y - mousePointTo.y * newScale,
|
||||
};
|
||||
const isMac = navigator.userAgent.includes("Mac");
|
||||
const isMacTrackpadScroll =
|
||||
isMac && !isPinchGesture && e.evt.deltaMode === 0 && absDelta <= 10;
|
||||
|
||||
stage.scale({ x: newScale, y: newScale });
|
||||
stage.position(newPos);
|
||||
let zoomFactor;
|
||||
|
||||
if (isPinchGesture) {
|
||||
const pinchSensitivity = 0.2;
|
||||
zoomFactor = 1 + deltaY * pinchSensitivity * -0.01;
|
||||
} else if (isMacTrackpadScroll) {
|
||||
return;
|
||||
} else {
|
||||
const mouseSensitivity = 0.0005;
|
||||
zoomFactor = Math.exp(-deltaY * mouseSensitivity);
|
||||
}
|
||||
|
||||
const newScale = Math.max(0.1, Math.min(10, oldScale * zoomFactor));
|
||||
|
||||
if (newScale !== oldScale) {
|
||||
const newPos = {
|
||||
x: pointer.x - mousePointTo.x * newScale,
|
||||
y: pointer.y - mousePointTo.y * newScale,
|
||||
};
|
||||
|
||||
stage.scale({ x: newScale, y: newScale });
|
||||
stage.position(newPos);
|
||||
}
|
||||
};
|
||||
|
||||
// Add click handler for the stage to clear selection
|
||||
@@ -463,7 +532,7 @@ export const EditPaintSurface = ({
|
||||
};
|
||||
|
||||
// Add stage hover handlers
|
||||
const handleStageMouseEnter = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const handleStageMouseEnter = () => {
|
||||
const stage = stageRef.current;
|
||||
if (!stage) {
|
||||
console.error("Stage reference is not available");
|
||||
@@ -483,7 +552,7 @@ export const EditPaintSurface = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleStageMouseLeave = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const handleStageMouseLeave = () => {
|
||||
const stage = stageRef.current;
|
||||
if (stage) {
|
||||
stage.container().style.cursor = "default";
|
||||
@@ -493,17 +562,93 @@ export const EditPaintSurface = ({
|
||||
};
|
||||
|
||||
// Update cursor appearance
|
||||
// Adding touch-related state
|
||||
const [lastPinchDistance, setLastPinchDistance] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [isPinching, setIsPinching] = useState(false);
|
||||
|
||||
// Touch helper functions
|
||||
const getTouchDistance = (touches: TouchList) => {
|
||||
if (touches.length < 2) return 0;
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2),
|
||||
);
|
||||
};
|
||||
|
||||
const getTouchCenter = (touches: TouchList) => {
|
||||
if (touches.length < 2) return null;
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return {
|
||||
x: (touch1.clientX + touch2.clientX) / 2,
|
||||
y: (touch1.clientY + touch2.clientY) / 2,
|
||||
};
|
||||
};
|
||||
|
||||
// Touch event handlers
|
||||
const handleTouchStart = (e: Konva.KonvaEventObject<TouchEvent>) => {
|
||||
const touches = e.evt.touches;
|
||||
if (touches.length === 2) {
|
||||
setIsPinching(true);
|
||||
setLastPinchDistance(getTouchDistance(touches));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: Konva.KonvaEventObject<TouchEvent>) => {
|
||||
const touches = e.evt.touches;
|
||||
if (touches.length === 2 && isPinching && lastPinchDistance) {
|
||||
e.evt.preventDefault();
|
||||
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
const currentDistance = getTouchDistance(touches);
|
||||
const center = getTouchCenter(touches);
|
||||
if (!center) return;
|
||||
|
||||
const oldScale = stage.scaleX();
|
||||
const scaleChange = currentDistance / lastPinchDistance;
|
||||
|
||||
const newScale = Math.max(0.1, Math.min(10, oldScale * scaleChange));
|
||||
|
||||
if (newScale !== oldScale) {
|
||||
const stageCenter = {
|
||||
x: (center.x - stage.x()) / oldScale,
|
||||
y: (center.y - stage.y()) / oldScale,
|
||||
};
|
||||
|
||||
const newPos = {
|
||||
x: center.x - stageCenter.x * newScale,
|
||||
y: center.y - stageCenter.y * newScale,
|
||||
};
|
||||
|
||||
stage.scale({ x: newScale, y: newScale });
|
||||
stage.position(newPos);
|
||||
}
|
||||
|
||||
setLastPinchDistance(currentDistance);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e: Konva.KonvaEventObject<TouchEvent>) => {
|
||||
const touches = e.evt.touches;
|
||||
if (touches.length < 2) {
|
||||
setIsPinching(false);
|
||||
setLastPinchDistance(null);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const cursorNode = cursorShapeRef.current;
|
||||
const cursorLayer = cursorLayerRef.current;
|
||||
const stage = stageRef.current;
|
||||
if (!cursorNode || !cursorLayer || !stage) return;
|
||||
|
||||
if (
|
||||
store.cursorVisible &&
|
||||
store.cursorPosition &&
|
||||
(activeTool === "edit")
|
||||
) {
|
||||
if (store.cursorVisible && store.cursorPosition && activeTool === "edit") {
|
||||
// Counteract stage transforms to position cursor in screen space
|
||||
const stageX = stage.x();
|
||||
const stageY = stage.y();
|
||||
@@ -532,7 +677,15 @@ export const EditPaintSurface = ({
|
||||
cursorNode.visible(false);
|
||||
}
|
||||
cursorLayer.batchDraw();
|
||||
}, [store.cursorVisible, store.cursorPosition, activeTool, brushColor, brushSize, stageRef, store.editOperation]);
|
||||
}, [
|
||||
store.cursorVisible,
|
||||
store.cursorPosition,
|
||||
activeTool,
|
||||
brushColor,
|
||||
brushSize,
|
||||
stageRef,
|
||||
store.editOperation,
|
||||
]);
|
||||
|
||||
const renderNode = (node: Node | LineNode) => {
|
||||
// Node Callbacks
|
||||
@@ -540,15 +693,21 @@ export const EditPaintSurface = ({
|
||||
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||
nodeId: string,
|
||||
) => {
|
||||
// Don't select nodes when draw or erase tools are active
|
||||
// Don't select nodes when edit tool is active
|
||||
if (activeTool === "edit") {
|
||||
return;
|
||||
}
|
||||
|
||||
// allow selection
|
||||
if (e.evt.button === 2) {
|
||||
const node = store.nodes.find(n => n.id === nodeId);
|
||||
const lineNode = store.lineNodes.find(n => n.id === nodeId);
|
||||
// Prevent middle mouse button from interacting with nodes (canvas panning only)
|
||||
if ("button" in e.evt && (e.evt as MouseEvent).button === 1) {
|
||||
setIsMiddleMousePressed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle right click for context menu or selection
|
||||
if ("button" in e.evt && (e.evt as MouseEvent).button === 2) {
|
||||
const node = store.nodes.find((n) => n.id === nodeId);
|
||||
const lineNode = store.lineNodes.find((n) => n.id === nodeId);
|
||||
const isLocked = (node?.locked || lineNode?.locked) ?? false;
|
||||
|
||||
// If locked, only allow selection for context menu
|
||||
@@ -559,13 +718,18 @@ export const EditPaintSurface = ({
|
||||
}
|
||||
|
||||
// Don't select locked nodes
|
||||
const node = nodes.find(n => n.id === nodeId) || store.lineNodes.find(n => n.id === nodeId);
|
||||
const node =
|
||||
nodes.find((n) => n.id === nodeId) ||
|
||||
store.lineNodes.find((n) => n.id === nodeId);
|
||||
if (node?.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Ctrl/Cmd key is pressed
|
||||
const isMultiSelect = e.evt.ctrlKey || e.evt.metaKey;
|
||||
const isMultiSelect =
|
||||
"ctrlKey" in e.evt
|
||||
? (e.evt as MouseEvent).ctrlKey || (e.evt as MouseEvent).metaKey
|
||||
: false;
|
||||
|
||||
// If clicking directly on the stage, clear selection
|
||||
if (e.target === e.target.getStage()) {
|
||||
@@ -573,6 +737,13 @@ export const EditPaintSurface = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// If the node is already selected and we're not multi-selecting, preserve the current selection
|
||||
const isAlreadySelected = selectedNodeIds.includes(nodeId);
|
||||
if (isAlreadySelected && !isMultiSelect && selectedNodeIds.length > 1) {
|
||||
// Don't change selection - preserve multi-select for dragging
|
||||
return;
|
||||
}
|
||||
|
||||
store.selectNode(nodeId, isMultiSelect);
|
||||
};
|
||||
|
||||
@@ -667,6 +838,7 @@ export const EditPaintSurface = ({
|
||||
shiftBehavior="inverted"
|
||||
ignoreStroke={true}
|
||||
onTransformEnd={(e: Konva.KonvaEventObject<Event>) => {
|
||||
// Cast not needed as we're not using event properties that differ between MouseEvent and TouchEvent
|
||||
// Typed event
|
||||
const konvaNode = e.target;
|
||||
const nodeId = konvaNode.id();
|
||||
@@ -684,15 +856,19 @@ export const EditPaintSurface = ({
|
||||
|
||||
// Save all transformation values to the store
|
||||
if (isLineNode) {
|
||||
store.updateLineNode(nodeId, {
|
||||
x: finalX,
|
||||
y: finalY,
|
||||
rotation: finalRotation,
|
||||
scaleX: finalScaleX,
|
||||
scaleY: finalScaleY,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
}, true);
|
||||
store.updateLineNode(
|
||||
nodeId,
|
||||
{
|
||||
x: finalX,
|
||||
y: finalY,
|
||||
rotation: finalRotation,
|
||||
scaleX: finalScaleX,
|
||||
scaleY: finalScaleY,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
store.updateNode(
|
||||
nodeId,
|
||||
@@ -720,7 +896,8 @@ export const EditPaintSurface = ({
|
||||
|
||||
const handleLineDragStart = (
|
||||
e: Konva.KonvaEventObject<DragEvent>,
|
||||
nodeId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_nodeId: string, // NodeId is not used but kept for function signature compatibility
|
||||
) => {
|
||||
const targetNode = e.target as Konva.Node & {
|
||||
lastX?: number;
|
||||
@@ -731,7 +908,8 @@ export const EditPaintSurface = ({
|
||||
};
|
||||
const handleLineDragMove = (
|
||||
e: Konva.KonvaEventObject<DragEvent>,
|
||||
nodeId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_nodeId: string, // NodeId is not used but kept for function signature compatibility
|
||||
) => {
|
||||
const targetNode = e.target as Konva.Node & {
|
||||
lastX?: number;
|
||||
@@ -752,12 +930,16 @@ export const EditPaintSurface = ({
|
||||
const finalOffsetY = targetNode.offsetY();
|
||||
|
||||
// Update the line node in the store with its final position and transformation
|
||||
store.updateLineNode(nodeId, {
|
||||
x: finalX,
|
||||
y: finalY,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
});
|
||||
store.updateLineNode(
|
||||
nodeId,
|
||||
{
|
||||
x: finalX,
|
||||
y: finalY,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -766,7 +948,7 @@ export const EditPaintSurface = ({
|
||||
id={lineNode.id}
|
||||
points={lineNode.points}
|
||||
stroke={lineNode.stroke}
|
||||
opacity={lineNode.opacity ?? 1} // Use the line node's opacity or default to 1
|
||||
opacity={lineNode.opacity ?? 1} // Use the line node's opacity or default to 1
|
||||
strokeWidth={
|
||||
isSelected
|
||||
? (lineNode.strokeWidth || 0) + 2
|
||||
@@ -777,8 +959,8 @@ export const EditPaintSurface = ({
|
||||
lineJoin="round"
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, lineNode.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, lineNode.id)}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
onMouseEnter={(e) => handleNodeMouseEnter(e)}
|
||||
onMouseLeave={(e) => handleNodeMouseLeave(e)}
|
||||
draggable={draggableIfToolsNotActive(
|
||||
activeTool,
|
||||
lineNode.draggable && lineNode.locked == false,
|
||||
@@ -795,7 +977,9 @@ export const EditPaintSurface = ({
|
||||
offsetY={lineNode.offsetY || 0}
|
||||
zIndex={lineNode.zIndex}
|
||||
// @ts-expect-error: We don't have globalCompositeOperation in the type definition
|
||||
globalCompositeOperation={lineNode.globalCompositeOperation || "source-over"}
|
||||
globalCompositeOperation={
|
||||
lineNode.globalCompositeOperation || "source-over"
|
||||
}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -820,11 +1004,14 @@ export const EditPaintSurface = ({
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
|
||||
draggable={draggableIfToolsNotActive(
|
||||
activeTool,
|
||||
node.draggable && node.locked == false,
|
||||
)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
onMouseEnter={(e) => handleNodeMouseEnter(e)}
|
||||
onMouseLeave={(e) => handleNodeMouseLeave(e)}
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
@@ -852,11 +1039,14 @@ export const EditPaintSurface = ({
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
|
||||
draggable={draggableIfToolsNotActive(
|
||||
activeTool,
|
||||
node.draggable && node.locked == false,
|
||||
)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
onMouseEnter={(e) => handleNodeMouseEnter(e)}
|
||||
onMouseLeave={(e) => handleNodeMouseLeave(e)}
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
@@ -884,11 +1074,14 @@ export const EditPaintSurface = ({
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
|
||||
draggable={draggableIfToolsNotActive(
|
||||
activeTool,
|
||||
node.draggable && node.locked == false,
|
||||
)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
onMouseEnter={(e) => handleNodeMouseEnter(e)}
|
||||
onMouseLeave={(e) => handleNodeMouseLeave(e)}
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
@@ -932,11 +1125,14 @@ export const EditPaintSurface = ({
|
||||
scaleY={node.scaleY || 1}
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable && node.locked == false)}
|
||||
draggable={draggableIfToolsNotActive(
|
||||
activeTool,
|
||||
node.draggable && node.locked == false,
|
||||
)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
onMouseEnter={(e) => handleNodeMouseEnter(e)}
|
||||
onMouseLeave={(e) => handleNodeMouseLeave(e)}
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
@@ -972,7 +1168,7 @@ export const EditPaintSurface = ({
|
||||
if (multiSelectTransformerRef.current && selectedNodeIds.length > 1) {
|
||||
const nodes = selectedNodeIds
|
||||
.map((id) => stageRef.current.findOne(`#${id}`))
|
||||
.filter(Boolean);
|
||||
.filter(Boolean) as Konva.Node[];
|
||||
if (nodes.length > 0) {
|
||||
multiSelectTransformerRef.current.nodes(nodes);
|
||||
multiSelectTransformerRef.current.getLayer()?.batchDraw();
|
||||
@@ -1007,14 +1203,23 @@ export const EditPaintSurface = ({
|
||||
onWheel={handleStageWheel}
|
||||
onMouseDown={handleStageMouseDown}
|
||||
onMouseMove={handleStageMouseMove}
|
||||
onMouseUp={handleStageMouseUp} // This global mouse up is likely better from useGlobalMouseUp
|
||||
onMouseUp={handleStageMouseUp}
|
||||
onClick={handleStageClick}
|
||||
onMouseEnter={handleStageMouseEnter}
|
||||
onMouseLeave={handleStageMouseLeave}
|
||||
// Touch events
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<Layer
|
||||
clipFunc={(ctx) => {
|
||||
ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight);
|
||||
ctx.rect(
|
||||
0,
|
||||
0,
|
||||
store.getAspectRatioDimensions().width,
|
||||
store.getAspectRatioDimensions().height,
|
||||
); // leftPanelWidth, leftPanelHeight);
|
||||
}}
|
||||
imageSmoothingEnabled={false} // Disable image smoothing for pixel art
|
||||
>
|
||||
@@ -1034,7 +1239,37 @@ export const EditPaintSurface = ({
|
||||
</Layer>
|
||||
<Layer
|
||||
clipFunc={(ctx) => {
|
||||
ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight);
|
||||
ctx.rect(
|
||||
0,
|
||||
0,
|
||||
store.getAspectRatioDimensions().width,
|
||||
store.getAspectRatioDimensions().height,
|
||||
); // leftPanelWidth, leftPanelHeight);
|
||||
}}
|
||||
imageSmoothingEnabled={false} // Disable image smoothing for pixel art
|
||||
>
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
fillPatternImage={checkerImage || undefined}
|
||||
fillPatternRepeat="repeat"
|
||||
fillPatternScaleX={window.devicePixelRatio}
|
||||
fillPatternScaleY={window.devicePixelRatio}
|
||||
width={store.getAspectRatioDimensions().width}
|
||||
height={store.getAspectRatioDimensions().height}
|
||||
listening={false}
|
||||
zIndex={-2}
|
||||
imageSmoothingEnabled={false} // Disable image smoothing for pixel art
|
||||
/>
|
||||
</Layer>
|
||||
<Layer
|
||||
clipFunc={(ctx) => {
|
||||
ctx.rect(
|
||||
0,
|
||||
0,
|
||||
store.getAspectRatioDimensions().width,
|
||||
store.getAspectRatioDimensions().height,
|
||||
); // leftPanelWidth, leftPanelHeight);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
@@ -1052,14 +1287,19 @@ export const EditPaintSurface = ({
|
||||
<Layer
|
||||
ref={leftPanelRef}
|
||||
clipFunc={(ctx) => {
|
||||
ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight);
|
||||
ctx.rect(
|
||||
0,
|
||||
0,
|
||||
store.getAspectRatioDimensions().width,
|
||||
store.getAspectRatioDimensions().height,
|
||||
); // leftPanelWidth, leftPanelHeight);
|
||||
}}
|
||||
>
|
||||
{/* Render all nodes including line nodes */}
|
||||
{[...nodes, ...store.lineNodes]
|
||||
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
|
||||
.map((node, index) => {
|
||||
// console.log(`Node ${index}:`, node);
|
||||
.map((node) => {
|
||||
// console.log("Rendering node:", node);
|
||||
return renderNode(node);
|
||||
})}
|
||||
|
||||
@@ -1097,11 +1337,11 @@ export const EditPaintSurface = ({
|
||||
scaleY={previewScale}
|
||||
x={0}
|
||||
y={0}
|
||||
// style={{
|
||||
// background: '#f5f5f5',
|
||||
// border: '1px solid #ddd',
|
||||
// borderRadius: '8px',
|
||||
// }}
|
||||
// style={{
|
||||
// background: '#f5f5f5',
|
||||
// border: '1px solid #ddd',
|
||||
// borderRadius: '8px',
|
||||
// }}
|
||||
>
|
||||
<Layer>
|
||||
<Image
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Konva from "konva"; // just for types
|
||||
|
||||
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap";
|
||||
import {
|
||||
EnqueueImageInpaint,
|
||||
EnqueueImageInpaintModel,
|
||||
@@ -121,9 +123,23 @@ const PageEdit = () => {
|
||||
};
|
||||
}, [store]);
|
||||
|
||||
// Auto-fit the canvas when base image is loaded
|
||||
useEffect(() => {
|
||||
// When baseImageBitmap changes from null to an actual image, call onFitPressed
|
||||
if (store.baseImageBitmap) {
|
||||
// Use a slight delay to ensure the image dimensions are properly loaded
|
||||
const timer = setTimeout(() => {
|
||||
onFitPressed();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [store.baseImageBitmap]);
|
||||
|
||||
const onFitPressed = async () => {
|
||||
// Get the stage and its container dimensions
|
||||
const stage = stageRef.current;
|
||||
if (!stage) return;
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = stage.container().offsetWidth;
|
||||
@@ -133,15 +149,30 @@ const PageEdit = () => {
|
||||
const canvasW = store.getAspectRatioDimensions().width;
|
||||
const canvasH = store.getAspectRatioDimensions().height;
|
||||
|
||||
// Calculate optimal scale to fit the canvas in the container
|
||||
// Use a padding factor to leave some margin around the canvas
|
||||
const paddingFactor = 0.95; // 5% padding on all sides
|
||||
const scaleX = (containerWidth * paddingFactor) / canvasW;
|
||||
const scaleY = (containerHeight * paddingFactor) / canvasH;
|
||||
|
||||
// Use the smaller scale to ensure the canvas fits in both dimensions
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
|
||||
// Limit the scale between reasonable bounds (0.1 to 3.0)
|
||||
const boundedScale = Math.max(0.1, Math.min(3.0, scale));
|
||||
|
||||
// Apply the calculated scale
|
||||
stage.scale({ x: boundedScale, y: boundedScale });
|
||||
|
||||
// Calculate position to center canvas in container
|
||||
stage.position({
|
||||
x: (containerWidth - canvasW) / 2,
|
||||
y: (containerHeight - canvasH) / 2,
|
||||
x: (containerWidth - canvasW * boundedScale) / 2,
|
||||
y: (containerHeight - canvasH * boundedScale) / 2,,
|
||||
});
|
||||
};
|
||||
|
||||
// Create a function to use the left layer ref and download the bitmap from it
|
||||
const getMaskArrayBuffer = async (): Promise<Uint8Array> => {
|
||||
const getMaskArrayBuffer = async (): Promise<Uint8Array> => {
|
||||
if (
|
||||
!stageRef.current ||
|
||||
!leftPanelRef.current ||
|
||||
@@ -170,8 +201,47 @@ const PageEdit = () => {
|
||||
rect.width(),
|
||||
rect.height(),
|
||||
);
|
||||
// // Using the pixelRatio scaling may result in off-by-one rounding errors,
|
||||
// // So we re-fit the image to a canvas of precise size.
|
||||
// const fittedCanvas = normalizeCanvas(layerCrop, rect.width(), rect.height());
|
||||
// const downloadCallback = (blob: Blob | null) => {
|
||||
// if (!blob) {
|
||||
// console.error("Failed to create blob from canvas");
|
||||
// return;
|
||||
// }
|
||||
// const url = URL.createObjectURL(blob);
|
||||
// const link = document.createElement("a");
|
||||
// link.href = url;
|
||||
// link.download = "artcraft_snapshot.png";
|
||||
// link.click();
|
||||
// console.log('downloadCallback', url);
|
||||
// }
|
||||
// fittedCanvas.toBlob(downloadCallback, "image/png", 1.0);
|
||||
|
||||
const blob = await fittedCanvas.convertToBlob({ type: "image/png" });
|
||||
const normalizeCanvas2 = (
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): OffscreenCanvas => {
|
||||
const newCanvas = new OffscreenCanvas(width, height);
|
||||
|
||||
const ctx = newCanvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error("Failed to get canvas context");
|
||||
}
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(canvas, 0, 0, width, height);
|
||||
return newCanvas;
|
||||
};
|
||||
|
||||
const fittedCanvas2 = normalizeCanvas2(
|
||||
layerCrop,
|
||||
rect.width(),
|
||||
rect.height(),
|
||||
);
|
||||
|
||||
const blob = await fittedCanvas2.convertToBlob({ type: "image/png" });
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
return new Uint8Array(arrayBuffer);
|
||||
@@ -250,10 +320,13 @@ const PageEdit = () => {
|
||||
<PromptEditor
|
||||
modelInfo={selectedModelInfo}
|
||||
onModeChange={(mode: string) => {
|
||||
|
||||
store.setActiveTool(mode as ActiveEditTool);
|
||||
;
|
||||
}}
|
||||
selectedMode={store.activeTool}
|
||||
onGenerateClick={handleGenerate}
|
||||
onFitPressed={onFitPressed}
|
||||
isDisabled={isEnqueuing}
|
||||
generationCount={generationCount}
|
||||
onGenerationCountChange={setGenerationCount}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import React from "react";
|
||||
import { PromptBoxEdit, PromptBoxEditProps } from "@storyteller/ui-promptbox";
|
||||
import { JobProvider } from "~/pages/PageDraw/JobContext";
|
||||
|
||||
const PromptEditor: React.FC<PromptBoxEditProps> = ({
|
||||
// Extending the props to include onFitPressed
|
||||
interface ExtendedPromptEditorProps extends PromptBoxEditProps {
|
||||
onFitPressed?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const PromptEditor: React.FC<ExtendedPromptEditorProps> = ({
|
||||
onModeChange,
|
||||
selectedMode,
|
||||
onGenerateClick,
|
||||
isDisabled,
|
||||
onFitPressed,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
@@ -17,6 +24,15 @@ const PromptEditor: React.FC<PromptBoxEditProps> = ({
|
||||
isDisabled={isDisabled}
|
||||
{...rest}
|
||||
/>
|
||||
<JobProvider>
|
||||
<PromptBoxEdit
|
||||
onModeChange={onModeChange}
|
||||
selectedMode={selectedMode}
|
||||
onGenerateClick={onGenerateClick}
|
||||
isDisabled={false}
|
||||
onFitPressed={onFitPressed}
|
||||
/>
|
||||
</JobProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
faMousePointer,
|
||||
faSparkles,
|
||||
faSpinnerThird,
|
||||
faFrame,,
|
||||
faCopy,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -21,6 +22,7 @@ export interface PromptBoxEditProps {
|
||||
selectedMode?: string;
|
||||
onGenerateClick: (prompt: string) => void;
|
||||
isDisabled?: boolean;
|
||||
onFitPressed?: () => void | Promise<void>;
|
||||
modelInfo?: ModelInfo;
|
||||
generationCount?: number;
|
||||
onGenerationCountChange?: (count: number) => void;
|
||||
@@ -31,6 +33,7 @@ export const PromptBoxEdit = ({
|
||||
selectedMode,
|
||||
onGenerateClick,
|
||||
isDisabled,
|
||||
onFitPressed,,
|
||||
modelInfo,
|
||||
generationCount: generationCountProp,
|
||||
onGenerationCountChange,
|
||||
@@ -189,6 +192,23 @@ export const PromptBoxEdit = ({
|
||||
onOptionChange={onModeSelectionChange}
|
||||
selectedOption={selectedMode}
|
||||
/>
|
||||
{onFitPressed && (
|
||||
<Tooltip
|
||||
content={"Fit canvas to screen"}
|
||||
position="top"
|
||||
className="z-50"
|
||||
delay={200}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-9"
|
||||
onClick={onFitPressed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFrame} className="h-4 w-4" />
|
||||
Fit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
useSystemPrompt
|
||||
|
||||
Reference in new issue
Block a user