mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 00:05:54 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04dcac98e3 | ||
|
|
8be5013260 | ||
|
|
3c9b81c3ea | ||
|
|
9d1b7d22ee | ||
|
|
b046f6ff2a |
@@ -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();
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ControlsTopButtons } from "./comps/ControlsTopButtons";
|
||||
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
|
||||
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
|
||||
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
import { authentication, pageHeight, pageWidth } from "~/signals";
|
||||
import { PoseModeSelector } from "./comps/PoseModeSelector";
|
||||
import ImageToVideo from "../PageVideo/ImageToVideo";
|
||||
import TextToImage from "../PageImage/TextToImage";
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
enableHotkeyInput,
|
||||
gridVisibility,
|
||||
setGridVisibility,
|
||||
DomLevels,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { EditorCanvas } from "./comps/EngineCanvases";
|
||||
import { SceneContainer } from "./comps/SceneContainer";
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
} from "~/components/signaled/TopBar/TopBar";
|
||||
import { uploadPlaneFromMediaToken } from "~/components/reusable/UploadModalMedia/uploadPlane";
|
||||
import { addObject } from "./signals/objectGroup/addObject";
|
||||
import { AssetType } from "~/enums";
|
||||
import { AssetType, AUTH_STATUS } from "~/enums";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { UploaderState } from "~/models";
|
||||
@@ -71,7 +72,7 @@ import {
|
||||
// videoGenerationModels,
|
||||
// useModelSelectorStore,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
|
||||
import { LoginModal } from "@storyteller/ui-login-modal";
|
||||
import PageDraw from "../PageDraw/PageDraw";
|
||||
import { useTabStore } from "../Stores/TabState";
|
||||
|
||||
@@ -541,7 +542,7 @@ export const PageEditor = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*<LoginModal
|
||||
<LoginModal
|
||||
onClose={() => {}}
|
||||
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
|
||||
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
|
||||
@@ -557,7 +558,7 @@ export const PageEditor = () => {
|
||||
authentication.status.value = AUTH_STATUS.LOGGED_IN;
|
||||
authentication.userInfo.value = userInfo;
|
||||
}}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager.js";
|
||||
import { UserInfo } from "./models/Users.js";
|
||||
import { FetchProxy as fetch } from "@storyteller/tauri-utils";
|
||||
|
||||
export class UsersApi extends ApiManager {
|
||||
private async authFetch<B, T>(
|
||||
endpoint: string,
|
||||
{
|
||||
method,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
body?: B;
|
||||
}
|
||||
): Promise<T> {
|
||||
const bodyInString = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: bodyInString,
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData as T;
|
||||
}
|
||||
|
||||
public GetSession(): Promise<
|
||||
ApiResponse<{
|
||||
loggedIn: boolean;
|
||||
@@ -25,7 +52,7 @@ export class UsersApi extends ApiManager {
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.mesasge,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -51,7 +78,7 @@ export class UsersApi extends ApiManager {
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -69,29 +96,34 @@ export class UsersApi extends ApiManager {
|
||||
username_or_email: usernameOrEmail,
|
||||
password: password,
|
||||
};
|
||||
return await this.post<
|
||||
{ username_or_email: string; password: string },
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: response.error_message,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.authFetch<
|
||||
{ username_or_email: string; password: string },
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>(endpoint, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
data: response.success
|
||||
? { signedSession: response.signed_session }
|
||||
: undefined,
|
||||
errorMessage: response.error_message,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Logout(): Promise<ApiResponse<null>> {
|
||||
@@ -128,36 +160,41 @@ export class UsersApi extends ApiManager {
|
||||
password_confirmation: passwordConfirmation,
|
||||
username,
|
||||
};
|
||||
return await this.post<
|
||||
{
|
||||
username: string;
|
||||
email_address: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
},
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_fields?: Record<string, string>;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: Object.values(response.error_fields ?? {}).join(","),
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.authFetch<
|
||||
{
|
||||
username: string;
|
||||
email_address: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
},
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_fields?: Record<string, string>;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>(endpoint, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
data: response.success
|
||||
? { signedSession: response.signed_session }
|
||||
: undefined,
|
||||
errorMessage:
|
||||
response.error_message ||
|
||||
Object.values(response.error_fields ?? {}).join(", "),
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { faUser, faKey, faEnvelope } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faUser,
|
||||
faKey,
|
||||
faEnvelope,
|
||||
faExclamationTriangle,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface ArtCraftSignUpProps {
|
||||
onSubmit: (
|
||||
@@ -26,7 +32,13 @@ export const ArtCraftSignUp = ({
|
||||
const [localError, setLocalError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalError(errorMessage);
|
||||
if (errorMessage) {
|
||||
setLocalError(
|
||||
errorMessage.charAt(0).toUpperCase() + errorMessage.slice(1)
|
||||
);
|
||||
} else {
|
||||
setLocalError(undefined);
|
||||
}
|
||||
}, [errorMessage]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -120,8 +132,8 @@ export const ArtCraftSignUp = ({
|
||||
/>
|
||||
)}
|
||||
<button type="submit" className="hidden" />
|
||||
<div className="relative mt-3">
|
||||
<div className="flex gap-2 items-center justify-center p-2 bg-white/5 rounded-lg">
|
||||
<div className="relative mt-3 flex flex-col items-center justify-center">
|
||||
<div className="flex gap-2 items-center justify-center p-2 px-3 bg-white/5 rounded-lg">
|
||||
<p className="text-sm opacity-80">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
</p>
|
||||
@@ -137,7 +149,8 @@ export const ArtCraftSignUp = ({
|
||||
</Button>
|
||||
</div>
|
||||
{localError && (
|
||||
<div className="absolute -bottom-10 left-0 right-0 text-red text-sm text-center mb-2 font-semibold">
|
||||
<div className="absolute w-fit -bottom-12 left-1/2 -translate-x-1/2 text-red bg-red/10 border border-red/20 rounded-lg py-1 px-2 justify-center text-sm text-center mb-2 font-semibold flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} />
|
||||
{localError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -235,11 +235,13 @@ export const PromptBox2D = ({
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pastedText = e.clipboardData.getData("text").trim();
|
||||
setPrompt(pastedText);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
|
||||
@@ -444,9 +446,11 @@ export const PromptBox2D = ({
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Stop propagation of keyboard events to prevent them from reaching the canvas
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
handleEnqueue();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -310,11 +310,13 @@ export const PromptBox3D = ({
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pastedText = e.clipboardData.getData("text").trim();
|
||||
setPrompt(pastedText);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
|
||||
@@ -484,6 +486,8 @@ export const PromptBox3D = ({
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleEnqueue();
|
||||
|
||||
Reference in New Issue
Block a user