@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.
Files changed (99) hide show
  1. package/contributions/animation-timeline.utility.tsx +44 -0
  2. package/contributions/three-integration.service.ts +13 -0
  3. package/package.json +96 -6
  4. package/src/adapter/renderer-config.ts +3 -4
  5. package/src/adapter/three-contract.ts +72 -0
  6. package/src/ecs/object-marks.ts +1 -1
  7. package/src/ecs/user-data.ts +0 -9
  8. package/src/host-hierarchy-objects.ts +31 -0
  9. package/src/kit/animation/three-clips-subject.ts +190 -0
  10. package/src/kit/asset-compare.ts +294 -0
  11. package/src/kit/asset-preview-command.ts +265 -0
  12. package/src/kit/asset-preview-framing.ts +357 -0
  13. package/src/kit/asset-preview.ts +2802 -0
  14. package/src/kit/asset-workflow/model-inspection.ts +830 -0
  15. package/src/kit/authoring/component-instance-root.ts +171 -0
  16. package/src/kit/authoring/design-time-settle.ts +343 -0
  17. package/src/kit/authoring/live-object-transform.ts +62 -0
  18. package/src/kit/authoring/object3d-document-session-registry.ts +154 -0
  19. package/src/kit/authoring/object3d-document-session.ts +1965 -0
  20. package/src/kit/authoring/object3d-gesture-controller.ts +113 -0
  21. package/src/kit/authoring/quarks-particle-systems.ts +19 -0
  22. package/src/kit/authoring/shell-viewport-policy.ts +48 -0
  23. package/src/kit/authoring/source-object3d-authoring-adapter.ts +526 -0
  24. package/src/kit/authoring/three-projection-core.ts +226 -0
  25. package/src/kit/authoring/viewport-pick-context.ts +39 -0
  26. package/src/kit/authoring/viewport-raycast.ts +240 -0
  27. package/src/kit/authoring/world-hidden-viewport.ts +95 -0
  28. package/src/kit/camera-authoring.ts +175 -0
  29. package/src/kit/components/CameraInfo.tsx +56 -0
  30. package/src/kit/components/InspectorObjectPreview.tsx +57 -0
  31. package/src/kit/components/Object3DDocumentToolbar.tsx +549 -0
  32. package/src/kit/components/Object3DDocumentViewport.tsx +58 -0
  33. package/src/kit/components/StageHost.tsx +2547 -0
  34. package/src/kit/components/StageOverlays.tsx +21 -0
  35. package/src/kit/components/StatsOverlay.tsx +78 -0
  36. package/src/kit/components/ToolObject3DPreview.tsx +39 -0
  37. package/src/kit/components/ViewportFurniture.tsx +655 -0
  38. package/src/kit/components/ViewportOverlay.tsx +215 -0
  39. package/src/kit/components/ViewportShadingMenu.tsx +340 -0
  40. package/src/kit/components/ViewportViewMenu.tsx +155 -0
  41. package/src/kit/components/asset-viewers/EntityModelDocument.tsx +121 -0
  42. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  43. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +395 -0
  44. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +444 -0
  45. package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  46. package/src/kit/components/asset-viewers/Object3DPreview.tsx +356 -0
  47. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +527 -0
  48. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  49. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +132 -0
  50. package/src/kit/components/object3d-contribution-surfaces.tsx +33 -0
  51. package/src/kit/components/stage-keyboard.tsx +40 -0
  52. package/src/kit/components/stage-overlay-set.tsx +105 -0
  53. package/src/kit/components/stage-presence-markers.ts +482 -0
  54. package/src/kit/components/stage-transform-chrome.ts +30 -0
  55. package/src/kit/components/stage-transform-tools.tsx +73 -0
  56. package/src/kit/components/stage-view-name.ts +30 -0
  57. package/src/kit/components/standard-viewport-dressing.ts +1042 -0
  58. package/src/kit/components/world-root-binding.ts +64 -0
  59. package/src/kit/constraint-helper.ts +338 -0
  60. package/src/kit/editor-shell-store.ts +814 -0
  61. package/src/kit/editor-viewport.ts +6621 -0
  62. package/src/kit/entity-lod.ts +31 -0
  63. package/src/kit/entity-object.ts +92 -0
  64. package/src/kit/hierarchy-mark-reader.ts +74 -0
  65. package/src/kit/instanced-presentation.ts +164 -0
  66. package/src/kit/live-module-source.ts +230 -0
  67. package/src/kit/model-thumbnail.ts +539 -0
  68. package/src/kit/play-camera-flight.ts +300 -0
  69. package/src/kit/projection/three.ts +898 -0
  70. package/src/kit/reflection-probe-helper.ts +142 -0
  71. package/src/kit/scene-document-viewport.ts +51 -0
  72. package/src/kit/scene-framing.ts +315 -0
  73. package/src/kit/scene-view-fog.ts +89 -0
  74. package/src/kit/spatial-handle-visuals.ts +332 -0
  75. package/src/kit/stories/three-story-model.ts +66 -0
  76. package/src/kit/three-canvas-render.ts +44 -0
  77. package/src/kit/three-hierarchy-row-media.ts +26 -0
  78. package/src/kit/three-inspection-media.ts +73 -0
  79. package/src/kit/three-integration.ts +86 -0
  80. package/src/kit/three-state.ts +33 -0
  81. package/src/kit/three-viewport/bone-selection-highlight.ts +119 -0
  82. package/src/kit/three-viewport/camera-fit.ts +41 -0
  83. package/src/kit/three-viewport/interactive-renderer.ts +132 -0
  84. package/src/kit/three-viewport/selection-brackets.ts +355 -0
  85. package/src/kit/three-viewport/selection-outline.ts +333 -0
  86. package/src/kit/three-viewport/skeleton-helper.ts +61 -0
  87. package/src/kit/three-viewport/source-color.ts +197 -0
  88. package/src/kit/three-viewport/studio-environment.ts +96 -0
  89. package/src/kit/trigger-volume-helper.ts +116 -0
  90. package/src/kit/viewport-actions.ts +128 -0
  91. package/src/kit/viewport-authoring-policy.ts +154 -0
  92. package/src/kit/viewport-commands.ts +318 -0
  93. package/src/kit/viewport-hotkeys.ts +119 -0
  94. package/src/kit/viewport-shading-boundary.ts +12 -0
  95. package/src/kit/viewport-status-facet.ts +53 -0
  96. package/src/object3d-contributions.ts +494 -0
  97. package/src/render/viewport-shading.ts +6 -2
  98. package/src/viewport-api.ts +92 -0
  99. package/src/viewport-door.ts +237 -0
@@ -0,0 +1,356 @@
1
+ import type { ToolObject3DPreviewContext, ToolObject3DPreviewExtension } from '../../../object3d-contributions';
2
+ import { themeVars } from '@volter/editor-sdk/widgets';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import * as THREE from 'three';
5
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
6
+ import {
7
+ createOwnedObjectPreviewSnapshot,
8
+ createPerspectiveAssetPreviewCamera,
9
+ disposeProjectAssetModel,
10
+ loadProjectAssetModel,
11
+ measureAssetPreview,
12
+ type NativeObjectPreviewSource,
13
+ type OwnedObjectPreviewSnapshot,
14
+ } from '../../asset-preview';
15
+ import { registerPreviewResource } from '@volter/editor-sdk/kit/asset-workflow/preview-resource-lifetime';
16
+ import { registerDesignTimeSurface } from '@volter/editor-sdk/kit/coverage/design-time-surfaces';
17
+ import {
18
+ isEditorPresentationActive,
19
+ subscribeEditorPresentationActivity,
20
+ } from '@volter/editor-sdk/kit/editor-presentation-activity';
21
+ import {
22
+ createModelPreviewSource,
23
+ loadModelThumbnailObject,
24
+ modelThumbnailFormat,
25
+ } from '../../model-thumbnail';
26
+
27
+ export interface Object3DPreviewProps {
28
+ readonly assetPath?: string | undefined;
29
+ readonly materialPath?: string | undefined;
30
+ readonly displayName?: string | undefined;
31
+ readonly active?: boolean | undefined;
32
+ readonly sourceFactory?: (() => NativeObjectPreviewSource) | undefined;
33
+ readonly showSkeleton?: boolean | undefined;
34
+ readonly background?: THREE.ColorRepresentation | undefined;
35
+ readonly cameraDirection?: readonly [number, number, number] | undefined;
36
+ readonly showGrid?: boolean | undefined;
37
+ readonly useDefaultLighting?: boolean | undefined;
38
+ readonly exposure?: number | undefined;
39
+ readonly setupPreview?:
40
+ | ((context: ToolObject3DPreviewContext) => ToolObject3DPreviewExtension | undefined)
41
+ | undefined;
42
+ }
43
+
44
+ /**
45
+ * Pose the model at its first clip's frame ZERO and HOLD it there. A preview
46
+ * card is a design-time surface: presentation (orbit damping, a project
47
+ * `setupPreview` extension's own chrome) keeps running, content time does not
48
+ * (`coverage/design-time-surfaces.ts`). There is no option to play it — the one
49
+ * place a clip runs on a design-time surface is the animation workspace's
50
+ * transport, on the document surface, under a human's hand.
51
+ */
52
+ function poseAtFirstFrame(loaded: THREE.Object3D): THREE.AnimationMixer | null {
53
+ const clip = loaded.animations[0];
54
+ if (!clip) return null;
55
+ const mixer = new THREE.AnimationMixer(loaded);
56
+ const action = mixer.clipAction(clip).play();
57
+ action.paused = true;
58
+ mixer.update(0);
59
+ return mixer;
60
+ }
61
+
62
+ /** Preview cards are anonymous (several can show the same asset at once), so the
63
+ * Edit-static row needs a per-MOUNT identity to name one by. */
64
+ let previewCardSequence = 0;
65
+
66
+ /** Compact, non-authoring Object3D preview for cards, catalogs, and thumbnails. */
67
+ export function Object3DPreview({
68
+ assetPath,
69
+ materialPath,
70
+ displayName = 'Object3D preview',
71
+ active = true,
72
+ sourceFactory,
73
+ showSkeleton = false,
74
+ background,
75
+ cameraDirection,
76
+ showGrid = true,
77
+ useDefaultLighting = true,
78
+ exposure = 1,
79
+ setupPreview,
80
+ }: Object3DPreviewProps) {
81
+ const containerRef = useRef<HTMLDivElement>(null);
82
+ const [error, setError] = useState<string | null>(null);
83
+ const [loading, setLoading] = useState(true);
84
+
85
+ useEffect(() => {
86
+ if (!active) return;
87
+ const container = containerRef.current;
88
+ if (!container) return;
89
+ const controller = new AbortController();
90
+ // This card holds its content time at zero for its whole life — nothing
91
+ // below advances a mixer or a source tick — and says so out loud, so
92
+ // Edit-static speaks for preview cards too instead of skipping them
93
+ // (`coverage/design-time-surfaces.ts`).
94
+ const unregisterDesignTimeSurface = registerDesignTimeSurface({
95
+ id: `preview-card:${++previewCardSequence}`,
96
+ label: `${displayName} (preview card)`,
97
+ contentClock: () => 0,
98
+ });
99
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
100
+ const releaseContext = registerPreviewResource('webglContext');
101
+ const releaseAnimationFrame = registerPreviewResource('animationFrame');
102
+ const releaseResizeObserver = registerPreviewResource('resizeObserver');
103
+ const scene = new THREE.Scene();
104
+ const camera = new THREE.PerspectiveCamera(35, 1, 0.01, 1000);
105
+ const controls = new OrbitControls(camera, renderer.domElement);
106
+ const clock = new THREE.Clock();
107
+ let mixer: THREE.AnimationMixer | null = null;
108
+ let ownedModel: THREE.Object3D | null = null;
109
+ let ownedFactory: OwnedObjectPreviewSnapshot | null = null;
110
+ let extension: ToolObject3DPreviewExtension | undefined;
111
+ let sparkRenderer: SparkRenderer | null = null;
112
+ let framing: ReturnType<typeof measureAssetPreview> | null = null;
113
+ const previewHelpers: THREE.Object3D[] = [];
114
+ let frame = 0;
115
+ let disposed = false;
116
+
117
+ setError(null);
118
+ setLoading(true);
119
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
120
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
121
+ renderer.toneMappingExposure = exposure;
122
+ renderer.shadowMap.enabled = true;
123
+ renderer.setClearColor(background ?? 0x20242a, 1);
124
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
125
+ // The backing buffer scales with DPR, but layout must remain exactly the
126
+ // size of the preview surface. Without an explicit CSS footprint a DPR 2
127
+ // canvas lays out at twice the intended dimensions and overflow clips it
128
+ // to the top-left quadrant.
129
+ renderer.domElement.style.width = '100%';
130
+ renderer.domElement.style.height = '100%';
131
+ renderer.domElement.style.display = 'block';
132
+ container.appendChild(renderer.domElement);
133
+ controls.enableDamping = true;
134
+ controls.dampingFactor = 0.1;
135
+
136
+ if (useDefaultLighting) {
137
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x354052, 1.8));
138
+ const key = new THREE.DirectionalLight(0xffffff, 2.2);
139
+ key.position.set(3, 5, 4);
140
+ scene.add(key);
141
+ }
142
+ if (showGrid) {
143
+ const grid = new THREE.GridHelper(10, 20, 0x51606f, 0x343b44);
144
+ scene.add(grid);
145
+ previewHelpers.push(grid);
146
+ }
147
+
148
+ const fitCamera = (width: number, height: number) => {
149
+ if (!framing) return;
150
+ const requestedDirection = cameraDirection
151
+ ? new THREE.Vector3(...cameraDirection)
152
+ : undefined;
153
+ const fitted = createPerspectiveAssetPreviewCamera(
154
+ framing,
155
+ Math.max(width, 1) / Math.max(height, 1),
156
+ requestedDirection,
157
+ );
158
+ camera.copy(fitted, false);
159
+ controls.target.copy(framing.center);
160
+ controls.update();
161
+ };
162
+
163
+ const install = async (loaded: THREE.Object3D) => {
164
+ if (disposed) return;
165
+ scene.add(loaded);
166
+ let containsSplat = false;
167
+ loaded.traverse((object) => {
168
+ if (hasUserData(object, 'gaussianSplat')) containsSplat = true;
169
+ });
170
+ if (containsSplat) {
171
+ const { SparkRenderer } = await import('@sparkjsdev/spark');
172
+ if (disposed) return;
173
+ sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
174
+ }
175
+ framing = measureAssetPreview(loaded, 'perspective');
176
+ fitCamera(container.clientWidth, container.clientHeight);
177
+ mixer = poseAtFirstFrame(loaded);
178
+ if (showSkeleton) {
179
+ const helper = new THREE.SkeletonHelper(loaded);
180
+ helper.name = '__compact_preview_skeleton';
181
+ scene.add(helper);
182
+ previewHelpers.push(helper);
183
+ }
184
+ extension = setupPreview?.({ scene, camera, renderer, model: loaded });
185
+ extension?.resize?.(
186
+ Math.max(container.clientWidth, 1),
187
+ Math.max(container.clientHeight, 1),
188
+ renderer.getPixelRatio(),
189
+ );
190
+ setLoading(false);
191
+ };
192
+
193
+ const load = async () => {
194
+ try {
195
+ if (sourceFactory) {
196
+ ownedFactory = createOwnedObjectPreviewSnapshot(sourceFactory);
197
+ await install(ownedFactory.root);
198
+ return;
199
+ }
200
+ if (!assetPath) throw new Error('Compact Object3D preview has no source.');
201
+ const format = modelThumbnailFormat(assetPath);
202
+ if (!format) throw new Error(`No native Three.js loader supports ${assetPath}.`);
203
+ const loaded =
204
+ format === 'glb' || format === 'gltf' || format === 'spz'
205
+ ? await loadProjectAssetModel(assetPath, controller.signal)
206
+ : await loadModelThumbnailObject({
207
+ url: assetPath,
208
+ format,
209
+ ...(materialPath ? { materialUrl: materialPath } : {}),
210
+ });
211
+ ownedModel = createModelPreviewSource(loaded);
212
+ if (controller.signal.aborted || disposed) {
213
+ disposeProjectAssetModel(ownedModel);
214
+ ownedModel = null;
215
+ return;
216
+ }
217
+ await install(ownedModel);
218
+ } catch (caught) {
219
+ if (!controller.signal.aborted && !disposed) {
220
+ setError(caught instanceof Error ? caught.message : String(caught));
221
+ setLoading(false);
222
+ }
223
+ }
224
+ };
225
+ void load();
226
+
227
+ const resize = () => {
228
+ const width = Math.max(container.clientWidth, 1);
229
+ const height = Math.max(container.clientHeight, 1);
230
+ renderer.setSize(width, height, false);
231
+ if (framing && !extension) fitCamera(width, height);
232
+ else {
233
+ camera.aspect = width / height;
234
+ camera.updateProjectionMatrix();
235
+ }
236
+ extension?.resize?.(width, height, renderer.getPixelRatio());
237
+ };
238
+ const resizeObserver = new ResizeObserver(resize);
239
+ resizeObserver.observe(container);
240
+ resize();
241
+
242
+ const render = () => {
243
+ frame = 0;
244
+ if (disposed || !isEditorPresentationActive()) return;
245
+ // PRESENTATION ONLY. The mixer is never advanced here — see
246
+ // `poseAtFirstFrame`; `delta` drives the project's own presentation
247
+ // extension and the orbit damping, both editor-owned.
248
+ const delta = Math.min(clock.getDelta(), 0.1);
249
+ controls.update();
250
+ if (sparkRenderer) scene.add(sparkRenderer);
251
+ try {
252
+ if (extension?.render) extension.render(delta);
253
+ else renderer.render(scene, camera);
254
+ } finally {
255
+ sparkRenderer?.removeFromParent();
256
+ }
257
+ frame = requestAnimationFrame(render);
258
+ };
259
+ const unsubscribeActivity = subscribeEditorPresentationActivity(() => {
260
+ if (!isEditorPresentationActive()) {
261
+ cancelAnimationFrame(frame);
262
+ frame = 0;
263
+ } else if (!disposed && frame === 0) {
264
+ clock.getDelta();
265
+ render();
266
+ }
267
+ });
268
+ if (isEditorPresentationActive()) frame = requestAnimationFrame(render);
269
+
270
+ return () => {
271
+ disposed = true;
272
+ unsubscribeActivity();
273
+ unregisterDesignTimeSurface();
274
+ controller.abort(new Error('Compact Object3D preview closed.'));
275
+ cancelAnimationFrame(frame);
276
+ resizeObserver.disconnect();
277
+ mixer?.stopAllAction();
278
+ extension?.dispose();
279
+ controls.dispose();
280
+ if (sparkRenderer) {
281
+ sparkRenderer.removeFromParent();
282
+ disposeSparkRendererWhenIdle(sparkRenderer);
283
+ sparkRenderer = null;
284
+ }
285
+ renderer.dispose();
286
+ renderer.forceContextLoss();
287
+ for (const helper of previewHelpers) {
288
+ const drawable = helper as THREE.Line;
289
+ drawable.geometry?.dispose();
290
+ const material = drawable.material;
291
+ if (Array.isArray(material)) {
292
+ for (const entry of material) entry.dispose();
293
+ } else material?.dispose();
294
+ }
295
+ if (ownedFactory) ownedFactory.dispose();
296
+ if (ownedModel) disposeProjectAssetModel(ownedModel);
297
+ if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
298
+ releaseResizeObserver();
299
+ releaseAnimationFrame();
300
+ releaseContext();
301
+ };
302
+ }, [
303
+ active,
304
+ assetPath,
305
+ background,
306
+ cameraDirection,
307
+ exposure,
308
+ materialPath,
309
+ setupPreview,
310
+ showGrid,
311
+ showSkeleton,
312
+ sourceFactory,
313
+ useDefaultLighting,
314
+ ]);
315
+
316
+ return (
317
+ <div
318
+ ref={containerRef}
319
+ data-testid="object3d-compact-preview"
320
+ role="region"
321
+ aria-label={`Interactive 3D preview: ${displayName}`}
322
+ aria-busy={loading}
323
+ style={{
324
+ width: '100%',
325
+ height: '100%',
326
+ minHeight: 220,
327
+ overflow: 'hidden',
328
+ position: 'relative',
329
+ background: themeVars.surface.inset,
330
+ }}
331
+ >
332
+ {loading && !error ? (
333
+ <span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
334
+ Loading…
335
+ </span>
336
+ ) : null}
337
+ {!active ? (
338
+ <span style={{ position: 'absolute', padding: 10, color: themeVars.content.muted }}>
339
+ Preview suspended while inactive.
340
+ </span>
341
+ ) : null}
342
+ {error ? (
343
+ <div
344
+ role="alert"
345
+ style={{ position: 'absolute', padding: 10, color: themeVars.semantic.danger }}
346
+ >
347
+ {error}
348
+ </div>
349
+ ) : null}
350
+ </div>
351
+ );
352
+ }
353
+
354
+ import type { SparkRenderer } from '@sparkjsdev/spark';
355
+ import { hasUserData } from '@volter/editor-threejs/ecs/user-data';
356
+ import { disposeSparkRendererWhenIdle } from '@volter/editor-threejs/render/spark-renderer-lifecycle';