@nice2dev/ui-3d 1.3.15 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/collaborative/collaborativeScene.js +160 -206
- package/dist/cjs/core/editorAsk.js +51 -41
- package/dist/cjs/core/i18n.js +13 -14
- package/dist/cjs/core/threeCompat.js +29 -42
- package/dist/cjs/dance/DanceBridge.js +148 -177
- package/dist/cjs/dance/DanceScoreEngine.js +209 -221
- package/dist/cjs/dance/PoseDetector.js +302 -275
- package/dist/cjs/index.js +110 -113
- package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
- package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
- package/dist/cjs/material/materialEditorTypes.js +35 -49
- package/dist/cjs/material/materialEditorUtils.js +684 -747
- package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
- package/dist/cjs/model/ModelEditor.js +90 -40
- package/dist/cjs/model/ModelEditor.module.css.js +257 -5
- package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
- package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
- package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
- package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
- package/dist/cjs/model/ModelEditorTimeline.js +232 -34
- package/dist/cjs/model/ModelEditorToolbar.js +170 -14
- package/dist/cjs/model/ModelEditorViewport.js +231 -39
- package/dist/cjs/model/ModelViewer.js +388 -63
- package/dist/cjs/model/ModelViewer.module.css.js +77 -5
- package/dist/cjs/model/NiceArmatureEditor.js +473 -274
- package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
- package/dist/cjs/model/NiceOctree.js +372 -320
- package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
- package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
- package/dist/cjs/model/NiceTerrainEditor.js +595 -385
- package/dist/cjs/model/NiceWeightPainter.js +384 -261
- package/dist/cjs/model/NiceXRPreview.js +385 -308
- package/dist/cjs/model/cadModeUtils.js +103 -106
- package/dist/cjs/model/editorShortcuts.js +171 -194
- package/dist/cjs/model/modelEditorTypes.js +100 -118
- package/dist/cjs/model/modelEditorUtils.js +1061 -1146
- package/dist/cjs/model/simsModeUtils.js +1023 -1053
- package/dist/cjs/model/useModelEditor.js +1977 -1933
- package/dist/cjs/model/useModelViewer.js +633 -665
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
- package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
- package/dist/cjs/particle/particleEditorTypes.js +62 -70
- package/dist/cjs/particle/particleEditorUtils.js +998 -1013
- package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
- package/dist/cjs/rendering/NiceRenderExport.js +424 -337
- package/dist/cjs/rendering/NiceSSAO.js +454 -337
- package/dist/cjs/rendering/NiceSSR.js +387 -261
- package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
- package/dist/cjs/uv/NiceUVEditor.js +787 -550
- package/dist/cjs/uv/UVEditor.module.css.js +53 -5
- package/dist/cjs/uv/uvEditorTypes.js +59 -76
- package/dist/cjs/uv/uvEditorUtils.js +501 -575
- package/dist/esm/collaborative/collaborativeScene.js +163 -205
- package/dist/esm/core/editorAsk.js +53 -40
- package/dist/esm/core/i18n.js +15 -13
- package/dist/esm/core/threeCompat.js +18 -26
- package/dist/esm/dance/DanceBridge.js +151 -176
- package/dist/esm/dance/DanceScoreEngine.js +211 -220
- package/dist/esm/dance/PoseDetector.js +304 -274
- package/dist/esm/index.js +258 -36
- package/dist/esm/material/MaterialEditor.module.css.js +242 -3
- package/dist/esm/material/NiceMaterialEditor.js +1215 -756
- package/dist/esm/material/materialEditorTypes.js +45 -48
- package/dist/esm/material/materialEditorUtils.js +713 -746
- package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
- package/dist/esm/model/ModelEditor.js +91 -39
- package/dist/esm/model/ModelEditor.module.css.js +257 -3
- package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
- package/dist/esm/model/ModelEditorMenuBar.js +359 -30
- package/dist/esm/model/ModelEditorRightPanel.js +634 -114
- package/dist/esm/model/ModelEditorSubComponents.js +265 -94
- package/dist/esm/model/ModelEditorTimeline.js +232 -32
- package/dist/esm/model/ModelEditorToolbar.js +170 -12
- package/dist/esm/model/ModelEditorViewport.js +231 -37
- package/dist/esm/model/ModelViewer.js +389 -62
- package/dist/esm/model/ModelViewer.module.css.js +77 -3
- package/dist/esm/model/NiceArmatureEditor.js +462 -258
- package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
- package/dist/esm/model/NiceOctree.js +361 -304
- package/dist/esm/model/NicePhysicsSimulation.js +517 -282
- package/dist/esm/model/NiceProceduralGeometry.js +667 -284
- package/dist/esm/model/NiceTerrainEditor.js +584 -369
- package/dist/esm/model/NiceWeightPainter.js +373 -245
- package/dist/esm/model/NiceXRPreview.js +374 -292
- package/dist/esm/model/cadModeUtils.js +95 -88
- package/dist/esm/model/editorShortcuts.js +172 -193
- package/dist/esm/model/modelEditorTypes.js +90 -100
- package/dist/esm/model/modelEditorUtils.js +1056 -1128
- package/dist/esm/model/simsModeUtils.js +1021 -1035
- package/dist/esm/model/useModelEditor.js +1946 -1920
- package/dist/esm/model/useModelViewer.js +619 -647
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +1188 -570
- package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
- package/dist/esm/particle/particleEditorTypes.js +69 -69
- package/dist/esm/particle/particleEditorUtils.js +1027 -1012
- package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
- package/dist/esm/rendering/NiceRenderExport.js +413 -321
- package/dist/esm/rendering/NiceSSAO.js +450 -328
- package/dist/esm/rendering/NiceSSR.js +383 -252
- package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
- package/dist/esm/uv/NiceUVEditor.js +788 -549
- package/dist/esm/uv/UVEditor.module.css.js +53 -3
- package/dist/esm/uv/uvEditorTypes.js +68 -75
- package/dist/esm/uv/uvEditorUtils.js +515 -557
- package/package.json +7 -12
|
@@ -1,851 +1,788 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/* ════════════════════════════════════════════════════════════════════════════
|
|
6
|
-
Material Editor Utilities
|
|
7
|
-
|
|
8
|
-
Graph operations, shader compilation, and helper functions:
|
|
9
|
-
- Graph manipulation (add/remove/connect nodes)
|
|
10
|
-
- Topological sorting for compilation order
|
|
11
|
-
- GLSL shader generation
|
|
12
|
-
- Import/export functionality
|
|
13
|
-
- Validation
|
|
14
|
-
════════════════════════════════════════════════════════════════════════════ */
|
|
15
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
ID Generation
|
|
17
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const materialNodeDefinitions = require("./materialNodeDefinitions.js");
|
|
18
4
|
let nodeIdCounter = 0;
|
|
19
5
|
let connectionIdCounter = 0;
|
|
20
6
|
function generateNodeId() {
|
|
21
|
-
|
|
7
|
+
return `node_${Date.now()}_${++nodeIdCounter}`;
|
|
22
8
|
}
|
|
23
9
|
function generateConnectionId() {
|
|
24
|
-
|
|
10
|
+
return `conn_${Date.now()}_${++connectionIdCounter}`;
|
|
25
11
|
}
|
|
26
12
|
function resetIdCounters() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
depthWrite: true,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
13
|
+
nodeIdCounter = 0;
|
|
14
|
+
connectionIdCounter = 0;
|
|
15
|
+
}
|
|
16
|
+
function createEmptyGraph(name = "New Material") {
|
|
17
|
+
const outputNode = {
|
|
18
|
+
id: generateNodeId(),
|
|
19
|
+
type: "output_pbr",
|
|
20
|
+
position: { x: 400, y: 200 },
|
|
21
|
+
properties: {},
|
|
22
|
+
inputValues: {}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
id: `graph_${Date.now()}`,
|
|
26
|
+
name,
|
|
27
|
+
nodes: [outputNode],
|
|
28
|
+
connections: [],
|
|
29
|
+
outputNodeId: outputNode.id,
|
|
30
|
+
metadata: {
|
|
31
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
32
|
+
modifiedAt: /* @__PURE__ */ new Date(),
|
|
33
|
+
blendMode: "opaque",
|
|
34
|
+
cullMode: "back",
|
|
35
|
+
depthTest: true,
|
|
36
|
+
depthWrite: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
56
39
|
}
|
|
57
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
-
Node Operations
|
|
59
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
60
40
|
function createNode(type, position, definitions = materialNodeDefinitions.MATERIAL_NODE_MAP) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
properties,
|
|
83
|
-
inputValues,
|
|
84
|
-
};
|
|
41
|
+
const definition = definitions.get(type);
|
|
42
|
+
if (!definition) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const properties = {};
|
|
46
|
+
const inputValues = {};
|
|
47
|
+
definition.properties?.forEach((prop) => {
|
|
48
|
+
properties[prop.id] = prop.default;
|
|
49
|
+
});
|
|
50
|
+
definition.inputs.forEach((input) => {
|
|
51
|
+
if (input.defaultValue !== void 0) {
|
|
52
|
+
inputValues[input.id] = input.defaultValue;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
id: generateNodeId(),
|
|
57
|
+
type,
|
|
58
|
+
position: { ...position },
|
|
59
|
+
properties,
|
|
60
|
+
inputValues
|
|
61
|
+
};
|
|
85
62
|
}
|
|
86
63
|
function cloneNode(node) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
return {
|
|
65
|
+
...node,
|
|
66
|
+
id: generateNodeId(),
|
|
67
|
+
position: { x: node.position.x + 20, y: node.position.y + 20 },
|
|
68
|
+
properties: { ...node.properties },
|
|
69
|
+
inputValues: { ...node.inputValues },
|
|
70
|
+
selected: false
|
|
71
|
+
};
|
|
95
72
|
}
|
|
96
73
|
function getNodeDefinition(node, definitions = materialNodeDefinitions.MATERIAL_NODE_MAP) {
|
|
97
|
-
|
|
74
|
+
return definitions.get(node.type);
|
|
98
75
|
}
|
|
99
76
|
function getNodeBounds(node, definition) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
77
|
+
const nodeWidth = 180;
|
|
78
|
+
const headerHeight = 32;
|
|
79
|
+
const portHeight = 24;
|
|
80
|
+
const maxPorts = Math.max(definition.inputs.length, definition.outputs.length);
|
|
81
|
+
const nodeHeight = headerHeight + maxPorts * portHeight + 8;
|
|
82
|
+
return {
|
|
83
|
+
x: node.position.x,
|
|
84
|
+
y: node.position.y,
|
|
85
|
+
width: nodeWidth,
|
|
86
|
+
height: nodeHeight
|
|
87
|
+
};
|
|
111
88
|
}
|
|
112
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
-
Connection Operations
|
|
114
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
115
89
|
function createConnection(sourceNodeId, sourcePortId, targetNodeId, targetPortId) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
return {
|
|
91
|
+
id: generateConnectionId(),
|
|
92
|
+
sourceNodeId,
|
|
93
|
+
sourcePortId,
|
|
94
|
+
targetNodeId,
|
|
95
|
+
targetPortId
|
|
96
|
+
};
|
|
123
97
|
}
|
|
124
98
|
function canConnect(sourceNode, sourcePort, targetNode, targetPort, existingConnections) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return { valid: true };
|
|
99
|
+
if (sourceNode.id === targetNode.id) {
|
|
100
|
+
return { valid: false, reason: "Cannot connect node to itself" };
|
|
101
|
+
}
|
|
102
|
+
if (sourcePort.direction !== "output" || targetPort.direction !== "input") {
|
|
103
|
+
return { valid: false, reason: "Invalid port direction" };
|
|
104
|
+
}
|
|
105
|
+
const existingConnection = existingConnections.find(
|
|
106
|
+
(conn) => conn.targetNodeId === targetNode.id && conn.targetPortId === targetPort.id
|
|
107
|
+
);
|
|
108
|
+
if (existingConnection) {
|
|
109
|
+
return { valid: false, reason: "Port already connected" };
|
|
110
|
+
}
|
|
111
|
+
if (!areTypesCompatible(sourcePort.dataType, targetPort.dataType)) {
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
reason: `Incompatible types: ${sourcePort.dataType} → ${targetPort.dataType}`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { valid: true };
|
|
146
118
|
}
|
|
147
119
|
function areTypesCompatible(sourceType, targetType) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (sourceType === 'vec3' && targetType === 'color') {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
return false;
|
|
120
|
+
if (sourceType === "any" || targetType === "any") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (sourceType === targetType) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (sourceType === "float" && ["vec2", "vec3", "vec4"].includes(targetType)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (sourceType === "color" && ["vec3", "vec4"].includes(targetType)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (sourceType === "vec3" && targetType === "color") {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
168
136
|
}
|
|
169
137
|
function getPortConnections(nodeId, portId, direction, connections) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
138
|
+
if (direction === "input") {
|
|
139
|
+
return connections.filter(
|
|
140
|
+
(conn) => conn.targetNodeId === nodeId && conn.targetPortId === portId
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return connections.filter((conn) => conn.sourceNodeId === nodeId && conn.sourcePortId === portId);
|
|
174
144
|
}
|
|
175
145
|
function getNodeConnections(nodeId, connections) {
|
|
176
|
-
|
|
146
|
+
return connections.filter((conn) => conn.sourceNodeId === nodeId || conn.targetNodeId === nodeId);
|
|
177
147
|
}
|
|
178
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
-
Graph Operations
|
|
180
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
181
148
|
function addNodeToGraph(graph, node) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
149
|
+
return {
|
|
150
|
+
...graph,
|
|
151
|
+
nodes: [...graph.nodes, node],
|
|
152
|
+
metadata: { ...graph.metadata, modifiedAt: /* @__PURE__ */ new Date() }
|
|
153
|
+
};
|
|
187
154
|
}
|
|
188
155
|
function removeNodesFromGraph(graph, nodeIds) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
156
|
+
const nodeIdSet = new Set(nodeIds);
|
|
157
|
+
if (nodeIdSet.has(graph.outputNodeId)) {
|
|
158
|
+
nodeIdSet.delete(graph.outputNodeId);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...graph,
|
|
162
|
+
nodes: graph.nodes.filter((node) => !nodeIdSet.has(node.id)),
|
|
163
|
+
connections: graph.connections.filter(
|
|
164
|
+
(conn) => !nodeIdSet.has(conn.sourceNodeId) && !nodeIdSet.has(conn.targetNodeId)
|
|
165
|
+
),
|
|
166
|
+
metadata: { ...graph.metadata, modifiedAt: /* @__PURE__ */ new Date() }
|
|
167
|
+
};
|
|
200
168
|
}
|
|
201
169
|
function addConnectionToGraph(graph, connection) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
170
|
+
const filteredConnections = graph.connections.filter(
|
|
171
|
+
(conn) => !(conn.targetNodeId === connection.targetNodeId && conn.targetPortId === connection.targetPortId)
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
...graph,
|
|
175
|
+
connections: [...filteredConnections, connection],
|
|
176
|
+
metadata: { ...graph.metadata, modifiedAt: /* @__PURE__ */ new Date() }
|
|
177
|
+
};
|
|
210
178
|
}
|
|
211
179
|
function removeConnectionFromGraph(graph, connectionId) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
180
|
+
return {
|
|
181
|
+
...graph,
|
|
182
|
+
connections: graph.connections.filter((conn) => conn.id !== connectionId),
|
|
183
|
+
metadata: { ...graph.metadata, modifiedAt: /* @__PURE__ */ new Date() }
|
|
184
|
+
};
|
|
217
185
|
}
|
|
218
186
|
function updateNodeInGraph(graph, nodeId, updates) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
187
|
+
return {
|
|
188
|
+
...graph,
|
|
189
|
+
nodes: graph.nodes.map((node) => node.id === nodeId ? { ...node, ...updates } : node),
|
|
190
|
+
metadata: { ...graph.metadata, modifiedAt: /* @__PURE__ */ new Date() }
|
|
191
|
+
};
|
|
224
192
|
}
|
|
225
193
|
function moveNodesInGraph(graph, nodeIds, delta) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
194
|
+
const nodeIdSet = new Set(nodeIds);
|
|
195
|
+
return {
|
|
196
|
+
...graph,
|
|
197
|
+
nodes: graph.nodes.map(
|
|
198
|
+
(node) => nodeIdSet.has(node.id) ? { ...node, position: { x: node.position.x + delta.x, y: node.position.y + delta.y } } : node
|
|
199
|
+
)
|
|
200
|
+
};
|
|
233
201
|
}
|
|
234
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
235
|
-
Topological Sort
|
|
236
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
237
202
|
function topologicalSort(graph) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
sorted.push(node);
|
|
262
|
-
}
|
|
263
|
-
return true;
|
|
203
|
+
const sorted = [];
|
|
204
|
+
const visited = /* @__PURE__ */ new Set();
|
|
205
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
206
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
207
|
+
function visit(nodeId) {
|
|
208
|
+
if (visited.has(nodeId)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (visiting.has(nodeId)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
visiting.add(nodeId);
|
|
215
|
+
const inputConnections = graph.connections.filter((conn) => conn.targetNodeId === nodeId);
|
|
216
|
+
for (const conn of inputConnections) {
|
|
217
|
+
if (!visit(conn.sourceNodeId)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
visiting.delete(nodeId);
|
|
222
|
+
visited.add(nodeId);
|
|
223
|
+
const node = nodeMap.get(nodeId);
|
|
224
|
+
if (node) {
|
|
225
|
+
sorted.push(node);
|
|
264
226
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
visit(graph.outputNodeId);
|
|
230
|
+
for (const node of graph.nodes) {
|
|
231
|
+
if (!visited.has(node.id)) {
|
|
232
|
+
visit(node.id);
|
|
272
233
|
}
|
|
273
|
-
|
|
234
|
+
}
|
|
235
|
+
return sorted;
|
|
274
236
|
}
|
|
275
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
276
|
-
Shader Compilation
|
|
277
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
278
237
|
function compileShader(graph, definitions = materialNodeDefinitions.MATERIAL_NODE_MAP) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (errors.length > 0) {
|
|
288
|
-
return {
|
|
289
|
-
vertexShader: generateDefaultVertexShader(),
|
|
290
|
-
fragmentShader: generateErrorFragmentShader(errors),
|
|
291
|
-
uniforms,
|
|
292
|
-
attributes: [],
|
|
293
|
-
defines: {},
|
|
294
|
-
errors,
|
|
295
|
-
warnings,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
// Sort nodes topologically
|
|
299
|
-
const sortedNodes = topologicalSort(graph);
|
|
300
|
-
// Generate code for each node
|
|
301
|
-
const nodeOutputs = new Map();
|
|
302
|
-
for (const node of sortedNodes) {
|
|
303
|
-
const definition = definitions.get(node.type);
|
|
304
|
-
if (!definition) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
const nodeCode = generateNodeCode(node, definition, graph, nodeOutputs);
|
|
308
|
-
if (nodeCode) {
|
|
309
|
-
fragmentLines.push(` // ${definition.name}`);
|
|
310
|
-
fragmentLines.push(nodeCode);
|
|
311
|
-
fragmentLines.push('');
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Generate final fragment shader
|
|
315
|
-
const fragmentShader = generateFragmentShader(graph, fragmentLines, definitions);
|
|
238
|
+
const errors = [];
|
|
239
|
+
const warnings = [];
|
|
240
|
+
const uniforms = [];
|
|
241
|
+
const fragmentLines = [];
|
|
242
|
+
const validationResult = validateGraph(graph, definitions);
|
|
243
|
+
errors.push(...validationResult.errors);
|
|
244
|
+
warnings.push(...validationResult.warnings);
|
|
245
|
+
if (errors.length > 0) {
|
|
316
246
|
return {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
],
|
|
325
|
-
defines: {},
|
|
326
|
-
errors,
|
|
327
|
-
warnings,
|
|
247
|
+
vertexShader: generateDefaultVertexShader(),
|
|
248
|
+
fragmentShader: generateErrorFragmentShader(errors),
|
|
249
|
+
uniforms,
|
|
250
|
+
attributes: [],
|
|
251
|
+
defines: {},
|
|
252
|
+
errors,
|
|
253
|
+
warnings
|
|
328
254
|
};
|
|
255
|
+
}
|
|
256
|
+
const sortedNodes = topologicalSort(graph);
|
|
257
|
+
const nodeOutputs = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const node of sortedNodes) {
|
|
259
|
+
const definition = definitions.get(node.type);
|
|
260
|
+
if (!definition) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const nodeCode = generateNodeCode(node, definition, graph, nodeOutputs);
|
|
264
|
+
if (nodeCode) {
|
|
265
|
+
fragmentLines.push(` // ${definition.name}`);
|
|
266
|
+
fragmentLines.push(nodeCode);
|
|
267
|
+
fragmentLines.push("");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const fragmentShader = generateFragmentShader(graph, fragmentLines, definitions);
|
|
271
|
+
return {
|
|
272
|
+
vertexShader: generateDefaultVertexShader(),
|
|
273
|
+
fragmentShader,
|
|
274
|
+
uniforms,
|
|
275
|
+
attributes: [
|
|
276
|
+
{ name: "position", type: "vec3", location: 0 },
|
|
277
|
+
{ name: "normal", type: "vec3", location: 1 },
|
|
278
|
+
{ name: "uv", type: "vec2", location: 2 }
|
|
279
|
+
],
|
|
280
|
+
defines: {},
|
|
281
|
+
errors,
|
|
282
|
+
warnings
|
|
283
|
+
};
|
|
329
284
|
}
|
|
330
285
|
function generateNodeCode(node, definition, graph, nodeOutputs, uniforms) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return lines.join('\n');
|
|
286
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
287
|
+
const lines = [];
|
|
288
|
+
const inputValues = {};
|
|
289
|
+
for (const input of definition.inputs) {
|
|
290
|
+
const connection = graph.connections.find(
|
|
291
|
+
(conn) => conn.targetNodeId === node.id && conn.targetPortId === input.id
|
|
292
|
+
);
|
|
293
|
+
if (connection) {
|
|
294
|
+
const sourceOutputs = nodeOutputs.get(connection.sourceNodeId);
|
|
295
|
+
inputValues[input.id] = sourceOutputs?.get(connection.sourcePortId) || "0.0";
|
|
296
|
+
} else {
|
|
297
|
+
inputValues[input.id] = formatDefaultValue(
|
|
298
|
+
node.inputValues[input.id] ?? input.defaultValue,
|
|
299
|
+
input.dataType
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const output of definition.outputs) {
|
|
304
|
+
const varName = `${node.id}_${output.id}`;
|
|
305
|
+
outputs.set(output.id, varName);
|
|
306
|
+
}
|
|
307
|
+
nodeOutputs.set(node.id, outputs);
|
|
308
|
+
const code = generateGLSLForNode(node, definition, inputValues);
|
|
309
|
+
if (code) {
|
|
310
|
+
lines.push(code);
|
|
311
|
+
}
|
|
312
|
+
return lines.join("\n");
|
|
359
313
|
}
|
|
360
314
|
function generateGLSLForNode(node, definition, inputs, _outputs, _uniforms) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
float ${nodeId}
|
|
376
|
-
float ${nodeId}_g = ${nodeId}_rgb.g;
|
|
377
|
-
float ${nodeId}_b = ${nodeId}_rgb.b;
|
|
315
|
+
const nodeId = node.id.replace(/-/g, "_");
|
|
316
|
+
switch (definition.type) {
|
|
317
|
+
// Input nodes
|
|
318
|
+
case "constant_float": {
|
|
319
|
+
const value = node.properties.value;
|
|
320
|
+
return ` float ${nodeId}_value = ${typeof value === "number" ? value.toFixed(4) : "0.0"};`;
|
|
321
|
+
}
|
|
322
|
+
case "constant_color": {
|
|
323
|
+
const rawColor = node.properties.color;
|
|
324
|
+
const color = parseColor(typeof rawColor === "string" ? rawColor : "var(--nice-bg, #fff)");
|
|
325
|
+
return ` vec3 ${nodeId}_rgb = vec3(${color.r.toFixed(4)}, ${color.g.toFixed(4)}, ${color.b.toFixed(4)});
|
|
326
|
+
vec4 ${nodeId}_rgba = vec4(${nodeId}_rgb, 1.0);
|
|
327
|
+
float ${nodeId}_r = ${nodeId}_rgb.r;
|
|
328
|
+
float ${nodeId}_g = ${nodeId}_rgb.g;
|
|
329
|
+
float ${nodeId}_b = ${nodeId}_rgb.b;
|
|
378
330
|
float ${nodeId}_a = 1.0;`;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
float ${nodeId}_u = vUv.x;
|
|
331
|
+
}
|
|
332
|
+
case "uv":
|
|
333
|
+
return ` vec2 ${nodeId}_uv = vUv;
|
|
334
|
+
float ${nodeId}_u = vUv.x;
|
|
383
335
|
float ${nodeId}_v = vUv.y;`;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
float ${nodeId}_sin = sin(uTime);
|
|
387
|
-
float ${nodeId}_cos = cos(uTime);
|
|
336
|
+
case "time":
|
|
337
|
+
return ` float ${nodeId}_time = uTime;
|
|
338
|
+
float ${nodeId}_sin = sin(uTime);
|
|
339
|
+
float ${nodeId}_cos = cos(uTime);
|
|
388
340
|
float ${nodeId}_delta = uDeltaTime;`;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
vec3 ${nodeId}_object = vPosition;
|
|
341
|
+
case "position":
|
|
342
|
+
return ` vec3 ${nodeId}_world = vWorldPosition;
|
|
343
|
+
vec3 ${nodeId}_object = vPosition;
|
|
392
344
|
vec3 ${nodeId}_view = vViewPosition;`;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
vec3 ${nodeId}_object = normalize(vNormal);
|
|
396
|
-
vec3 ${nodeId}_view = normalize(vViewNormal);
|
|
397
|
-
vec3 ${nodeId}_tangent = normalize(vTangent);
|
|
345
|
+
case "normal":
|
|
346
|
+
return ` vec3 ${nodeId}_world = normalize(vWorldNormal);
|
|
347
|
+
vec3 ${nodeId}_object = normalize(vNormal);
|
|
348
|
+
vec3 ${nodeId}_view = normalize(vViewNormal);
|
|
349
|
+
vec3 ${nodeId}_tangent = normalize(vTangent);
|
|
398
350
|
vec3 ${nodeId}_bitangent = normalize(vBitangent);`;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
float ${nodeId}_y = ${inputs.vector}.y;
|
|
351
|
+
// Math nodes
|
|
352
|
+
case "add":
|
|
353
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = ${inputs.a} + ${inputs.b};`;
|
|
354
|
+
case "subtract":
|
|
355
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = ${inputs.a} - ${inputs.b};`;
|
|
356
|
+
case "multiply":
|
|
357
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = ${inputs.a} * ${inputs.b};`;
|
|
358
|
+
case "divide":
|
|
359
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = ${inputs.a} / max(${inputs.b}, 0.0001);`;
|
|
360
|
+
case "sin":
|
|
361
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = sin(${inputs.value});`;
|
|
362
|
+
case "cos":
|
|
363
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = cos(${inputs.value});`;
|
|
364
|
+
case "abs":
|
|
365
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = abs(${inputs.value});`;
|
|
366
|
+
case "floor":
|
|
367
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = floor(${inputs.value});`;
|
|
368
|
+
case "fract":
|
|
369
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = fract(${inputs.value});`;
|
|
370
|
+
case "min":
|
|
371
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = min(${inputs.a}, ${inputs.b});`;
|
|
372
|
+
case "max":
|
|
373
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = max(${inputs.a}, ${inputs.b});`;
|
|
374
|
+
// Vector nodes
|
|
375
|
+
case "combine_vec2":
|
|
376
|
+
return ` vec2 ${nodeId}_vector = vec2(${inputs.x}, ${inputs.y});`;
|
|
377
|
+
case "combine_vec3":
|
|
378
|
+
return ` vec3 ${nodeId}_vector = vec3(${inputs.x}, ${inputs.y}, ${inputs.z});`;
|
|
379
|
+
case "combine_vec4":
|
|
380
|
+
return ` vec4 ${nodeId}_vector = vec4(${inputs.x}, ${inputs.y}, ${inputs.z}, ${inputs.w});`;
|
|
381
|
+
case "split_vec3":
|
|
382
|
+
return ` float ${nodeId}_x = ${inputs.vector}.x;
|
|
383
|
+
float ${nodeId}_y = ${inputs.vector}.y;
|
|
432
384
|
float ${nodeId}_z = ${inputs.vector}.z;`;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
385
|
+
case "normalize":
|
|
386
|
+
return ` vec3 ${nodeId}_result = normalize(${inputs.vector});`;
|
|
387
|
+
case "dot":
|
|
388
|
+
return ` float ${nodeId}_result = dot(${inputs.a}, ${inputs.b});`;
|
|
389
|
+
case "cross":
|
|
390
|
+
return ` vec3 ${nodeId}_result = cross(${inputs.a}, ${inputs.b});`;
|
|
391
|
+
// Utility nodes
|
|
392
|
+
case "lerp":
|
|
393
|
+
return ` ${getTypeForAny(inputs.a)} ${nodeId}_result = mix(${inputs.a}, ${inputs.b}, ${inputs.t});`;
|
|
394
|
+
case "clamp":
|
|
395
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = clamp(${inputs.value}, ${inputs.min}, ${inputs.max});`;
|
|
396
|
+
case "saturate":
|
|
397
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = clamp(${inputs.value}, 0.0, 1.0);`;
|
|
398
|
+
case "smoothstep":
|
|
399
|
+
return ` float ${nodeId}_result = smoothstep(${inputs.edge0}, ${inputs.edge1}, ${inputs.x});`;
|
|
400
|
+
case "one_minus":
|
|
401
|
+
return ` ${getTypeForAny(inputs.value)} ${nodeId}_result = 1.0 - ${inputs.value};`;
|
|
402
|
+
case "fresnel":
|
|
403
|
+
return ` float ${nodeId}_result = pow(1.0 - max(dot(${inputs.normal}, ${inputs.viewDir}), 0.0), ${inputs.power});`;
|
|
404
|
+
// Procedural nodes
|
|
405
|
+
case "noise_perlin":
|
|
406
|
+
return ` float ${nodeId}_value = perlinNoise(${inputs.uv} * ${inputs.scale});`;
|
|
407
|
+
case "checker":
|
|
408
|
+
return ` float ${nodeId}_value = mod(floor(${inputs.uv}.x * ${inputs.scale}) + floor(${inputs.uv}.y * ${inputs.scale}), 2.0);
|
|
457
409
|
vec3 ${nodeId}_color = mix(${inputs.colorA}, ${inputs.colorB}, ${nodeId}_value);`;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
410
|
+
case "gradient_linear": {
|
|
411
|
+
const dir = node.properties.direction || "horizontal";
|
|
412
|
+
const gradientCoord = dir === "horizontal" ? "x" : dir === "vertical" ? "y" : "(x + y) * 0.5";
|
|
413
|
+
return ` float ${nodeId}_value = ${inputs.uv}.${gradientCoord === "(x + y) * 0.5" ? gradientCoord : gradientCoord};`;
|
|
414
|
+
}
|
|
415
|
+
case "gradient_radial":
|
|
416
|
+
return ` float ${nodeId}_value = 1.0 - clamp(distance(${inputs.uv}, ${inputs.center}) / ${inputs.radius}, 0.0, 1.0);`;
|
|
417
|
+
// Color nodes
|
|
418
|
+
case "invert":
|
|
419
|
+
return ` vec3 ${nodeId}_result = vec3(1.0) - ${inputs.color};`;
|
|
420
|
+
case "grayscale":
|
|
421
|
+
return ` float ${nodeId}_gray = dot(${inputs.color}, vec3(0.299, 0.587, 0.114));
|
|
470
422
|
vec3 ${nodeId}_rgb = vec3(${nodeId}_gray);`;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
default:
|
|
478
|
-
return ` // Unsupported node type: ${definition.type}`;
|
|
423
|
+
case "blend": {
|
|
424
|
+
const rawMode = node.properties.mode;
|
|
425
|
+
const mode = typeof rawMode === "string" ? rawMode : "normal";
|
|
426
|
+
const blendCode = generateBlendMode(mode, inputs.base, inputs.blend);
|
|
427
|
+
return ` vec3 ${nodeId}_result = mix(${inputs.base}, ${blendCode}, ${inputs.factor});`;
|
|
479
428
|
}
|
|
429
|
+
default:
|
|
430
|
+
return ` // Unsupported node type: ${definition.type}`;
|
|
431
|
+
}
|
|
480
432
|
}
|
|
481
433
|
function generateBlendMode(mode, base, blend) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
434
|
+
switch (mode) {
|
|
435
|
+
case "multiply":
|
|
436
|
+
return `${base} * ${blend}`;
|
|
437
|
+
case "screen":
|
|
438
|
+
return `1.0 - (1.0 - ${base}) * (1.0 - ${blend})`;
|
|
439
|
+
case "overlay":
|
|
440
|
+
return `mix(2.0 * ${base} * ${blend}, 1.0 - 2.0 * (1.0 - ${base}) * (1.0 - ${blend}), step(0.5, ${base}))`;
|
|
441
|
+
case "add":
|
|
442
|
+
return `min(${base} + ${blend}, vec3(1.0))`;
|
|
443
|
+
case "subtract":
|
|
444
|
+
return `max(${base} - ${blend}, vec3(0.0))`;
|
|
445
|
+
case "difference":
|
|
446
|
+
return `abs(${base} - ${blend})`;
|
|
447
|
+
default:
|
|
448
|
+
return blend;
|
|
449
|
+
}
|
|
498
450
|
}
|
|
499
451
|
function getTypeForAny(value) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
452
|
+
if (value.startsWith("vec4")) {
|
|
453
|
+
return "vec4";
|
|
454
|
+
}
|
|
455
|
+
if (value.startsWith("vec3")) {
|
|
456
|
+
return "vec3";
|
|
457
|
+
}
|
|
458
|
+
if (value.startsWith("vec2")) {
|
|
459
|
+
return "vec2";
|
|
460
|
+
}
|
|
461
|
+
return "float";
|
|
510
462
|
}
|
|
511
463
|
function formatDefaultValue(value, dataType) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
464
|
+
if (value === void 0 || value === null) {
|
|
465
|
+
switch (dataType) {
|
|
466
|
+
case "float":
|
|
467
|
+
return "0.0";
|
|
468
|
+
case "vec2":
|
|
469
|
+
return "vec2(0.0)";
|
|
470
|
+
case "vec3":
|
|
471
|
+
return "vec3(0.0)";
|
|
472
|
+
case "vec4":
|
|
473
|
+
return "vec4(0.0)";
|
|
474
|
+
case "bool":
|
|
475
|
+
return "false";
|
|
476
|
+
case "int":
|
|
477
|
+
return "0";
|
|
478
|
+
default:
|
|
479
|
+
return "0.0";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(value)) {
|
|
483
|
+
if (value.length === 2) {
|
|
484
|
+
return `vec2(${value[0].toFixed(4)}, ${value[1].toFixed(4)})`;
|
|
485
|
+
}
|
|
486
|
+
if (value.length === 3) {
|
|
487
|
+
return `vec3(${value[0].toFixed(4)}, ${value[1].toFixed(4)}, ${value[2].toFixed(4)})`;
|
|
488
|
+
}
|
|
489
|
+
if (value.length === 4) {
|
|
490
|
+
return `vec4(${value[0].toFixed(4)}, ${value[1].toFixed(4)}, ${value[2].toFixed(4)}, ${value[3].toFixed(4)})`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (typeof value === "number") {
|
|
494
|
+
return value.toFixed(4);
|
|
495
|
+
}
|
|
496
|
+
if (typeof value === "boolean") {
|
|
497
|
+
return value ? "true" : "false";
|
|
498
|
+
}
|
|
499
|
+
return String(value);
|
|
548
500
|
}
|
|
549
501
|
function parseColor(hex) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
: { r: 1, g: 1, b: 1 };
|
|
502
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
503
|
+
return result ? {
|
|
504
|
+
r: parseInt(result[1], 16) / 255,
|
|
505
|
+
g: parseInt(result[2], 16) / 255,
|
|
506
|
+
b: parseInt(result[3], 16) / 255
|
|
507
|
+
} : { r: 1, g: 1, b: 1 };
|
|
558
508
|
}
|
|
559
509
|
function generateDefaultVertexShader() {
|
|
560
|
-
|
|
561
|
-
precision highp float;
|
|
562
|
-
|
|
563
|
-
in vec3 position;
|
|
564
|
-
in vec3 normal;
|
|
565
|
-
in vec2 uv;
|
|
566
|
-
in vec3 tangent;
|
|
567
|
-
|
|
568
|
-
uniform mat4 modelMatrix;
|
|
569
|
-
uniform mat4 viewMatrix;
|
|
570
|
-
uniform mat4 projectionMatrix;
|
|
571
|
-
uniform mat3 normalMatrix;
|
|
572
|
-
|
|
573
|
-
out vec3 vPosition;
|
|
574
|
-
out vec3 vNormal;
|
|
575
|
-
out vec2 vUv;
|
|
576
|
-
out vec3 vWorldPosition;
|
|
577
|
-
out vec3 vWorldNormal;
|
|
578
|
-
out vec3 vViewPosition;
|
|
579
|
-
out vec3 vViewNormal;
|
|
580
|
-
out vec3 vTangent;
|
|
581
|
-
out vec3 vBitangent;
|
|
582
|
-
|
|
583
|
-
void main() {
|
|
584
|
-
vPosition = position;
|
|
585
|
-
vNormal = normal;
|
|
586
|
-
vUv = uv;
|
|
587
|
-
|
|
588
|
-
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
589
|
-
vWorldPosition = worldPosition.xyz;
|
|
590
|
-
vWorldNormal = normalMatrix * normal;
|
|
591
|
-
|
|
592
|
-
vec4 viewPosition = viewMatrix * worldPosition;
|
|
593
|
-
vViewPosition = viewPosition.xyz;
|
|
594
|
-
vViewNormal = mat3(viewMatrix) * vWorldNormal;
|
|
595
|
-
|
|
596
|
-
vTangent = normalMatrix * tangent;
|
|
597
|
-
vBitangent = cross(vWorldNormal, vTangent);
|
|
598
|
-
|
|
599
|
-
gl_Position = projectionMatrix * viewPosition;
|
|
510
|
+
return `#version 300 es
|
|
511
|
+
precision highp float;
|
|
512
|
+
|
|
513
|
+
in vec3 position;
|
|
514
|
+
in vec3 normal;
|
|
515
|
+
in vec2 uv;
|
|
516
|
+
in vec3 tangent;
|
|
517
|
+
|
|
518
|
+
uniform mat4 modelMatrix;
|
|
519
|
+
uniform mat4 viewMatrix;
|
|
520
|
+
uniform mat4 projectionMatrix;
|
|
521
|
+
uniform mat3 normalMatrix;
|
|
522
|
+
|
|
523
|
+
out vec3 vPosition;
|
|
524
|
+
out vec3 vNormal;
|
|
525
|
+
out vec2 vUv;
|
|
526
|
+
out vec3 vWorldPosition;
|
|
527
|
+
out vec3 vWorldNormal;
|
|
528
|
+
out vec3 vViewPosition;
|
|
529
|
+
out vec3 vViewNormal;
|
|
530
|
+
out vec3 vTangent;
|
|
531
|
+
out vec3 vBitangent;
|
|
532
|
+
|
|
533
|
+
void main() {
|
|
534
|
+
vPosition = position;
|
|
535
|
+
vNormal = normal;
|
|
536
|
+
vUv = uv;
|
|
537
|
+
|
|
538
|
+
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
539
|
+
vWorldPosition = worldPosition.xyz;
|
|
540
|
+
vWorldNormal = normalMatrix * normal;
|
|
541
|
+
|
|
542
|
+
vec4 viewPosition = viewMatrix * worldPosition;
|
|
543
|
+
vViewPosition = viewPosition.xyz;
|
|
544
|
+
vViewNormal = mat3(viewMatrix) * vWorldNormal;
|
|
545
|
+
|
|
546
|
+
vTangent = normalMatrix * tangent;
|
|
547
|
+
vBitangent = cross(vWorldNormal, vTangent);
|
|
548
|
+
|
|
549
|
+
gl_Position = projectionMatrix * viewPosition;
|
|
600
550
|
}`;
|
|
601
551
|
}
|
|
602
552
|
function generateFragmentShader(graph, nodeCode, definitions) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
precision highp float;
|
|
607
|
-
|
|
608
|
-
in vec3 vPosition;
|
|
609
|
-
in vec3 vNormal;
|
|
610
|
-
in vec2 vUv;
|
|
611
|
-
in vec3 vWorldPosition;
|
|
612
|
-
in vec3 vWorldNormal;
|
|
613
|
-
in vec3 vViewPosition;
|
|
614
|
-
in vec3 vViewNormal;
|
|
615
|
-
in vec3 vTangent;
|
|
616
|
-
in vec3 vBitangent;
|
|
617
|
-
|
|
618
|
-
uniform float uTime;
|
|
619
|
-
uniform float uDeltaTime;
|
|
620
|
-
uniform vec3 uCameraPosition;
|
|
621
|
-
uniform vec2 uResolution;
|
|
622
|
-
|
|
623
|
-
out vec4 fragColor;
|
|
624
|
-
|
|
625
|
-
// Noise functions
|
|
626
|
-
float hash(vec2 p) {
|
|
627
|
-
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
float perlinNoise(vec2 p) {
|
|
631
|
-
vec2 i = floor(p);
|
|
632
|
-
vec2 f = fract(p);
|
|
633
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
634
|
-
|
|
635
|
-
float a = hash(i);
|
|
636
|
-
float b = hash(i + vec2(1.0, 0.0));
|
|
637
|
-
float c = hash(i + vec2(0.0, 1.0));
|
|
638
|
-
float d = hash(i + vec2(1.0, 1.0));
|
|
639
|
-
|
|
640
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
void main() {
|
|
644
|
-
${nodeCode.join(
|
|
645
|
-
|
|
646
|
-
// Output
|
|
647
|
-
${generateOutputAssignment(graph)}
|
|
553
|
+
const outputNode = graph.nodes.find((n) => n.id === graph.outputNodeId);
|
|
554
|
+
outputNode ? definitions.get(outputNode.type) : null;
|
|
555
|
+
return `#version 300 es
|
|
556
|
+
precision highp float;
|
|
557
|
+
|
|
558
|
+
in vec3 vPosition;
|
|
559
|
+
in vec3 vNormal;
|
|
560
|
+
in vec2 vUv;
|
|
561
|
+
in vec3 vWorldPosition;
|
|
562
|
+
in vec3 vWorldNormal;
|
|
563
|
+
in vec3 vViewPosition;
|
|
564
|
+
in vec3 vViewNormal;
|
|
565
|
+
in vec3 vTangent;
|
|
566
|
+
in vec3 vBitangent;
|
|
567
|
+
|
|
568
|
+
uniform float uTime;
|
|
569
|
+
uniform float uDeltaTime;
|
|
570
|
+
uniform vec3 uCameraPosition;
|
|
571
|
+
uniform vec2 uResolution;
|
|
572
|
+
|
|
573
|
+
out vec4 fragColor;
|
|
574
|
+
|
|
575
|
+
// Noise functions
|
|
576
|
+
float hash(vec2 p) {
|
|
577
|
+
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
float perlinNoise(vec2 p) {
|
|
581
|
+
vec2 i = floor(p);
|
|
582
|
+
vec2 f = fract(p);
|
|
583
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
584
|
+
|
|
585
|
+
float a = hash(i);
|
|
586
|
+
float b = hash(i + vec2(1.0, 0.0));
|
|
587
|
+
float c = hash(i + vec2(0.0, 1.0));
|
|
588
|
+
float d = hash(i + vec2(1.0, 1.0));
|
|
589
|
+
|
|
590
|
+
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
void main() {
|
|
594
|
+
${nodeCode.join("\n")}
|
|
595
|
+
|
|
596
|
+
// Output
|
|
597
|
+
${generateOutputAssignment(graph)}
|
|
648
598
|
}`;
|
|
649
599
|
}
|
|
650
600
|
function generateOutputAssignment(graph, _definitions) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return 'fragColor = vec4(1.0);';
|
|
601
|
+
const outputNode = graph.nodes.find((n) => n.id === graph.outputNodeId);
|
|
602
|
+
if (!outputNode) {
|
|
603
|
+
return "fragColor = vec4(1.0, 0.0, 1.0, 1.0); // Error: no output node";
|
|
604
|
+
}
|
|
605
|
+
outputNode.id.replace(/-/g, "_");
|
|
606
|
+
const albedoConn = graph.connections.find(
|
|
607
|
+
(c) => c.targetNodeId === outputNode.id && c.targetPortId === "albedo"
|
|
608
|
+
);
|
|
609
|
+
const colorConn = graph.connections.find(
|
|
610
|
+
(c) => c.targetNodeId === outputNode.id && c.targetPortId === "color"
|
|
611
|
+
);
|
|
612
|
+
if (outputNode.type === "output_pbr") {
|
|
613
|
+
const albedo = albedoConn ? `${albedoConn.sourceNodeId.replace(/-/g, "_")}_${albedoConn.sourcePortId}` : "vec3(1.0)";
|
|
614
|
+
return `fragColor = vec4(${albedo}, 1.0);`;
|
|
615
|
+
}
|
|
616
|
+
if (outputNode.type === "output_unlit") {
|
|
617
|
+
const color = colorConn ? `${colorConn.sourceNodeId.replace(/-/g, "_")}_${colorConn.sourcePortId}` : "vec3(1.0)";
|
|
618
|
+
return `fragColor = vec4(${color}, 1.0);`;
|
|
619
|
+
}
|
|
620
|
+
return "fragColor = vec4(1.0);";
|
|
672
621
|
}
|
|
673
622
|
function generateErrorFragmentShader(errors) {
|
|
674
|
-
|
|
675
|
-
precision highp float;
|
|
676
|
-
|
|
677
|
-
out vec4 fragColor;
|
|
678
|
-
|
|
679
|
-
void main() {
|
|
680
|
-
// Compilation errors: ${errors.length}
|
|
681
|
-
// ${errors.map((e) => e.message).join(
|
|
682
|
-
fragColor = vec4(1.0, 0.0, 1.0, 1.0);
|
|
623
|
+
return `#version 300 es
|
|
624
|
+
precision highp float;
|
|
625
|
+
|
|
626
|
+
out vec4 fragColor;
|
|
627
|
+
|
|
628
|
+
void main() {
|
|
629
|
+
// Compilation errors: ${errors.length}
|
|
630
|
+
// ${errors.map((e) => e.message).join("\n // ")}
|
|
631
|
+
fragColor = vec4(1.0, 0.0, 1.0, 1.0);
|
|
683
632
|
}`;
|
|
684
633
|
}
|
|
685
634
|
function validateGraph(graph, definitions = materialNodeDefinitions.MATERIAL_NODE_MAP) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
errors.push({ nodeId: node.id, message: `Unknown node type: ${node.type}` });
|
|
701
|
-
continue;
|
|
702
|
-
}
|
|
703
|
-
// Check required inputs
|
|
704
|
-
for (const input of definition.inputs) {
|
|
705
|
-
const hasConnection = graph.connections.some((conn) => conn.targetNodeId === node.id && conn.targetPortId === input.id);
|
|
706
|
-
const hasDefault = input.defaultValue !== undefined || node.inputValues[input.id] !== undefined;
|
|
707
|
-
if (!hasConnection && !hasDefault) {
|
|
708
|
-
warnings.push({ nodeId: node.id, message: `Input '${input.name}' is not connected` });
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
// Check for cycles
|
|
713
|
-
const hasCycle = detectCycle(graph);
|
|
714
|
-
if (hasCycle) {
|
|
715
|
-
errors.push({ nodeId: '', message: 'Graph contains a cycle' });
|
|
716
|
-
}
|
|
717
|
-
// Check connections
|
|
718
|
-
for (const conn of graph.connections) {
|
|
719
|
-
const sourceNode = graph.nodes.find((n) => n.id === conn.sourceNodeId);
|
|
720
|
-
const targetNode = graph.nodes.find((n) => n.id === conn.targetNodeId);
|
|
721
|
-
if (!sourceNode) {
|
|
722
|
-
errors.push({ nodeId: conn.sourceNodeId, message: 'Source node not found' });
|
|
723
|
-
}
|
|
724
|
-
if (!targetNode) {
|
|
725
|
-
errors.push({ nodeId: conn.targetNodeId, message: 'Target node not found' });
|
|
726
|
-
}
|
|
635
|
+
const errors = [];
|
|
636
|
+
const warnings = [];
|
|
637
|
+
if (!graph.outputNodeId) {
|
|
638
|
+
errors.push({ nodeId: "", message: "No output node defined" });
|
|
639
|
+
}
|
|
640
|
+
const outputNode = graph.nodes.find((n) => n.id === graph.outputNodeId);
|
|
641
|
+
if (!outputNode) {
|
|
642
|
+
errors.push({ nodeId: graph.outputNodeId, message: "Output node not found in graph" });
|
|
643
|
+
}
|
|
644
|
+
for (const node of graph.nodes) {
|
|
645
|
+
const definition = definitions.get(node.type);
|
|
646
|
+
if (!definition) {
|
|
647
|
+
errors.push({ nodeId: node.id, message: `Unknown node type: ${node.type}` });
|
|
648
|
+
continue;
|
|
727
649
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
650
|
+
for (const input of definition.inputs) {
|
|
651
|
+
const hasConnection = graph.connections.some(
|
|
652
|
+
(conn) => conn.targetNodeId === node.id && conn.targetPortId === input.id
|
|
653
|
+
);
|
|
654
|
+
const hasDefault = input.defaultValue !== void 0 || node.inputValues[input.id] !== void 0;
|
|
655
|
+
if (!hasConnection && !hasDefault) {
|
|
656
|
+
warnings.push({ nodeId: node.id, message: `Input '${input.name}' is not connected` });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const hasCycle = detectCycle(graph);
|
|
661
|
+
if (hasCycle) {
|
|
662
|
+
errors.push({ nodeId: "", message: "Graph contains a cycle" });
|
|
663
|
+
}
|
|
664
|
+
for (const conn of graph.connections) {
|
|
665
|
+
const sourceNode = graph.nodes.find((n) => n.id === conn.sourceNodeId);
|
|
666
|
+
const targetNode = graph.nodes.find((n) => n.id === conn.targetNodeId);
|
|
667
|
+
if (!sourceNode) {
|
|
668
|
+
errors.push({ nodeId: conn.sourceNodeId, message: "Source node not found" });
|
|
669
|
+
}
|
|
670
|
+
if (!targetNode) {
|
|
671
|
+
errors.push({ nodeId: conn.targetNodeId, message: "Target node not found" });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
valid: errors.length === 0,
|
|
676
|
+
errors,
|
|
677
|
+
warnings
|
|
678
|
+
};
|
|
733
679
|
}
|
|
734
680
|
function detectCycle(graph) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
visiting.delete(nodeId);
|
|
752
|
-
visited.add(nodeId);
|
|
753
|
-
return false;
|
|
754
|
-
}
|
|
755
|
-
for (const node of graph.nodes) {
|
|
756
|
-
if (dfs(node.id)) {
|
|
757
|
-
return true;
|
|
758
|
-
}
|
|
681
|
+
const visited = /* @__PURE__ */ new Set();
|
|
682
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
683
|
+
function dfs(nodeId) {
|
|
684
|
+
if (visiting.has(nodeId)) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
if (visited.has(nodeId)) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
visiting.add(nodeId);
|
|
691
|
+
const outgoingConnections = graph.connections.filter((conn) => conn.sourceNodeId === nodeId);
|
|
692
|
+
for (const conn of outgoingConnections) {
|
|
693
|
+
if (dfs(conn.targetNodeId)) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
759
696
|
}
|
|
697
|
+
visiting.delete(nodeId);
|
|
698
|
+
visited.add(nodeId);
|
|
760
699
|
return false;
|
|
700
|
+
}
|
|
701
|
+
for (const node of graph.nodes) {
|
|
702
|
+
if (dfs(node.id)) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
761
707
|
}
|
|
762
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
763
|
-
View Utilities
|
|
764
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
765
708
|
function screenToGraph(screenPoint, viewTransform) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
709
|
+
return {
|
|
710
|
+
x: (screenPoint.x - viewTransform.x) / viewTransform.zoom,
|
|
711
|
+
y: (screenPoint.y - viewTransform.y) / viewTransform.zoom
|
|
712
|
+
};
|
|
770
713
|
}
|
|
771
714
|
function graphToScreen(graphPoint, viewTransform) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
715
|
+
return {
|
|
716
|
+
x: graphPoint.x * viewTransform.zoom + viewTransform.x,
|
|
717
|
+
y: graphPoint.y * viewTransform.zoom + viewTransform.y
|
|
718
|
+
};
|
|
776
719
|
}
|
|
777
720
|
function calculateBoundingBox(nodes) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
721
|
+
if (nodes.length === 0) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
let minX = Infinity, minY = Infinity;
|
|
725
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
726
|
+
for (const node of nodes) {
|
|
727
|
+
minX = Math.min(minX, node.position.x);
|
|
728
|
+
minY = Math.min(minY, node.position.y);
|
|
729
|
+
maxX = Math.max(maxX, node.position.x + 180);
|
|
730
|
+
maxY = Math.max(maxY, node.position.y + 120);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
x: minX,
|
|
734
|
+
y: minY,
|
|
735
|
+
width: maxX - minX,
|
|
736
|
+
height: maxY - minY
|
|
737
|
+
};
|
|
795
738
|
}
|
|
796
739
|
function calculateZoomToFit(nodes, viewportWidth, viewportHeight, padding = 50) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
740
|
+
const bbox = calculateBoundingBox(nodes);
|
|
741
|
+
if (!bbox) {
|
|
742
|
+
return { x: 0, y: 0, zoom: 1 };
|
|
743
|
+
}
|
|
744
|
+
const scaleX = (viewportWidth - padding * 2) / bbox.width;
|
|
745
|
+
const scaleY = (viewportHeight - padding * 2) / bbox.height;
|
|
746
|
+
const zoom = Math.min(scaleX, scaleY, 1);
|
|
747
|
+
const centerX = bbox.x + bbox.width / 2;
|
|
748
|
+
const centerY = bbox.y + bbox.height / 2;
|
|
749
|
+
return {
|
|
750
|
+
x: viewportWidth / 2 - centerX * zoom,
|
|
751
|
+
y: viewportHeight / 2 - centerY * zoom,
|
|
752
|
+
zoom
|
|
753
|
+
};
|
|
811
754
|
}
|
|
812
|
-
/* ═══════════════════════════════════════════════════════════════════════════
|
|
813
|
-
Import/Export
|
|
814
|
-
═══════════════════════════════════════════════════════════════════════════ */
|
|
815
755
|
function exportGraphToJSON(graph) {
|
|
816
|
-
|
|
756
|
+
return JSON.stringify(graph, null, 2);
|
|
817
757
|
}
|
|
818
758
|
function importGraphFromJSON(json) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
return parsed;
|
|
826
|
-
}
|
|
827
|
-
catch (_a) {
|
|
828
|
-
return null;
|
|
759
|
+
try {
|
|
760
|
+
const parsed = JSON.parse(json);
|
|
761
|
+
if (!parsed.id || !parsed.nodes || !parsed.connections) {
|
|
762
|
+
return null;
|
|
829
763
|
}
|
|
764
|
+
return parsed;
|
|
765
|
+
} catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
830
768
|
}
|
|
831
769
|
function exportToThreeJS(shader) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const material = new THREE.ShaderMaterial({
|
|
835
|
-
uniforms: {
|
|
836
|
-
uTime: { value: 0 },
|
|
837
|
-
uDeltaTime: { value: 0 },
|
|
838
|
-
uCameraPosition: { value: new THREE.Vector3() },
|
|
839
|
-
uResolution: { value: new THREE.Vector2() },
|
|
840
|
-
${shader.uniforms.map((u) => ` ${u.name}: { value: ${JSON.stringify(u.value)} },`).join(
|
|
841
|
-
},
|
|
842
|
-
vertexShader: \`${shader.vertexShader}\`,
|
|
843
|
-
fragmentShader: \`${shader.fragmentShader}\`,
|
|
844
|
-
});
|
|
845
|
-
|
|
770
|
+
return `import * as THREE from 'three';
|
|
771
|
+
|
|
772
|
+
const material = new THREE.ShaderMaterial({
|
|
773
|
+
uniforms: {
|
|
774
|
+
uTime: { value: 0 },
|
|
775
|
+
uDeltaTime: { value: 0 },
|
|
776
|
+
uCameraPosition: { value: new THREE.Vector3() },
|
|
777
|
+
uResolution: { value: new THREE.Vector2() },
|
|
778
|
+
${shader.uniforms.map((u) => ` ${u.name}: { value: ${JSON.stringify(u.value)} },`).join("\n")}
|
|
779
|
+
},
|
|
780
|
+
vertexShader: \`${shader.vertexShader}\`,
|
|
781
|
+
fragmentShader: \`${shader.fragmentShader}\`,
|
|
782
|
+
});
|
|
783
|
+
|
|
846
784
|
export default material;`;
|
|
847
785
|
}
|
|
848
|
-
|
|
849
786
|
exports.addConnectionToGraph = addConnectionToGraph;
|
|
850
787
|
exports.addNodeToGraph = addNodeToGraph;
|
|
851
788
|
exports.areTypesCompatible = areTypesCompatible;
|