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