@genex-ai/cli-demo 1.5.2-dev.398 → 1.6.0-dev.403
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/dist/index.js +288 -10
- package/package.json +1 -1
- package/templates/asset-viewer/asset.config.json +25 -0
- package/templates/asset-viewer/genex-asset.example.json +222 -0
- package/templates/asset-viewer/index.html +200 -0
- package/templates/asset-viewer/package.json +24 -0
- package/templates/asset-viewer/public/fonts/Geist-variable.woff2 +0 -0
- package/templates/asset-viewer/public/fonts/GeistMono-variable.woff2 +0 -0
- package/templates/asset-viewer/shared-files.sha256.json +14 -0
- package/templates/asset-viewer/src/asset/PLACEHOLDER.ts +237 -0
- package/templates/asset-viewer/src/main.js +149 -0
- package/templates/asset-viewer/src/viewer/gates-overlay.js +521 -0
- package/templates/asset-viewer/src/viewer/hud.js +73 -0
- package/templates/asset-viewer/src/viewer/stage.js +776 -0
- package/templates/asset-viewer/tools/emit-manifest.mjs +653 -0
- package/templates/asset-viewer/tools/gates.mjs +682 -0
- package/templates/asset-viewer/tools/stamp-manifest.mjs +134 -0
- package/templates/asset-viewer/vite.config.js +24 -0
- package/templates/skills/genex-asset-author/SKILL.md +101 -0
- package/templates/skills/genex-game-director/references/routing-map.md +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Placeholder Asset - procedural Three.js asset.
|
|
2
|
+
// MIT License - Copyright (c) 2026 Genex. Free to use, modify and ship.
|
|
3
|
+
// Units: metres. Y-up, +Z forward. Origin at base centre (y = 0).
|
|
4
|
+
// Requires: three >= r160. No other dependency.
|
|
5
|
+
//
|
|
6
|
+
// This is the module scripts/new-asset.mjs seeds a new asset project with: a
|
|
7
|
+
// revolved pedestal marker that already satisfies every rule in the asset
|
|
8
|
+
// contract, so a freshly scaffolded folder builds, gates and deploys before a
|
|
9
|
+
// single line of the real asset is written. Replace the body - keep the shape.
|
|
10
|
+
|
|
11
|
+
import * as THREE from 'three';
|
|
12
|
+
|
|
13
|
+
// --- scaffold parameters ----------------------------------------------------
|
|
14
|
+
// scripts/new-asset.mjs rewrites these four lines from asset.config.json.
|
|
15
|
+
// Keep them on one line each, and keep the names: the scaffolder matches them.
|
|
16
|
+
const SLUG = 'placeholder';
|
|
17
|
+
const NAME = 'Placeholder Asset';
|
|
18
|
+
const SIZE_METERS: [number, number, number] = [0.4, 0.6, 0.4];
|
|
19
|
+
const TARGET_TRIANGLES = 1400;
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface PlaceholderOptions {
|
|
23
|
+
/** Feeds the in-file PRNG. Same seed and options → identical vertex data. */
|
|
24
|
+
seed?: number;
|
|
25
|
+
castShadow?: boolean;
|
|
26
|
+
receiveShadow?: boolean;
|
|
27
|
+
detail?: 'low' | 'standard' | 'high';
|
|
28
|
+
wireframe?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Deterministic PRNG. Contract rule 14: an asset never calls Math.random(), or
|
|
33
|
+
* every measured number in its manifest is a one-time coincidence.
|
|
34
|
+
*/
|
|
35
|
+
function mulberry32(seed: number): () => number {
|
|
36
|
+
let a = seed >>> 0;
|
|
37
|
+
return () => {
|
|
38
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
39
|
+
let t = a;
|
|
40
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
41
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
42
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The revolve profile in unit space: radius 0…0.5, height 0…1. Scaling the
|
|
48
|
+
* finished geometry by SIZE_METERS is what makes the bounding box match the
|
|
49
|
+
* stated real-world dimensions exactly, rather than approximately.
|
|
50
|
+
*/
|
|
51
|
+
const PROFILE: ReadonlyArray<readonly [number, number]> = [
|
|
52
|
+
[0.0, 0.0],
|
|
53
|
+
[0.46, 0.0],
|
|
54
|
+
[0.5, 0.012],
|
|
55
|
+
[0.5, 0.06],
|
|
56
|
+
[0.44, 0.085],
|
|
57
|
+
[0.3, 0.11],
|
|
58
|
+
[0.27, 0.15],
|
|
59
|
+
[0.25, 0.56],
|
|
60
|
+
[0.29, 0.64],
|
|
61
|
+
[0.28, 0.7],
|
|
62
|
+
[0.24, 0.76],
|
|
63
|
+
[0.2, 0.86],
|
|
64
|
+
[0.12, 0.945],
|
|
65
|
+
[0.0, 1.0],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const STUD_COUNT = 8;
|
|
69
|
+
const STUD_TRIANGLES = STUD_COUNT * 12;
|
|
70
|
+
const PROFILE_SEGMENTS = PROFILE.length - 1;
|
|
71
|
+
/** Lathe (2 tris per profile segment) + the collar ring (2 tris), per radial step. */
|
|
72
|
+
const TRIANGLES_PER_RADIAL_STEP = PROFILE_SEGMENTS * 2 + 2;
|
|
73
|
+
|
|
74
|
+
const DETAIL_SCALE: Record<'low' | 'standard' | 'high', number> = {
|
|
75
|
+
low: 0.5,
|
|
76
|
+
standard: 1,
|
|
77
|
+
high: 2,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function radialSegmentsFor(detail: 'low' | 'standard' | 'high'): number {
|
|
81
|
+
const budget = (TARGET_TRIANGLES - STUD_TRIANGLES) * DETAIL_SCALE[detail];
|
|
82
|
+
return Math.min(256, Math.max(12, Math.round(budget / TRIANGLES_PER_RADIAL_STEP)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A banded surface map built from a typed array. Contract rule 12: procedural
|
|
87
|
+
* detail comes from geometry plus DataTexture - CanvasTexture needs `document`
|
|
88
|
+
* and would break the module in a worker and in SSR.
|
|
89
|
+
*/
|
|
90
|
+
function createBandTexture(random: () => number): THREE.DataTexture {
|
|
91
|
+
const size = 64;
|
|
92
|
+
const data = new Uint8Array(size * size * 4);
|
|
93
|
+
for (let y = 0; y < size; y += 1) {
|
|
94
|
+
const v = y / (size - 1);
|
|
95
|
+
// Two darker collars, and a slight vertical gradient elsewhere.
|
|
96
|
+
const band = v > 0.56 && v < 0.68 ? 0.45 : v > 0.2 && v < 0.26 ? 0.62 : 1;
|
|
97
|
+
for (let x = 0; x < size; x += 1) {
|
|
98
|
+
const grain = 0.94 + random() * 0.06;
|
|
99
|
+
const shade = Math.round(255 * band * grain * (0.78 + v * 0.22));
|
|
100
|
+
const i = (y * size + x) * 4;
|
|
101
|
+
data[i] = shade;
|
|
102
|
+
data[i + 1] = Math.round(shade * 0.97);
|
|
103
|
+
data[i + 2] = Math.round(shade * 0.93);
|
|
104
|
+
data[i + 3] = 255;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
|
|
108
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
109
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
110
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
111
|
+
// DataTexture defaults to NearestFilter with no mipmaps, which reads as
|
|
112
|
+
// blocky the moment the surface is bigger than a few hundred pixels.
|
|
113
|
+
texture.magFilter = THREE.LinearFilter;
|
|
114
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
115
|
+
texture.generateMipmaps = true;
|
|
116
|
+
texture.anisotropy = 4;
|
|
117
|
+
texture.needsUpdate = true;
|
|
118
|
+
return texture;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build the placeholder asset.
|
|
123
|
+
*
|
|
124
|
+
* Origin sits at the base centre on y = 0, so a game does
|
|
125
|
+
* `model.position.set(x, groundY, z)` and it stands on the floor.
|
|
126
|
+
*/
|
|
127
|
+
export function createPlaceholderModel(options: PlaceholderOptions = {}): THREE.Group {
|
|
128
|
+
const {
|
|
129
|
+
seed = 1,
|
|
130
|
+
castShadow = true,
|
|
131
|
+
receiveShadow = true,
|
|
132
|
+
detail = 'standard',
|
|
133
|
+
wireframe = false,
|
|
134
|
+
} = options;
|
|
135
|
+
|
|
136
|
+
const random = mulberry32(seed);
|
|
137
|
+
const [width, height, depth] = SIZE_METERS;
|
|
138
|
+
const radial = radialSegmentsFor(detail);
|
|
139
|
+
|
|
140
|
+
// --- body ----------------------------------------------------------------
|
|
141
|
+
const bodyGeometry = new THREE.LatheGeometry(
|
|
142
|
+
PROFILE.map(([r, y]) => new THREE.Vector2(r, y)),
|
|
143
|
+
radial,
|
|
144
|
+
);
|
|
145
|
+
bodyGeometry.scale(width, height, depth);
|
|
146
|
+
bodyGeometry.name = `${SLUG}-body`;
|
|
147
|
+
|
|
148
|
+
const bandTexture = createBandTexture(random);
|
|
149
|
+
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
150
|
+
color: 0xd6dae1,
|
|
151
|
+
map: bandTexture,
|
|
152
|
+
roughness: 0.62,
|
|
153
|
+
metalness: 0.05,
|
|
154
|
+
wireframe,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
158
|
+
body.name = 'body';
|
|
159
|
+
body.castShadow = castShadow;
|
|
160
|
+
body.receiveShadow = receiveShadow;
|
|
161
|
+
|
|
162
|
+
// --- collar --------------------------------------------------------------
|
|
163
|
+
const collarGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.045, radial, 1, true);
|
|
164
|
+
collarGeometry.translate(0, 0.62, 0);
|
|
165
|
+
collarGeometry.scale(width, height, depth);
|
|
166
|
+
collarGeometry.name = `${SLUG}-collar`;
|
|
167
|
+
|
|
168
|
+
const collarMaterial = new THREE.MeshStandardMaterial({
|
|
169
|
+
color: 0x8d949e,
|
|
170
|
+
roughness: 0.34,
|
|
171
|
+
metalness: 0.85,
|
|
172
|
+
side: THREE.DoubleSide,
|
|
173
|
+
wireframe,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const collar = new THREE.Mesh(collarGeometry, collarMaterial);
|
|
177
|
+
collar.name = 'collar';
|
|
178
|
+
collar.castShadow = castShadow;
|
|
179
|
+
collar.receiveShadow = receiveShadow;
|
|
180
|
+
|
|
181
|
+
// --- studs ---------------------------------------------------------------
|
|
182
|
+
// Contract rule 15: repetition is one InstancedMesh, never N meshes.
|
|
183
|
+
const studSize = Math.min(width, depth) * 0.09;
|
|
184
|
+
const studGeometry = new THREE.BoxGeometry(studSize, studSize * 0.6, studSize);
|
|
185
|
+
studGeometry.name = `${SLUG}-stud`;
|
|
186
|
+
const studMaterial = new THREE.MeshStandardMaterial({
|
|
187
|
+
color: 0x5f6570,
|
|
188
|
+
roughness: 0.45,
|
|
189
|
+
metalness: 0.6,
|
|
190
|
+
wireframe,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const studs = new THREE.InstancedMesh(studGeometry, studMaterial, STUD_COUNT);
|
|
194
|
+
studs.name = 'studs';
|
|
195
|
+
studs.castShadow = castShadow;
|
|
196
|
+
studs.receiveShadow = receiveShadow;
|
|
197
|
+
const matrix = new THREE.Matrix4();
|
|
198
|
+
const position = new THREE.Vector3();
|
|
199
|
+
const quaternion = new THREE.Quaternion();
|
|
200
|
+
const scale = new THREE.Vector3(1, 1, 1);
|
|
201
|
+
for (let i = 0; i < STUD_COUNT; i += 1) {
|
|
202
|
+
const angle = (i / STUD_COUNT) * Math.PI * 2;
|
|
203
|
+
const jitter = (random() - 0.5) * 0.05;
|
|
204
|
+
position.set(Math.sin(angle) * 0.38 * width, 0.038 * height + studSize * 0.3, Math.cos(angle) * 0.38 * depth);
|
|
205
|
+
quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle + jitter);
|
|
206
|
+
matrix.compose(position, quaternion, scale);
|
|
207
|
+
studs.setMatrixAt(i, matrix);
|
|
208
|
+
}
|
|
209
|
+
studs.instanceMatrix.needsUpdate = true;
|
|
210
|
+
|
|
211
|
+
// --- assembly ------------------------------------------------------------
|
|
212
|
+
const root = new THREE.Group();
|
|
213
|
+
root.name = SLUG;
|
|
214
|
+
root.add(body, collar, studs);
|
|
215
|
+
|
|
216
|
+
const nodes: Record<string, THREE.Object3D> = { body, collar, studs };
|
|
217
|
+
const meshes = [body, collar, studs];
|
|
218
|
+
const materials = [bodyMaterial, collarMaterial, studMaterial];
|
|
219
|
+
const geometries = [bodyGeometry, collarGeometry, studGeometry];
|
|
220
|
+
const textures = [bandTexture];
|
|
221
|
+
|
|
222
|
+
root.userData.assetRuntime = { nodes, meshes, materials, geometries };
|
|
223
|
+
root.userData.assetInfo = {
|
|
224
|
+
slug: SLUG,
|
|
225
|
+
name: NAME,
|
|
226
|
+
sizeMeters: [width, height, depth],
|
|
227
|
+
units: 'meters',
|
|
228
|
+
license: 'MIT',
|
|
229
|
+
};
|
|
230
|
+
root.userData.dispose = () => {
|
|
231
|
+
for (const geometry of geometries) geometry.dispose();
|
|
232
|
+
for (const material of materials) material.dispose();
|
|
233
|
+
for (const texture of textures) texture.dispose();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return root;
|
|
237
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Viewer bootstrap. Byte-identical in all ten asset projects - the only file
|
|
2
|
+
// that varies is src/asset/<slug>.ts, which this module finds by glob rather
|
|
3
|
+
// than by name so it never has to know which asset it is showing.
|
|
4
|
+
//
|
|
5
|
+
// Copied verbatim into every asset project and hashed by gate G13. Edit it in
|
|
6
|
+
// packages/asset-viewer-template/template/src.
|
|
7
|
+
|
|
8
|
+
import * as THREE from "three";
|
|
9
|
+
import assetConfig from "../asset.config.json";
|
|
10
|
+
import { createStage } from "./viewer/stage.js";
|
|
11
|
+
import { createHud, showFault } from "./viewer/hud.js";
|
|
12
|
+
import { installGateHooks, isGatesMode, measureStatic } from "./viewer/gates-overlay.js";
|
|
13
|
+
|
|
14
|
+
const EXPLODE_SPREAD = 0.35;
|
|
15
|
+
|
|
16
|
+
function resolveFactory() {
|
|
17
|
+
const modules = import.meta.glob("./asset/*.ts", { eager: true });
|
|
18
|
+
const paths = Object.keys(modules);
|
|
19
|
+
if (paths.length !== 1) {
|
|
20
|
+
throw new Error(`src/asset must hold exactly one module, found ${paths.length}: ${paths.join(", ") || "none"}`);
|
|
21
|
+
}
|
|
22
|
+
const module = modules[paths[0]];
|
|
23
|
+
const names = Object.keys(module).filter((n) => /^create[A-Z][A-Za-z0-9_]*Model$/.test(n));
|
|
24
|
+
if (names.length !== 1) {
|
|
25
|
+
throw new Error(`${paths[0]} must export exactly one create<Name>Model function, found ${names.length}`);
|
|
26
|
+
}
|
|
27
|
+
return { factory: module[names[0]], entry: names[0], path: paths[0] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setupExplode(root) {
|
|
31
|
+
const nodes = Object.values(root.userData?.assetRuntime?.nodes ?? {}).filter((n) => n && n.isObject3D);
|
|
32
|
+
const box = new THREE.Box3().setFromObject(root);
|
|
33
|
+
const centre = box.getCenter(new THREE.Vector3());
|
|
34
|
+
const diagonal = Math.max(box.getSize(new THREE.Vector3()).length(), 0.2);
|
|
35
|
+
|
|
36
|
+
// Push each part away from the model's centre along the direction of ITS OWN
|
|
37
|
+
// bounds, not its origin: assets are encouraged to bake offsets into geometry,
|
|
38
|
+
// so every part's `position` is usually (0,0,0) and origin-based directions
|
|
39
|
+
// would slide the whole model in one lump instead of taking it apart.
|
|
40
|
+
const parts = nodes.map((node) => {
|
|
41
|
+
const nodeCentre = new THREE.Box3().setFromObject(node).getCenter(new THREE.Vector3());
|
|
42
|
+
const direction = nodeCentre.sub(centre);
|
|
43
|
+
const offCentre = direction.length() > diagonal * 0.02;
|
|
44
|
+
return {
|
|
45
|
+
node,
|
|
46
|
+
home: node.position.clone(),
|
|
47
|
+
direction: offCentre ? direction.normalize() : null, // null → this part is the core, it stays
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const scale = diagonal * EXPLODE_SPREAD;
|
|
51
|
+
return (t) => {
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (!part.direction) continue;
|
|
54
|
+
part.node.position.copy(part.home).addScaledVector(part.direction, t * scale);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function boot() {
|
|
60
|
+
const container = document.getElementById("stage");
|
|
61
|
+
const gates = isGatesMode();
|
|
62
|
+
const { factory } = resolveFactory();
|
|
63
|
+
|
|
64
|
+
const root = factory();
|
|
65
|
+
if (!root || !root.isObject3D) throw new Error("the asset factory must return a THREE.Group");
|
|
66
|
+
|
|
67
|
+
const stage = createStage({ container, deterministic: gates, groundShadow: assetConfig.viewer?.groundShadow !== false });
|
|
68
|
+
stage.assetRoot.add(root);
|
|
69
|
+
|
|
70
|
+
const statics = measureStatic(root);
|
|
71
|
+
const box = new THREE.Box3().setFromObject(root);
|
|
72
|
+
stage.fit(box, assetConfig.viewer?.cameraDistanceMul ?? 1);
|
|
73
|
+
|
|
74
|
+
document.title = `${assetConfig.name} - Genex asset`;
|
|
75
|
+
const hud = createHud(document);
|
|
76
|
+
hud.setAsset({ name: assetConfig.name, summary: assetConfig.summary });
|
|
77
|
+
hud.setStats({
|
|
78
|
+
sizeMeters: statics.sizeMeters,
|
|
79
|
+
triangles: statics.triangles,
|
|
80
|
+
drawCalls: statics.drawCalls,
|
|
81
|
+
materials: statics.materials,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const applyExplode = setupExplode(root);
|
|
85
|
+
let explodeTarget = 0;
|
|
86
|
+
let explode = 0;
|
|
87
|
+
let wireframe = false;
|
|
88
|
+
|
|
89
|
+
window.addEventListener("keydown", (event) => {
|
|
90
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
91
|
+
const key = event.key.toLowerCase();
|
|
92
|
+
if (key === "w") {
|
|
93
|
+
wireframe = !wireframe;
|
|
94
|
+
root.traverse((obj) => {
|
|
95
|
+
const materials = Array.isArray(obj.material) ? obj.material : obj.material ? [obj.material] : [];
|
|
96
|
+
for (const material of materials) if ("wireframe" in material) material.wireframe = wireframe;
|
|
97
|
+
});
|
|
98
|
+
stage.invalidate();
|
|
99
|
+
} else if (key === "e") {
|
|
100
|
+
explodeTarget = explodeTarget > 0 ? 0 : 1;
|
|
101
|
+
} else if (key === " ") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
stage.toggleTurntable();
|
|
104
|
+
} else if (key === "r") {
|
|
105
|
+
explodeTarget = 0;
|
|
106
|
+
// Snap the parts home BEFORE the bounds are measured below. setFromObject
|
|
107
|
+
// on a still-exploded model fits the camera to the blown-apart bounds, so
|
|
108
|
+
// reset left the asset standing smaller in frame than it started. Measured:
|
|
109
|
+
// pressing R twice fixed it, because the second press ran with the parts
|
|
110
|
+
// already retracted.
|
|
111
|
+
explode = 0;
|
|
112
|
+
applyExplode(0);
|
|
113
|
+
wireframe = false;
|
|
114
|
+
root.traverse((obj) => {
|
|
115
|
+
const materials = Array.isArray(obj.material) ? obj.material : obj.material ? [obj.material] : [];
|
|
116
|
+
for (const material of materials) if ("wireframe" in material) material.wireframe = false;
|
|
117
|
+
});
|
|
118
|
+
stage.fit(new THREE.Box3().setFromObject(root), assetConfig.viewer?.cameraDistanceMul ?? 1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (gates) {
|
|
123
|
+
// Deterministic pose: no turntable, no explode, camera at the framed home.
|
|
124
|
+
stage.setTurntable(false);
|
|
125
|
+
stage.renderOnce();
|
|
126
|
+
installGateHooks({ stage, root, factory, config: assetConfig, statics });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
stage.start((dt) => {
|
|
131
|
+
if (explode !== explodeTarget) {
|
|
132
|
+
const step = dt * 2.2;
|
|
133
|
+
explode = explodeTarget > explode ? Math.min(explodeTarget, explode + step) : Math.max(explodeTarget, explode - step);
|
|
134
|
+
applyExplode(explode);
|
|
135
|
+
stage.invalidate();
|
|
136
|
+
// Re-bake the ground shadow once the parts have settled, not on every
|
|
137
|
+
// frame of the animation: the turntable moves the camera rather than the
|
|
138
|
+
// model, so this is the only thing in the viewer that can change it.
|
|
139
|
+
if (explode === explodeTarget) stage.refreshShadow(new THREE.Box3().setFromObject(root));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
boot();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
showFault(`Asset viewer failed to start.\n\n${error?.stack ?? error}`);
|
|
149
|
+
}
|