@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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B8.4 — the Asset Lab COMPARE surface (`vgai screenshot <model.glb>
|
|
3
|
+
* --compare <ref.glb>`): renders the project asset AND a caller-supplied reference GLB
|
|
4
|
+
* with matched orthographic front + side framing, then scores their
|
|
5
|
+
* silhouettes (IoU) and composes review overlays, so an agent can
|
|
6
|
+
* numerically converge a procedural character toward a reference.
|
|
7
|
+
*
|
|
8
|
+
* Framing is DELIBERATELY bounding-box based (equal-height normalization,
|
|
9
|
+
* shared ground plane, centered footprint) rather than the humanoid shot
|
|
10
|
+
* set's Mixamo-skeleton anchors: the reference may carry any rig (UE joint
|
|
11
|
+
* names, no rig at all), and the bone-anchored path throws on missing joints
|
|
12
|
+
* by design. Both models are yaw-normalized to FACE the front camera —
|
|
13
|
+
* forward read from GLB `extras`/`userData.forward` when present (our
|
|
14
|
+
* generated rigs persist `[0,0,-1]`), else the glTF +Z convention, with an
|
|
15
|
+
* explicit reference override for models whose extras lie.
|
|
16
|
+
*
|
|
17
|
+
* Pure scoring math lives in `asset-compare-core.ts` (unit-tested in node);
|
|
18
|
+
* this module owns only loading, scene assembly, and WebGL capture. Fixed
|
|
19
|
+
* cameras + fixed resolution + flat unlit override ⇒ identical IoU numbers
|
|
20
|
+
* across runs (a stated acceptance gate).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
|
|
24
|
+
import * as THREE from 'three';
|
|
25
|
+
import {
|
|
26
|
+
ASSET_COMPARE_VIEWS,
|
|
27
|
+
type AssetCompareView,
|
|
28
|
+
COMPARE_FRAME_CENTER_Y,
|
|
29
|
+
COMPARE_FRAME_HALF_HEIGHT,
|
|
30
|
+
forwardYawRadians,
|
|
31
|
+
maskIoU,
|
|
32
|
+
normalizedPlacement,
|
|
33
|
+
overlayRgba,
|
|
34
|
+
silhouetteMaskFromRgba,
|
|
35
|
+
} from '@volter/editor-sdk/kit/asset-compare-core';
|
|
36
|
+
import {
|
|
37
|
+
checkedDimension,
|
|
38
|
+
createAssetPreviewSnapshot,
|
|
39
|
+
disposeAssetPreviewSnapshot,
|
|
40
|
+
disposeProjectAssetModel,
|
|
41
|
+
loadProjectAssetModel,
|
|
42
|
+
measureAssetPreview,
|
|
43
|
+
parseGlbBytesModel,
|
|
44
|
+
pngBase64,
|
|
45
|
+
readModelForward,
|
|
46
|
+
} from './asset-preview';
|
|
47
|
+
|
|
48
|
+
export type { AssetCompareView } from '@volter/editor-sdk/kit/asset-compare-core';
|
|
49
|
+
// The forward detection moved to `asset-preview.ts` (the shot sets need it
|
|
50
|
+
// too); re-exported here so compare-mode consumers keep their import path.
|
|
51
|
+
export { readModelForward } from './asset-preview';
|
|
52
|
+
|
|
53
|
+
export interface AssetCompareImage {
|
|
54
|
+
base64: string;
|
|
55
|
+
mimeType: 'image/png';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AssetCompareViewResult {
|
|
59
|
+
view: AssetCompareView;
|
|
60
|
+
/** Silhouette intersection-over-union in [0, 1]. */
|
|
61
|
+
iou: number;
|
|
62
|
+
/** Both silhouettes in distinct colors (orange asset / cyan ref / white agreement). */
|
|
63
|
+
overlay: AssetCompareImage;
|
|
64
|
+
/** The asset's own silhouette (white on black), as captured for scoring. */
|
|
65
|
+
asset: AssetCompareImage;
|
|
66
|
+
/** The reference's silhouette (white on black), as captured for scoring. */
|
|
67
|
+
ref: AssetCompareImage;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AssetCompareCapture {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
views: AssetCompareViewResult[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AssetCompareOptions {
|
|
77
|
+
width?: number;
|
|
78
|
+
height?: number;
|
|
79
|
+
/** Override the reference GLB's forward vector (its facing in the ground
|
|
80
|
+
* plane). Defaults to the GLB's own `userData.forward` extras, else +Z. */
|
|
81
|
+
refForward?: [number, number, number];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const SILHOUETTE_BACKGROUND = 0x000000;
|
|
85
|
+
const SILHOUETTE_FOREGROUND = 0xffffff;
|
|
86
|
+
const COMPARE_CAMERA_DISTANCE = 10;
|
|
87
|
+
|
|
88
|
+
/** The base64-GLB decoder moved to `asset-preview.ts` (`parseGlbBytesModel`)
|
|
89
|
+
* when the module-look lane needed the same wire-carried bytes; this keeps
|
|
90
|
+
* compare's own error wording. */
|
|
91
|
+
function parseRefGlb(base64: string): Promise<THREE.Object3D> {
|
|
92
|
+
return parseGlbBytesModel(base64, 'Compare reference GLB');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Snapshot + face-the-camera yaw + equal-height ground-aligned placement,
|
|
96
|
+
* as one disposable wrapper ready to drop into a silhouette scene. */
|
|
97
|
+
function buildNormalizedSubject(
|
|
98
|
+
source: THREE.Object3D,
|
|
99
|
+
forward: [number, number, number],
|
|
100
|
+
): { wrapper: THREE.Object3D; dispose: () => void } {
|
|
101
|
+
const snapshot = createAssetPreviewSnapshot(source);
|
|
102
|
+
const pivot = new THREE.Group();
|
|
103
|
+
pivot.rotation.y = forwardYawRadians(forward);
|
|
104
|
+
pivot.add(snapshot);
|
|
105
|
+
const wrapper = new THREE.Group();
|
|
106
|
+
wrapper.add(pivot);
|
|
107
|
+
wrapper.updateWorldMatrix(true, true);
|
|
108
|
+
const bounds = measureAssetPreview(wrapper, 'front');
|
|
109
|
+
const min: [number, number, number] = [
|
|
110
|
+
bounds.center.x - bounds.size.x / 2,
|
|
111
|
+
bounds.center.y - bounds.size.y / 2,
|
|
112
|
+
bounds.center.z - bounds.size.z / 2,
|
|
113
|
+
];
|
|
114
|
+
const max: [number, number, number] = [
|
|
115
|
+
bounds.center.x + bounds.size.x / 2,
|
|
116
|
+
bounds.center.y + bounds.size.y / 2,
|
|
117
|
+
bounds.center.z + bounds.size.z / 2,
|
|
118
|
+
];
|
|
119
|
+
const placement = normalizedPlacement(min, max);
|
|
120
|
+
wrapper.scale.setScalar(placement.scale);
|
|
121
|
+
wrapper.position.set(...placement.position);
|
|
122
|
+
wrapper.updateWorldMatrix(true, true);
|
|
123
|
+
return { wrapper, dispose: () => disposeAssetPreviewSnapshot(snapshot) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compareCamera(view: AssetCompareView, aspect: number): THREE.OrthographicCamera {
|
|
127
|
+
const halfHeight = COMPARE_FRAME_HALF_HEIGHT;
|
|
128
|
+
const halfWidth = halfHeight * aspect;
|
|
129
|
+
const camera = new THREE.OrthographicCamera(
|
|
130
|
+
-halfWidth,
|
|
131
|
+
halfWidth,
|
|
132
|
+
halfHeight,
|
|
133
|
+
-halfHeight,
|
|
134
|
+
0.01,
|
|
135
|
+
COMPARE_CAMERA_DISTANCE * 4,
|
|
136
|
+
);
|
|
137
|
+
const direction = view === 'front' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0);
|
|
138
|
+
camera.position
|
|
139
|
+
.set(0, COMPARE_FRAME_CENTER_Y, 0)
|
|
140
|
+
.addScaledVector(direction, COMPARE_CAMERA_DISTANCE);
|
|
141
|
+
camera.up.set(0, 1, 0);
|
|
142
|
+
camera.lookAt(0, COMPARE_FRAME_CENTER_Y, 0);
|
|
143
|
+
camera.updateProjectionMatrix();
|
|
144
|
+
camera.updateMatrixWorld(true);
|
|
145
|
+
return camera;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface SilhouetteShot {
|
|
149
|
+
image: AssetCompareImage;
|
|
150
|
+
mask: Uint8Array;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Compare an already-loaded asset model against an already-loaded reference:
|
|
155
|
+
* the shared capture core behind both the asset-path and entity entry
|
|
156
|
+
* points. Neither input is mutated (both are snapshotted).
|
|
157
|
+
*/
|
|
158
|
+
export function captureAssetComparePreview(
|
|
159
|
+
assetSource: THREE.Object3D,
|
|
160
|
+
refSource: THREE.Object3D,
|
|
161
|
+
options: AssetCompareOptions = {},
|
|
162
|
+
): AssetCompareCapture {
|
|
163
|
+
const width = checkedDimension(options.width);
|
|
164
|
+
const height = checkedDimension(options.height);
|
|
165
|
+
const aspect = width / height;
|
|
166
|
+
const disposers: Array<() => void> = [];
|
|
167
|
+
let renderer: THREE.WebGLRenderer | null = null;
|
|
168
|
+
try {
|
|
169
|
+
const asset = buildNormalizedSubject(assetSource, readModelForward(assetSource));
|
|
170
|
+
disposers.push(asset.dispose);
|
|
171
|
+
const ref = buildNormalizedSubject(
|
|
172
|
+
refSource,
|
|
173
|
+
options.refForward ?? readModelForward(refSource),
|
|
174
|
+
);
|
|
175
|
+
disposers.push(ref.dispose);
|
|
176
|
+
|
|
177
|
+
const override = new THREE.MeshBasicMaterial({ color: SILHOUETTE_FOREGROUND });
|
|
178
|
+
disposers.push(() => override.dispose());
|
|
179
|
+
const sceneFor = (subject: THREE.Object3D): THREE.Scene => {
|
|
180
|
+
const scene = new THREE.Scene();
|
|
181
|
+
scene.background = new THREE.Color(SILHOUETTE_BACKGROUND);
|
|
182
|
+
scene.overrideMaterial = override;
|
|
183
|
+
scene.add(subject);
|
|
184
|
+
return scene;
|
|
185
|
+
};
|
|
186
|
+
const assetScene = sceneFor(asset.wrapper);
|
|
187
|
+
const refScene = sceneFor(ref.wrapper);
|
|
188
|
+
|
|
189
|
+
// Antialiasing off: silhouette masks want a crisp, deterministic
|
|
190
|
+
// coverage decision per pixel, not blended edge ramps.
|
|
191
|
+
const activeRenderer = markHostRenderer(
|
|
192
|
+
new THREE.WebGLRenderer({
|
|
193
|
+
antialias: false,
|
|
194
|
+
alpha: false,
|
|
195
|
+
preserveDrawingBuffer: true,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
renderer = activeRenderer;
|
|
199
|
+
activeRenderer.setPixelRatio(1);
|
|
200
|
+
activeRenderer.toneMapping = THREE.NoToneMapping;
|
|
201
|
+
activeRenderer.setClearColor(SILHOUETTE_BACKGROUND, 1);
|
|
202
|
+
activeRenderer.setSize(width, height, false);
|
|
203
|
+
|
|
204
|
+
const readCanvas = document.createElement('canvas');
|
|
205
|
+
readCanvas.width = width;
|
|
206
|
+
readCanvas.height = height;
|
|
207
|
+
const readContext = readCanvas.getContext('2d');
|
|
208
|
+
if (!readContext) throw new Error('Unable to create the compare readback canvas.');
|
|
209
|
+
|
|
210
|
+
const captureSilhouette = (
|
|
211
|
+
scene: THREE.Scene,
|
|
212
|
+
camera: THREE.Camera,
|
|
213
|
+
label: string,
|
|
214
|
+
view: AssetCompareView,
|
|
215
|
+
): SilhouetteShot => {
|
|
216
|
+
activeRenderer.render(scene, camera);
|
|
217
|
+
readContext.clearRect(0, 0, width, height);
|
|
218
|
+
readContext.drawImage(activeRenderer.domElement, 0, 0);
|
|
219
|
+
const rgba = readContext.getImageData(0, 0, width, height).data;
|
|
220
|
+
const mask = silhouetteMaskFromRgba(rgba, width * height);
|
|
221
|
+
if (!mask.some((value) => value !== 0)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Compare ${view} view rendered an empty ${label} silhouette — nothing to score.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
image: { base64: pngBase64(readCanvas), mimeType: 'image/png' },
|
|
228
|
+
mask,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const overlayCanvas = document.createElement('canvas');
|
|
233
|
+
overlayCanvas.width = width;
|
|
234
|
+
overlayCanvas.height = height;
|
|
235
|
+
const overlayContext = overlayCanvas.getContext('2d');
|
|
236
|
+
if (!overlayContext) throw new Error('Unable to create the compare overlay canvas.');
|
|
237
|
+
|
|
238
|
+
const views: AssetCompareViewResult[] = [];
|
|
239
|
+
for (const view of ASSET_COMPARE_VIEWS) {
|
|
240
|
+
const camera = compareCamera(view, aspect);
|
|
241
|
+
const assetShot = captureSilhouette(assetScene, camera, 'asset', view);
|
|
242
|
+
const refShot = captureSilhouette(refScene, camera, 'reference', view);
|
|
243
|
+
const stats = maskIoU(assetShot.mask, refShot.mask);
|
|
244
|
+
overlayContext.putImageData(
|
|
245
|
+
new ImageData(overlayRgba(assetShot.mask, refShot.mask), width, height),
|
|
246
|
+
0,
|
|
247
|
+
0,
|
|
248
|
+
);
|
|
249
|
+
views.push({
|
|
250
|
+
view,
|
|
251
|
+
iou: stats.iou,
|
|
252
|
+
overlay: { base64: pngBase64(overlayCanvas), mimeType: 'image/png' },
|
|
253
|
+
asset: assetShot.image,
|
|
254
|
+
ref: refShot.image,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return { width, height, views };
|
|
258
|
+
} finally {
|
|
259
|
+
renderer?.dispose();
|
|
260
|
+
renderer?.forceContextLoss();
|
|
261
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The `--asset <path> --compare` entry point: loads the project GLB the
|
|
266
|
+
* same bounded/validated way the other asset-path captures do, parses the
|
|
267
|
+
* relayed reference GLB bytes, and scores them. */
|
|
268
|
+
export async function captureModelComparePreview(
|
|
269
|
+
assetPath: string,
|
|
270
|
+
refGlbBase64: string,
|
|
271
|
+
options: AssetCompareOptions = {},
|
|
272
|
+
): Promise<AssetCompareCapture> {
|
|
273
|
+
const model = await loadProjectAssetModel(assetPath);
|
|
274
|
+
try {
|
|
275
|
+
return await captureEntityComparePreview(model, refGlbBase64, options);
|
|
276
|
+
} finally {
|
|
277
|
+
disposeProjectAssetModel(model);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The `--entity <id> --compare` entry point (also the shared tail of the
|
|
282
|
+
* asset-path leg): parses the reference and runs the capture core. */
|
|
283
|
+
export async function captureEntityComparePreview(
|
|
284
|
+
entity: THREE.Object3D,
|
|
285
|
+
refGlbBase64: string,
|
|
286
|
+
options: AssetCompareOptions = {},
|
|
287
|
+
): Promise<AssetCompareCapture> {
|
|
288
|
+
const refModel = await parseRefGlb(refGlbBase64);
|
|
289
|
+
try {
|
|
290
|
+
return captureAssetComparePreview(entity, refModel, options);
|
|
291
|
+
} finally {
|
|
292
|
+
disposeProjectAssetModel(refModel);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `capture-asset-preview` — the Asset Lab's photographs of a model or an entity:
|
|
3
|
+
* the plain four views, a project shot set, the source review set, a compare
|
|
4
|
+
* against a reference GLB and the live scene stage. Registered with the
|
|
5
|
+
* viewport's relay verbs (`viewport-commands.ts`).
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorCommandMessage, EditorCommandResult } from '@volter/editor-sdk/commands';
|
|
8
|
+
import { parseForwardVector } from '@volter/editor-sdk/kit/asset-compare-core';
|
|
9
|
+
import { captureEntityComparePreview, captureModelComparePreview } from './asset-compare';
|
|
10
|
+
import {
|
|
11
|
+
type AssetPreviewBackground,
|
|
12
|
+
captureGlbBytesAssetPreview,
|
|
13
|
+
captureModelAssetPreview,
|
|
14
|
+
captureObjectAssetPreview,
|
|
15
|
+
captureSceneStageAssetPreview,
|
|
16
|
+
captureShotSetAssetPreview,
|
|
17
|
+
captureShotSetGlbBytesPreview,
|
|
18
|
+
captureShotSetModelPreview,
|
|
19
|
+
captureSourceReviewShotSetAssetPreview,
|
|
20
|
+
captureSourceReviewShotSetModelPreview,
|
|
21
|
+
parseShotSetDefinition,
|
|
22
|
+
} from './asset-preview';
|
|
23
|
+
import { activeDocumentAuthoring } from '@volter/editor-sdk/kit/authoring/shell-document-ops';
|
|
24
|
+
import { parseCameraChoice, parsePoseChoice } from '@volter/editor-sdk/kit/capture-camera-pose';
|
|
25
|
+
import type { EditorShellStore } from './editor-shell-store';
|
|
26
|
+
import { entityObject3D } from './entity-object';
|
|
27
|
+
|
|
28
|
+
export async function handleAssetPreviewCommand(
|
|
29
|
+
store: EditorShellStore,
|
|
30
|
+
cmd: EditorCommandMessage,
|
|
31
|
+
): Promise<EditorCommandResult> {
|
|
32
|
+
const assetPath = cmd['assetPath'];
|
|
33
|
+
const entityId = cmd['entityId'];
|
|
34
|
+
// The THIRD source: a GLB that travels IN the command rather than being
|
|
35
|
+
// fetched or looked up — the module-look lane (`project.bake.preview`)
|
|
36
|
+
// builds an Object3D in Node, where there is no GPU, and hands the editor
|
|
37
|
+
// the exported bytes. Counted rather than XOR-ed because there are now more
|
|
38
|
+
// than two sources and "exactly one" has to stay exactly one.
|
|
39
|
+
const glbBase64 = cmd['glbBase64'];
|
|
40
|
+
const sourceCount =
|
|
41
|
+
Number(typeof assetPath === 'string') +
|
|
42
|
+
Number(typeof entityId === 'string') +
|
|
43
|
+
Number(typeof glbBase64 === 'string');
|
|
44
|
+
if (sourceCount !== 1) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: 'capture-asset-preview requires exactly one of assetPath, entityId or glbBase64.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const width = cmd['width'];
|
|
51
|
+
const height = cmd['height'];
|
|
52
|
+
const background = cmd['background'];
|
|
53
|
+
const shots = cmd['shots'];
|
|
54
|
+
const shotSet = cmd['shotSet'];
|
|
55
|
+
const forward = cmd['forward'];
|
|
56
|
+
const compare = cmd['compare'];
|
|
57
|
+
const stage = cmd['stage'];
|
|
58
|
+
const camera = cmd['camera'];
|
|
59
|
+
const pose = cmd['pose'];
|
|
60
|
+
if (
|
|
61
|
+
(width !== undefined && typeof width !== 'number') ||
|
|
62
|
+
(height !== undefined && typeof height !== 'number') ||
|
|
63
|
+
(background !== undefined && background !== 'neutral' && background !== 'transparent') ||
|
|
64
|
+
(shots !== undefined && shots !== 'source')
|
|
65
|
+
) {
|
|
66
|
+
return { ok: false, error: 'Invalid asset preview dimensions, background, or shots mode.' };
|
|
67
|
+
}
|
|
68
|
+
if (stage !== undefined && stage !== 'lab' && stage !== 'scene') {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `capture-asset-preview stage must be "lab" or "scene", got ${String(stage)}.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (shotSet !== undefined && shots !== undefined) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'capture-asset-preview cannot combine a shotSet definition with a shots mode.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// The free capture camera and clip pose (`--azimuth/--elevation/--distance`,
|
|
81
|
+
// `--clip/--time`) belong to the plain Asset Lab legs: a shot set / source
|
|
82
|
+
// review / compare each stage their own cameras and poses, and the scene
|
|
83
|
+
// stage photographs an entity where it stands. Refused by name, never
|
|
84
|
+
// silently ignored.
|
|
85
|
+
const cameraChoice = parseCameraChoice('capture-asset-preview', camera);
|
|
86
|
+
if (typeof cameraChoice === 'string') return { ok: false, error: cameraChoice };
|
|
87
|
+
const posedClip = parsePoseChoice('capture-asset-preview', pose);
|
|
88
|
+
if (typeof posedClip === 'string') return { ok: false, error: posedClip };
|
|
89
|
+
if (
|
|
90
|
+
(cameraChoice || posedClip) &&
|
|
91
|
+
(shots !== undefined || shotSet !== undefined || compare !== undefined || stage === 'scene')
|
|
92
|
+
) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error:
|
|
96
|
+
'capture-asset-preview cannot combine camera/pose with a shots mode, a shotSet ' +
|
|
97
|
+
'definition, compare, or stage "scene" — those legs stage their own cameras and poses.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// A project-defined labeled shot set travels WITH the command (the CLI
|
|
101
|
+
// resolves `--shots <set>` through the registered
|
|
102
|
+
// `project.<set>.previewShots` tool); validate the untrusted
|
|
103
|
+
// definition at the relay boundary so a malformed contribution fails with
|
|
104
|
+
// a named reason instead of a deep three.js error.
|
|
105
|
+
let shotSetDefinition: ReturnType<typeof parseShotSetDefinition> | undefined;
|
|
106
|
+
if (shotSet !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
shotSetDefinition = parseShotSetDefinition(shotSet);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const parsedForward = parseForwardVector(forward);
|
|
114
|
+
if (shots === 'source' && parsedForward === null) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: 'capture-asset-preview --shots source requires a non-degenerate forward [x,y,z].',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (compare !== undefined) {
|
|
121
|
+
// B8.4 — the compare mode (`vgai screenshot <model.glb> --compare <ref.glb>`).
|
|
122
|
+
// Validated here at the relay boundary so a malformed payload fails with
|
|
123
|
+
// a named reason instead of a deep three.js error.
|
|
124
|
+
if (shots !== undefined || shotSetDefinition !== undefined) {
|
|
125
|
+
return { ok: false, error: 'capture-asset-preview cannot combine compare with shots.' };
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
typeof compare !== 'object' ||
|
|
129
|
+
compare === null ||
|
|
130
|
+
typeof (compare as Record<string, unknown>)['glbBase64'] !== 'string' ||
|
|
131
|
+
((compare as Record<string, unknown>)['forward'] !== undefined &&
|
|
132
|
+
parseForwardVector((compare as Record<string, unknown>)['forward']) === null)
|
|
133
|
+
) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error:
|
|
137
|
+
'capture-asset-preview compare requires { glbBase64: string, forward?: [x, y, z] } ' +
|
|
138
|
+
'with a non-degenerate ground-plane forward.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Wire-carried GLB bytes stand NOWHERE, so they take no scene stage and no
|
|
143
|
+
// compare (whose reference is named some other way). An explicit `shotSet`
|
|
144
|
+
// definition is a different matter and is served: a shot set stages the
|
|
145
|
+
// subject itself, which is what lets `vgai screenshot <module> --orbit <n>`
|
|
146
|
+
// circle a model that only ever existed as bytes. `shots: 'source'` stays
|
|
147
|
+
// out — it is the asset-path review set, keyed to a stored forward vector.
|
|
148
|
+
if (typeof glbBase64 === 'string') {
|
|
149
|
+
if (shots !== undefined || compare !== undefined) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: 'capture-asset-preview cannot combine glbBase64 with a named shots mode or compare.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (stage === 'scene') {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error:
|
|
159
|
+
'capture-asset-preview stage "scene" photographs a live scene entity — glbBase64 bytes stand nowhere in the scene.',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// `stage: 'scene'` photographs a LIVE entity in the live scene (see
|
|
164
|
+
// `captureSceneStageAssetPreview`), so it is meaningful only for the plain
|
|
165
|
+
// four-view entity capture: a model loaded from `assetPath` stands nowhere,
|
|
166
|
+
// and the shot-set/source/compare modes each stage their own subject.
|
|
167
|
+
// Refused by name at this boundary rather than silently downgraded to the
|
|
168
|
+
// lab stage — a caller who asked for the scene and got the studio would
|
|
169
|
+
// never know.
|
|
170
|
+
const liveScene = stage === 'scene' ? store.scene : null;
|
|
171
|
+
if (stage === 'scene') {
|
|
172
|
+
if (typeof entityId !== 'string') {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error:
|
|
176
|
+
'capture-asset-preview stage "scene" photographs a live scene entity — pass entityId, not assetPath.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (shots !== undefined || shotSetDefinition !== undefined || compare !== undefined) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error:
|
|
183
|
+
'capture-asset-preview cannot combine stage "scene" with a shots mode, a shotSet definition or compare.',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!liveScene) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error:
|
|
190
|
+
'capture-asset-preview stage "scene" requires a bound viewport scene; none is bound yet.',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Resolve the screenshot subject through the SAME active-document adapter
|
|
195
|
+
// that produced `status.entities`, drives Hierarchy/Inspector selection and
|
|
196
|
+
// answers `frame-entity`. An adopted Play scene can own a stable OID in its
|
|
197
|
+
// projection without publishing that object through the shell's edit-mode
|
|
198
|
+
// `objectMap`; checking the map alone made the final door reject an id every
|
|
199
|
+
// preceding door had just accepted.
|
|
200
|
+
const entityObject =
|
|
201
|
+
typeof entityId === 'string'
|
|
202
|
+
? entityObject3D(activeDocumentAuthoring(store.shell), store.objectMap, entityId)
|
|
203
|
+
: null;
|
|
204
|
+
if (typeof entityId === 'string' && !entityObject) {
|
|
205
|
+
return { ok: false, error: `Entity not found: ${entityId}` };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const options = {
|
|
209
|
+
...(typeof width === 'number' ? { width } : {}),
|
|
210
|
+
...(typeof height === 'number' ? { height } : {}),
|
|
211
|
+
...(background ? { background: background as AssetPreviewBackground } : {}),
|
|
212
|
+
...(cameraChoice ? { camera: cameraChoice } : {}),
|
|
213
|
+
...(posedClip ? { pose: posedClip } : {}),
|
|
214
|
+
};
|
|
215
|
+
if (compare !== undefined) {
|
|
216
|
+
const compareRecord = compare as { glbBase64: string; forward?: unknown };
|
|
217
|
+
const refForward = parseForwardVector(compareRecord.forward);
|
|
218
|
+
const compareOptions = {
|
|
219
|
+
...(typeof width === 'number' ? { width } : {}),
|
|
220
|
+
...(typeof height === 'number' ? { height } : {}),
|
|
221
|
+
...(refForward ? { refForward } : {}),
|
|
222
|
+
};
|
|
223
|
+
const capture =
|
|
224
|
+
typeof assetPath === 'string'
|
|
225
|
+
? await captureModelComparePreview(assetPath, compareRecord.glbBase64, compareOptions)
|
|
226
|
+
: await captureEntityComparePreview(
|
|
227
|
+
entityObject!,
|
|
228
|
+
compareRecord.glbBase64,
|
|
229
|
+
compareOptions,
|
|
230
|
+
);
|
|
231
|
+
return { ok: true, data: { ...capture } };
|
|
232
|
+
}
|
|
233
|
+
if (shotSetDefinition !== undefined) {
|
|
234
|
+
const capture =
|
|
235
|
+
typeof glbBase64 === 'string'
|
|
236
|
+
? await captureShotSetGlbBytesPreview(glbBase64, shotSetDefinition, options)
|
|
237
|
+
: typeof assetPath === 'string'
|
|
238
|
+
? await captureShotSetModelPreview(assetPath, shotSetDefinition, options)
|
|
239
|
+
: captureShotSetAssetPreview(entityObject!, shotSetDefinition, options);
|
|
240
|
+
return { ok: true, data: { ...capture } };
|
|
241
|
+
}
|
|
242
|
+
if (shots === 'source') {
|
|
243
|
+
const capture =
|
|
244
|
+
typeof assetPath === 'string'
|
|
245
|
+
? await captureSourceReviewShotSetModelPreview(assetPath, parsedForward!, options)
|
|
246
|
+
: captureSourceReviewShotSetAssetPreview(entityObject!, parsedForward!, options);
|
|
247
|
+
return { ok: true, data: { ...capture } };
|
|
248
|
+
}
|
|
249
|
+
if (liveScene) {
|
|
250
|
+
const capture = captureSceneStageAssetPreview(entityObject!, liveScene, options);
|
|
251
|
+
return { ok: true, data: { ...capture } };
|
|
252
|
+
}
|
|
253
|
+
if (typeof glbBase64 === 'string') {
|
|
254
|
+
const capture = await captureGlbBytesAssetPreview(glbBase64, options);
|
|
255
|
+
return { ok: true, data: { ...capture } };
|
|
256
|
+
}
|
|
257
|
+
const capture =
|
|
258
|
+
typeof assetPath === 'string'
|
|
259
|
+
? await captureModelAssetPreview(assetPath, options)
|
|
260
|
+
: captureObjectAssetPreview(entityObject!, options);
|
|
261
|
+
return { ok: true, data: { ...capture } };
|
|
262
|
+
} catch (err) {
|
|
263
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
264
|
+
}
|
|
265
|
+
}
|