diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/JobContext.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/JobContext.tsx new file mode 100644 index 0000000000..1945b22e90 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/JobContext.tsx @@ -0,0 +1,37 @@ +import React, { createContext, useContext, useState } from "react"; +import { JobContextType } from "@storyteller/common"; +const JobContext = createContext(undefined); + +export const JobProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [jobTokens, setJobTokens] = useState([]); + + const addJobToken = (token: string) => { + setJobTokens((prevTokens) => [...prevTokens, token]); + }; + + const removeJobToken = (token: string) => { + setJobTokens((prevTokens) => prevTokens.filter((t) => t !== token)); + }; + + const clearJobTokens = () => { + setJobTokens([]); + }; + + return ( + + {children} + + ); +}; + +export const useJobContext = () => { + const context = useContext(JobContext); + if (context === undefined) { + throw new Error("useJobContext must be used within a JobProvider"); + } + return context; +}; diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx index b939f4bac9..ceaa7ca925 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx @@ -1,17 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { PaintSurface } from './PaintSurface'; +import React, { useState } from "react"; +import { PaintSurface } from "./PaintSurface"; // https://github.com/SaladTechnologies/comfyui-api -import './App.css' -import PromptEditor from './PromptEditor/PromptEditor'; -import SideToolbar from './components/ui/SideToolbar'; +import "./App.css"; +import PromptEditor from "./PromptEditor/PromptEditor"; +import SideToolbar from "./components/ui/SideToolbar"; // Import the Zustand store -import { useSceneStore } from './stores/SceneState'; -import { useUndoRedoHotkeys } from './hooks/useUndoRedoHotkeys'; -import { useDeleteHotkeys } from './hooks/useDeleteHotkeys'; -import { useCopyPasteHotkeys } from './hooks/useCopyPasteHotkeys'; // Import the hook - -const PageDraw = (): JSX.Element => { +import { useSceneStore } from "./stores/SceneState"; +import { useUndoRedoHotkeys } from "./hooks/useUndoRedoHotkeys"; +import { useDeleteHotkeys } from "./hooks/useDeleteHotkeys"; +import { useCopyPasteHotkeys } from "./hooks/useCopyPasteHotkeys"; // Import the hook +const PageDraw = () => { // State for canvas dimensions const canvasWidth = React.useRef(1024); const canvasHeight = React.useRef(1024); @@ -20,125 +19,127 @@ const PageDraw = (): JSX.Element => { // Use the Zustand store const store = useSceneStore(); - + // Pass store actions directly as callbacks useDeleteHotkeys({ onDelete: store.deleteSelectedItems }); useUndoRedoHotkeys({ undo: store.undo, redo: store.redo }); - useCopyPasteHotkeys({ onCopy: store.copySelectedItems, onPaste: store.pasteItems }); + useCopyPasteHotkeys({ + onCopy: store.copySelectedItems, + onPaste: store.pasteItems, + }); const handleImageUpload = (files: File[]): void => { // Place images at center of viewport with offset for multiple images const centerX = 512; // leftPanelWidth / 2 const centerY = 512; // leftPanelHeight / 2 - + files.forEach((file, index) => { store.createImageFromFile( - centerX + (index * 60), // Offset each image - centerY + (index * 60), - file + centerX + index * 60, // Offset each image + centerY + index * 60, + file, ); }); }; return ( <> -
- { - console.log('Prompt changed:', prompt); - // Handle prompt changes here +
+ { + console.log("Prompt changed:", prompt); + // Handle prompt changes here + }} + onRandomize={() => { + console.log("Randomize clicked"); + // Handle randomize action here + }} + onVary={() => { + console.log("Vary clicked"); + // Handle vary action here + }} + onAIStrengthChange={(strength: number) => { + console.log("AI Strength:", strength); + // Handle AI strength changes here + }} + onAspectRatioChange={(ratio: string) => { + console.log("Aspect ratio:", ratio); + // Handle aspect ratio changes here + }} + /> +
+ { + store.setActiveTool("select"); }} - onRandomize={() => { - console.log('Randomize clicked'); - // Handle randomize action here - }} - onVary={() => { - console.log('Vary clicked'); - // Handle vary action here - }} - onAIStrengthChange={(strength: number) => { - console.log('AI Strength:', strength); - // Handle AI strength changes here - }} - onAspectRatioChange={(ratio: string) => { - console.log('Aspect ratio:', ratio); - // Handle aspect ratio changes here - }} - onImageStyleChange={(images: string[]) => { - console.log('Image styles:', images); - // Handle image style changes here - }} - /> -
- { - store.setActiveTool("select") - }} - onAddShape={(shape: 'rectangle' | 'circle' | 'triangle'): void => { - // Calculate center position based on canvas dimensions - const centerX = canvasWidth.current / 3; - const centerY = canvasHeight.current / 3; - if (shape === 'rectangle') { - store.createRectangle(centerX, centerY); - } else if (shape === 'circle') { - store.createCircle(centerX, centerY); - } else if (shape === 'triangle') { - store.createTriangle(centerX, centerY); - } - - }} - onPaintBrush={(hex: string, size: number): void => { - store.setActiveTool("draw") - store.setBrushColor(hex); - store.setBrushSize(size); - }} - onEraser={(size: number): void => { - store.setActiveTool("eraser") - store.setBrushSize(size); - }} - - onCanvasBackground={(hex: string): void => { - console.log("Canvas background activated", { color: hex }); - // Add background change logic here - // TODO: minor bug needs to update the preview panel - // Debounce also causes issues with real time color change. - store.setFillColor(hex); - }} - onGenerateImage={(): void => { - console.log("Generate image activated"); - // Add image generation logic here - - }} - onUploadImage={(): void => { - console.log("Clicking"); - // Create input element dynamically like in PromptEditor - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.multiple = true; // Allow multiple file selection - input.onchange = (e: Event) => { - const target = e.target as HTMLInputElement; - if (target.files) { - const files = Array.from(target.files); - const imageFiles = files.filter(file => file.type.startsWith('image/')); - - if (imageFiles.length > 0) { - handleImageUpload(imageFiles); - } + onAddShape={(shape: "rectangle" | "circle" | "triangle"): void => { + // Calculate center position based on canvas dimensions + const centerX = canvasWidth.current / 3; + const centerY = canvasHeight.current / 3; + if (shape === "rectangle") { + store.createRectangle(centerX, centerY); + } else if (shape === "circle") { + store.createCircle(centerX, centerY); + } else if (shape === "triangle") { + store.createTriangle(centerX, centerY); } - }; - input.click(); - }} - onDelete={(): void => { // This onDelete prop for SideToolbar might still be needed for the button - store.deleteSelectedItems(); - }} - activeToolId={store.activeTool} - /> -
- { + store.setActiveTool("draw"); + store.setBrushColor(hex); + store.setBrushSize(size); + }} + onEraser={(size: number): void => { + store.setActiveTool("eraser"); + store.setBrushSize(size); + }} + onCanvasBackground={(hex: string): void => { + console.log("Canvas background activated", { color: hex }); + // Add background change logic here + // TODO: minor bug needs to update the preview panel + // Debounce also causes issues with real time color change. + store.setFillColor(hex); + }} + onGenerateImage={(): void => { + console.log("Generate image activated"); + // Add image generation logic here + }} + onUploadImage={(): void => { + console.log("Clicking"); + // Create input element dynamically like in PromptEditor + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.multiple = true; // Allow multiple file selection + input.onchange = (e: Event) => { + const target = e.target as HTMLInputElement; + if (target.files) { + const files = Array.from(target.files); + const imageFiles = files.filter((file) => + file.type.startsWith("image/"), + ); + + if (imageFiles.length > 0) { + handleImageUpload(imageFiles); + } + } + }; + input.click(); + }} + onDelete={(): void => { + // This onDelete prop for SideToolbar might still be needed for the button + store.deleteSelectedItems(); + }} + activeToolId={store.activeTool} + /> +
+ { diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx index aee4fdff4f..ff1667b577 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx @@ -1,25 +1,35 @@ -import React, { useState, useEffect,useLayoutEffect } from 'react'; -import { Stage, Layer, Rect, Circle, Text, Line,Image,RegularPolygon, Transformer } from 'react-konva'; -import Konva from 'konva'; // Import Konva namespace for types +import React, { useState, useEffect, useLayoutEffect } from "react"; +import { + Stage, + Layer, + Rect, + Circle, + Text, + Line, + Image, + RegularPolygon, + Transformer, +} from "react-konva"; +import Konva from "konva"; // Import Konva namespace for types -import { LineNode } from './stores/SceneState'; -import { Node, NodeType } from './Node'; -import { useStageSnapshot } from './hooks/useUpdateSnapshot'; +import { LineNode } from "./stores/SceneState"; +import { Node, NodeType } from "./Node"; +import { useStageSnapshot } from "./hooks/useUpdateSnapshot"; // https://github.com/SaladTechnologies/comfyui-api -import './App.css' -import SplitPane from './components/ui/SplitPane'; +import "./App.css"; +import SplitPane from "./components/ui/SplitPane"; -import { useSceneStore } from './stores/SceneState'; -import { useRightPanelLayoutManagement } from './hooks/useRightPanelLayoutManagement'; -import { useStageCentering } from './hooks/useCenteredStage'; -import { useGlobalMouseUp } from './hooks/useGlobalMouseUp'; +import { useSceneStore } from "./stores/SceneState"; +import { useRightPanelLayoutManagement } from "./hooks/useRightPanelLayoutManagement"; +import { useStageCentering } from "./hooks/useCenteredStage"; +import { useGlobalMouseUp } from "./hooks/useGlobalMouseUp"; type MiraiProps = { nodes: Node[]; selectedNodeIds: string[]; onCanvasSizeChange?: (width: number, height: number) => void; fillColor?: string; - activeTool?:'select' | 'draw' | 'eraser' | 'backgroundColor'| 'shape'; + activeTool?: "select" | "draw" | "eraser" | "backgroundColor" | "shape"; brushColor?: string; brushSize?: number; onSelectionChange?: (isSelecting: boolean) => void; @@ -30,101 +40,170 @@ export const PaintSurface = ({ selectedNodeIds, onCanvasSizeChange, fillColor, - activeTool = 'select', - brushColor = '#000000', + activeTool = "select", + brushColor = "#000000", brushSize = 5, - onSelectionChange + onSelectionChange, }: MiraiProps) => { - const store = useSceneStore(); // Use store directly - - const [snapshotImage, setSnapshotImage] = useState(null); - const leftPanelRef = React.useRef(null); - const stageRef = React.useRef(null); - const rightContainerRef = React.useRef(null); - const cursorLayerRef = React.useRef(null); - const cursorShapeRef = React.useRef(null); + const store = useSceneStore(); // Use store directly - // Layout 683 by 1024 - const [leftPanelWidth, setLeftPanelWidth] = useState(1024); - const [leftPanelHeight, setLeftPanelHeight] = useState(1024); - const [rightPanelWidth, setRightPanelWidth] = useState(1024); - const [rightPanelHeight, setRightPanelHeight] = useState(1024); + const [snapshotImage, setSnapshotImage] = useState( + null, + ); + const leftPanelRef = React.useRef(null); + const stageRef = React.useRef(null); + const rightContainerRef = React.useRef(null); + const cursorLayerRef = React.useRef(null); + const cursorShapeRef = React.useRef(null); - /* 1️⃣ Track SplitPane percent so we can re-measure */ - const [leftPct, setLeftPct] = useState(50); - const [isDrawing, setIsDrawing] = useState(false); - const [lastPoint, setLastPoint] = useState<{ x: number, y: number } | null>(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); - const selectionRectRef = React.useRef(null); - const [selectionRect, setSelectionRect] = useState<{ - startX: number; - startY: number; - endX: number; - endY: number; - } | null>(null); - const [isSelecting, setIsSelecting] = useState(false); - const [isDragging, setIsDragging] = useState(false); - // Add a ref to track if we're currently selecting - const isSelectingRef = React.useRef(false); + /* 1️⃣ Track SplitPane percent so we can re-measure */ + const [leftPct, setLeftPct] = useState(50); + const [isDrawing, setIsDrawing] = useState(false); + const [lastPoint, setLastPoint] = useState<{ x: number; y: number } | null>( + null, + ); - // Add state to track the current line being drawn - const [currentLineId, setCurrentLineId] = useState(null); - const stagePosition = useStageCentering(stageRef, leftPct, leftPanelWidth, leftPanelHeight); + const selectionRectRef = React.useRef(null); + const [selectionRect, setSelectionRect] = useState<{ + startX: number; + startY: number; + endX: number; + endY: number; + } | null>(null); + const [isSelecting, setIsSelecting] = useState(false); + const [isDragging, setIsDragging] = useState(false); + // Add a ref to track if we're currently selecting + const isSelectingRef = React.useRef(false); - const NATURAL_WIDTH = rightPanelWidth; - const NATURAL_HEIGHT = rightPanelHeight; + // Add state to track the current line being drawn + const [currentLineId, setCurrentLineId] = useState(null); + const stagePosition = useStageCentering( + stageRef, + leftPct, + leftPanelWidth, + leftPanelHeight, + ); - // transform variables - const transformerRefs = React.useRef<{ [key: string]: Konva.Transformer | null }>({}); - const multiSelectTransformerRef = React.useRef(null); - const imageRef = React.useRef(null); + const NATURAL_WIDTH = rightPanelWidth; + const NATURAL_HEIGHT = rightPanelHeight; - // better to double buffer and use a queue and submit to a queue - // useStageSnapshot( - // stageRef, - // imageRef, - // isSelectingRef, - // transformerRefs - // ); + // transform variables + const transformerRefs = React.useRef<{ + [key: string]: Konva.Transformer | null; + }>({}); + const multiSelectTransformerRef = React.useRef(null); + const imageRef = React.useRef(null); - const previewScale = useRightPanelLayoutManagement( - rightContainerRef, - NATURAL_WIDTH, - NATURAL_HEIGHT, - leftPct, - onCanvasSizeChange - ); - - useGlobalMouseUp( - setIsDragging, - setIsDrawing, - setCurrentLineId, - setIsSelecting, - isSelectingRef, - setSelectionRect, - setLastPoint, - onSelectionChange, + // better to double buffer and use a queue and submit to a queue + // useStageSnapshot( + // stageRef, + // imageRef, + // isSelectingRef, + // transformerRefs + // ); + + const previewScale = useRightPanelLayoutManagement( + rightContainerRef, + NATURAL_WIDTH, + NATURAL_HEIGHT, + leftPct, + onCanvasSizeChange, + ); + + useGlobalMouseUp( + setIsDragging, + setIsDrawing, + setCurrentLineId, + setIsSelecting, + isSelectingRef, + setSelectionRect, + setLastPoint, + onSelectionChange, + ); + + const clampToLeftPanel = (point: { + x: number; + y: number; + }): { x: number; y: number } => { + return { + x: Math.max(0, Math.min(point.x, leftPanelWidth)), + y: Math.max(0, Math.min(point.y, leftPanelHeight)), + }; + }; + + const isWithinLeftPanel = (point: { x: number; y: number }): boolean => { + return ( + point.x >= 0 && + point.x <= leftPanelWidth && + point.y >= 0 && + point.y <= leftPanelHeight ); + }; - const clampToLeftPanel = (point: { x: number; y: number }): { x: number; y: number } => { - return { - x: Math.max(0, Math.min(point.x, leftPanelWidth)), - y: Math.max(0, Math.min(point.y, leftPanelHeight)) + // Helper function to determine if nodes should be draggable + const draggableIfToolsNotActive = ( + activeTool: string, + nodeDraggable: boolean, + ): boolean => { + return activeTool !== "draw" && activeTool !== "eraser" && nodeDraggable; + }; + + // Combined mouse handlers + const handleStageMouseDown = ( + e: Konva.KonvaEventObject, + ) => { + const stage = stageRef.current; + if (!stage) return; + const point = stage.getPointerPosition(); + if (!point) return; + + const stagePoint = { + x: (point.x - stage.x()) / stage.scaleX(), + y: (point.y - stage.y()) / stage.scaleY(), + }; + + // Only set isDragging if the point is outside the panel bounds first mouse button + if (!isWithinLeftPanel(stagePoint)) { + setIsDragging(true); + return; + } + + // Dragging and panning the stage given the middle mouse. + if (e.evt.button === 1 || e.evt.button === 2) { + setIsDragging(true); + return; + } + + // Handle drawing tools - only start if within bounds + if ( + (activeTool === "draw" || activeTool === "eraser") && + isWithinLeftPanel(stagePoint) + ) { + const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const newLineNode: LineNode = { + id: lineId, + type: "line", + points: [stagePoint.x, stagePoint.y], + stroke: activeTool === "draw" ? brushColor : fillColor || "#ffffff", + strokeWidth: brushSize / stage.scaleX(), + draggable: true, }; - }; + store.selectNode(null); + store.addLineNode(newLineNode); + setCurrentLineId(lineId); + setIsDrawing(true); + setLastPoint(stagePoint); + return; + } - const isWithinLeftPanel = (point: { x: number; y: number }): boolean => { - return point.x >= 0 && point.x <= leftPanelWidth && - point.y >= 0 && point.y <= leftPanelHeight; - }; - - // Helper function to determine if nodes should be draggable - const draggableIfToolsNotActive = (activeTool: string, nodeDraggable: boolean): boolean => { - return activeTool !== "draw" && activeTool !== "eraser" && nodeDraggable; - }; - - // Combined mouse handlers - const handleStageMouseDown = (e: Konva.KonvaEventObject) => { + // Handle selection rectangle + if (activeTool === "select" && e.target === e.target.getStage()) { const stage = stageRef.current; if (!stage) return; const point = stage.getPointerPosition(); @@ -132,725 +211,755 @@ export const PaintSurface = ({ const stagePoint = { x: (point.x - stage.x()) / stage.scaleX(), - y: (point.y - stage.y()) / stage.scaleY() + y: (point.y - stage.y()) / stage.scaleY(), }; - // Only set isDragging if the point is outside the panel bounds first mouse button - if (!isWithinLeftPanel(stagePoint)) { - setIsDragging(true); - return; - } - - // Dragging and panning the stage given the middle mouse. - if (e.evt.button === 1 || e.evt.button === 2) { - setIsDragging(true); - return; - } + // Clamp the starting point to left panel bounds + const clampedPoint = clampToLeftPanel(stagePoint); - // Handle drawing tools - only start if within bounds - if ((activeTool === 'draw' || activeTool === 'eraser') && isWithinLeftPanel(stagePoint)) { - const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const newLineNode: LineNode = { - id: lineId, - type: 'line', - points: [stagePoint.x, stagePoint.y], - stroke: activeTool === 'draw' ? brushColor : fillColor || '#ffffff', - strokeWidth: brushSize / stage.scaleX(), - draggable: true, - }; - store.selectNode(null); - store.addLineNode(newLineNode); - setCurrentLineId(lineId); - setIsDrawing(true); - setLastPoint(stagePoint); - return; - } + setIsSelecting(true); + isSelectingRef.current = true; + onSelectionChange?.(true); + setSelectionRect({ + startX: clampedPoint.x, + startY: clampedPoint.y, + endX: clampedPoint.x, + endY: clampedPoint.y, + }); + } + }; - // Handle selection rectangle - if (activeTool === 'select' && e.target === e.target.getStage()) { - const stage = stageRef.current; - if (!stage) return; - const point = stage.getPointerPosition(); - if (!point) return; + const handleStageMouseMove = ( + e: Konva.KonvaEventObject, + ) => { + const stage = stageRef.current; + if (!stage) return; - const stagePoint = { - x: (point.x - stage.x()) / stage.scaleX(), - y: (point.y - stage.y()) / stage.scaleY() - }; - - // Clamp the starting point to left panel bounds - const clampedPoint = clampToLeftPanel(stagePoint); - - setIsSelecting(true); - isSelectingRef.current = true; - onSelectionChange?.(true); - setSelectionRect({ - startX: clampedPoint.x, - startY: clampedPoint.y, - endX: clampedPoint.x, - endY: clampedPoint.y - }); - } - }; - - const handleStageMouseMove = (e: Konva.KonvaEventObject) => { - const stage = stageRef.current; - if (!stage) return; - - // Update cursor position for draw/eraser tools - if (activeTool === 'draw' || activeTool === 'eraser') { - const pointer = stage.getPointerPosition(); - if (pointer) { - store.setCursorPosition(pointer); - store.setCursorVisible(true); - } - } else { - store.setCursorVisible(false); - } - - // Only handle panning if we're actually 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.position(newPos); - return; - } - - // Handle drawing - only add points if within bounds - if (isDrawing && currentLineId && (activeTool === 'draw' || activeTool === 'eraser')) { - const point = stage.getPointerPosition(); - if (!point) return; - - const stagePoint = { - x: (point.x - stage.x()) / stage.scaleX(), - y: (point.y - stage.y()) / stage.scaleY() - }; - - // Only add point if it's within the left panel bounds - 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 }); - } - setLastPoint(stagePoint); - } else { - // Stop drawing when going out of bounds - setIsDrawing(false); - setCurrentLineId(null); - setLastPoint(null); - } - } - - // Handle selection rectangle - Update directly through Konva - if (isSelecting && selectionRectRef.current) { - const point = stage.getPointerPosition(); - if (!point) return; - const stagePoint = { - x: (point.x - stage.x()) / stage.scaleX(), - y: (point.y - stage.y()) / stage.scaleY() - }; - - // Clamp the current point to left panel bounds - const clampedPoint = clampToLeftPanel(stagePoint); - - // Update the rectangle directly through Konva without React state - if (selectionRect) { - selectionRectRef.current.setAttrs({ - x: Math.min(selectionRect.startX, clampedPoint.x), - y: Math.min(selectionRect.startY, clampedPoint.y), - width: Math.abs(clampedPoint.x - selectionRect.startX), - height: Math.abs(clampedPoint.y - selectionRect.startY) - }); - } - - // Only update React state for the end points (needed for selection logic) - setSelectionRect(prev => prev ? ({ // Ensure prev is not null - ...prev, - endX: clampedPoint.x, - endY: clampedPoint.y - }) : null); - } - }; - - const handleStageMouseUp = () => { - if (isSelecting && selectionRect) { - // Calculate the bounds of the selection rectangle - const left = Math.min(selectionRect.startX, selectionRect.endX); - const right = Math.max(selectionRect.startX, selectionRect.endX); - const top = Math.min(selectionRect.startY, selectionRect.endY); - const bottom = Math.max(selectionRect.startY, selectionRect.endY); - - // Find all nodes that intersect with the selection rectangle - const selectedIds: string[] = []; - - // Check regular nodes - nodes.forEach(node => { - if ( - node.x + node.width >= left && - node.x <= right && - node.y + node.height >= top && - node.y <= bottom - ) { - selectedIds.push(node.id); - } - }); - - // Check line nodes - store.lineNodes.forEach(node => { - // Check if any point of the line is within the selection rectangle - const isInSelection = node.points.some((point, index) => { - if (index % 2 === 0) { // x coordinate - const x = point; - const y = node.points[index + 1]; - return x >= left && x <= right && y >= top && y <= bottom; - } - return false; - }); - - if (isInSelection) { - selectedIds.push(node.id); - } - }); - - // Select all found nodes - if (selectedIds.length > 0) { - store.selectNode(selectedIds[0], true); - selectedIds.slice(1).forEach(id => { - store.selectNode(id, true); - }); - } - } - - // Always reset all states - setIsDragging(false); - setIsDrawing(false); - setCurrentLineId(null); // Clear the current line ID - setIsSelecting(false); - isSelectingRef.current = false; - onSelectionChange?.(false); - setSelectionRect(null); - setLastPoint(null); - }; - - - const handleStageWheel = (e: Konva.KonvaEventObject) => { - e.evt.preventDefault(); - const stage = e.target.getStage(); - if(!stage) return; - const oldScale = stage.scaleX(); + // Update cursor position for draw/eraser tools + if (activeTool === "draw" || activeTool === "eraser") { const pointer = stage.getPointerPosition(); - if(!pointer) return; - - const mousePointTo = { - x: (pointer.x - stage.x()) / oldScale, - y: (pointer.y - stage.y()) / oldScale, - }; - - // Calculate new scale - const newScale = e.evt.deltaY < 0 ? oldScale * 1.1 : oldScale / 1.1; - - // Calculate new position - 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 - const handleStageClick = (e: Konva.KonvaEventObject) => { - // Only clear selection if clicking directly on the stage - if (e.target === e.target.getStage()) { - store.selectNode(null); - } - - }; - - // End Node Callbacks - - // Add hover handlers - const handleNodeMouseEnter = (e: Konva.KonvaEventObject) => { - if (activeTool === 'select') { - const container = e.target.getStage()?.container(); - if (container) container.style.cursor = 'move'; - } - }; - - const handleNodeMouseLeave = (e: Konva.KonvaEventObject) => { - const container = e.target.getStage()?.container(); - if (!container) return; - const defaultCursor = activeTool === 'draw' || activeTool === 'eraser' - ? 'none' - : activeTool === 'select' - ? 'grab' - : 'default'; - container.style.cursor = defaultCursor; - }; - - // Add stage hover handlers - const handleStageMouseEnter = (e: Konva.KonvaEventObject) => { - const stage = stageRef.current; - if (!stage) { - console.error('Stage reference is not available'); - return; - } - - if (activeTool === 'draw' || activeTool === 'eraser') { - stage.container().style.cursor = 'none'; - const pointer = stage.getPointerPosition(); - if (pointer) { - store.setCursorPosition(pointer); - store.setCursorVisible(true); - } - } else { - stage.container().style.cursor = 'default'; - store.setCursorVisible(false); - } - }; - - const handleStageMouseLeave = (e: Konva.KonvaEventObject) => { - const stage = stageRef.current; - if (stage) { - stage.container().style.cursor = 'default'; + if (pointer) { + store.setCursorPosition(pointer); + store.setCursorVisible(true); } + } else { store.setCursorVisible(false); - store.setCursorPosition(null); - }; + } - // Update cursor appearance - useLayoutEffect(() => { - const cursorNode = cursorShapeRef.current; - const cursorLayer = cursorLayerRef.current; - const stage = stageRef.current; - if (!cursorNode || !cursorLayer || !stage) return; + // Only handle panning if we're actually 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.position(newPos); + return; + } - if (store.cursorVisible && store.cursorPosition && (activeTool === 'draw' || activeTool === 'eraser')) { - // Counteract stage transforms to position cursor in screen space - const stageX = stage.x(); - const stageY = stage.y(); - const scaleX = stage.scaleX(); - const scaleY = stage.scaleY(); - - cursorLayer.x(-stageX / scaleX); - cursorLayer.y(-stageY / scaleY); - cursorLayer.scaleX(1 / scaleX); - cursorLayer.scaleY(1 / scaleY); - - cursorNode.visible(true); - cursorNode.position(store.cursorPosition); - cursorNode.radius(brushSize / 2); + // Handle drawing - only add points if within bounds + if ( + isDrawing && + currentLineId && + (activeTool === "draw" || activeTool === "eraser") + ) { + const point = stage.getPointerPosition(); + if (!point) return; - if (activeTool === 'draw') { - cursorNode.fill(brushColor); - cursorNode.stroke('rgba(255, 255, 255, 0.7)'); - cursorNode.strokeWidth(1); - } else { - cursorNode.fill('rgba(255, 255, 255, 0.3)'); - cursorNode.stroke('rgba(0, 0, 0, 0.7)'); - cursorNode.strokeWidth(1); - } - } else { - cursorNode.visible(false); - } - cursorLayer.batchDraw(); - }, [store.cursorVisible, store.cursorPosition, activeTool, brushColor, brushSize]); - - const renderNode = (node: Node | LineNode) => { - // Node Callbacks - const handleNodeMouseDown = (e: Konva.KonvaEventObject, nodeId: string) => { - // Don't select nodes when draw or erase tools are active - if (activeTool === 'draw' || activeTool === 'eraser') { - return; - } - - // Check if Ctrl/Cmd key is pressed - const isMultiSelect = e.evt.ctrlKey || e.evt.metaKey; - - // If clicking directly on the stage, clear selection - if (e.target === e.target.getStage()) { - store.selectNode(null); - return; - } - - store.selectNode(nodeId, isMultiSelect); - }; - - const handleNodeDragStart = (e: Konva.KonvaEventObject, nodeId:string) => { - const targetNode = e.target as Konva.Node & { lastX?: number; lastY?: number }; - targetNode.lastX = targetNode.x(); - targetNode.lastY = targetNode.y(); - - - store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, false); - } - - const handleNodeDragMove = (e: Konva.KonvaEventObject, nodeId: string) => { - const targetNode = e.target as Konva.Node & { lastX?: number; lastY?: number }; - - // Calculate the movement delta - const dx = targetNode.x() - (targetNode.lastX || targetNode.x()); - const dy = targetNode.y() - (targetNode.lastY || targetNode.y()); - - // Update the last position - targetNode.lastX = targetNode.x(); - targetNode.lastY = targetNode.y(); - - // Print movement values - console.log('Node Movement:', { - currentX: targetNode.x(), - currentY: targetNode.y(), - lastX: targetNode.lastX, - lastY: targetNode.lastY, - deltaX: dx, - deltaY: dy - }); - - store.moveNode(nodeId, targetNode.x(), targetNode.y(), dx, dy, false); - }; - - const handleNodeDragEnd = (e: Konva.KonvaEventObject, nodeId:string) => { - const targetNode = e.target as Konva.Node & { lastX?: number; lastY?: number }; - targetNode.lastX = targetNode.x(); - targetNode.lastY = targetNode.y(); - - store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, true); - } - - const isSelected = selectedNodeIds.includes(node.id); - - const renderTransformer = () => { - if (isSelected) { - return ( - { // Added type for ref - if (ref) { - transformerRefs.current[node.id] = ref; - } - }} - boundBoxFunc={(oldBox, newBox) => { - const minSize = 5; - const maxSize = Math.max(leftPanelWidth, leftPanelHeight); - - if ( - newBox.width < minSize || - newBox.height < minSize || - newBox.width > maxSize || - newBox.height > maxSize - ) { - return oldBox; - } - return newBox; - }} - enabledAnchors={[ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right' - ]} - rotateEnabled={true} - keepRatio={false} - centeredScaling={true} - padding={5} - ignoreStroke={true} - onTransformEnd={(e: Konva.KonvaEventObject) => { // Typed event - const konvaNode = e.target; - const nodeId = konvaNode.id(); - - // Get all transformation values including anchor points - const finalRotation = konvaNode.rotation(); - const finalScaleX = konvaNode.scaleX(); - const finalScaleY = konvaNode.scaleY(); - const finalX = konvaNode.x(); - const finalY = konvaNode.y(); - const finalOffsetX = konvaNode.offsetX(); - const finalOffsetY = konvaNode.offsetY(); - - const isLineNode = store.lineNodes.find(ln => ln.id === nodeId); - - // 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 - }); - } else { - store.updateNode(nodeId, { - x: finalX, - y: finalY, - rotation: finalRotation, - scaleX: finalScaleX, - scaleY: finalScaleY, - offsetX: finalOffsetX, - offsetY: finalOffsetY - }, true); // Assuming shouldSaveState is true for regular nodes - } - }} - /> - ); - } - return null; + const stagePoint = { + x: (point.x - stage.x()) / stage.scaleX(), + y: (point.y - stage.y()) / stage.scaleY(), }; - if (node.type === 'line') { - const lineNode = node as LineNode; - - const handleLineDragStart = (e: Konva.KonvaEventObject, nodeId:string) => { - const targetNode = e.target as Konva.Node & { lastX?: number; lastY?: number }; - targetNode.lastX = targetNode.x(); - targetNode.lastY = targetNode.y(); - + // Only add point if it's within the left panel bounds + 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 }); } - const handleLineDragMove = (e: Konva.KonvaEventObject, nodeId: string) => { - const targetNode = e.target as Konva.Node & { lastX?: number; lastY?: number }; - targetNode.lastX = targetNode.x(); // This doesn't seem to be used for lines to calculate dx/dy - targetNode.lastY = targetNode.y(); // but is set for consistency or future use - - } - const handleLineDragEnd = (e: Konva.KonvaEventObject, nodeId: string) => { - - // Move the line node in the store - const targetNode = e.target as Konva.Node; - const finalX = targetNode.x(); - const finalY = targetNode.y(); - const finalOffsetX = targetNode.offsetX(); // Konva might adjust offsetX/Y - const finalOffsetY = targetNode.offsetY(); + setLastPoint(stagePoint); + } else { + // Stop drawing when going out of bounds + setIsDrawing(false); + setCurrentLineId(null); + setLastPoint(null); + } + } - // Update the line node in the store with its final position and transformation - store.updateLineNode(nodeId, { - x: finalX, - y: finalY, - offsetX: finalOffsetX, - offsetY: finalOffsetY - }); - } + // Handle selection rectangle - Update directly through Konva + if (isSelecting && selectionRectRef.current) { + const point = stage.getPointerPosition(); + if (!point) return; + const stagePoint = { + x: (point.x - stage.x()) / stage.scaleX(), + y: (point.y - stage.y()) / stage.scaleY(), + }; - return ( - - handleNodeMouseDown(e, lineNode.id)} - onTap={(e) => handleNodeMouseDown(e, lineNode.id)} - onMouseEnter={handleNodeMouseEnter} - onMouseLeave={handleNodeMouseLeave} - draggable={draggableIfToolsNotActive(activeTool, lineNode.draggable)} - onDragMove={(e) => handleLineDragMove(e, lineNode.id)} - onDragStart={(e) => handleLineDragStart(e, lineNode.id)} - onDragEnd={(e) => handleLineDragEnd(e, lineNode.id)} - x={lineNode.x || 0} - y={lineNode.y || 0} - scaleX={lineNode.scaleX || 1} - scaleY={lineNode.scaleY || 1} - rotation={lineNode.rotation || 0} - offsetX={lineNode.offsetX || 0} - offsetY={lineNode.offsetY || 0} - /> - {renderTransformer()} - - ); + // Clamp the current point to left panel bounds + const clampedPoint = clampToLeftPanel(stagePoint); + + // Update the rectangle directly through Konva without React state + if (selectionRect) { + selectionRectRef.current.setAttrs({ + x: Math.min(selectionRect.startX, clampedPoint.x), + y: Math.min(selectionRect.startY, clampedPoint.y), + width: Math.abs(clampedPoint.x - selectionRect.startX), + height: Math.abs(clampedPoint.y - selectionRect.startY), + }); } - if (node.type === 'circle') { - - return ( - - handleNodeMouseDown(e, node.id)} - onTap={(e) => handleNodeMouseDown(e, node.id)} - onMouseEnter={handleNodeMouseEnter} - onMouseLeave={handleNodeMouseLeave} - onDragMove={(e) => handleNodeDragMove(e, node.id)} - onDragStart={(e) => handleNodeDragStart(e, node.id)} - onDragEnd={(e) => handleNodeDragEnd(e, node.id)} - /> - {renderTransformer()} - - ); - } + // Only update React state for the end points (needed for selection logic) + setSelectionRect((prev) => + prev + ? { + // Ensure prev is not null + ...prev, + endX: clampedPoint.x, + endY: clampedPoint.y, + } + : null, + ); + } + }; - if (node.type === 'rectangle') { - return ( - - handleNodeMouseDown(e, node.id)} - onTap={(e) => handleNodeMouseDown(e, node.id)} - onMouseEnter={handleNodeMouseEnter} - onMouseLeave={handleNodeMouseLeave} - onDragMove={(e) => handleNodeDragMove(e, node.id)} - onDragStart={(e) => handleNodeDragStart(e, node.id)} - onDragEnd={(e) => handleNodeDragEnd(e, node.id)} - /> - {renderTransformer()} - - ); - } + const handleStageMouseUp = () => { + if (isSelecting && selectionRect) { + // Calculate the bounds of the selection rectangle + const left = Math.min(selectionRect.startX, selectionRect.endX); + const right = Math.max(selectionRect.startX, selectionRect.endX); + const top = Math.min(selectionRect.startY, selectionRect.endY); + const bottom = Math.max(selectionRect.startY, selectionRect.endY); - if (node.type === 'triangle') { - return ( - - handleNodeMouseDown(e, node.id)} - onTap={(e) => handleNodeMouseDown(e, node.id)} - onMouseEnter={handleNodeMouseEnter} - onMouseLeave={handleNodeMouseLeave} - onDragMove={(e) => handleNodeDragMove(e, node.id)} - onDragStart={(e) => handleNodeDragStart(e, node.id)} - onDragEnd={(e) => handleNodeDragEnd(e, node.id)} - /> - {renderTransformer()} - - ); - } + // Find all nodes that intersect with the selection rectangle + const selectedIds: string[] = []; - if (node.type === 'image') { - return ( - - {/* Render background rectangle if backgroundColor is set */} - {node.backgroundColor && node.backgroundColor !== 'transparent' && ( - - )} - handleNodeMouseDown(e, node.id)} - onTap={(e) => handleNodeMouseDown(e, node.id)} - onMouseEnter={handleNodeMouseEnter} - onMouseLeave={handleNodeMouseLeave} - onDragMove={(e) => handleNodeDragMove(e, node.id)} - onDragStart={(e) => handleNodeDragStart(e, node.id)} - onDragEnd={(e) => handleNodeDragEnd(e, node.id)} - /> - {renderTransformer()} - - ); - } - - return null; - }; - - - // TODO refactor this out into zustand? and render the draw ? - useEffect(() => { - if (!stageRef.current) return; - // Update individual transformers - Object.entries(transformerRefs.current).forEach(([nodeId, transformer]) => { - if (!transformer) return; // Transformer might be null if ref was cleared - - const isSelected = selectedNodeIds.includes(nodeId); - transformer.visible(isSelected); - - if (isSelected) { - const node = stageRef.current!.findOne(`#${nodeId}`); // stageRef.current is checked - if (node) { - transformer.nodes([node]); - transformer.getLayer()?.batchDraw(); - } + // Check regular nodes + nodes.forEach((node) => { + if ( + node.x + node.width >= left && + node.x <= right && + node.y + node.height >= top && + node.y <= bottom + ) { + selectedIds.push(node.id); } }); - // Update multi-select transformer - if (multiSelectTransformerRef.current && selectedNodeIds.length > 1) { - const nodes = selectedNodeIds.map(id => stageRef.current.findOne(`#${id}`)).filter(Boolean); - if (nodes.length > 0) { - multiSelectTransformerRef.current.nodes(nodes); - multiSelectTransformerRef.current.getLayer()?.batchDraw(); + // Check line nodes + store.lineNodes.forEach((node) => { + // Check if any point of the line is within the selection rectangle + const isInSelection = node.points.some((point, index) => { + if (index % 2 === 0) { + // x coordinate + const x = point; + const y = node.points[index + 1]; + return x >= left && x <= right && y >= top && y <= bottom; + } + return false; + }); + + if (isInSelection) { + selectedIds.push(node.id); + } + }); + + // Select all found nodes + if (selectedIds.length > 0) { + store.selectNode(selectedIds[0], true); + selectedIds.slice(1).forEach((id) => { + store.selectNode(id, true); + }); + } + } + + // Always reset all states + setIsDragging(false); + setIsDrawing(false); + setCurrentLineId(null); // Clear the current line ID + setIsSelecting(false); + isSelectingRef.current = false; + onSelectionChange?.(false); + setSelectionRect(null); + setLastPoint(null); + }; + + const handleStageWheel = (e: Konva.KonvaEventObject) => { + e.evt.preventDefault(); + const stage = e.target.getStage(); + if (!stage) return; + const oldScale = stage.scaleX(); + const pointer = stage.getPointerPosition(); + if (!pointer) return; + + const mousePointTo = { + x: (pointer.x - stage.x()) / oldScale, + y: (pointer.y - stage.y()) / oldScale, + }; + + // Calculate new scale + const newScale = e.evt.deltaY < 0 ? oldScale * 1.1 : oldScale / 1.1; + + // Calculate new position + 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 + const handleStageClick = ( + e: Konva.KonvaEventObject, + ) => { + // Only clear selection if clicking directly on the stage + if (e.target === e.target.getStage()) { + store.selectNode(null); + } + }; + + // End Node Callbacks + + // Add hover handlers + const handleNodeMouseEnter = (e: Konva.KonvaEventObject) => { + if (activeTool === "select") { + const container = e.target.getStage()?.container(); + if (container) container.style.cursor = "move"; + } + }; + + const handleNodeMouseLeave = (e: Konva.KonvaEventObject) => { + const container = e.target.getStage()?.container(); + if (!container) return; + const defaultCursor = + activeTool === "draw" || activeTool === "eraser" + ? "none" + : activeTool === "select" + ? "grab" + : "default"; + container.style.cursor = defaultCursor; + }; + + // Add stage hover handlers + const handleStageMouseEnter = (e: Konva.KonvaEventObject) => { + const stage = stageRef.current; + if (!stage) { + console.error("Stage reference is not available"); + return; + } + + if (activeTool === "draw" || activeTool === "eraser") { + stage.container().style.cursor = "none"; + const pointer = stage.getPointerPosition(); + if (pointer) { + store.setCursorPosition(pointer); + store.setCursorVisible(true); + } + } else { + stage.container().style.cursor = "default"; + store.setCursorVisible(false); + } + }; + + const handleStageMouseLeave = (e: Konva.KonvaEventObject) => { + const stage = stageRef.current; + if (stage) { + stage.container().style.cursor = "default"; + } + store.setCursorVisible(false); + store.setCursorPosition(null); + }; + + // Update cursor appearance + useLayoutEffect(() => { + const cursorNode = cursorShapeRef.current; + const cursorLayer = cursorLayerRef.current; + const stage = stageRef.current; + if (!cursorNode || !cursorLayer || !stage) return; + + if ( + store.cursorVisible && + store.cursorPosition && + (activeTool === "draw" || activeTool === "eraser") + ) { + // Counteract stage transforms to position cursor in screen space + const stageX = stage.x(); + const stageY = stage.y(); + const scaleX = stage.scaleX(); + const scaleY = stage.scaleY(); + + cursorLayer.x(-stageX / scaleX); + cursorLayer.y(-stageY / scaleY); + cursorLayer.scaleX(1 / scaleX); + cursorLayer.scaleY(1 / scaleY); + + cursorNode.visible(true); + cursorNode.position(store.cursorPosition); + cursorNode.radius(brushSize / 2); + + if (activeTool === "draw") { + cursorNode.fill(brushColor); + cursorNode.stroke("rgba(255, 255, 255, 0.7)"); + cursorNode.strokeWidth(1); + } else { + cursorNode.fill("rgba(255, 255, 255, 0.3)"); + cursorNode.stroke("rgba(0, 0, 0, 0.7)"); + cursorNode.strokeWidth(1); + } + } else { + cursorNode.visible(false); + } + cursorLayer.batchDraw(); + }, [ + store.cursorVisible, + store.cursorPosition, + activeTool, + brushColor, + brushSize, + ]); + + const renderNode = (node: Node | LineNode) => { + // Node Callbacks + const handleNodeMouseDown = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + // Don't select nodes when draw or erase tools are active + if (activeTool === "draw" || activeTool === "eraser") { + return; + } + + // Check if Ctrl/Cmd key is pressed + const isMultiSelect = e.evt.ctrlKey || e.evt.metaKey; + + // If clicking directly on the stage, clear selection + if (e.target === e.target.getStage()) { + store.selectNode(null); + return; + } + + store.selectNode(nodeId, isMultiSelect); + }; + + const handleNodeDragStart = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + const targetNode = e.target as Konva.Node & { + lastX?: number; + lastY?: number; + }; + targetNode.lastX = targetNode.x(); + targetNode.lastY = targetNode.y(); + + store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, false); + }; + + const handleNodeDragMove = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + const targetNode = e.target as Konva.Node & { + lastX?: number; + lastY?: number; + }; + + // Calculate the movement delta + const dx = targetNode.x() - (targetNode.lastX || targetNode.x()); + const dy = targetNode.y() - (targetNode.lastY || targetNode.y()); + + // Update the last position + targetNode.lastX = targetNode.x(); + targetNode.lastY = targetNode.y(); + + // Print movement values + console.log("Node Movement:", { + currentX: targetNode.x(), + currentY: targetNode.y(), + lastX: targetNode.lastX, + lastY: targetNode.lastY, + deltaX: dx, + deltaY: dy, + }); + + store.moveNode(nodeId, targetNode.x(), targetNode.y(), dx, dy, false); + }; + + const handleNodeDragEnd = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + const targetNode = e.target as Konva.Node & { + lastX?: number; + lastY?: number; + }; + targetNode.lastX = targetNode.x(); + targetNode.lastY = targetNode.y(); + + store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, true); + }; + + const isSelected = selectedNodeIds.includes(node.id); + + const renderTransformer = () => { + if (isSelected) { + return ( + { + // Added type for ref + if (ref) { + transformerRefs.current[node.id] = ref; + } + }} + boundBoxFunc={(oldBox, newBox) => { + const minSize = 5; + const maxSize = Math.max(leftPanelWidth, leftPanelHeight); + + if ( + newBox.width < minSize || + newBox.height < minSize || + newBox.width > maxSize || + newBox.height > maxSize + ) { + return oldBox; + } + return newBox; + }} + enabledAnchors={[ + "top-left", + "top-center", + "top-right", + "middle-right", + "middle-left", + "bottom-left", + "bottom-center", + "bottom-right", + ]} + rotateEnabled={true} + keepRatio={false} + centeredScaling={true} + padding={5} + ignoreStroke={true} + onTransformEnd={(e: Konva.KonvaEventObject) => { + // Typed event + const konvaNode = e.target; + const nodeId = konvaNode.id(); + + // Get all transformation values including anchor points + const finalRotation = konvaNode.rotation(); + const finalScaleX = konvaNode.scaleX(); + const finalScaleY = konvaNode.scaleY(); + const finalX = konvaNode.x(); + const finalY = konvaNode.y(); + const finalOffsetX = konvaNode.offsetX(); + const finalOffsetY = konvaNode.offsetY(); + + const isLineNode = store.lineNodes.find((ln) => ln.id === nodeId); + + // 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, + }); + } else { + store.updateNode( + nodeId, + { + x: finalX, + y: finalY, + rotation: finalRotation, + scaleX: finalScaleX, + scaleY: finalScaleY, + offsetX: finalOffsetX, + offsetY: finalOffsetY, + }, + true, + ); // Assuming shouldSaveState is true for regular nodes + } + }} + /> + ); + } + return null; + }; + + if (node.type === "line") { + const lineNode = node as LineNode; + + const handleLineDragStart = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + const targetNode = e.target as Konva.Node & { + lastX?: number; + lastY?: number; + }; + targetNode.lastX = targetNode.x(); + targetNode.lastY = targetNode.y(); + }; + const handleLineDragMove = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + const targetNode = e.target as Konva.Node & { + lastX?: number; + lastY?: number; + }; + targetNode.lastX = targetNode.x(); // This doesn't seem to be used for lines to calculate dx/dy + targetNode.lastY = targetNode.y(); // but is set for consistency or future use + }; + const handleLineDragEnd = ( + e: Konva.KonvaEventObject, + nodeId: string, + ) => { + // Move the line node in the store + const targetNode = e.target as Konva.Node; + const finalX = targetNode.x(); + const finalY = targetNode.y(); + const finalOffsetX = targetNode.offsetX(); // Konva might adjust offsetX/Y + 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, + }); + }; + + return ( + + handleNodeMouseDown(e, lineNode.id)} + onTap={(e) => handleNodeMouseDown(e, lineNode.id)} + onMouseEnter={handleNodeMouseEnter} + onMouseLeave={handleNodeMouseLeave} + draggable={draggableIfToolsNotActive( + activeTool, + lineNode.draggable, + )} + onDragMove={(e) => handleLineDragMove(e, lineNode.id)} + onDragStart={(e) => handleLineDragStart(e, lineNode.id)} + onDragEnd={(e) => handleLineDragEnd(e, lineNode.id)} + x={lineNode.x || 0} + y={lineNode.y || 0} + scaleX={lineNode.scaleX || 1} + scaleY={lineNode.scaleY || 1} + rotation={lineNode.rotation || 0} + offsetX={lineNode.offsetX || 0} + offsetY={lineNode.offsetY || 0} + /> + {renderTransformer()} + + ); + } + + if (node.type === "circle") { + return ( + + handleNodeMouseDown(e, node.id)} + onTap={(e) => handleNodeMouseDown(e, node.id)} + onMouseEnter={handleNodeMouseEnter} + onMouseLeave={handleNodeMouseLeave} + onDragMove={(e) => handleNodeDragMove(e, node.id)} + onDragStart={(e) => handleNodeDragStart(e, node.id)} + onDragEnd={(e) => handleNodeDragEnd(e, node.id)} + /> + {renderTransformer()} + + ); + } + + if (node.type === "rectangle") { + return ( + + handleNodeMouseDown(e, node.id)} + onTap={(e) => handleNodeMouseDown(e, node.id)} + onMouseEnter={handleNodeMouseEnter} + onMouseLeave={handleNodeMouseLeave} + onDragMove={(e) => handleNodeDragMove(e, node.id)} + onDragStart={(e) => handleNodeDragStart(e, node.id)} + onDragEnd={(e) => handleNodeDragEnd(e, node.id)} + /> + {renderTransformer()} + + ); + } + + if (node.type === "triangle") { + return ( + + handleNodeMouseDown(e, node.id)} + onTap={(e) => handleNodeMouseDown(e, node.id)} + onMouseEnter={handleNodeMouseEnter} + onMouseLeave={handleNodeMouseLeave} + onDragMove={(e) => handleNodeDragMove(e, node.id)} + onDragStart={(e) => handleNodeDragStart(e, node.id)} + onDragEnd={(e) => handleNodeDragEnd(e, node.id)} + /> + {renderTransformer()} + + ); + } + + if (node.type === "image") { + return ( + + {/* Render background rectangle if backgroundColor is set */} + {node.backgroundColor && node.backgroundColor !== "transparent" && ( + + )} + handleNodeMouseDown(e, node.id)} + onTap={(e) => handleNodeMouseDown(e, node.id)} + onMouseEnter={handleNodeMouseEnter} + onMouseLeave={handleNodeMouseLeave} + onDragMove={(e) => handleNodeDragMove(e, node.id)} + onDragStart={(e) => handleNodeDragStart(e, node.id)} + onDragEnd={(e) => handleNodeDragEnd(e, node.id)} + /> + {renderTransformer()} + + ); + } + + return null; + }; + + // TODO refactor this out into zustand? and render the draw ? + useEffect(() => { + if (!stageRef.current) return; + // Update individual transformers + Object.entries(transformerRefs.current).forEach(([nodeId, transformer]) => { + if (!transformer) return; // Transformer might be null if ref was cleared + + const isSelected = selectedNodeIds.includes(nodeId); + transformer.visible(isSelected); + + if (isSelected) { + const node = stageRef.current!.findOne(`#${nodeId}`); // stageRef.current is checked + if (node) { + transformer.nodes([node]); + transformer.getLayer()?.batchDraw(); } } - }, [selectedNodeIds]); + }); + // Update multi-select transformer + if (multiSelectTransformerRef.current && selectedNodeIds.length > 1) { + const nodes = selectedNodeIds + .map((id) => stageRef.current.findOne(`#${id}`)) + .filter(Boolean); + if (nodes.length > 0) { + multiSelectTransformerRef.current.nodes(nodes); + multiSelectTransformerRef.current.getLayer()?.batchDraw(); + } + } + }, [selectedNodeIds]); - return ( {/* Left Panel */} - { ctx.rect(0, 0, leftPanelWidth, leftPanelHeight); @@ -888,10 +997,10 @@ export const PaintSurface = ({ listening={false} zIndex={-1} /> - - {/* Render all nodes including line nodes */} - {[...nodes, ...store.lineNodes].map((node) => renderNode(node))} - + + {/* Render all nodes including line nodes */} + {[...nodes, ...store.lineNodes].map((node) => renderNode(node))} + {/* Render selection rectangle */} {selectionRect && ( )} - +
} right={ -
+
- - + + + />
} - />) -}; \ No newline at end of file + /> + ); +}; diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx index ad6cf512a1..77d774aaab 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx @@ -1,109 +1,65 @@ -import React, { useState, useRef } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import Slider from './Slider'; -import PromptTextArea from './PromptTextArea'; -import ImageStyleSelector from './ImageStyleSelector'; -import { PromptEditorProps, AspectRatio, ImageStyle } from './types'; +import React, { useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import Slider from "./Slider"; +import ImageStyleSelector from "./ImageStyleSelector"; +import { PromptEditorProps, ImageStyle } from "./types"; +import { PromptBox2D } from "@storyteller/ui-promptbox"; +import { uploadImage } from "../../../components/reusable/UploadModalMedia/uploadImage"; +import { getCanvasRenderBitmap } from "../../../signals/canvasRenderBitmap"; +import { EncodeImageBitmapToBase64 } from "../utilities/EncodeImageBitmapToBase64"; +import { JobProvider, useJobContext } from "../JobContext"; const PromptEditor: React.FC = ({ - initialPrompt = '', - onPromptChange, - onRandomize, - onVary, onAIStrengthChange, - onAspectRatioChange, - onImageStyleChange + onImageStyleChange, }) => { - const [prompt, setPrompt] = useState(initialPrompt); const [aiStrength, setAIStrength] = useState(0.75); - const [aspectRatio, setAspectRatio] = useState('1:1'); const [images, setImages] = useState([]); - - const handlePromptChange = (newPrompt: string) => { - setPrompt(newPrompt); - onPromptChange?.(newPrompt); - }; - + const handleAIStrengthChange = (strength: number) => { setAIStrength(strength); onAIStrengthChange?.(strength); }; - - const handleAspectRatioChange = (ratio: AspectRatio) => { - setAspectRatio(ratio); - onAspectRatioChange?.(ratio); - }; const handleImageSelect = (file: File) => { const imageUrl = URL.createObjectURL(file); const newImage: ImageStyle = { id: uuidv4(), url: imageUrl, - weight: 0.5 + weight: 0.5, }; - + const updatedImages = [...images, newImage]; setImages(updatedImages); onImageStyleChange?.(updatedImages); }; - - const handleImageStyleClick = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.onchange = (e) => { - const target = e.target as HTMLInputElement; - if (target.files && target.files[0]) { - handleImageSelect(target.files[0]); - } - }; - input.click(); - }; - - const handleImageUpdate = (id: string, updates: Partial) => { - const updatedImages = images.map(img => - img.id === id ? { ...img, ...updates } : img - ); - setImages(updatedImages); - onImageStyleChange?.(updatedImages); - }; - - const handleImageRemove = (id: string) => { - const updatedImages = images.filter(img => img.id !== id); - setImages(updatedImages); - onImageStyleChange?.(updatedImages); - }; - - const handleRandomize = () => { - onRandomize?.(); - }; - - const handleVary = () => { - onVary?.(); - }; return ( -
-
- +
+
+
- {}} - onImageStyleClick={handleImageStyleClick} - onRandomizeClick={handleRandomize} - onVaryClick={handleVary} - /> - + + + { + // await renderEngineRef.current?.render(); //TODO: Render the canvas + }} + /> + +
); }; -export default PromptEditor; \ No newline at end of file +export default PromptEditor; diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptTextArea.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptTextArea.tsx index 615e580cbe..75cd02a77e 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptTextArea.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptTextArea.tsx @@ -1,12 +1,7 @@ -import React from 'react'; -import { - SparklesIcon, - Undo2, - Smartphone, - LayoutPanelTop, -} from 'lucide-react'; -import { PromptTextAreaProps, AspectRatio } from './types'; -import ImagePreview from './ImagePreview'; +import React from "react"; +import { SparklesIcon, Undo2, Smartphone, LayoutPanelTop } from "lucide-react"; +import { PromptTextAreaProps, AspectRatio } from "./types"; +import ImagePreview from "./ImagePreview"; const PromptTextArea: React.FC< PromptTextAreaProps & { @@ -32,35 +27,36 @@ const PromptTextArea: React.FC< }) => { /* helper to cycle aspect ratios */ const getNextAspectRatio = (): AspectRatio => { - const ratios: AspectRatio[] = ['1:1', '3:2', '2:3']; + const ratios: AspectRatio[] = ["1:1", "3:2", "2:3"]; const i = ratios.indexOf(aspectRatio); return ratios[(i + 1) % ratios.length]; }; /* button styling – matches existing toolbar */ const buttonClass = - 'flex items-center gap-1 rounded-lg px-3 py-1 ' + - 'bg-[#3A3A3A] text-sm text-white hover:bg-[#4A4A4A] ' + - 'transition-colors'; + "flex items-center gap-1 rounded-lg px-3 py-1 " + + "bg-[#3A3A3A] text-sm text-white hover:bg-[#4A4A4A] " + + "transition-colors"; /* ───────── render ───────── */ return ( -
+
{/* LEFT ▸ prompt + toolbar */} -
+
{/* prompt field */}
-