@celestia-island/hikari 0.55.101 → 0.55.103
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 +52 -0
- package/src/components/HkBoard3D.controls.test.tsx +140 -0
- package/src/components/HkBoard3D.test.tsx +585 -1
- package/src/components/HkBoard3D.tsx +410 -5
- package/src/components/HkMinimap3D.test.tsx +1 -1
- package/src/components/board3dHoverBox.test.ts +200 -0
- package/src/components/board3dHoverBox.ts +210 -0
- package/src/index.ts +22 -1
- 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/src/utils/scene3d.test.ts +69 -0
- package/src/utils/scene3d.ts +67 -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", () => {
|
|
@@ -40,6 +52,23 @@ describe("hikari barrel — 3D board surface", () => {
|
|
|
40
52
|
expect(barrel.BOARD3D_HELPERS_LAYER).toBe(1);
|
|
41
53
|
});
|
|
42
54
|
|
|
55
|
+
it("exports the main-camera gizmo layer and the hover-frame geometry", () => {
|
|
56
|
+
// Consumers drawing their own main-camera-only affordance (and the
|
|
57
|
+
// tests pinning that the minimap never sees one) need the layer; the
|
|
58
|
+
// corner-bracket maths is shared so a bespoke frame matches the
|
|
59
|
+
// built-in one.
|
|
60
|
+
expect(barrel.BOARD3D_MAIN_LAYER).toBe(2);
|
|
61
|
+
expect(typeof barrel.hoverBoxSegments).toBe("function");
|
|
62
|
+
expect(barrel.hoverBoxSegments([1, 1, 1])).toHaveLength(48 * 3);
|
|
63
|
+
for (const name of [
|
|
64
|
+
"HOVER_BOX_ARM_RATIO",
|
|
65
|
+
"HOVER_BOX_PADDING",
|
|
66
|
+
"HOVER_BOX_MIN_EXTENT_RATIO",
|
|
67
|
+
] as const) {
|
|
68
|
+
expect(typeof (barrel as Record<string, unknown>)[name], name).toBe("number");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
43
72
|
it("exports the scene3d utilities (palette, orbit, fit, zoom)", () => {
|
|
44
73
|
const fns = [
|
|
45
74
|
"hashIdToUnit",
|
|
@@ -57,6 +86,29 @@ describe("hikari barrel — 3D board surface", () => {
|
|
|
57
86
|
}
|
|
58
87
|
});
|
|
59
88
|
|
|
89
|
+
it("exports the GLB model layer", () => {
|
|
90
|
+
expect(typeof barrel.ModelLayer).toBe("function");
|
|
91
|
+
// The engine's model surface is part of that contract.
|
|
92
|
+
const engineMethods = [
|
|
93
|
+
"loadModel",
|
|
94
|
+
"setModelOpacity",
|
|
95
|
+
"highlightModel",
|
|
96
|
+
"clearHighlights",
|
|
97
|
+
"modelObject",
|
|
98
|
+
"modelWorldPosition",
|
|
99
|
+
"removeModel",
|
|
100
|
+
] as const;
|
|
101
|
+
const engine = null as Engine | null;
|
|
102
|
+
void engine;
|
|
103
|
+
// Type-level: the methods exist on Board3DEngine (vue-tsc covers this
|
|
104
|
+
// file, so a rename breaks the gate rather than a consumer).
|
|
105
|
+
type HasModelSurface = Engine extends Record<(typeof engineMethods)[number], unknown>
|
|
106
|
+
? true
|
|
107
|
+
: false;
|
|
108
|
+
const has: HasModelSurface = true;
|
|
109
|
+
expect(has).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
60
112
|
it("exports the minimap governance constants, drag sensitivity included", () => {
|
|
61
113
|
// A consumer writing its own drag control needs the same sensitivity
|
|
62
114
|
// 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
|
+
});
|