Compare commits

...
Author SHA1 Message Date
bflatastic 351df69072 brush icon and active mode 2025-07-07 15:49:27 +07:00
bflatastic aef01dbde3 canvas annoying scroll and fit logic fixed 2025-07-07 15:32:13 +07:00
7 changed files with 343 additions and 224 deletions
@@ -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<ImageBitmap> => {
// 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<ImageBitmap> => {
// 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}
/>
</div>
<SideToolbar
@@ -345,98 +366,114 @@ const PageDraw = () => {
activeToolId={store.activeTool}
/>
<div className="relative z-0">
<ContextMenuContainer
onAction={(e, action) => {
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
<ContextMenuContainer
onAction={(e, action) => {
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;
})}
>
<PaintSurface
nodes={store.nodes}
selectedNodeIds={store.selectedNodeIds}
onCanvasSizeChange={(width: number, height: number): void => {
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}
/>
</ContextMenuContainer>
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;
})}
>
<PaintSurface
nodes={store.nodes}
selectedNodeIds={store.selectedNodeIds}
onCanvasSizeChange={(width: number, height: number): void => {
canvasWidth.current = width;
canvasHeight.current = height;
}}
fillColor={store.fillColor}
activeTool={store.activeTool}
brushColor={store.brushColor}
brushSize={store.brushSize}
onSelectionChange={setIsSelecting}
stageRef={stageRef}
transformerRefs={transformerRefs}
/>
</ContextMenuContainer>
</div>
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-2">
<PopoverMenu
@@ -449,12 +486,6 @@ const PageDraw = () => {
showIconsInList
triggerLabel="Model"
/>
<button
className="bg-transparent p-2 text-lg text-white hover:text-white hover:bg-white/20 rounded transition"
onClick={onFitPressed}
>
Fit
</button>
</div>
</>
);
@@ -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<HTMLImageElement>(null);
const [snapshotImage, setSnapshotImage] = useState<HTMLImageElement | null>(
@@ -62,6 +62,13 @@ export const PaintSurface = ({
const cursorLayerRef = React.useRef<Konva.Layer>(null);
const cursorShapeRef = React.useRef<Konva.Circle>(null);
// Add container ref and dimensions state
const containerRef = React.useRef<HTMLDivElement>(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<string | null>(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 (
<SplitPane
singlePaneMode={singlePaneMode}
initialPercent={singlePaneMode?100 : 50}
initialPercent={singlePaneMode ? 100 : 50}
onChange={setLeftPct}
left={
<div className="flex h-full w-full items-center justify-center overflow-hidden">
<div
ref={containerRef}
className="flex h-full w-full items-center justify-center overflow-hidden"
>
<Stage
ref={stageRef}
width={window.innerWidth * (leftPct / 100)}
height={window.innerHeight}
width={containerDimensions.width * (leftPct / 100)}
height={containerDimensions.height}
scaleX={1} // Initial scale, controlled by wheel/zoom
scaleY={1} // Initial scale, controlled by wheel/zoom
style={{
@@ -1012,7 +1070,12 @@ export const PaintSurface = ({
<Layer
ref={leftPanelRef}
clipFunc={(ctx) => {
ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight);
ctx.rect(
0,
0,
store.getAspectRatioDimensions().width,
store.getAspectRatioDimensions().height,
); // leftPanelWidth, leftPanelHeight);
}}
>
<Rect
@@ -1029,9 +1092,9 @@ export const PaintSurface = ({
{[...nodes, ...store.lineNodes]
.sort((a, b) => (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 && (
@@ -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<PromptEditorProps> = ({
onImageStyleChange,
onEnqueuePressed,
onAspectRatioChange,
onFitPressed,
}) => {
const [images, setImages] = useState<ImageStyle[]>([]);
@@ -31,8 +32,7 @@ const PromptEditor: React.FC<PromptEditorProps> = ({
return (
<div className="mx-auto flex w-full max-w-3xl flex-col space-y-2">
<div className="flex w-full justify-center">
</div>
<div className="flex w-full justify-center"></div>
<JobProvider>
<PromptBox2D
@@ -42,6 +42,7 @@ const PromptEditor: React.FC<PromptEditorProps> = ({
useJobContext={useJobContext}
onEnqueuePressed={onEnqueuePressed}
onAspectRatioChange={onAspectRatioChange}
onFitPressed={onFitPressed}
/>
</JobProvider>
@@ -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<ImageStyle>) => void;
onRemove: (id: string) => void;
}
}
@@ -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<T extends (...args: A) => 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<SideToolbarProps> = ({
onSelect,
onAddShape,
onPaintBrush,
onEraser,
onCanvasBackground,
onGenerateImage,
onUploadImage,
onDelete,
activeToolId,
@@ -86,8 +79,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
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<SideToolbarProps> = ({
brushHsva,
setBrushHsva,
(hex) => sendPaint(hex, brushSize, brushHsva.a),
<>
<div className="relative">
<p className="mb-2 text-sm font-medium text-white">Brush Size</p>
@@ -203,39 +195,36 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
},
{ id: "separator-2", type: "separator" },
{
id: "paint",
id: "draw",
label: "Brush",
icon: (
<span
className="inline-block h-5 w-5 rounded-full border-2 border-white"
style={{ backgroundColor: hsvaToHex(brushHsva) }}
/>
),
icon: null,
onClick: () => {
sendPaint(hsvaToHex(brushHsva), brushSize, brushHsva.a);
store.setActiveTool("draw");
},
popout: BrushPopout,
},
{
id: "eraser",
label: "Eraser",
icon: <FontAwesomeIcon icon={faEraser} className="h-5 w-5" />,
onClick: () => {
onEraser(brushSize);
},
popout: (
<div className="p-4">
<SliderWithIndicator
value={brushSize}
onChange={(size) => {
setBrushSize(size);
onEraser(size);
}}
label="Eraser Size"
/>
</div>
),
},
// Commented out for now
// {
// id: "eraser",
// label: "Eraser",
// icon: <FontAwesomeIcon icon={faEraser} className="h-5 w-5" />,
// onClick: () => {
// onEraser(brushSize);
// },
// popout: (
// <div className="p-4">
// <SliderWithIndicator
// value={brushSize}
// onChange={(size) => {
// setBrushSize(size);
// onEraser(size);
// }}
// label="Eraser Size"
// />
// </div>
// ),
// },
{
id: "delete",
label: "Delete",
@@ -323,6 +312,19 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
? "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 ? (
<span
className="inline-block h-5 w-5 rounded-full border-2 border-white"
style={{ backgroundColor: hsvaToHex(brushHsva) }}
/>
) : (
<FontAwesomeIcon icon={faPaintBrush} className="h-5 w-5" />
);
}
return (
<div key={id} className="relative">
<Tooltip
@@ -340,7 +342,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
}}
className={`${baseBtn} ${btnStyle}`}
>
{icon}
{displayIcon}
</button>
) : (
<button
@@ -349,7 +351,7 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
}}
className={`${baseBtn} ${btnStyle}`}
>
{icon}
{displayIcon}
</button>
)}
</Tooltip>
@@ -26,7 +26,7 @@ export default function SplitPane({
maxPercent = 85,
onChange,
className = "",
singlePaneMode = true
singlePaneMode = true,
}: SplitPaneProps) {
const rootRef = useRef<HTMLDivElement>(null);
const [leftPct, setLeftPct] = useState(initialPercent);
@@ -65,7 +65,7 @@ export default function SplitPane({
return singlePaneMode ? (
<div
ref={rootRef}
className={`pegboard-bg relative flex h-screen w-full overflow-hidden items-center justify-center ${className}`}
className={`pegboard-bg relative flex h-[calc(100vh-56px)] w-full items-center justify-center overflow-hidden ${className}`}
>
<div style={leftStyle} className="h-full min-w-0 overflow-hidden">
{left}
@@ -74,7 +74,7 @@ export default function SplitPane({
) : (
<div
ref={rootRef}
className={`pegboard-bg relative flex h-screen w-full overflow-hidden ${className}`}
className={`pegboard-bg relative flex h-[calc(100vh-56px)] w-full overflow-hidden ${className}`}
>
<div style={leftStyle} className="h-full min-w-0 overflow-hidden">
{left}
@@ -21,6 +21,7 @@ import {
faPlus,
faUpload,
faImages,
faExpandWide,
} from "@fortawesome/pro-solid-svg-icons";
import {
faRectangleVertical,
@@ -42,6 +43,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;
@@ -64,6 +67,7 @@ interface PromptBox2DProps {
useJobContext: () => JobContextType;
onEnqueuePressed?: () => void | Promise<void>;
onAspectRatioChange?: (ratio: AspectRatio) => void;
onFitPressed?: () => void | Promise<void>;
}
export const PromptBox2D = ({
@@ -73,6 +77,7 @@ export const PromptBox2D = ({
useJobContext,
onEnqueuePressed,
onAspectRatioChange,
onFitPressed,
}: PromptBox2DProps) => {
useSignals();
@@ -122,7 +127,7 @@ export const PromptBox2D = ({
});
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
onAspectRatioChange?.(selectedItem.label);
onAspectRatioChange?.(selectedItem.label as AspectRatio);
setAspectRatioList((prev) =>
prev.map((item) => ({
...item,
@@ -602,6 +607,22 @@ export const PromptBox2D = ({
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
/>
</Tooltip>
{onFitPressed && (
<Tooltip
content={"Fit canvas to screen"}
position="top"
className="z-50"
delay={200}
>
<Button
variant="secondary"
className="h-9 w-9"
onClick={onFitPressed}
>
<FontAwesomeIcon icon={faExpandWide} className="h-4 w-4" />
</Button>
</Tooltip>
)}
</div>
<div className="flex items-center gap-2">
<Button