mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c96f21bd7 | ||
|
|
245409de08 | ||
|
|
90d0f15176 | ||
|
|
257db9b892 | ||
|
|
70348a922a | ||
|
|
95e724e4bd | ||
|
|
21ea7d05e2 | ||
|
|
e8305bd112 |
@@ -20,6 +20,7 @@ export interface NodeProps {
|
||||
scaleY?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export class Node {
|
||||
@@ -42,6 +43,7 @@ export class Node {
|
||||
scaleY: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
zIndex: number;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
@@ -62,7 +64,8 @@ export class Node {
|
||||
scaleX = 1,
|
||||
scaleY = 1,
|
||||
offsetX = 0,
|
||||
offsetY = 0
|
||||
offsetY = 0,
|
||||
zIndex = 0
|
||||
}: NodeProps) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
@@ -83,6 +86,7 @@ export class Node {
|
||||
this.scaleY = scaleY;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.zIndex = zIndex;
|
||||
}
|
||||
|
||||
setPosition(x: number, y: number): void {
|
||||
|
||||
@@ -14,8 +14,8 @@ 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";
|
||||
const PageDraw = () => {
|
||||
//useStateSceneLoader();
|
||||
@@ -271,23 +271,58 @@ const PageDraw = () => {
|
||||
activeToolId={store.activeTool}
|
||||
/>
|
||||
<div className="relative z-0">
|
||||
<ContextMenuContainer>
|
||||
<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>
|
||||
<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
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
onMenuAction={(action) => {
|
||||
|
||||
switch (action) {
|
||||
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;
|
||||
default:
|
||||
// No action needed for unhandled cases
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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
|
||||
|
||||
@@ -774,6 +774,7 @@ export const PaintSurface = ({
|
||||
rotation={lineNode.rotation || 0}
|
||||
offsetX={lineNode.offsetX || 0}
|
||||
offsetY={lineNode.offsetY || 0}
|
||||
zIndex={lineNode.zIndex}
|
||||
/>
|
||||
{renderTransformer()}
|
||||
</React.Fragment>
|
||||
@@ -791,12 +792,13 @@ export const PaintSurface = ({
|
||||
height={node.height}
|
||||
fill={node.fill}
|
||||
stroke={node.stroke}
|
||||
strokeWidth={node.strokeWidth}
|
||||
strokeWidth={0}
|
||||
rotation={node.rotation || 0}
|
||||
scaleX={node.scaleX || 1}
|
||||
scaleY={node.scaleY || 1}
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
@@ -822,12 +824,13 @@ export const PaintSurface = ({
|
||||
height={node.height}
|
||||
fill={node.fill}
|
||||
stroke={node.stroke}
|
||||
strokeWidth={node.strokeWidth}
|
||||
strokeWidth={0}
|
||||
rotation={node.rotation || 0}
|
||||
scaleX={node.scaleX || 1}
|
||||
scaleY={node.scaleY || 1}
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
@@ -853,12 +856,13 @@ export const PaintSurface = ({
|
||||
radius={Math.min(node.width, node.height) / 2}
|
||||
fill={node.fill}
|
||||
stroke={node.stroke}
|
||||
strokeWidth={node.strokeWidth}
|
||||
strokeWidth={0}
|
||||
rotation={node.rotation || 0}
|
||||
scaleX={node.scaleX || 1}
|
||||
scaleY={node.scaleY || 1}
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
zIndex={node.zIndex}
|
||||
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onTap={(e) => handleNodeMouseDown(e, node.id)}
|
||||
@@ -892,6 +896,7 @@ export const PaintSurface = ({
|
||||
offsetX={node.offsetX || 0}
|
||||
offsetY={node.offsetY || 0}
|
||||
listening={false}
|
||||
zIndex={node.zIndex}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
@@ -999,7 +1004,12 @@ export const PaintSurface = ({
|
||||
/>
|
||||
|
||||
{/* Render all nodes including line nodes */}
|
||||
{[...nodes, ...store.lineNodes].map((node) => renderNode(node))}
|
||||
{[...nodes, ...store.lineNodes]
|
||||
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
|
||||
.map((node, index) => {
|
||||
// console.log(`Node ${index}:`, node);
|
||||
return renderNode(node);
|
||||
})}
|
||||
|
||||
{/* Render selection rectangle */}
|
||||
{selectionRect && (
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface MenuPosition {
|
||||
export interface MenuPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
export interface MenuItem {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
action: string; // Instead of onClick, we use an action identifier
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
items: MenuItem[];
|
||||
onAction: (action: string) => void; // Single callback for all menu items
|
||||
onClose: () => void;
|
||||
position: MenuPosition;
|
||||
}
|
||||
|
||||
export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, position }) => {
|
||||
export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClose, position }) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,7 +46,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, positi
|
||||
<React.Fragment key={index}>
|
||||
<button
|
||||
onClick={() => {
|
||||
item.onClick();
|
||||
onAction(item.action); // Pass the action identifier up
|
||||
onClose();
|
||||
}}
|
||||
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-[#333333] text-[#ECECEC] text-[13px] transition-colors duration-75 ease-in-out"
|
||||
@@ -62,109 +63,56 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, positi
|
||||
);
|
||||
};
|
||||
|
||||
// Example usage with icons
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 3L4 7.5L12 12L20 7.5L12 3Z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4 16.5L12 21L20 16.5" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4 12L12 16.5L20 12" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Convert to 3D',
|
||||
onClick: () => console.log('Convert to 3D')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M4 4v16h16M4 20l16-16" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Remove background',
|
||||
onClick: () => console.log('Remove background')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 10V14M8 6H16C17.1046 6 18 6.89543 18 8V16C18 17.1046 17.1046 18 16 18H8C6.89543 18 6 17.1046 6 16V8C6 6.89543 6.89543 6 8 6Z" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Lock',
|
||||
onClick: () => console.log('Lock'),
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M5 15L12 8L19 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Bring to front',
|
||||
onClick: () => console.log('Bring to front')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M8 14L12 10L16 14" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Bring forward',
|
||||
onClick: () => console.log('Bring forward')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M8 10L12 14L16 10" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Send backward',
|
||||
onClick: () => console.log('Send backward')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M5 9L12 16L19 9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Send to back',
|
||||
onClick: () => console.log('Send to back'),
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M16 8V6M16 8H8M16 8V16M8 8V6M8 8V16M8 16H6M8 16H16M16 16H18M6 6H8M8 6H16M16 6H18" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Duplicate',
|
||||
onClick: () => console.log('Duplicate')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M7 16L17 8M7 8L17 16" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Flip Horizontal',
|
||||
onClick: () => console.log('Flip Horizontal')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M8 7L16 17M8 17L16 7" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Flip Vertical',
|
||||
onClick: () => console.log('Flip Vertical')
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M4 12V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H12" strokeWidth="2" strokeLinecap="round"/>
|
||||
<path d="M9 15H4V20" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Fit to screen',
|
||||
onClick: () => console.log('Fit to screen')
|
||||
}
|
||||
];
|
||||
interface ContextMenuContainerProps {
|
||||
children: React.ReactNode;
|
||||
items?: MenuItem[];
|
||||
onAction?: (e: React.MouseEvent, actionName: string) => boolean | void;
|
||||
onMenuAction?: (action: string) => void; // New prop for menu item actions
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Container component that handles the right-click
|
||||
export const ContextMenuContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
|
||||
children,
|
||||
items = defaultMenuItems,
|
||||
onAction,
|
||||
onMenuAction,
|
||||
className = ''
|
||||
}) => {
|
||||
const [showMenu, setShowMenu] = React.useState(false);
|
||||
const [position, setPosition] = React.useState<MenuPosition>({ x: 0, y: 0 });
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setPosition({ x: e.clientX, y: e.clientY });
|
||||
setShowMenu(true);
|
||||
const newPosition = { x: e.clientX, y: e.clientY };
|
||||
setPosition(newPosition);
|
||||
|
||||
const shouldShowMenu = onAction?.(e, 'contextMenu');
|
||||
if (shouldShowMenu !== undefined) {
|
||||
setShowMenu(shouldShowMenu);
|
||||
} else {
|
||||
setShowMenu(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
onAction?.(e, 'click');
|
||||
};
|
||||
|
||||
const handleMenuAction = (action: string) => {
|
||||
onMenuAction?.(action);
|
||||
};
|
||||
|
||||
return (
|
||||
<div onContextMenu={handleContextMenu} className="w-full h-full">
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={`w-full h-full ${className}`}
|
||||
>
|
||||
{children}
|
||||
{showMenu && (
|
||||
<ContextMenu
|
||||
items={menuItems}
|
||||
items={items}
|
||||
onAction={handleMenuAction}
|
||||
position={position}
|
||||
onClose={() => setShowMenu(false)}
|
||||
/>
|
||||
@@ -172,3 +120,59 @@ export const ContextMenuContainer: React.FC<{ children: React.ReactNode }> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Default menu items now use action identifiers instead of callbacks
|
||||
const defaultMenuItems: MenuItem[] = [
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M4 4v16h16M4 20l16-16" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Remove background',
|
||||
action: 'REMOVE_BACKGROUND'
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 10V14M8 6H16C17.1046 6 18 6.89543 18 8V16C18 17.1046 17.1046 18 16 18H8C6.89543 18 6 17.1046 6 16V8C6 6.89543 6.89543 6 8 6Z" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Lock',
|
||||
action: 'LOCK',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M5 15L12 8L19 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Bring to front',
|
||||
action: 'BRING_TO_FRONT'
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M8 14L12 10L16 14" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Bring forward',
|
||||
action: 'BRING_FORWARD'
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M8 10L12 14L16 10" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Send backward',
|
||||
action: 'SEND_BACKWARD'
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M5 9L12 16L19 9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>,
|
||||
label: 'Send to back',
|
||||
action: 'SEND_TO_BACK',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M16 8V6M16 8H8M16 8V16M8 8V6M8 8V16M8 16H6M8 16H16M16 16H18M6 6H8M8 6H16M16 6H18" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>,
|
||||
label: 'Duplicate',
|
||||
action: 'DUPLICATE'
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export type LineNode = {
|
||||
scaleY?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
zIndex: number;
|
||||
};
|
||||
|
||||
export type ActiveTool = 'select' | 'draw' | 'eraser' | 'backgroundColor' | 'shape';
|
||||
@@ -103,6 +104,12 @@ interface SceneState {
|
||||
// Add these two properties
|
||||
serializeSceneToString: () => Promise<string>;
|
||||
loadSceneFromString: (jsonString: string) => boolean;
|
||||
|
||||
// New functions
|
||||
bringToFront: (nodeIds: string[]) => void;
|
||||
sendToBack: (nodeIds: string[]) => void;
|
||||
bringForward: (nodeIds: string[]) => void;
|
||||
sendBackward: (nodeIds: string[]) => void;
|
||||
}
|
||||
|
||||
const generateId = (): string => {
|
||||
@@ -112,6 +119,22 @@ const generateId = (): string => {
|
||||
// Add a flag to prevent saving state during restoration
|
||||
let isRestoring = false;
|
||||
|
||||
// Add this helper function at the top of the store (before create<SceneState>)
|
||||
const getNextZIndex = (nodes: Node[], lineNodes: LineNode[]): number => {
|
||||
const allZIndices = [
|
||||
...nodes.map((n, index) => {
|
||||
//console.log(`Node: ${JSON.stringify(n)}, Index: ${index}`);
|
||||
return n.zIndex || 0;
|
||||
}),
|
||||
...lineNodes.map((n, index) => {
|
||||
//console.log(`LineNode: ${JSON.stringify(n)}, Index: ${index}`);
|
||||
return n.zIndex || 0;
|
||||
})
|
||||
];
|
||||
|
||||
return allZIndices.length > 0 ? Math.max(...allZIndices) + 1 : 0;
|
||||
};
|
||||
|
||||
export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Initial state
|
||||
nodes: [],
|
||||
@@ -135,8 +158,17 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Actions
|
||||
addNode: (node: Node) => {
|
||||
set((state) => {
|
||||
const newNodes = [...state.nodes, node];
|
||||
return { nodes: newNodes };
|
||||
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
|
||||
console.log(`NextZ for node ID ${node.id}: ${nextZ}`);
|
||||
const newNode = new Node({
|
||||
...node,
|
||||
zIndex: nextZ
|
||||
});
|
||||
// console.log("New Node with ID:", newNode.id);
|
||||
let nodes = [...state.nodes, newNode]
|
||||
console.log("Nodes after update")
|
||||
console.log(nodes)
|
||||
return { nodes: nodes };
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
@@ -161,9 +193,17 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
|
||||
updateNode: (id: string, updates: Partial<Node>, shouldSaveState: boolean = true) => {
|
||||
set((state) => {
|
||||
|
||||
const newNodes = state.nodes.map(node => {
|
||||
let newZIndex = updates.zIndex !== undefined ? updates.zIndex : node.zIndex
|
||||
|
||||
|
||||
if (node.id === id) {
|
||||
return new Node({ ...node, ...updates });
|
||||
return new Node({
|
||||
...node,
|
||||
...updates,
|
||||
zIndex: newZIndex
|
||||
});
|
||||
}
|
||||
return node;
|
||||
});
|
||||
@@ -451,8 +491,13 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Add new line node actions
|
||||
addLineNode: (lineNode: LineNode) => {
|
||||
set((state) => {
|
||||
const newLineNodes = [...state.lineNodes, lineNode];
|
||||
return { lineNodes: newLineNodes };
|
||||
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
|
||||
|
||||
const newLineNode = {
|
||||
...lineNode,
|
||||
zIndex: nextZ
|
||||
};
|
||||
return { lineNodes: [...state.lineNodes, newLineNode] };
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
@@ -479,7 +524,11 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
set((state) => {
|
||||
const newLineNodes = state.lineNodes.map(node => {
|
||||
if (node.id === id) {
|
||||
return { ...node, ...updates };
|
||||
return {
|
||||
...node,
|
||||
...updates,
|
||||
zIndex: updates.zIndex !== undefined ? updates.zIndex : node.zIndex
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
@@ -585,11 +634,11 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
// Load the image asynchronously
|
||||
node.setImage(source).then(() => {
|
||||
// Update the node in the store after image loads
|
||||
console.log('Image loaded for node:', node.id);
|
||||
get().updateNode(node.id, node, false);
|
||||
get().addNode(node);
|
||||
//get().updateNode(node.id, node, false);
|
||||
}).catch((error)=>console.error('Error loading image:', error));
|
||||
|
||||
get().addNode(node);
|
||||
|
||||
},
|
||||
|
||||
// Action for deleting selected items
|
||||
@@ -888,5 +937,198 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
},
|
||||
loadSceneFromString: (jsonString: string): boolean => {
|
||||
return get().importSceneFromJson(jsonString);
|
||||
}
|
||||
},
|
||||
|
||||
// New functions
|
||||
bringToFront: (nodeIds: string[]) => {
|
||||
set(state => {
|
||||
// Get highest zIndex from all nodes and line nodes
|
||||
const allZIndices = [
|
||||
...state.nodes.map(n => n.zIndex || 0),
|
||||
...state.lineNodes.map(n => n.zIndex || 0)
|
||||
];
|
||||
const maxZ = Math.max(...allZIndices, 0) + 1;
|
||||
|
||||
return {
|
||||
nodes: state.nodes.map(node =>
|
||||
nodeIds.includes(node.id)
|
||||
? new Node({ ...node, zIndex: maxZ })
|
||||
: node
|
||||
),
|
||||
lineNodes: state.lineNodes.map(node =>
|
||||
nodeIds.includes(node.id)
|
||||
? { ...node, zIndex: maxZ }
|
||||
: node
|
||||
)
|
||||
};
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
|
||||
sendToBack: (nodeIds: string[]) => {
|
||||
set(state => {
|
||||
// Get lowest zIndex from all nodes and line nodes
|
||||
const allZIndices = [
|
||||
...state.nodes.map(n => n.zIndex || 0),
|
||||
...state.lineNodes.map(n => n.zIndex || 0)
|
||||
];
|
||||
const minZ = Math.min(...allZIndices, 0) - 1;
|
||||
|
||||
return {
|
||||
nodes: state.nodes.map(node =>
|
||||
nodeIds.includes(node.id)
|
||||
? new Node({ ...node, zIndex: minZ })
|
||||
: node
|
||||
),
|
||||
lineNodes: state.lineNodes.map(node =>
|
||||
nodeIds.includes(node.id)
|
||||
? { ...node, zIndex: minZ }
|
||||
: node
|
||||
)
|
||||
};
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
|
||||
bringForward: (nodeIds: string[]) => {
|
||||
set(state => {
|
||||
// Combine all items with their current zIndex
|
||||
const allItems = [
|
||||
...state.nodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) })),
|
||||
...state.lineNodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) }))
|
||||
].sort((a, b) => a.z - b.z);
|
||||
|
||||
// Find the highest z-index among selected items
|
||||
const selectedItems = allItems.filter(item => item.selected);
|
||||
if (selectedItems.length === 0) return state;
|
||||
|
||||
const highestSelectedZ = Math.max(...selectedItems.map(item => item.z));
|
||||
|
||||
// Find the closest item above our selection
|
||||
const nextItem = allItems.find(item => !item.selected && item.z > highestSelectedZ);
|
||||
|
||||
// If no item above, swap with the highest non-selected item
|
||||
if (!nextItem) {
|
||||
const highestNonSelected = allItems.filter(item => !item.selected)
|
||||
.sort((a, b) => b.z - a.z)[0];
|
||||
if (!highestNonSelected) return state;
|
||||
|
||||
// Swap positions
|
||||
return {
|
||||
nodes: state.nodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return new Node({ ...node, zIndex: highestNonSelected.z });
|
||||
}
|
||||
if (node.id === highestNonSelected.id) {
|
||||
return new Node({ ...node, zIndex: highestSelectedZ });
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
lineNodes: state.lineNodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return { ...node, zIndex: highestNonSelected.z };
|
||||
}
|
||||
if (node.id === highestNonSelected.id) {
|
||||
return { ...node, zIndex: highestSelectedZ };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const newZ = nextItem.z;
|
||||
return {
|
||||
nodes: state.nodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return new Node({ ...node, zIndex: newZ });
|
||||
}
|
||||
if (node.id === nextItem.id) {
|
||||
return new Node({ ...node, zIndex: highestSelectedZ });
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
lineNodes: state.lineNodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return { ...node, zIndex: newZ };
|
||||
}
|
||||
if (node.id === nextItem.id) {
|
||||
return { ...node, zIndex: highestSelectedZ };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
};
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
|
||||
sendBackward: (nodeIds: string[]) => {
|
||||
set(state => {
|
||||
// Combine all items with their current zIndex
|
||||
const allItems = [
|
||||
...state.nodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) })),
|
||||
...state.lineNodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) }))
|
||||
].sort((a, b) => b.z - a.z);
|
||||
|
||||
// Find the lowest z-index among selected items
|
||||
const selectedItems = allItems.filter(item => item.selected);
|
||||
if (selectedItems.length === 0) return state;
|
||||
|
||||
const lowestSelectedZ = Math.min(...selectedItems.map(item => item.z));
|
||||
|
||||
// Find the closest item below our selection
|
||||
const nextItem = allItems.find(item => !item.selected && item.z < lowestSelectedZ);
|
||||
|
||||
// If no item below, swap with the lowest non-selected item
|
||||
if (!nextItem) {
|
||||
const lowestNonSelected = allItems.filter(item => !item.selected)
|
||||
.sort((a, b) => a.z - b.z)[0];
|
||||
if (!lowestNonSelected) return state;
|
||||
|
||||
// Swap positions
|
||||
return {
|
||||
nodes: state.nodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return new Node({ ...node, zIndex: lowestNonSelected.z });
|
||||
}
|
||||
if (node.id === lowestNonSelected.id) {
|
||||
return new Node({ ...node, zIndex: lowestSelectedZ });
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
lineNodes: state.lineNodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return { ...node, zIndex: lowestNonSelected.z };
|
||||
}
|
||||
if (node.id === lowestNonSelected.id) {
|
||||
return { ...node, zIndex: lowestSelectedZ };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const newZ = nextItem.z;
|
||||
return {
|
||||
nodes: state.nodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return new Node({ ...node, zIndex: newZ });
|
||||
}
|
||||
if (node.id === nextItem.id) {
|
||||
return new Node({ ...node, zIndex: lowestSelectedZ });
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
lineNodes: state.lineNodes.map(node => {
|
||||
if (nodeIds.includes(node.id)) {
|
||||
return { ...node, zIndex: newZ };
|
||||
}
|
||||
if (node.id === nextItem.id) {
|
||||
return { ...node, zIndex: lowestSelectedZ };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
};
|
||||
});
|
||||
get().saveState();
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user