@volter/editor-threejs 0.5.211 → 0.5.212
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/NOTICE +7 -2
- package/README.md +8 -13
- package/contributions/animation-mixers.service.ts +2 -4
- package/contributions/animation-timeline.utility.tsx +1 -2
- package/contributions/three.page.ts +50 -0
- package/dist-node/serving.mjs +84 -3
- package/dist-node/shared-three.mjs +145 -0
- package/package.json +296 -16
- package/serving/animation-stamp.ts +1 -1
- package/serving/index.ts +22 -5
- package/serving/model-import-conversion.ts +6 -3
- package/serving/shared-three.ts +176 -0
- package/src/adapter/constraint.ts +1 -18
- package/src/adapter/object3d-authoring-subject.ts +1 -12
- package/src/adapter/reflection-probe.ts +1 -9
- package/src/adapter/three-contract.ts +6 -60
- package/src/animation/live-mixers.ts +0 -4
- package/src/api.ts +7 -0
- package/src/asset-loaders.ts +2 -178
- package/src/capture/photograph-sampling.ts +333 -0
- package/src/capture/scene.ts +1 -1
- package/src/ecs/object-marks.ts +6 -14
- package/src/ecs/user-data.ts +3 -138
- package/src/kit/animation/three-clips-subject.ts +19 -42
- package/src/kit/asset-compare-core.ts +161 -0
- package/src/kit/asset-compare.ts +8 -27
- package/src/kit/asset-preview-command.ts +14 -19
- package/src/kit/asset-preview-framing.ts +8 -67
- package/src/kit/asset-preview.ts +109 -342
- package/src/kit/asset-workflow/model-inspection.ts +11 -154
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
- package/src/kit/authoring/component-instance-root.ts +3 -39
- package/src/kit/authoring/live-object-transform.ts +1 -30
- package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
- package/src/kit/authoring/object3d-document-session.ts +471 -208
- package/src/kit/authoring/shell-viewport-policy.ts +18 -24
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
- package/src/kit/authoring/three-projection-core.ts +11 -40
- package/src/kit/authoring/viewport-pick-context.ts +5 -12
- package/src/kit/authoring/viewport-raycast.ts +15 -93
- package/src/kit/authoring/world-hidden-viewport.ts +8 -61
- package/src/kit/capture-camera-pose.ts +68 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/components/CameraInfo.tsx +4 -6
- package/src/kit/components/InspectorObjectPreview.tsx +3 -30
- package/src/kit/components/Object3DDocumentToolbar.css +75 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
- package/src/kit/components/Object3DDocumentViewport.tsx +5 -8
- package/src/kit/components/StageHost.css +40 -0
- package/src/kit/components/StageHost.tsx +181 -404
- package/src/kit/components/StageOverlays.tsx +1 -2
- package/src/kit/components/StatsOverlay.tsx +5 -17
- package/src/kit/components/ViewportFurniture.tsx +27 -154
- package/src/kit/components/ViewportOverlay.tsx +13 -17
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/ViewportShadingMenu.tsx +2 -42
- package/src/kit/components/ViewportViewMenu.tsx +7 -18
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +4 -13
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +5 -5
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +24 -102
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +6 -6
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +10 -45
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +2 -2
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +11 -10
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
- package/src/kit/components/object3d-contribution-surfaces.tsx +1 -1
- package/src/kit/components/stage-keyboard.tsx +6 -12
- package/src/kit/components/stage-overlay-set.tsx +11 -20
- package/src/kit/components/stage-presence-markers.ts +9 -32
- package/src/kit/components/stage-transform-chrome.ts +4 -4
- package/src/kit/components/stage-transform-tools.tsx +7 -14
- package/src/kit/components/standard-viewport-dressing.ts +47 -167
- package/src/kit/components/world-root-binding.ts +2 -7
- package/src/kit/constraint-helper.ts +1 -1
- package/src/kit/document-renderer-session.ts +114 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-shell-store.ts +31 -403
- package/src/kit/editor-viewport.ts +476 -1762
- package/src/kit/entity-lod.ts +3 -16
- package/src/kit/entity-object.ts +1 -39
- package/src/kit/hierarchy-mark-reader.ts +13 -30
- package/src/kit/instanced-presentation.ts +1 -47
- package/src/kit/live-gesture-lock.ts +35 -0
- package/src/kit/live-module-source.ts +12 -52
- package/src/kit/model-thumbnail.ts +7 -35
- package/src/kit/play-camera-flight.ts +7 -16
- package/src/kit/projection/three.ts +13 -366
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-viewport.ts +3 -3
- package/src/kit/scene-framing.ts +11 -152
- package/src/kit/scene-view-fog.ts +2 -47
- package/src/kit/spatial-handle-visuals.ts +19 -28
- package/src/kit/stories/three-story-model.ts +5 -33
- package/src/kit/three-canvas-render.ts +3 -4
- package/src/kit/three-hierarchy-row-media.ts +3 -11
- package/src/kit/three-inspection-media.ts +6 -6
- package/src/kit/three-integration.ts +18 -18
- package/src/kit/three-state.ts +2 -19
- package/src/kit/three-viewport/camera-fit.ts +2 -6
- package/src/kit/three-viewport/selection-brackets.ts +5 -20
- package/src/kit/three-viewport/selection-outline.ts +4 -56
- package/src/kit/three-viewport/skeleton-helper.ts +1 -32
- package/src/kit/three-viewport/source-color.ts +1 -33
- package/src/kit/three-viewport/studio-environment.ts +4 -55
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/transport-strip.tsx +147 -0
- package/src/kit/viewport-actions.ts +8 -50
- package/src/kit/viewport-authoring-policy.ts +5 -52
- package/src/kit/viewport-commands.ts +28 -57
- package/src/kit/viewport-hotkeys.ts +29 -31
- package/src/kit/viewport-status-facet.ts +4 -5
- package/src/kit/viewport-surface-status.tsx +44 -0
- package/src/loader.ts +8 -71
- package/src/object3d-contributions.ts +18 -34
- package/src/viewport/clip-planes.ts +4 -32
- package/src/viewport/content-bounds.ts +13 -142
- package/src/viewport/editor-layers.ts +4 -26
- package/src/viewport/environment.ts +1 -1
- package/src/viewport/preview-renderer.ts +7 -45
- package/src/viewport/renderer-ownership.ts +14 -62
- package/src/viewport-api.ts +14 -33
- package/src/viewport-door.ts +16 -86
- package/contributions/three-integration.service.ts +0 -13
- package/src/adapter/hierarchy-marks.ts +0 -156
- package/src/adapter/ingest/scene-capture.ts +0 -843
- package/src/adapter/ingest/structural-ids.ts +0 -139
- package/src/adapter/ingest/visible-capture-window.ts +0 -302
- package/src/adapter/renderer-config.ts +0 -115
- package/src/asset-parse-error.ts +0 -29
- package/src/host-hierarchy-objects.ts +0 -31
- package/src/kit/authoring/design-time-settle.ts +0 -343
- package/src/kit/camera-authoring.ts +0 -175
package/src/kit/asset-preview.ts
CHANGED
|
@@ -13,12 +13,11 @@ import type {
|
|
|
13
13
|
} from '@volter/sdk';
|
|
14
14
|
import { EDITOR_LAYER, isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
|
|
15
15
|
import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
|
|
16
|
-
import { isBuiltInternal } from '@volter/editor-threejs/adapter/hierarchy-marks';
|
|
17
16
|
import { loadSplat } from '@volter/editor-threejs/asset-loaders';
|
|
18
17
|
import { hasUserData } from '@volter/editor-threejs/ecs/user-data';
|
|
19
18
|
import { gltfLoader } from '@volter/editor-threejs/loader';
|
|
20
19
|
import * as THREE from 'three';
|
|
21
|
-
import { forwardYawRadians, parseForwardVector } from '
|
|
20
|
+
import { forwardYawRadians, parseForwardVector } from './asset-compare-core';
|
|
22
21
|
import {
|
|
23
22
|
ASSET_PREVIEW_PADDING,
|
|
24
23
|
boneZoomCenter,
|
|
@@ -39,37 +38,22 @@ import {
|
|
|
39
38
|
turntableViewBasis,
|
|
40
39
|
unionOrthographicFrames,
|
|
41
40
|
} from './asset-preview-framing';
|
|
42
|
-
import { drawBitmapLabel, measureBitmapLabel } from '@volter/sdk/
|
|
41
|
+
import { drawBitmapLabel, measureBitmapLabel } from '@volter/sdk/bitmap-label';
|
|
43
42
|
import {
|
|
44
43
|
applyStudioEnvironment,
|
|
45
44
|
disposeStudioEnvironment,
|
|
46
45
|
STUDIO_AMBIENT_WITH_ENVIRONMENT,
|
|
47
46
|
} from './three-viewport/studio-environment';
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const ASSET_PREVIEW_VIEWS = ['front', 'right', 'top', 'perspective'] as const;
|
|
49
|
+
type AssetPreviewView = (typeof ASSET_PREVIEW_VIEWS)[number];
|
|
51
50
|
export type AssetPreviewBackground = 'neutral' | 'transparent';
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
* Where an ENTITY capture is staged.
|
|
55
|
-
*
|
|
56
|
-
* `'lab'` — the neutral Asset Lab stage {@link captureObjectAssetPreview}
|
|
57
|
-
* renders: an isolated, yaw-normalized snapshot under fixed studio lighting,
|
|
58
|
-
* identical whatever the entity's surroundings are. It is the default
|
|
59
|
-
* everywhere.
|
|
60
|
-
*
|
|
61
|
-
* `'scene'` — {@link captureSceneStageAssetPreview}: the entity photographed
|
|
62
|
-
* WHERE IT STANDS, in the live scene, under the scene's own lighting, with
|
|
63
|
-
* the editor's own furniture excluded. Entity captures only — a model loaded
|
|
64
|
-
* from an asset path does not stand anywhere.
|
|
65
|
-
*/
|
|
66
|
-
export type AssetPreviewStage = 'lab' | 'scene';
|
|
67
|
-
|
|
68
|
-
export interface AssetPreviewOptions {
|
|
52
|
+
interface AssetPreviewOptions {
|
|
69
53
|
width?: number;
|
|
70
54
|
height?: number;
|
|
71
55
|
background?: AssetPreviewBackground;
|
|
72
|
-
/** ONE view from a chosen angle instead of the fixed
|
|
56
|
+
/** ONE view from a chosen angle instead of the fixed view sheet — see the SDK's
|
|
73
57
|
* {@link AssetPreviewCameraChoice} for the angle conventions. */
|
|
74
58
|
camera?: AssetPreviewCameraChoice;
|
|
75
59
|
/** Sample a named clip at a time on the capture's disposable snapshot
|
|
@@ -77,7 +61,7 @@ export interface AssetPreviewOptions {
|
|
|
77
61
|
pose?: AssetPreviewPose;
|
|
78
62
|
}
|
|
79
63
|
|
|
80
|
-
|
|
64
|
+
interface AssetPreviewImage {
|
|
81
65
|
/** One of the four fixed views, or `below` — the underside tile the lab
|
|
82
66
|
* stage adds to its sheet (a cap's gills, a barrel's floor). */
|
|
83
67
|
view: AssetPreviewView | 'below';
|
|
@@ -85,26 +69,15 @@ export interface AssetPreviewImage {
|
|
|
85
69
|
mimeType: 'image/png';
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
*
|
|
91
|
-
* `faceFrontSubject` yaw-normalizes a subject so the fixed front camera
|
|
92
|
-
* photographs its declared front. For a rig persisting `forward = [0,0,-1]`
|
|
93
|
-
* that is a PI yaw — correct photography whose projection maps authored +X to
|
|
94
|
-
* screen-LEFT in the front view. Applied silently, that read as "the front
|
|
95
|
-
* view mirrors world X" and cost a cold-round agent its geometry reasoning
|
|
96
|
-
* (round #4, 2026-08-30), so the yaw is now REPORTED here and stamped onto
|
|
97
|
-
* the sheet's own pixels ({@link renderFourViewCapture}'s axis markers) —
|
|
98
|
-
* never inferred, never silent.
|
|
99
|
-
*/
|
|
100
|
-
export interface AssetPreviewOrientation {
|
|
72
|
+
/** The yaw applied to align the captured subject’s authored forward with the front camera, also printed on the sheet’s axis markers. */
|
|
73
|
+
interface AssetPreviewOrientation {
|
|
101
74
|
/** The subject's declared forward, `[0,0,1]` when it declares none. */
|
|
102
75
|
forward: [number, number, number];
|
|
103
76
|
/** Yaw applied to face the front camera; 0 means authored axes = world axes. */
|
|
104
77
|
yawDegrees: number;
|
|
105
78
|
}
|
|
106
79
|
|
|
107
|
-
|
|
80
|
+
interface AssetPreviewCapture {
|
|
108
81
|
width: number;
|
|
109
82
|
height: number;
|
|
110
83
|
orientation: AssetPreviewOrientation;
|
|
@@ -117,33 +90,12 @@ export interface AssetPreviewCapture {
|
|
|
117
90
|
};
|
|
118
91
|
}
|
|
119
92
|
|
|
120
|
-
|
|
93
|
+
interface AssetPreviewFraming {
|
|
121
94
|
center: THREE.Vector3;
|
|
122
95
|
size: THREE.Vector3;
|
|
123
96
|
radius: number;
|
|
124
97
|
}
|
|
125
98
|
|
|
126
|
-
/**
|
|
127
|
-
* The shot-set contract is declared ONCE, in `@volter/sdk`
|
|
128
|
-
* (`packages/sdk/src/types.ts`), because it crosses the editor relay:
|
|
129
|
-
* a capability tool authors a set, the CLI carries it over, and THIS module is
|
|
130
|
-
* the generic capture engine that renders it. Importing rather than
|
|
131
|
-
* re-declaring is what makes `parseShotSetDefinition`'s return annotation
|
|
132
|
-
* below a real drift check against the wire type — see the SDK block's own
|
|
133
|
-
* comment for the contract and for what a pose step's `radians` is measured
|
|
134
|
-
* from.
|
|
135
|
-
*/
|
|
136
|
-
export type {
|
|
137
|
-
AssetPreviewShotSetDefinition,
|
|
138
|
-
AssetPreviewShotWarning,
|
|
139
|
-
LabeledShotSetCapture,
|
|
140
|
-
ShotSetPoseMorph,
|
|
141
|
-
ShotSetPoseRotation,
|
|
142
|
-
ShotSetPoseStep,
|
|
143
|
-
ShotSetPoseTranslation,
|
|
144
|
-
ShotSetShot,
|
|
145
|
-
} from '@volter/sdk';
|
|
146
|
-
|
|
147
99
|
/** Definitions cross the editor relay as untyped JSON from project-registered
|
|
148
100
|
* tools; capped so one capture stays within the relay's request budget. */
|
|
149
101
|
const MAX_SHOT_SET_SHOTS = 24;
|
|
@@ -259,8 +211,8 @@ export function parseShotSetDefinition(value: unknown): AssetPreviewShotSetDefin
|
|
|
259
211
|
for (const [poseName, steps] of Object.entries(poses)) {
|
|
260
212
|
if (!Array.isArray(steps) || !steps.every(isPoseStep)) {
|
|
261
213
|
fail(
|
|
262
|
-
`pose ${JSON.stringify(poseName)} must be an array of {bone, axis, radians} ` +
|
|
263
|
-
'and/or {morph, influence}.',
|
|
214
|
+
`pose ${JSON.stringify(poseName)} must be an array of {bone, axis, radians}, ` +
|
|
215
|
+
'{bone, axis, meters}, and/or {morph, influence}.',
|
|
264
216
|
);
|
|
265
217
|
}
|
|
266
218
|
}
|
|
@@ -289,17 +241,15 @@ export function parseShotSetDefinition(value: unknown): AssetPreviewShotSetDefin
|
|
|
289
241
|
return definition as AssetPreviewShotSetDefinition;
|
|
290
242
|
}
|
|
291
243
|
|
|
292
|
-
|
|
293
|
-
'front'
|
|
294
|
-
'back'
|
|
295
|
-
'left'
|
|
296
|
-
'right'
|
|
297
|
-
'quarter-left'
|
|
298
|
-
'quarter-right'
|
|
299
|
-
] as const;
|
|
300
|
-
export type SourceReviewShotLabel = (typeof SOURCE_REVIEW_SHOT_LABELS)[number];
|
|
244
|
+
type SourceReviewShotLabel =
|
|
245
|
+
| 'front'
|
|
246
|
+
| 'back'
|
|
247
|
+
| 'left'
|
|
248
|
+
| 'right'
|
|
249
|
+
| 'quarter-left'
|
|
250
|
+
| 'quarter-right';
|
|
301
251
|
|
|
302
|
-
|
|
252
|
+
interface SourceReviewShotSetCapture {
|
|
303
253
|
width: number;
|
|
304
254
|
height: number;
|
|
305
255
|
shots: Array<{ label: SourceReviewShotLabel; base64: string; mimeType: 'image/png' }>;
|
|
@@ -318,8 +268,6 @@ type MorphableObject = THREE.Object3D & {
|
|
|
318
268
|
|
|
319
269
|
const DEFAULT_SIZE = 512;
|
|
320
270
|
const MIN_SIZE = 64;
|
|
321
|
-
// Four views plus the 2x2 sheet cross the editor relay as base64 JSON. At
|
|
322
|
-
// 1024px even incompressible RGBA data remains below its 50 MB request limit.
|
|
323
271
|
const MAX_SIZE = 1024;
|
|
324
272
|
const NEUTRAL_COLOR = 0x20242a;
|
|
325
273
|
const MODEL_FETCH_TIMEOUT_MS = 25_000;
|
|
@@ -335,21 +283,12 @@ export function checkedDimension(value: number | undefined): number {
|
|
|
335
283
|
|
|
336
284
|
export function pngBase64(canvas: HTMLCanvasElement): string {
|
|
337
285
|
const dataUrl = canvas.toDataURL('image/png');
|
|
338
|
-
|
|
339
|
-
return comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
|
286
|
+
return dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
340
287
|
}
|
|
341
288
|
|
|
342
289
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* both the mesh and every bone, the skeleton root the refusal in
|
|
346
|
-
* `resolveExternalBones` names. A skinned mesh sits BESIDE its Armature in
|
|
347
|
-
* three's own glTF layout (`Scene > [SkinnedMesh, Armature > bones]`), so a
|
|
348
|
-
* click on a character lands on the mesh, and the mesh alone cannot be drawn
|
|
349
|
-
* without the bones that pose it: every Humanoid exhibit's Inspector preview
|
|
350
|
-
* was the red refusal listing all 68 bones (runhuman pass 143; traced live —
|
|
351
|
-
* selecting the rig root renders a clean bind pose). A bone whose chain never
|
|
352
|
-
* meets `object`'s stays external, so the honest refusal still fires there.
|
|
290
|
+
* Choose an ancestor containing the selected object and its referenced bones where their
|
|
291
|
+
* hierarchies meet. Unrelated bones remain external for resolveExternalBones to refuse.
|
|
353
292
|
*/
|
|
354
293
|
export function assetPreviewSubject(object: THREE.Object3D): THREE.Object3D {
|
|
355
294
|
const inside = new Set<THREE.Object3D>();
|
|
@@ -386,14 +325,10 @@ export function assetPreviewSubject(object: THREE.Object3D): THREE.Object3D {
|
|
|
386
325
|
* snapshot shares immutable geometry/material resources.
|
|
387
326
|
*/
|
|
388
327
|
export function createAssetPreviewSnapshot(source: THREE.Object3D): THREE.Object3D {
|
|
389
|
-
// A skinned mesh can
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// owns a same-named bone for each of them (it does for exactly that mount),
|
|
394
|
-
// the snapshot binds to its own copies — the pose those copies carry is the
|
|
395
|
-
// one the mount intended. Anything left unresolved is refused BY NAME below,
|
|
396
|
-
// because binding to a bone that is not there would be an invented pose.
|
|
328
|
+
// A skinned mesh can reference bones outside the selected subtree. Resolve
|
|
329
|
+
// each reference to an unambiguous same-named bone owned by the subtree, then
|
|
330
|
+
// bind the snapshot to its own copy of that pose. Unresolved references are
|
|
331
|
+
// refused by name rather than inventing a pose from absent bones.
|
|
397
332
|
const sourceNodes = new Set<THREE.Object3D>();
|
|
398
333
|
source.traverse((object) => {
|
|
399
334
|
sourceNodes.add(object);
|
|
@@ -415,13 +350,6 @@ export function createAssetPreviewSnapshot(source: THREE.Object3D): THREE.Object
|
|
|
415
350
|
// scene; descendant visibility remains authored and the source is untouched.
|
|
416
351
|
snapshot.visible = true;
|
|
417
352
|
|
|
418
|
-
// Editor-only helpers are presentation siblings/children, never asset data.
|
|
419
|
-
const helpers: THREE.Object3D[] = [];
|
|
420
|
-
snapshot.traverse((object) => {
|
|
421
|
-
if (object !== snapshot && object.userData['editorHelper']) helpers.push(object);
|
|
422
|
-
});
|
|
423
|
-
for (const helper of helpers) helper.parent?.remove(helper);
|
|
424
|
-
|
|
425
353
|
return snapshot;
|
|
426
354
|
}
|
|
427
355
|
|
|
@@ -446,18 +374,7 @@ function objectWorldMatrix(object: THREE.Object3D): THREE.Matrix4 {
|
|
|
446
374
|
return world;
|
|
447
375
|
}
|
|
448
376
|
|
|
449
|
-
/**
|
|
450
|
-
* For every bone a skinned mesh in `source` references from OUTSIDE `source`,
|
|
451
|
-
* the same-named bone inside it — or a thrown refusal naming what could not be
|
|
452
|
-
* resolved. An empty map means the subtree's skeletons are already complete.
|
|
453
|
-
*
|
|
454
|
-
* The unresolvable case is real (a bone with no name, or one whose owner truly
|
|
455
|
-
* lives above the selection), and it stays an error: the alternative is a
|
|
456
|
-
* picture of a mesh bound to bones the caller never selected. What changed is
|
|
457
|
-
* that the error now says WHICH mesh and WHICH bones, because "Select the
|
|
458
|
-
* complete skeleton root" with nothing named is a dead end — there is no way
|
|
459
|
-
* to act on it from the terminal that produced it.
|
|
460
|
-
*/
|
|
377
|
+
/** Resolve every external skeleton reference to a same-named bone inside source. Return an empty map for a complete subtree; refuse with mesh and bone names when a reference cannot resolve. */
|
|
461
378
|
function resolveExternalBones(
|
|
462
379
|
source: THREE.Object3D,
|
|
463
380
|
sourceNodes: ReadonlySet<THREE.Object3D>,
|
|
@@ -485,14 +402,8 @@ function resolveExternalBones(
|
|
|
485
402
|
const meshName = skinned.name || skinned.uuid;
|
|
486
403
|
for (const bone of skinned.skeleton.bones) {
|
|
487
404
|
if (bone && sourceNodes.has(bone)) continue;
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
// GLTFLoader gives every primitive of one skin a SkinnedMesh bound to the
|
|
491
|
-
// SAME Skeleton, which is the ordinary body+outfit character. Reading
|
|
492
|
-
// this as "already handled, nothing to do here" and falling through to
|
|
493
|
-
// the unresolved ledger refused exactly those models, with a sentence
|
|
494
|
-
// ("owns no bone of the same name") that the first mesh had just
|
|
495
|
-
// disproved.
|
|
405
|
+
// Bone substitution is shared across meshes referencing the same bone object, including glTF
|
|
406
|
+
// primitives bound to one skeleton.
|
|
496
407
|
if (bone && substitutes.has(bone)) continue;
|
|
497
408
|
const candidates = bone ? (nameIndex().get(bone.name) ?? []) : [];
|
|
498
409
|
if (candidates.length === 1) {
|
|
@@ -509,10 +420,7 @@ function resolveExternalBones(
|
|
|
509
420
|
});
|
|
510
421
|
|
|
511
422
|
if (unresolved.size > 0) {
|
|
512
|
-
//
|
|
513
|
-
// bind to, versus several and no way to tell which was meant. Reporting
|
|
514
|
-
// one as the other is how an un-actionable error survives a fix that was
|
|
515
|
-
// supposed to remove it.
|
|
423
|
+
// Distinguish a missing same-named bone from several ambiguous candidates in the refusal.
|
|
516
424
|
const detail = [...unresolved.values()]
|
|
517
425
|
.map(({ mesh, bone, sameNamed }) =>
|
|
518
426
|
sameNamed > 1
|
|
@@ -543,7 +451,7 @@ function resolveExternalBones(
|
|
|
543
451
|
*/
|
|
544
452
|
function cloneRenderableHierarchy(
|
|
545
453
|
source: THREE.Object3D,
|
|
546
|
-
boneSubstitutes: ReadonlyMap<THREE.Bone, THREE.Bone
|
|
454
|
+
boneSubstitutes: ReadonlyMap<THREE.Bone, THREE.Bone>,
|
|
547
455
|
): THREE.Object3D {
|
|
548
456
|
const cloneBySource = new Map<THREE.Object3D, THREE.Object3D>();
|
|
549
457
|
const skinnedPairs: Array<[THREE.SkinnedMesh, THREE.SkinnedMesh]> = [];
|
|
@@ -760,7 +668,7 @@ export function createOwnedObjectPreviewSnapshot(
|
|
|
760
668
|
|
|
761
669
|
export function measureAssetPreview(
|
|
762
670
|
object: THREE.Object3D,
|
|
763
|
-
view: AssetPreviewView
|
|
671
|
+
view: AssetPreviewView,
|
|
764
672
|
): AssetPreviewFraming {
|
|
765
673
|
resolveWorldMatricesLikeRenderer(object);
|
|
766
674
|
const box = readOnlyRenderableBounds(object, view);
|
|
@@ -774,33 +682,7 @@ export function measureAssetPreview(
|
|
|
774
682
|
};
|
|
775
683
|
}
|
|
776
684
|
|
|
777
|
-
/**
|
|
778
|
-
* Bring a subtree's world matrices to exactly the state the RENDERER will
|
|
779
|
-
* compute for it, then measure from that.
|
|
780
|
-
*
|
|
781
|
-
* This is not interchangeable with `updateWorldMatrix`, and the difference
|
|
782
|
-
* is measurable rather than stylistic: `SkinnedMesh` OVERRIDES
|
|
783
|
-
* `updateMatrixWorld` to refresh `bindMatrixInverse` from its current world
|
|
784
|
-
* matrix (attached bind mode), and `getVertexPosition` — how this module
|
|
785
|
-
* reads skinned geometry — runs the vertex through
|
|
786
|
-
* `bindMatrixInverse * boneWorld * boneInverse * bindMatrix`. Measure after
|
|
787
|
-
* `updateWorldMatrix` alone and `bindMatrixInverse` is still the value from
|
|
788
|
-
* before the subject was re-parented (for example under
|
|
789
|
-
* {@link faceFrontSubject}'s yaw pivot), so the mesh's own transform gets
|
|
790
|
-
* applied TWICE to every skinned vertex while the render applies it once.
|
|
791
|
-
*
|
|
792
|
-
* A yaw pivot of PI (our generated rigs persist `forward = [0,0,-1]`)
|
|
793
|
-
* therefore mirrored the measured bounds in Z: the size stayed right and the
|
|
794
|
-
* CENTRE flipped sign, which is invisible when the camera looks down Z
|
|
795
|
-
* (front/back) and pushes the subject bodily out of frame when it looks
|
|
796
|
-
* down X (left/right). That is the whole of the "long body runs off the
|
|
797
|
-
* frame edge at yaw +/-PI/2" defect.
|
|
798
|
-
*
|
|
799
|
-
* `updateMatrixWorld(force)` is used deliberately in preference to
|
|
800
|
-
* `updateWorldMatrix`: it honours `matrixWorldAutoUpdate` exactly as the
|
|
801
|
-
* renderer's own `scene.updateMatrixWorld()` does, so a snapshot that pins
|
|
802
|
-
* an exact composed matrix is measured the way it is drawn.
|
|
803
|
-
*/
|
|
685
|
+
/** Update matrices through the renderer’s updateMatrixWorld path before measuring, including SkinnedMesh.bindMatrixInverse and matrixWorldAutoUpdate. */
|
|
804
686
|
function resolveWorldMatricesLikeRenderer(object: THREE.Object3D): void {
|
|
805
687
|
object.parent?.updateWorldMatrix(true, false);
|
|
806
688
|
object.updateMatrixWorld(true);
|
|
@@ -832,7 +714,6 @@ function measureProjectedSpans(
|
|
|
832
714
|
bases: readonly OrthographicViewBasis[],
|
|
833
715
|
): ProjectedSpan[] {
|
|
834
716
|
const spans = bases.map(() => createProjectedSpan());
|
|
835
|
-
if (bases.length === 0) return spans;
|
|
836
717
|
resolveWorldMatricesLikeRenderer(root);
|
|
837
718
|
forEachRenderableVertex(root, bases[0]!, false, (point) => {
|
|
838
719
|
for (let index = 0; index < bases.length; index++) {
|
|
@@ -867,7 +748,6 @@ function measureShotFrameCoverage(
|
|
|
867
748
|
): { framed: number[]; total: number } {
|
|
868
749
|
const framed = frames.map(() => 0);
|
|
869
750
|
let total = 0;
|
|
870
|
-
if (frames.length === 0) return { framed, total };
|
|
871
751
|
resolveWorldMatricesLikeRenderer(subject);
|
|
872
752
|
const spans = frames.map(() => createProjectedSpan());
|
|
873
753
|
let open = -1;
|
|
@@ -918,14 +798,6 @@ function forEachRenderableVertex(
|
|
|
918
798
|
let primitive = 0;
|
|
919
799
|
const visit = (object: THREE.Object3D): void => {
|
|
920
800
|
if (!object.visible) return;
|
|
921
|
-
// A live scene subject (`captureSceneStageAssetPreview`) carries its game's
|
|
922
|
-
// implementation as real children — a world-space particle renderer at
|
|
923
|
-
// identity, a pooled batch — and framing on those puts the subject in a
|
|
924
|
-
// corner of its own portrait. Subtree-scoped, same rule as the selection
|
|
925
|
-
// cage (`content-bounds.ts`); a loaded model file carries no marks, so the
|
|
926
|
-
// asset lanes are unaffected. The ROOT is always measured: a caller who
|
|
927
|
-
// named it meant it.
|
|
928
|
-
if (object !== root && isBuiltInternal(object)) return;
|
|
929
801
|
if (hasUserData(object, 'gaussianSplat')) {
|
|
930
802
|
splatBounds.copy((object as SplatMesh).getBoundingBox()).applyMatrix4(object.matrixWorld);
|
|
931
803
|
const splatPrimitive = primitive++;
|
|
@@ -1108,12 +980,7 @@ function orthographicCamera(
|
|
|
1108
980
|
|
|
1109
981
|
if (view === 'right') {
|
|
1110
982
|
horizontal = size.z;
|
|
1111
|
-
} else
|
|
1112
|
-
if (view === 'top') {
|
|
1113
|
-
horizontal = size.x;
|
|
1114
|
-
vertical = size.z;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
983
|
+
} else if (view === 'top') vertical = size.z;
|
|
1117
984
|
|
|
1118
985
|
const frame = fitOrthographicFrame(horizontal / 2, vertical / 2, aspect);
|
|
1119
986
|
return createOrthographicShotCamera(
|
|
@@ -1187,7 +1054,7 @@ function assetViewBasis(view: AssetPreviewView): {
|
|
|
1187
1054
|
return { direction, right, up };
|
|
1188
1055
|
}
|
|
1189
1056
|
|
|
1190
|
-
|
|
1057
|
+
function createAssetPreviewCamera(
|
|
1191
1058
|
view: AssetPreviewView,
|
|
1192
1059
|
framing: AssetPreviewFraming,
|
|
1193
1060
|
aspect: number,
|
|
@@ -1207,22 +1074,7 @@ export function createPerspectiveAssetPreviewCamera(
|
|
|
1207
1074
|
return perspectiveCamera(framing, aspect, direction);
|
|
1208
1075
|
}
|
|
1209
1076
|
|
|
1210
|
-
/**
|
|
1211
|
-
* The turntable cameras for one staged subject, framed from what each yaw
|
|
1212
|
-
* ACTUALLY sees.
|
|
1213
|
-
*
|
|
1214
|
-
* Every yaw of a subject is measured separately (an oriented projection of
|
|
1215
|
-
* the rendered vertices onto that yaw's screen axes), each shot is centred
|
|
1216
|
-
* on its own projection, and the SCALE is the union across the set's yaws —
|
|
1217
|
-
* see {@link unionOrthographicFrames} for why the union rather than a
|
|
1218
|
-
* per-shot exact fit. The depth budget still comes from the subject's own
|
|
1219
|
-
* bounding radius, which no yaw can exceed.
|
|
1220
|
-
*
|
|
1221
|
-
* The predecessor derived every yaw from one world-axis-aligned box, which
|
|
1222
|
-
* both over-framed a bent, elongated subject and — because it centred every
|
|
1223
|
-
* yaw on that box's centre — placed the subject wrong the moment the
|
|
1224
|
-
* measured box and the drawn subject disagreed.
|
|
1225
|
-
*/
|
|
1077
|
+
/** Frame each turntable yaw from its projected rendered vertices, centring each view independently and sharing the union scale. Use the subject radius for depth. */
|
|
1226
1078
|
function turntableCameras(
|
|
1227
1079
|
subject: THREE.Object3D,
|
|
1228
1080
|
framing: AssetPreviewFraming,
|
|
@@ -1250,13 +1102,13 @@ function turntableCameras(
|
|
|
1250
1102
|
return cameras;
|
|
1251
1103
|
}
|
|
1252
1104
|
|
|
1253
|
-
/** Six source-review views aligned to the asset's declared semantic forward
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1105
|
+
/** Six source-review views aligned to the asset's declared semantic forward.
|
|
1106
|
+
* Cardinal views share a scale fitted to projected rendered vertices; quarter
|
|
1107
|
+
* views fit the bounds. No skeleton is required or modified. */
|
|
1256
1108
|
export function captureSourceReviewShotSetAssetPreview(
|
|
1257
1109
|
source: THREE.Object3D,
|
|
1258
1110
|
forward: readonly [number, number, number],
|
|
1259
|
-
options: AssetPreviewOptions
|
|
1111
|
+
options: AssetPreviewOptions,
|
|
1260
1112
|
): SourceReviewShotSetCapture {
|
|
1261
1113
|
const width = checkedDimension(options.width);
|
|
1262
1114
|
const height = checkedDimension(options.height);
|
|
@@ -1361,23 +1213,9 @@ export function captureSourceReviewShotSetAssetPreview(
|
|
|
1361
1213
|
}
|
|
1362
1214
|
|
|
1363
1215
|
/**
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
* goblin-scaled). A single anchor (head, one shoulder) yields a fixed-size
|
|
1368
|
-
* crop around that point; two anchors that are naturally far apart in a
|
|
1369
|
-
* T-pose rig (both hands, both feet) still get a camera wide/tall enough to
|
|
1370
|
-
* hold both — the historical fixed-height-zoom defect the line-up harness's
|
|
1371
|
-
* own `zoom=head` fix (B5.1) already had to correct once, generalized here
|
|
1372
|
-
* to "the crop must actually contain what it claims to frame" for every
|
|
1373
|
-
* region, not just proportional scaling.
|
|
1374
|
-
*
|
|
1375
|
-
* The crop is taken from the shot's own `yaw` (the turntable convention;
|
|
1376
|
-
* omitted means 0, the front camera this used to be hardcoded to). An angle
|
|
1377
|
-
* is not decoration on a long subject: a junction whose axis runs down the
|
|
1378
|
-
* body's length — a tail root, a hip chain, a sail seen edge-on — has the
|
|
1379
|
-
* body itself standing between the front camera and the anchor, so the crop
|
|
1380
|
-
* frames the right point and photographs the wrong thing.
|
|
1216
|
+
* Frame every world-space bone anchor with an orthographic close-up. Scale the margin and minimum
|
|
1217
|
+
* crop size by the whole model’s bounding radius, and expand the crop to contain separated anchors.
|
|
1218
|
+
* Use the shot’s yaw with the turntable convention; omitted yaw selects the front view.
|
|
1381
1219
|
*/
|
|
1382
1220
|
function boneZoomCamera(
|
|
1383
1221
|
anchors: readonly THREE.Vector3[],
|
|
@@ -1398,11 +1236,8 @@ function boneZoomCamera(
|
|
|
1398
1236
|
);
|
|
1399
1237
|
}
|
|
1400
1238
|
|
|
1401
|
-
/** Locate every bone
|
|
1402
|
-
*
|
|
1403
|
-
* which joints are missing, when the model has no rig at all or is missing
|
|
1404
|
-
* a required joint — no silent fallback to a generic bounding-box zoom for
|
|
1405
|
-
* a set that promised skeleton-anchored framing. */
|
|
1239
|
+
/** Locate every required bone on the source's skeleton. Refuse absent rigs
|
|
1240
|
+
* and missing joints by name before framing a skeleton-anchored shot. */
|
|
1406
1241
|
function findRequiredSkeletonBones(
|
|
1407
1242
|
root: THREE.Object3D,
|
|
1408
1243
|
definition: AssetPreviewShotSetDefinition,
|
|
@@ -1417,13 +1252,13 @@ function findRequiredSkeletonBones(
|
|
|
1417
1252
|
const hint = definition.rigRequirementHint ? ` (${definition.rigRequirementHint})` : '';
|
|
1418
1253
|
if (required.length > 0 && bonesByName.size === 0) {
|
|
1419
1254
|
throw new Error(
|
|
1420
|
-
`Asset preview '
|
|
1255
|
+
`Asset preview shot set '${definition.name}' requires a rigged model${hint}, but this ` +
|
|
1421
1256
|
`model has no skeleton at all. Required joints: ${required.join(', ')}.`,
|
|
1422
1257
|
);
|
|
1423
1258
|
}
|
|
1424
1259
|
if (missing.length > 0) {
|
|
1425
1260
|
throw new Error(
|
|
1426
|
-
`Asset preview '
|
|
1261
|
+
`Asset preview shot set '${definition.name}' requires joints this model's skeleton does ` +
|
|
1427
1262
|
`not have${hint}. Missing joint(s): ${missing.join(', ')}.`,
|
|
1428
1263
|
);
|
|
1429
1264
|
}
|
|
@@ -1432,8 +1267,8 @@ function findRequiredSkeletonBones(
|
|
|
1432
1267
|
|
|
1433
1268
|
/**
|
|
1434
1269
|
* Apply one of the definition's named poses to a snapshot in place — bone
|
|
1435
|
-
* rotations on the skeleton, morph influences on
|
|
1436
|
-
* them.
|
|
1270
|
+
* rotations/translations on the skeleton, morph influences on meshes that
|
|
1271
|
+
* declare them.
|
|
1437
1272
|
*
|
|
1438
1273
|
* BOTH halves degrade rather than throw, for the same reason: a pose may
|
|
1439
1274
|
* touch a joint or a morph beyond the definition's `requiredBones` (a rig can
|
|
@@ -1479,9 +1314,7 @@ function applyShotSetPose(
|
|
|
1479
1314
|
});
|
|
1480
1315
|
}
|
|
1481
1316
|
|
|
1482
|
-
/**
|
|
1483
|
-
* the rig root, which sits below the GLTF scene root), else glTF's +Z.
|
|
1484
|
-
* Shared with the compare mode (`asset-compare.ts` re-exports it). */
|
|
1317
|
+
/** Read the first valid `userData.forward` in the subtree, defaulting to glTF's +Z. */
|
|
1485
1318
|
export function readModelForward(root: THREE.Object3D): [number, number, number] {
|
|
1486
1319
|
let found: [number, number, number] | null = null;
|
|
1487
1320
|
root.traverse((object) => {
|
|
@@ -1491,19 +1324,9 @@ export function readModelForward(root: THREE.Object3D): [number, number, number]
|
|
|
1491
1324
|
return found ?? [0, 0, 1];
|
|
1492
1325
|
}
|
|
1493
1326
|
|
|
1494
|
-
/**
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
* model's own forward faces +Z — the direction every 'front' camera looks
|
|
1498
|
-
* from. Models without forward extras keep the glTF +Z convention (yaw 0, an
|
|
1499
|
-
* exact no-op), so arbitrary GLBs render exactly as before; our generated
|
|
1500
|
-
* humanoids persist `userData.forward = [0,0,-1]` and previously had their
|
|
1501
|
-
* BACK photographed by every front-labeled shot.
|
|
1502
|
-
*
|
|
1503
|
-
* `forward` must be read from the SOURCE object (the compare path does the
|
|
1504
|
-
* same): render snapshots deliberately copy presentation state only, so the
|
|
1505
|
-
* forward extras never survive onto the snapshot itself.
|
|
1506
|
-
*/
|
|
1327
|
+
/** Yaw a snapshot so the model’s forward faces the front camera at +Z. Read forward from the
|
|
1328
|
+
* source object because snapshots copy presentation state without project extras. Missing
|
|
1329
|
+
* forward extras use glTF’s +Z convention and need no rotation. */
|
|
1507
1330
|
function faceFrontSubject(
|
|
1508
1331
|
snapshot: THREE.Object3D,
|
|
1509
1332
|
forward: readonly [number, number, number],
|
|
@@ -1528,15 +1351,9 @@ interface StagedShotSubject {
|
|
|
1528
1351
|
}
|
|
1529
1352
|
|
|
1530
1353
|
/**
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* renderer and every caller here builds its scene before its renderer.
|
|
1535
|
-
*
|
|
1536
|
-
* The hemisphere is scaled down by {@link STUDIO_AMBIENT_WITH_ENVIRONMENT}:
|
|
1537
|
-
* with an environment map in the scene it is the second, cruder ambient term,
|
|
1538
|
-
* and at its historical 1.8 the two together flattened the subject. Key and
|
|
1539
|
-
* fill are untouched — they still own the form.
|
|
1354
|
+
* Build the Asset Lab studio scene with hemisphere, key, fill and underside lights.
|
|
1355
|
+
* applyStudioEnvironment adds the renderer-dependent IBL separately. Scale the
|
|
1356
|
+
* hemisphere by STUDIO_AMBIENT_WITH_ENVIRONMENT to balance its ambient term with the IBL.
|
|
1540
1357
|
*/
|
|
1541
1358
|
function createPreviewScene(snapshot: THREE.Object3D, framing: AssetPreviewFraming): THREE.Scene {
|
|
1542
1359
|
const scene = new THREE.Scene();
|
|
@@ -1552,11 +1369,7 @@ function createPreviewScene(snapshot: THREE.Object3D, framing: AssetPreviewFrami
|
|
|
1552
1369
|
.copy(framing.center)
|
|
1553
1370
|
.addScaledVector(new THREE.Vector3(-1, 0.5, -0.7), framing.radius * 2);
|
|
1554
1371
|
scene.add(fill);
|
|
1555
|
-
//
|
|
1556
|
-
// gills under a cap, a barrel's closed bottom) reads as a surface instead
|
|
1557
|
-
// of a black disc. Measured on the blind modeling bench (round 10): an
|
|
1558
|
-
// agent spent three looks and a probe deciding whether an unlit underside
|
|
1559
|
-
// was closed. Tops are unchanged — this light sees only downward faces.
|
|
1372
|
+
// A weak warm fill from below makes downward-facing surfaces visible in underside shots.
|
|
1560
1373
|
const under = new THREE.DirectionalLight(0xfff1dc, 0.45);
|
|
1561
1374
|
under.position
|
|
1562
1375
|
.copy(framing.center)
|
|
@@ -1596,7 +1409,7 @@ if (import.meta.hot) import.meta.hot.dispose(disposeObjectCaptureRenderer);
|
|
|
1596
1409
|
|
|
1597
1410
|
export function captureObjectAssetPreview(
|
|
1598
1411
|
source: THREE.Object3D,
|
|
1599
|
-
options: AssetPreviewOptions
|
|
1412
|
+
options: AssetPreviewOptions,
|
|
1600
1413
|
): AssetPreviewCapture {
|
|
1601
1414
|
let containsSplat = false;
|
|
1602
1415
|
source.traverse((object) => {
|
|
@@ -1657,8 +1470,7 @@ export function captureObjectAssetPreview(
|
|
|
1657
1470
|
createAssetPreviewCamera(view, measureAssetPreview(subject, view), width / height),
|
|
1658
1471
|
);
|
|
1659
1472
|
}
|
|
1660
|
-
//
|
|
1661
|
-
// to take as a separate second capture and write as `below.png`.
|
|
1473
|
+
// Add an underside view from 30° azimuth and −60° elevation.
|
|
1662
1474
|
const below = perspectiveCamera(
|
|
1663
1475
|
framing,
|
|
1664
1476
|
width / height,
|
|
@@ -1725,7 +1537,7 @@ function applyClipPose(snapshot: THREE.Object3D, pose: AssetPreviewPose): void {
|
|
|
1725
1537
|
`Asset preview pose time must be a finite number of seconds >= 0, got ${pose.timeSeconds}.`,
|
|
1726
1538
|
);
|
|
1727
1539
|
}
|
|
1728
|
-
const clips = snapshot.animations
|
|
1540
|
+
const clips = snapshot.animations;
|
|
1729
1541
|
const clip = clips.find((candidate) => candidate.name === pose.clip);
|
|
1730
1542
|
if (!clip) {
|
|
1731
1543
|
const available = clips.map((candidate) => `'${candidate.name}'`).join(', ');
|
|
@@ -1807,9 +1619,8 @@ function renderFreeCameraCapture(
|
|
|
1807
1619
|
/**
|
|
1808
1620
|
* The axis marker for one orthographic view: which AUTHORED axis points
|
|
1809
1621
|
* screen-right after the subject's face-front yaw, spelled as `+X>` (that
|
|
1810
|
-
* axis points right) or `<+X` (it points left).
|
|
1811
|
-
*
|
|
1812
|
-
* the yaw itself so nothing is silently askew.
|
|
1622
|
+
* axis points right) or `<+X` (it points left). Perspective views return null;
|
|
1623
|
+
* non-cardinal orientations name their yaw.
|
|
1813
1624
|
*/
|
|
1814
1625
|
function authoredAxisMarker(
|
|
1815
1626
|
view: AssetPreviewView,
|
|
@@ -1832,14 +1643,7 @@ function authoredAxisMarker(
|
|
|
1832
1643
|
}
|
|
1833
1644
|
|
|
1834
1645
|
/**
|
|
1835
|
-
* Render
|
|
1836
|
-
*
|
|
1837
|
-
* Extracted verbatim from {@link captureObjectAssetPreview}'s own loop (its
|
|
1838
|
-
* only caller until the scene stage below): the two stages differ in WHAT
|
|
1839
|
-
* they photograph — an isolated snapshot on a studio stage versus the entity
|
|
1840
|
-
* standing in the live scene — never in how the sheet is assembled, so a
|
|
1841
|
-
* caller comparing a lab and a scene capture is comparing subjects rather
|
|
1842
|
-
* than two independently-drifting sheet builders.
|
|
1646
|
+
* Render four fixed views and a 2×2 contact sheet, or a 3×2 sheet when the lab includes its underside view.
|
|
1843
1647
|
*/
|
|
1844
1648
|
function renderFourViewCapture(
|
|
1845
1649
|
renderer: THREE.WebGLRenderer,
|
|
@@ -1848,10 +1652,7 @@ function renderFourViewCapture(
|
|
|
1848
1652
|
width: number,
|
|
1849
1653
|
height: number,
|
|
1850
1654
|
orientation: AssetPreviewOrientation,
|
|
1851
|
-
/**
|
|
1852
|
-
* Every blind modeling session read the sheet and then `below.png` as a
|
|
1853
|
-
* second image, ten seconds of generation per look; one sheet that carries
|
|
1854
|
-
* the underside is one read (2026-09-06). */
|
|
1655
|
+
/** The underside camera occupies the fifth tile of the 3 × 2 contact sheet. */
|
|
1855
1656
|
below?: THREE.Camera,
|
|
1856
1657
|
): AssetPreviewCapture {
|
|
1857
1658
|
const views: AssetPreviewImage[] = [];
|
|
@@ -1874,8 +1675,7 @@ function renderFourViewCapture(
|
|
|
1874
1675
|
}
|
|
1875
1676
|
renderer.setSize(width, height, false);
|
|
1876
1677
|
tiles.forEach((view, index) => {
|
|
1877
|
-
const camera = view === 'below' ? below : cameras.get(view)
|
|
1878
|
-
if (!camera) return;
|
|
1678
|
+
const camera = view === 'below' ? below! : cameras.get(view)!;
|
|
1879
1679
|
renderer.render(scene, camera);
|
|
1880
1680
|
views.push({
|
|
1881
1681
|
view,
|
|
@@ -1934,7 +1734,7 @@ function renderFourViewCapture(
|
|
|
1934
1734
|
* thing here that is not obviously self-restoring: it is undone in `finally`,
|
|
1935
1735
|
* so a capture that throws mid-render still hands the human back their grid
|
|
1936
1736
|
* and gizmos. The renderer is this function's own (created and destroyed
|
|
1937
|
-
* here
|
|
1737
|
+
* here) — the editor's
|
|
1938
1738
|
* viewport renderer and camera are never borrowed, so there is nothing of
|
|
1939
1739
|
* theirs to put back.
|
|
1940
1740
|
*
|
|
@@ -1944,7 +1744,7 @@ function renderFourViewCapture(
|
|
|
1944
1744
|
export function captureSceneStageAssetPreview(
|
|
1945
1745
|
source: THREE.Object3D,
|
|
1946
1746
|
scene: THREE.Scene,
|
|
1947
|
-
options: AssetPreviewOptions
|
|
1747
|
+
options: AssetPreviewOptions,
|
|
1948
1748
|
): AssetPreviewCapture {
|
|
1949
1749
|
let containsSplat = false;
|
|
1950
1750
|
source.traverse((object) => {
|
|
@@ -2016,7 +1816,7 @@ export function captureSceneStageAssetPreview(
|
|
|
2016
1816
|
}
|
|
2017
1817
|
|
|
2018
1818
|
/** One shot, staged and framed but not yet rendered. */
|
|
2019
|
-
|
|
1819
|
+
interface PlannedShot {
|
|
2020
1820
|
label: string;
|
|
2021
1821
|
camera: THREE.Camera;
|
|
2022
1822
|
scene: THREE.Scene;
|
|
@@ -2027,7 +1827,7 @@ export interface PlannedShot {
|
|
|
2027
1827
|
|
|
2028
1828
|
/** Everything {@link captureShotSetAssetPreview} needs before it touches a
|
|
2029
1829
|
* GPU, plus the snapshots that staging allocated. */
|
|
2030
|
-
|
|
1830
|
+
interface ShotSetCapturePlan {
|
|
2031
1831
|
shots: PlannedShot[];
|
|
2032
1832
|
warnings: AssetPreviewShotWarning[];
|
|
2033
1833
|
dispose(): void;
|
|
@@ -2059,28 +1859,19 @@ function emptyFrameWarning(
|
|
|
2059
1859
|
...(shot.view === 'bone-zoom' ? { bones: [...shot.bones] } : {}),
|
|
2060
1860
|
...(shot.pose === undefined ? {} : { pose: shot.pose }),
|
|
2061
1861
|
message:
|
|
2062
|
-
`Asset preview '
|
|
1862
|
+
`Asset preview shot set '${definitionName}' shot ${JSON.stringify(shot.label)} (${aimed}` +
|
|
2063
1863
|
`${posed}) framed NONE of the subject's ${totalPrimitives} rendered faces: the image is ` +
|
|
2064
1864
|
`background only and proves nothing about the asset. ${cause}`,
|
|
2065
1865
|
};
|
|
2066
1866
|
}
|
|
2067
1867
|
|
|
2068
1868
|
/**
|
|
2069
|
-
* Stage and frame a labeled shot set
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2072
|
-
*
|
|
2073
|
-
* the rest-pose shots and the loaded source model are never affected. Zoom
|
|
2074
|
-
* `spanFraction`s size each crop relative to the whole model's own bounding
|
|
2075
|
-
* radius — a short "goblin" rig gets a goblin-scaled crop, never a fixed
|
|
2076
|
-
* reference-human height.
|
|
2077
|
-
*
|
|
2078
|
-
* Split out of {@link captureShotSetAssetPreview} so that everything the
|
|
2079
|
-
* capture DECIDES — which snapshot, which camera, and whether a shot frames
|
|
2080
|
-
* any geometry at all — is reachable without a WebGL context, for the same
|
|
2081
|
-
* reason `asset-preview-framing.ts` exists. The caller owns `dispose()`.
|
|
1869
|
+
* Stage and frame a labeled shot set using the model’s skeleton and bounds without rendering. Each
|
|
1870
|
+
* named pose uses a disposable snapshot; rest shots and the loaded source remain unchanged.
|
|
1871
|
+
* Turntable angles and bone crops use the supplied definition, with spanFraction scaled to the
|
|
1872
|
+
* whole model’s radius. The caller owns dispose().
|
|
2082
1873
|
*/
|
|
2083
|
-
|
|
1874
|
+
function planShotSetCapture(
|
|
2084
1875
|
source: THREE.Object3D,
|
|
2085
1876
|
definition: AssetPreviewShotSetDefinition,
|
|
2086
1877
|
aspect: number,
|
|
@@ -2094,11 +1885,8 @@ export function planShotSetCapture(
|
|
|
2094
1885
|
const snapshot = createAssetPreviewSnapshot(source);
|
|
2095
1886
|
snapshots.push(snapshot);
|
|
2096
1887
|
const bonesByName = findRequiredSkeletonBones(snapshot, definition);
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2099
|
-
// labels AND the +Z-fixed zoom crops are only true to the model's
|
|
2100
|
-
// facing once its forward actually points at them. Bone anchors are
|
|
2101
|
-
// read AFTER the wrap, so the zoom framing follows the rotation.
|
|
1888
|
+
// Normalize forward before framing so turntable labels and yaw-based bone crops use the model’s
|
|
1889
|
+
// facing. Read bone anchors after that rotation.
|
|
2102
1890
|
const forward = readModelForward(source);
|
|
2103
1891
|
const subject = faceFrontSubject(snapshot, forward);
|
|
2104
1892
|
const framing = measureAssetPreview(subject, 'perspective');
|
|
@@ -2112,7 +1900,7 @@ export function planShotSetCapture(
|
|
|
2112
1900
|
const poseStages = new Map<string, StagedShotSubject>();
|
|
2113
1901
|
for (const shot of definition.shots) {
|
|
2114
1902
|
if (shot.pose === undefined || poseStages.has(shot.pose)) continue;
|
|
2115
|
-
const steps = definition.poses
|
|
1903
|
+
const steps = definition.poses![shot.pose]!;
|
|
2116
1904
|
const poseSnapshot = createAssetPreviewSnapshot(source);
|
|
2117
1905
|
snapshots.push(poseSnapshot);
|
|
2118
1906
|
const poseBones = findRequiredSkeletonBones(poseSnapshot, definition);
|
|
@@ -2140,13 +1928,13 @@ export function planShotSetCapture(
|
|
|
2140
1928
|
for (const pose of new Set(
|
|
2141
1929
|
definition.shots
|
|
2142
1930
|
.filter((shot) => shot.view === 'turntable')
|
|
2143
|
-
.map((shot) => shot.pose
|
|
1931
|
+
.map((shot) => shot.pose),
|
|
2144
1932
|
)) {
|
|
2145
1933
|
const staged = stageFor(pose);
|
|
2146
1934
|
const yaws = [
|
|
2147
1935
|
...new Set(
|
|
2148
1936
|
definition.shots
|
|
2149
|
-
.filter((shot) => shot.view === 'turntable' && (shot.pose
|
|
1937
|
+
.filter((shot) => shot.view === 'turntable' && (shot.pose) === pose)
|
|
2150
1938
|
.map((shot) => (shot as Extract<ShotSetShot, { view: 'turntable' }>).yaw),
|
|
2151
1939
|
),
|
|
2152
1940
|
];
|
|
@@ -2167,28 +1955,19 @@ export function planShotSetCapture(
|
|
|
2167
1955
|
if (shot.view === 'turntable') {
|
|
2168
1956
|
return {
|
|
2169
1957
|
label: shot.label,
|
|
2170
|
-
camera: turntableByStage.get(shot.pose
|
|
1958
|
+
camera: turntableByStage.get(shot.pose)!.get(shot.yaw)!,
|
|
2171
1959
|
scene: staged.scene,
|
|
2172
1960
|
stage: staged,
|
|
2173
1961
|
basis: turntableViewBasis(shot.yaw),
|
|
2174
1962
|
};
|
|
2175
1963
|
}
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
// (several poses framed on the same head) that is the point, not a
|
|
2179
|
-
// limitation: one anchor set means one camera, so the frames differ
|
|
2180
|
-
// only by the face. A pose that MOVED its own anchor would want the
|
|
2181
|
-
// pose snapshot's bone map instead; no definition does that today,
|
|
2182
|
-
// and a strip never should.
|
|
2183
|
-
//
|
|
2184
|
-
// A zoom on multiple bones (both hands, both feet) sizes the crop to
|
|
2185
|
-
// actually contain every anchor, not just apply a fixed fraction of
|
|
2186
|
-
// the model's radius — see {@link boneZoomCamera}'s doc comment.
|
|
1964
|
+
// Read bone-zoom anchors from the rest snapshot so expression-strip poses share a camera.
|
|
1965
|
+
// Multiple anchors expand the crop to contain them all; see boneZoomCamera.
|
|
2187
1966
|
const missingAnchor = shot.bones.find((bone) => !bonesByName.has(bone));
|
|
2188
1967
|
if (missingAnchor !== undefined) {
|
|
2189
1968
|
throw new Error(
|
|
2190
|
-
`Asset preview '
|
|
2191
|
-
`${JSON.stringify(shot.label)} zooms on a joint this
|
|
1969
|
+
`Asset preview shot set '${definition.name}' shot ` +
|
|
1970
|
+
`${JSON.stringify(shot.label)} zooms on a joint this model's skeleton does not ` +
|
|
2192
1971
|
`have: ${missingAnchor}. List it in the definition's requiredBones or fix the rig.`,
|
|
2193
1972
|
);
|
|
2194
1973
|
}
|
|
@@ -2273,7 +2052,7 @@ export function planShotSetCapture(
|
|
|
2273
2052
|
export function captureShotSetAssetPreview(
|
|
2274
2053
|
source: THREE.Object3D,
|
|
2275
2054
|
definition: AssetPreviewShotSetDefinition,
|
|
2276
|
-
options: AssetPreviewOptions
|
|
2055
|
+
options: AssetPreviewOptions,
|
|
2277
2056
|
): LabeledShotSetCapture {
|
|
2278
2057
|
const width = checkedDimension(options.width);
|
|
2279
2058
|
const height = checkedDimension(options.height);
|
|
@@ -2298,7 +2077,9 @@ export function captureShotSetAssetPreview(
|
|
|
2298
2077
|
activeRenderer.setSize(width, height, false);
|
|
2299
2078
|
// A pose gets its own staged scene, so every scene in the plan is lit —
|
|
2300
2079
|
// once, off the renderer's single cached bake.
|
|
2301
|
-
for (const
|
|
2080
|
+
for (const shotScene of new Set(plan.shots.map((shot) => shot.scene))) {
|
|
2081
|
+
applyStudioEnvironment(shotScene, activeRenderer);
|
|
2082
|
+
}
|
|
2302
2083
|
|
|
2303
2084
|
const shots: LabeledShotSetCapture['shots'] = [];
|
|
2304
2085
|
const columns = 2;
|
|
@@ -2372,7 +2153,7 @@ function projectModelUrl(rawAssetPath: string): URL {
|
|
|
2372
2153
|
if (decodedPath.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
2373
2154
|
throw new Error('Asset Editor model paths cannot traverse outside the project root.');
|
|
2374
2155
|
}
|
|
2375
|
-
const projectOrigin = globalThis.location
|
|
2156
|
+
const projectOrigin = globalThis.location.origin;
|
|
2376
2157
|
const url = new URL(assetPath, projectOrigin);
|
|
2377
2158
|
if (url.origin !== projectOrigin || !url.pathname.toLowerCase().match(/\.(?:glb|gltf|spz)$/)) {
|
|
2378
2159
|
throw new Error(
|
|
@@ -2487,7 +2268,7 @@ function projectDependencyUrl(uri: string, sourceUrl: URL): URL {
|
|
|
2487
2268
|
|
|
2488
2269
|
function dependencyMimeType(resource: ExternalGltfResource, url: URL): string {
|
|
2489
2270
|
if (typeof resource.mimeType === 'string') return resource.mimeType;
|
|
2490
|
-
const extension = url.pathname.split('.').pop()
|
|
2271
|
+
const extension = url.pathname.split('.').pop()!.toLowerCase();
|
|
2491
2272
|
if (extension === 'png') return 'image/png';
|
|
2492
2273
|
if (extension === 'jpg' || extension === 'jpeg') return 'image/jpeg';
|
|
2493
2274
|
if (extension === 'webp') return 'image/webp';
|
|
@@ -2525,17 +2306,8 @@ async function inlineExternalGltfResources(
|
|
|
2525
2306
|
return JSON.stringify(document);
|
|
2526
2307
|
}
|
|
2527
2308
|
|
|
2528
|
-
/**
|
|
2529
|
-
*
|
|
2530
|
-
* fetched from the project origin.
|
|
2531
|
-
*
|
|
2532
|
-
* EXTRACTED VERBATIM from `asset-compare.ts`'s private `parseRefGlb` (the
|
|
2533
|
-
* compare mode has shipped reference GLBs over the wire this way since B8.4);
|
|
2534
|
-
* `label` is the one addition, so each caller keeps its own error wording
|
|
2535
|
-
* while there is a single decoder. The bound is the SAME 64 MiB the fetched
|
|
2536
|
-
* path enforces (`MAX_MODEL_BYTES`) — bytes that arrive over the relay are no
|
|
2537
|
-
* cheaper to rasterize than bytes fetched over HTTP.
|
|
2538
|
-
*/
|
|
2309
|
+
/** Parse a base64 GLB carried by the relay command. The caller's label names
|
|
2310
|
+
* decoding failures. MAX_MODEL_BYTES bounds both relay and fetched models. */
|
|
2539
2311
|
export function parseGlbBytesModel(base64: string, label: string): Promise<THREE.Object3D> {
|
|
2540
2312
|
let bytes: Uint8Array<ArrayBuffer>;
|
|
2541
2313
|
try {
|
|
@@ -2564,18 +2336,17 @@ export function parseGlbBytesModel(base64: string, label: string): Promise<THREE
|
|
|
2564
2336
|
}
|
|
2565
2337
|
|
|
2566
2338
|
/**
|
|
2567
|
-
* The Asset Lab's
|
|
2568
|
-
* project path, no live entity.
|
|
2569
|
-
*
|
|
2570
|
-
*
|
|
2571
|
-
* here, because Node has no GPU and the editor session does.
|
|
2339
|
+
* The Asset Lab's contact-sheet capture of a model that exists only as BYTES — no
|
|
2340
|
+
* project path, no live entity. A Node host that built an `Object3D` exports
|
|
2341
|
+
* it to an in-memory GLB and hands those bytes here, because Node has no GPU
|
|
2342
|
+
* and the editor session does.
|
|
2572
2343
|
*
|
|
2573
2344
|
* Deliberately the `'lab'` stage and nothing else — bytes stand nowhere, so
|
|
2574
2345
|
* there is no scene to stage them in.
|
|
2575
2346
|
*/
|
|
2576
2347
|
export async function captureGlbBytesAssetPreview(
|
|
2577
2348
|
glbBase64: string,
|
|
2578
|
-
options: AssetPreviewOptions
|
|
2349
|
+
options: AssetPreviewOptions,
|
|
2579
2350
|
): Promise<AssetPreviewCapture> {
|
|
2580
2351
|
const model = await parseGlbBytesModel(glbBase64, 'Asset preview GLB bytes');
|
|
2581
2352
|
try {
|
|
@@ -2586,16 +2357,15 @@ export async function captureGlbBytesAssetPreview(
|
|
|
2586
2357
|
}
|
|
2587
2358
|
|
|
2588
2359
|
/**
|
|
2589
|
-
* The same bytes, photographed as a LABELED SHOT SET
|
|
2590
|
-
*
|
|
2591
|
-
* stages the subject itself, exactly the way the asset-path lane's
|
|
2360
|
+
* The same bytes, photographed as a LABELED SHOT SET. A shot set stages the
|
|
2361
|
+
* subject itself, exactly the way the asset-path lane's
|
|
2592
2362
|
* {@link captureShotSetModelPreview} does, so bytes are no less valid a
|
|
2593
2363
|
* subject here than a project GLB is.
|
|
2594
2364
|
*/
|
|
2595
2365
|
export async function captureShotSetGlbBytesPreview(
|
|
2596
2366
|
glbBase64: string,
|
|
2597
2367
|
definition: AssetPreviewShotSetDefinition,
|
|
2598
|
-
options: AssetPreviewOptions
|
|
2368
|
+
options: AssetPreviewOptions,
|
|
2599
2369
|
): Promise<LabeledShotSetCapture> {
|
|
2600
2370
|
const model = await parseGlbBytesModel(glbBase64, 'Asset preview GLB bytes');
|
|
2601
2371
|
try {
|
|
@@ -2648,11 +2418,8 @@ export async function loadProjectAssetModelSource(
|
|
|
2648
2418
|
}
|
|
2649
2419
|
}
|
|
2650
2420
|
|
|
2651
|
-
export async function loadProjectAssetModel(
|
|
2652
|
-
assetPath
|
|
2653
|
-
externalSignal?: AbortSignal,
|
|
2654
|
-
): Promise<THREE.Object3D> {
|
|
2655
|
-
return (await loadProjectAssetModelSource(assetPath, externalSignal)).root;
|
|
2421
|
+
export async function loadProjectAssetModel(assetPath: string): Promise<THREE.Object3D> {
|
|
2422
|
+
return (await loadProjectAssetModelSource(assetPath)).root;
|
|
2656
2423
|
}
|
|
2657
2424
|
|
|
2658
2425
|
/** Release a model returned by {@link loadProjectAssetModel}. */
|
|
@@ -2677,7 +2444,7 @@ export function disposeProjectAssetModel(root: THREE.Object3D): void {
|
|
|
2677
2444
|
|
|
2678
2445
|
export async function captureModelAssetPreview(
|
|
2679
2446
|
assetPath: string,
|
|
2680
|
-
options: AssetPreviewOptions
|
|
2447
|
+
options: AssetPreviewOptions,
|
|
2681
2448
|
): Promise<AssetPreviewCapture> {
|
|
2682
2449
|
const model = await loadProjectAssetModel(assetPath);
|
|
2683
2450
|
try {
|
|
@@ -2692,7 +2459,7 @@ export async function captureModelAssetPreview(
|
|
|
2692
2459
|
export async function captureSourceReviewShotSetModelPreview(
|
|
2693
2460
|
assetPath: string,
|
|
2694
2461
|
forward: readonly [number, number, number],
|
|
2695
|
-
options: AssetPreviewOptions
|
|
2462
|
+
options: AssetPreviewOptions,
|
|
2696
2463
|
): Promise<SourceReviewShotSetCapture> {
|
|
2697
2464
|
const model = await loadProjectAssetModel(assetPath);
|
|
2698
2465
|
try {
|
|
@@ -2784,14 +2551,14 @@ async function captureSplatAssetPreview(
|
|
|
2784
2551
|
}
|
|
2785
2552
|
}
|
|
2786
2553
|
|
|
2787
|
-
/** The
|
|
2554
|
+
/** The shot set's asset-path entry point — loads the project model the
|
|
2788
2555
|
* same bounded/validated way {@link captureModelAssetPreview} does, then
|
|
2789
2556
|
* renders the definition's labeled shot set against ITS OWN skeleton
|
|
2790
2557
|
* (see {@link captureShotSetAssetPreview}). */
|
|
2791
2558
|
export async function captureShotSetModelPreview(
|
|
2792
2559
|
assetPath: string,
|
|
2793
2560
|
definition: AssetPreviewShotSetDefinition,
|
|
2794
|
-
options: AssetPreviewOptions
|
|
2561
|
+
options: AssetPreviewOptions,
|
|
2795
2562
|
): Promise<LabeledShotSetCapture> {
|
|
2796
2563
|
const model = await loadProjectAssetModel(assetPath);
|
|
2797
2564
|
try {
|