From fa495c65d165e47cd434ab22d41e78876ea96224 Mon Sep 17 00:00:00 2001 From: BFlat Date: Mon, 7 Jul 2025 22:33:39 +0700 Subject: [PATCH] 2d bug fixes and improvements (#702) **fixes:** - moved canvas fit to screen button to inside prompt box - canvas fit to screen logic works properly and always centered - canvas doesnt overflow screen and annoyingly scroll anymore - brush now has an active state and border and shows brush icon when not active - hidden eraser button for now since it doesn't work TODO: - missing features --- .../app/src/pages/PageDraw/PageDraw.tsx | 305 ++++++++++-------- .../app/src/pages/PageDraw/PaintSurface.tsx | 135 +++++--- .../PageDraw/PromptEditor/PromptEditor.tsx | 7 +- .../src/pages/PageDraw/PromptEditor/types.ts | 5 +- .../PageDraw/components/ui/SideToolbar.tsx | 86 ++--- .../PageDraw/components/ui/SplitPane.tsx | 6 +- .../promptbox/src/lib/PromptBox2D.tsx | 23 +- 7 files changed, 343 insertions(+), 224 deletions(-) diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx index e626c1c9c..178d43a88 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PageDraw.tsx @@ -15,45 +15,43 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faClock, faImage } from "@fortawesome/pro-solid-svg-icons"; import Konva from "konva"; // just for types -import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap" -import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot" +import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap"; +import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot"; import { ContextMenuContainer } from "./components/ui/ContextMenu"; -import { - FalBackgroundRemoval, - FalBackgroundRemovalRequest, -} from "@storyteller/tauri-api"; +import { FalBackgroundRemoval } from "@storyteller/tauri-api"; - - /** +/** * This decodes Base64 encoded PNG images into an image element. * @param base64String a standard (not "web safe") base64-encoded string */ - export const DecodeBase64ToImage = async (base64String: string) : Promise => { - // Create an image element - const img = document.createElement("img"); - - // Convert base64 to data URL if it doesn't include the prefix - const dataUrl = base64String.startsWith("data:") - ? base64String - : `data:image/png;base64,${base64String}`; - - // Create a promise to handle the image loading - return new Promise((resolve, reject) => { - img.onload = async () => { - try { - const bitmap = await createImageBitmap(img); - resolve(bitmap); - } catch (error) { - reject(error); - } - }; - - img.onerror = () => reject(new Error("Failed to load image")); - - // Set the source to trigger loading - img.src = dataUrl; - }); - } +export const DecodeBase64ToImage = async ( + base64String: string, +): Promise => { + // Create an image element + const img = document.createElement("img"); + + // Convert base64 to data URL if it doesn't include the prefix + const dataUrl = base64String.startsWith("data:") + ? base64String + : `data:image/png;base64,${base64String}`; + + // Create a promise to handle the image loading + return new Promise((resolve, reject) => { + img.onload = async () => { + try { + const bitmap = await createImageBitmap(img); + resolve(bitmap); + } catch (error) { + reject(error); + } + }; + + img.onerror = () => reject(new Error("Failed to load image")); + + // Set the source to trigger loading + img.src = dataUrl; + }); +}; const PageDraw = () => { //useStateSceneLoader(); @@ -196,9 +194,10 @@ const PageDraw = () => { } }; - const onFitPressed = async () => { + 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; @@ -208,13 +207,31 @@ const PageDraw = () => { const canvasW = store.getAspectRatioDimensions().width; const canvasH = store.getAspectRatioDimensions().height; - // Calculate position to center canvas in container + // Add padding to ensure canvas doesn't touch the edges + const padding = 40; + const availableWidth = containerWidth - padding * 2; + const availableHeight = containerHeight - padding * 2; + + // Calculate scale to fit canvas within container while maintaining aspect ratio + const scaleX = availableWidth / canvasW; + const scaleY = availableHeight / canvasH; + const scale = Math.min(scaleX, scaleY, 1); // Don't scale up beyond 100% + + // Set the scale + stage.scale({ x: scale, y: scale }); + + // Calculate position to center the scaled canvas in container + const scaledCanvasW = canvasW * scale; + const scaledCanvasH = canvasH * scale; + stage.position({ - x: (containerWidth - canvasW) / 2, - y: (containerHeight - canvasH) / 2 + x: (containerWidth - scaledCanvasW) / 2, + y: (containerHeight - scaledCanvasH) / 2, }); - } + // Redraw the stage + stage.batchDraw(); + }; return ( <> @@ -237,7 +254,7 @@ const PageDraw = () => { console.log("Vary clicked"); // Handle vary action here }} - onAspectRatioChange={(ratio: string) => { + onAspectRatioChange={async (ratio: string) => { console.log("Aspect ratio:", ratio); // Convert ratio string to AspectRatioType enum const ratioToType = (ratio: string): AspectRatioType => { @@ -255,9 +272,13 @@ const PageDraw = () => { const aspectRatioType = ratioToType(ratio); store.setAspectRatioType(aspectRatioType); - onFitPressed() + + // Wait for the next frame to ensure the store has updated + await new Promise((resolve) => requestAnimationFrame(resolve)); + onFitPressed(); }} onEnqueuePressed={onEnqueuedPressed} + onFitPressed={onFitPressed} /> { activeToolId={store.activeTool} />
- { - if (action === "contextMenu") { - const hasSelection = store.selectedNodeIds.length > 0; - if (hasSelection) { - console.log("An item is selected."); - // You can add additional actions here based on the selection - return true - } else { - console.log("No item is selected."); - return false + { + if (action === "contextMenu") { + const hasSelection = store.selectedNodeIds.length > 0; + if (hasSelection) { + console.log("An item is selected."); + // You can add additional actions here based on the selection + return true; + } else { + console.log("No item is selected."); + return false; + } } - } - return false - }} - onMenuAction={async (action) => { - switch (action) { - case 'LOCK': - store.toggleLock(store.selectedNodeIds); - break; - case 'REMOVE_BACKGROUND': - // returns a success if we have selected images only - await store.removeBackground(store.selectedNodeIds, - async (success: boolean, image_base64: string, message: string) => { - if (!success) { - console.error(message); - return { success: false }; - } - try { - const response = await FalBackgroundRemoval({ base64_image: image_base64 }); - if (response.status !== "success" || !("payload" in response)) { - console.error("Failed to remove background", response); - return { success: false }; - } - - const base64String = response.payload?.base64_bytes as string; - const binaryString = atob(base64String); - const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0)); - const blob = new Blob([bytes], { type: "image/png" }); - const file = new File([blob], "generated_image.png", { type: blob.type }); - return { success: true, file }; - } catch (error) { - console.error("Failed to remove background", error); - return { success: false }; - } - }); - break; - case 'BRING_TO_FRONT': - store.bringToFront(store.selectedNodeIds); - break; - case 'BRING_FORWARD': - store.bringForward(store.selectedNodeIds); - break; - case 'SEND_BACKWARD': - store.sendBackward(store.selectedNodeIds); - break; - case 'SEND_TO_BACK': - store.sendToBack(store.selectedNodeIds); - break; - case 'DUPLICATE': - store.copySelectedItems() - store.pasteItems() - break; - case 'DELETE': - store.deleteSelectedItems() - break; - default: - // No action needed for unhandled cases - } - }} - isLocked={store.selectedNodeIds.some(id => { - const node = store.nodes.find(n => n.id === id); - const lineNode = store.lineNodes.find(n => n.id === id); - return (node?.locked || lineNode?.locked) ?? false; - })} - > - { - canvasWidth.current = width; - canvasHeight.current = height; + return false; }} - fillColor={store.fillColor} - activeTool={store.activeTool} - brushColor={store.brushColor} - brushSize={store.brushSize} - onSelectionChange={setIsSelecting} - stageRef={stageRef} - transformerRefs={transformerRefs} - /> - + onMenuAction={async (action) => { + switch (action) { + case "LOCK": + store.toggleLock(store.selectedNodeIds); + break; + case "REMOVE_BACKGROUND": + // returns a success if we have selected images only + await store.removeBackground( + store.selectedNodeIds, + async ( + success: boolean, + image_base64: string, + message: string, + ) => { + if (!success) { + console.error(message); + return { success: false }; + } + try { + const response = await FalBackgroundRemoval({ + base64_image: image_base64, + }); + if ( + response.status !== "success" || + !("payload" in response) + ) { + console.error("Failed to remove background", response); + return { success: false }; + } + + const base64String = response.payload + ?.base64_bytes as string; + const binaryString = atob(base64String); + const bytes = Uint8Array.from(binaryString, (c) => + c.charCodeAt(0), + ); + const blob = new Blob([bytes], { type: "image/png" }); + const file = new File([blob], "generated_image.png", { + type: blob.type, + }); + return { success: true, file }; + } catch (error) { + console.error("Failed to remove background", error); + return { success: false }; + } + }, + ); + break; + case "BRING_TO_FRONT": + store.bringToFront(store.selectedNodeIds); + break; + case "BRING_FORWARD": + store.bringForward(store.selectedNodeIds); + break; + case "SEND_BACKWARD": + store.sendBackward(store.selectedNodeIds); + break; + case "SEND_TO_BACK": + store.sendToBack(store.selectedNodeIds); + break; + case "DUPLICATE": + store.copySelectedItems(); + store.pasteItems(); + break; + case "DELETE": + store.deleteSelectedItems(); + break; + default: + // No action needed for unhandled cases + } + }} + isLocked={store.selectedNodeIds.some((id) => { + const node = store.nodes.find((n) => n.id === id); + const lineNode = store.lineNodes.find((n) => n.id === id); + return (node?.locked || lineNode?.locked) ?? false; + })} + > + { + canvasWidth.current = width; + canvasHeight.current = height; + }} + fillColor={store.fillColor} + activeTool={store.activeTool} + brushColor={store.brushColor} + brushSize={store.brushSize} + onSelectionChange={setIsSelecting} + stageRef={stageRef} + transformerRefs={transformerRefs} + /> +
{ showIconsInList triggerLabel="Model" /> -
); diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx index 130bfe3b9..d6842f76f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx @@ -50,8 +50,8 @@ export const PaintSurface = ({ transformerRefs, }: MiraiProps) => { // switch off to be preview panel mode. - const singlePaneMode = true - + const singlePaneMode = true; + const store = useSceneStore(); // Use store directly const imageRef = React.useRef(null); const [snapshotImage, setSnapshotImage] = useState( @@ -62,6 +62,13 @@ export const PaintSurface = ({ const cursorLayerRef = React.useRef(null); const cursorShapeRef = React.useRef(null); + // Add container ref and dimensions state + const containerRef = React.useRef(null); + const [containerDimensions, setContainerDimensions] = useState({ + width: window.innerWidth, + height: window.innerHeight - 56, + }); + // Layout 683 by 1024 const [leftPanelWidth, setLeftPanelWidth] = useState(1024); const [leftPanelHeight, setLeftPanelHeight] = useState(1024); @@ -69,7 +76,7 @@ export const PaintSurface = ({ const [rightPanelHeight, setRightPanelHeight] = useState(1024); /* 1️⃣ Track SplitPane percent so we can re-measure */ - const [leftPct, setLeftPct] = useState(singlePaneMode ? 100:50); + const [leftPct, setLeftPct] = useState(singlePaneMode ? 100 : 50); const [isDrawing, setIsDrawing] = useState(false); const [lastPoint, setLastPoint] = useState<{ x: number; y: number } | null>( null, @@ -89,6 +96,34 @@ export const PaintSurface = ({ // Add state to track the current line being drawn const [currentLineId, setCurrentLineId] = useState(null); + + // Add useLayoutEffect to measure container dimensions + useLayoutEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setContainerDimensions({ + width: rect.width, + height: rect.height, + }); + } + }; + + // Initial measurement + updateDimensions(); + + // Add resize observer for responsive updates + const resizeObserver = new ResizeObserver(updateDimensions); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Cleanup + return () => { + resizeObserver.disconnect(); + }; + }, [leftPct]); // Re-run when split pane percentage changes + const stagePosition = useStageCentering( stageRef, leftPct, @@ -109,7 +144,7 @@ export const PaintSurface = ({ // isSelectingRef, // transformerRefs // ); - + const previewScale = useRightPanelLayoutManagement( rightContainerRef, NATURAL_WIDTH, @@ -135,7 +170,10 @@ export const PaintSurface = ({ }): { 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)), }; }; @@ -201,7 +239,7 @@ export const PaintSurface = ({ locked: false, }; 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); @@ -288,7 +326,7 @@ export const PaintSurface = ({ stagePoint.x, stagePoint.y, ]; - store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing + store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing } setLastPoint(stagePoint); } else { @@ -337,7 +375,7 @@ export const PaintSurface = ({ 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) { @@ -549,19 +587,21 @@ export const PaintSurface = ({ // 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); + 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 if (isLocked) { store.selectNode(nodeId); return; } } - + // 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; } @@ -686,15 +726,19 @@ export const PaintSurface = ({ // 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, @@ -768,7 +812,7 @@ export const PaintSurface = ({ 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 @@ -820,7 +864,10 @@ export const PaintSurface = ({ 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} @@ -852,7 +899,10 @@ export const PaintSurface = ({ 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} @@ -884,7 +934,10 @@ export const PaintSurface = ({ 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} @@ -932,7 +985,10 @@ export const PaintSurface = ({ 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} @@ -980,18 +1036,20 @@ export const PaintSurface = ({ } }, [selectedNodeIds]); - return ( +
{ - ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight); + ctx.rect( + 0, + 0, + store.getAspectRatioDimensions().width, + store.getAspectRatioDimensions().height, + ); // leftPanelWidth, leftPanelHeight); }} > (a.zIndex || 0) - (b.zIndex || 0)) .map((node, index) => { - // console.log(`Node ${index}:`, node); - return renderNode(node); - })} + // console.log(`Node ${index}:`, node); + return renderNode(node); + })} {/* Render selection rectangle */} {selectionRect && ( 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 e95644a0d..485a552dc 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/PromptEditor.tsx @@ -8,11 +8,12 @@ import { EncodeImageBitmapToBase64 } from "../utilities/EncodeImageBitmapToBase6 import { JobProvider, useJobContext } from "../JobContext"; // Set this value on when enqueue is pressed nasty global variable. -import { getCanvasRenderBitmap } from "../../../signals/canvasRenderBitmap" +import { getCanvasRenderBitmap } from "../../../signals/canvasRenderBitmap"; const PromptEditor: React.FC = ({ onImageStyleChange, onEnqueuePressed, onAspectRatioChange, + onFitPressed, }) => { const [images, setImages] = useState([]); @@ -31,8 +32,7 @@ const PromptEditor: React.FC = ({ return (
-
-
+
= ({ useJobContext={useJobContext} onEnqueuePressed={onEnqueuePressed} onAspectRatioChange={onAspectRatioChange} + onFitPressed={onFitPressed} /> diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/types.ts b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/types.ts index bbee5ebaf..d92163303 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/types.ts +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PromptEditor/types.ts @@ -7,9 +7,10 @@ export interface PromptEditorProps { onAspectRatioChange?: (ratio: AspectRatio) => void; onImageStyleChange?: (images: ImageStyle[]) => void; onEnqueuePressed?: () => void; + onFitPressed?: () => void; } -export type AspectRatio = '1:1' | '3:2' | '2:3'; +export type AspectRatio = "1:1" | "3:2" | "2:3"; export interface ImageStyle { id: string; @@ -51,4 +52,4 @@ export interface ImagePreviewProps { image: ImageStyle; onUpdate: (id: string, updates: Partial) => void; onRemove: (id: string) => void; -} \ No newline at end of file +} diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SideToolbar.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SideToolbar.tsx index 0ae3f6b1c..80fd1e0ba 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SideToolbar.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SideToolbar.tsx @@ -3,18 +3,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faMousePointer, faShapes, - faEraser, faTrash, faEyeDropper, faImage, faSquare, faCircle, faPlay, - faSparkles, - faFileImport, - faFileExport, faUndo, - faRedo + faRedo, + faPaintBrush, } from "@fortawesome/pro-solid-svg-icons"; import "../../App.css"; import { HsvaColorPicker, HsvaColor } from "react-colorful"; @@ -42,10 +39,8 @@ function useDebounced void, A extends unknown[]>( export interface SideToolbarProps { onSelect: () => void; onAddShape: (shape: "rectangle" | "circle" | "triangle") => void; - onPaintBrush: (hex: string, size: number,opacity: number) => void; - onEraser: (size: number) => void; + onPaintBrush: (hex: string, size: number, opacity: number) => void; onCanvasBackground: (hex: string) => void; - onGenerateImage: () => void; onUploadImage: () => void; onDelete: () => void; activeToolId: string; @@ -56,9 +51,7 @@ const SideToolbar: React.FC = ({ onSelect, onAddShape, onPaintBrush, - onEraser, onCanvasBackground, - onGenerateImage, onUploadImage, onDelete, activeToolId, @@ -86,8 +79,7 @@ const SideToolbar: React.FC = ({ const sendPaint = useDebounced< (hex: string, size: number, opacity: number) => void, [string, number, number] - >(onPaintBrush, 75, 1); - + >(onPaintBrush, 75); const sendBg = useDebounced<(hex: string) => void, [string]>( onCanvasBackground, @@ -123,7 +115,7 @@ const SideToolbar: React.FC = ({ brushHsva, setBrushHsva, (hex) => sendPaint(hex, brushSize, brushHsva.a), - + <>

Brush Size

@@ -203,39 +195,36 @@ const SideToolbar: React.FC = ({ }, { id: "separator-2", type: "separator" }, { - id: "paint", + id: "draw", label: "Brush", - icon: ( - - ), + icon: null, onClick: () => { sendPaint(hsvaToHex(brushHsva), brushSize, brushHsva.a); + store.setActiveTool("draw"); }, popout: BrushPopout, }, - { - id: "eraser", - label: "Eraser", - icon: , - onClick: () => { - onEraser(brushSize); - }, - popout: ( -
- { - setBrushSize(size); - onEraser(size); - }} - label="Eraser Size" - /> -
- ), - }, + // Commented out for now + // { + // id: "eraser", + // label: "Eraser", + // icon: , + // onClick: () => { + // onEraser(brushSize); + // }, + // popout: ( + //
+ // { + // setBrushSize(size); + // onEraser(size); + // }} + // label="Eraser Size" + // /> + //
+ // ), + // }, { id: "delete", label: "Delete", @@ -323,6 +312,19 @@ const SideToolbar: React.FC = ({ ? "bg-primary/30 border-2 !border-primary text-white" : "hover:bg-white/10 text-white"; + // Dynamic icon for brush/draw tool + let displayIcon = icon; + if (id === "draw") { + displayIcon = active ? ( + + ) : ( + + ); + } + return (
= ({ }} className={`${baseBtn} ${btnStyle}`} > - {icon} + {displayIcon} ) : ( )} diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SplitPane.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SplitPane.tsx index 6e744485d..d8a5a8f93 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SplitPane.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/components/ui/SplitPane.tsx @@ -26,7 +26,7 @@ export default function SplitPane({ maxPercent = 85, onChange, className = "", - singlePaneMode = true + singlePaneMode = true, }: SplitPaneProps) { const rootRef = useRef(null); const [leftPct, setLeftPct] = useState(initialPercent); @@ -65,7 +65,7 @@ export default function SplitPane({ return singlePaneMode ? (
{left} @@ -74,7 +74,7 @@ export default function SplitPane({ ) : (
{left} diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx index 5cf75d451..029a9e59e 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx @@ -21,6 +21,7 @@ import { faPlus, faUpload, faImages, + faExpandWide, } from "@fortawesome/pro-solid-svg-icons"; import { faRectangleVertical, @@ -44,6 +45,8 @@ import { import { showActionReminder } from "@storyteller/ui-action-reminder-modal"; import { invoke } from "@tauri-apps/api/core"; +export type AspectRatio = "1:1" | "3:2" | "2:3"; + interface ReferenceImage { id: string; url: string; @@ -66,6 +69,7 @@ interface PromptBox2DProps { useJobContext: () => JobContextType; onEnqueuePressed?: () => void | Promise; onAspectRatioChange?: (ratio: AspectRatio) => void; + onFitPressed?: () => void | Promise; } export const PromptBox2D = ({ @@ -75,6 +79,7 @@ export const PromptBox2D = ({ useJobContext, onEnqueuePressed, onAspectRatioChange, + onFitPressed, }: PromptBox2DProps) => { useSignals(); @@ -124,7 +129,7 @@ export const PromptBox2D = ({ }); const handleAspectRatioSelect = (selectedItem: PopoverItem) => { - onAspectRatioChange?.(selectedItem.label); + onAspectRatioChange?.(selectedItem.label as AspectRatio); setAspectRatioList((prev) => prev.map((item) => ({ ...item, @@ -612,6 +617,22 @@ export const PromptBox2D = ({ onClick={() => setUseSystemPrompt(!useSystemPrompt)} /> + {onFitPressed && ( + + + + )}