diff --git a/frontend/apps/editor3d/app/src/Helpers/CanvasHelpers.ts b/frontend/apps/editor3d/app/src/Helpers/CanvasHelpers.ts new file mode 100644 index 0000000000..d55a3b87d9 --- /dev/null +++ b/frontend/apps/editor3d/app/src/Helpers/CanvasHelpers.ts @@ -0,0 +1,14 @@ +export const normalizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement => { + const newCanvas = document.createElement("canvas"); + newCanvas.width = width; + newCanvas.height = 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; +} diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx new file mode 100644 index 0000000000..6e36664c53 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx @@ -0,0 +1,20 @@ +import { faImage } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export type BaseImageSelectorProps = { + onImageSelect: (imageUrl: string) => void; +} + +const BaseImageSelector = ({ + onImageSelect +}: BaseImageSelectorProps) => { + + return ( +
+ + Click to upload or drag and drop an image here to edit +
+
+
+ ) +} diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx index 1efa98ba16..09873e5c7f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx @@ -32,6 +32,8 @@ export type EditPaintSurfaceProps = { onSelectionChange?: (isSelecting: boolean) => void; stageRef: React.RefObject; transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>; + leftPanelRef: React.RefObject; + rectRef: React.RefObject; }; export const EditPaintSurface = ({ @@ -45,6 +47,8 @@ export const EditPaintSurface = ({ onSelectionChange, stageRef, transformerRefs, + leftPanelRef, + rectRef }: EditPaintSurfaceProps) => { // switch off to be preview panel mode. const singlePaneMode = true @@ -54,7 +58,6 @@ export const EditPaintSurface = ({ const [snapshotImage, setSnapshotImage] = useState( null, ); - const leftPanelRef = React.useRef(null); const rightContainerRef = React.useRef(null); const cursorLayerRef = React.useRef(null); const cursorShapeRef = React.useRef(null); @@ -977,7 +980,6 @@ export const EditPaintSurface = ({ } }, [selectedNodeIds]); - return ( { //useStateSceneLoader(); @@ -32,6 +33,8 @@ const PageEdit = () => { const [selectedModel, setSelectedModel] = useState("GPT-4o"); // Create refs for stage and image const stageRef = useRef({} as Konva.Stage); + const leftPanelRef = useRef({} as Konva.Layer); + const rectRef = useRef({} as Konva.Rect); const transformerRefs = useRef<{ [key: string]: Konva.Transformer }>({}); // Use the Zustand store @@ -179,9 +182,47 @@ const PageEdit = () => { x: (containerWidth - canvasW) / 2, y: (containerHeight - canvasH) / 2 }); - } + // Create a function to use the left layer ref and download the bitmap from it + const downloadLeftPanelBitmap = () => { + if (!stageRef.current || !leftPanelRef.current || !rectRef.current) { + console.error("Stage or left panel ref is not available"); + return; + } + + const layer = leftPanelRef.current; + + // Get the canvas area that's covered by the image/rectangle + const rect = rectRef.current; + const layerCrop = layer.toCanvas({ + x: stageRef.current.x(), + y: stageRef.current.y(), + width: rect.width() * stageRef.current.scaleX(), + height: rect.height() * stageRef.current.scaleY(), + pixelRatio: 1 / stageRef.current.scaleX(), + }); + + // 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(); + } + + fittedCanvas.toBlob(downloadCallback, "image/png", 1.0); + }; + return ( <>
{ onModeChange={(mode: string) => { store.setActiveTool(mode as ActiveEditTool) }} selectedMode={store.activeTool} onEnqueuePressed={onEnqueuedPressed} + onGenerateClick={downloadLeftPanelBitmap} />
{ onSelectionChange={setIsSelecting} stageRef={stageRef} transformerRefs={transformerRefs} + leftPanelRef={leftPanelRef} + rectRef={rectRef} /> diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx index f2333c369e..40c586fbb1 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx @@ -11,6 +11,7 @@ const PromptEditor: React.FC = ({ onEnqueuePressed, onModeChange, selectedMode, + onGenerateClick, }) => { return (
@@ -26,6 +27,7 @@ const PromptEditor: React.FC = ({ onEnqueuePressed={onEnqueuePressed} onModeSelectionChange={onModeChange} selectedMode={selectedMode} + onGenerateClick={onGenerateClick} />
diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/types.ts b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/types.ts index 7aceb51b38..6bce25a437 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/types.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/types.ts @@ -8,6 +8,7 @@ export interface PromptEditorProps { onEnqueuePressed?: () => void; onModeChange?: (mode: string) => void; selectedMode?: string; + onGenerateClick: () => void; } export type AspectRatio = '1:1' | '3:2' | '2:3'; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts b/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts index 4169c198fd..700e4ffca1 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts @@ -26,6 +26,9 @@ interface EditState { cursorPosition: { x: number; y: number } | null; cursorVisible: boolean; + // Base image state + baseImage: ImageBitmap | null; + // Actions addNode: (node: Node) => void; removeNode: (id: string, shouldSaveState?: boolean) => void; @@ -131,6 +134,9 @@ export const useEditStore = create((set, get) => ({ cursorPosition: null, cursorVisible: false, + // Base image initial state + baseImage: null, + // Add initial aspect ratio state aspectRatioType: AspectRatioType.NONE, diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx index 0c19ec90ee..96f790cedc 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx @@ -69,6 +69,7 @@ interface PromptBoxEditProps { onEnqueuePressed?: () => void | Promise; onModeSelectionChange?: (mode: string) => void; selectedMode?: string; + onGenerateClick: () => void; } export const PromptBoxEdit = ({ @@ -79,6 +80,7 @@ export const PromptBoxEdit = ({ onEnqueuePressed, onModeSelectionChange, selectedMode, + onGenerateClick }: PromptBoxEditProps) => { useSignals(); @@ -553,7 +555,7 @@ export const PromptBoxEdit = ({