mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 01:59:31 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b49e39de20 | ||
|
|
ef48e48080 | ||
|
|
926e7a0f68 |
@@ -238,24 +238,24 @@ const PageDraw = () => {
|
||||
const autoFitCanvas = async () => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20; // Increased attempts
|
||||
|
||||
|
||||
const tryFit = async () => {
|
||||
const stage = stageRef.current;
|
||||
if (stage && stage.container && stage.container().offsetWidth > 0) {
|
||||
// Wait a bit more to ensure everything is rendered
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
onFitPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return tryFit();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
await tryFit();
|
||||
};
|
||||
|
||||
@@ -315,17 +315,13 @@ const PageDraw = () => {
|
||||
onSelect={(): void => {
|
||||
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);
|
||||
}
|
||||
onActivateShapeTool={(
|
||||
shape: "rectangle" | "circle" | "triangle",
|
||||
): void => {
|
||||
store.selectNode(null);
|
||||
store.setCurrentShape(shape);
|
||||
store.setActiveTool("shape");
|
||||
store.selectNode(null);
|
||||
}}
|
||||
onPaintBrush={(hex: string, size: number, opacity: number): void => {
|
||||
store.setActiveTool("draw");
|
||||
@@ -333,10 +329,6 @@ const PageDraw = () => {
|
||||
store.setBrushSize(size);
|
||||
store.setBrushOpacity(opacity);
|
||||
}}
|
||||
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
|
||||
@@ -344,10 +336,6 @@ const PageDraw = () => {
|
||||
// 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 => {
|
||||
// Create input element dynamically like in PromptEditor
|
||||
console.log("Upload image activated");
|
||||
@@ -393,6 +381,7 @@ const PageDraw = () => {
|
||||
store.deleteSelectedItems();
|
||||
}}
|
||||
activeToolId={store.activeTool}
|
||||
currentShape={store.currentShape}
|
||||
/>
|
||||
<div className="relative z-0">
|
||||
<ContextMenuContainer
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Stage,
|
||||
Layer,
|
||||
Rect,
|
||||
Ellipse,
|
||||
Circle,
|
||||
Text,
|
||||
Line,
|
||||
@@ -97,6 +98,20 @@ export const PaintSurface = ({
|
||||
// Add state to track the current line being drawn
|
||||
const [currentLineId, setCurrentLineId] = useState<string | null>(null);
|
||||
|
||||
/* ---------- shape drawing state ---------- */
|
||||
const [isDrawingShape, setIsDrawingShape] = useState(false);
|
||||
const [currentShapeId, setCurrentShapeId] = useState<string | null>(null);
|
||||
const [shapeStartPoint, setShapeStartPoint] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const [shapePreview, setShapePreview] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
|
||||
// Add useLayoutEffect to measure container dimensions
|
||||
useLayoutEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
@@ -191,7 +206,12 @@ export const PaintSurface = ({
|
||||
activeTool: string,
|
||||
nodeDraggable: boolean,
|
||||
): boolean => {
|
||||
return activeTool !== "draw" && activeTool !== "eraser" && nodeDraggable;
|
||||
return (
|
||||
activeTool !== "draw" &&
|
||||
activeTool !== "eraser" &&
|
||||
activeTool !== "shape" &&
|
||||
nodeDraggable
|
||||
);
|
||||
};
|
||||
|
||||
// Combined mouse handlers
|
||||
@@ -271,6 +291,53 @@ export const PaintSurface = ({
|
||||
endY: clampedPoint.y,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle shape drawing tool
|
||||
if (
|
||||
activeTool === "shape" &&
|
||||
store.currentShape &&
|
||||
isWithinLeftPanel(stagePoint) &&
|
||||
e.target === e.target.getStage()
|
||||
) {
|
||||
const id = `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const commonProps = {
|
||||
id,
|
||||
x: stagePoint.x,
|
||||
y: stagePoint.y,
|
||||
width: 1,
|
||||
height: 1,
|
||||
fill: "#e0e0e0",
|
||||
stroke: "#444",
|
||||
strokeWidth: 2,
|
||||
draggable: true,
|
||||
} as const;
|
||||
|
||||
let newNode: Node;
|
||||
switch (store.currentShape) {
|
||||
case "circle":
|
||||
newNode = new Node({ ...commonProps, type: "circle" });
|
||||
break;
|
||||
case "triangle":
|
||||
newNode = new Node({ ...commonProps, type: "triangle" });
|
||||
break;
|
||||
case "rectangle":
|
||||
default:
|
||||
newNode = new Node({ ...commonProps, type: "rectangle" });
|
||||
break;
|
||||
}
|
||||
|
||||
store.addNode(newNode);
|
||||
setIsDrawingShape(true);
|
||||
setCurrentShapeId(id);
|
||||
setShapeStartPoint(stagePoint);
|
||||
setShapePreview({
|
||||
x: stagePoint.x,
|
||||
y: stagePoint.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStageMouseMove = (
|
||||
@@ -286,9 +353,9 @@ export const PaintSurface = ({
|
||||
x: (pointer.x - stage.x()) / stage.scaleX(),
|
||||
y: (pointer.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
|
||||
const isWithinCanvas = isWithinLeftPanel(stagePoint);
|
||||
|
||||
|
||||
// Update cursor based on position and tool
|
||||
if (activeTool === "draw" || activeTool === "eraser") {
|
||||
if (isWithinCanvas) {
|
||||
@@ -299,6 +366,13 @@ export const PaintSurface = ({
|
||||
stage.container().style.cursor = "grab";
|
||||
store.setCursorVisible(false);
|
||||
}
|
||||
} else if (activeTool === "shape") {
|
||||
if (isWithinCanvas) {
|
||||
stage.container().style.cursor = "crosshair";
|
||||
} else {
|
||||
stage.container().style.cursor = "grab";
|
||||
}
|
||||
store.setCursorVisible(false);
|
||||
} else {
|
||||
if (isWithinCanvas) {
|
||||
stage.container().style.cursor = "default";
|
||||
@@ -313,15 +387,15 @@ export const PaintSurface = ({
|
||||
if (isDragging) {
|
||||
const currentStage = e.target.getStage();
|
||||
if (!currentStage) return;
|
||||
|
||||
|
||||
// Update cursor to grabbing while dragging
|
||||
currentStage.container().style.cursor = "grabbing";
|
||||
|
||||
// Use a multiplier to make middle mouse dragging more responsive
|
||||
const dragSensitivity = 3.0; // Much higher sensitivity for faster dragging
|
||||
|
||||
// Use a multiplier to make middle mouse dragging more responsive
|
||||
const dragSensitivity = 3.0; // Much higher sensitivity for faster dragging
|
||||
const newPos = {
|
||||
x: currentStage.x() + (e.evt.movementX * dragSensitivity),
|
||||
y: currentStage.y() + (e.evt.movementY * dragSensitivity),
|
||||
x: currentStage.x() + e.evt.movementX * dragSensitivity,
|
||||
y: currentStage.y() + e.evt.movementY * dragSensitivity,
|
||||
};
|
||||
currentStage.position(newPos);
|
||||
return;
|
||||
@@ -362,6 +436,52 @@ export const PaintSurface = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shape resizing during drawing
|
||||
if (isDrawingShape && currentShapeId && shapeStartPoint) {
|
||||
const point = stage.getPointerPosition();
|
||||
if (!point) return;
|
||||
|
||||
const stagePoint = {
|
||||
x: (point.x - stage.x()) / stage.scaleX(),
|
||||
y: (point.y - stage.y()) / stage.scaleY(),
|
||||
};
|
||||
|
||||
// Clamp within bounds
|
||||
const clamped = clampToLeftPanel(stagePoint);
|
||||
|
||||
const start = shapeStartPoint;
|
||||
|
||||
// Deltas
|
||||
let dx = clamped.x - start.x;
|
||||
let dy = clamped.y - start.y;
|
||||
|
||||
// If Shift key is held, constrain to proportional (square / circle / equilateral triangle)
|
||||
const shiftHeld = (e.evt as MouseEvent).shiftKey;
|
||||
const keepSquare = shiftHeld;
|
||||
if (keepSquare) {
|
||||
const side = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
dx = dx < 0 ? -side : side;
|
||||
dy = dy < 0 ? -side : side;
|
||||
}
|
||||
|
||||
// Compute top-left corner & positive width/height
|
||||
let newX = dx < 0 ? start.x + dx : start.x;
|
||||
let newY = dy < 0 ? start.y + dy : start.y;
|
||||
const newWidth = Math.abs(dx);
|
||||
const newHeight = Math.abs(dy);
|
||||
|
||||
const updateProps = {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
|
||||
store.updateNode(currentShapeId, updateProps, false);
|
||||
|
||||
setShapePreview({ x: newX, y: newY, width: newWidth, height: newHeight });
|
||||
}
|
||||
|
||||
// Handle selection rectangle - Update directly through Konva
|
||||
if (isSelecting && selectionRectRef.current) {
|
||||
const point = stage.getPointerPosition();
|
||||
@@ -403,6 +523,13 @@ export const PaintSurface = ({
|
||||
store.saveState(); // Save state only when the stroke is complete
|
||||
}
|
||||
|
||||
if (isDrawingShape && currentShapeId) {
|
||||
store.saveState();
|
||||
// Automatically return to select tool after shape creation and select the new node
|
||||
store.setActiveTool("select");
|
||||
store.selectNode(currentShapeId);
|
||||
}
|
||||
|
||||
if (isSelecting && selectionRect) {
|
||||
// Calculate the bounds of the selection rectangle
|
||||
const left = Math.min(selectionRect.startX, selectionRect.endX);
|
||||
@@ -461,10 +588,16 @@ export const PaintSurface = ({
|
||||
onSelectionChange?.(false);
|
||||
setSelectionRect(null);
|
||||
setLastPoint(null);
|
||||
setIsDrawingShape(false);
|
||||
setCurrentShapeId(null);
|
||||
setShapeStartPoint(null);
|
||||
setShapePreview(null);
|
||||
};
|
||||
|
||||
// State for tracking pinch gestures
|
||||
const [lastPinchDistance, setLastPinchDistance] = useState<number | null>(null);
|
||||
const [lastPinchDistance, setLastPinchDistance] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [isPinching, setIsPinching] = useState(false);
|
||||
|
||||
// Helper function to get distance between two touch points
|
||||
@@ -473,8 +606,8 @@ export const PaintSurface = ({
|
||||
const touch1 = touches[0];
|
||||
const touch2 = touches[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2)
|
||||
Math.pow(touch2.clientX - touch1.clientX, 2) +
|
||||
Math.pow(touch2.clientY - touch1.clientY, 2),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -506,16 +639,16 @@ export const PaintSurface = ({
|
||||
const isPinchGesture = e.evt.ctrlKey;
|
||||
const deltaY = e.evt.deltaY;
|
||||
const absDelta = Math.abs(deltaY);
|
||||
|
||||
|
||||
// Detect Mac trackpad vs mouse wheel more precisely
|
||||
const isMac = navigator.userAgent.includes('Mac');
|
||||
const isMac = navigator.userAgent.includes("Mac");
|
||||
|
||||
// Track-pad scroll on Mac: pixel-based (deltaMode===0) and very small deltas (|delta|≤10)
|
||||
const isMacTrackpadScroll =
|
||||
isMac && !isPinchGesture && e.evt.deltaMode === 0 && absDelta <= 10;
|
||||
|
||||
|
||||
let zoomFactor;
|
||||
|
||||
|
||||
if (isPinchGesture) {
|
||||
// Mac trackpad pinch gesture – fast & smooth
|
||||
const pinchSensitivity = 0.2;
|
||||
@@ -531,7 +664,7 @@ export const PaintSurface = ({
|
||||
|
||||
// Apply zoom limits
|
||||
const newScale = Math.max(0.1, Math.min(10, oldScale * zoomFactor));
|
||||
|
||||
|
||||
// Only apply zoom if within limits
|
||||
if (newScale !== oldScale) {
|
||||
// Calculate new position for zoom-to-cursor
|
||||
@@ -558,37 +691,37 @@ export const PaintSurface = ({
|
||||
const touches = e.evt.touches;
|
||||
if (touches.length === 2 && isPinching && lastPinchDistance) {
|
||||
e.evt.preventDefault();
|
||||
|
||||
|
||||
const stage = e.target.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
|
||||
const currentDistance = getTouchDistance(touches);
|
||||
const center = getTouchCenter(touches);
|
||||
if (!center) return;
|
||||
|
||||
|
||||
const oldScale = stage.scaleX();
|
||||
const scaleChange = currentDistance / lastPinchDistance;
|
||||
|
||||
|
||||
// Apply smooth scaling with limits
|
||||
const newScale = Math.max(0.1, Math.min(10, oldScale * scaleChange));
|
||||
|
||||
|
||||
if (newScale !== oldScale) {
|
||||
// Get stage coordinates for the center point
|
||||
const stageCenter = {
|
||||
x: (center.x - stage.x()) / oldScale,
|
||||
y: (center.y - stage.y()) / oldScale,
|
||||
};
|
||||
|
||||
|
||||
// Calculate new position to zoom toward the center of the pinch
|
||||
const newPos = {
|
||||
x: center.x - stageCenter.x * newScale,
|
||||
y: center.y - stageCenter.y * newScale,
|
||||
};
|
||||
|
||||
|
||||
stage.scale({ x: newScale, y: newScale });
|
||||
stage.position(newPos);
|
||||
}
|
||||
|
||||
|
||||
setLastPinchDistance(currentDistance);
|
||||
}
|
||||
};
|
||||
@@ -717,8 +850,12 @@ export const PaintSurface = ({
|
||||
e: Konva.KonvaEventObject<MouseEvent | TouchEvent>,
|
||||
nodeId: string,
|
||||
) => {
|
||||
// Don't select nodes when draw or erase tools are active
|
||||
if (activeTool === "draw" || activeTool === "eraser") {
|
||||
// Don't select nodes when draw, erase, or shape tools are active
|
||||
if (
|
||||
activeTool === "draw" ||
|
||||
activeTool === "eraser" ||
|
||||
activeTool === "shape"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -766,7 +903,16 @@ export const PaintSurface = ({
|
||||
targetNode.lastX = targetNode.x();
|
||||
targetNode.lastY = targetNode.y();
|
||||
|
||||
store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, false);
|
||||
let newX = targetNode.x();
|
||||
let newY = targetNode.y();
|
||||
const draggedNode =
|
||||
nodes.find((n) => n.id === nodeId) ||
|
||||
(store.lineNodes as any).find((n: any) => n.id === nodeId);
|
||||
if (draggedNode && draggedNode.type === "circle") {
|
||||
newX = targetNode.x() - draggedNode.width / 2;
|
||||
newY = targetNode.y() - draggedNode.height / 2;
|
||||
}
|
||||
store.moveNode(nodeId, newX, newY, 0, 0, false);
|
||||
};
|
||||
|
||||
const handleNodeDragMove = (
|
||||
@@ -786,7 +932,16 @@ export const PaintSurface = ({
|
||||
targetNode.lastX = targetNode.x();
|
||||
targetNode.lastY = targetNode.y();
|
||||
|
||||
store.moveNode(nodeId, targetNode.x(), targetNode.y(), dx, dy, false);
|
||||
let newXMove = targetNode.x();
|
||||
let newYMove = targetNode.y();
|
||||
const movingNode =
|
||||
nodes.find((n) => n.id === nodeId) ||
|
||||
(store.lineNodes as any).find((n: any) => n.id === nodeId);
|
||||
if (movingNode && movingNode.type === "circle") {
|
||||
newXMove = targetNode.x() - movingNode.width / 2;
|
||||
newYMove = targetNode.y() - movingNode.height / 2;
|
||||
}
|
||||
store.moveNode(nodeId, newXMove, newYMove, dx, dy, false);
|
||||
};
|
||||
|
||||
const handleNodeDragEnd = (
|
||||
@@ -800,7 +955,16 @@ export const PaintSurface = ({
|
||||
targetNode.lastX = targetNode.x();
|
||||
targetNode.lastY = targetNode.y();
|
||||
|
||||
store.moveNode(nodeId, targetNode.x(), targetNode.y(), 0, 0, true);
|
||||
let endX = targetNode.x();
|
||||
let endY = targetNode.y();
|
||||
const endNode =
|
||||
nodes.find((n) => n.id === nodeId) ||
|
||||
(store.lineNodes as any).find((n: any) => n.id === nodeId);
|
||||
if (endNode && endNode.type === "circle") {
|
||||
endX = targetNode.x() - endNode.width / 2;
|
||||
endY = targetNode.y() - endNode.height / 2;
|
||||
}
|
||||
store.moveNode(nodeId, endX, endY, 0, 0, true);
|
||||
};
|
||||
|
||||
const isSelected = selectedNodeIds.includes(node.id);
|
||||
@@ -898,6 +1062,8 @@ export const PaintSurface = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
const listeningEnabled = activeTool !== "shape";
|
||||
|
||||
if (node.type === "line") {
|
||||
const lineNode = node as LineNode;
|
||||
|
||||
@@ -977,6 +1143,7 @@ export const PaintSurface = ({
|
||||
offsetX={lineNode.offsetX || 0}
|
||||
offsetY={lineNode.offsetY || 0}
|
||||
zIndex={lineNode.zIndex}
|
||||
listening={listeningEnabled}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -986,12 +1153,12 @@ export const PaintSurface = ({
|
||||
if (node.type === "circle") {
|
||||
return (
|
||||
<React.Fragment key={node.id}>
|
||||
<Circle
|
||||
<Ellipse
|
||||
id={node.id}
|
||||
x={node.x}
|
||||
y={node.y}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
x={node.x + node.width / 2}
|
||||
y={node.y + node.height / 2}
|
||||
radiusX={node.width / 2}
|
||||
radiusY={node.height / 2}
|
||||
fill={node.fill}
|
||||
stroke={node.stroke}
|
||||
strokeWidth={0}
|
||||
@@ -1012,6 +1179,7 @@ export const PaintSurface = ({
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
listening={listeningEnabled}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -1047,6 +1215,7 @@ export const PaintSurface = ({
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
listening={listeningEnabled}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -1054,14 +1223,23 @@ export const PaintSurface = ({
|
||||
}
|
||||
|
||||
if (node.type === "triangle") {
|
||||
const points = [
|
||||
0,
|
||||
node.height, // bottom-left
|
||||
node.width / 2,
|
||||
0, // top-center
|
||||
node.width,
|
||||
node.height, // bottom-right
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment key={node.id}>
|
||||
<RegularPolygon
|
||||
<Line
|
||||
id={node.id}
|
||||
x={node.x}
|
||||
y={node.y}
|
||||
sides={3}
|
||||
radius={Math.min(node.width, node.height) / 2}
|
||||
points={points}
|
||||
closed
|
||||
fill={node.fill}
|
||||
stroke={node.stroke}
|
||||
strokeWidth={0}
|
||||
@@ -1082,6 +1260,7 @@ export const PaintSurface = ({
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
listening={listeningEnabled}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -1133,6 +1312,7 @@ export const PaintSurface = ({
|
||||
onDragMove={(e) => handleNodeDragMove(e, node.id)}
|
||||
onDragStart={(e) => handleNodeDragStart(e, node.id)}
|
||||
onDragEnd={(e) => handleNodeDragEnd(e, node.id)}
|
||||
listening={listeningEnabled}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -1251,6 +1431,20 @@ export const PaintSurface = ({
|
||||
cornerRadius={2}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shape preview bounding box */}
|
||||
{shapePreview && (
|
||||
<Rect
|
||||
x={shapePreview.x}
|
||||
y={shapePreview.y}
|
||||
width={shapePreview.width}
|
||||
height={shapePreview.height}
|
||||
stroke="#3b82f6"
|
||||
dash={[4, 4]}
|
||||
strokeWidth={1}
|
||||
listening={false}
|
||||
/>
|
||||
)}
|
||||
</Layer>
|
||||
<Layer ref={cursorLayerRef} listening={false} draggable={false}>
|
||||
<Circle ref={cursorShapeRef} visible={false} />
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
faImage,
|
||||
faSquare,
|
||||
faCircle,
|
||||
faPlay,
|
||||
faUndo,
|
||||
faRedo,
|
||||
faPaintBrush,
|
||||
faTriangle,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import "../../App.css";
|
||||
import { HsvaColorPicker, HsvaColor } from "react-colorful";
|
||||
@@ -38,7 +38,8 @@ function useDebounced<T extends (...args: A) => void, A extends unknown[]>(
|
||||
|
||||
export interface SideToolbarProps {
|
||||
onSelect: () => void;
|
||||
onAddShape: (shape: "rectangle" | "circle" | "triangle") => void;
|
||||
onActivateShapeTool: (shape: "rectangle" | "circle" | "triangle") => void;
|
||||
currentShape: "rectangle" | "circle" | "triangle" | null;
|
||||
onPaintBrush: (hex: string, size: number, opacity: number) => void;
|
||||
onCanvasBackground: (hex: string) => void;
|
||||
onUploadImage: () => void;
|
||||
@@ -49,7 +50,8 @@ export interface SideToolbarProps {
|
||||
|
||||
const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
onSelect,
|
||||
onAddShape,
|
||||
onActivateShapeTool,
|
||||
currentShape,
|
||||
onPaintBrush,
|
||||
onCanvasBackground,
|
||||
onUploadImage,
|
||||
@@ -153,21 +155,23 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
id: "add-shape",
|
||||
label: "Add Shape",
|
||||
icon: <FontAwesomeIcon icon={faShapes} className="h-5 w-5" />,
|
||||
onClick: () => {
|
||||
// Activate shape tool using currently selected shape if any and clear selection
|
||||
store.selectNode(null);
|
||||
if (!store.currentShape) {
|
||||
store.setCurrentShape("rectangle");
|
||||
}
|
||||
store.setActiveTool("shape");
|
||||
},
|
||||
popout: (
|
||||
<div
|
||||
className={`flex items-center gap-1.5 rounded-full px-1.5 py-1.5 shadow-lg`}
|
||||
>
|
||||
{[
|
||||
faSquare,
|
||||
faCircle,
|
||||
faPlay, // triangle
|
||||
].map((faIcon, i) => (
|
||||
<div className="flex items-center gap-1.5 rounded-full px-1.5 py-1.5 shadow-lg">
|
||||
{[faSquare, faCircle, faTriangle].map((faIcon, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={shapeIconBtn}
|
||||
onClick={() => {
|
||||
const shapes = ["rectangle", "circle", "triangle"] as const;
|
||||
onAddShape(shapes[i]);
|
||||
onActivateShapeTool(shapes[i]);
|
||||
setOpen(null);
|
||||
}}
|
||||
>
|
||||
@@ -177,14 +181,6 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// id: "generate",
|
||||
// label: "Generate Image",
|
||||
// icon: <FontAwesomeIcon icon={faSparkles} className="h-5 w-5" />,
|
||||
// onClick: () => {
|
||||
// onGenerateImage();
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "upload",
|
||||
label: "Upload Image",
|
||||
@@ -204,27 +200,6 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
},
|
||||
popout: BrushPopout,
|
||||
},
|
||||
// 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",
|
||||
@@ -261,36 +236,6 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
store.redo(); // Assuming store has a redo method
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: "save-scene",
|
||||
// label: "Save Scene",
|
||||
// icon: <FontAwesomeIcon icon={faFileExport} className="h-5 w-5" />,
|
||||
// onClick: () => {
|
||||
// store.saveSceneToFile();
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// id: "load-scene",
|
||||
// label: "Load Scene",
|
||||
// icon: <FontAwesomeIcon icon={faFileImport} className="h-5 w-5" />,
|
||||
// onClick: () => {
|
||||
// const input = document.createElement("input");
|
||||
// input.type = "file";
|
||||
// input.accept = ".json";
|
||||
// input.onchange = async (e: Event) => {
|
||||
// const target = e.target as HTMLInputElement;
|
||||
// if (target.files && target.files[0]) {
|
||||
// const success = await store.loadSceneFromFile(target.files[0]);
|
||||
// if (success) {
|
||||
// console.log("Scene loaded successfully");
|
||||
// } else {
|
||||
// console.error("Failed to load scene");
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// input.click();
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
/* ------------------------------------------------ render ---------- */
|
||||
@@ -307,7 +252,9 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
}
|
||||
|
||||
const { id, icon, onClick, popout, label } = tool;
|
||||
const active = id === activeToolId;
|
||||
const active =
|
||||
id === activeToolId ||
|
||||
(id === "add-shape" && activeToolId === "shape");
|
||||
const btnStyle = active
|
||||
? "bg-primary/30 border-2 !border-primary text-white"
|
||||
: "hover:bg-white/10 text-white";
|
||||
@@ -325,6 +272,21 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamic icon for shape tool
|
||||
if (id === "add-shape" && activeToolId === "shape" && currentShape) {
|
||||
const shapeIcons = {
|
||||
rectangle: faSquare,
|
||||
circle: faCircle,
|
||||
triangle: faTriangle,
|
||||
} as const;
|
||||
displayIcon = (
|
||||
<FontAwesomeIcon
|
||||
icon={shapeIcons[currentShape]}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className="relative">
|
||||
<Tooltip
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user