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 = ({