@@ -12,6 +12,7 @@ import examples from './examples';
1212import { saveAs } from 'file-saver' ;
1313import GraphInspector from './components/GraphInspector' ;
1414import Node from './engine/Node' ;
15+ import NodeContextMenu from './components/NodeContextMenu' ;
1516
1617const VisualScripting = ( ) => {
1718 // #region State Declarations
@@ -50,6 +51,7 @@ const VisualScripting = () => {
5051 const [ isMultiSelectMode , setIsMultiSelectMode ] = useState ( false ) ;
5152 const [ tabs , setTabs ] = useState ( [ { id : 'untitled-1' , title : 'Untitled-1' , type : 'Export' } ] ) ;
5253 const [ activeTab , setActiveTab ] = useState ( 'untitled-1' ) ;
54+ const [ nodeContextMenu , setNodeContextMenu ] = useState ( { visible : false , x : 0 , y : 0 } ) ;
5355 // #endregion
5456
5557 // #region Drawing Functions
@@ -72,7 +74,16 @@ const VisualScripting = () => {
7274 e . preventDefault ( ) ;
7375 const rect = canvasRef . current . getBoundingClientRect ( ) ;
7476 const { x, y } = camera . screenToWorld ( e . clientX - rect . left , e . clientY - rect . top ) ;
75- setContextMenu ( { visible : true , x, y } ) ;
77+
78+ const clickedNode = findClickedNode ( x , y ) ;
79+ if ( clickedNode ) {
80+ setNodeContextMenu ( { visible : true , x, y } ) ;
81+ if ( ! selectedNodes . includes ( clickedNode ) ) {
82+ setSelectedNodes ( [ clickedNode ] ) ;
83+ }
84+ } else {
85+ setContextMenu ( { visible : true , x, y } ) ;
86+ }
7687 setNeedsRedraw ( true ) ;
7788 } ;
7889
@@ -83,6 +94,9 @@ const VisualScripting = () => {
8394 if ( contextMenu . visible ) {
8495 setContextMenu ( { ...contextMenu , visible : false } ) ;
8596 }
97+ if ( nodeContextMenu . visible ) {
98+ setNodeContextMenu ( { ...nodeContextMenu , visible : false } ) ;
99+ }
86100
87101 if ( connecting ) {
88102 const clickedPort = findClickedPort ( x , y ) ;
@@ -117,6 +131,13 @@ const VisualScripting = () => {
117131 const rect = canvasRef . current . getBoundingClientRect ( ) ;
118132 const { x, y } = camera . screenToWorld ( e . clientX - rect . left , e . clientY - rect . top ) ;
119133
134+ if ( contextMenu . visible ) {
135+ setContextMenu ( { ...contextMenu , visible : false } ) ;
136+ }
137+ if ( nodeContextMenu . visible ) {
138+ setNodeContextMenu ( { ...nodeContextMenu , visible : false } ) ;
139+ }
140+
120141 const clickedPort = findClickedPort ( x , y ) ;
121142 if ( clickedPort ) {
122143 const node = nodes . find ( n => n . id === clickedPort . nodeId ) ;
@@ -229,8 +250,20 @@ const VisualScripting = () => {
229250 for ( const node of nodes ) {
230251 const nodeType = nodeTypes [ node . type ] ;
231252 const dimensions = renderer . getNodeDimensions ( node , canvasRef . current . getContext ( '2d' ) ) ;
232- const clickedPort = node . findClickedPort ( x , y , dimensions , nodeType ) ;
233- if ( clickedPort ) return clickedPort ;
253+
254+ // Check input ports
255+ for ( let i = 0 ; i < nodeType . inputs . length ; i ++ ) {
256+ if ( node . isPortClicked ( x , y , i , true , dimensions ) ) {
257+ return node . getPortPosition ( i , true , dimensions ) ;
258+ }
259+ }
260+
261+ // Check output ports
262+ for ( let i = 0 ; i < nodeType . outputs . length ; i ++ ) {
263+ if ( node . isPortClicked ( x , y , i , false , dimensions ) ) {
264+ return node . getPortPosition ( i , false , dimensions ) ;
265+ }
266+ }
234267 }
235268 return null ;
236269 } ;
@@ -721,6 +754,37 @@ const VisualScripting = () => {
721754 } ;
722755 // #endregion
723756
757+ // #region Handle Node Context Menu Actions
758+ const handleNodeContextMenuAction = ( action ) => {
759+ switch ( action ) {
760+ case 'copy' :
761+ setCopiedNodes ( [ ...selectedNodes ] ) ;
762+ break ;
763+ case 'delete' :
764+ deleteSelectedNodes ( ) ;
765+ break ;
766+ case 'cut' :
767+ setCopiedNodes ( [ ...selectedNodes ] ) ;
768+ deleteSelectedNodes ( ) ;
769+ break ;
770+ case 'duplicate' :
771+ const newNodes = selectedNodes . map ( node => {
772+ // Create a proper Node instance using the static create method
773+ const duplicatedNode = Node . create ( node . type , node . x + 20 , node . y + 20 , nodeTypes ) ;
774+ // Copy over the properties
775+ duplicatedNode . properties = { ...node . properties } ;
776+ return duplicatedNode ;
777+ } ) ;
778+ setNodes ( [ ...nodes , ...newNodes ] ) ;
779+ setSelectedNodes ( newNodes ) ;
780+ break ;
781+ default :
782+ console . log ( `Unhandled node context menu action: ${ action } ` ) ;
783+ }
784+ setNodeContextMenu ( { ...nodeContextMenu , visible : false } ) ;
785+ } ;
786+ // #endregion
787+
724788 // #region Render
725789 return (
726790 < div
@@ -818,6 +882,13 @@ const VisualScripting = () => {
818882 addNode = { addNode }
819883 camera = { camera }
820884 />
885+ < NodeContextMenu
886+ visible = { nodeContextMenu . visible }
887+ x = { nodeContextMenu . x }
888+ y = { nodeContextMenu . y }
889+ camera = { camera }
890+ onAction = { handleNodeContextMenuAction }
891+ />
821892 </ div >
822893 </ >
823894 ) : activeTab === 'settings' ? (
0 commit comments