@nice2dev/ui-3d 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/dist/cjs/collaborative/collaborativeScene.js +210 -0
  3. package/dist/cjs/collaborative/collaborativeScene.js.map +1 -0
  4. package/dist/cjs/core/i18n.js +3 -3
  5. package/dist/cjs/core/i18n.js.map +1 -1
  6. package/dist/cjs/dance/DanceBridge.js +162 -0
  7. package/dist/cjs/dance/DanceBridge.js.map +1 -0
  8. package/dist/cjs/dance/DanceScoreEngine.js +210 -0
  9. package/dist/cjs/dance/DanceScoreEngine.js.map +1 -0
  10. package/dist/cjs/dance/PoseDetector.js +199 -0
  11. package/dist/cjs/dance/PoseDetector.js.map +1 -0
  12. package/dist/cjs/index.js +254 -0
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/material/MaterialEditor.module.css.js +6 -0
  15. package/dist/cjs/material/MaterialEditor.module.css.js.map +1 -0
  16. package/dist/cjs/material/NiceMaterialEditor.js +737 -0
  17. package/dist/cjs/material/NiceMaterialEditor.js.map +1 -0
  18. package/dist/cjs/material/materialEditorTypes.js +73 -0
  19. package/dist/cjs/material/materialEditorTypes.js.map +1 -0
  20. package/dist/cjs/material/materialEditorUtils.js +841 -0
  21. package/dist/cjs/material/materialEditorUtils.js.map +1 -0
  22. package/dist/cjs/material/materialNodeDefinitions.js +1285 -0
  23. package/dist/cjs/material/materialNodeDefinitions.js.map +1 -0
  24. package/dist/cjs/model/ModelEditor.js +4 -1
  25. package/dist/cjs/model/ModelEditor.js.map +1 -1
  26. package/dist/cjs/model/ModelEditor.module.css.js +1 -1
  27. package/dist/cjs/model/ModelEditorLeftPanel.js +5 -4
  28. package/dist/cjs/model/ModelEditorLeftPanel.js.map +1 -1
  29. package/dist/cjs/model/ModelEditorMenuBar.js +8 -3
  30. package/dist/cjs/model/ModelEditorMenuBar.js.map +1 -1
  31. package/dist/cjs/model/ModelEditorRightPanel.js +27 -26
  32. package/dist/cjs/model/ModelEditorRightPanel.js.map +1 -1
  33. package/dist/cjs/model/ModelEditorSubComponents.js +20 -16
  34. package/dist/cjs/model/ModelEditorSubComponents.js.map +1 -1
  35. package/dist/cjs/model/ModelEditorTimeline.js +5 -4
  36. package/dist/cjs/model/ModelEditorTimeline.js.map +1 -1
  37. package/dist/cjs/model/ModelEditorToolbar.js +4 -3
  38. package/dist/cjs/model/ModelEditorToolbar.js.map +1 -1
  39. package/dist/cjs/model/ModelEditorViewport.js +2 -2
  40. package/dist/cjs/model/ModelEditorViewport.js.map +1 -1
  41. package/dist/cjs/model/ModelViewer.js +68 -0
  42. package/dist/cjs/model/ModelViewer.js.map +1 -0
  43. package/dist/cjs/model/ModelViewer.module.css.js +6 -0
  44. package/dist/cjs/model/ModelViewer.module.css.js.map +1 -0
  45. package/dist/cjs/model/NiceArmatureEditor.js +255 -0
  46. package/dist/cjs/model/NiceArmatureEditor.js.map +1 -0
  47. package/dist/cjs/model/NiceMorphTargetEditor.js +206 -0
  48. package/dist/cjs/model/NiceMorphTargetEditor.js.map +1 -0
  49. package/dist/cjs/model/NiceOctree.js +339 -0
  50. package/dist/cjs/model/NiceOctree.js.map +1 -0
  51. package/dist/cjs/model/NicePhysicsSimulation.js +283 -0
  52. package/dist/cjs/model/NicePhysicsSimulation.js.map +1 -0
  53. package/dist/cjs/model/NiceProceduralGeometry.js +269 -0
  54. package/dist/cjs/model/NiceProceduralGeometry.js.map +1 -0
  55. package/dist/cjs/model/NiceTerrainEditor.js +343 -0
  56. package/dist/cjs/model/NiceTerrainEditor.js.map +1 -0
  57. package/dist/cjs/model/NiceWeightPainter.js +258 -0
  58. package/dist/cjs/model/NiceWeightPainter.js.map +1 -0
  59. package/dist/cjs/model/NiceXRPreview.js +269 -0
  60. package/dist/cjs/model/NiceXRPreview.js.map +1 -0
  61. package/dist/cjs/model/cadModeUtils.js +130 -0
  62. package/dist/cjs/model/cadModeUtils.js.map +1 -0
  63. package/dist/cjs/model/editorShortcuts.js +187 -0
  64. package/dist/cjs/model/editorShortcuts.js.map +1 -0
  65. package/dist/cjs/model/modelEditorTypes.js +11 -0
  66. package/dist/cjs/model/modelEditorTypes.js.map +1 -1
  67. package/dist/cjs/model/modelEditorUtils.js +1049 -0
  68. package/dist/cjs/model/modelEditorUtils.js.map +1 -0
  69. package/dist/cjs/model/simsModeUtils.js +358 -0
  70. package/dist/cjs/model/simsModeUtils.js.map +1 -0
  71. package/dist/cjs/model/useModelEditor.js +319 -115
  72. package/dist/cjs/model/useModelEditor.js.map +1 -1
  73. package/dist/cjs/model/useModelViewer.js +634 -0
  74. package/dist/cjs/model/useModelViewer.js.map +1 -0
  75. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  76. package/dist/cjs/particle/NiceParticleEditor.js +526 -0
  77. package/dist/cjs/particle/NiceParticleEditor.js.map +1 -0
  78. package/dist/cjs/particle/ParticleEditor.module.css.js +6 -0
  79. package/dist/cjs/particle/ParticleEditor.module.css.js.map +1 -0
  80. package/dist/cjs/particle/particleEditorTypes.js +92 -0
  81. package/dist/cjs/particle/particleEditorTypes.js.map +1 -0
  82. package/dist/cjs/particle/particleEditorUtils.js +1084 -0
  83. package/dist/cjs/particle/particleEditorUtils.js.map +1 -0
  84. package/dist/cjs/rendering/NiceCascadedShadows.js +266 -0
  85. package/dist/cjs/rendering/NiceCascadedShadows.js.map +1 -0
  86. package/dist/cjs/rendering/NiceRenderExport.js +341 -0
  87. package/dist/cjs/rendering/NiceRenderExport.js.map +1 -0
  88. package/dist/cjs/rendering/NiceSSAO.js +359 -0
  89. package/dist/cjs/rendering/NiceSSAO.js.map +1 -0
  90. package/dist/cjs/rendering/NiceSSR.js +277 -0
  91. package/dist/cjs/rendering/NiceSSR.js.map +1 -0
  92. package/dist/cjs/rendering/NiceWebGPURenderer.js +215 -0
  93. package/dist/cjs/rendering/NiceWebGPURenderer.js.map +1 -0
  94. package/dist/cjs/ui/dist/index.js +50089 -0
  95. package/dist/cjs/ui/dist/index.js.map +1 -0
  96. package/dist/cjs/uv/NiceUVEditor.js +520 -0
  97. package/dist/cjs/uv/NiceUVEditor.js.map +1 -0
  98. package/dist/cjs/uv/UVEditor.module.css.js +6 -0
  99. package/dist/cjs/uv/UVEditor.module.css.js.map +1 -0
  100. package/dist/cjs/uv/uvEditorTypes.js +98 -0
  101. package/dist/cjs/uv/uvEditorTypes.js.map +1 -0
  102. package/dist/cjs/uv/uvEditorUtils.js +670 -0
  103. package/dist/cjs/uv/uvEditorUtils.js.map +1 -0
  104. package/dist/esm/collaborative/collaborativeScene.js +206 -0
  105. package/dist/esm/collaborative/collaborativeScene.js.map +1 -0
  106. package/dist/esm/dance/DanceBridge.js +158 -0
  107. package/dist/esm/dance/DanceBridge.js.map +1 -0
  108. package/dist/esm/dance/DanceScoreEngine.js +207 -0
  109. package/dist/esm/dance/DanceScoreEngine.js.map +1 -0
  110. package/dist/esm/dance/PoseDetector.js +195 -0
  111. package/dist/esm/dance/PoseDetector.js.map +1 -0
  112. package/dist/esm/index.js +35 -1
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/material/MaterialEditor.module.css.js +4 -0
  115. package/dist/esm/material/MaterialEditor.module.css.js.map +1 -0
  116. package/dist/esm/material/NiceMaterialEditor.js +734 -0
  117. package/dist/esm/material/NiceMaterialEditor.js.map +1 -0
  118. package/dist/esm/material/materialEditorTypes.js +62 -0
  119. package/dist/esm/material/materialEditorTypes.js.map +1 -0
  120. package/dist/esm/material/materialEditorUtils.js +811 -0
  121. package/dist/esm/material/materialEditorUtils.js.map +1 -0
  122. package/dist/esm/material/materialNodeDefinitions.js +1280 -0
  123. package/dist/esm/material/materialNodeDefinitions.js.map +1 -0
  124. package/dist/esm/model/ModelEditor.js +4 -2
  125. package/dist/esm/model/ModelEditor.js.map +1 -1
  126. package/dist/esm/model/ModelEditor.module.css.js +1 -1
  127. package/dist/esm/model/ModelEditorLeftPanel.js +5 -4
  128. package/dist/esm/model/ModelEditorLeftPanel.js.map +1 -1
  129. package/dist/esm/model/ModelEditorMenuBar.js +8 -3
  130. package/dist/esm/model/ModelEditorMenuBar.js.map +1 -1
  131. package/dist/esm/model/ModelEditorRightPanel.js +27 -26
  132. package/dist/esm/model/ModelEditorRightPanel.js.map +1 -1
  133. package/dist/esm/model/ModelEditorSubComponents.js +17 -13
  134. package/dist/esm/model/ModelEditorSubComponents.js.map +1 -1
  135. package/dist/esm/model/ModelEditorTimeline.js +5 -4
  136. package/dist/esm/model/ModelEditorTimeline.js.map +1 -1
  137. package/dist/esm/model/ModelEditorToolbar.js +4 -3
  138. package/dist/esm/model/ModelEditorToolbar.js.map +1 -1
  139. package/dist/esm/model/ModelEditorViewport.js +2 -2
  140. package/dist/esm/model/ModelEditorViewport.js.map +1 -1
  141. package/dist/esm/model/ModelViewer.js +65 -0
  142. package/dist/esm/model/ModelViewer.js.map +1 -0
  143. package/dist/esm/model/ModelViewer.module.css.js +4 -0
  144. package/dist/esm/model/ModelViewer.module.css.js.map +1 -0
  145. package/dist/esm/model/NiceArmatureEditor.js +233 -0
  146. package/dist/esm/model/NiceArmatureEditor.js.map +1 -0
  147. package/dist/esm/model/NiceMorphTargetEditor.js +184 -0
  148. package/dist/esm/model/NiceMorphTargetEditor.js.map +1 -0
  149. package/dist/esm/model/NiceOctree.js +317 -0
  150. package/dist/esm/model/NiceOctree.js.map +1 -0
  151. package/dist/esm/model/NicePhysicsSimulation.js +261 -0
  152. package/dist/esm/model/NicePhysicsSimulation.js.map +1 -0
  153. package/dist/esm/model/NiceProceduralGeometry.js +242 -0
  154. package/dist/esm/model/NiceProceduralGeometry.js.map +1 -0
  155. package/dist/esm/model/NiceTerrainEditor.js +321 -0
  156. package/dist/esm/model/NiceTerrainEditor.js.map +1 -0
  157. package/dist/esm/model/NiceWeightPainter.js +236 -0
  158. package/dist/esm/model/NiceWeightPainter.js.map +1 -0
  159. package/dist/esm/model/NiceXRPreview.js +247 -0
  160. package/dist/esm/model/NiceXRPreview.js.map +1 -0
  161. package/dist/esm/model/cadModeUtils.js +103 -0
  162. package/dist/esm/model/cadModeUtils.js.map +1 -0
  163. package/dist/esm/model/editorShortcuts.js +185 -0
  164. package/dist/esm/model/editorShortcuts.js.map +1 -0
  165. package/dist/esm/model/modelEditorTypes.js +11 -0
  166. package/dist/esm/model/modelEditorTypes.js.map +1 -1
  167. package/dist/esm/model/modelEditorUtils.js +997 -0
  168. package/dist/esm/model/modelEditorUtils.js.map +1 -0
  169. package/dist/esm/model/simsModeUtils.js +325 -0
  170. package/dist/esm/model/simsModeUtils.js.map +1 -0
  171. package/dist/esm/model/useModelEditor.js +204 -0
  172. package/dist/esm/model/useModelEditor.js.map +1 -1
  173. package/dist/esm/model/useModelViewer.js +613 -0
  174. package/dist/esm/model/useModelViewer.js.map +1 -0
  175. package/dist/esm/nice2dev-ui-3d.css +1 -1
  176. package/dist/esm/particle/NiceParticleEditor.js +523 -0
  177. package/dist/esm/particle/NiceParticleEditor.js.map +1 -0
  178. package/dist/esm/particle/ParticleEditor.module.css.js +4 -0
  179. package/dist/esm/particle/ParticleEditor.module.css.js.map +1 -0
  180. package/dist/esm/particle/particleEditorTypes.js +84 -0
  181. package/dist/esm/particle/particleEditorTypes.js.map +1 -0
  182. package/dist/esm/particle/particleEditorUtils.js +1054 -0
  183. package/dist/esm/particle/particleEditorUtils.js.map +1 -0
  184. package/dist/esm/rendering/NiceCascadedShadows.js +244 -0
  185. package/dist/esm/rendering/NiceCascadedShadows.js.map +1 -0
  186. package/dist/esm/rendering/NiceRenderExport.js +319 -0
  187. package/dist/esm/rendering/NiceRenderExport.js.map +1 -0
  188. package/dist/esm/rendering/NiceSSAO.js +337 -0
  189. package/dist/esm/rendering/NiceSSAO.js.map +1 -0
  190. package/dist/esm/rendering/NiceSSR.js +255 -0
  191. package/dist/esm/rendering/NiceSSR.js.map +1 -0
  192. package/dist/esm/rendering/NiceWebGPURenderer.js +193 -0
  193. package/dist/esm/rendering/NiceWebGPURenderer.js.map +1 -0
  194. package/dist/esm/ui/dist/index.js +49686 -0
  195. package/dist/esm/ui/dist/index.js.map +1 -0
  196. package/dist/esm/uv/NiceUVEditor.js +518 -0
  197. package/dist/esm/uv/NiceUVEditor.js.map +1 -0
  198. package/dist/esm/uv/UVEditor.module.css.js +4 -0
  199. package/dist/esm/uv/UVEditor.module.css.js.map +1 -0
  200. package/dist/esm/uv/uvEditorTypes.js +88 -0
  201. package/dist/esm/uv/uvEditorTypes.js.map +1 -0
  202. package/dist/esm/uv/uvEditorUtils.js +621 -0
  203. package/dist/esm/uv/uvEditorUtils.js.map +1 -0
  204. package/package.json +3 -4
@@ -0,0 +1,734 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useRef, useState, useMemo, useEffect, useCallback, useReducer } from 'react';
3
+ import { MAX_ZOOM, MIN_ZOOM, NODE_CATEGORY_COLORS, NODE_CATEGORY_NAMES, NODE_WIDTH, NODE_HEADER_HEIGHT, PORT_HEIGHT, PORT_TYPE_COLORS, DEFAULT_GRID_SIZE } from './materialEditorTypes.js';
4
+ import { MATERIAL_NODE_MAP, searchNodes, MATERIAL_NODE_DEFINITIONS } from './materialNodeDefinitions.js';
5
+ import { compileShader, importGraphFromJSON, exportGraphToJSON, screenToGraph, updateNodeInGraph, calculateZoomToFit, cloneNode, generateNodeId, removeNodesFromGraph, removeConnectionFromGraph, getNodeDefinition, canConnect, createConnection, addConnectionToGraph, moveNodesInGraph, createNode, addNodeToGraph, createEmptyGraph } from './materialEditorUtils.js';
6
+ import styles from './MaterialEditor.module.css.js';
7
+
8
+ /* ═══════════════════════════════════════════════════════════════════════════
9
+ State Reducer
10
+ ═══════════════════════════════════════════════════════════════════════════ */
11
+ function createInitialState(graph) {
12
+ const initialGraph = graph || createEmptyGraph();
13
+ return {
14
+ graph: initialGraph,
15
+ selectedNodes: [],
16
+ selectedConnections: [],
17
+ clipboard: null,
18
+ viewTransform: { x: 100, y: 100, zoom: 1 },
19
+ previewMode: 'sphere',
20
+ showGrid: true,
21
+ showMinimap: true,
22
+ snapToGrid: true,
23
+ gridSize: DEFAULT_GRID_SIZE,
24
+ undoStack: [],
25
+ redoStack: [],
26
+ compiledShader: null,
27
+ compiling: false,
28
+ autoCompile: true,
29
+ searchQuery: '',
30
+ expandedCategories: ['input', 'output', 'math', 'color', 'texture'],
31
+ };
32
+ }
33
+ function pushToUndoStack(state) {
34
+ return {
35
+ ...state,
36
+ undoStack: [...state.undoStack.slice(-49), state.graph],
37
+ redoStack: [],
38
+ };
39
+ }
40
+ function materialEditorReducer(state, action) {
41
+ switch (action.type) {
42
+ case 'ADD_NODE': {
43
+ const node = createNode(action.nodeType, action.position);
44
+ if (!node)
45
+ return state;
46
+ const newState = pushToUndoStack(state);
47
+ return {
48
+ ...newState,
49
+ graph: addNodeToGraph(newState.graph, node),
50
+ selectedNodes: [node.id],
51
+ };
52
+ }
53
+ case 'DELETE_NODES': {
54
+ const newState = pushToUndoStack(state);
55
+ return {
56
+ ...newState,
57
+ graph: removeNodesFromGraph(newState.graph, action.nodeIds),
58
+ selectedNodes: [],
59
+ selectedConnections: [],
60
+ };
61
+ }
62
+ case 'MOVE_NODES': {
63
+ return {
64
+ ...state,
65
+ graph: moveNodesInGraph(state.graph, action.nodeIds, action.delta),
66
+ };
67
+ }
68
+ case 'UPDATE_NODE_PROPERTY': {
69
+ const newState = pushToUndoStack(state);
70
+ const node = newState.graph.nodes.find(n => n.id === action.nodeId);
71
+ if (!node)
72
+ return state;
73
+ return {
74
+ ...newState,
75
+ graph: updateNodeInGraph(newState.graph, action.nodeId, {
76
+ properties: { ...node.properties, [action.propertyId]: action.value },
77
+ }),
78
+ };
79
+ }
80
+ case 'UPDATE_INPUT_VALUE': {
81
+ const newState = pushToUndoStack(state);
82
+ const node = newState.graph.nodes.find(n => n.id === action.nodeId);
83
+ if (!node)
84
+ return state;
85
+ return {
86
+ ...newState,
87
+ graph: updateNodeInGraph(newState.graph, action.nodeId, {
88
+ inputValues: { ...node.inputValues, [action.portId]: action.value },
89
+ }),
90
+ };
91
+ }
92
+ case 'CONNECT': {
93
+ const sourceNode = state.graph.nodes.find(n => n.id === action.connection.sourceNodeId);
94
+ const targetNode = state.graph.nodes.find(n => n.id === action.connection.targetNodeId);
95
+ if (!sourceNode || !targetNode)
96
+ return state;
97
+ const sourceDef = getNodeDefinition(sourceNode);
98
+ const targetDef = getNodeDefinition(targetNode);
99
+ if (!sourceDef || !targetDef)
100
+ return state;
101
+ const sourcePort = sourceDef.outputs.find(p => p.id === action.connection.sourcePortId);
102
+ const targetPort = targetDef.inputs.find(p => p.id === action.connection.targetPortId);
103
+ if (!sourcePort || !targetPort)
104
+ return state;
105
+ const { valid } = canConnect(sourceNode, sourcePort, targetNode, targetPort, state.graph.connections);
106
+ if (!valid)
107
+ return state;
108
+ const newState = pushToUndoStack(state);
109
+ const connection = createConnection(action.connection.sourceNodeId, action.connection.sourcePortId, action.connection.targetNodeId, action.connection.targetPortId);
110
+ return {
111
+ ...newState,
112
+ graph: addConnectionToGraph(newState.graph, connection),
113
+ };
114
+ }
115
+ case 'DISCONNECT': {
116
+ const newState = pushToUndoStack(state);
117
+ return {
118
+ ...newState,
119
+ graph: removeConnectionFromGraph(newState.graph, action.connectionId),
120
+ selectedConnections: state.selectedConnections.filter(id => id !== action.connectionId),
121
+ };
122
+ }
123
+ case 'SELECT_NODES': {
124
+ return {
125
+ ...state,
126
+ selectedNodes: action.additive
127
+ ? [...new Set([...state.selectedNodes, ...action.nodeIds])]
128
+ : action.nodeIds,
129
+ selectedConnections: action.additive ? state.selectedConnections : [],
130
+ };
131
+ }
132
+ case 'SELECT_CONNECTIONS': {
133
+ return {
134
+ ...state,
135
+ selectedConnections: action.additive
136
+ ? [...new Set([...state.selectedConnections, ...action.connectionIds])]
137
+ : action.connectionIds,
138
+ selectedNodes: action.additive ? state.selectedNodes : [],
139
+ };
140
+ }
141
+ case 'SELECT_ALL': {
142
+ return {
143
+ ...state,
144
+ selectedNodes: state.graph.nodes.map(n => n.id),
145
+ selectedConnections: state.graph.connections.map(c => c.id),
146
+ };
147
+ }
148
+ case 'DESELECT_ALL': {
149
+ return {
150
+ ...state,
151
+ selectedNodes: [],
152
+ selectedConnections: [],
153
+ };
154
+ }
155
+ case 'COPY': {
156
+ const nodesToCopy = state.graph.nodes.filter(n => state.selectedNodes.includes(n.id));
157
+ const connectionsToCopy = state.graph.connections.filter(c => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId));
158
+ return {
159
+ ...state,
160
+ clipboard: { nodes: nodesToCopy, connections: connectionsToCopy },
161
+ };
162
+ }
163
+ case 'PASTE': {
164
+ if (!state.clipboard)
165
+ return state;
166
+ const newState = pushToUndoStack(state);
167
+ const idMap = new Map();
168
+ const newNodes = state.clipboard.nodes.map(node => {
169
+ const newId = generateNodeId();
170
+ idMap.set(node.id, newId);
171
+ return {
172
+ ...node,
173
+ id: newId,
174
+ position: {
175
+ x: node.position.x + action.position.x - state.clipboard.nodes[0].position.x + 20,
176
+ y: node.position.y + action.position.y - state.clipboard.nodes[0].position.y + 20,
177
+ },
178
+ selected: false,
179
+ };
180
+ });
181
+ const newConnections = state.clipboard.connections.map(conn => ({
182
+ ...conn,
183
+ id: generateNodeId(),
184
+ sourceNodeId: idMap.get(conn.sourceNodeId),
185
+ targetNodeId: idMap.get(conn.targetNodeId),
186
+ }));
187
+ return {
188
+ ...newState,
189
+ graph: {
190
+ ...newState.graph,
191
+ nodes: [...newState.graph.nodes, ...newNodes],
192
+ connections: [...newState.graph.connections, ...newConnections],
193
+ },
194
+ selectedNodes: newNodes.map(n => n.id),
195
+ };
196
+ }
197
+ case 'CUT': {
198
+ const nodesToCut = state.graph.nodes.filter(n => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId);
199
+ const connectionsToCut = state.graph.connections.filter(c => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId));
200
+ const newState = pushToUndoStack(state);
201
+ return {
202
+ ...newState,
203
+ clipboard: { nodes: nodesToCut, connections: connectionsToCut },
204
+ graph: removeNodesFromGraph(newState.graph, nodesToCut.map(n => n.id)),
205
+ selectedNodes: [],
206
+ };
207
+ }
208
+ case 'DUPLICATE': {
209
+ const nodesToDupe = state.graph.nodes.filter(n => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId);
210
+ const newState = pushToUndoStack(state);
211
+ const idMap = new Map();
212
+ const newNodes = nodesToDupe.map(node => {
213
+ const cloned = cloneNode(node);
214
+ idMap.set(node.id, cloned.id);
215
+ return cloned;
216
+ });
217
+ const connectionsToDupe = state.graph.connections.filter(c => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId));
218
+ const newConnections = connectionsToDupe.map(conn => ({
219
+ ...conn,
220
+ id: generateNodeId(),
221
+ sourceNodeId: idMap.get(conn.sourceNodeId),
222
+ targetNodeId: idMap.get(conn.targetNodeId),
223
+ }));
224
+ return {
225
+ ...newState,
226
+ graph: {
227
+ ...newState.graph,
228
+ nodes: [...newState.graph.nodes, ...newNodes],
229
+ connections: [...newState.graph.connections, ...newConnections],
230
+ },
231
+ selectedNodes: newNodes.map(n => n.id),
232
+ };
233
+ }
234
+ case 'UNDO': {
235
+ if (state.undoStack.length === 0)
236
+ return state;
237
+ const previous = state.undoStack[state.undoStack.length - 1];
238
+ return {
239
+ ...state,
240
+ graph: previous,
241
+ undoStack: state.undoStack.slice(0, -1),
242
+ redoStack: [...state.redoStack, state.graph],
243
+ selectedNodes: [],
244
+ selectedConnections: [],
245
+ };
246
+ }
247
+ case 'REDO': {
248
+ if (state.redoStack.length === 0)
249
+ return state;
250
+ const next = state.redoStack[state.redoStack.length - 1];
251
+ return {
252
+ ...state,
253
+ graph: next,
254
+ undoStack: [...state.undoStack, state.graph],
255
+ redoStack: state.redoStack.slice(0, -1),
256
+ selectedNodes: [],
257
+ selectedConnections: [],
258
+ };
259
+ }
260
+ case 'SET_VIEW_TRANSFORM':
261
+ return { ...state, viewTransform: action.transform };
262
+ case 'ZOOM_TO_FIT': {
263
+ const transform = calculateZoomToFit(state.graph.nodes, 800, 600);
264
+ return { ...state, viewTransform: transform };
265
+ }
266
+ case 'SET_PREVIEW_MODE':
267
+ return { ...state, previewMode: action.mode };
268
+ case 'TOGGLE_GRID':
269
+ return { ...state, showGrid: !state.showGrid };
270
+ case 'TOGGLE_MINIMAP':
271
+ return { ...state, showMinimap: !state.showMinimap };
272
+ case 'TOGGLE_SNAP':
273
+ return { ...state, snapToGrid: !state.snapToGrid };
274
+ case 'SET_GRID_SIZE':
275
+ return { ...state, gridSize: action.size };
276
+ case 'SET_SEARCH_QUERY':
277
+ return { ...state, searchQuery: action.query };
278
+ case 'TOGGLE_CATEGORY':
279
+ return {
280
+ ...state,
281
+ expandedCategories: state.expandedCategories.includes(action.category)
282
+ ? state.expandedCategories.filter(c => c !== action.category)
283
+ : [...state.expandedCategories, action.category],
284
+ };
285
+ case 'COLLAPSE_NODE':
286
+ return {
287
+ ...state,
288
+ graph: updateNodeInGraph(state.graph, action.nodeId, { collapsed: true }),
289
+ };
290
+ case 'EXPAND_NODE':
291
+ return {
292
+ ...state,
293
+ graph: updateNodeInGraph(state.graph, action.nodeId, { collapsed: false }),
294
+ };
295
+ case 'ADD_COMMENT':
296
+ return {
297
+ ...state,
298
+ graph: updateNodeInGraph(state.graph, action.nodeId, { comment: action.comment }),
299
+ };
300
+ case 'LOCK_NODE':
301
+ return {
302
+ ...state,
303
+ graph: updateNodeInGraph(state.graph, action.nodeId, { locked: true }),
304
+ };
305
+ case 'UNLOCK_NODE':
306
+ return {
307
+ ...state,
308
+ graph: updateNodeInGraph(state.graph, action.nodeId, { locked: false }),
309
+ };
310
+ case 'COMPILE':
311
+ return { ...state, compiling: true };
312
+ case 'SET_COMPILED_SHADER':
313
+ return { ...state, compiledShader: action.shader, compiling: false };
314
+ case 'TOGGLE_AUTO_COMPILE':
315
+ return { ...state, autoCompile: !state.autoCompile };
316
+ case 'LOAD_GRAPH':
317
+ return {
318
+ ...createInitialState(action.graph),
319
+ autoCompile: state.autoCompile,
320
+ };
321
+ case 'NEW_GRAPH':
322
+ return createInitialState();
323
+ case 'SET_METADATA':
324
+ return {
325
+ ...state,
326
+ graph: {
327
+ ...state.graph,
328
+ metadata: { ...state.graph.metadata, ...action.metadata },
329
+ },
330
+ };
331
+ default:
332
+ return state;
333
+ }
334
+ }
335
+ /* ═══════════════════════════════════════════════════════════════════════════
336
+ Hook: useMaterialEditor
337
+ ═══════════════════════════════════════════════════════════════════════════ */
338
+ function useMaterialEditor(initialGraph) {
339
+ const [state, dispatch] = useReducer(materialEditorReducer, initialGraph, createInitialState);
340
+ // Auto-compile when graph changes
341
+ useEffect(() => {
342
+ if (state.autoCompile && !state.compiling) {
343
+ const timeout = setTimeout(() => {
344
+ dispatch({ type: 'COMPILE' });
345
+ const shader = compileShader(state.graph);
346
+ dispatch({ type: 'SET_COMPILED_SHADER', shader });
347
+ }, 300);
348
+ return () => clearTimeout(timeout);
349
+ }
350
+ }, [state.graph, state.autoCompile]);
351
+ return { state, dispatch };
352
+ }
353
+ function NodePalette({ searchQuery, expandedCategories, onSearch, onToggleCategory, onAddNode, onDragStart, }) {
354
+ const filteredNodes = searchQuery
355
+ ? searchNodes(searchQuery)
356
+ : MATERIAL_NODE_DEFINITIONS;
357
+ const categories = useMemo(() => {
358
+ const cats = new Map();
359
+ for (const node of filteredNodes) {
360
+ if (!cats.has(node.category)) {
361
+ cats.set(node.category, []);
362
+ }
363
+ cats.get(node.category).push(node);
364
+ }
365
+ return cats;
366
+ }, [filteredNodes]);
367
+ return (jsxs("div", { className: styles.palette, children: [jsx("div", { className: styles.paletteSearch, children: jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: e => onSearch(e.target.value), className: styles.searchInput }) }), jsx("div", { className: styles.paletteCategories, children: Array.from(categories.entries()).map(([category, nodes]) => (jsxs("div", { className: styles.paletteCategory, children: [jsxs("button", { className: styles.categoryHeader, onClick: () => onToggleCategory(category), style: { borderLeftColor: NODE_CATEGORY_COLORS[category] }, children: [jsx("span", { className: styles.categoryIcon, children: expandedCategories.includes(category) ? '▼' : '►' }), jsx("span", { children: NODE_CATEGORY_NAMES[category] }), jsx("span", { className: styles.categoryCount, children: nodes.length })] }), expandedCategories.includes(category) && (jsx("div", { className: styles.categoryNodes, children: nodes.map(nodeDef => (jsxs("div", { className: styles.paletteNode, draggable: true, onDragStart: e => onDragStart(nodeDef.type, e), onDoubleClick: () => onAddNode(nodeDef.type), title: nodeDef.description, children: [jsx("span", { className: styles.nodeColor, style: { backgroundColor: nodeDef.color || NODE_CATEGORY_COLORS[nodeDef.category] } }), jsx("span", { className: styles.nodeName, children: nodeDef.name })] }, nodeDef.type))) }))] }, category))) })] }));
368
+ }
369
+ function GraphNode({ node, definition, selected, viewTransform, connections, onSelect, onMove, onStartConnection, }) {
370
+ const nodeRef = useRef(null);
371
+ const [isDragging, setIsDragging] = useState(false);
372
+ const dragStart = useRef(null);
373
+ const handleMouseDown = (e) => {
374
+ if (e.button !== 0)
375
+ return;
376
+ e.stopPropagation();
377
+ if (!selected) {
378
+ onSelect(node.id, e.shiftKey || e.ctrlKey);
379
+ }
380
+ if (!node.locked) {
381
+ setIsDragging(true);
382
+ dragStart.current = { x: e.clientX, y: e.clientY };
383
+ }
384
+ };
385
+ useEffect(() => {
386
+ if (!isDragging)
387
+ return;
388
+ const handleMouseMove = (e) => {
389
+ if (dragStart.current) {
390
+ const delta = {
391
+ x: (e.clientX - dragStart.current.x) / viewTransform.zoom,
392
+ y: (e.clientY - dragStart.current.y) / viewTransform.zoom,
393
+ };
394
+ dragStart.current = { x: e.clientX, y: e.clientY };
395
+ onMove(node.id, delta);
396
+ }
397
+ };
398
+ const handleMouseUp = () => {
399
+ setIsDragging(false);
400
+ dragStart.current = null;
401
+ };
402
+ document.addEventListener('mousemove', handleMouseMove);
403
+ document.addEventListener('mouseup', handleMouseUp);
404
+ return () => {
405
+ document.removeEventListener('mousemove', handleMouseMove);
406
+ document.removeEventListener('mouseup', handleMouseUp);
407
+ };
408
+ }, [isDragging, viewTransform.zoom, node.id, onMove]);
409
+ const handlePortMouseDown = (e, portId, portType) => {
410
+ e.stopPropagation();
411
+ const rect = e.target.getBoundingClientRect();
412
+ const position = {
413
+ x: rect.left + rect.width / 2,
414
+ y: rect.top + rect.height / 2,
415
+ };
416
+ onStartConnection(node.id, portId, portType, position);
417
+ };
418
+ const nodeColor = definition.color || NODE_CATEGORY_COLORS[definition.category];
419
+ const maxPorts = Math.max(definition.inputs.length, definition.outputs.length);
420
+ const nodeHeight = node.collapsed
421
+ ? NODE_HEADER_HEIGHT
422
+ : NODE_HEADER_HEIGHT + maxPorts * PORT_HEIGHT + 8;
423
+ return (jsxs("div", { ref: nodeRef, className: `${styles.graphNode} ${selected ? styles.selected : ''} ${node.locked ? styles.locked : ''}`, style: {
424
+ left: node.position.x * viewTransform.zoom + viewTransform.x,
425
+ top: node.position.y * viewTransform.zoom + viewTransform.y,
426
+ width: NODE_WIDTH * viewTransform.zoom,
427
+ height: nodeHeight * viewTransform.zoom,
428
+ transform: `scale(${viewTransform.zoom})`,
429
+ transformOrigin: 'top left',
430
+ }, onMouseDown: handleMouseDown, children: [jsxs("div", { className: styles.nodeHeader, style: { backgroundColor: nodeColor }, children: [jsx("span", { className: styles.nodeTitle, children: definition.name }), node.locked && jsx("span", { className: styles.lockIcon, children: "\uD83D\uDD12" })] }), !node.collapsed && (jsxs("div", { className: styles.nodePorts, children: [jsx("div", { className: styles.inputPorts, children: definition.inputs.map((port, index) => {
431
+ const isConnected = connections.some(c => c.targetNodeId === node.id && c.targetPortId === port.id);
432
+ return (jsxs("div", { className: styles.port, style: { top: index * PORT_HEIGHT + NODE_HEADER_HEIGHT }, children: [jsx("div", { className: `${styles.portHandle} ${styles.inputHandle} ${isConnected ? styles.connected : ''}`, style: { backgroundColor: PORT_TYPE_COLORS[port.dataType] }, onMouseDown: e => handlePortMouseDown(e, port.id, 'input') }), jsx("span", { className: styles.portLabel, children: port.name })] }, port.id));
433
+ }) }), jsx("div", { className: styles.outputPorts, children: definition.outputs.map((port, index) => {
434
+ const isConnected = connections.some(c => c.sourceNodeId === node.id && c.sourcePortId === port.id);
435
+ return (jsxs("div", { className: styles.port, style: { top: index * PORT_HEIGHT + NODE_HEADER_HEIGHT }, children: [jsx("span", { className: styles.portLabel, children: port.name }), jsx("div", { className: `${styles.portHandle} ${styles.outputHandle} ${isConnected ? styles.connected : ''}`, style: { backgroundColor: PORT_TYPE_COLORS[port.dataType] }, onMouseDown: e => handlePortMouseDown(e, port.id, 'output') })] }, port.id));
436
+ }) })] })), node.comment && (jsx("div", { className: styles.nodeComment, children: node.comment }))] }));
437
+ }
438
+ function Toolbar({ canUndo, canRedo, canPaste, hasSelection, autoCompile, compiling, showGrid, snapToGrid, onUndo, onRedo, onCopy, onCut, onPaste, onDuplicate, onDelete, onSelectAll, onZoomToFit, onToggleGrid, onToggleSnap, onToggleAutoCompile, onCompile, onNew, onExport, onImport, }) {
439
+ return (jsxs("div", { className: styles.toolbar, children: [jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onNew, title: "New Material (Ctrl+N)", children: "\uD83D\uDCC4" }), jsx("button", { onClick: onImport, title: "Import (Ctrl+O)", children: "\uD83D\uDCC2" }), jsx("button", { onClick: onExport, title: "Export (Ctrl+S)", children: "\uD83D\uDCBE" })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onUndo, disabled: !canUndo, title: "Undo (Ctrl+Z)", children: "\u21A9\uFE0F" }), jsx("button", { onClick: onRedo, disabled: !canRedo, title: "Redo (Ctrl+Y)", children: "\u21AA\uFE0F" })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onCut, disabled: !hasSelection, title: "Cut (Ctrl+X)", children: "\u2702\uFE0F" }), jsx("button", { onClick: onCopy, disabled: !hasSelection, title: "Copy (Ctrl+C)", children: "\uD83D\uDCCB" }), jsx("button", { onClick: onPaste, disabled: !canPaste, title: "Paste (Ctrl+V)", children: "\uD83D\uDCE5" }), jsx("button", { onClick: onDuplicate, disabled: !hasSelection, title: "Duplicate (Ctrl+D)", children: "\uD83D\uDD04" }), jsx("button", { onClick: onDelete, disabled: !hasSelection, title: "Delete (Del)", children: "\uD83D\uDDD1\uFE0F" })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onSelectAll, title: "Select All (Ctrl+A)", children: "\u2B1C" }), jsx("button", { onClick: onZoomToFit, title: "Zoom to Fit (F)", children: "\uD83D\uDD0D" }), jsx("button", { onClick: onToggleGrid, className: showGrid ? styles.active : '', title: "Toggle Grid (G)", children: "#" }), jsx("button", { onClick: onToggleSnap, className: snapToGrid ? styles.active : '', title: "Toggle Snap (S)", children: "\uD83E\uDDF2" })] }), jsx("div", { className: styles.toolbarSeparator }), jsxs("div", { className: styles.toolbarGroup, children: [jsx("button", { onClick: onToggleAutoCompile, className: autoCompile ? styles.active : '', title: "Auto Compile", children: "\u26A1" }), jsx("button", { onClick: onCompile, disabled: compiling, title: "Compile (F5)", children: compiling ? '⏳' : '▶️' })] })] }));
440
+ }
441
+ function PreviewPanel({ shader, mode, compiling, onModeChange }) {
442
+ const canvasRef = useRef(null);
443
+ const previewModes = ['sphere', 'cube', 'plane', 'cylinder', 'torus'];
444
+ // Simple preview rendering (in real implementation would use Three.js)
445
+ useEffect(() => {
446
+ const canvas = canvasRef.current;
447
+ if (!canvas)
448
+ return;
449
+ const ctx = canvas.getContext('2d');
450
+ if (!ctx)
451
+ return;
452
+ // Simple gradient preview
453
+ const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
454
+ if ((shader === null || shader === void 0 ? void 0 : shader.errors.length) === 0) {
455
+ gradient.addColorStop(0, '#4CAF50');
456
+ gradient.addColorStop(1, '#2E7D32');
457
+ }
458
+ else {
459
+ gradient.addColorStop(0, '#f44336');
460
+ gradient.addColorStop(1, '#c62828');
461
+ }
462
+ ctx.fillStyle = '#1e1e1e';
463
+ ctx.fillRect(0, 0, 200, 200);
464
+ ctx.fillStyle = gradient;
465
+ ctx.beginPath();
466
+ ctx.arc(100, 100, 80, 0, Math.PI * 2);
467
+ ctx.fill();
468
+ }, [shader, mode]);
469
+ return (jsxs("div", { className: styles.previewPanel, children: [jsxs("div", { className: styles.previewHeader, children: [jsx("span", { children: "Preview" }), jsx("select", { value: mode, onChange: e => onModeChange(e.target.value), className: styles.previewModeSelect, children: previewModes.map(m => (jsx("option", { value: m, children: m.charAt(0).toUpperCase() + m.slice(1) }, m))) })] }), jsxs("div", { className: styles.previewCanvas, children: [compiling && jsx("div", { className: styles.previewCompiling, children: "Compiling..." }), jsx("canvas", { ref: canvasRef, width: 200, height: 200 })] }), (shader === null || shader === void 0 ? void 0 : shader.errors.length) ? (jsx("div", { className: styles.previewErrors, children: shader.errors.slice(0, 3).map((err, i) => (jsx("div", { className: styles.previewError, children: err.message }, i))) })) : null] }));
470
+ }
471
+ /* ═══════════════════════════════════════════════════════════════════════════
472
+ Main Component
473
+ ═══════════════════════════════════════════════════════════════════════════ */
474
+ function NiceMaterialEditor({ graph: initialGraph, onGraphChange, onCompile, customNodes, textures, width = '100%', height = '600px', readOnly = false, showToolbar = true, showPalette = true, showProperties = true, showPreview = true, previewMode: initialPreviewMode = 'sphere', theme = 'dark', className, }) {
475
+ const { state, dispatch } = useMaterialEditor(initialGraph);
476
+ const canvasRef = useRef(null);
477
+ const [pendingConnection, setPendingConnection] = useState(null);
478
+ // Merge custom node definitions
479
+ const definitions = useMemo(() => {
480
+ const map = new Map(MATERIAL_NODE_MAP);
481
+ customNodes === null || customNodes === void 0 ? void 0 : customNodes.forEach(def => map.set(def.type, def));
482
+ return map;
483
+ }, [customNodes]);
484
+ // Notify parent of changes
485
+ useEffect(() => {
486
+ onGraphChange === null || onGraphChange === void 0 ? void 0 : onGraphChange(state.graph);
487
+ }, [state.graph, onGraphChange]);
488
+ useEffect(() => {
489
+ if (state.compiledShader) {
490
+ onCompile === null || onCompile === void 0 ? void 0 : onCompile(state.compiledShader);
491
+ }
492
+ }, [state.compiledShader, onCompile]);
493
+ // Keyboard shortcuts
494
+ useEffect(() => {
495
+ const handleKeyDown = (e) => {
496
+ if (readOnly)
497
+ return;
498
+ const isCtrl = e.ctrlKey || e.metaKey;
499
+ if (isCtrl && e.key === 'z') {
500
+ e.preventDefault();
501
+ dispatch({ type: 'UNDO' });
502
+ }
503
+ else if (isCtrl && e.key === 'y') {
504
+ e.preventDefault();
505
+ dispatch({ type: 'REDO' });
506
+ }
507
+ else if (isCtrl && e.key === 'c') {
508
+ e.preventDefault();
509
+ dispatch({ type: 'COPY' });
510
+ }
511
+ else if (isCtrl && e.key === 'x') {
512
+ e.preventDefault();
513
+ dispatch({ type: 'CUT' });
514
+ }
515
+ else if (isCtrl && e.key === 'v') {
516
+ e.preventDefault();
517
+ dispatch({ type: 'PASTE', position: { x: 100, y: 100 } });
518
+ }
519
+ else if (isCtrl && e.key === 'd') {
520
+ e.preventDefault();
521
+ dispatch({ type: 'DUPLICATE' });
522
+ }
523
+ else if (isCtrl && e.key === 'a') {
524
+ e.preventDefault();
525
+ dispatch({ type: 'SELECT_ALL' });
526
+ }
527
+ else if (e.key === 'Delete' || e.key === 'Backspace') {
528
+ if (state.selectedNodes.length > 0) {
529
+ e.preventDefault();
530
+ dispatch({ type: 'DELETE_NODES', nodeIds: state.selectedNodes });
531
+ }
532
+ }
533
+ else if (e.key === 'f' || e.key === 'F') {
534
+ e.preventDefault();
535
+ dispatch({ type: 'ZOOM_TO_FIT' });
536
+ }
537
+ else if (e.key === 'g' || e.key === 'G') {
538
+ e.preventDefault();
539
+ dispatch({ type: 'TOGGLE_GRID' });
540
+ }
541
+ else if (e.key === 's' && !isCtrl) {
542
+ e.preventDefault();
543
+ dispatch({ type: 'TOGGLE_SNAP' });
544
+ }
545
+ else if (e.key === 'Escape') {
546
+ dispatch({ type: 'DESELECT_ALL' });
547
+ setPendingConnection(null);
548
+ }
549
+ };
550
+ window.addEventListener('keydown', handleKeyDown);
551
+ return () => window.removeEventListener('keydown', handleKeyDown);
552
+ }, [readOnly, state.selectedNodes, dispatch]);
553
+ // Handle wheel zoom
554
+ const handleWheel = useCallback((e) => {
555
+ var _a;
556
+ e.preventDefault();
557
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
558
+ const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, state.viewTransform.zoom * delta));
559
+ const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
560
+ if (!rect)
561
+ return;
562
+ const mouseX = e.clientX - rect.left;
563
+ const mouseY = e.clientY - rect.top;
564
+ const newX = mouseX - (mouseX - state.viewTransform.x) * (newZoom / state.viewTransform.zoom);
565
+ const newY = mouseY - (mouseY - state.viewTransform.y) * (newZoom / state.viewTransform.zoom);
566
+ dispatch({
567
+ type: 'SET_VIEW_TRANSFORM',
568
+ transform: { x: newX, y: newY, zoom: newZoom },
569
+ });
570
+ }, [state.viewTransform, dispatch]);
571
+ // Handle canvas pan
572
+ const [isPanning, setIsPanning] = useState(false);
573
+ const panStart = useRef(null);
574
+ const handleCanvasMouseDown = (e) => {
575
+ if (e.button === 1 || (e.button === 0 && e.altKey)) {
576
+ setIsPanning(true);
577
+ panStart.current = { x: e.clientX, y: e.clientY };
578
+ }
579
+ else if (e.button === 0 && e.target === canvasRef.current) {
580
+ dispatch({ type: 'DESELECT_ALL' });
581
+ }
582
+ };
583
+ const handleCanvasMouseMove = (e) => {
584
+ if (isPanning && panStart.current) {
585
+ const delta = {
586
+ x: e.clientX - panStart.current.x,
587
+ y: e.clientY - panStart.current.y,
588
+ };
589
+ panStart.current = { x: e.clientX, y: e.clientY };
590
+ dispatch({
591
+ type: 'SET_VIEW_TRANSFORM',
592
+ transform: {
593
+ ...state.viewTransform,
594
+ x: state.viewTransform.x + delta.x,
595
+ y: state.viewTransform.y + delta.y,
596
+ },
597
+ });
598
+ }
599
+ if (pendingConnection) {
600
+ setPendingConnection({
601
+ ...pendingConnection,
602
+ currentPosition: { x: e.clientX, y: e.clientY },
603
+ });
604
+ }
605
+ };
606
+ const handleCanvasMouseUp = () => {
607
+ setIsPanning(false);
608
+ panStart.current = null;
609
+ };
610
+ // Handle drag and drop from palette
611
+ const handleDragOver = (e) => {
612
+ e.preventDefault();
613
+ };
614
+ const handleDrop = (e) => {
615
+ var _a;
616
+ e.preventDefault();
617
+ const nodeType = e.dataTransfer.getData('text/plain');
618
+ if (!nodeType)
619
+ return;
620
+ const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
621
+ if (!rect)
622
+ return;
623
+ const position = screenToGraph({ x: e.clientX - rect.left, y: e.clientY - rect.top }, state.viewTransform);
624
+ dispatch({ type: 'ADD_NODE', nodeType, position });
625
+ };
626
+ // Palette handlers
627
+ const handlePaletteAddNode = (nodeType) => {
628
+ const position = { x: 200, y: 200 };
629
+ dispatch({ type: 'ADD_NODE', nodeType, position });
630
+ };
631
+ const handlePaletteDragStart = (nodeType, e) => {
632
+ e.dataTransfer.setData('text/plain', nodeType);
633
+ };
634
+ // Connection handlers
635
+ const handleStartConnection = (nodeId, portId, portType, position) => {
636
+ setPendingConnection({
637
+ nodeId,
638
+ portId,
639
+ portType,
640
+ startPosition: position,
641
+ currentPosition: position,
642
+ });
643
+ };
644
+ // Export/Import
645
+ const handleExport = () => {
646
+ const json = exportGraphToJSON(state.graph);
647
+ const blob = new Blob([json], { type: 'application/json' });
648
+ const url = URL.createObjectURL(blob);
649
+ const a = document.createElement('a');
650
+ a.href = url;
651
+ a.download = `${state.graph.name}.material.json`;
652
+ a.click();
653
+ URL.revokeObjectURL(url);
654
+ };
655
+ const handleImport = () => {
656
+ const input = document.createElement('input');
657
+ input.type = 'file';
658
+ input.accept = '.json';
659
+ input.onchange = async (e) => {
660
+ var _a;
661
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
662
+ if (!file)
663
+ return;
664
+ const text = await file.text();
665
+ const graph = importGraphFromJSON(text);
666
+ if (graph) {
667
+ dispatch({ type: 'LOAD_GRAPH', graph });
668
+ }
669
+ };
670
+ input.click();
671
+ };
672
+ // Render connections
673
+ const renderConnections = () => {
674
+ return state.graph.connections.map(conn => {
675
+ const sourceNode = state.graph.nodes.find(n => n.id === conn.sourceNodeId);
676
+ const targetNode = state.graph.nodes.find(n => n.id === conn.targetNodeId);
677
+ if (!sourceNode || !targetNode)
678
+ return null;
679
+ const sourceDef = definitions.get(sourceNode.type);
680
+ const targetDef = definitions.get(targetNode.type);
681
+ if (!sourceDef || !targetDef)
682
+ return null;
683
+ const sourcePortIndex = sourceDef.outputs.findIndex(p => p.id === conn.sourcePortId);
684
+ const targetPortIndex = targetDef.inputs.findIndex(p => p.id === conn.targetPortId);
685
+ if (sourcePortIndex === -1 || targetPortIndex === -1)
686
+ return null;
687
+ const sourcePort = sourceDef.outputs[sourcePortIndex];
688
+ const x1 = (sourceNode.position.x + NODE_WIDTH) * state.viewTransform.zoom + state.viewTransform.x;
689
+ const y1 = (sourceNode.position.y + NODE_HEADER_HEIGHT + sourcePortIndex * PORT_HEIGHT + PORT_HEIGHT / 2) * state.viewTransform.zoom + state.viewTransform.y;
690
+ const x2 = targetNode.position.x * state.viewTransform.zoom + state.viewTransform.x;
691
+ const y2 = (targetNode.position.y + NODE_HEADER_HEIGHT + targetPortIndex * PORT_HEIGHT + PORT_HEIGHT / 2) * state.viewTransform.zoom + state.viewTransform.y;
692
+ const isSelected = state.selectedConnections.includes(conn.id);
693
+ const controlX = (x1 + x2) / 2;
694
+ return (jsx("path", { d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`, fill: "none", stroke: isSelected ? '#fff' : PORT_TYPE_COLORS[sourcePort.dataType], strokeWidth: isSelected ? 3 : 2, className: styles.connection, onClick: () => dispatch({ type: 'SELECT_CONNECTIONS', connectionIds: [conn.id] }) }, conn.id));
695
+ });
696
+ };
697
+ // Render pending connection
698
+ const renderPendingConnection = () => {
699
+ var _a;
700
+ if (!pendingConnection)
701
+ return null;
702
+ const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
703
+ if (!rect)
704
+ return null;
705
+ const x1 = pendingConnection.startPosition.x - rect.left;
706
+ const y1 = pendingConnection.startPosition.y - rect.top;
707
+ const x2 = pendingConnection.currentPosition.x - rect.left;
708
+ const y2 = pendingConnection.currentPosition.y - rect.top;
709
+ const controlX = (x1 + x2) / 2;
710
+ return (jsx("path", { d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`, fill: "none", stroke: "#fff", strokeWidth: 2, strokeDasharray: "5,5", className: styles.pendingConnection }));
711
+ };
712
+ // Render grid
713
+ const renderGrid = () => {
714
+ if (!state.showGrid)
715
+ return null;
716
+ const gridSize = state.gridSize * state.viewTransform.zoom;
717
+ const offsetX = state.viewTransform.x % gridSize;
718
+ const offsetY = state.viewTransform.y % gridSize;
719
+ return (jsx("pattern", { id: "grid", width: gridSize, height: gridSize, patternUnits: "userSpaceOnUse", x: offsetX, y: offsetY, children: jsx("path", { d: `M ${gridSize} 0 L 0 0 0 ${gridSize}`, fill: "none", stroke: "rgba(255,255,255,0.1)", strokeWidth: "1" }) }));
720
+ };
721
+ return (jsxs("div", { className: `${styles.materialEditor} ${styles[theme]} ${className || ''}`, style: { width, height }, children: [showToolbar && (jsx(Toolbar, { canUndo: state.undoStack.length > 0, canRedo: state.redoStack.length > 0, canPaste: state.clipboard !== null, hasSelection: state.selectedNodes.length > 0, autoCompile: state.autoCompile, compiling: state.compiling, showGrid: state.showGrid, snapToGrid: state.snapToGrid, onUndo: () => dispatch({ type: 'UNDO' }), onRedo: () => dispatch({ type: 'REDO' }), onCopy: () => dispatch({ type: 'COPY' }), onCut: () => dispatch({ type: 'CUT' }), onPaste: () => dispatch({ type: 'PASTE', position: { x: 100, y: 100 } }), onDuplicate: () => dispatch({ type: 'DUPLICATE' }), onDelete: () => dispatch({ type: 'DELETE_NODES', nodeIds: state.selectedNodes }), onSelectAll: () => dispatch({ type: 'SELECT_ALL' }), onZoomToFit: () => dispatch({ type: 'ZOOM_TO_FIT' }), onToggleGrid: () => dispatch({ type: 'TOGGLE_GRID' }), onToggleSnap: () => dispatch({ type: 'TOGGLE_SNAP' }), onToggleAutoCompile: () => dispatch({ type: 'TOGGLE_AUTO_COMPILE' }), onCompile: () => {
722
+ dispatch({ type: 'COMPILE' });
723
+ const shader = compileShader(state.graph, definitions);
724
+ dispatch({ type: 'SET_COMPILED_SHADER', shader });
725
+ }, onNew: () => dispatch({ type: 'NEW_GRAPH' }), onExport: handleExport, onImport: handleImport })), jsxs("div", { className: styles.editorBody, children: [showPalette && (jsx(NodePalette, { searchQuery: state.searchQuery, expandedCategories: state.expandedCategories, onSearch: query => dispatch({ type: 'SET_SEARCH_QUERY', query }), onToggleCategory: cat => dispatch({ type: 'TOGGLE_CATEGORY', category: cat }), onAddNode: handlePaletteAddNode, onDragStart: handlePaletteDragStart })), jsxs("div", { ref: canvasRef, className: styles.graphCanvas, onWheel: handleWheel, onMouseDown: handleCanvasMouseDown, onMouseMove: handleCanvasMouseMove, onMouseUp: handleCanvasMouseUp, onMouseLeave: handleCanvasMouseUp, onDragOver: handleDragOver, onDrop: handleDrop, children: [jsxs("svg", { className: styles.connectionsSvg, children: [jsx("defs", { children: renderGrid() }), state.showGrid && jsx("rect", { width: "100%", height: "100%", fill: "url(#grid)" }), renderConnections(), renderPendingConnection()] }), state.graph.nodes.map(node => {
726
+ const def = definitions.get(node.type);
727
+ if (!def)
728
+ return null;
729
+ return (jsx(GraphNode, { node: node, definition: def, selected: state.selectedNodes.includes(node.id), viewTransform: state.viewTransform, connections: state.graph.connections, onSelect: (nodeId, additive) => dispatch({ type: 'SELECT_NODES', nodeIds: [nodeId], additive }), onMove: (nodeId, delta) => dispatch({ type: 'MOVE_NODES', nodeIds: state.selectedNodes.includes(nodeId) ? state.selectedNodes : [nodeId], delta }), onStartConnection: handleStartConnection }, node.id));
730
+ })] }), showPreview && (jsx(PreviewPanel, { shader: state.compiledShader, mode: state.previewMode, compiling: state.compiling, onModeChange: mode => dispatch({ type: 'SET_PREVIEW_MODE', mode }) }))] })] }));
731
+ }
732
+
733
+ export { NiceMaterialEditor, useMaterialEditor };
734
+ //# sourceMappingURL=NiceMaterialEditor.js.map