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