@volter/editor-threejs 0.5.66 → 0.5.68
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/contributions/animation-timeline.utility.tsx +44 -0
- package/contributions/three-integration.service.ts +13 -0
- package/package.json +96 -6
- package/src/adapter/renderer-config.ts +3 -4
- package/src/adapter/three-contract.ts +72 -0
- package/src/ecs/object-marks.ts +1 -1
- package/src/ecs/user-data.ts +0 -9
- package/src/host-hierarchy-objects.ts +31 -0
- package/src/kit/animation/three-clips-subject.ts +190 -0
- package/src/kit/asset-compare.ts +294 -0
- package/src/kit/asset-preview-command.ts +265 -0
- package/src/kit/asset-preview-framing.ts +357 -0
- package/src/kit/asset-preview.ts +2802 -0
- package/src/kit/asset-workflow/model-inspection.ts +830 -0
- package/src/kit/authoring/component-instance-root.ts +171 -0
- package/src/kit/authoring/design-time-settle.ts +343 -0
- package/src/kit/authoring/live-object-transform.ts +62 -0
- package/src/kit/authoring/object3d-document-session-registry.ts +154 -0
- package/src/kit/authoring/object3d-document-session.ts +1965 -0
- package/src/kit/authoring/object3d-gesture-controller.ts +113 -0
- package/src/kit/authoring/quarks-particle-systems.ts +19 -0
- package/src/kit/authoring/shell-viewport-policy.ts +48 -0
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/kit/authoring/three-projection-core.ts +226 -0
- package/src/kit/authoring/viewport-pick-context.ts +39 -0
- package/src/kit/authoring/viewport-raycast.ts +240 -0
- package/src/kit/authoring/world-hidden-viewport.ts +95 -0
- package/src/kit/camera-authoring.ts +175 -0
- package/src/kit/components/CameraInfo.tsx +56 -0
- package/src/kit/components/InspectorObjectPreview.tsx +57 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +549 -0
- package/src/kit/components/Object3DDocumentViewport.tsx +58 -0
- package/src/kit/components/StageHost.tsx +2547 -0
- package/src/kit/components/StageOverlays.tsx +21 -0
- package/src/kit/components/StatsOverlay.tsx +78 -0
- package/src/kit/components/ToolObject3DPreview.tsx +39 -0
- package/src/kit/components/ViewportFurniture.tsx +655 -0
- package/src/kit/components/ViewportOverlay.tsx +215 -0
- package/src/kit/components/ViewportShadingMenu.tsx +340 -0
- package/src/kit/components/ViewportViewMenu.tsx +155 -0
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +121 -0
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +395 -0
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +356 -0
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +527 -0
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +132 -0
- package/src/kit/components/object3d-contribution-surfaces.tsx +33 -0
- package/src/kit/components/stage-keyboard.tsx +40 -0
- package/src/kit/components/stage-overlay-set.tsx +105 -0
- package/src/kit/components/stage-presence-markers.ts +482 -0
- package/src/kit/components/stage-transform-chrome.ts +30 -0
- package/src/kit/components/stage-transform-tools.tsx +73 -0
- package/src/kit/components/stage-view-name.ts +30 -0
- package/src/kit/components/standard-viewport-dressing.ts +1042 -0
- package/src/kit/components/world-root-binding.ts +64 -0
- package/src/kit/constraint-helper.ts +338 -0
- package/src/kit/editor-shell-store.ts +814 -0
- package/src/kit/editor-viewport.ts +6621 -0
- package/src/kit/entity-lod.ts +31 -0
- package/src/kit/entity-object.ts +92 -0
- package/src/kit/hierarchy-mark-reader.ts +74 -0
- package/src/kit/instanced-presentation.ts +164 -0
- package/src/kit/live-module-source.ts +230 -0
- package/src/kit/model-thumbnail.ts +539 -0
- package/src/kit/play-camera-flight.ts +300 -0
- package/src/kit/projection/three.ts +898 -0
- package/src/kit/reflection-probe-helper.ts +142 -0
- package/src/kit/scene-document-viewport.ts +51 -0
- package/src/kit/scene-framing.ts +315 -0
- package/src/kit/scene-view-fog.ts +89 -0
- package/src/kit/spatial-handle-visuals.ts +332 -0
- package/src/kit/stories/three-story-model.ts +66 -0
- package/src/kit/three-canvas-render.ts +44 -0
- package/src/kit/three-hierarchy-row-media.ts +26 -0
- package/src/kit/three-inspection-media.ts +73 -0
- package/src/kit/three-integration.ts +86 -0
- package/src/kit/three-state.ts +33 -0
- package/src/kit/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/kit/three-viewport/camera-fit.ts +41 -0
- package/src/kit/three-viewport/interactive-renderer.ts +132 -0
- package/src/kit/three-viewport/selection-brackets.ts +355 -0
- package/src/kit/three-viewport/selection-outline.ts +333 -0
- package/src/kit/three-viewport/skeleton-helper.ts +61 -0
- package/src/kit/three-viewport/source-color.ts +197 -0
- package/src/kit/three-viewport/studio-environment.ts +96 -0
- package/src/kit/trigger-volume-helper.ts +116 -0
- package/src/kit/viewport-actions.ts +128 -0
- package/src/kit/viewport-authoring-policy.ts +154 -0
- package/src/kit/viewport-commands.ts +318 -0
- package/src/kit/viewport-hotkeys.ts +119 -0
- package/src/kit/viewport-shading-boundary.ts +12 -0
- package/src/kit/viewport-status-facet.ts +53 -0
- package/src/object3d-contributions.ts +494 -0
- package/src/render/viewport-shading.ts +6 -2
- package/src/viewport-api.ts +92 -0
- package/src/viewport-door.ts +237 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/** Viewport rendering for the format-neutral spatial-handle contract. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SpatialDragHandle,
|
|
5
|
+
SpatialHandleGuide,
|
|
6
|
+
SpatialHandleLayer,
|
|
7
|
+
} from '@volter/editor-project/adapter';
|
|
8
|
+
import { EDITOR_LAYER } from '@volter/editor-threejs/viewport/editor-layers';
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
|
|
11
|
+
export interface SpatialHandleBinding {
|
|
12
|
+
readonly nodeId: string;
|
|
13
|
+
readonly handle: SpatialDragHandle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SpatialHandleMesh extends THREE.Mesh {
|
|
17
|
+
readonly isSpatialHandleMesh: true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpatialHandleVisualSet {
|
|
21
|
+
readonly roots: THREE.Object3D[];
|
|
22
|
+
readonly handles: SpatialHandleMesh[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const bindingByMesh = new WeakMap<SpatialHandleMesh, SpatialHandleBinding>();
|
|
26
|
+
const HANDLE_GEOMETRY = new THREE.SphereGeometry(1, 12, 8);
|
|
27
|
+
|
|
28
|
+
function vector(value: readonly [number, number, number]): THREE.Vector3 {
|
|
29
|
+
return new THREE.Vector3(value[0], value[1], value[2]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lineMaterial(guide: SpatialHandleGuide): THREE.LineBasicMaterial {
|
|
33
|
+
const options = {
|
|
34
|
+
color: guide.color,
|
|
35
|
+
transparent: true,
|
|
36
|
+
opacity: guide.opacity ?? 0.68,
|
|
37
|
+
depthTest: false,
|
|
38
|
+
};
|
|
39
|
+
return guide.dashed
|
|
40
|
+
? new THREE.LineDashedMaterial({ ...options, dashSize: 0.18, gapSize: 0.12 })
|
|
41
|
+
: new THREE.LineBasicMaterial(options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function addLine(
|
|
45
|
+
root: THREE.Group,
|
|
46
|
+
points: readonly THREE.Vector3[],
|
|
47
|
+
material: THREE.LineBasicMaterial,
|
|
48
|
+
closed = false,
|
|
49
|
+
): void {
|
|
50
|
+
const linePoints = closed && points.length > 0 ? [...points, points[0]!] : [...points];
|
|
51
|
+
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(linePoints), material);
|
|
52
|
+
line.renderOrder = 997;
|
|
53
|
+
line.layers.set(EDITOR_LAYER);
|
|
54
|
+
if (material instanceof THREE.LineDashedMaterial) line.computeLineDistances();
|
|
55
|
+
root.add(line);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function circlePoints(
|
|
59
|
+
center: THREE.Vector3,
|
|
60
|
+
radius: number,
|
|
61
|
+
axisA: THREE.Vector3,
|
|
62
|
+
axisB: THREE.Vector3,
|
|
63
|
+
segments = 64,
|
|
64
|
+
): THREE.Vector3[] {
|
|
65
|
+
const points: THREE.Vector3[] = [];
|
|
66
|
+
for (let i = 0; i < segments; i++) {
|
|
67
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
68
|
+
points.push(
|
|
69
|
+
center
|
|
70
|
+
.clone()
|
|
71
|
+
.addScaledVector(axisA, Math.cos(angle) * radius)
|
|
72
|
+
.addScaledVector(axisB, Math.sin(angle) * radius),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return points;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sphereGuide(guide: Extract<SpatialHandleGuide, { kind: 'sphere' }>): THREE.Group {
|
|
79
|
+
const root = new THREE.Group();
|
|
80
|
+
const center = vector(guide.center);
|
|
81
|
+
const material = lineMaterial(guide);
|
|
82
|
+
addLine(
|
|
83
|
+
root,
|
|
84
|
+
circlePoints(center, guide.radius, new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)),
|
|
85
|
+
material,
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
88
|
+
addLine(
|
|
89
|
+
root,
|
|
90
|
+
circlePoints(center, guide.radius, new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)),
|
|
91
|
+
material,
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
addLine(
|
|
95
|
+
root,
|
|
96
|
+
circlePoints(center, guide.radius, new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)),
|
|
97
|
+
material,
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
return root;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lineGuide(guide: Extract<SpatialHandleGuide, { kind: 'line' }>): THREE.Group {
|
|
104
|
+
const root = new THREE.Group();
|
|
105
|
+
addLine(root, guide.points.map(vector), lineMaterial(guide));
|
|
106
|
+
return root;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function coneGuide(guide: Extract<SpatialHandleGuide, { kind: 'cone' }>): THREE.Group {
|
|
110
|
+
const root = new THREE.Group();
|
|
111
|
+
const origin = vector(guide.origin);
|
|
112
|
+
const direction = vector(guide.direction).normalize();
|
|
113
|
+
const seed =
|
|
114
|
+
Math.abs(direction.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0);
|
|
115
|
+
const right = new THREE.Vector3().crossVectors(seed, direction).normalize();
|
|
116
|
+
const up = new THREE.Vector3().crossVectors(direction, right).normalize();
|
|
117
|
+
const halfAngle = THREE.MathUtils.degToRad(Math.max(0, Math.min(360, guide.angle)) / 2);
|
|
118
|
+
const material = lineMaterial(guide);
|
|
119
|
+
const rim: THREE.Vector3[] = [];
|
|
120
|
+
for (let i = 0; i < 64; i++) {
|
|
121
|
+
const around = (i / 64) * Math.PI * 2;
|
|
122
|
+
const radial = right
|
|
123
|
+
.clone()
|
|
124
|
+
.multiplyScalar(Math.cos(around))
|
|
125
|
+
.addScaledVector(up, Math.sin(around));
|
|
126
|
+
rim.push(
|
|
127
|
+
origin
|
|
128
|
+
.clone()
|
|
129
|
+
.addScaledVector(direction, Math.cos(halfAngle) * guide.length)
|
|
130
|
+
.addScaledVector(radial, Math.sin(halfAngle) * guide.length),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
addLine(root, rim, material, true);
|
|
134
|
+
for (const index of [0, 16, 32, 48]) addLine(root, [origin, rim[index]!], material);
|
|
135
|
+
return root;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function orientedPoint(
|
|
139
|
+
local: THREE.Vector3,
|
|
140
|
+
center: readonly [number, number, number],
|
|
141
|
+
rotation: readonly [number, number, number, number],
|
|
142
|
+
): THREE.Vector3 {
|
|
143
|
+
return local.applyQuaternion(new THREE.Quaternion(...rotation)).add(new THREE.Vector3(...center));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function boxGuide(guide: Extract<SpatialHandleGuide, { kind: 'box' }>): THREE.Group {
|
|
147
|
+
const root = new THREE.Group();
|
|
148
|
+
const material = lineMaterial(guide);
|
|
149
|
+
const [x, y, z] = guide.halfExtents;
|
|
150
|
+
const corners = [
|
|
151
|
+
[-x, -y, -z],
|
|
152
|
+
[x, -y, -z],
|
|
153
|
+
[x, y, -z],
|
|
154
|
+
[-x, y, -z],
|
|
155
|
+
[-x, -y, z],
|
|
156
|
+
[x, -y, z],
|
|
157
|
+
[x, y, z],
|
|
158
|
+
[-x, y, z],
|
|
159
|
+
].map(([cx, cy, cz]) =>
|
|
160
|
+
orientedPoint(new THREE.Vector3(cx, cy, cz), guide.center, guide.rotation),
|
|
161
|
+
);
|
|
162
|
+
for (const [a, b] of [
|
|
163
|
+
[0, 1],
|
|
164
|
+
[1, 2],
|
|
165
|
+
[2, 3],
|
|
166
|
+
[3, 0],
|
|
167
|
+
[4, 5],
|
|
168
|
+
[5, 6],
|
|
169
|
+
[6, 7],
|
|
170
|
+
[7, 4],
|
|
171
|
+
[0, 4],
|
|
172
|
+
[1, 5],
|
|
173
|
+
[2, 6],
|
|
174
|
+
[3, 7],
|
|
175
|
+
] as const) {
|
|
176
|
+
addLine(root, [corners[a]!, corners[b]!], material);
|
|
177
|
+
}
|
|
178
|
+
return root;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function capsuleGuide(guide: Extract<SpatialHandleGuide, { kind: 'capsule' }>): THREE.Group {
|
|
182
|
+
const root = new THREE.Group();
|
|
183
|
+
const material = lineMaterial(guide);
|
|
184
|
+
const rotate = new THREE.Quaternion(...guide.rotation);
|
|
185
|
+
const center = vector(guide.center);
|
|
186
|
+
const transform = (point: THREE.Vector3): THREE.Vector3 =>
|
|
187
|
+
point.applyQuaternion(rotate).add(center);
|
|
188
|
+
const circle = (y: number, axisA: THREE.Vector3, axisB: THREE.Vector3): THREE.Vector3[] =>
|
|
189
|
+
circlePoints(new THREE.Vector3(0, y, 0), guide.radius, axisA, axisB).map(transform);
|
|
190
|
+
addLine(
|
|
191
|
+
root,
|
|
192
|
+
circle(guide.halfHeight, new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)),
|
|
193
|
+
material,
|
|
194
|
+
true,
|
|
195
|
+
);
|
|
196
|
+
addLine(
|
|
197
|
+
root,
|
|
198
|
+
circle(-guide.halfHeight, new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)),
|
|
199
|
+
material,
|
|
200
|
+
true,
|
|
201
|
+
);
|
|
202
|
+
for (const [x, z] of [
|
|
203
|
+
[guide.radius, 0],
|
|
204
|
+
[-guide.radius, 0],
|
|
205
|
+
[0, guide.radius],
|
|
206
|
+
[0, -guide.radius],
|
|
207
|
+
]) {
|
|
208
|
+
addLine(
|
|
209
|
+
root,
|
|
210
|
+
[
|
|
211
|
+
transform(new THREE.Vector3(x, -guide.halfHeight, z)),
|
|
212
|
+
transform(new THREE.Vector3(x, guide.halfHeight, z)),
|
|
213
|
+
],
|
|
214
|
+
material,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
for (const axis of ['x', 'z'] as const) {
|
|
218
|
+
const points: THREE.Vector3[] = [];
|
|
219
|
+
for (let i = 0; i <= 32; i++) {
|
|
220
|
+
const angle = (i / 32) * Math.PI;
|
|
221
|
+
const point = new THREE.Vector3(0, guide.halfHeight + Math.sin(angle) * guide.radius, 0);
|
|
222
|
+
point[axis] = Math.cos(angle) * guide.radius;
|
|
223
|
+
points.push(transform(point));
|
|
224
|
+
}
|
|
225
|
+
for (let i = 0; i <= 32; i++) {
|
|
226
|
+
const angle = Math.PI + (i / 32) * Math.PI;
|
|
227
|
+
const point = new THREE.Vector3(0, -guide.halfHeight + Math.sin(angle) * guide.radius, 0);
|
|
228
|
+
point[axis] = Math.cos(angle) * guide.radius;
|
|
229
|
+
points.push(transform(point));
|
|
230
|
+
}
|
|
231
|
+
addLine(root, points, material);
|
|
232
|
+
}
|
|
233
|
+
return root;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function guideObject(guide: SpatialHandleGuide): THREE.Object3D {
|
|
237
|
+
const root =
|
|
238
|
+
guide.kind === 'line'
|
|
239
|
+
? lineGuide(guide)
|
|
240
|
+
: guide.kind === 'sphere'
|
|
241
|
+
? sphereGuide(guide)
|
|
242
|
+
: guide.kind === 'cone'
|
|
243
|
+
? coneGuide(guide)
|
|
244
|
+
: guide.kind === 'box'
|
|
245
|
+
? boxGuide(guide)
|
|
246
|
+
: capsuleGuide(guide);
|
|
247
|
+
root.layers.set(EDITOR_LAYER);
|
|
248
|
+
root.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
249
|
+
return root;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function handleMesh(nodeId: string, handle: SpatialDragHandle): SpatialHandleMesh {
|
|
253
|
+
const material = new THREE.MeshBasicMaterial({
|
|
254
|
+
color: handle.color,
|
|
255
|
+
depthTest: false,
|
|
256
|
+
transparent: true,
|
|
257
|
+
opacity: handle.writable ? 0.92 : 0.35,
|
|
258
|
+
});
|
|
259
|
+
const mesh = new THREE.Mesh(HANDLE_GEOMETRY, material) as unknown as SpatialHandleMesh;
|
|
260
|
+
Object.defineProperty(mesh, 'isSpatialHandleMesh', { value: true });
|
|
261
|
+
mesh.position.copy(vector(handle.position));
|
|
262
|
+
mesh.renderOrder = 998;
|
|
263
|
+
mesh.layers.set(EDITOR_LAYER);
|
|
264
|
+
bindingByMesh.set(mesh, { nodeId, handle });
|
|
265
|
+
return mesh;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function createSpatialHandleVisuals(
|
|
269
|
+
nodeId: string,
|
|
270
|
+
layers: readonly SpatialHandleLayer[],
|
|
271
|
+
): SpatialHandleVisualSet {
|
|
272
|
+
const roots: THREE.Object3D[] = [];
|
|
273
|
+
const handles: SpatialHandleMesh[] = [];
|
|
274
|
+
for (const layer of layers) {
|
|
275
|
+
for (const guide of layer.guides) roots.push(guideObject(guide));
|
|
276
|
+
for (const handle of layer.handles) {
|
|
277
|
+
const mesh = handleMesh(nodeId, handle);
|
|
278
|
+
roots.push(mesh);
|
|
279
|
+
handles.push(mesh);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { roots, handles };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function spatialHandleBinding(mesh: SpatialHandleMesh): SpatialHandleBinding | null {
|
|
286
|
+
return bindingByMesh.get(mesh) ?? null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function setSpatialHandleHovered(mesh: SpatialHandleMesh, hovered: boolean): void {
|
|
290
|
+
const binding = bindingByMesh.get(mesh);
|
|
291
|
+
const material = mesh.material as THREE.MeshBasicMaterial;
|
|
292
|
+
material.color.set(
|
|
293
|
+
hovered && binding?.handle.writable ? '#ffffff' : (binding?.handle.color ?? '#ffffff'),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function scaleSpatialHandle(mesh: SpatialHandleMesh, camera: THREE.Camera): void {
|
|
298
|
+
const distance = mesh.position.distanceTo(camera.position);
|
|
299
|
+
mesh.scale.setScalar(Math.max(0.025, distance * 0.012));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function disposeRenderable(
|
|
303
|
+
child: THREE.Object3D,
|
|
304
|
+
disposedGeometries: Set<THREE.BufferGeometry>,
|
|
305
|
+
disposedMaterials: Set<THREE.Material>,
|
|
306
|
+
): void {
|
|
307
|
+
const renderable = child as THREE.Line | THREE.Mesh;
|
|
308
|
+
const geometry = renderable.geometry;
|
|
309
|
+
if (geometry && geometry !== HANDLE_GEOMETRY && !disposedGeometries.has(geometry)) {
|
|
310
|
+
geometry.dispose();
|
|
311
|
+
disposedGeometries.add(geometry);
|
|
312
|
+
}
|
|
313
|
+
const materials = Array.isArray(renderable.material)
|
|
314
|
+
? renderable.material
|
|
315
|
+
: renderable.material
|
|
316
|
+
? [renderable.material]
|
|
317
|
+
: [];
|
|
318
|
+
for (const material of materials) {
|
|
319
|
+
if (disposedMaterials.has(material)) continue;
|
|
320
|
+
material.dispose();
|
|
321
|
+
disposedMaterials.add(material);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function disposeSpatialHandleVisuals(roots: readonly THREE.Object3D[]): void {
|
|
326
|
+
const disposedGeometries = new Set<THREE.BufferGeometry>();
|
|
327
|
+
const disposedMaterials = new Set<THREE.Material>();
|
|
328
|
+
for (const root of roots) {
|
|
329
|
+
root.removeFromParent();
|
|
330
|
+
root.traverse((child) => disposeRenderable(child, disposedGeometries, disposedMaterials));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, headless facts about a `three` STORY: whether a mounted story holds
|
|
3
|
+
* three content, and how a story's preview thumbnail is keyed in an in-memory
|
|
4
|
+
* cache. Kept pure and off the React tree so every one of them is
|
|
5
|
+
* unit-testable with no renderer.
|
|
6
|
+
*
|
|
7
|
+
* Membership belongs to the CONSUMER, never to the registry, and it is
|
|
8
|
+
* DECLARED (`story-declared-medium.ts`). The `3D Components` board admits
|
|
9
|
+
* stories whose medium is `three`, the canvas board admits `canvas`, the
|
|
10
|
+
* DOM design board admits `dom` ({@link domStoryBoardMembers}). An
|
|
11
|
+
* undeclared story is a named gap and is not a candidate on any board —
|
|
12
|
+
* nothing here mounts to guess. The project's story registry stays
|
|
13
|
+
* deliberately unfiltered.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type * as THREE from 'three';
|
|
17
|
+
import { getProjectStoryRegions } from '@volter/editor-sdk/kit/stories/project-story-regions';
|
|
18
|
+
import { declaredStoryMedium, reportUndeclaredStoryMedium } from '@volter/editor-sdk/kit/stories/story-declared-medium';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a mounted story's root wrapper actually holds `three` content.
|
|
22
|
+
*
|
|
23
|
+
* A story that renders nothing (or renders only React fragments/null) commits
|
|
24
|
+
* successfully into the wrapper group and yields an EMPTY `Object3D` — a
|
|
25
|
+
* successful mount with nothing in it. A surface shows a story that rendered
|
|
26
|
+
* something; "the mount succeeded" alone is not the test.
|
|
27
|
+
*/
|
|
28
|
+
export function mountedStoryHasThreeContent(root: THREE.Object3D): boolean {
|
|
29
|
+
return root.children.length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The DOM design board's membership — every story DECLARED as `dom`.
|
|
34
|
+
*
|
|
35
|
+
* An undeclared story is named once and is not a member. Nothing here
|
|
36
|
+
* mounts: medium is a declaration, and a last-resort mount would make
|
|
37
|
+
* "residue 0" unfalsifiable (the orphan would land on this board).
|
|
38
|
+
*
|
|
39
|
+
* The board (`authoring/design-time-layers.ts`) renders each member through
|
|
40
|
+
* `react-dom`, so a three story landing on it renders `<group>`/`<primitive>`
|
|
41
|
+
* as unknown DOM tags. Declaration is what keeps those stories off this
|
|
42
|
+
* board; the project's story registry stays deliberately unfiltered.
|
|
43
|
+
*/
|
|
44
|
+
export function domStoryBoardMembers<
|
|
45
|
+
T extends {
|
|
46
|
+
readonly id?: string;
|
|
47
|
+
readonly name?: string;
|
|
48
|
+
readonly modulePath?: string;
|
|
49
|
+
readonly tags?: readonly string[];
|
|
50
|
+
},
|
|
51
|
+
>(stories: readonly T[]): T[] {
|
|
52
|
+
const regions = getProjectStoryRegions();
|
|
53
|
+
const members: T[] = [];
|
|
54
|
+
for (const story of stories) {
|
|
55
|
+
const declared = declaredStoryMedium({ modulePath: story.modulePath, regions });
|
|
56
|
+
if (declared.medium === 'dom') {
|
|
57
|
+
members.push(story);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (declared.via === 'undeclared' && story.modulePath) {
|
|
61
|
+
reportUndeclaredStoryMedium(story.modulePath, declared.reason);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return members;
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Three set's canvases, rendered at a caller's scale
|
|
3
|
+
* (`@volter/editor-sdk/kit/canvas-frames`): the Scene viewport's presented frame
|
|
4
|
+
* or a fresh render of it, and an Object3D document's presented frame or an
|
|
5
|
+
* offscreen render of its subject. The editor-chrome photograph asks here, so a
|
|
6
|
+
* 2x capture of a viewport gets a 2x render rather than a 1x one stretched.
|
|
7
|
+
*/
|
|
8
|
+
import { registerCanvasRender } from '@volter/editor-sdk/kit/canvas-frames';
|
|
9
|
+
import { allObject3DDocumentSessions } from './authoring/object3d-document-session-registry';
|
|
10
|
+
import { liveCanvasFrame } from '@volter/editor-sdk/kit/live-canvas-frame';
|
|
11
|
+
import { threeStoreForHost } from './three-state';
|
|
12
|
+
|
|
13
|
+
async function imageOf(dataUrl: string): Promise<HTMLImageElement> {
|
|
14
|
+
const image = new Image();
|
|
15
|
+
image.src = dataUrl;
|
|
16
|
+
await image.decode();
|
|
17
|
+
return image;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerThreeCanvasRender(): () => void {
|
|
21
|
+
return registerCanvasRender((canvas, size) => {
|
|
22
|
+
const store = threeStoreForHost();
|
|
23
|
+
if (store && store.viewportCanvas() === canvas) {
|
|
24
|
+
return (async () => {
|
|
25
|
+
const presented = await liveCanvasFrame(canvas);
|
|
26
|
+
if (presented) return presented;
|
|
27
|
+
const dataUrl = store.captureViewportImage(size);
|
|
28
|
+
return dataUrl ? imageOf(dataUrl) : null;
|
|
29
|
+
})();
|
|
30
|
+
}
|
|
31
|
+
const session = allObject3DDocumentSessions().find((candidate) => candidate.renderer.domElement === canvas);
|
|
32
|
+
if (!session) return null;
|
|
33
|
+
return (async () => {
|
|
34
|
+
// The PRESENTED frame first: everything the host drew over the document's
|
|
35
|
+
// own render — the compass above all — lives only in the default
|
|
36
|
+
// framebuffer, and `captureImage` is an offscreen re-render of the subject
|
|
37
|
+
// alone. Falling back to it keeps a stopped loop photographable.
|
|
38
|
+
const presented = await session.requestPresentedFrame();
|
|
39
|
+
if (presented) return presented;
|
|
40
|
+
const dataUrl = session.captureImage(size);
|
|
41
|
+
return dataUrl ? imageOf(dataUrl) : null;
|
|
42
|
+
})();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Three integration's answers to the hierarchy panel's row questions
|
|
3
|
+
* (`@volter/editor-sdk/kit/hierarchy-row-media`): an instanced mesh's row names the units it
|
|
4
|
+
* draws, a node's component marks come off its `Object3D`, and the store's Three half logs which
|
|
5
|
+
* projected parents its object map changed so the panel can patch rows.
|
|
6
|
+
*/
|
|
7
|
+
import { registerHierarchyRowMedia } from '@volter/editor-sdk/kit/hierarchy-row-media';
|
|
8
|
+
import { entityObject3D } from './entity-object';
|
|
9
|
+
import { markReaderFor } from './hierarchy-mark-reader';
|
|
10
|
+
import { instancedRowDetail } from './instanced-presentation';
|
|
11
|
+
import { threeStateOf } from './three-state';
|
|
12
|
+
|
|
13
|
+
export function registerThreeHierarchyRowMedia(): () => void {
|
|
14
|
+
return registerHierarchyRowMedia({
|
|
15
|
+
detail: (adapter, nodeId, store) =>
|
|
16
|
+
instancedRowDetail(entityObject3D(adapter, threeStateOf(store).objectMap, nodeId)),
|
|
17
|
+
marks: (hierarchy) => markReaderFor(hierarchy),
|
|
18
|
+
structureLog: (store) => {
|
|
19
|
+
const three = threeStateOf(store);
|
|
20
|
+
return {
|
|
21
|
+
epoch: () => three.gizmoEpoch,
|
|
22
|
+
parentChangesSince: (epoch) => three.ingestObjectMapChangesSince(epoch),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE THREE.JS ANSWER TO "what does this node look like"
|
|
3
|
+
* (`@volter/editor-sdk/kit/inspection-node-media`): a selected node the shell's
|
|
4
|
+
* Three store resolves to a live `Object3D` is previewed as an isolated live
|
|
5
|
+
* view of that object, a camera has no Preview section (its view is the
|
|
6
|
+
* viewport's), an instanced mesh names its units, and a node in the object map
|
|
7
|
+
* has an Asset Editor document to jump to. The kit's composer
|
|
8
|
+
* (`compose-subject.ts`) builds the sections from this answer.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
type InspectionNodeMedia,
|
|
12
|
+
registerInspectionNodeMedia,
|
|
13
|
+
} from '@volter/editor-sdk/kit/inspection-node-media';
|
|
14
|
+
import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
|
|
15
|
+
import { createElement, lazy } from 'react';
|
|
16
|
+
import type * as THREE from 'three';
|
|
17
|
+
import { openRegisteredDocument } from '@volter/editor-sdk/kit/document-open-registry';
|
|
18
|
+
import { InspectorPreviewBody } from '@volter/editor-sdk/kit/components/inspector-preview-section';
|
|
19
|
+
import { entityObject3D } from './entity-object';
|
|
20
|
+
import { describeInstancedPresentation } from './instanced-presentation';
|
|
21
|
+
import { threeStoreForHost } from './three-state';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// The heavy render stack stays behind `lazy()`, so a headless composer
|
|
25
|
+
// (`editor.inspect`, which never renders) pays nothing for it.
|
|
26
|
+
const InspectorObjectPreview = lazy(() =>
|
|
27
|
+
import('./components/InspectorObjectPreview').then((m) => ({ default: m.InspectorObjectPreview })),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function objectMedia(
|
|
31
|
+
object: THREE.Object3D,
|
|
32
|
+
assetDocument: InspectionNodeMedia['assetDocument'],
|
|
33
|
+
): InspectionNodeMedia {
|
|
34
|
+
return {
|
|
35
|
+
preview: (object as THREE.Camera).isCamera
|
|
36
|
+
? null
|
|
37
|
+
: ({ displayName, previewKey, actions }) =>
|
|
38
|
+
(mode) =>
|
|
39
|
+
createElement(InspectorPreviewBody, {
|
|
40
|
+
picture: () =>
|
|
41
|
+
createElement(InspectorObjectPreview, { previewKey, object, displayName, fill: true }),
|
|
42
|
+
previewKey,
|
|
43
|
+
actions,
|
|
44
|
+
...(mode ? { mode } : {}),
|
|
45
|
+
}),
|
|
46
|
+
// `contentWorldBounds` resolves instance matrices live — three's own cached
|
|
47
|
+
// object box would answer for whatever pose was first asked about.
|
|
48
|
+
instanced: describeInstancedPresentation(object, contentWorldBounds(object)),
|
|
49
|
+
assetDocument,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerThreeInspectionMedia(): () => void {
|
|
54
|
+
return registerInspectionNodeMedia((adapter, nodeId) => {
|
|
55
|
+
const store = threeStoreForHost();
|
|
56
|
+
if (!store) return null;
|
|
57
|
+
const object = entityObject3D(adapter, store.objectMap, nodeId);
|
|
58
|
+
if (!object) return null;
|
|
59
|
+
return objectMedia(
|
|
60
|
+
object,
|
|
61
|
+
store.objectMap.has(nodeId)
|
|
62
|
+
? {
|
|
63
|
+
// The live `Object3D.name` is the only name there is.
|
|
64
|
+
open: () =>
|
|
65
|
+
openRegisteredDocument('asset-editor:entity', store.shell, {
|
|
66
|
+
entityId: nodeId,
|
|
67
|
+
title: object.name || nodeId,
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
: null,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE THREE INTEGRATION, INSTALLED — every registration the kit's Three viewport answers
|
|
3
|
+
* through: the Object3D surfaces contributions mount, the viewport's relay verbs, its Asset Lab
|
|
4
|
+
* viewers, the inspector's node media, model thumbnails, the viewport status facet,
|
|
5
|
+
* canvas captures, the gizmo viewport's authoring policy, and the view state each session store's
|
|
6
|
+
* Three half saves. The kit constructs none of it; the integration installs it once, and every
|
|
7
|
+
* Three surface asks for it before it mounts (`ensureThreeIntegration`).
|
|
8
|
+
*/
|
|
9
|
+
import { registerContributedCommands } from '@volter/editor-sdk/kit/command-registry';
|
|
10
|
+
import { saveThumbnail } from '@volter/editor-sdk/kit/editor-api';
|
|
11
|
+
import { registerHostHierarchyObjects } from '../host-hierarchy-objects';
|
|
12
|
+
import { registerContributionSurfaces } from '@volter/editor-sdk/kit/contribution-surfaces';
|
|
13
|
+
import { registerRendererResourceCounts } from '@volter/editor-sdk/kit/renderer-resource-counts';
|
|
14
|
+
import { interactiveViewportRendererCounts } from './three-viewport/interactive-renderer';
|
|
15
|
+
import { inspectorPreviewRendererCounts } from '../viewport/preview-renderer';
|
|
16
|
+
import { liveHostRendererCount } from '../viewport/renderer-ownership';
|
|
17
|
+
import type * as THREE from 'three';
|
|
18
|
+
import { SHELL_VIEWPORT_AUTHORING_POLICY } from './authoring/shell-viewport-policy';
|
|
19
|
+
import { Object3DAuthoringSurface, Object3DPreviewSurface } from './components/object3d-contribution-surfaces';
|
|
20
|
+
import { registerThreeAssetViewers } from './components/asset-viewers/three-asset-viewers';
|
|
21
|
+
import type { EditorStatePersistence } from './editor-shell-store';
|
|
22
|
+
import { registerModelThumbnails } from './model-thumbnail';
|
|
23
|
+
import { writeProjectLocalSection } from '@volter/editor-sdk/kit/project-local-state';
|
|
24
|
+
import { onShellStore } from '@volter/editor-sdk/kit/shell-store-door';
|
|
25
|
+
import { registerThreeCanvasRender } from './three-canvas-render';
|
|
26
|
+
import { registerThreeHierarchyRowMedia } from './three-hierarchy-row-media';
|
|
27
|
+
import { registerThreePlayCameraFlight } from './play-camera-flight';
|
|
28
|
+
import { registerThreeInspectionMedia } from './three-inspection-media';
|
|
29
|
+
import { threeStateOf, threeStoreForHost } from './three-state';
|
|
30
|
+
import { installViewportAuthoringPolicy } from './viewport-authoring-policy';
|
|
31
|
+
import { viewportCommands } from './viewport-commands';
|
|
32
|
+
import { reportViewportStatus } from './viewport-status-facet';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The session's view state (tools, helpers, shading, camera) and its thumbnails, saved to the
|
|
36
|
+
* project's own `view` section. ONE object across a Fast Refresh of this module, because
|
|
37
|
+
* `attachStatePersistence` keeps one owner and a re-evaluated module constant is a new one.
|
|
38
|
+
*/
|
|
39
|
+
const hotData = (import.meta as ImportMeta & { hot?: { data: Record<string, unknown> } }).hot?.data;
|
|
40
|
+
const VIEW_STATE_PERSISTENCE: EditorStatePersistence = (hotData?.['viewStatePersistence'] as
|
|
41
|
+
| EditorStatePersistence
|
|
42
|
+
| undefined) ?? {
|
|
43
|
+
save: (state) => writeProjectLocalSection('view', state),
|
|
44
|
+
saveThumbnail,
|
|
45
|
+
};
|
|
46
|
+
if (hotData) hotData['viewStatePersistence'] = VIEW_STATE_PERSISTENCE;
|
|
47
|
+
|
|
48
|
+
const NO_OBJECTS: ReadonlyMap<string, THREE.Object3D> = new Map();
|
|
49
|
+
|
|
50
|
+
let installed: (() => void) | null = null;
|
|
51
|
+
|
|
52
|
+
/** Install the integration; idempotent. Returns the teardown of the first install. */
|
|
53
|
+
export function ensureThreeIntegration(): () => void {
|
|
54
|
+
if (installed) return installed;
|
|
55
|
+
const stops = [
|
|
56
|
+
registerContributionSurfaces({
|
|
57
|
+
Object3DPreview: Object3DPreviewSurface,
|
|
58
|
+
Object3DAuthoring: Object3DAuthoringSurface,
|
|
59
|
+
}),
|
|
60
|
+
registerContributedCommands('three-viewport', viewportCommands),
|
|
61
|
+
registerThreeAssetViewers(),
|
|
62
|
+
registerThreeInspectionMedia(),
|
|
63
|
+
registerModelThumbnails(),
|
|
64
|
+
reportViewportStatus(),
|
|
65
|
+
registerThreeCanvasRender(),
|
|
66
|
+
registerThreeHierarchyRowMedia(),
|
|
67
|
+
registerThreePlayCameraFlight(),
|
|
68
|
+
registerHostHierarchyObjects({
|
|
69
|
+
object: (id) => threeStoreForHost()?.objectMap.get(id) ?? null,
|
|
70
|
+
objects: () => threeStoreForHost()?.objectMap ?? NO_OBJECTS,
|
|
71
|
+
}),
|
|
72
|
+
registerRendererResourceCounts('hostLive', liveHostRendererCount),
|
|
73
|
+
registerRendererResourceCounts('interactive', interactiveViewportRendererCounts),
|
|
74
|
+
registerRendererResourceCounts('inspectorPreview', inspectorPreviewRendererCounts),
|
|
75
|
+
// The session store's Three half saves its view state; attached once per store.
|
|
76
|
+
onShellStore((store) => threeStateOf(store).attachStatePersistence(VIEW_STATE_PERSISTENCE)),
|
|
77
|
+
];
|
|
78
|
+
// The gizmo viewport's questions (which adapter is active, what is under the pointer, which
|
|
79
|
+
// root is world-hidden) are answered before any viewport mounts.
|
|
80
|
+
installViewportAuthoringPolicy(SHELL_VIEWPORT_AUTHORING_POLICY);
|
|
81
|
+
installed = () => {
|
|
82
|
+
for (const stop of stops.reverse()) stop();
|
|
83
|
+
installed = null;
|
|
84
|
+
};
|
|
85
|
+
return installed;
|
|
86
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A shell store's Three half (`threeStateOf`, beside the half in `editor-shell-store.ts`). */
|
|
2
|
+
import type { ShellStore } from '@volter/editor-sdk/kit/shell-store';
|
|
3
|
+
import { type EditorShellStore, threeStateOf } from './editor-shell-store';
|
|
4
|
+
import { useEditorStore, useOptionalEditorStore } from '@volter/editor-sdk/kit/editor-runtime';
|
|
5
|
+
import { onShellStore, shellStoreForHost } from '@volter/editor-sdk/kit/shell-store-door';
|
|
6
|
+
|
|
7
|
+
export { threeStateOf };
|
|
8
|
+
|
|
9
|
+
/** {@link threeStateOf} for a store that may be absent (outside a session). */
|
|
10
|
+
export function optionalThreeStateOf(store: ShellStore | null): EditorShellStore | null {
|
|
11
|
+
return store === null ? null : threeStateOf(store);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The session store's Three half (`threeStateOf`), for a component that reads the scene,
|
|
15
|
+
* object map, camera or viewport tools. */
|
|
16
|
+
export function useThreeEditorStore(): EditorShellStore {
|
|
17
|
+
return threeStateOf(useEditorStore());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** {@link useThreeEditorStore} outside a session answers `null`. */
|
|
21
|
+
export function useOptionalThreeEditorStore(): EditorShellStore | null {
|
|
22
|
+
return optionalThreeStateOf(useOptionalEditorStore());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The session store's Three half (`three-state.ts`), or `null` before a session opens. */
|
|
26
|
+
export function threeStoreForHost(): EditorShellStore | null {
|
|
27
|
+
return optionalThreeStateOf(shellStoreForHost());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** {@link onShellStore} for a lane that works on the store's Three half. */
|
|
31
|
+
export function onThreeStore(listener: (store: EditorShellStore) => void): () => void {
|
|
32
|
+
return onShellStore((store) => listener(threeStateOf(store)));
|
|
33
|
+
}
|