Compare commits

...
Author SHA1 Message Date
bflatastic 04dcac98e3 login error messages now show properly 2025-06-13 17:32:47 +07:00
bflatastic 8be5013260 re-enable login modal 2025-06-13 16:29:09 +07:00
bflatastic 3c9b81c3ea bug fix: 3d promptbox events 2025-06-12 22:35:27 +07:00
BFlat 9d1b7d22ee bug fix: ensure no keyboard events leak through to the canvas (#669) 2025-06-12 22:30:43 +07:00
ArEnScandCrossPr0duct b046f6ff2a Feature/Zlayer and opacity issues (#667)
This adds all the z-layer work and context menus back for composition.
Also fixes the alpha issues and removes menus that are not needed for
now.

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-11 13:43:29 -04:00
10 changed files with 549 additions and 195 deletions
@@ -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>
);
};
+91 -54
View File
@@ -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();