@celestia-island/hikari 0.55.101 → 0.55.102
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/package.json +1 -1
- package/src/board3dSurface.test.ts +35 -0
- package/src/components/HkBoard3D.controls.test.tsx +140 -0
- package/src/components/HkBoard3D.test.tsx +208 -0
- package/src/components/HkBoard3D.tsx +252 -2
- package/src/components/HkMinimap3D.test.tsx +1 -1
- package/src/index.ts +13 -0
- package/src/scene3d/ground.test.ts +93 -0
- package/src/scene3d/ground.ts +199 -0
- package/src/scene3d/lighting.test.ts +129 -0
- package/src/scene3d/lighting.ts +140 -0
- package/src/scene3d/modelLayer.test.ts +351 -0
- package/src/scene3d/modelLayer.ts +371 -0
package/package.json
CHANGED
|
@@ -20,15 +20,27 @@ import * as barrel from "./index";
|
|
|
20
20
|
type Engine = barrel.Board3DEngine;
|
|
21
21
|
type Def = barrel.Board3DObjectDef;
|
|
22
22
|
type Vec = barrel.Vec3;
|
|
23
|
+
type ModelOpts = barrel.Board3DModelOptions;
|
|
24
|
+
type CameraCfg = barrel.Board3DCameraConfig;
|
|
25
|
+
type FocusOpts = barrel.Board3DFocusOptions;
|
|
26
|
+
type ViewModeT = barrel.Board3DViewMode;
|
|
23
27
|
|
|
24
28
|
// Type-only probes: these lines exist so the checker resolves the three
|
|
25
29
|
// exported types through the barrel itself.
|
|
26
30
|
const engineProbe: Engine | null = null;
|
|
27
31
|
const defProbe: Def | null = null;
|
|
28
32
|
const vecProbe: Vec = [0, 0, 0];
|
|
33
|
+
const modelProbe: ModelOpts | null = null;
|
|
34
|
+
const cameraProbe: CameraCfg = { fov: 45 };
|
|
35
|
+
const focusProbe: FocusOpts = { padding: 1.6, lateralBias: 0 };
|
|
36
|
+
const viewModeProbe: ViewModeT = "orbit";
|
|
29
37
|
void engineProbe;
|
|
30
38
|
void defProbe;
|
|
31
39
|
void vecProbe;
|
|
40
|
+
void modelProbe;
|
|
41
|
+
void cameraProbe;
|
|
42
|
+
void focusProbe;
|
|
43
|
+
void viewModeProbe;
|
|
32
44
|
|
|
33
45
|
describe("hikari barrel — 3D board surface", () => {
|
|
34
46
|
it("exports both 3D components", () => {
|
|
@@ -57,6 +69,29 @@ describe("hikari barrel — 3D board surface", () => {
|
|
|
57
69
|
}
|
|
58
70
|
});
|
|
59
71
|
|
|
72
|
+
it("exports the GLB model layer", () => {
|
|
73
|
+
expect(typeof barrel.ModelLayer).toBe("function");
|
|
74
|
+
// The engine's model surface is part of that contract.
|
|
75
|
+
const engineMethods = [
|
|
76
|
+
"loadModel",
|
|
77
|
+
"setModelOpacity",
|
|
78
|
+
"highlightModel",
|
|
79
|
+
"clearHighlights",
|
|
80
|
+
"modelObject",
|
|
81
|
+
"modelWorldPosition",
|
|
82
|
+
"removeModel",
|
|
83
|
+
] as const;
|
|
84
|
+
const engine = null as Engine | null;
|
|
85
|
+
void engine;
|
|
86
|
+
// Type-level: the methods exist on Board3DEngine (vue-tsc covers this
|
|
87
|
+
// file, so a rename breaks the gate rather than a consumer).
|
|
88
|
+
type HasModelSurface = Engine extends Record<(typeof engineMethods)[number], unknown>
|
|
89
|
+
? true
|
|
90
|
+
: false;
|
|
91
|
+
const has: HasModelSurface = true;
|
|
92
|
+
expect(has).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
60
95
|
it("exports the minimap governance constants, drag sensitivity included", () => {
|
|
61
96
|
// A consumer writing its own drag control needs the same sensitivity
|
|
62
97
|
// the built-in one uses — it must not stay module-private.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* The board's camera interplay with the REAL OrbitControls (r185) — the
|
|
2
|
+
* main suite mocks controls wholesale, which hides exactly the behaviours
|
|
3
|
+
* an externally-driven consumer (the holographic panel) depends on:
|
|
4
|
+
* external pose writes surviving the per-frame controls.update(), the
|
|
5
|
+
* orbit distance window clamping such poses, and the window being
|
|
6
|
+
* relaxed when the board is not interactive. Only the WebGLRenderer and
|
|
7
|
+
* the CSS2D renderer stay faked (no GL in happy-dom). */
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { createApp, defineComponent, h, nextTick } from "vue";
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
|
|
13
|
+
const H = vi.hoisted(() => {
|
|
14
|
+
class FakeWebGLRenderer {
|
|
15
|
+
static instances: FakeWebGLRenderer[] = [];
|
|
16
|
+
domElement: HTMLCanvasElement;
|
|
17
|
+
shadowMap = { enabled: false };
|
|
18
|
+
disposed = false;
|
|
19
|
+
constructor(opts: { canvas: HTMLCanvasElement }) {
|
|
20
|
+
this.domElement = opts.canvas;
|
|
21
|
+
FakeWebGLRenderer.instances.push(this);
|
|
22
|
+
}
|
|
23
|
+
setPixelRatio(): void {}
|
|
24
|
+
setSize(): void {}
|
|
25
|
+
setClearColor(): void {}
|
|
26
|
+
render(): void {}
|
|
27
|
+
dispose(): void {
|
|
28
|
+
this.disposed = true;
|
|
29
|
+
}
|
|
30
|
+
forceContextLoss(): void {}
|
|
31
|
+
}
|
|
32
|
+
return { FakeWebGLRenderer };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock("three", async (importOriginal) => {
|
|
36
|
+
const actual = await importOriginal<typeof import("three")>();
|
|
37
|
+
return { ...actual, WebGLRenderer: H.FakeWebGLRenderer };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
vi.mock("three/examples/jsm/renderers/CSS2DRenderer.js", () => {
|
|
41
|
+
class FakeCSS2DRenderer {
|
|
42
|
+
domElement: HTMLElement = document.createElement("div");
|
|
43
|
+
setSize(): void {}
|
|
44
|
+
render(): void {}
|
|
45
|
+
}
|
|
46
|
+
return { CSS2DRenderer: FakeCSS2DRenderer };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
import HkBoard3D, { type Board3DEngine } from "./HkBoard3D";
|
|
50
|
+
|
|
51
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
52
|
+
|
|
53
|
+
function mountBoard(
|
|
54
|
+
props: Record<string, unknown>,
|
|
55
|
+
onReady: (engine: Board3DEngine) => void,
|
|
56
|
+
) {
|
|
57
|
+
const container = document.createElement("div");
|
|
58
|
+
document.body.appendChild(container);
|
|
59
|
+
const app = createApp(
|
|
60
|
+
defineComponent({
|
|
61
|
+
setup() {
|
|
62
|
+
return () => h(HkBoard3D, { ...props, onReady: onReady as never });
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
app.mount(container);
|
|
67
|
+
mounts.push({ app, container });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function tickFrames(): Promise<void> {
|
|
71
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
H.FakeWebGLRenderer.instances = [];
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
for (const m of mounts.splice(0)) {
|
|
80
|
+
m.app.unmount();
|
|
81
|
+
m.container.remove();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("HkBoard3D × real OrbitControls", () => {
|
|
86
|
+
it("lets an externally driven camera pose survive the frame loop", async () => {
|
|
87
|
+
let engine: Board3DEngine | null = null;
|
|
88
|
+
mountBoard({ interactive: false }, (e) => (engine = e));
|
|
89
|
+
await nextTick();
|
|
90
|
+
await tickFrames();
|
|
91
|
+
|
|
92
|
+
// The panel's pattern: write position+target per animation tick.
|
|
93
|
+
engine!.camera.position.set(300, 120, 400);
|
|
94
|
+
engine!.controls.target.set(10, 0, 5);
|
|
95
|
+
engine!.controls.update();
|
|
96
|
+
await tickFrames();
|
|
97
|
+
expect(engine!.camera.position.x).toBeCloseTo(300, 3);
|
|
98
|
+
expect(engine!.controls.target.x).toBeCloseTo(10, 3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("clamps an external pose into the orbit window when interactive", async () => {
|
|
102
|
+
let engine: Board3DEngine | null = null;
|
|
103
|
+
mountBoard({ interactive: true, minDistance: 2, maxDistance: 600 }, (e) => (engine = e));
|
|
104
|
+
await nextTick();
|
|
105
|
+
await tickFrames();
|
|
106
|
+
|
|
107
|
+
engine!.camera.position.set(3000, 0, 0);
|
|
108
|
+
engine!.controls.target.set(0, 0, 0);
|
|
109
|
+
await tickFrames();
|
|
110
|
+
expect(engine!.camera.position.length()).toBeLessThanOrEqual(601);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("relaxes the distance window when the board is not interactive", async () => {
|
|
114
|
+
let engine: Board3DEngine | null = null;
|
|
115
|
+
mountBoard({ interactive: false, minDistance: 2, maxDistance: 600 }, (e) => (engine = e));
|
|
116
|
+
await nextTick();
|
|
117
|
+
await tickFrames();
|
|
118
|
+
|
|
119
|
+
expect(engine!.controls.minDistance).toBe(0);
|
|
120
|
+
expect(engine!.controls.maxDistance).toBe(Infinity);
|
|
121
|
+
// A far pose is left alone — nothing fights the external driver.
|
|
122
|
+
engine!.camera.position.set(5000, 0, 0);
|
|
123
|
+
engine!.controls.target.set(0, 0, 0);
|
|
124
|
+
await tickFrames();
|
|
125
|
+
expect(engine!.camera.position.length()).toBeGreaterThan(4000);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("applies a camera-configured distance window", async () => {
|
|
129
|
+
let engine: Board3DEngine | null = null;
|
|
130
|
+
mountBoard({ interactive: true }, (e) => (engine = e));
|
|
131
|
+
await nextTick();
|
|
132
|
+
engine!.applyCameraConfig({ minDistance: 10, maxDistance: 50 });
|
|
133
|
+
expect(engine!.controls.minDistance).toBe(10);
|
|
134
|
+
expect(engine!.controls.maxDistance).toBe(50);
|
|
135
|
+
// A reversed window normalises.
|
|
136
|
+
engine!.applyCameraConfig({ minDistance: 80, maxDistance: 20 });
|
|
137
|
+
expect(engine!.controls.minDistance).toBe(20);
|
|
138
|
+
expect(engine!.controls.maxDistance).toBe(80);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -432,6 +432,214 @@ describe("HkBoard3D", () => {
|
|
|
432
432
|
expect(engine!.camera.position.distanceTo(engine!.controls.target)).toBeLessThan(100);
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
+
it("applies a camera configuration and ignores degenerate values", async () => {
|
|
436
|
+
let engine: Board3DEngine | null = null;
|
|
437
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
438
|
+
await nextTick();
|
|
439
|
+
|
|
440
|
+
engine!.applyCameraConfig({
|
|
441
|
+
fov: 30,
|
|
442
|
+
near: 0.5,
|
|
443
|
+
far: 900,
|
|
444
|
+
position: [4, 5, 6],
|
|
445
|
+
target: [1, 1, 1],
|
|
446
|
+
});
|
|
447
|
+
expect(engine!.camera.fov).toBe(30);
|
|
448
|
+
expect(engine!.camera.near).toBeCloseTo(0.5, 6);
|
|
449
|
+
expect(engine!.camera.far).toBeCloseTo(900, 6);
|
|
450
|
+
expect(engine!.camera.position.x).toBeCloseTo(4, 6);
|
|
451
|
+
expect(engine!.controls.target.x).toBeCloseTo(1, 6);
|
|
452
|
+
|
|
453
|
+
// A degenerate fov must not brick the projection (round-3 defect
|
|
454
|
+
// class): the previous good value survives.
|
|
455
|
+
engine!.applyCameraConfig({ fov: 0 });
|
|
456
|
+
expect(engine!.camera.fov).toBe(30);
|
|
457
|
+
// Neither may a non-finite pose.
|
|
458
|
+
engine!.applyCameraConfig({ position: [Number.NaN, 0, 0] as never });
|
|
459
|
+
expect(engine!.camera.position.x).toBeCloseTo(4, 6);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("flies to a focus sphere with padding and lateral bias", async () => {
|
|
463
|
+
let engine: Board3DEngine | null = null;
|
|
464
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
465
|
+
await nextTick();
|
|
466
|
+
|
|
467
|
+
engine!.flyToFocus([0, 0, 0], 10, 0);
|
|
468
|
+
const dist = engine!.camera.position.length();
|
|
469
|
+
// padding 1.6 over the fit distance for radius 10 at fov 45.
|
|
470
|
+
const fit = 10 / Math.sin(((45 * Math.PI) / 180 / 2)) * 1.6;
|
|
471
|
+
expect(dist).toBeCloseTo(fit, 1);
|
|
472
|
+
expect(engine!.controls.target.length()).toBeCloseTo(0, 6);
|
|
473
|
+
|
|
474
|
+
// A non-finite target or radius is ignored, not applied.
|
|
475
|
+
const before = engine!.camera.position.clone();
|
|
476
|
+
engine!.flyToFocus([Number.NaN, 0, 0] as never, 5, 0);
|
|
477
|
+
expect(engine!.camera.position.equals(before)).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("parks the camera top-down and restores the orbit pose", async () => {
|
|
481
|
+
let engine: Board3DEngine | null = null;
|
|
482
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
483
|
+
await nextTick();
|
|
484
|
+
|
|
485
|
+
engine!.applyCameraConfig({ position: [8, 6, 10], target: [2, 0, 2] });
|
|
486
|
+
engine!.setViewMode("topdown", 0);
|
|
487
|
+
expect(engine!.viewMode()).toBe("topdown");
|
|
488
|
+
// Instant application (the 300 ms flight is time-based; the mode and
|
|
489
|
+
// the destination are already computed).
|
|
490
|
+
const t = engine!.controls.target;
|
|
491
|
+
const p = engine!.camera.position;
|
|
492
|
+
expect(p.x).toBeCloseTo(t.x, 3);
|
|
493
|
+
// Directly above, bar the hair of Z that keeps `up` from degenerating.
|
|
494
|
+
expect(Math.abs(p.z - t.z)).toBeLessThan(0.01);
|
|
495
|
+
expect(p.y).toBeGreaterThan(t.y);
|
|
496
|
+
|
|
497
|
+
engine!.setViewMode("orbit", 0);
|
|
498
|
+
expect(engine!.viewMode()).toBe("orbit");
|
|
499
|
+
// …and the saved orbit pose came back.
|
|
500
|
+
expect(engine!.camera.position.x).toBeCloseTo(8, 3);
|
|
501
|
+
expect(engine!.controls.target.x).toBeCloseTo(2, 3);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("builds a ground on demand and strips it on unmount", async () => {
|
|
505
|
+
let engine: Board3DEngine | null = null;
|
|
506
|
+
const { container } = mountBoard(
|
|
507
|
+
{},
|
|
508
|
+
{ ready: ((e: Board3DEngine) => { engine = e; }) as never },
|
|
509
|
+
);
|
|
510
|
+
await nextTick();
|
|
511
|
+
const sceneChildrenBefore = engine!.scene.children.length;
|
|
512
|
+
|
|
513
|
+
engine!.setGround({ enabled: true, y: -1 });
|
|
514
|
+
expect(engine!.scene.children.length).toBe(sceneChildrenBefore + 1);
|
|
515
|
+
|
|
516
|
+
engine!.setGround({ enabled: false });
|
|
517
|
+
expect(engine!.scene.children.length).toBe(sceneChildrenBefore);
|
|
518
|
+
|
|
519
|
+
// Rebuild for the unmount path: a leaked ground would survive the
|
|
520
|
+
// component's death in the (detached) scene.
|
|
521
|
+
engine!.setGround({ enabled: true });
|
|
522
|
+
const m = mounts.splice(0)[0];
|
|
523
|
+
m.app.unmount();
|
|
524
|
+
m.container.remove();
|
|
525
|
+
expect(engine!.scene.children.some((c) => (c as THREE.Group).isObject3D && c.type === "Group" && c.children.length === 2)).toBe(false);
|
|
526
|
+
void container;
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("refits the ground onto the framed content", async () => {
|
|
530
|
+
let engine: Board3DEngine | null = null;
|
|
531
|
+
// No minimap: its helper glyph is also a two-child group and would
|
|
532
|
+
// win the lookup below.
|
|
533
|
+
mountBoard({ minimap: false }, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
534
|
+
await nextTick();
|
|
535
|
+
engine!.setGround({ enabled: true });
|
|
536
|
+
// The grid plane is the second child of the ground group.
|
|
537
|
+
const grid = engine!.scene.children
|
|
538
|
+
.find((c) => c.type === "Group" && c.children.length === 2)?.children[1] as THREE.Mesh;
|
|
539
|
+
expect(grid).toBeTruthy();
|
|
540
|
+
const uniforms = (grid.material as THREE.ShaderMaterial).uniforms as Record<
|
|
541
|
+
string,
|
|
542
|
+
{ value: unknown }
|
|
543
|
+
>;
|
|
544
|
+
|
|
545
|
+
const body = new THREE.Mesh(new THREE.SphereGeometry(2, 8, 6), new THREE.MeshBasicMaterial());
|
|
546
|
+
body.position.set(100, 0, -40);
|
|
547
|
+
engine!.setObject("far", { object: body });
|
|
548
|
+
engine!.frameAll(1.3, 0);
|
|
549
|
+
const center = uniforms.uCenter.value as THREE.Vector2;
|
|
550
|
+
expect(center.x).toBeCloseTo(100, 6);
|
|
551
|
+
expect(center.y).toBeCloseTo(-40, 6);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("clips the camera planes to the framed extent when autoClipping", async () => {
|
|
555
|
+
let engine: Board3DEngine | null = null;
|
|
556
|
+
mountBoard(
|
|
557
|
+
{ autoClipping: true },
|
|
558
|
+
{ ready: ((e: Board3DEngine) => { engine = e; }) as never },
|
|
559
|
+
);
|
|
560
|
+
await nextTick();
|
|
561
|
+
const near0 = engine!.camera.near;
|
|
562
|
+
const far0 = engine!.camera.far;
|
|
563
|
+
// A BIG subject (not a distant small one — framing flies to it) is
|
|
564
|
+
// what pushes the planes apart.
|
|
565
|
+
const body = new THREE.Mesh(new THREE.SphereGeometry(200, 8, 6), new THREE.MeshBasicMaterial());
|
|
566
|
+
engine!.setObject("big", { object: body });
|
|
567
|
+
engine!.frameAll(1.3, 0);
|
|
568
|
+
expect(engine!.camera.near).toBeGreaterThan(near0);
|
|
569
|
+
expect(engine!.camera.far).toBeGreaterThan(far0);
|
|
570
|
+
expect(engine!.camera.far / engine!.camera.near).toBeGreaterThan(1000);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("clears the studio environment on demand", async () => {
|
|
574
|
+
let engine: Board3DEngine | null = null;
|
|
575
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
576
|
+
await nextTick();
|
|
577
|
+
engine!.scene.environment = new THREE.Texture();
|
|
578
|
+
engine!.applyEnvironment("none");
|
|
579
|
+
expect(engine!.scene.environment).toBeNull();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("fits the ground and drives the ambient directly", async () => {
|
|
583
|
+
let engine: Board3DEngine | null = null;
|
|
584
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
585
|
+
await nextTick();
|
|
586
|
+
|
|
587
|
+
engine!.setGround({ enabled: true });
|
|
588
|
+
engine!.applyLighting({ ambientIntensity: 0.9 });
|
|
589
|
+
engine!.fitGround({ min: [0, 0, 0], max: [10, 0, 10] });
|
|
590
|
+
engine!.setAmbientIntensity(0.3);
|
|
591
|
+
const ambient = engine!.scene.children.find(
|
|
592
|
+
(c) => c.constructor.name === "AmbientLight",
|
|
593
|
+
) as unknown as { intensity: number };
|
|
594
|
+
expect(ambient.intensity).toBeCloseTo(0.3, 5);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("disposes the lighting rig (and its shadow map) on unmount", async () => {
|
|
598
|
+
let engine: Board3DEngine | null = null;
|
|
599
|
+
mountBoard(
|
|
600
|
+
{},
|
|
601
|
+
{ ready: ((e: Board3DEngine) => { engine = e; }) as never },
|
|
602
|
+
);
|
|
603
|
+
await nextTick();
|
|
604
|
+
engine!.applyLighting({
|
|
605
|
+
ambientIntensity: 0.9,
|
|
606
|
+
points: [{ color: [1, 0, 0], intensity: 2, range: 10, position: [0, 3, 0] }],
|
|
607
|
+
});
|
|
608
|
+
const sun = engine!.scene.children.find(
|
|
609
|
+
(c) => c.constructor.name === "DirectionalLight",
|
|
610
|
+
) as unknown as { dispose: () => void; shadow: { dispose: () => void } };
|
|
611
|
+
const sunSpy = vi.spyOn(sun, "dispose");
|
|
612
|
+
const shadowSpy = vi.spyOn(sun.shadow, "dispose");
|
|
613
|
+
const scene = engine!.scene;
|
|
614
|
+
|
|
615
|
+
const m = mounts.splice(0)[0];
|
|
616
|
+
m.app.unmount();
|
|
617
|
+
m.container.remove();
|
|
618
|
+
expect(sunSpy).toHaveBeenCalled();
|
|
619
|
+
expect(shadowSpy).toHaveBeenCalled();
|
|
620
|
+
// No rig light survives in the (detached) scene.
|
|
621
|
+
const kinds = scene.children.map((c) => c.constructor.name);
|
|
622
|
+
expect(kinds).not.toContain("AmbientLight");
|
|
623
|
+
expect(kinds).not.toContain("DirectionalLight");
|
|
624
|
+
expect(kinds).not.toContain("HemisphereLight");
|
|
625
|
+
expect(kinds).not.toContain("PointLight");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("installs the lighting rig on first applyLighting", async () => {
|
|
629
|
+
let engine: Board3DEngine | null = null;
|
|
630
|
+
mountBoard({}, { ready: ((e: Board3DEngine) => { engine = e; }) as never });
|
|
631
|
+
await nextTick();
|
|
632
|
+
|
|
633
|
+
engine!.applyLighting({ ambientIntensity: 0.9, points: [
|
|
634
|
+
{ color: [1, 0, 0], intensity: 2, range: 10, position: [0, 3, 0] },
|
|
635
|
+
] });
|
|
636
|
+
const kinds = engine!.scene.children.map((c) => c.constructor.name);
|
|
637
|
+
expect(kinds).toContain("AmbientLight");
|
|
638
|
+
expect(kinds).toContain("DirectionalLight");
|
|
639
|
+
expect(kinds).toContain("HemisphereLight");
|
|
640
|
+
expect(kinds).toContain("PointLight");
|
|
641
|
+
});
|
|
642
|
+
|
|
435
643
|
it("projectToScreen lands the origin at canvas centre for the default pose", async () => {
|
|
436
644
|
let engine: Board3DEngine | null = null;
|
|
437
645
|
const { container } = mountBoard(
|