@nice2dev/ui-3d 1.3.14 → 1.3.16

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