@nice2dev/ui-3d 1.3.15 → 1.3.17

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 (108) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,796 +1,1252 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var ui = require('@nice2dev/ui');
5
- var React = require('react');
6
- var i18n = require('../core/i18n.js');
7
- var MaterialEditor_module = require('./MaterialEditor.module.css.js');
8
- var materialEditorTypes = require('./materialEditorTypes.js');
9
- var materialEditorUtils = require('./materialEditorUtils.js');
10
- var materialNodeDefinitions = require('./materialNodeDefinitions.js');
11
-
12
- /* ═══════════════════════════════════════════════════════════════════════════
13
- State Reducer
14
- ═══════════════════════════════════════════════════════════════════════════ */
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const ui = require("@nice2dev/ui");
5
+ const React = require("react");
6
+ const i18n = require("../core/i18n.js");
7
+ const MaterialEditor_module = require("./MaterialEditor.module.css.js");
8
+ const materialEditorTypes = require("./materialEditorTypes.js");
9
+ const materialEditorUtils = require("./materialEditorUtils.js");
10
+ const materialNodeDefinitions = require("./materialNodeDefinitions.js");
15
11
  function createInitialState(graph) {
16
- const initialGraph = graph || materialEditorUtils.createEmptyGraph();
17
- return {
18
- graph: initialGraph,
19
- selectedNodes: [],
20
- selectedConnections: [],
21
- clipboard: null,
22
- viewTransform: { x: 100, y: 100, zoom: 1 },
23
- previewMode: 'sphere',
24
- showGrid: true,
25
- showMinimap: true,
26
- snapToGrid: true,
27
- gridSize: materialEditorTypes.DEFAULT_GRID_SIZE,
28
- undoStack: [],
29
- redoStack: [],
30
- compiledShader: null,
31
- compiling: false,
32
- autoCompile: true,
33
- searchQuery: '',
34
- expandedCategories: ['input', 'output', 'math', 'color', 'texture'],
35
- };
12
+ const initialGraph = graph || materialEditorUtils.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: materialEditorTypes.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
+ };
36
32
  }
37
33
  function pushToUndoStack(state) {
38
- return {
39
- ...state,
40
- undoStack: [...state.undoStack.slice(-49), state.graph],
41
- redoStack: [],
42
- };
34
+ return {
35
+ ...state,
36
+ undoStack: [...state.undoStack.slice(-49), state.graph],
37
+ redoStack: []
38
+ };
43
39
  }
44
40
  function materialEditorReducer(state, action) {
45
- switch (action.type) {
46
- case 'ADD_NODE': {
47
- const node = materialEditorUtils.createNode(action.nodeType, action.position);
48
- if (!node) {
49
- return state;
50
- }
51
- const newState = pushToUndoStack(state);
52
- return {
53
- ...newState,
54
- graph: materialEditorUtils.addNodeToGraph(newState.graph, node),
55
- selectedNodes: [node.id],
56
- };
57
- }
58
- case 'DELETE_NODES': {
59
- const newState = pushToUndoStack(state);
60
- return {
61
- ...newState,
62
- graph: materialEditorUtils.removeNodesFromGraph(newState.graph, action.nodeIds),
63
- selectedNodes: [],
64
- selectedConnections: [],
65
- };
66
- }
67
- case 'MOVE_NODES': {
68
- return {
69
- ...state,
70
- graph: materialEditorUtils.moveNodesInGraph(state.graph, action.nodeIds, action.delta),
71
- };
72
- }
73
- case 'UPDATE_NODE_PROPERTY': {
74
- const newState = pushToUndoStack(state);
75
- const node = newState.graph.nodes.find((n) => n.id === action.nodeId);
76
- if (!node) {
77
- return state;
78
- }
79
- return {
80
- ...newState,
81
- graph: materialEditorUtils.updateNodeInGraph(newState.graph, action.nodeId, {
82
- properties: { ...node.properties, [action.propertyId]: action.value },
83
- }),
84
- };
85
- }
86
- case 'UPDATE_INPUT_VALUE': {
87
- const newState = pushToUndoStack(state);
88
- const node = newState.graph.nodes.find((n) => n.id === action.nodeId);
89
- if (!node) {
90
- return state;
91
- }
92
- return {
93
- ...newState,
94
- graph: materialEditorUtils.updateNodeInGraph(newState.graph, action.nodeId, {
95
- inputValues: { ...node.inputValues, [action.portId]: action.value },
96
- }),
97
- };
98
- }
99
- case 'CONNECT': {
100
- const sourceNode = state.graph.nodes.find((n) => n.id === action.connection.sourceNodeId);
101
- const targetNode = state.graph.nodes.find((n) => n.id === action.connection.targetNodeId);
102
- if (!sourceNode || !targetNode) {
103
- return state;
104
- }
105
- const sourceDef = materialEditorUtils.getNodeDefinition(sourceNode);
106
- const targetDef = materialEditorUtils.getNodeDefinition(targetNode);
107
- if (!sourceDef || !targetDef) {
108
- return state;
109
- }
110
- const sourcePort = sourceDef.outputs.find((p) => p.id === action.connection.sourcePortId);
111
- const targetPort = targetDef.inputs.find((p) => p.id === action.connection.targetPortId);
112
- if (!sourcePort || !targetPort) {
113
- return state;
114
- }
115
- const { valid } = materialEditorUtils.canConnect(sourceNode, sourcePort, targetNode, targetPort, state.graph.connections);
116
- if (!valid) {
117
- return state;
118
- }
119
- const newState = pushToUndoStack(state);
120
- const connection = materialEditorUtils.createConnection(action.connection.sourceNodeId, action.connection.sourcePortId, action.connection.targetNodeId, action.connection.targetPortId);
121
- return {
122
- ...newState,
123
- graph: materialEditorUtils.addConnectionToGraph(newState.graph, connection),
124
- };
125
- }
126
- case 'DISCONNECT': {
127
- const newState = pushToUndoStack(state);
128
- return {
129
- ...newState,
130
- graph: materialEditorUtils.removeConnectionFromGraph(newState.graph, action.connectionId),
131
- selectedConnections: state.selectedConnections.filter((id) => id !== action.connectionId),
132
- };
41
+ switch (action.type) {
42
+ case "ADD_NODE": {
43
+ const node = materialEditorUtils.createNode(action.nodeType, action.position);
44
+ if (!node) {
45
+ return state;
46
+ }
47
+ const newState = pushToUndoStack(state);
48
+ return {
49
+ ...newState,
50
+ graph: materialEditorUtils.addNodeToGraph(newState.graph, node),
51
+ selectedNodes: [node.id]
52
+ };
53
+ }
54
+ case "DELETE_NODES": {
55
+ const newState = pushToUndoStack(state);
56
+ return {
57
+ ...newState,
58
+ graph: materialEditorUtils.removeNodesFromGraph(newState.graph, action.nodeIds),
59
+ selectedNodes: [],
60
+ selectedConnections: []
61
+ };
62
+ }
63
+ case "MOVE_NODES": {
64
+ return {
65
+ ...state,
66
+ graph: materialEditorUtils.moveNodesInGraph(state.graph, action.nodeIds, action.delta)
67
+ };
68
+ }
69
+ case "UPDATE_NODE_PROPERTY": {
70
+ const newState = pushToUndoStack(state);
71
+ const node = newState.graph.nodes.find((n) => n.id === action.nodeId);
72
+ if (!node) {
73
+ return state;
74
+ }
75
+ return {
76
+ ...newState,
77
+ graph: materialEditorUtils.updateNodeInGraph(newState.graph, action.nodeId, {
78
+ properties: { ...node.properties, [action.propertyId]: action.value }
79
+ })
80
+ };
81
+ }
82
+ case "UPDATE_INPUT_VALUE": {
83
+ const newState = pushToUndoStack(state);
84
+ const node = newState.graph.nodes.find((n) => n.id === action.nodeId);
85
+ if (!node) {
86
+ return state;
87
+ }
88
+ return {
89
+ ...newState,
90
+ graph: materialEditorUtils.updateNodeInGraph(newState.graph, action.nodeId, {
91
+ inputValues: { ...node.inputValues, [action.portId]: action.value }
92
+ })
93
+ };
94
+ }
95
+ case "CONNECT": {
96
+ const sourceNode = state.graph.nodes.find((n) => n.id === action.connection.sourceNodeId);
97
+ const targetNode = state.graph.nodes.find((n) => n.id === action.connection.targetNodeId);
98
+ if (!sourceNode || !targetNode) {
99
+ return state;
100
+ }
101
+ const sourceDef = materialEditorUtils.getNodeDefinition(sourceNode);
102
+ const targetDef = materialEditorUtils.getNodeDefinition(targetNode);
103
+ if (!sourceDef || !targetDef) {
104
+ return state;
105
+ }
106
+ const sourcePort = sourceDef.outputs.find((p) => p.id === action.connection.sourcePortId);
107
+ const targetPort = targetDef.inputs.find((p) => p.id === action.connection.targetPortId);
108
+ if (!sourcePort || !targetPort) {
109
+ return state;
110
+ }
111
+ const { valid } = materialEditorUtils.canConnect(
112
+ sourceNode,
113
+ sourcePort,
114
+ targetNode,
115
+ targetPort,
116
+ state.graph.connections
117
+ );
118
+ if (!valid) {
119
+ return state;
120
+ }
121
+ const newState = pushToUndoStack(state);
122
+ const connection = materialEditorUtils.createConnection(
123
+ action.connection.sourceNodeId,
124
+ action.connection.sourcePortId,
125
+ action.connection.targetNodeId,
126
+ action.connection.targetPortId
127
+ );
128
+ return {
129
+ ...newState,
130
+ graph: materialEditorUtils.addConnectionToGraph(newState.graph, connection)
131
+ };
132
+ }
133
+ case "DISCONNECT": {
134
+ const newState = pushToUndoStack(state);
135
+ return {
136
+ ...newState,
137
+ graph: materialEditorUtils.removeConnectionFromGraph(newState.graph, action.connectionId),
138
+ selectedConnections: state.selectedConnections.filter((id) => id !== action.connectionId)
139
+ };
140
+ }
141
+ case "SELECT_NODES": {
142
+ return {
143
+ ...state,
144
+ selectedNodes: action.additive ? [.../* @__PURE__ */ new Set([...state.selectedNodes, ...action.nodeIds])] : action.nodeIds,
145
+ selectedConnections: action.additive ? state.selectedConnections : []
146
+ };
147
+ }
148
+ case "SELECT_CONNECTIONS": {
149
+ return {
150
+ ...state,
151
+ selectedConnections: action.additive ? [.../* @__PURE__ */ new Set([...state.selectedConnections, ...action.connectionIds])] : action.connectionIds,
152
+ selectedNodes: action.additive ? state.selectedNodes : []
153
+ };
154
+ }
155
+ case "SELECT_ALL": {
156
+ return {
157
+ ...state,
158
+ selectedNodes: state.graph.nodes.map((n) => n.id),
159
+ selectedConnections: state.graph.connections.map((c) => c.id)
160
+ };
161
+ }
162
+ case "DESELECT_ALL": {
163
+ return {
164
+ ...state,
165
+ selectedNodes: [],
166
+ selectedConnections: []
167
+ };
168
+ }
169
+ case "COPY": {
170
+ const nodesToCopy = state.graph.nodes.filter((n) => state.selectedNodes.includes(n.id));
171
+ const connectionsToCopy = state.graph.connections.filter(
172
+ (c) => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId)
173
+ );
174
+ return {
175
+ ...state,
176
+ clipboard: { nodes: nodesToCopy, connections: connectionsToCopy }
177
+ };
178
+ }
179
+ case "PASTE": {
180
+ if (!state.clipboard) {
181
+ return state;
182
+ }
183
+ const newState = pushToUndoStack(state);
184
+ const idMap = /* @__PURE__ */ new Map();
185
+ const newNodes = state.clipboard.nodes.map((node) => {
186
+ const newId = materialEditorUtils.generateNodeId();
187
+ idMap.set(node.id, newId);
188
+ return {
189
+ ...node,
190
+ id: newId,
191
+ position: {
192
+ x: node.position.x + action.position.x - state.clipboard.nodes[0].position.x + 20,
193
+ y: node.position.y + action.position.y - state.clipboard.nodes[0].position.y + 20
194
+ },
195
+ selected: false
196
+ };
197
+ });
198
+ const newConnections = state.clipboard.connections.map((conn) => ({
199
+ ...conn,
200
+ id: materialEditorUtils.generateNodeId(),
201
+ sourceNodeId: idMap.get(conn.sourceNodeId),
202
+ targetNodeId: idMap.get(conn.targetNodeId)
203
+ }));
204
+ return {
205
+ ...newState,
206
+ graph: {
207
+ ...newState.graph,
208
+ nodes: [...newState.graph.nodes, ...newNodes],
209
+ connections: [...newState.graph.connections, ...newConnections]
210
+ },
211
+ selectedNodes: newNodes.map((n) => n.id)
212
+ };
213
+ }
214
+ case "CUT": {
215
+ const nodesToCut = state.graph.nodes.filter(
216
+ (n) => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId
217
+ );
218
+ const connectionsToCut = state.graph.connections.filter(
219
+ (c) => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId)
220
+ );
221
+ const newState = pushToUndoStack(state);
222
+ return {
223
+ ...newState,
224
+ clipboard: { nodes: nodesToCut, connections: connectionsToCut },
225
+ graph: materialEditorUtils.removeNodesFromGraph(
226
+ newState.graph,
227
+ nodesToCut.map((n) => n.id)
228
+ ),
229
+ selectedNodes: []
230
+ };
231
+ }
232
+ case "DUPLICATE": {
233
+ const nodesToDupe = state.graph.nodes.filter(
234
+ (n) => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId
235
+ );
236
+ const newState = pushToUndoStack(state);
237
+ const idMap = /* @__PURE__ */ new Map();
238
+ const newNodes = nodesToDupe.map((node) => {
239
+ const cloned = materialEditorUtils.cloneNode(node);
240
+ idMap.set(node.id, cloned.id);
241
+ return cloned;
242
+ });
243
+ const connectionsToDupe = state.graph.connections.filter(
244
+ (c) => state.selectedNodes.includes(c.sourceNodeId) && state.selectedNodes.includes(c.targetNodeId)
245
+ );
246
+ const newConnections = connectionsToDupe.map((conn) => ({
247
+ ...conn,
248
+ id: materialEditorUtils.generateNodeId(),
249
+ sourceNodeId: idMap.get(conn.sourceNodeId),
250
+ targetNodeId: idMap.get(conn.targetNodeId)
251
+ }));
252
+ return {
253
+ ...newState,
254
+ graph: {
255
+ ...newState.graph,
256
+ nodes: [...newState.graph.nodes, ...newNodes],
257
+ connections: [...newState.graph.connections, ...newConnections]
258
+ },
259
+ selectedNodes: newNodes.map((n) => n.id)
260
+ };
261
+ }
262
+ case "UNDO": {
263
+ if (state.undoStack.length === 0) {
264
+ return state;
265
+ }
266
+ const previous = state.undoStack[state.undoStack.length - 1];
267
+ return {
268
+ ...state,
269
+ graph: previous,
270
+ undoStack: state.undoStack.slice(0, -1),
271
+ redoStack: [...state.redoStack, state.graph],
272
+ selectedNodes: [],
273
+ selectedConnections: []
274
+ };
275
+ }
276
+ case "REDO": {
277
+ if (state.redoStack.length === 0) {
278
+ return state;
279
+ }
280
+ const next = state.redoStack[state.redoStack.length - 1];
281
+ return {
282
+ ...state,
283
+ graph: next,
284
+ undoStack: [...state.undoStack, state.graph],
285
+ redoStack: state.redoStack.slice(0, -1),
286
+ selectedNodes: [],
287
+ selectedConnections: []
288
+ };
289
+ }
290
+ case "SET_VIEW_TRANSFORM":
291
+ return { ...state, viewTransform: action.transform };
292
+ case "ZOOM_TO_FIT": {
293
+ const transform = materialEditorUtils.calculateZoomToFit(state.graph.nodes, 800, 600);
294
+ return { ...state, viewTransform: transform };
295
+ }
296
+ case "SET_PREVIEW_MODE":
297
+ return { ...state, previewMode: action.mode };
298
+ case "TOGGLE_GRID":
299
+ return { ...state, showGrid: !state.showGrid };
300
+ case "TOGGLE_MINIMAP":
301
+ return { ...state, showMinimap: !state.showMinimap };
302
+ case "TOGGLE_SNAP":
303
+ return { ...state, snapToGrid: !state.snapToGrid };
304
+ case "SET_GRID_SIZE":
305
+ return { ...state, gridSize: action.size };
306
+ case "SET_SEARCH_QUERY":
307
+ return { ...state, searchQuery: action.query };
308
+ case "TOGGLE_CATEGORY":
309
+ return {
310
+ ...state,
311
+ expandedCategories: state.expandedCategories.includes(action.category) ? state.expandedCategories.filter((c) => c !== action.category) : [...state.expandedCategories, action.category]
312
+ };
313
+ case "COLLAPSE_NODE":
314
+ return {
315
+ ...state,
316
+ graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { collapsed: true })
317
+ };
318
+ case "EXPAND_NODE":
319
+ return {
320
+ ...state,
321
+ graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { collapsed: false })
322
+ };
323
+ case "ADD_COMMENT":
324
+ return {
325
+ ...state,
326
+ graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { comment: action.comment })
327
+ };
328
+ case "LOCK_NODE":
329
+ return {
330
+ ...state,
331
+ graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { locked: true })
332
+ };
333
+ case "UNLOCK_NODE":
334
+ return {
335
+ ...state,
336
+ graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { locked: false })
337
+ };
338
+ case "COMPILE":
339
+ return { ...state, compiling: true };
340
+ case "SET_COMPILED_SHADER":
341
+ return { ...state, compiledShader: action.shader, compiling: false };
342
+ case "TOGGLE_AUTO_COMPILE":
343
+ return { ...state, autoCompile: !state.autoCompile };
344
+ case "LOAD_GRAPH":
345
+ return {
346
+ ...createInitialState(action.graph),
347
+ autoCompile: state.autoCompile
348
+ };
349
+ case "NEW_GRAPH":
350
+ return createInitialState();
351
+ case "SET_METADATA":
352
+ return {
353
+ ...state,
354
+ graph: {
355
+ ...state.graph,
356
+ metadata: { ...state.graph.metadata, ...action.metadata }
133
357
  }
134
- case 'SELECT_NODES': {
135
- return {
136
- ...state,
137
- selectedNodes: action.additive
138
- ? [...new Set([...state.selectedNodes, ...action.nodeIds])]
139
- : action.nodeIds,
140
- selectedConnections: action.additive ? state.selectedConnections : [],
141
- };
358
+ };
359
+ default:
360
+ return state;
361
+ }
362
+ }
363
+ function useMaterialEditor(initialGraph) {
364
+ const [state, dispatch] = React.useReducer(materialEditorReducer, initialGraph, createInitialState);
365
+ React.useEffect(() => {
366
+ if (state.autoCompile && !state.compiling) {
367
+ const timeout = setTimeout(() => {
368
+ dispatch({ type: "COMPILE" });
369
+ const shader = materialEditorUtils.compileShader(state.graph);
370
+ dispatch({ type: "SET_COMPILED_SHADER", shader });
371
+ }, 300);
372
+ return () => clearTimeout(timeout);
373
+ }
374
+ }, [state.graph, state.autoCompile]);
375
+ return { state, dispatch };
376
+ }
377
+ function NodePalette({
378
+ searchQuery,
379
+ expandedCategories,
380
+ onSearch,
381
+ onToggleCategory,
382
+ onAddNode,
383
+ onDragStart
384
+ }) {
385
+ const { t } = i18n.useNiceTranslation();
386
+ const filteredNodes = searchQuery ? materialNodeDefinitions.searchNodes(searchQuery) : materialNodeDefinitions.MATERIAL_NODE_DEFINITIONS;
387
+ const categories = React.useMemo(() => {
388
+ const cats = /* @__PURE__ */ new Map();
389
+ for (const node of filteredNodes) {
390
+ if (!cats.has(node.category)) {
391
+ cats.set(node.category, []);
392
+ }
393
+ cats.get(node.category).push(node);
394
+ }
395
+ return cats;
396
+ }, [filteredNodes]);
397
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.palette, children: [
398
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.paletteSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
399
+ "input",
400
+ {
401
+ type: "text",
402
+ placeholder: t("materialEditor.searchNodes", "Search nodes..."),
403
+ value: searchQuery,
404
+ onChange: (e) => onSearch(e.target.value),
405
+ className: MaterialEditor_module.default.searchInput
406
+ }
407
+ ) }),
408
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.paletteCategories, children: Array.from(categories.entries()).map(([category, nodes]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.paletteCategory, children: [
409
+ /* @__PURE__ */ jsxRuntime.jsxs(
410
+ "button",
411
+ {
412
+ className: MaterialEditor_module.default.categoryHeader,
413
+ onClick: () => onToggleCategory(category),
414
+ style: { borderLeftColor: materialEditorTypes.NODE_CATEGORY_COLORS[category] },
415
+ children: [
416
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.categoryIcon, children: /* @__PURE__ */ jsxRuntime.jsx(
417
+ ui.NiceIcon,
418
+ {
419
+ name: expandedCategories.includes(category) ? "chevron-down" : "chevron-right",
420
+ size: 14
421
+ }
422
+ ) }),
423
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: materialEditorTypes.NODE_CATEGORY_NAMES[category] }),
424
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.categoryCount, children: nodes.length })
425
+ ]
142
426
  }
143
- case 'SELECT_CONNECTIONS': {
144
- return {
145
- ...state,
146
- selectedConnections: action.additive
147
- ? [...new Set([...state.selectedConnections, ...action.connectionIds])]
148
- : action.connectionIds,
149
- selectedNodes: action.additive ? state.selectedNodes : [],
150
- };
427
+ ),
428
+ expandedCategories.includes(category) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.categoryNodes, children: nodes.map((nodeDef) => /* @__PURE__ */ jsxRuntime.jsxs(
429
+ "div",
430
+ {
431
+ className: MaterialEditor_module.default.paletteNode,
432
+ draggable: true,
433
+ onDragStart: (e) => onDragStart(nodeDef.type, e),
434
+ onDoubleClick: () => onAddNode(nodeDef.type),
435
+ title: nodeDef.description,
436
+ children: [
437
+ /* @__PURE__ */ jsxRuntime.jsx(
438
+ "span",
439
+ {
440
+ className: MaterialEditor_module.default.nodeColor,
441
+ style: {
442
+ backgroundColor: nodeDef.color || materialEditorTypes.NODE_CATEGORY_COLORS[nodeDef.category]
443
+ }
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.nodeName, children: nodeDef.name })
447
+ ]
448
+ },
449
+ nodeDef.type
450
+ )) })
451
+ ] }, category)) })
452
+ ] });
453
+ }
454
+ function GraphNode({
455
+ node,
456
+ definition,
457
+ selected,
458
+ viewTransform,
459
+ connections,
460
+ onSelect,
461
+ onMove,
462
+ onStartConnection
463
+ }) {
464
+ const nodeRef = React.useRef(null);
465
+ const [isDragging, setIsDragging] = React.useState(false);
466
+ const dragStart = React.useRef(null);
467
+ const handleMouseDown = (e) => {
468
+ if (e.button !== 0) {
469
+ return;
470
+ }
471
+ e.stopPropagation();
472
+ if (!selected) {
473
+ onSelect(node.id, e.shiftKey || e.ctrlKey);
474
+ }
475
+ if (!node.locked) {
476
+ setIsDragging(true);
477
+ dragStart.current = { x: e.clientX, y: e.clientY };
478
+ }
479
+ };
480
+ React.useEffect(() => {
481
+ if (!isDragging) {
482
+ return;
483
+ }
484
+ const handleMouseMove = (e) => {
485
+ if (dragStart.current) {
486
+ const delta = {
487
+ x: (e.clientX - dragStart.current.x) / viewTransform.zoom,
488
+ y: (e.clientY - dragStart.current.y) / viewTransform.zoom
489
+ };
490
+ dragStart.current = { x: e.clientX, y: e.clientY };
491
+ onMove(node.id, delta);
492
+ }
493
+ };
494
+ const handleMouseUp = () => {
495
+ setIsDragging(false);
496
+ dragStart.current = null;
497
+ };
498
+ document.addEventListener("mousemove", handleMouseMove);
499
+ document.addEventListener("mouseup", handleMouseUp);
500
+ return () => {
501
+ document.removeEventListener("mousemove", handleMouseMove);
502
+ document.removeEventListener("mouseup", handleMouseUp);
503
+ };
504
+ }, [isDragging, viewTransform.zoom, node.id, onMove]);
505
+ const handlePortMouseDown = (e, portId, portType) => {
506
+ e.stopPropagation();
507
+ const rect = e.target.getBoundingClientRect();
508
+ const position = {
509
+ x: rect.left + rect.width / 2,
510
+ y: rect.top + rect.height / 2
511
+ };
512
+ onStartConnection(node.id, portId, portType, position);
513
+ };
514
+ const nodeColor = definition.color || materialEditorTypes.NODE_CATEGORY_COLORS[definition.category];
515
+ const maxPorts = Math.max(definition.inputs.length, definition.outputs.length);
516
+ const nodeHeight = node.collapsed ? materialEditorTypes.NODE_HEADER_HEIGHT : materialEditorTypes.NODE_HEADER_HEIGHT + maxPorts * materialEditorTypes.PORT_HEIGHT + 8;
517
+ return /* @__PURE__ */ jsxRuntime.jsxs(
518
+ "div",
519
+ {
520
+ ref: nodeRef,
521
+ className: `${MaterialEditor_module.default.graphNode} ${selected ? MaterialEditor_module.default.selected : ""} ${node.locked ? MaterialEditor_module.default.locked : ""}`,
522
+ style: {
523
+ left: node.position.x * viewTransform.zoom + viewTransform.x,
524
+ top: node.position.y * viewTransform.zoom + viewTransform.y,
525
+ width: materialEditorTypes.NODE_WIDTH * viewTransform.zoom,
526
+ height: nodeHeight * viewTransform.zoom,
527
+ transform: `scale(${viewTransform.zoom})`,
528
+ transformOrigin: "top left"
529
+ },
530
+ onMouseDown: handleMouseDown,
531
+ children: [
532
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.nodeHeader, style: { backgroundColor: nodeColor }, children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.nodeTitle, children: definition.name }),
534
+ node.locked && /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.lockIcon, children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "lock", size: 14 }) })
535
+ ] }),
536
+ !node.collapsed && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.nodePorts, children: [
537
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.inputPorts, children: definition.inputs.map((port, index) => {
538
+ const isConnected = connections.some(
539
+ (c) => c.targetNodeId === node.id && c.targetPortId === port.id
540
+ );
541
+ return /* @__PURE__ */ jsxRuntime.jsxs(
542
+ "div",
543
+ {
544
+ className: MaterialEditor_module.default.port,
545
+ style: { top: index * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.NODE_HEADER_HEIGHT },
546
+ children: [
547
+ /* @__PURE__ */ jsxRuntime.jsx(
548
+ "div",
549
+ {
550
+ className: `${MaterialEditor_module.default.portHandle} ${MaterialEditor_module.default.inputHandle} ${isConnected ? MaterialEditor_module.default.connected : ""}`,
551
+ style: { backgroundColor: materialEditorTypes.PORT_TYPE_COLORS[port.dataType] },
552
+ onMouseDown: (e) => handlePortMouseDown(e, port.id, "input")
553
+ }
554
+ ),
555
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.portLabel, children: port.name })
556
+ ]
557
+ },
558
+ port.id
559
+ );
560
+ }) }),
561
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.outputPorts, children: definition.outputs.map((port, index) => {
562
+ const isConnected = connections.some(
563
+ (c) => c.sourceNodeId === node.id && c.sourcePortId === port.id
564
+ );
565
+ return /* @__PURE__ */ jsxRuntime.jsxs(
566
+ "div",
567
+ {
568
+ className: MaterialEditor_module.default.port,
569
+ style: { top: index * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.NODE_HEADER_HEIGHT },
570
+ children: [
571
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MaterialEditor_module.default.portLabel, children: port.name }),
572
+ /* @__PURE__ */ jsxRuntime.jsx(
573
+ "div",
574
+ {
575
+ className: `${MaterialEditor_module.default.portHandle} ${MaterialEditor_module.default.outputHandle} ${isConnected ? MaterialEditor_module.default.connected : ""}`,
576
+ style: { backgroundColor: materialEditorTypes.PORT_TYPE_COLORS[port.dataType] },
577
+ onMouseDown: (e) => handlePortMouseDown(e, port.id, "output")
578
+ }
579
+ )
580
+ ]
581
+ },
582
+ port.id
583
+ );
584
+ }) })
585
+ ] }),
586
+ node.comment && /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.nodeComment, children: node.comment })
587
+ ]
588
+ }
589
+ );
590
+ }
591
+ function Toolbar({
592
+ canUndo,
593
+ canRedo,
594
+ canPaste,
595
+ hasSelection,
596
+ autoCompile,
597
+ compiling,
598
+ showGrid,
599
+ snapToGrid,
600
+ onUndo,
601
+ onRedo,
602
+ onCopy,
603
+ onCut,
604
+ onPaste,
605
+ onDuplicate,
606
+ onDelete,
607
+ onSelectAll,
608
+ onZoomToFit,
609
+ onToggleGrid,
610
+ onToggleSnap,
611
+ onToggleAutoCompile,
612
+ onCompile,
613
+ onNew,
614
+ onExport,
615
+ onImport
616
+ }) {
617
+ const { t } = i18n.useNiceTranslation();
618
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbar, children: [
619
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbarGroup, children: [
620
+ /* @__PURE__ */ jsxRuntime.jsx(
621
+ "button",
622
+ {
623
+ onClick: onNew,
624
+ title: t("materialEditor.newMaterialCtrlN", "New Material (Ctrl+N)"),
625
+ "aria-label": t("materialEditor.newMaterial", "New Material"),
626
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "file", size: 16 })
151
627
  }
152
- case 'SELECT_ALL': {
153
- return {
154
- ...state,
155
- selectedNodes: state.graph.nodes.map((n) => n.id),
156
- selectedConnections: state.graph.connections.map((c) => c.id),
157
- };
628
+ ),
629
+ /* @__PURE__ */ jsxRuntime.jsx(
630
+ "button",
631
+ {
632
+ onClick: onImport,
633
+ title: t("materialEditor.importCtrlO", "Import (Ctrl+O)"),
634
+ "aria-label": t("materialEditor.import", "Import"),
635
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "folder", size: 16 })
158
636
  }
159
- case 'DESELECT_ALL': {
160
- return {
161
- ...state,
162
- selectedNodes: [],
163
- selectedConnections: [],
164
- };
637
+ ),
638
+ /* @__PURE__ */ jsxRuntime.jsx(
639
+ "button",
640
+ {
641
+ onClick: onExport,
642
+ title: t("materialEditor.exportCtrlS", "Export (Ctrl+S)"),
643
+ "aria-label": t("materialEditor.export", "Export"),
644
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "save", size: 16 })
165
645
  }
166
- case 'COPY': {
167
- const nodesToCopy = state.graph.nodes.filter((n) => state.selectedNodes.includes(n.id));
168
- const connectionsToCopy = state.graph.connections.filter((c) => state.selectedNodes.includes(c.sourceNodeId) &&
169
- state.selectedNodes.includes(c.targetNodeId));
170
- return {
171
- ...state,
172
- clipboard: { nodes: nodesToCopy, connections: connectionsToCopy },
173
- };
646
+ )
647
+ ] }),
648
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.toolbarSeparator }),
649
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbarGroup, children: [
650
+ /* @__PURE__ */ jsxRuntime.jsx(
651
+ "button",
652
+ {
653
+ onClick: onUndo,
654
+ disabled: !canUndo,
655
+ title: t("materialEditor.undoCtrlZ", "Undo (Ctrl+Z)"),
656
+ "aria-label": t("materialEditor.undo", "Undo"),
657
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 })
174
658
  }
175
- case 'PASTE': {
176
- if (!state.clipboard) {
177
- return state;
178
- }
179
- const newState = pushToUndoStack(state);
180
- const idMap = new Map();
181
- const newNodes = state.clipboard.nodes.map((node) => {
182
- const newId = materialEditorUtils.generateNodeId();
183
- idMap.set(node.id, newId);
184
- return {
185
- ...node,
186
- id: newId,
187
- position: {
188
- x: node.position.x + action.position.x - state.clipboard.nodes[0].position.x + 20,
189
- y: node.position.y + action.position.y - state.clipboard.nodes[0].position.y + 20,
190
- },
191
- selected: false,
192
- };
193
- });
194
- const newConnections = state.clipboard.connections.map((conn) => ({
195
- ...conn,
196
- id: materialEditorUtils.generateNodeId(),
197
- sourceNodeId: idMap.get(conn.sourceNodeId),
198
- targetNodeId: idMap.get(conn.targetNodeId),
199
- }));
200
- return {
201
- ...newState,
202
- graph: {
203
- ...newState.graph,
204
- nodes: [...newState.graph.nodes, ...newNodes],
205
- connections: [...newState.graph.connections, ...newConnections],
206
- },
207
- selectedNodes: newNodes.map((n) => n.id),
208
- };
659
+ ),
660
+ /* @__PURE__ */ jsxRuntime.jsx(
661
+ "button",
662
+ {
663
+ onClick: onRedo,
664
+ disabled: !canRedo,
665
+ title: t("materialEditor.redoCtrlY", "Redo (Ctrl+Y)"),
666
+ "aria-label": t("materialEditor.redo", "Redo"),
667
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 })
209
668
  }
210
- case 'CUT': {
211
- const nodesToCut = state.graph.nodes.filter((n) => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId);
212
- const connectionsToCut = state.graph.connections.filter((c) => state.selectedNodes.includes(c.sourceNodeId) &&
213
- state.selectedNodes.includes(c.targetNodeId));
214
- const newState = pushToUndoStack(state);
215
- return {
216
- ...newState,
217
- clipboard: { nodes: nodesToCut, connections: connectionsToCut },
218
- graph: materialEditorUtils.removeNodesFromGraph(newState.graph, nodesToCut.map((n) => n.id)),
219
- selectedNodes: [],
220
- };
669
+ )
670
+ ] }),
671
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.toolbarSeparator }),
672
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbarGroup, children: [
673
+ /* @__PURE__ */ jsxRuntime.jsx(
674
+ "button",
675
+ {
676
+ onClick: onCut,
677
+ disabled: !hasSelection,
678
+ title: t("materialEditor.cutCtrlX", "Cut (Ctrl+X)"),
679
+ "aria-label": t("materialEditor.cut", "Cut"),
680
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "scissors", size: 16 })
221
681
  }
222
- case 'DUPLICATE': {
223
- const nodesToDupe = state.graph.nodes.filter((n) => state.selectedNodes.includes(n.id) && n.id !== state.graph.outputNodeId);
224
- const newState = pushToUndoStack(state);
225
- const idMap = new Map();
226
- const newNodes = nodesToDupe.map((node) => {
227
- const cloned = materialEditorUtils.cloneNode(node);
228
- idMap.set(node.id, cloned.id);
229
- return cloned;
230
- });
231
- const connectionsToDupe = state.graph.connections.filter((c) => state.selectedNodes.includes(c.sourceNodeId) &&
232
- state.selectedNodes.includes(c.targetNodeId));
233
- const newConnections = connectionsToDupe.map((conn) => ({
234
- ...conn,
235
- id: materialEditorUtils.generateNodeId(),
236
- sourceNodeId: idMap.get(conn.sourceNodeId),
237
- targetNodeId: idMap.get(conn.targetNodeId),
238
- }));
239
- return {
240
- ...newState,
241
- graph: {
242
- ...newState.graph,
243
- nodes: [...newState.graph.nodes, ...newNodes],
244
- connections: [...newState.graph.connections, ...newConnections],
245
- },
246
- selectedNodes: newNodes.map((n) => n.id),
247
- };
682
+ ),
683
+ /* @__PURE__ */ jsxRuntime.jsx(
684
+ "button",
685
+ {
686
+ onClick: onCopy,
687
+ disabled: !hasSelection,
688
+ title: t("materialEditor.copyCtrlC", "Copy (Ctrl+C)"),
689
+ "aria-label": t("materialEditor.copy", "Copy"),
690
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 16 })
248
691
  }
249
- case 'UNDO': {
250
- if (state.undoStack.length === 0) {
251
- return state;
252
- }
253
- const previous = state.undoStack[state.undoStack.length - 1];
254
- return {
255
- ...state,
256
- graph: previous,
257
- undoStack: state.undoStack.slice(0, -1),
258
- redoStack: [...state.redoStack, state.graph],
259
- selectedNodes: [],
260
- selectedConnections: [],
261
- };
692
+ ),
693
+ /* @__PURE__ */ jsxRuntime.jsx(
694
+ "button",
695
+ {
696
+ onClick: onPaste,
697
+ disabled: !canPaste,
698
+ title: t("materialEditor.pasteCtrlV", "Paste (Ctrl+V)"),
699
+ "aria-label": t("materialEditor.paste", "Paste"),
700
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "download", size: 16 })
262
701
  }
263
- case 'REDO': {
264
- if (state.redoStack.length === 0) {
265
- return state;
266
- }
267
- const next = state.redoStack[state.redoStack.length - 1];
268
- return {
269
- ...state,
270
- graph: next,
271
- undoStack: [...state.undoStack, state.graph],
272
- redoStack: state.redoStack.slice(0, -1),
273
- selectedNodes: [],
274
- selectedConnections: [],
275
- };
702
+ ),
703
+ /* @__PURE__ */ jsxRuntime.jsx(
704
+ "button",
705
+ {
706
+ onClick: onDuplicate,
707
+ disabled: !hasSelection,
708
+ title: t("materialEditor.duplicateCtrlD", "Duplicate (Ctrl+D)"),
709
+ "aria-label": t("materialEditor.duplicate", "Duplicate"),
710
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "refresh", size: 16 })
276
711
  }
277
- case 'SET_VIEW_TRANSFORM':
278
- return { ...state, viewTransform: action.transform };
279
- case 'ZOOM_TO_FIT': {
280
- const transform = materialEditorUtils.calculateZoomToFit(state.graph.nodes, 800, 600);
281
- return { ...state, viewTransform: transform };
712
+ ),
713
+ /* @__PURE__ */ jsxRuntime.jsx(
714
+ "button",
715
+ {
716
+ onClick: onDelete,
717
+ disabled: !hasSelection,
718
+ title: t("materialEditor.deleteDel", "Delete (Del)"),
719
+ "aria-label": t("materialEditor.delete", "Delete"),
720
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "trash", size: 16 })
282
721
  }
283
- case 'SET_PREVIEW_MODE':
284
- return { ...state, previewMode: action.mode };
285
- case 'TOGGLE_GRID':
286
- return { ...state, showGrid: !state.showGrid };
287
- case 'TOGGLE_MINIMAP':
288
- return { ...state, showMinimap: !state.showMinimap };
289
- case 'TOGGLE_SNAP':
290
- return { ...state, snapToGrid: !state.snapToGrid };
291
- case 'SET_GRID_SIZE':
292
- return { ...state, gridSize: action.size };
293
- case 'SET_SEARCH_QUERY':
294
- return { ...state, searchQuery: action.query };
295
- case 'TOGGLE_CATEGORY':
296
- return {
297
- ...state,
298
- expandedCategories: state.expandedCategories.includes(action.category)
299
- ? state.expandedCategories.filter((c) => c !== action.category)
300
- : [...state.expandedCategories, action.category],
301
- };
302
- case 'COLLAPSE_NODE':
303
- return {
304
- ...state,
305
- graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { collapsed: true }),
306
- };
307
- case 'EXPAND_NODE':
308
- return {
309
- ...state,
310
- graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { collapsed: false }),
311
- };
312
- case 'ADD_COMMENT':
313
- return {
314
- ...state,
315
- graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { comment: action.comment }),
316
- };
317
- case 'LOCK_NODE':
318
- return {
319
- ...state,
320
- graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { locked: true }),
321
- };
322
- case 'UNLOCK_NODE':
323
- return {
324
- ...state,
325
- graph: materialEditorUtils.updateNodeInGraph(state.graph, action.nodeId, { locked: false }),
326
- };
327
- case 'COMPILE':
328
- return { ...state, compiling: true };
329
- case 'SET_COMPILED_SHADER':
330
- return { ...state, compiledShader: action.shader, compiling: false };
331
- case 'TOGGLE_AUTO_COMPILE':
332
- return { ...state, autoCompile: !state.autoCompile };
333
- case 'LOAD_GRAPH':
334
- return {
335
- ...createInitialState(action.graph),
336
- autoCompile: state.autoCompile,
337
- };
338
- case 'NEW_GRAPH':
339
- return createInitialState();
340
- case 'SET_METADATA':
341
- return {
342
- ...state,
343
- graph: {
344
- ...state.graph,
345
- metadata: { ...state.graph.metadata, ...action.metadata },
346
- },
347
- };
348
- default:
349
- return state;
350
- }
351
- }
352
- /* ═══════════════════════════════════════════════════════════════════════════
353
- Hook: useMaterialEditor
354
- ═══════════════════════════════════════════════════════════════════════════ */
355
- function useMaterialEditor(initialGraph) {
356
- const [state, dispatch] = React.useReducer(materialEditorReducer, initialGraph, createInitialState);
357
- // Auto-compile when graph changes
358
- React.useEffect(() => {
359
- if (state.autoCompile && !state.compiling) {
360
- const timeout = setTimeout(() => {
361
- dispatch({ type: 'COMPILE' });
362
- const shader = materialEditorUtils.compileShader(state.graph);
363
- dispatch({ type: 'SET_COMPILED_SHADER', shader });
364
- }, 300);
365
- return () => clearTimeout(timeout);
722
+ )
723
+ ] }),
724
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.toolbarSeparator }),
725
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbarGroup, children: [
726
+ /* @__PURE__ */ jsxRuntime.jsx(
727
+ "button",
728
+ {
729
+ onClick: onSelectAll,
730
+ title: t("materialEditor.selectAllCtrlA", "Select All (Ctrl+A)"),
731
+ "aria-label": t("materialEditor.selectAll", "Select All"),
732
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "square", size: 16 })
366
733
  }
367
- // eslint-disable-next-line react-hooks/exhaustive-deps -- compiling only stops a second compile; as a dependency its true -> false flip would compile the same graph again, forever
368
- }, [state.graph, state.autoCompile]);
369
- return { state, dispatch };
370
- }
371
- function NodePalette({ searchQuery, expandedCategories, onSearch, onToggleCategory, onAddNode, onDragStart, }) {
372
- const { t } = i18n.useNiceTranslation();
373
- const filteredNodes = searchQuery ? materialNodeDefinitions.searchNodes(searchQuery) : materialNodeDefinitions.MATERIAL_NODE_DEFINITIONS;
374
- const categories = React.useMemo(() => {
375
- const cats = new Map();
376
- for (const node of filteredNodes) {
377
- if (!cats.has(node.category)) {
378
- cats.set(node.category, []);
379
- }
380
- cats.get(node.category).push(node);
734
+ ),
735
+ /* @__PURE__ */ jsxRuntime.jsx(
736
+ "button",
737
+ {
738
+ onClick: onZoomToFit,
739
+ title: t("materialEditor.zoomToFitF", "Zoom to Fit (F)"),
740
+ "aria-label": t("materialEditor.zoomToFit", "Zoom to Fit"),
741
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "search", size: 16 })
381
742
  }
382
- return cats;
383
- }, [filteredNodes]);
384
- return (jsxRuntime.jsxs("div", { className: MaterialEditor_module.palette, children: [jsxRuntime.jsx("div", { className: MaterialEditor_module.paletteSearch, children: jsxRuntime.jsx("input", { type: "text", placeholder: t('materialEditor.searchNodes', 'Search nodes...'), value: searchQuery, onChange: (e) => onSearch(e.target.value), className: MaterialEditor_module.searchInput }) }), jsxRuntime.jsx("div", { className: MaterialEditor_module.paletteCategories, children: Array.from(categories.entries()).map(([category, nodes]) => (jsxRuntime.jsxs("div", { className: MaterialEditor_module.paletteCategory, children: [jsxRuntime.jsxs("button", { className: MaterialEditor_module.categoryHeader, onClick: () => onToggleCategory(category), style: { borderLeftColor: materialEditorTypes.NODE_CATEGORY_COLORS[category] }, children: [jsxRuntime.jsx("span", { className: MaterialEditor_module.categoryIcon, children: jsxRuntime.jsx(ui.NiceIcon, { name: expandedCategories.includes(category) ? 'chevron-down' : 'chevron-right', size: 14 }) }), jsxRuntime.jsx("span", { children: materialEditorTypes.NODE_CATEGORY_NAMES[category] }), jsxRuntime.jsx("span", { className: MaterialEditor_module.categoryCount, children: nodes.length })] }), expandedCategories.includes(category) && (jsxRuntime.jsx("div", { className: MaterialEditor_module.categoryNodes, children: nodes.map((nodeDef) => (jsxRuntime.jsxs("div", { className: MaterialEditor_module.paletteNode, draggable: true, onDragStart: (e) => onDragStart(nodeDef.type, e), onDoubleClick: () => onAddNode(nodeDef.type), title: nodeDef.description, children: [jsxRuntime.jsx("span", { className: MaterialEditor_module.nodeColor, style: {
385
- backgroundColor: nodeDef.color || materialEditorTypes.NODE_CATEGORY_COLORS[nodeDef.category],
386
- } }), jsxRuntime.jsx("span", { className: MaterialEditor_module.nodeName, children: nodeDef.name })] }, nodeDef.type))) }))] }, category))) })] }));
387
- }
388
- function GraphNode({ node, definition, selected, viewTransform, connections, onSelect, onMove, onStartConnection, }) {
389
- const nodeRef = React.useRef(null);
390
- const [isDragging, setIsDragging] = React.useState(false);
391
- const dragStart = React.useRef(null);
392
- const handleMouseDown = (e) => {
393
- if (e.button !== 0) {
394
- return;
743
+ ),
744
+ /* @__PURE__ */ jsxRuntime.jsx(
745
+ "button",
746
+ {
747
+ onClick: onToggleGrid,
748
+ className: showGrid ? MaterialEditor_module.default.active : "",
749
+ title: t("materialEditor.toggleGridG", "Toggle Grid (G)"),
750
+ "aria-label": t("materialEditor.toggleGrid", "Toggle Grid"),
751
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "hash", size: 16 })
395
752
  }
396
- e.stopPropagation();
397
- if (!selected) {
398
- onSelect(node.id, e.shiftKey || e.ctrlKey);
753
+ ),
754
+ /* @__PURE__ */ jsxRuntime.jsx(
755
+ "button",
756
+ {
757
+ onClick: onToggleSnap,
758
+ className: snapToGrid ? MaterialEditor_module.default.active : "",
759
+ title: t("materialEditor.toggleSnapS", "Toggle Snap (S)"),
760
+ "aria-label": t("materialEditor.toggleSnap", "Toggle Snap"),
761
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "target", size: 16 })
399
762
  }
400
- if (!node.locked) {
401
- setIsDragging(true);
402
- dragStart.current = { x: e.clientX, y: e.clientY };
763
+ )
764
+ ] }),
765
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.toolbarSeparator }),
766
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.toolbarGroup, children: [
767
+ /* @__PURE__ */ jsxRuntime.jsx(
768
+ "button",
769
+ {
770
+ onClick: onToggleAutoCompile,
771
+ className: autoCompile ? MaterialEditor_module.default.active : "",
772
+ title: t("materialEditor.autoCompile", "Auto Compile"),
773
+ "aria-label": t("materialEditor.autoCompile", "Auto Compile"),
774
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "zap", size: 16 })
403
775
  }
404
- };
405
- React.useEffect(() => {
406
- if (!isDragging) {
407
- return;
776
+ ),
777
+ /* @__PURE__ */ jsxRuntime.jsx(
778
+ "button",
779
+ {
780
+ onClick: onCompile,
781
+ disabled: compiling,
782
+ title: t("materialEditor.compileF5", "Compile (F5)"),
783
+ "aria-label": t("materialEditor.compile", "Compile"),
784
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: compiling ? "clock" : "play", size: 16 })
408
785
  }
409
- const handleMouseMove = (e) => {
410
- if (dragStart.current) {
411
- const delta = {
412
- x: (e.clientX - dragStart.current.x) / viewTransform.zoom,
413
- y: (e.clientY - dragStart.current.y) / viewTransform.zoom,
414
- };
415
- dragStart.current = { x: e.clientX, y: e.clientY };
416
- onMove(node.id, delta);
417
- }
418
- };
419
- const handleMouseUp = () => {
420
- setIsDragging(false);
421
- dragStart.current = null;
422
- };
423
- document.addEventListener('mousemove', handleMouseMove);
424
- document.addEventListener('mouseup', handleMouseUp);
425
- return () => {
426
- document.removeEventListener('mousemove', handleMouseMove);
427
- document.removeEventListener('mouseup', handleMouseUp);
428
- };
429
- }, [isDragging, viewTransform.zoom, node.id, onMove]);
430
- const handlePortMouseDown = (e, portId, portType) => {
431
- e.stopPropagation();
432
- const rect = e.target.getBoundingClientRect();
433
- const position = {
434
- x: rect.left + rect.width / 2,
435
- y: rect.top + rect.height / 2,
436
- };
437
- onStartConnection(node.id, portId, portType, position);
438
- };
439
- const nodeColor = definition.color || materialEditorTypes.NODE_CATEGORY_COLORS[definition.category];
440
- const maxPorts = Math.max(definition.inputs.length, definition.outputs.length);
441
- const nodeHeight = node.collapsed
442
- ? materialEditorTypes.NODE_HEADER_HEIGHT
443
- : materialEditorTypes.NODE_HEADER_HEIGHT + maxPorts * materialEditorTypes.PORT_HEIGHT + 8;
444
- return (jsxRuntime.jsxs("div", { ref: nodeRef, className: `${MaterialEditor_module.graphNode} ${selected ? MaterialEditor_module.selected : ''} ${node.locked ? MaterialEditor_module.locked : ''}`, style: {
445
- left: node.position.x * viewTransform.zoom + viewTransform.x,
446
- top: node.position.y * viewTransform.zoom + viewTransform.y,
447
- width: materialEditorTypes.NODE_WIDTH * viewTransform.zoom,
448
- height: nodeHeight * viewTransform.zoom,
449
- transform: `scale(${viewTransform.zoom})`,
450
- transformOrigin: 'top left',
451
- }, onMouseDown: handleMouseDown, children: [jsxRuntime.jsxs("div", { className: MaterialEditor_module.nodeHeader, style: { backgroundColor: nodeColor }, children: [jsxRuntime.jsx("span", { className: MaterialEditor_module.nodeTitle, children: definition.name }), node.locked && (jsxRuntime.jsx("span", { className: MaterialEditor_module.lockIcon, children: jsxRuntime.jsx(ui.NiceIcon, { name: "lock", size: 14 }) }))] }), !node.collapsed && (jsxRuntime.jsxs("div", { className: MaterialEditor_module.nodePorts, children: [jsxRuntime.jsx("div", { className: MaterialEditor_module.inputPorts, children: definition.inputs.map((port, index) => {
452
- const isConnected = connections.some((c) => c.targetNodeId === node.id && c.targetPortId === port.id);
453
- return (jsxRuntime.jsxs("div", { className: MaterialEditor_module.port, style: { top: index * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.NODE_HEADER_HEIGHT }, children: [jsxRuntime.jsx("div", { className: `${MaterialEditor_module.portHandle} ${MaterialEditor_module.inputHandle} ${isConnected ? MaterialEditor_module.connected : ''}`, style: { backgroundColor: materialEditorTypes.PORT_TYPE_COLORS[port.dataType] }, onMouseDown: (e) => handlePortMouseDown(e, port.id, 'input') }), jsxRuntime.jsx("span", { className: MaterialEditor_module.portLabel, children: port.name })] }, port.id));
454
- }) }), jsxRuntime.jsx("div", { className: MaterialEditor_module.outputPorts, children: definition.outputs.map((port, index) => {
455
- const isConnected = connections.some((c) => c.sourceNodeId === node.id && c.sourcePortId === port.id);
456
- return (jsxRuntime.jsxs("div", { className: MaterialEditor_module.port, style: { top: index * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.NODE_HEADER_HEIGHT }, children: [jsxRuntime.jsx("span", { className: MaterialEditor_module.portLabel, children: port.name }), jsxRuntime.jsx("div", { className: `${MaterialEditor_module.portHandle} ${MaterialEditor_module.outputHandle} ${isConnected ? MaterialEditor_module.connected : ''}`, style: { backgroundColor: materialEditorTypes.PORT_TYPE_COLORS[port.dataType] }, onMouseDown: (e) => handlePortMouseDown(e, port.id, 'output') })] }, port.id));
457
- }) })] })), node.comment && jsxRuntime.jsx("div", { className: MaterialEditor_module.nodeComment, children: node.comment })] }));
458
- }
459
- 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, }) {
460
- const { t } = i18n.useNiceTranslation();
461
- return (jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbar, children: [jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onNew, title: t('materialEditor.newMaterialCtrlN', 'New Material (Ctrl+N)'), "aria-label": t('materialEditor.newMaterial', 'New Material'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "file", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onImport, title: t('materialEditor.importCtrlO', 'Import (Ctrl+O)'), "aria-label": t('materialEditor.import', 'Import'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "folder", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onExport, title: t('materialEditor.exportCtrlS', 'Export (Ctrl+S)'), "aria-label": t('materialEditor.export', 'Export'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "save", size: 16 }) })] }), jsxRuntime.jsx("div", { className: MaterialEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onUndo, disabled: !canUndo, title: t('materialEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('materialEditor.undo', 'Undo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onRedo, disabled: !canRedo, title: t('materialEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('materialEditor.redo', 'Redo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsxRuntime.jsx("div", { className: MaterialEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onCut, disabled: !hasSelection, title: t('materialEditor.cutCtrlX', 'Cut (Ctrl+X)'), "aria-label": t('materialEditor.cut', 'Cut'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "scissors", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onCopy, disabled: !hasSelection, title: t('materialEditor.copyCtrlC', 'Copy (Ctrl+C)'), "aria-label": t('materialEditor.copy', 'Copy'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onPaste, disabled: !canPaste, title: t('materialEditor.pasteCtrlV', 'Paste (Ctrl+V)'), "aria-label": t('materialEditor.paste', 'Paste'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "download", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onDuplicate, disabled: !hasSelection, title: t('materialEditor.duplicateCtrlD', 'Duplicate (Ctrl+D)'), "aria-label": t('materialEditor.duplicate', 'Duplicate'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "refresh", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onDelete, disabled: !hasSelection, title: t('materialEditor.deleteDel', 'Delete (Del)'), "aria-label": t('materialEditor.delete', 'Delete'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "trash", size: 16 }) })] }), jsxRuntime.jsx("div", { className: MaterialEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onSelectAll, title: t('materialEditor.selectAllCtrlA', 'Select All (Ctrl+A)'), "aria-label": t('materialEditor.selectAll', 'Select All'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "square", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onZoomToFit, title: t('materialEditor.zoomToFitF', 'Zoom to Fit (F)'), "aria-label": t('materialEditor.zoomToFit', 'Zoom to Fit'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "search", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onToggleGrid, className: showGrid ? MaterialEditor_module.active : '', title: t('materialEditor.toggleGridG', 'Toggle Grid (G)'), "aria-label": t('materialEditor.toggleGrid', 'Toggle Grid'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "hash", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onToggleSnap, className: snapToGrid ? MaterialEditor_module.active : '', title: t('materialEditor.toggleSnapS', 'Toggle Snap (S)'), "aria-label": t('materialEditor.toggleSnap', 'Toggle Snap'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "target", size: 16 }) })] }), jsxRuntime.jsx("div", { className: MaterialEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: MaterialEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onToggleAutoCompile, className: autoCompile ? MaterialEditor_module.active : '', title: t('materialEditor.autoCompile', 'Auto Compile'), "aria-label": t('materialEditor.autoCompile', 'Auto Compile'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "zap", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onCompile, disabled: compiling, title: t('materialEditor.compileF5', 'Compile (F5)'), "aria-label": t('materialEditor.compile', 'Compile'), children: jsxRuntime.jsx(ui.NiceIcon, { name: compiling ? 'clock' : 'play', size: 16 }) })] })] }));
786
+ )
787
+ ] })
788
+ ] });
462
789
  }
463
790
  function PreviewPanel({ shader, mode, compiling, onModeChange }) {
464
- const { t } = i18n.useNiceTranslation();
465
- const canvasRef = React.useRef(null);
466
- const previewModes = ['sphere', 'cube', 'plane', 'cylinder', 'torus'];
467
- // Simple preview rendering (in real implementation would use Three.js)
468
- React.useEffect(() => {
469
- const canvas = canvasRef.current;
470
- if (!canvas) {
471
- return;
472
- }
473
- const ctx = canvas.getContext('2d');
474
- if (!ctx) {
475
- return;
476
- }
477
- // Simple gradient preview
478
- const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
479
- if ((shader === null || shader === void 0 ? void 0 : shader.errors.length) === 0) {
480
- gradient.addColorStop(0, ui.resolveCssColor('var(--nice-success, #4caf50)', ctx.canvas));
481
- gradient.addColorStop(1, ui.resolveCssColor('var(--nice-success-dark, #2e7d32)', ctx.canvas));
482
- }
483
- else {
484
- gradient.addColorStop(0, ui.resolveCssColor('var(--nice-danger, #f44336)', ctx.canvas));
485
- gradient.addColorStop(1, ui.resolveCssColor('var(--nice-danger-dark, #c62828)', ctx.canvas));
791
+ const { t } = i18n.useNiceTranslation();
792
+ const canvasRef = React.useRef(null);
793
+ const previewModes = ["sphere", "cube", "plane", "cylinder", "torus"];
794
+ React.useEffect(() => {
795
+ const canvas = canvasRef.current;
796
+ if (!canvas) {
797
+ return;
798
+ }
799
+ const ctx = canvas.getContext("2d");
800
+ if (!ctx) {
801
+ return;
802
+ }
803
+ const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
804
+ if (shader?.errors.length === 0) {
805
+ gradient.addColorStop(0, ui.resolveCssColor("var(--nice-success, #4caf50)", ctx.canvas));
806
+ gradient.addColorStop(1, ui.resolveCssColor("var(--nice-success-dark, #2e7d32)", ctx.canvas));
807
+ } else {
808
+ gradient.addColorStop(0, ui.resolveCssColor("var(--nice-danger, #f44336)", ctx.canvas));
809
+ gradient.addColorStop(1, ui.resolveCssColor("var(--nice-danger-dark, #c62828)", ctx.canvas));
810
+ }
811
+ ctx.fillStyle = ui.resolveCssColor("var(--nice-bg, #1e1e1e)", ctx.canvas);
812
+ ctx.fillRect(0, 0, 200, 200);
813
+ ctx.fillStyle = gradient;
814
+ ctx.beginPath();
815
+ ctx.arc(100, 100, 80, 0, Math.PI * 2);
816
+ ctx.fill();
817
+ }, [shader, mode]);
818
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.previewPanel, children: [
819
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.previewHeader, children: [
820
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("materialEditor.preview", "Preview") }),
821
+ /* @__PURE__ */ jsxRuntime.jsx(
822
+ "select",
823
+ {
824
+ value: mode,
825
+ onChange: (e) => onModeChange(e.target.value),
826
+ className: MaterialEditor_module.default.previewModeSelect,
827
+ children: previewModes.map((m) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: m, children: m.charAt(0).toUpperCase() + m.slice(1) }, m))
486
828
  }
487
- ctx.fillStyle = ui.resolveCssColor('var(--nice-bg, #1e1e1e)', ctx.canvas);
488
- ctx.fillRect(0, 0, 200, 200);
489
- ctx.fillStyle = gradient;
490
- ctx.beginPath();
491
- ctx.arc(100, 100, 80, 0, Math.PI * 2);
492
- ctx.fill();
493
- }, [shader, mode]);
494
- return (jsxRuntime.jsxs("div", { className: MaterialEditor_module.previewPanel, children: [jsxRuntime.jsxs("div", { className: MaterialEditor_module.previewHeader, children: [jsxRuntime.jsx("span", { children: t('materialEditor.preview', 'Preview') }), jsxRuntime.jsx("select", { value: mode, onChange: (e) => onModeChange(e.target.value), className: MaterialEditor_module.previewModeSelect, children: previewModes.map((m) => (jsxRuntime.jsx("option", { value: m, children: m.charAt(0).toUpperCase() + m.slice(1) }, m))) })] }), jsxRuntime.jsxs("div", { className: MaterialEditor_module.previewCanvas, children: [compiling && (jsxRuntime.jsx("div", { className: MaterialEditor_module.previewCompiling, children: t('materialEditor.compiling', 'Compiling...') })), jsxRuntime.jsx("canvas", { ref: canvasRef, width: 200, height: 200 })] }), (shader === null || shader === void 0 ? void 0 : shader.errors.length) ? (jsxRuntime.jsx("div", { className: MaterialEditor_module.previewErrors, children: shader.errors.slice(0, 3).map((err, i) => (jsxRuntime.jsx("div", { className: MaterialEditor_module.previewError, children: err.message }, i))) })) : null] }));
829
+ )
830
+ ] }),
831
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MaterialEditor_module.default.previewCanvas, children: [
832
+ compiling && /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.previewCompiling, children: t("materialEditor.compiling", "Compiling...") }),
833
+ /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, width: 200, height: 200 })
834
+ ] }),
835
+ shader?.errors.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.previewErrors, children: shader.errors.slice(0, 3).map((err, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: MaterialEditor_module.default.previewError, children: err.message }, i)) }) : null
836
+ ] });
495
837
  }
496
- /* ═══════════════════════════════════════════════════════════════════════════
497
- Main Component
498
- ═══════════════════════════════════════════════════════════════════════════ */
499
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
500
838
  const MATERIAL_EDITOR_TOUR = [
501
- {
502
- target: '.nice-material-editor',
503
- titleKey: 'tutorial.materialEditor.intro.title',
504
- title: 'Material editor',
505
- contentKey: 'tutorial.materialEditor.intro.content',
506
- content: 'Build a shader as a node graph: drag nodes from the palette, wire their ports together, edit properties, and preview the compiled material.',
507
- placement: 'auto',
508
- },
839
+ {
840
+ target: ".nice-material-editor",
841
+ titleKey: "tutorial.materialEditor.intro.title",
842
+ title: "Material editor",
843
+ contentKey: "tutorial.materialEditor.intro.content",
844
+ content: "Build a shader as a node graph: drag nodes from the palette, wire their ports together, edit properties, and preview the compiled material.",
845
+ placement: "auto"
846
+ }
509
847
  ];
510
- function NiceMaterialEditor({ graph: initialGraph, onGraphChange, onCompile, customNodes, textures: _textures, width = '100%', height = '600px', readOnly = false, showToolbar = true, showPalette = true, showProperties: _showProperties = true, showPreview = true, previewMode: _initialPreviewMode = 'sphere', theme = 'dark', className, tutorial, }) {
511
- const { state, dispatch } = useMaterialEditor(initialGraph);
512
- const canvasRef = React.useRef(null);
513
- const [pendingConnection, setPendingConnection] = React.useState(null);
514
- // Merge custom node definitions
515
- const definitions = React.useMemo(() => {
516
- const map = new Map(materialNodeDefinitions.MATERIAL_NODE_MAP);
517
- customNodes === null || customNodes === void 0 ? void 0 : customNodes.forEach((def) => map.set(def.type, def));
518
- return map;
519
- }, [customNodes]);
520
- // Notify parent of changes
521
- React.useEffect(() => {
522
- onGraphChange === null || onGraphChange === void 0 ? void 0 : onGraphChange(state.graph);
523
- }, [state.graph, onGraphChange]);
524
- React.useEffect(() => {
525
- if (state.compiledShader) {
526
- onCompile === null || onCompile === void 0 ? void 0 : onCompile(state.compiledShader);
527
- }
528
- }, [state.compiledShader, onCompile]);
529
- // Keyboard shortcuts
530
- React.useEffect(() => {
531
- const handleKeyDown = (e) => {
532
- if (readOnly) {
533
- return;
534
- }
535
- const isCtrl = e.ctrlKey || e.metaKey;
536
- if (isCtrl && e.key === 'z') {
537
- e.preventDefault();
538
- dispatch({ type: 'UNDO' });
539
- }
540
- else if (isCtrl && e.key === 'y') {
541
- e.preventDefault();
542
- dispatch({ type: 'REDO' });
543
- }
544
- else if (isCtrl && e.key === 'c') {
545
- e.preventDefault();
546
- dispatch({ type: 'COPY' });
547
- }
548
- else if (isCtrl && e.key === 'x') {
549
- e.preventDefault();
550
- dispatch({ type: 'CUT' });
551
- }
552
- else if (isCtrl && e.key === 'v') {
553
- e.preventDefault();
554
- dispatch({ type: 'PASTE', position: { x: 100, y: 100 } });
555
- }
556
- else if (isCtrl && e.key === 'd') {
557
- e.preventDefault();
558
- dispatch({ type: 'DUPLICATE' });
559
- }
560
- else if (isCtrl && e.key === 'a') {
561
- e.preventDefault();
562
- dispatch({ type: 'SELECT_ALL' });
563
- }
564
- else if (e.key === 'Delete' || e.key === 'Backspace') {
565
- if (state.selectedNodes.length > 0) {
566
- e.preventDefault();
567
- dispatch({ type: 'DELETE_NODES', nodeIds: state.selectedNodes });
568
- }
569
- }
570
- else if (e.key === 'f' || e.key === 'F') {
571
- e.preventDefault();
572
- dispatch({ type: 'ZOOM_TO_FIT' });
573
- }
574
- else if (e.key === 'g' || e.key === 'G') {
575
- e.preventDefault();
576
- dispatch({ type: 'TOGGLE_GRID' });
577
- }
578
- else if (e.key === 's' && !isCtrl) {
579
- e.preventDefault();
580
- dispatch({ type: 'TOGGLE_SNAP' });
581
- }
582
- else if (e.key === 'Escape') {
583
- dispatch({ type: 'DESELECT_ALL' });
584
- setPendingConnection(null);
585
- }
586
- };
587
- window.addEventListener('keydown', handleKeyDown);
588
- return () => window.removeEventListener('keydown', handleKeyDown);
589
- }, [readOnly, state.selectedNodes, dispatch]);
590
- // Handle wheel zoom
591
- const handleWheel = React.useCallback((e) => {
592
- var _a;
848
+ function NiceMaterialEditor({
849
+ graph: initialGraph,
850
+ onGraphChange,
851
+ onCompile,
852
+ customNodes,
853
+ textures: _textures,
854
+ width = "100%",
855
+ height = "600px",
856
+ readOnly = false,
857
+ showToolbar = true,
858
+ showPalette = true,
859
+ showProperties: _showProperties = true,
860
+ showPreview = true,
861
+ previewMode: _initialPreviewMode = "sphere",
862
+ theme = "dark",
863
+ className,
864
+ tutorial
865
+ }) {
866
+ const { state, dispatch } = useMaterialEditor(initialGraph);
867
+ const canvasRef = React.useRef(null);
868
+ const [pendingConnection, setPendingConnection] = React.useState(null);
869
+ const definitions = React.useMemo(() => {
870
+ const map = new Map(materialNodeDefinitions.MATERIAL_NODE_MAP);
871
+ customNodes?.forEach((def) => map.set(def.type, def));
872
+ return map;
873
+ }, [customNodes]);
874
+ React.useEffect(() => {
875
+ onGraphChange?.(state.graph);
876
+ }, [state.graph, onGraphChange]);
877
+ React.useEffect(() => {
878
+ if (state.compiledShader) {
879
+ onCompile?.(state.compiledShader);
880
+ }
881
+ }, [state.compiledShader, onCompile]);
882
+ React.useEffect(() => {
883
+ const handleKeyDown = (e) => {
884
+ if (readOnly) {
885
+ return;
886
+ }
887
+ const isCtrl = e.ctrlKey || e.metaKey;
888
+ if (isCtrl && e.key === "z") {
593
889
  e.preventDefault();
594
- const delta = e.deltaY > 0 ? 0.9 : 1.1;
595
- const newZoom = Math.min(materialEditorTypes.MAX_ZOOM, Math.max(materialEditorTypes.MIN_ZOOM, state.viewTransform.zoom * delta));
596
- const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
597
- if (!rect) {
598
- return;
599
- }
600
- const mouseX = e.clientX - rect.left;
601
- const mouseY = e.clientY - rect.top;
602
- const newX = mouseX - (mouseX - state.viewTransform.x) * (newZoom / state.viewTransform.zoom);
603
- const newY = mouseY - (mouseY - state.viewTransform.y) * (newZoom / state.viewTransform.zoom);
604
- dispatch({
605
- type: 'SET_VIEW_TRANSFORM',
606
- transform: { x: newX, y: newY, zoom: newZoom },
607
- });
608
- }, [state.viewTransform, dispatch]);
609
- // Handle canvas pan
610
- const [isPanning, setIsPanning] = React.useState(false);
611
- const panStart = React.useRef(null);
612
- const handleCanvasMouseDown = (e) => {
613
- if (e.button === 1 || (e.button === 0 && e.altKey)) {
614
- setIsPanning(true);
615
- panStart.current = { x: e.clientX, y: e.clientY };
616
- }
617
- else if (e.button === 0 && e.target === canvasRef.current) {
618
- dispatch({ type: 'DESELECT_ALL' });
619
- }
620
- };
621
- const handleCanvasMouseMove = (e) => {
622
- if (isPanning && panStart.current) {
623
- const delta = {
624
- x: e.clientX - panStart.current.x,
625
- y: e.clientY - panStart.current.y,
626
- };
627
- panStart.current = { x: e.clientX, y: e.clientY };
628
- dispatch({
629
- type: 'SET_VIEW_TRANSFORM',
630
- transform: {
631
- ...state.viewTransform,
632
- x: state.viewTransform.x + delta.x,
633
- y: state.viewTransform.y + delta.y,
634
- },
635
- });
636
- }
637
- if (pendingConnection) {
638
- setPendingConnection({
639
- ...pendingConnection,
640
- currentPosition: { x: e.clientX, y: e.clientY },
641
- });
642
- }
643
- };
644
- const handleCanvasMouseUp = () => {
645
- setIsPanning(false);
646
- panStart.current = null;
647
- };
648
- // Handle drag and drop from palette
649
- const handleDragOver = (e) => {
890
+ dispatch({ type: "UNDO" });
891
+ } else if (isCtrl && e.key === "y") {
650
892
  e.preventDefault();
651
- };
652
- const handleDrop = (e) => {
653
- var _a;
893
+ dispatch({ type: "REDO" });
894
+ } else if (isCtrl && e.key === "c") {
654
895
  e.preventDefault();
655
- const nodeType = e.dataTransfer.getData('text/plain');
656
- if (!nodeType) {
657
- return;
658
- }
659
- const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
660
- if (!rect) {
661
- return;
662
- }
663
- const position = materialEditorUtils.screenToGraph({ x: e.clientX - rect.left, y: e.clientY - rect.top }, state.viewTransform);
664
- dispatch({ type: 'ADD_NODE', nodeType, position });
665
- };
666
- // Palette handlers
667
- const handlePaletteAddNode = (nodeType) => {
668
- const position = { x: 200, y: 200 };
669
- dispatch({ type: 'ADD_NODE', nodeType, position });
670
- };
671
- const handlePaletteDragStart = (nodeType, e) => {
672
- e.dataTransfer.setData('text/plain', nodeType);
673
- };
674
- // Connection handlers
675
- const handleStartConnection = (nodeId, portId, portType, position) => {
676
- setPendingConnection({
677
- nodeId,
678
- portId,
679
- portType,
680
- startPosition: position,
681
- currentPosition: position,
682
- });
683
- };
684
- // Export/Import
685
- const handleExport = () => {
686
- const json = materialEditorUtils.exportGraphToJSON(state.graph);
687
- const blob = new Blob([json], { type: 'application/json' });
688
- const url = URL.createObjectURL(blob);
689
- const a = document.createElement('a');
690
- a.href = url;
691
- a.download = `${state.graph.name}.material.json`;
692
- a.click();
693
- URL.revokeObjectURL(url);
694
- };
695
- const handleImport = () => {
696
- const input = document.createElement('input');
697
- input.type = 'file';
698
- input.accept = '.json';
699
- input.onchange = async (e) => {
700
- var _a;
701
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
702
- if (!file) {
703
- return;
704
- }
705
- const text = await file.text();
706
- const graph = materialEditorUtils.importGraphFromJSON(text);
707
- if (graph) {
708
- dispatch({ type: 'LOAD_GRAPH', graph });
709
- }
710
- };
711
- input.click();
712
- };
713
- // Render connections
714
- const renderConnections = () => {
715
- return state.graph.connections.map((conn) => {
716
- const sourceNode = state.graph.nodes.find((n) => n.id === conn.sourceNodeId);
717
- const targetNode = state.graph.nodes.find((n) => n.id === conn.targetNodeId);
718
- if (!sourceNode || !targetNode) {
719
- return null;
720
- }
721
- const sourceDef = definitions.get(sourceNode.type);
722
- const targetDef = definitions.get(targetNode.type);
723
- if (!sourceDef || !targetDef) {
724
- return null;
725
- }
726
- const sourcePortIndex = sourceDef.outputs.findIndex((p) => p.id === conn.sourcePortId);
727
- const targetPortIndex = targetDef.inputs.findIndex((p) => p.id === conn.targetPortId);
728
- if (sourcePortIndex === -1 || targetPortIndex === -1) {
729
- return null;
730
- }
731
- const sourcePort = sourceDef.outputs[sourcePortIndex];
732
- const x1 = (sourceNode.position.x + materialEditorTypes.NODE_WIDTH) * state.viewTransform.zoom + state.viewTransform.x;
733
- const y1 = (sourceNode.position.y +
734
- materialEditorTypes.NODE_HEADER_HEIGHT +
735
- sourcePortIndex * materialEditorTypes.PORT_HEIGHT +
736
- materialEditorTypes.PORT_HEIGHT / 2) *
737
- state.viewTransform.zoom +
738
- state.viewTransform.y;
739
- const x2 = targetNode.position.x * state.viewTransform.zoom + state.viewTransform.x;
740
- const y2 = (targetNode.position.y +
741
- materialEditorTypes.NODE_HEADER_HEIGHT +
742
- targetPortIndex * materialEditorTypes.PORT_HEIGHT +
743
- materialEditorTypes.PORT_HEIGHT / 2) *
744
- state.viewTransform.zoom +
745
- state.viewTransform.y;
746
- const isSelected = state.selectedConnections.includes(conn.id);
747
- const controlX = (x1 + x2) / 2;
748
- return (jsxRuntime.jsx("path", { d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`, fill: "none", stroke: isSelected ? 'var(--nice-bg, #fff)' : materialEditorTypes.PORT_TYPE_COLORS[sourcePort.dataType], strokeWidth: isSelected ? 3 : 2, className: MaterialEditor_module.connection, onClick: () => dispatch({ type: 'SELECT_CONNECTIONS', connectionIds: [conn.id] }) }, conn.id));
749
- });
750
- };
751
- // Render pending connection
752
- const renderPendingConnection = () => {
753
- var _a;
754
- if (!pendingConnection) {
755
- return null;
756
- }
757
- const rect = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
758
- if (!rect) {
759
- return null;
896
+ dispatch({ type: "COPY" });
897
+ } else if (isCtrl && e.key === "x") {
898
+ e.preventDefault();
899
+ dispatch({ type: "CUT" });
900
+ } else if (isCtrl && e.key === "v") {
901
+ e.preventDefault();
902
+ dispatch({ type: "PASTE", position: { x: 100, y: 100 } });
903
+ } else if (isCtrl && e.key === "d") {
904
+ e.preventDefault();
905
+ dispatch({ type: "DUPLICATE" });
906
+ } else if (isCtrl && e.key === "a") {
907
+ e.preventDefault();
908
+ dispatch({ type: "SELECT_ALL" });
909
+ } else if (e.key === "Delete" || e.key === "Backspace") {
910
+ if (state.selectedNodes.length > 0) {
911
+ e.preventDefault();
912
+ dispatch({ type: "DELETE_NODES", nodeIds: state.selectedNodes });
760
913
  }
761
- const x1 = pendingConnection.startPosition.x - rect.left;
762
- const y1 = pendingConnection.startPosition.y - rect.top;
763
- const x2 = pendingConnection.currentPosition.x - rect.left;
764
- const y2 = pendingConnection.currentPosition.y - rect.top;
765
- const controlX = (x1 + x2) / 2;
766
- return (jsxRuntime.jsx("path", { d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`, fill: "none", stroke: "var(--nice-bg, #fff)", strokeWidth: 2, strokeDasharray: "5,5", className: MaterialEditor_module.pendingConnection }));
914
+ } else if (e.key === "f" || e.key === "F") {
915
+ e.preventDefault();
916
+ dispatch({ type: "ZOOM_TO_FIT" });
917
+ } else if (e.key === "g" || e.key === "G") {
918
+ e.preventDefault();
919
+ dispatch({ type: "TOGGLE_GRID" });
920
+ } else if (e.key === "s" && !isCtrl) {
921
+ e.preventDefault();
922
+ dispatch({ type: "TOGGLE_SNAP" });
923
+ } else if (e.key === "Escape") {
924
+ dispatch({ type: "DESELECT_ALL" });
925
+ setPendingConnection(null);
926
+ }
767
927
  };
768
- // Render grid
769
- const renderGrid = () => {
770
- if (!state.showGrid) {
771
- return null;
928
+ window.addEventListener("keydown", handleKeyDown);
929
+ return () => window.removeEventListener("keydown", handleKeyDown);
930
+ }, [readOnly, state.selectedNodes, dispatch]);
931
+ const handleWheel = React.useCallback(
932
+ (e) => {
933
+ e.preventDefault();
934
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
935
+ const newZoom = Math.min(materialEditorTypes.MAX_ZOOM, Math.max(materialEditorTypes.MIN_ZOOM, state.viewTransform.zoom * delta));
936
+ const rect = canvasRef.current?.getBoundingClientRect();
937
+ if (!rect) {
938
+ return;
939
+ }
940
+ const mouseX = e.clientX - rect.left;
941
+ const mouseY = e.clientY - rect.top;
942
+ const newX = mouseX - (mouseX - state.viewTransform.x) * (newZoom / state.viewTransform.zoom);
943
+ const newY = mouseY - (mouseY - state.viewTransform.y) * (newZoom / state.viewTransform.zoom);
944
+ dispatch({
945
+ type: "SET_VIEW_TRANSFORM",
946
+ transform: { x: newX, y: newY, zoom: newZoom }
947
+ });
948
+ },
949
+ [state.viewTransform, dispatch]
950
+ );
951
+ const [isPanning, setIsPanning] = React.useState(false);
952
+ const panStart = React.useRef(null);
953
+ const handleCanvasMouseDown = (e) => {
954
+ if (e.button === 1 || e.button === 0 && e.altKey) {
955
+ setIsPanning(true);
956
+ panStart.current = { x: e.clientX, y: e.clientY };
957
+ } else if (e.button === 0 && e.target === canvasRef.current) {
958
+ dispatch({ type: "DESELECT_ALL" });
959
+ }
960
+ };
961
+ const handleCanvasMouseMove = (e) => {
962
+ if (isPanning && panStart.current) {
963
+ const delta = {
964
+ x: e.clientX - panStart.current.x,
965
+ y: e.clientY - panStart.current.y
966
+ };
967
+ panStart.current = { x: e.clientX, y: e.clientY };
968
+ dispatch({
969
+ type: "SET_VIEW_TRANSFORM",
970
+ transform: {
971
+ ...state.viewTransform,
972
+ x: state.viewTransform.x + delta.x,
973
+ y: state.viewTransform.y + delta.y
772
974
  }
773
- const gridSize = state.gridSize * state.viewTransform.zoom;
774
- const offsetX = state.viewTransform.x % gridSize;
775
- const offsetY = state.viewTransform.y % gridSize;
776
- return (jsxRuntime.jsx("pattern", { id: "grid", width: gridSize, height: gridSize, patternUnits: "userSpaceOnUse", x: offsetX, y: offsetY, children: jsxRuntime.jsx("path", { d: `M ${gridSize} 0 L 0 0 0 ${gridSize}`, fill: "none", stroke: "var(--nice-overlay-light-10, rgba(255, 255, 255, 0.1))", strokeWidth: "1" }) }));
975
+ });
976
+ }
977
+ if (pendingConnection) {
978
+ setPendingConnection({
979
+ ...pendingConnection,
980
+ currentPosition: { x: e.clientX, y: e.clientY }
981
+ });
982
+ }
983
+ };
984
+ const handleCanvasMouseUp = () => {
985
+ setIsPanning(false);
986
+ panStart.current = null;
987
+ };
988
+ const handleDragOver = (e) => {
989
+ e.preventDefault();
990
+ };
991
+ const handleDrop = (e) => {
992
+ e.preventDefault();
993
+ const nodeType = e.dataTransfer.getData("text/plain");
994
+ if (!nodeType) {
995
+ return;
996
+ }
997
+ const rect = canvasRef.current?.getBoundingClientRect();
998
+ if (!rect) {
999
+ return;
1000
+ }
1001
+ const position = materialEditorUtils.screenToGraph(
1002
+ { x: e.clientX - rect.left, y: e.clientY - rect.top },
1003
+ state.viewTransform
1004
+ );
1005
+ dispatch({ type: "ADD_NODE", nodeType, position });
1006
+ };
1007
+ const handlePaletteAddNode = (nodeType) => {
1008
+ const position = { x: 200, y: 200 };
1009
+ dispatch({ type: "ADD_NODE", nodeType, position });
1010
+ };
1011
+ const handlePaletteDragStart = (nodeType, e) => {
1012
+ e.dataTransfer.setData("text/plain", nodeType);
1013
+ };
1014
+ const handleStartConnection = (nodeId, portId, portType, position) => {
1015
+ setPendingConnection({
1016
+ nodeId,
1017
+ portId,
1018
+ portType,
1019
+ startPosition: position,
1020
+ currentPosition: position
1021
+ });
1022
+ };
1023
+ const handleExport = () => {
1024
+ const json = materialEditorUtils.exportGraphToJSON(state.graph);
1025
+ const blob = new Blob([json], { type: "application/json" });
1026
+ const url = URL.createObjectURL(blob);
1027
+ const a = document.createElement("a");
1028
+ a.href = url;
1029
+ a.download = `${state.graph.name}.material.json`;
1030
+ a.click();
1031
+ URL.revokeObjectURL(url);
1032
+ };
1033
+ const handleImport = () => {
1034
+ const input = document.createElement("input");
1035
+ input.type = "file";
1036
+ input.accept = ".json";
1037
+ input.onchange = async (e) => {
1038
+ const file = e.target.files?.[0];
1039
+ if (!file) {
1040
+ return;
1041
+ }
1042
+ const text = await file.text();
1043
+ const graph = materialEditorUtils.importGraphFromJSON(text);
1044
+ if (graph) {
1045
+ dispatch({ type: "LOAD_GRAPH", graph });
1046
+ }
777
1047
  };
778
- return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, MATERIAL_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-material-editor ${MaterialEditor_module.materialEditor} ${MaterialEditor_module[theme]} ${className || ''}`, flush: true, toolbar: showToolbar ? (jsxRuntime.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: () => {
779
- dispatch({ type: 'COMPILE' });
780
- const shader = materialEditorUtils.compileShader(state.graph, definitions);
781
- dispatch({ type: 'SET_COMPILED_SHADER', shader });
782
- }, onNew: () => dispatch({ type: 'NEW_GRAPH' }), onExport: handleExport, onImport: handleImport })) : undefined, sidebar: showPalette ? (jsxRuntime.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 })) : undefined, rightSidebar: showPreview ? (jsxRuntime.jsx(PreviewPanel, { shader: state.compiledShader, mode: state.previewMode, compiling: state.compiling, onModeChange: (mode) => dispatch({ type: 'SET_PREVIEW_MODE', mode }) })) : undefined, children: jsxRuntime.jsxs("div", { ref: canvasRef, className: MaterialEditor_module.graphCanvas, onWheel: handleWheel, onMouseDown: handleCanvasMouseDown, onMouseMove: handleCanvasMouseMove, onMouseUp: handleCanvasMouseUp, onMouseLeave: handleCanvasMouseUp, onDragOver: handleDragOver, onDrop: handleDrop, children: [jsxRuntime.jsxs("svg", { className: MaterialEditor_module.connectionsSvg, children: [jsxRuntime.jsx("defs", { children: renderGrid() }), state.showGrid && jsxRuntime.jsx("rect", { width: "100%", height: "100%", fill: "url(#grid)" }), renderConnections(), renderPendingConnection()] }), state.graph.nodes.map((node) => {
783
- const def = definitions.get(node.type);
784
- if (!def) {
785
- return null;
786
- }
787
- return (jsxRuntime.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({
788
- type: 'MOVE_NODES',
789
- nodeIds: state.selectedNodes.includes(nodeId) ? state.selectedNodes : [nodeId],
790
- delta,
791
- }), onStartConnection: handleStartConnection }, node.id));
792
- })] }) })] }));
1048
+ input.click();
1049
+ };
1050
+ const renderConnections = () => {
1051
+ return state.graph.connections.map((conn) => {
1052
+ const sourceNode = state.graph.nodes.find((n) => n.id === conn.sourceNodeId);
1053
+ const targetNode = state.graph.nodes.find((n) => n.id === conn.targetNodeId);
1054
+ if (!sourceNode || !targetNode) {
1055
+ return null;
1056
+ }
1057
+ const sourceDef = definitions.get(sourceNode.type);
1058
+ const targetDef = definitions.get(targetNode.type);
1059
+ if (!sourceDef || !targetDef) {
1060
+ return null;
1061
+ }
1062
+ const sourcePortIndex = sourceDef.outputs.findIndex((p) => p.id === conn.sourcePortId);
1063
+ const targetPortIndex = targetDef.inputs.findIndex((p) => p.id === conn.targetPortId);
1064
+ if (sourcePortIndex === -1 || targetPortIndex === -1) {
1065
+ return null;
1066
+ }
1067
+ const sourcePort = sourceDef.outputs[sourcePortIndex];
1068
+ const x1 = (sourceNode.position.x + materialEditorTypes.NODE_WIDTH) * state.viewTransform.zoom + state.viewTransform.x;
1069
+ const y1 = (sourceNode.position.y + materialEditorTypes.NODE_HEADER_HEIGHT + sourcePortIndex * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.PORT_HEIGHT / 2) * state.viewTransform.zoom + state.viewTransform.y;
1070
+ const x2 = targetNode.position.x * state.viewTransform.zoom + state.viewTransform.x;
1071
+ const y2 = (targetNode.position.y + materialEditorTypes.NODE_HEADER_HEIGHT + targetPortIndex * materialEditorTypes.PORT_HEIGHT + materialEditorTypes.PORT_HEIGHT / 2) * state.viewTransform.zoom + state.viewTransform.y;
1072
+ const isSelected = state.selectedConnections.includes(conn.id);
1073
+ const controlX = (x1 + x2) / 2;
1074
+ return /* @__PURE__ */ jsxRuntime.jsx(
1075
+ "path",
1076
+ {
1077
+ d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`,
1078
+ fill: "none",
1079
+ stroke: isSelected ? "var(--nice-bg, #fff)" : materialEditorTypes.PORT_TYPE_COLORS[sourcePort.dataType],
1080
+ strokeWidth: isSelected ? 3 : 2,
1081
+ className: MaterialEditor_module.default.connection,
1082
+ onClick: () => dispatch({ type: "SELECT_CONNECTIONS", connectionIds: [conn.id] })
1083
+ },
1084
+ conn.id
1085
+ );
1086
+ });
1087
+ };
1088
+ const renderPendingConnection = () => {
1089
+ if (!pendingConnection) {
1090
+ return null;
1091
+ }
1092
+ const rect = canvasRef.current?.getBoundingClientRect();
1093
+ if (!rect) {
1094
+ return null;
1095
+ }
1096
+ const x1 = pendingConnection.startPosition.x - rect.left;
1097
+ const y1 = pendingConnection.startPosition.y - rect.top;
1098
+ const x2 = pendingConnection.currentPosition.x - rect.left;
1099
+ const y2 = pendingConnection.currentPosition.y - rect.top;
1100
+ const controlX = (x1 + x2) / 2;
1101
+ return /* @__PURE__ */ jsxRuntime.jsx(
1102
+ "path",
1103
+ {
1104
+ d: `M ${x1} ${y1} C ${controlX} ${y1}, ${controlX} ${y2}, ${x2} ${y2}`,
1105
+ fill: "none",
1106
+ stroke: "var(--nice-bg, #fff)",
1107
+ strokeWidth: 2,
1108
+ strokeDasharray: "5,5",
1109
+ className: MaterialEditor_module.default.pendingConnection
1110
+ }
1111
+ );
1112
+ };
1113
+ const renderGrid = () => {
1114
+ if (!state.showGrid) {
1115
+ return null;
1116
+ }
1117
+ const gridSize = state.gridSize * state.viewTransform.zoom;
1118
+ const offsetX = state.viewTransform.x % gridSize;
1119
+ const offsetY = state.viewTransform.y % gridSize;
1120
+ return /* @__PURE__ */ jsxRuntime.jsx(
1121
+ "pattern",
1122
+ {
1123
+ id: "grid",
1124
+ width: gridSize,
1125
+ height: gridSize,
1126
+ patternUnits: "userSpaceOnUse",
1127
+ x: offsetX,
1128
+ y: offsetY,
1129
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1130
+ "path",
1131
+ {
1132
+ d: `M ${gridSize} 0 L 0 0 0 ${gridSize}`,
1133
+ fill: "none",
1134
+ stroke: "var(--nice-overlay-light-10, rgba(255, 255, 255, 0.1))",
1135
+ strokeWidth: "1"
1136
+ }
1137
+ )
1138
+ }
1139
+ );
1140
+ };
1141
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", width, height }, children: [
1142
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { position: "absolute", top: 6, right: 6, zIndex: 30 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, MATERIAL_EDITOR_TOUR) }) }),
1143
+ /* @__PURE__ */ jsxRuntime.jsx(
1144
+ ui.NiceAppShell,
1145
+ {
1146
+ className: `nice-material-editor ${MaterialEditor_module.default.materialEditor} ${MaterialEditor_module.default[theme]} ${className || ""}`,
1147
+ flush: true,
1148
+ toolbar: showToolbar ? /* @__PURE__ */ jsxRuntime.jsx(
1149
+ Toolbar,
1150
+ {
1151
+ canUndo: state.undoStack.length > 0,
1152
+ canRedo: state.redoStack.length > 0,
1153
+ canPaste: state.clipboard !== null,
1154
+ hasSelection: state.selectedNodes.length > 0,
1155
+ autoCompile: state.autoCompile,
1156
+ compiling: state.compiling,
1157
+ showGrid: state.showGrid,
1158
+ snapToGrid: state.snapToGrid,
1159
+ onUndo: () => dispatch({ type: "UNDO" }),
1160
+ onRedo: () => dispatch({ type: "REDO" }),
1161
+ onCopy: () => dispatch({ type: "COPY" }),
1162
+ onCut: () => dispatch({ type: "CUT" }),
1163
+ onPaste: () => dispatch({ type: "PASTE", position: { x: 100, y: 100 } }),
1164
+ onDuplicate: () => dispatch({ type: "DUPLICATE" }),
1165
+ onDelete: () => dispatch({ type: "DELETE_NODES", nodeIds: state.selectedNodes }),
1166
+ onSelectAll: () => dispatch({ type: "SELECT_ALL" }),
1167
+ onZoomToFit: () => dispatch({ type: "ZOOM_TO_FIT" }),
1168
+ onToggleGrid: () => dispatch({ type: "TOGGLE_GRID" }),
1169
+ onToggleSnap: () => dispatch({ type: "TOGGLE_SNAP" }),
1170
+ onToggleAutoCompile: () => dispatch({ type: "TOGGLE_AUTO_COMPILE" }),
1171
+ onCompile: () => {
1172
+ dispatch({ type: "COMPILE" });
1173
+ const shader = materialEditorUtils.compileShader(state.graph, definitions);
1174
+ dispatch({ type: "SET_COMPILED_SHADER", shader });
1175
+ },
1176
+ onNew: () => dispatch({ type: "NEW_GRAPH" }),
1177
+ onExport: handleExport,
1178
+ onImport: handleImport
1179
+ }
1180
+ ) : void 0,
1181
+ sidebar: showPalette ? /* @__PURE__ */ jsxRuntime.jsx(
1182
+ NodePalette,
1183
+ {
1184
+ searchQuery: state.searchQuery,
1185
+ expandedCategories: state.expandedCategories,
1186
+ onSearch: (query) => dispatch({ type: "SET_SEARCH_QUERY", query }),
1187
+ onToggleCategory: (cat) => dispatch({ type: "TOGGLE_CATEGORY", category: cat }),
1188
+ onAddNode: handlePaletteAddNode,
1189
+ onDragStart: handlePaletteDragStart
1190
+ }
1191
+ ) : void 0,
1192
+ rightSidebar: showPreview ? /* @__PURE__ */ jsxRuntime.jsx(
1193
+ PreviewPanel,
1194
+ {
1195
+ shader: state.compiledShader,
1196
+ mode: state.previewMode,
1197
+ compiling: state.compiling,
1198
+ onModeChange: (mode) => dispatch({ type: "SET_PREVIEW_MODE", mode })
1199
+ }
1200
+ ) : void 0,
1201
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1202
+ "div",
1203
+ {
1204
+ ref: canvasRef,
1205
+ className: MaterialEditor_module.default.graphCanvas,
1206
+ onWheel: handleWheel,
1207
+ onMouseDown: handleCanvasMouseDown,
1208
+ onMouseMove: handleCanvasMouseMove,
1209
+ onMouseUp: handleCanvasMouseUp,
1210
+ onMouseLeave: handleCanvasMouseUp,
1211
+ onDragOver: handleDragOver,
1212
+ onDrop: handleDrop,
1213
+ children: [
1214
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: MaterialEditor_module.default.connectionsSvg, children: [
1215
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: renderGrid() }),
1216
+ state.showGrid && /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", fill: "url(#grid)" }),
1217
+ renderConnections(),
1218
+ renderPendingConnection()
1219
+ ] }),
1220
+ state.graph.nodes.map((node) => {
1221
+ const def = definitions.get(node.type);
1222
+ if (!def) {
1223
+ return null;
1224
+ }
1225
+ return /* @__PURE__ */ jsxRuntime.jsx(
1226
+ GraphNode,
1227
+ {
1228
+ node,
1229
+ definition: def,
1230
+ selected: state.selectedNodes.includes(node.id),
1231
+ viewTransform: state.viewTransform,
1232
+ connections: state.graph.connections,
1233
+ onSelect: (nodeId, additive) => dispatch({ type: "SELECT_NODES", nodeIds: [nodeId], additive }),
1234
+ onMove: (nodeId, delta) => dispatch({
1235
+ type: "MOVE_NODES",
1236
+ nodeIds: state.selectedNodes.includes(nodeId) ? state.selectedNodes : [nodeId],
1237
+ delta
1238
+ }),
1239
+ onStartConnection: handleStartConnection
1240
+ },
1241
+ node.id
1242
+ );
1243
+ })
1244
+ ]
1245
+ }
1246
+ )
1247
+ }
1248
+ )
1249
+ ] });
793
1250
  }
794
-
795
1251
  exports.NiceMaterialEditor = NiceMaterialEditor;
796
1252
  exports.useMaterialEditor = useMaterialEditor;