@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,997 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* modelEditorUtils.ts — Utility functions for the ModelEditor.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Undo/Redo stack
|
|
8
|
+
* - LOD auto-generation
|
|
9
|
+
* - PBR material presets
|
|
10
|
+
* - Batch import
|
|
11
|
+
* - Instancing helpers
|
|
12
|
+
*/
|
|
13
|
+
class UndoRedoStack {
|
|
14
|
+
constructor(maxSize = 50) {
|
|
15
|
+
this.undoStack = [];
|
|
16
|
+
this.redoStack = [];
|
|
17
|
+
this.maxSize = maxSize;
|
|
18
|
+
}
|
|
19
|
+
push(action) {
|
|
20
|
+
this.undoStack.push(action);
|
|
21
|
+
if (this.undoStack.length > this.maxSize) {
|
|
22
|
+
this.undoStack.shift();
|
|
23
|
+
}
|
|
24
|
+
this.redoStack.length = 0;
|
|
25
|
+
}
|
|
26
|
+
undo() {
|
|
27
|
+
const action = this.undoStack.pop();
|
|
28
|
+
if (!action)
|
|
29
|
+
return null;
|
|
30
|
+
action.undo();
|
|
31
|
+
this.redoStack.push(action);
|
|
32
|
+
return action.label;
|
|
33
|
+
}
|
|
34
|
+
redo() {
|
|
35
|
+
const action = this.redoStack.pop();
|
|
36
|
+
if (!action)
|
|
37
|
+
return null;
|
|
38
|
+
action.redo();
|
|
39
|
+
this.undoStack.push(action);
|
|
40
|
+
return action.label;
|
|
41
|
+
}
|
|
42
|
+
canUndo() {
|
|
43
|
+
return this.undoStack.length > 0;
|
|
44
|
+
}
|
|
45
|
+
canRedo() {
|
|
46
|
+
return this.redoStack.length > 0;
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
this.undoStack.length = 0;
|
|
50
|
+
this.redoStack.length = 0;
|
|
51
|
+
}
|
|
52
|
+
get undoLabel() {
|
|
53
|
+
return this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1].label : null;
|
|
54
|
+
}
|
|
55
|
+
get redoLabel() {
|
|
56
|
+
return this.redoStack.length > 0 ? this.redoStack[this.redoStack.length - 1].label : null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const DEFAULT_LOD_LEVELS = [
|
|
60
|
+
{ distance: 0, ratio: 1.0 },
|
|
61
|
+
{ distance: 10, ratio: 0.5 },
|
|
62
|
+
{ distance: 25, ratio: 0.25 },
|
|
63
|
+
{ distance: 50, ratio: 0.1 },
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Simple vertex decimation by merging nearby vertices.
|
|
67
|
+
* Returns a new geometry with reduced vertex count.
|
|
68
|
+
*/
|
|
69
|
+
function decimateGeometry(geometry, ratio) {
|
|
70
|
+
if (ratio >= 1.0)
|
|
71
|
+
return geometry.clone();
|
|
72
|
+
const posAttr = geometry.getAttribute("position");
|
|
73
|
+
if (!posAttr)
|
|
74
|
+
return geometry.clone();
|
|
75
|
+
const positions = Array.from(posAttr.array);
|
|
76
|
+
const vertexCount = posAttr.count;
|
|
77
|
+
const targetCount = Math.max(3, Math.floor(vertexCount * ratio));
|
|
78
|
+
// Grid-based vertex merging
|
|
79
|
+
const gridSize = Math.cbrt(vertexCount / targetCount) * 0.5;
|
|
80
|
+
const merged = new Map();
|
|
81
|
+
const vertexMap = new Int32Array(vertexCount);
|
|
82
|
+
let newIndex = 0;
|
|
83
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
84
|
+
const x = positions[i * 3];
|
|
85
|
+
const y = positions[i * 3 + 1];
|
|
86
|
+
const z = positions[i * 3 + 2];
|
|
87
|
+
const key = `${Math.round(x / gridSize)},${Math.round(y / gridSize)},${Math.round(z / gridSize)}`;
|
|
88
|
+
const existing = merged.get(key);
|
|
89
|
+
if (existing) {
|
|
90
|
+
existing.x += x;
|
|
91
|
+
existing.y += y;
|
|
92
|
+
existing.z += z;
|
|
93
|
+
existing.count++;
|
|
94
|
+
vertexMap[i] = existing.index;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
merged.set(key, { x, y, z, count: 1, index: newIndex });
|
|
98
|
+
vertexMap[i] = newIndex;
|
|
99
|
+
newIndex++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Build new positions
|
|
103
|
+
const newPositions = new Float32Array(merged.size * 3);
|
|
104
|
+
for (const cell of merged.values()) {
|
|
105
|
+
newPositions[cell.index * 3] = cell.x / cell.count;
|
|
106
|
+
newPositions[cell.index * 3 + 1] = cell.y / cell.count;
|
|
107
|
+
newPositions[cell.index * 3 + 2] = cell.z / cell.count;
|
|
108
|
+
}
|
|
109
|
+
// Build new indices
|
|
110
|
+
const oldIndex = geometry.index;
|
|
111
|
+
const newIndices = [];
|
|
112
|
+
if (oldIndex) {
|
|
113
|
+
for (let i = 0; i < oldIndex.count; i += 3) {
|
|
114
|
+
const a = vertexMap[oldIndex.array[i]];
|
|
115
|
+
const b = vertexMap[oldIndex.array[i + 1]];
|
|
116
|
+
const c = vertexMap[oldIndex.array[i + 2]];
|
|
117
|
+
if (a !== b && b !== c && a !== c) {
|
|
118
|
+
newIndices.push(a, b, c);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
for (let i = 0; i < vertexCount; i += 3) {
|
|
124
|
+
const a = vertexMap[i];
|
|
125
|
+
const b = vertexMap[i + 1];
|
|
126
|
+
const c = vertexMap[i + 2];
|
|
127
|
+
if (a !== b && b !== c && a !== c) {
|
|
128
|
+
newIndices.push(a, b, c);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const result = new THREE.BufferGeometry();
|
|
133
|
+
result.setAttribute("position", new THREE.BufferAttribute(newPositions, 3));
|
|
134
|
+
result.setIndex(newIndices);
|
|
135
|
+
result.computeVertexNormals();
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates a THREE.LOD object from a mesh with automatic level generation.
|
|
140
|
+
*/
|
|
141
|
+
function generateLOD(mesh, levels = DEFAULT_LOD_LEVELS) {
|
|
142
|
+
const lod = new THREE.LOD();
|
|
143
|
+
lod.name = mesh.name + "_LOD";
|
|
144
|
+
lod.position.copy(mesh.position);
|
|
145
|
+
lod.rotation.copy(mesh.rotation);
|
|
146
|
+
lod.scale.copy(mesh.scale);
|
|
147
|
+
for (const level of levels) {
|
|
148
|
+
const decimated = decimateGeometry(mesh.geometry, level.ratio);
|
|
149
|
+
const mat = Array.isArray(mesh.material)
|
|
150
|
+
? mesh.material.map((m) => m.clone())
|
|
151
|
+
: mesh.material.clone();
|
|
152
|
+
const lodMesh = new THREE.Mesh(decimated, mat);
|
|
153
|
+
lodMesh.castShadow = mesh.castShadow;
|
|
154
|
+
lodMesh.receiveShadow = mesh.receiveShadow;
|
|
155
|
+
lod.addLevel(lodMesh, level.distance);
|
|
156
|
+
}
|
|
157
|
+
return lod;
|
|
158
|
+
}
|
|
159
|
+
const PBR_PRESETS = [
|
|
160
|
+
// Metals
|
|
161
|
+
{ name: "Gold", category: "Metal", params: { color: 0xffd700, metalness: 1.0, roughness: 0.15 } },
|
|
162
|
+
{ name: "Silver", category: "Metal", params: { color: 0xc0c0c0, metalness: 1.0, roughness: 0.1 } },
|
|
163
|
+
{ name: "Copper", category: "Metal", params: { color: 0xb87333, metalness: 1.0, roughness: 0.25 } },
|
|
164
|
+
{ name: "Iron", category: "Metal", params: { color: 0x808080, metalness: 0.95, roughness: 0.6 } },
|
|
165
|
+
{ name: "Brushed Steel", category: "Metal", params: { color: 0xa0a0a0, metalness: 0.95, roughness: 0.4 } },
|
|
166
|
+
{ name: "Chrome", category: "Metal", params: { color: 0xdddddd, metalness: 1.0, roughness: 0.02 } },
|
|
167
|
+
{ name: "Bronze", category: "Metal", params: { color: 0xcd7f32, metalness: 0.9, roughness: 0.35 } },
|
|
168
|
+
{ name: "Aluminum", category: "Metal", params: { color: 0xd6d6d6, metalness: 0.85, roughness: 0.2 } },
|
|
169
|
+
// Non-metals
|
|
170
|
+
{ name: "Plastic (White)", category: "Plastic", params: { color: 0xf0f0f0, metalness: 0.0, roughness: 0.3 } },
|
|
171
|
+
{ name: "Plastic (Red)", category: "Plastic", params: { color: 0xcc2222, metalness: 0.0, roughness: 0.35 } },
|
|
172
|
+
{ name: "Rubber", category: "Plastic", params: { color: 0x333333, metalness: 0.0, roughness: 0.9 } },
|
|
173
|
+
// Glass
|
|
174
|
+
{ name: "Clear Glass", category: "Glass", params: { color: 0xffffff, metalness: 0.0, roughness: 0.0, opacity: 0.15, transparent: true, ior: 1.5, transmission: 0.95 } },
|
|
175
|
+
{ name: "Frosted Glass", category: "Glass", params: { color: 0xeeeeff, metalness: 0.0, roughness: 0.5, opacity: 0.3, transparent: true, ior: 1.5, transmission: 0.7 } },
|
|
176
|
+
{ name: "Tinted Glass", category: "Glass", params: { color: 0x224488, metalness: 0.0, roughness: 0.05, opacity: 0.3, transparent: true, ior: 1.5, transmission: 0.8 } },
|
|
177
|
+
// Natural
|
|
178
|
+
{ name: "Wood (Light)", category: "Natural", params: { color: 0xc19a6b, metalness: 0.0, roughness: 0.7 } },
|
|
179
|
+
{ name: "Wood (Dark)", category: "Natural", params: { color: 0x5c3317, metalness: 0.0, roughness: 0.65 } },
|
|
180
|
+
{ name: "Stone", category: "Natural", params: { color: 0x888888, metalness: 0.0, roughness: 0.85 } },
|
|
181
|
+
{ name: "Marble", category: "Natural", params: { color: 0xf0ece0, metalness: 0.0, roughness: 0.15 } },
|
|
182
|
+
{ name: "Clay", category: "Natural", params: { color: 0xb5651d, metalness: 0.0, roughness: 0.8 } },
|
|
183
|
+
{ name: "Concrete", category: "Natural", params: { color: 0x999999, metalness: 0.0, roughness: 0.95 } },
|
|
184
|
+
// Fabric
|
|
185
|
+
{ name: "Cotton (White)", category: "Fabric", params: { color: 0xf5f5dc, metalness: 0.0, roughness: 0.9, sheen: 0.5, sheenRoughness: 0.8, sheenColor: 0xffffff } },
|
|
186
|
+
{ name: "Silk", category: "Fabric", params: { color: 0xe3dac9, metalness: 0.0, roughness: 0.3, sheen: 1.0, sheenRoughness: 0.3, sheenColor: 0xffeedd } },
|
|
187
|
+
{ name: "Velvet", category: "Fabric", params: { color: 0x800020, metalness: 0.0, roughness: 0.95, sheen: 0.9, sheenRoughness: 0.9, sheenColor: 0xff4060 } },
|
|
188
|
+
{ name: "Leather", category: "Fabric", params: { color: 0x553322, metalness: 0.0, roughness: 0.6 } },
|
|
189
|
+
{ name: "Denim", category: "Fabric", params: { color: 0x3b5998, metalness: 0.0, roughness: 0.85 } },
|
|
190
|
+
// Skin
|
|
191
|
+
{ name: "Skin (Light)", category: "Skin", params: { color: 0xffdbac, metalness: 0.0, roughness: 0.55 } },
|
|
192
|
+
{ name: "Skin (Medium)", category: "Skin", params: { color: 0xc68642, metalness: 0.0, roughness: 0.5 } },
|
|
193
|
+
{ name: "Skin (Dark)", category: "Skin", params: { color: 0x8d5524, metalness: 0.0, roughness: 0.5 } },
|
|
194
|
+
// Special
|
|
195
|
+
{ name: "Car Paint", category: "Special", params: { color: 0xcc0000, metalness: 0.5, roughness: 0.15, clearcoat: 1.0, clearcoatRoughness: 0.05 } },
|
|
196
|
+
{ name: "Emissive (Blue)", category: "Special", params: { color: 0x111111, metalness: 0.0, roughness: 0.5, emissive: 0x0088ff, emissiveIntensity: 2.0 } },
|
|
197
|
+
{ name: "Emissive (Orange)", category: "Special", params: { color: 0x111111, metalness: 0.0, roughness: 0.5, emissive: 0xff6600, emissiveIntensity: 2.0 } },
|
|
198
|
+
{ name: "Neon", category: "Special", params: { color: 0x000000, metalness: 0.0, roughness: 0.1, emissive: 0x00ff88, emissiveIntensity: 5.0 } },
|
|
199
|
+
];
|
|
200
|
+
/**
|
|
201
|
+
* Apply a PBR preset to a MeshStandardMaterial or MeshPhysicalMaterial.
|
|
202
|
+
*/
|
|
203
|
+
function applyPBRPreset(material, preset) {
|
|
204
|
+
var _a, _b;
|
|
205
|
+
const p = preset.params;
|
|
206
|
+
// Upgrade to MeshPhysicalMaterial if needed for advanced features
|
|
207
|
+
const needsPhysical = p.clearcoat != null || p.transmission != null || p.sheen != null || p.ior != null;
|
|
208
|
+
if (material instanceof THREE.MeshPhysicalMaterial) {
|
|
209
|
+
material.color.setHex(p.color);
|
|
210
|
+
material.metalness = p.metalness;
|
|
211
|
+
material.roughness = p.roughness;
|
|
212
|
+
if (p.emissive != null)
|
|
213
|
+
material.emissive.setHex(p.emissive);
|
|
214
|
+
if (p.emissiveIntensity != null)
|
|
215
|
+
material.emissiveIntensity = p.emissiveIntensity;
|
|
216
|
+
if (p.opacity != null)
|
|
217
|
+
material.opacity = p.opacity;
|
|
218
|
+
material.transparent = (_a = p.transparent) !== null && _a !== void 0 ? _a : false;
|
|
219
|
+
if (p.clearcoat != null)
|
|
220
|
+
material.clearcoat = p.clearcoat;
|
|
221
|
+
if (p.clearcoatRoughness != null)
|
|
222
|
+
material.clearcoatRoughness = p.clearcoatRoughness;
|
|
223
|
+
if (p.ior != null)
|
|
224
|
+
material.ior = p.ior;
|
|
225
|
+
if (p.transmission != null)
|
|
226
|
+
material.transmission = p.transmission;
|
|
227
|
+
if (p.sheen != null)
|
|
228
|
+
material.sheen = p.sheen;
|
|
229
|
+
if (p.sheenRoughness != null)
|
|
230
|
+
material.sheenRoughness = p.sheenRoughness;
|
|
231
|
+
if (p.sheenColor != null)
|
|
232
|
+
material.sheenColor.setHex(p.sheenColor);
|
|
233
|
+
material.needsUpdate = true;
|
|
234
|
+
}
|
|
235
|
+
else if (material instanceof THREE.MeshStandardMaterial && !needsPhysical) {
|
|
236
|
+
material.color.setHex(p.color);
|
|
237
|
+
material.metalness = p.metalness;
|
|
238
|
+
material.roughness = p.roughness;
|
|
239
|
+
if (p.emissive != null)
|
|
240
|
+
material.emissive.setHex(p.emissive);
|
|
241
|
+
if (p.emissiveIntensity != null)
|
|
242
|
+
material.emissiveIntensity = p.emissiveIntensity;
|
|
243
|
+
if (p.opacity != null)
|
|
244
|
+
material.opacity = p.opacity;
|
|
245
|
+
material.transparent = (_b = p.transparent) !== null && _b !== void 0 ? _b : false;
|
|
246
|
+
material.needsUpdate = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Create a new MeshPhysicalMaterial from a preset. */
|
|
250
|
+
function createMaterialFromPreset(preset) {
|
|
251
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
252
|
+
const p = preset.params;
|
|
253
|
+
return new THREE.MeshPhysicalMaterial({
|
|
254
|
+
color: p.color,
|
|
255
|
+
metalness: p.metalness,
|
|
256
|
+
roughness: p.roughness,
|
|
257
|
+
emissive: (_a = p.emissive) !== null && _a !== void 0 ? _a : 0x000000,
|
|
258
|
+
emissiveIntensity: (_b = p.emissiveIntensity) !== null && _b !== void 0 ? _b : 0,
|
|
259
|
+
opacity: (_c = p.opacity) !== null && _c !== void 0 ? _c : 1,
|
|
260
|
+
transparent: (_d = p.transparent) !== null && _d !== void 0 ? _d : false,
|
|
261
|
+
clearcoat: (_e = p.clearcoat) !== null && _e !== void 0 ? _e : 0,
|
|
262
|
+
clearcoatRoughness: (_f = p.clearcoatRoughness) !== null && _f !== void 0 ? _f : 0,
|
|
263
|
+
ior: (_g = p.ior) !== null && _g !== void 0 ? _g : 1.5,
|
|
264
|
+
transmission: (_h = p.transmission) !== null && _h !== void 0 ? _h : 0,
|
|
265
|
+
sheen: (_j = p.sheen) !== null && _j !== void 0 ? _j : 0,
|
|
266
|
+
sheenRoughness: (_k = p.sheenRoughness) !== null && _k !== void 0 ? _k : 0,
|
|
267
|
+
sheenColor: new THREE.Color((_l = p.sheenColor) !== null && _l !== void 0 ? _l : 0x000000),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/* ═══════════════════════════════════════════
|
|
271
|
+
4. INSTANCING
|
|
272
|
+
═══════════════════════════════════════════ */
|
|
273
|
+
/**
|
|
274
|
+
* Convert duplicate meshes to InstancedMesh for better performance.
|
|
275
|
+
* Groups meshes by geometry UUID + material UUID.
|
|
276
|
+
*/
|
|
277
|
+
function convertToInstanced(parent) {
|
|
278
|
+
var _a;
|
|
279
|
+
const groups = new Map();
|
|
280
|
+
parent.traverse((child) => {
|
|
281
|
+
if (child instanceof THREE.Mesh && !child.name.startsWith("__")) {
|
|
282
|
+
const geoId = child.geometry.uuid;
|
|
283
|
+
const matId = Array.isArray(child.material)
|
|
284
|
+
? child.material.map((m) => m.uuid).join(",")
|
|
285
|
+
: child.material.uuid;
|
|
286
|
+
const key = `${geoId}|${matId}`;
|
|
287
|
+
let list = groups.get(key);
|
|
288
|
+
if (!list) {
|
|
289
|
+
list = [];
|
|
290
|
+
groups.set(key, list);
|
|
291
|
+
}
|
|
292
|
+
list.push(child);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
let created = 0;
|
|
296
|
+
let removed = 0;
|
|
297
|
+
for (const [, meshes] of groups) {
|
|
298
|
+
if (meshes.length < 2)
|
|
299
|
+
continue;
|
|
300
|
+
const source = meshes[0];
|
|
301
|
+
const instancedMesh = new THREE.InstancedMesh(source.geometry, source.material, meshes.length);
|
|
302
|
+
instancedMesh.name = source.name + "_Instanced";
|
|
303
|
+
instancedMesh.castShadow = source.castShadow;
|
|
304
|
+
instancedMesh.receiveShadow = source.receiveShadow;
|
|
305
|
+
const matrix = new THREE.Matrix4();
|
|
306
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
307
|
+
meshes[i].updateWorldMatrix(true, false);
|
|
308
|
+
matrix.copy(meshes[i].matrixWorld);
|
|
309
|
+
instancedMesh.setMatrixAt(i, matrix);
|
|
310
|
+
}
|
|
311
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
312
|
+
parent.add(instancedMesh);
|
|
313
|
+
created++;
|
|
314
|
+
for (const mesh of meshes) {
|
|
315
|
+
(_a = mesh.parent) === null || _a === void 0 ? void 0 : _a.remove(mesh);
|
|
316
|
+
removed++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { created, removed };
|
|
320
|
+
}
|
|
321
|
+
/* ═══════════════════════════════════════════
|
|
322
|
+
5. GEOMETRY HELPERS
|
|
323
|
+
═══════════════════════════════════════════ */
|
|
324
|
+
/** Count total triangles in a scene. */
|
|
325
|
+
function countTriangles(object) {
|
|
326
|
+
let count = 0;
|
|
327
|
+
object.traverse((child) => {
|
|
328
|
+
if (child instanceof THREE.Mesh) {
|
|
329
|
+
const geo = child.geometry;
|
|
330
|
+
if (geo.index) {
|
|
331
|
+
count += geo.index.count / 3;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const pos = geo.getAttribute("position");
|
|
335
|
+
if (pos)
|
|
336
|
+
count += pos.count / 3;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return Math.round(count);
|
|
341
|
+
}
|
|
342
|
+
/** Compute bounding box for an object and all its children. */
|
|
343
|
+
function computeSceneBounds(object) {
|
|
344
|
+
const box = new THREE.Box3();
|
|
345
|
+
object.traverse((child) => {
|
|
346
|
+
if (child instanceof THREE.Mesh) {
|
|
347
|
+
child.geometry.computeBoundingBox();
|
|
348
|
+
if (child.geometry.boundingBox) {
|
|
349
|
+
const worldBox = child.geometry.boundingBox.clone();
|
|
350
|
+
worldBox.applyMatrix4(child.matrixWorld);
|
|
351
|
+
box.union(worldBox);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return box;
|
|
356
|
+
}
|
|
357
|
+
/** Measure distance between two world-space points. */
|
|
358
|
+
function measureDistance(a, b) {
|
|
359
|
+
return a.distanceTo(b);
|
|
360
|
+
}
|
|
361
|
+
/** Measure angle (in degrees) between three world-space points (vertex at B). */
|
|
362
|
+
function measureAngle(a, b, c) {
|
|
363
|
+
const ba = new THREE.Vector3().subVectors(a, b).normalize();
|
|
364
|
+
const bc = new THREE.Vector3().subVectors(c, b).normalize();
|
|
365
|
+
return THREE.MathUtils.radToDeg(Math.acos(THREE.MathUtils.clamp(ba.dot(bc), -1, 1)));
|
|
366
|
+
}
|
|
367
|
+
const HDRI_PRESETS = [
|
|
368
|
+
// Studio
|
|
369
|
+
{
|
|
370
|
+
name: "Studio Soft",
|
|
371
|
+
category: "Studio",
|
|
372
|
+
bgColor: 0x1a1a2e,
|
|
373
|
+
envIntensity: 1.0,
|
|
374
|
+
ambientColor: 0xffffff,
|
|
375
|
+
ambientIntensity: 0.6,
|
|
376
|
+
dirColor: 0xffffff,
|
|
377
|
+
dirIntensity: 1.2,
|
|
378
|
+
dirPosition: [5, 10, 7],
|
|
379
|
+
exposure: 1.0,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "Studio High Key",
|
|
383
|
+
category: "Studio",
|
|
384
|
+
bgColor: 0xf0f0f0,
|
|
385
|
+
envIntensity: 1.2,
|
|
386
|
+
ambientColor: 0xffffff,
|
|
387
|
+
ambientIntensity: 1.0,
|
|
388
|
+
dirColor: 0xffffff,
|
|
389
|
+
dirIntensity: 1.5,
|
|
390
|
+
dirPosition: [2, 8, 3],
|
|
391
|
+
exposure: 1.2,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "Studio Low Key",
|
|
395
|
+
category: "Studio",
|
|
396
|
+
bgColor: 0x0a0a0a,
|
|
397
|
+
envIntensity: 0.5,
|
|
398
|
+
ambientColor: 0x222244,
|
|
399
|
+
ambientIntensity: 0.2,
|
|
400
|
+
dirColor: 0xffeedd,
|
|
401
|
+
dirIntensity: 2.0,
|
|
402
|
+
dirPosition: [3, 5, 2],
|
|
403
|
+
exposure: 0.8,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "Studio Rim",
|
|
407
|
+
category: "Studio",
|
|
408
|
+
bgColor: 0x111111,
|
|
409
|
+
envIntensity: 0.3,
|
|
410
|
+
ambientColor: 0x111122,
|
|
411
|
+
ambientIntensity: 0.15,
|
|
412
|
+
dirColor: 0xffffff,
|
|
413
|
+
dirIntensity: 2.5,
|
|
414
|
+
dirPosition: [-5, 3, -2],
|
|
415
|
+
exposure: 0.9,
|
|
416
|
+
},
|
|
417
|
+
// Outdoor
|
|
418
|
+
{
|
|
419
|
+
name: "Sunny Day",
|
|
420
|
+
category: "Outdoor",
|
|
421
|
+
bgColor: 0x87ceeb,
|
|
422
|
+
envIntensity: 1.0,
|
|
423
|
+
ambientColor: 0x8ec8f0,
|
|
424
|
+
ambientIntensity: 0.4,
|
|
425
|
+
dirColor: 0xfff4e0,
|
|
426
|
+
dirIntensity: 2.0,
|
|
427
|
+
dirPosition: [5, 10, 5],
|
|
428
|
+
exposure: 1.0,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "Overcast",
|
|
432
|
+
category: "Outdoor",
|
|
433
|
+
bgColor: 0xb0b0b8,
|
|
434
|
+
envIntensity: 0.8,
|
|
435
|
+
ambientColor: 0xccccdd,
|
|
436
|
+
ambientIntensity: 0.7,
|
|
437
|
+
dirColor: 0xddddee,
|
|
438
|
+
dirIntensity: 0.5,
|
|
439
|
+
dirPosition: [0, 10, 0],
|
|
440
|
+
fog: { color: 0xccccdd, near: 20, far: 200 },
|
|
441
|
+
exposure: 1.0,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: "Golden Hour",
|
|
445
|
+
category: "Outdoor",
|
|
446
|
+
bgColor: 0xff8844,
|
|
447
|
+
envIntensity: 1.0,
|
|
448
|
+
ambientColor: 0xffaa55,
|
|
449
|
+
ambientIntensity: 0.3,
|
|
450
|
+
dirColor: 0xff9933,
|
|
451
|
+
dirIntensity: 2.5,
|
|
452
|
+
dirPosition: [10, 2, 5],
|
|
453
|
+
exposure: 1.1,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "Blue Hour",
|
|
457
|
+
category: "Outdoor",
|
|
458
|
+
bgColor: 0x1a2a4a,
|
|
459
|
+
envIntensity: 0.6,
|
|
460
|
+
ambientColor: 0x334466,
|
|
461
|
+
ambientIntensity: 0.3,
|
|
462
|
+
dirColor: 0x6688cc,
|
|
463
|
+
dirIntensity: 0.8,
|
|
464
|
+
dirPosition: [-5, 3, 5],
|
|
465
|
+
exposure: 0.8,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "Night",
|
|
469
|
+
category: "Outdoor",
|
|
470
|
+
bgColor: 0x050510,
|
|
471
|
+
envIntensity: 0.1,
|
|
472
|
+
ambientColor: 0x111133,
|
|
473
|
+
ambientIntensity: 0.05,
|
|
474
|
+
dirColor: 0xaabbdd,
|
|
475
|
+
dirIntensity: 0.3,
|
|
476
|
+
dirPosition: [-3, 8, 2],
|
|
477
|
+
exposure: 0.5,
|
|
478
|
+
},
|
|
479
|
+
// Interior
|
|
480
|
+
{
|
|
481
|
+
name: "Warm Interior",
|
|
482
|
+
category: "Interior",
|
|
483
|
+
bgColor: 0x2a1a10,
|
|
484
|
+
envIntensity: 0.7,
|
|
485
|
+
ambientColor: 0xffddaa,
|
|
486
|
+
ambientIntensity: 0.4,
|
|
487
|
+
dirColor: 0xffeebb,
|
|
488
|
+
dirIntensity: 1.0,
|
|
489
|
+
dirPosition: [2, 5, 3],
|
|
490
|
+
exposure: 1.0,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "Cool Office",
|
|
494
|
+
category: "Interior",
|
|
495
|
+
bgColor: 0x1a2230,
|
|
496
|
+
envIntensity: 0.8,
|
|
497
|
+
ambientColor: 0xddeeff,
|
|
498
|
+
ambientIntensity: 0.6,
|
|
499
|
+
dirColor: 0xffffff,
|
|
500
|
+
dirIntensity: 0.8,
|
|
501
|
+
dirPosition: [0, 8, 0],
|
|
502
|
+
exposure: 1.0,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "Gallery",
|
|
506
|
+
category: "Interior",
|
|
507
|
+
bgColor: 0xfafafa,
|
|
508
|
+
envIntensity: 1.0,
|
|
509
|
+
ambientColor: 0xffffff,
|
|
510
|
+
ambientIntensity: 0.8,
|
|
511
|
+
dirColor: 0xffffff,
|
|
512
|
+
dirIntensity: 0.6,
|
|
513
|
+
dirPosition: [0, 10, 2],
|
|
514
|
+
exposure: 1.1,
|
|
515
|
+
},
|
|
516
|
+
// Dramatic
|
|
517
|
+
{
|
|
518
|
+
name: "Neon City",
|
|
519
|
+
category: "Dramatic",
|
|
520
|
+
bgColor: 0x0a0015,
|
|
521
|
+
envIntensity: 0.4,
|
|
522
|
+
ambientColor: 0xff00ff,
|
|
523
|
+
ambientIntensity: 0.2,
|
|
524
|
+
dirColor: 0x00ffff,
|
|
525
|
+
dirIntensity: 1.5,
|
|
526
|
+
dirPosition: [-3, 5, 2],
|
|
527
|
+
exposure: 1.0,
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "Cinematic",
|
|
531
|
+
category: "Dramatic",
|
|
532
|
+
bgColor: 0x0f0f1a,
|
|
533
|
+
envIntensity: 0.5,
|
|
534
|
+
ambientColor: 0x223344,
|
|
535
|
+
ambientIntensity: 0.15,
|
|
536
|
+
dirColor: 0xffeedd,
|
|
537
|
+
dirIntensity: 2.0,
|
|
538
|
+
dirPosition: [8, 4, 2],
|
|
539
|
+
exposure: 0.9,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: "Sunrise",
|
|
543
|
+
category: "Dramatic",
|
|
544
|
+
bgColor: 0xff6633,
|
|
545
|
+
envIntensity: 0.8,
|
|
546
|
+
ambientColor: 0xffaa66,
|
|
547
|
+
ambientIntensity: 0.3,
|
|
548
|
+
dirColor: 0xff7744,
|
|
549
|
+
dirIntensity: 2.0,
|
|
550
|
+
dirPosition: [15, 1, 0],
|
|
551
|
+
exposure: 1.2,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: "Desert",
|
|
555
|
+
category: "Dramatic",
|
|
556
|
+
bgColor: 0xe8c888,
|
|
557
|
+
envIntensity: 1.0,
|
|
558
|
+
ambientColor: 0xddcc99,
|
|
559
|
+
ambientIntensity: 0.5,
|
|
560
|
+
dirColor: 0xfff0cc,
|
|
561
|
+
dirIntensity: 2.5,
|
|
562
|
+
dirPosition: [5, 12, 3],
|
|
563
|
+
exposure: 1.3,
|
|
564
|
+
},
|
|
565
|
+
];
|
|
566
|
+
/**
|
|
567
|
+
* Apply an HDRI preset to a Three.js scene and renderer.
|
|
568
|
+
*/
|
|
569
|
+
function applyHDRIPreset(scene, renderer, preset) {
|
|
570
|
+
// Background
|
|
571
|
+
scene.background = new THREE.Color(preset.bgColor);
|
|
572
|
+
// Tone mapping exposure
|
|
573
|
+
renderer.toneMappingExposure = preset.exposure;
|
|
574
|
+
// Update ambient light (find existing or create)
|
|
575
|
+
let ambient;
|
|
576
|
+
let directional;
|
|
577
|
+
scene.traverse((child) => {
|
|
578
|
+
if (child instanceof THREE.AmbientLight && !ambient)
|
|
579
|
+
ambient = child;
|
|
580
|
+
if (child instanceof THREE.DirectionalLight && !directional)
|
|
581
|
+
directional = child;
|
|
582
|
+
});
|
|
583
|
+
if (ambient != null) {
|
|
584
|
+
ambient.color.setHex(preset.ambientColor);
|
|
585
|
+
ambient.intensity = preset.ambientIntensity;
|
|
586
|
+
}
|
|
587
|
+
if (directional != null) {
|
|
588
|
+
directional.color.setHex(preset.dirColor);
|
|
589
|
+
directional.intensity = preset.dirIntensity;
|
|
590
|
+
directional.position.set(...preset.dirPosition);
|
|
591
|
+
}
|
|
592
|
+
// Fog
|
|
593
|
+
if (preset.fog) {
|
|
594
|
+
scene.fog = new THREE.Fog(preset.fog.color, preset.fog.near, preset.fog.far);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
scene.fog = null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/** Snap a point to the nearest grid intersection. */
|
|
601
|
+
function snapToGrid(point, gridSize) {
|
|
602
|
+
return new THREE.Vector3(Math.round(point.x / gridSize) * gridSize, Math.round(point.y / gridSize) * gridSize, Math.round(point.z / gridSize) * gridSize);
|
|
603
|
+
}
|
|
604
|
+
/** Find the closest vertex in a mesh to a given world-space point. */
|
|
605
|
+
function snapToVertex(point, scene, maxDistance = Infinity) {
|
|
606
|
+
let closest = null;
|
|
607
|
+
let minDist = maxDistance;
|
|
608
|
+
const tmpV = new THREE.Vector3();
|
|
609
|
+
scene.traverse((child) => {
|
|
610
|
+
if (!(child instanceof THREE.Mesh))
|
|
611
|
+
return;
|
|
612
|
+
const posAttr = child.geometry.getAttribute("position");
|
|
613
|
+
if (!posAttr)
|
|
614
|
+
return;
|
|
615
|
+
child.updateWorldMatrix(true, false);
|
|
616
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
617
|
+
tmpV.fromBufferAttribute(posAttr, i);
|
|
618
|
+
tmpV.applyMatrix4(child.matrixWorld);
|
|
619
|
+
const d = point.distanceTo(tmpV);
|
|
620
|
+
if (d < minDist) {
|
|
621
|
+
minDist = d;
|
|
622
|
+
closest = { point: tmpV.clone(), target: "vertex", object: child };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
return closest;
|
|
627
|
+
}
|
|
628
|
+
/** Find the closest point on any edge in the scene to a given world-space point. */
|
|
629
|
+
function snapToEdge(point, scene, maxDistance = Infinity) {
|
|
630
|
+
let closest = null;
|
|
631
|
+
let minDist = maxDistance;
|
|
632
|
+
const a = new THREE.Vector3();
|
|
633
|
+
const b = new THREE.Vector3();
|
|
634
|
+
scene.traverse((child) => {
|
|
635
|
+
if (!(child instanceof THREE.Mesh))
|
|
636
|
+
return;
|
|
637
|
+
const posAttr = child.geometry.getAttribute("position");
|
|
638
|
+
if (!posAttr)
|
|
639
|
+
return;
|
|
640
|
+
const index = child.geometry.index;
|
|
641
|
+
child.updateWorldMatrix(true, false);
|
|
642
|
+
const processEdge = (i0, i1) => {
|
|
643
|
+
a.fromBufferAttribute(posAttr, i0).applyMatrix4(child.matrixWorld);
|
|
644
|
+
b.fromBufferAttribute(posAttr, i1).applyMatrix4(child.matrixWorld);
|
|
645
|
+
const closestOnEdge = closestPointOnSegment(point, a, b);
|
|
646
|
+
const d = point.distanceTo(closestOnEdge);
|
|
647
|
+
if (d < minDist) {
|
|
648
|
+
minDist = d;
|
|
649
|
+
closest = { point: closestOnEdge.clone(), target: "edge", object: child };
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
if (index) {
|
|
653
|
+
for (let i = 0; i < index.count; i += 3) {
|
|
654
|
+
const i0 = index.array[i], i1 = index.array[i + 1], i2 = index.array[i + 2];
|
|
655
|
+
processEdge(i0, i1);
|
|
656
|
+
processEdge(i1, i2);
|
|
657
|
+
processEdge(i2, i0);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
for (let i = 0; i < posAttr.count; i += 3) {
|
|
662
|
+
processEdge(i, i + 1);
|
|
663
|
+
processEdge(i + 1, i + 2);
|
|
664
|
+
processEdge(i + 2, i);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
return closest;
|
|
669
|
+
}
|
|
670
|
+
/** Find closest point on a line segment. */
|
|
671
|
+
function closestPointOnSegment(p, a, b) {
|
|
672
|
+
const ab = new THREE.Vector3().subVectors(b, a);
|
|
673
|
+
const ap = new THREE.Vector3().subVectors(p, a);
|
|
674
|
+
const t = THREE.MathUtils.clamp(ap.dot(ab) / ab.dot(ab), 0, 1);
|
|
675
|
+
return new THREE.Vector3().addVectors(a, ab.multiplyScalar(t));
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Multi-mode snap: tries vertex/edge/grid in priority order.
|
|
679
|
+
* Returns the best snap within maxDistance, or grid snap as fallback.
|
|
680
|
+
*/
|
|
681
|
+
function snapMulti(point, scene, gridSize, modes = ["vertex", "edge", "grid"], maxDistance = 0.5) {
|
|
682
|
+
for (const mode of modes) {
|
|
683
|
+
if (mode === "vertex") {
|
|
684
|
+
const result = snapToVertex(point, scene, maxDistance);
|
|
685
|
+
if (result)
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
else if (mode === "edge") {
|
|
689
|
+
const result = snapToEdge(point, scene, maxDistance);
|
|
690
|
+
if (result)
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
else if (mode === "grid") {
|
|
694
|
+
return { point: snapToGrid(point, gridSize), target: "grid" };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return { point: snapToGrid(point, gridSize), target: "grid" };
|
|
698
|
+
}
|
|
699
|
+
/** Evaluate a cubic bezier segment at parameter t (0-1). */
|
|
700
|
+
function cubicBezier(p0, p1, p2, p3, t) {
|
|
701
|
+
const t2 = t * t;
|
|
702
|
+
const t3 = t2 * t;
|
|
703
|
+
const mt = 1 - t;
|
|
704
|
+
const mt2 = mt * mt;
|
|
705
|
+
const mt3 = mt2 * mt;
|
|
706
|
+
return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
|
|
707
|
+
}
|
|
708
|
+
/** Evaluate a keyframe track at a given time. */
|
|
709
|
+
function evaluateTrack(track, time) {
|
|
710
|
+
var _a, _b;
|
|
711
|
+
const { keyframes } = track;
|
|
712
|
+
if (keyframes.length === 0)
|
|
713
|
+
return 0;
|
|
714
|
+
if (keyframes.length === 1)
|
|
715
|
+
return keyframes[0].value;
|
|
716
|
+
// Clamp to range
|
|
717
|
+
if (time <= keyframes[0].time)
|
|
718
|
+
return keyframes[0].value;
|
|
719
|
+
if (time >= keyframes[keyframes.length - 1].time)
|
|
720
|
+
return keyframes[keyframes.length - 1].value;
|
|
721
|
+
// Find surrounding keyframes
|
|
722
|
+
let left = 0;
|
|
723
|
+
for (let i = 1; i < keyframes.length; i++) {
|
|
724
|
+
if (keyframes[i].time >= time) {
|
|
725
|
+
left = i - 1;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const right = left + 1;
|
|
730
|
+
const kfL = keyframes[left];
|
|
731
|
+
const kfR = keyframes[right];
|
|
732
|
+
const dt = kfR.time - kfL.time;
|
|
733
|
+
if (dt === 0)
|
|
734
|
+
return kfL.value;
|
|
735
|
+
const t = (time - kfL.time) / dt;
|
|
736
|
+
switch (kfL.interpolation) {
|
|
737
|
+
case "constant":
|
|
738
|
+
return kfL.value;
|
|
739
|
+
case "linear":
|
|
740
|
+
return kfL.value + (kfR.value - kfL.value) * t;
|
|
741
|
+
case "ease-in":
|
|
742
|
+
return kfL.value + (kfR.value - kfL.value) * (t * t);
|
|
743
|
+
case "ease-out":
|
|
744
|
+
return kfL.value + (kfR.value - kfL.value) * (1 - (1 - t) * (1 - t));
|
|
745
|
+
case "ease-in-out":
|
|
746
|
+
return kfL.value + (kfR.value - kfL.value) * (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t));
|
|
747
|
+
case "bezier": {
|
|
748
|
+
const handleR = (_a = kfL.handleRight) !== null && _a !== void 0 ? _a : { y: 0 };
|
|
749
|
+
const handleL = (_b = kfR.handleLeft) !== null && _b !== void 0 ? _b : { y: 0 };
|
|
750
|
+
return cubicBezier(kfL.value, kfL.value + handleR.y, kfR.value + handleL.y, kfR.value, t);
|
|
751
|
+
}
|
|
752
|
+
default:
|
|
753
|
+
return kfL.value + (kfR.value - kfL.value) * t;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** Insert or update a keyframe at a given time. */
|
|
757
|
+
function setKeyframe(track, time, value, interpolation = "bezier") {
|
|
758
|
+
// Find existing keyframe at same time (within epsilon)
|
|
759
|
+
const epsilon = 0.001;
|
|
760
|
+
const existingIdx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
|
|
761
|
+
const kf = { time, value, interpolation };
|
|
762
|
+
if (existingIdx >= 0) {
|
|
763
|
+
track.keyframes[existingIdx] = kf;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
track.keyframes.push(kf);
|
|
767
|
+
track.keyframes.sort((a, b) => a.time - b.time);
|
|
768
|
+
}
|
|
769
|
+
return kf;
|
|
770
|
+
}
|
|
771
|
+
/** Remove a keyframe at a given time. */
|
|
772
|
+
function removeKeyframe(track, time) {
|
|
773
|
+
const epsilon = 0.001;
|
|
774
|
+
const idx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
|
|
775
|
+
if (idx >= 0) {
|
|
776
|
+
track.keyframes.splice(idx, 1);
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
/** Convert KeyframeTrack to THREE.KeyframeTrack for use in AnimationClip. */
|
|
782
|
+
function toThreeKeyframeTrack(track, sampleRate = 30) {
|
|
783
|
+
if (track.keyframes.length === 0) {
|
|
784
|
+
return new THREE.NumberKeyframeTrack(track.property, [0], [0]);
|
|
785
|
+
}
|
|
786
|
+
const startTime = track.keyframes[0].time;
|
|
787
|
+
const endTime = track.keyframes[track.keyframes.length - 1].time;
|
|
788
|
+
const duration = endTime - startTime;
|
|
789
|
+
const numSamples = Math.max(2, Math.ceil(duration * sampleRate));
|
|
790
|
+
const times = [];
|
|
791
|
+
const values = [];
|
|
792
|
+
for (let i = 0; i < numSamples; i++) {
|
|
793
|
+
const t = startTime + (duration * i) / (numSamples - 1);
|
|
794
|
+
times.push(t);
|
|
795
|
+
values.push(evaluateTrack(track, t));
|
|
796
|
+
}
|
|
797
|
+
return new THREE.NumberKeyframeTrack(track.property, times, values);
|
|
798
|
+
}
|
|
799
|
+
/** Create an AnimationClip from multiple KeyframeTracks. */
|
|
800
|
+
function createAnimationClip(name, tracks, sampleRate = 30) {
|
|
801
|
+
const threeTracks = tracks.map((t) => toThreeKeyframeTrack(t, sampleRate));
|
|
802
|
+
return new THREE.AnimationClip(name, -1, threeTracks);
|
|
803
|
+
}
|
|
804
|
+
/** Trigger a file download in the browser. */
|
|
805
|
+
function downloadBlob(blob, filename) {
|
|
806
|
+
const url = URL.createObjectURL(blob);
|
|
807
|
+
const a = document.createElement("a");
|
|
808
|
+
a.href = url;
|
|
809
|
+
a.download = filename;
|
|
810
|
+
document.body.appendChild(a);
|
|
811
|
+
a.click();
|
|
812
|
+
document.body.removeChild(a);
|
|
813
|
+
URL.revokeObjectURL(url);
|
|
814
|
+
}
|
|
815
|
+
/** Export scene to ArrayBuffer as GLTF/GLB with optional Draco. */
|
|
816
|
+
async function exportSceneGLTF(scene, options = {}) {
|
|
817
|
+
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js');
|
|
818
|
+
const exporter = new GLTFExporter();
|
|
819
|
+
return new Promise((resolve, reject) => {
|
|
820
|
+
var _a, _b;
|
|
821
|
+
exporter.parse(scene, (result) => resolve(result), (error) => reject(error), {
|
|
822
|
+
binary: (_a = options.binary) !== null && _a !== void 0 ? _a : true,
|
|
823
|
+
animations: (_b = options.animations) !== null && _b !== void 0 ? _b : [],
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/** Export scene to USDZ format (Apple AR). */
|
|
828
|
+
async function exportSceneUSDZ(scene) {
|
|
829
|
+
const { USDZExporter } = await import('three/examples/jsm/exporters/USDZExporter.js');
|
|
830
|
+
const exporter = new USDZExporter();
|
|
831
|
+
const arraybuffer = await new Promise((resolve, reject) => {
|
|
832
|
+
exporter.parse(scene, resolve, reject);
|
|
833
|
+
});
|
|
834
|
+
return new Blob([arraybuffer], { type: "model/vnd.usdz+zip" });
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Setup crossfade between two animation actions.
|
|
838
|
+
* Smoothly transitions from current to target over duration.
|
|
839
|
+
*/
|
|
840
|
+
function crossfadeAnimations(mixer, fromAction, toAction, duration = 0.5) {
|
|
841
|
+
toAction.reset();
|
|
842
|
+
toAction.setEffectiveTimeScale(1);
|
|
843
|
+
toAction.setEffectiveWeight(1);
|
|
844
|
+
toAction.play();
|
|
845
|
+
fromAction.crossFadeTo(toAction, duration, true);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Setup additive blending for an animation action.
|
|
849
|
+
* Additive animations can layer on top of other animations.
|
|
850
|
+
*/
|
|
851
|
+
function makeAdditiveAction(mixer, clip, referenceClip) {
|
|
852
|
+
const action = mixer.clipAction(clip);
|
|
853
|
+
if (referenceClip) {
|
|
854
|
+
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
|
|
855
|
+
}
|
|
856
|
+
action.blendMode = THREE.AdditiveAnimationBlendMode;
|
|
857
|
+
return action;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Apply weighted blend to multiple animation actions.
|
|
861
|
+
* Normalizes weights so they sum to 1.0.
|
|
862
|
+
*/
|
|
863
|
+
function blendAnimations(actions) {
|
|
864
|
+
const totalWeight = actions.reduce((sum, a) => sum + a.weight, 0);
|
|
865
|
+
if (totalWeight === 0)
|
|
866
|
+
return;
|
|
867
|
+
for (const { action, weight } of actions) {
|
|
868
|
+
const normalizedWeight = weight / totalWeight;
|
|
869
|
+
action.setEffectiveWeight(normalizedWeight);
|
|
870
|
+
if (!action.isRunning()) {
|
|
871
|
+
action.play();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Create a sub-clip from an existing animation clip (trim).
|
|
877
|
+
*/
|
|
878
|
+
function subclip(clip, name, startFrame, endFrame, fps = 30) {
|
|
879
|
+
return THREE.AnimationUtils.subclip(clip, name, startFrame, endFrame, fps);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Simple CCD (Cyclic Coordinate Descent) IK solver.
|
|
883
|
+
* Iteratively rotates each bone in the chain to reach the target.
|
|
884
|
+
*/
|
|
885
|
+
function solveIKChain(skeleton, chain) {
|
|
886
|
+
const { bones: boneNames, target, iterations = 10, tolerance = 0.001 } = chain;
|
|
887
|
+
const bones = [];
|
|
888
|
+
for (const name of boneNames) {
|
|
889
|
+
const bone = skeleton.bones.find((b) => b.name === name);
|
|
890
|
+
if (!bone)
|
|
891
|
+
return false;
|
|
892
|
+
bones.push(bone);
|
|
893
|
+
}
|
|
894
|
+
if (bones.length < 2)
|
|
895
|
+
return false;
|
|
896
|
+
const endEffector = bones[bones.length - 1];
|
|
897
|
+
const endPos = new THREE.Vector3();
|
|
898
|
+
const toEnd = new THREE.Vector3();
|
|
899
|
+
const toTarget = new THREE.Vector3();
|
|
900
|
+
const axis = new THREE.Vector3();
|
|
901
|
+
const quat = new THREE.Quaternion();
|
|
902
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
903
|
+
// Get end-effector world position
|
|
904
|
+
endEffector.updateWorldMatrix(true, false);
|
|
905
|
+
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
906
|
+
if (endPos.distanceTo(target) < tolerance) {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
// Iterate bones from end to root (skip end-effector)
|
|
910
|
+
for (let i = bones.length - 2; i >= 0; i--) {
|
|
911
|
+
const bone = bones[i];
|
|
912
|
+
bone.updateWorldMatrix(true, false);
|
|
913
|
+
const boneWorldPos = new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
|
|
914
|
+
// Direction from bone to end-effector
|
|
915
|
+
endEffector.updateWorldMatrix(true, false);
|
|
916
|
+
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
917
|
+
toEnd.subVectors(endPos, boneWorldPos).normalize();
|
|
918
|
+
// Direction from bone to target
|
|
919
|
+
toTarget.subVectors(target, boneWorldPos).normalize();
|
|
920
|
+
// Calculate rotation axis and angle
|
|
921
|
+
const dot = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);
|
|
922
|
+
const angle = Math.acos(dot);
|
|
923
|
+
if (angle > 0.0001) {
|
|
924
|
+
axis.crossVectors(toEnd, toTarget).normalize();
|
|
925
|
+
// Convert to bone local space
|
|
926
|
+
const boneWorldQuat = new THREE.Quaternion();
|
|
927
|
+
bone.getWorldQuaternion(boneWorldQuat);
|
|
928
|
+
const invWorldQuat = boneWorldQuat.clone().invert();
|
|
929
|
+
const localAxis = axis.clone().applyQuaternion(invWorldQuat);
|
|
930
|
+
// Apply limited rotation
|
|
931
|
+
const maxAngle = Math.PI / 6; // 30° per iteration
|
|
932
|
+
const clampedAngle = Math.min(angle, maxAngle);
|
|
933
|
+
quat.setFromAxisAngle(localAxis, clampedAngle);
|
|
934
|
+
bone.quaternion.premultiply(quat);
|
|
935
|
+
bone.updateWorldMatrix(true, true);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
endEffector.updateWorldMatrix(true, false);
|
|
940
|
+
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
941
|
+
return endPos.distanceTo(target) < tolerance;
|
|
942
|
+
}
|
|
943
|
+
/** Standard quad viewport layout. */
|
|
944
|
+
const QUAD_VIEWPORT = [
|
|
945
|
+
{ preset: "perspective", x: 0.5, y: 0.5, width: 0.5, height: 0.5 },
|
|
946
|
+
{ preset: "top", x: 0, y: 0.5, width: 0.5, height: 0.5 },
|
|
947
|
+
{ preset: "front", x: 0, y: 0, width: 0.5, height: 0.5 },
|
|
948
|
+
{ preset: "right", x: 0.5, y: 0, width: 0.5, height: 0.5 },
|
|
949
|
+
];
|
|
950
|
+
/** Camera position/rotation for orthographic viewport presets. */
|
|
951
|
+
const VIEWPORT_CAMERAS = {
|
|
952
|
+
perspective: { position: [5, 5, 5], up: [0, 1, 0], ortho: false },
|
|
953
|
+
top: { position: [0, 10, 0], up: [0, 0, -1], ortho: true },
|
|
954
|
+
front: { position: [0, 0, 10], up: [0, 1, 0], ortho: true },
|
|
955
|
+
right: { position: [10, 0, 0], up: [0, 1, 0], ortho: true },
|
|
956
|
+
left: { position: [-10, 0, 0], up: [0, 1, 0], ortho: true },
|
|
957
|
+
back: { position: [0, 0, -10], up: [0, 1, 0], ortho: true },
|
|
958
|
+
bottom: { position: [0, -10, 0], up: [0, 0, 1], ortho: true },
|
|
959
|
+
};
|
|
960
|
+
/**
|
|
961
|
+
* Create a camera for a viewport preset.
|
|
962
|
+
*/
|
|
963
|
+
function createViewportCamera(preset, aspect = 1, frustumSize = 10) {
|
|
964
|
+
const config = VIEWPORT_CAMERAS[preset];
|
|
965
|
+
if (config.ortho) {
|
|
966
|
+
const camera = new THREE.OrthographicCamera(-frustumSize * aspect / 2, frustumSize * aspect / 2, frustumSize / 2, -frustumSize / 2, 0.01, 10000);
|
|
967
|
+
camera.position.set(...config.position);
|
|
968
|
+
camera.up.set(...config.up);
|
|
969
|
+
camera.lookAt(0, 0, 0);
|
|
970
|
+
return camera;
|
|
971
|
+
}
|
|
972
|
+
const camera = new THREE.PerspectiveCamera(50, aspect, 0.01, 10000);
|
|
973
|
+
camera.position.set(...config.position);
|
|
974
|
+
camera.up.set(...config.up);
|
|
975
|
+
camera.lookAt(0, 0, 0);
|
|
976
|
+
return camera;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Render a multi-viewport layout to a single renderer.
|
|
980
|
+
*/
|
|
981
|
+
function renderMultiViewport(renderer, scene, viewports) {
|
|
982
|
+
const { width, height } = renderer.getSize(new THREE.Vector2());
|
|
983
|
+
renderer.setScissorTest(true);
|
|
984
|
+
for (const { camera, config } of viewports) {
|
|
985
|
+
const left = Math.floor(config.x * width);
|
|
986
|
+
const bottom = Math.floor(config.y * height);
|
|
987
|
+
const vpWidth = Math.floor(config.width * width);
|
|
988
|
+
const vpHeight = Math.floor(config.height * height);
|
|
989
|
+
renderer.setViewport(left, bottom, vpWidth, vpHeight);
|
|
990
|
+
renderer.setScissor(left, bottom, vpWidth, vpHeight);
|
|
991
|
+
renderer.render(scene, camera);
|
|
992
|
+
}
|
|
993
|
+
renderer.setScissorTest(false);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export { HDRI_PRESETS, PBR_PRESETS, QUAD_VIEWPORT, UndoRedoStack, applyHDRIPreset, applyPBRPreset, blendAnimations, computeSceneBounds, convertToInstanced, countTriangles, createAnimationClip, createMaterialFromPreset, createViewportCamera, crossfadeAnimations, downloadBlob, evaluateTrack, exportSceneGLTF, exportSceneUSDZ, generateLOD, makeAdditiveAction, measureAngle, measureDistance, removeKeyframe, renderMultiViewport, setKeyframe, snapMulti, snapToEdge, snapToGrid, snapToVertex, solveIKChain, subclip, toThreeKeyframeTrack };
|
|
997
|
+
//# sourceMappingURL=modelEditorUtils.js.map
|