From 0392bda03ca2ef751031e203255d346c9df765bb Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Thu, 20 Feb 2025 08:38:29 -0800 Subject: [PATCH] Forward color picker events to engine (#418) --- frontend/realtime/src/KonvaApp/Engine.ts | 36 ++++++++++----- .../EngineUtitlities/CommandManager.ts | 4 ++ .../NodesManagers/SelectionManager.ts | 22 +++++++--- .../NodesManagers/ToolbarNodeUtilities.ts | 44 +++++++++++++++++++ .../realtime/src/KonvaApp/types/NodeData.ts | 4 ++ .../ContextualToolbarNode.tsx | 16 ++++++- .../features/ToolbarNode/ToolbarNode.tsx | 2 +- .../src/signals/uiAccess/toolbarNode.ts | 2 +- .../realtime/src/signals/uiEvents/addMedia.ts | 4 +- .../src/signals/uiEvents/toolbarNode.ts | 26 +++++++++++ 10 files changed, 137 insertions(+), 23 deletions(-) diff --git a/frontend/realtime/src/KonvaApp/Engine.ts b/frontend/realtime/src/KonvaApp/Engine.ts index c9d19facfb..7dde8a037f 100644 --- a/frontend/realtime/src/KonvaApp/Engine.ts +++ b/frontend/realtime/src/KonvaApp/Engine.ts @@ -28,6 +28,7 @@ import { LoadingVideosProvider } from "./EngineUtitlities/LoadingVideosProvider" import { VideoExtractionHandler } from "./EngineUtitlities/VideoExtractionHandler/VideoExtractionHandler"; import { RealTimeDrawEngine, ShapeType } from "./RenderingPrimitives/RealTimeDrawEngine"; +import { NodeColor } from "~/signals/uiEvents/toolbarNode"; // for testing loading files from system // import { FileUtilities } from "./FileUtilities/FileUtilities"; @@ -180,8 +181,8 @@ export class Engine { this.setupEventSystem(); this.setAppMode(AppModes.SELECT); - - + + } @@ -364,7 +365,7 @@ export class Engine { // Listen to Toolbar Main uiEvents.toolbarMain.UNDO.onClick(() => this.undoStackManager.undo()); uiEvents.toolbarMain.REDO.onClick(() => this.undoStackManager.redo()); - + uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => { await this.realTimeDrawEngine.saveOutput(); @@ -398,7 +399,7 @@ export class Engine { this.addImage(image); }); - + uiEvents.onAddTextToEngine((textdata) => { this.addText(textdata); @@ -430,7 +431,7 @@ export class Engine { } } }); - + uiEvents.promptEvents.onPromptStrengthChanged((strength) => { this.realTimeDrawEngine.currentStrength = strength; }); @@ -439,13 +440,12 @@ export class Engine { this.realTimeDrawEngine.currentPrompt = prompt; }); - uiEvents.onAddShapeToEngine((shapeData)=> { - + uiEvents.onAddShapeToEngine((shapeData) => { switch (shapeData.shape) { case "circle": this.addShape(ShapeType.Circle, 100); break; - case "square": + case "square": this.addShape(ShapeType.Square, 100); break; case "triangle": @@ -454,6 +454,9 @@ export class Engine { } }); + uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => { + this.changeNodeColor(nodeColor); + }) } disableAllButtons() { @@ -489,7 +492,7 @@ export class Engine { } // Sandbox is quickly a way to test your idea. - public async sandbox() {} + public async sandbox() { } public onMessage(event: MessageEvent) { console.log("Message From Shared Worker"); @@ -536,6 +539,15 @@ export class Engine { this.addKeyboardShortcuts(); } + private changeNodeColor(nodeColor: NodeColor) { + // Find the node that is selected with the id + const idSelector = '#' + nodeColor.kNodeId; + const selectedNode = this.mediaLayer.findOne(idSelector); + + // TODO: Change the color of the node + // Turns out shapes are image nodes... how do we change the image's fill?? + } + public addText(textNodeData: TextNodeData) { const textNode = new TextNode({ textNodeData: textNodeData, @@ -558,8 +570,10 @@ export class Engine { selectionManagerRef: this.selectionManager }); shapeNode.kNode.zIndex(1); // Replace desiredZIndex with a number - + this.commandManager.createNode(shapeNode); + console.debug("Added shapenode:", shapeNode); + console.debug("Added node's ID:", shapeNode.kNode._id); //this.realTimeDrawEngine.addNodes(shapeNode); } @@ -617,7 +631,7 @@ export class Engine { if ( longestVideoNode === undefined || node.videoComponent.duration > - longestVideoNode.videoComponent.duration + longestVideoNode.videoComponent.duration ) { longestVideoNode = node; } diff --git a/frontend/realtime/src/KonvaApp/EngineUtitlities/CommandManager.ts b/frontend/realtime/src/KonvaApp/EngineUtitlities/CommandManager.ts index bb008055f2..58fd59e3c4 100644 --- a/frontend/realtime/src/KonvaApp/EngineUtitlities/CommandManager.ts +++ b/frontend/realtime/src/KonvaApp/EngineUtitlities/CommandManager.ts @@ -58,6 +58,10 @@ export class CommandManager { renderEngineRef: this.renderEngineRef, }); this.undoStackManagerRef.executeCommand(command); + + // Set the kNode's manual id to same as kNode's internal _id + // This helps identify the kNode in the Konva layer with find + node.kNode.id(node.kNode._id.toString()); } deleteNodes() { const nodes = this.selectionManagerRef.getSelectedNodes(); diff --git a/frontend/realtime/src/KonvaApp/NodesManagers/SelectionManager.ts b/frontend/realtime/src/KonvaApp/NodesManagers/SelectionManager.ts index 999044072b..0ae333e798 100644 --- a/frontend/realtime/src/KonvaApp/NodesManagers/SelectionManager.ts +++ b/frontend/realtime/src/KonvaApp/NodesManagers/SelectionManager.ts @@ -9,6 +9,7 @@ import { calculateContextualsPosition, getImageNodeButtonStates, getMultiSelectButtonStates, + getShapeNodeButtonStates, getTextNodeButtonStates, getVideoNodeButtonStates, } from "./ToolbarNodeUtilities"; @@ -271,22 +272,31 @@ export class SelectionManager { return; // no node } if (this.selectedNodes.size === 1) { - if (node instanceof ImageNode || node instanceof ShapeNode) { + if (node instanceof ImageNode) { showOrUpdate({ + knodeIds: [node.kNode.id()], locked: node.isLocked(), buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }), + }); + } + else if (node instanceof ShapeNode) { + console.debug("Selected node is of type ShapeNode", node.kNode.id()) + showOrUpdate({ + knodeIds: [node.kNode.id()], + locked: node.isLocked(), + buttonStates: getShapeNodeButtonStates({ locked: node.isLocked() }), color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000", }); } - if (node instanceof TextNode) { + else if (node instanceof TextNode) { showOrUpdate({ - // TODO: Add ID to identify/mutate the color? + knodeIds: [node.kNode.id()], locked: node.isLocked(), buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }), - + color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000", }); } - if (node instanceof VideoNode) { + else if (node instanceof VideoNode) { showOrUpdate({ locked: node.isLocked(), buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }), @@ -317,7 +327,7 @@ export class SelectionManager { this.hideContextComponents(); return; // no nodes } - + // console.log("SelectionManager > updateContextComponents for node:", node); const coord = calculateContextualsPosition( this.nodeTransformerRef.getKonvaNode(), diff --git a/frontend/realtime/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts b/frontend/realtime/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts index 9b82c82d5d..8e09c585be 100644 --- a/frontend/realtime/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts +++ b/frontend/realtime/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts @@ -152,6 +152,50 @@ export function getVideoNodeButtonStates( {} as ContextualToolbarProps["buttonStates"], ); } + +export function getShapeNodeButtonStates( + props: { locked?: boolean | "unknown" } = {}, +) { + const ButtonNames = ToolbarNodeButtonNames; + return Object.values(ButtonNames).reduce( + (buttonStates, buttonName) => { + // transform button depends on locked state + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates.TRANSFORM = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + return buttonStates; + } + + // hidden buttons + if ( + buttonName === ButtonNames.AI_STYLIZE || + buttonName === ButtonNames.SEGMENTATION || + buttonName === ButtonNames.DOWNLOAD || + buttonName === ButtonNames.CHROMA + ) { + buttonStates[buttonName] = { + disabled: true, + hidden: true, + active: false, + }; + return buttonStates; + } + + // all other buttons + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, + {} as ContextualToolbarProps["buttonStates"], + ); +} + export function getMultiSelectButtonStates( props: { locked?: boolean | "unknown" } = {}, ) { diff --git a/frontend/realtime/src/KonvaApp/types/NodeData.ts b/frontend/realtime/src/KonvaApp/types/NodeData.ts index dae440e821..fb220c43d1 100644 --- a/frontend/realtime/src/KonvaApp/types/NodeData.ts +++ b/frontend/realtime/src/KonvaApp/types/NodeData.ts @@ -26,6 +26,10 @@ export type VideoNodeData = { extractionUrl?: string; }; +export type ShapeNodeData = { + shape: "triangle" | "square" | "circle"; +} + export type NodeData = { type: NodeType; transform: TransformationData; diff --git a/frontend/realtime/src/KonvaRootComponent/ContextualToolbarNode.tsx b/frontend/realtime/src/KonvaRootComponent/ContextualToolbarNode.tsx index 16d32a0c0d..e02482ba7c 100644 --- a/frontend/realtime/src/KonvaRootComponent/ContextualToolbarNode.tsx +++ b/frontend/realtime/src/KonvaRootComponent/ContextualToolbarNode.tsx @@ -50,6 +50,20 @@ export const ContextualToolbarNode = () => { }; }, []); + const handleColorChange = (color: string) => { + // Don't do anything if no node is known to be bound + if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) { + return; + } + + // Update the UI component + uiAccess.toolbarNode.setColor(color); + + // Signal the engine with the node ID (just grab the first -and expected only- node) + const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0]; + dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color }); + } + const boundedTopPosition = Math.min(position.y, winSize.height - 164); return ( { dispatchUiEvents.toolbarNode.lock(e); }} color={uiAccess.toolbarNode.signal.value.color} - onColorChange={(color) => { uiAccess.toolbarNode.setColor(color) }} + onColorChange={handleColorChange} /> ); diff --git a/frontend/realtime/src/components/features/ToolbarNode/ToolbarNode.tsx b/frontend/realtime/src/components/features/ToolbarNode/ToolbarNode.tsx index b720864a8f..324aa4272a 100644 --- a/frontend/realtime/src/components/features/ToolbarNode/ToolbarNode.tsx +++ b/frontend/realtime/src/components/features/ToolbarNode/ToolbarNode.tsx @@ -97,7 +97,7 @@ export const ToolbarNode = ({ ); } else { return ( - + ) } })} diff --git a/frontend/realtime/src/signals/uiAccess/toolbarNode.ts b/frontend/realtime/src/signals/uiAccess/toolbarNode.ts index bb22159dc9..49308fcff1 100644 --- a/frontend/realtime/src/signals/uiAccess/toolbarNode.ts +++ b/frontend/realtime/src/signals/uiAccess/toolbarNode.ts @@ -10,7 +10,7 @@ type ButtonStates = { }; }; export interface ContextualToolbarProps extends ContextualUi { - knodeIds: number[]; + knodeIds: string[]; disabled: boolean; downloadUrl?: string; locked: boolean | "unknown"; diff --git a/frontend/realtime/src/signals/uiEvents/addMedia.ts b/frontend/realtime/src/signals/uiEvents/addMedia.ts index c546061025..25954e9fcd 100644 --- a/frontend/realtime/src/signals/uiEvents/addMedia.ts +++ b/frontend/realtime/src/signals/uiEvents/addMedia.ts @@ -1,5 +1,5 @@ import { signal, effect } from "@preact/signals-react"; -import { TextNodeData } from "~/KonvaApp/types"; +import { ShapeNodeData, TextNodeData } from "~/KonvaApp/types"; // ADDING IMAGES const stagedImage = signal(null); @@ -50,8 +50,6 @@ const onAddTextToEngine = (callback: (data: TextNodeData) => void) => { }; // Add shapes -// TODO: Remove this line and replace with actual shape data that engine needs -type ShapeNodeData = any; const stagedShape = signal(null); const addShapeToEngine = (data: ShapeNodeData) => { stagedShape.value = data; diff --git a/frontend/realtime/src/signals/uiEvents/toolbarNode.ts b/frontend/realtime/src/signals/uiEvents/toolbarNode.ts index 35c6f02219..cabe547a2d 100644 --- a/frontend/realtime/src/signals/uiEvents/toolbarNode.ts +++ b/frontend/realtime/src/signals/uiEvents/toolbarNode.ts @@ -44,6 +44,11 @@ const buttonEventsRecords = Object.values(ButtonNames).reduce( const buttonEventsHandlers = Object.values(ButtonNames).reduce( (acc, buttonName) => { + // We define custom events/handlers for color changes + if (buttonName === ButtonNames.COLOR) { + return acc; + } + acc[buttonName] = { onClick: (callback: MouseEventHandler) => { const { eventSignal, effectCleanup, lastEventTimestamp } = @@ -111,9 +116,27 @@ const retryEventHandler = (callback: MouseEventHandler) => { const retryDispatcher = (e: React.MouseEvent) => { retryEvent.value = e; }; + +export type NodeColor = { + kNodeId: string; + color: string; +} +const selectedColorEvent = signal(); +const setSelectedColor = (nodeColor: NodeColor) => { + selectedColorEvent.value = nodeColor; +} +const onSelectedColorChanged = (callback: (color: NodeColor) => void) => { + effect(() => { + if (selectedColorEvent.value) { + callback(selectedColorEvent.value); + } + }); +}; + export const dispatchers = { lock: lockDispatcher, retry: retryDispatcher, + setSelectedColor, ...buttonDispatchers, }; export const eventsHandlers = { @@ -123,5 +146,8 @@ export const eventsHandlers = { retry: { onClick: retryEventHandler, }, + color: { + onConfirmChanged: onSelectedColorChanged, + }, ...buttonEventsHandlers, };