@celestia-island/hikari 0.55.263 → 0.55.264
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
CHANGED
|
@@ -104,6 +104,61 @@ describe("makeFleetHologram", () => {
|
|
|
104
104
|
// more content; a node core is smaller and sparser — never identical.
|
|
105
105
|
expect(dialCounts(hub)).toEqual({ rings: 3, ticks: 48, arcs: 2, core: 1, spindle: 9 });
|
|
106
106
|
expect(dialCounts(node)).toEqual({ rings: 2, ticks: 28, arcs: 1, core: 1, spindle: 6 });
|
|
107
|
+
|
|
108
|
+
// 2026-10-31 follow-up ("大号的那些不够亮"): the large ones are also
|
|
109
|
+
// BRIGHTER, not just denser — outer ring, ticks and the equator
|
|
110
|
+
// spindle ring must all beat their planet-tier twins.
|
|
111
|
+
const outerRingOpacity = (hologram: ReturnType<typeof makeFleetHologram>) => {
|
|
112
|
+
const rings = hologram.frontDial.getObjectsByProperty(
|
|
113
|
+
"name",
|
|
114
|
+
"fleet-hologram-face-dial-ring",
|
|
115
|
+
) as THREE.LineLoop[];
|
|
116
|
+
return (rings[0]!.material as THREE.LineBasicMaterial).opacity;
|
|
117
|
+
};
|
|
118
|
+
const ticksOpacity = (hologram: ReturnType<typeof makeFleetHologram>) =>
|
|
119
|
+
((hologram.frontDial.getObjectByName("fleet-hologram-face-dial-ticks") as THREE.LineSegments)
|
|
120
|
+
.material as THREE.LineBasicMaterial).opacity;
|
|
121
|
+
const equatorSpindleOpacity = (hologram: ReturnType<typeof makeFleetHologram>) => {
|
|
122
|
+
const rings = hologram.shell.getObjectsByProperty(
|
|
123
|
+
"name",
|
|
124
|
+
"fleet-hologram-ring-spindle-circle",
|
|
125
|
+
) as THREE.LineLoop[];
|
|
126
|
+
const radiusOf = (ring: THREE.LineLoop) => {
|
|
127
|
+
const pos = ring.geometry.getAttribute("position");
|
|
128
|
+
return Math.hypot(pos.getX(0), pos.getY(0));
|
|
129
|
+
};
|
|
130
|
+
const widest = rings.reduce((best, r) => (radiusOf(r) > radiusOf(best) ? r : best), rings[0]!);
|
|
131
|
+
return (widest.material as THREE.LineBasicMaterial).opacity;
|
|
132
|
+
};
|
|
133
|
+
expect(outerRingOpacity(hub)).toBeGreaterThan(outerRingOpacity(node));
|
|
134
|
+
expect(ticksOpacity(hub)).toBeGreaterThan(ticksOpacity(node));
|
|
135
|
+
expect(equatorSpindleOpacity(hub)).toBeGreaterThan(equatorSpindleOpacity(node));
|
|
136
|
+
// Absolute pins for BOTH tiers (R3 S2 lesson: comparisons alone let a
|
|
137
|
+
// revert to the dim pre-fix star pass — the star must BURN at these
|
|
138
|
+
// levels, and the node must hold its own baseline).
|
|
139
|
+
expect(outerRingOpacity(hub)).toBeCloseTo(0.95, 5);
|
|
140
|
+
expect(ticksOpacity(hub)).toBeCloseTo(0.78, 5);
|
|
141
|
+
expect(equatorSpindleOpacity(hub)).toBeCloseTo(0.3, 5);
|
|
142
|
+
expect(outerRingOpacity(node)).toBeCloseTo(0.66, 5);
|
|
143
|
+
// And the star dial actually burns: the primary arc is fully lit.
|
|
144
|
+
const arcs = hub.frontDial.getObjectsByProperty(
|
|
145
|
+
"name",
|
|
146
|
+
"fleet-hologram-face-dial-arc",
|
|
147
|
+
) as THREE.Line[];
|
|
148
|
+
expect((arcs[0]!.material as THREE.LineBasicMaterial).opacity).toBe(1);
|
|
149
|
+
|
|
150
|
+
// Ceiling guard: every material opacity in EITHER hologram stays
|
|
151
|
+
// within [0, 1] — brightness tiers must never push past full lit.
|
|
152
|
+
for (const hologram of [hub, node]) {
|
|
153
|
+
hologram.group.traverse((obj) => {
|
|
154
|
+
const material = (obj as THREE.Mesh).material as
|
|
155
|
+
| (THREE.Material & { opacity?: number })
|
|
156
|
+
| undefined;
|
|
157
|
+
if (!material || !("opacity" in material) || material.opacity === undefined) return;
|
|
158
|
+
expect(material.opacity).toBeLessThanOrEqual(1);
|
|
159
|
+
expect(material.opacity).toBeGreaterThanOrEqual(0);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
107
162
|
});
|
|
108
163
|
|
|
109
164
|
it("builds the ball from circles only — no solid sphere, no wireframe cage", () => {
|
|
@@ -93,26 +93,31 @@ function lineMaterial(color: THREE.Color, opacity: number): THREE.LineBasicMater
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** How much content a dial tier draws (user direction: main cores carry
|
|
96
|
-
* MORE content than node cores — rings, ticks, arcs — never identical
|
|
96
|
+
* MORE content than node cores — rings, ticks, arcs — never identical;
|
|
97
|
+
* 2026-10-31 follow-up: they are also BRIGHTER, not just denser). */
|
|
97
98
|
interface DialTier {
|
|
98
99
|
rings: ReadonlyArray<{ scale: number; opacity: number }>;
|
|
99
100
|
tickCount: number;
|
|
101
|
+
tickOpacity: number;
|
|
100
102
|
arcs: ReadonlyArray<{ scale: number; start: number; span: number; opacity: number }>;
|
|
101
103
|
core: boolean;
|
|
104
|
+
coreOpacity: number;
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
const FOCUS_STAR: DialTier = {
|
|
105
108
|
rings: [
|
|
106
|
-
{ scale: 1, opacity: 0.
|
|
107
|
-
{ scale: 0.64, opacity: 0.
|
|
108
|
-
{ scale: 0.38, opacity: 0.
|
|
109
|
+
{ scale: 1, opacity: 0.95 },
|
|
110
|
+
{ scale: 0.64, opacity: 0.45 },
|
|
111
|
+
{ scale: 0.38, opacity: 0.32 },
|
|
109
112
|
],
|
|
110
113
|
tickCount: 48,
|
|
114
|
+
tickOpacity: 0.78,
|
|
111
115
|
arcs: [
|
|
112
|
-
{ scale: 1, start: -0.25, span: Math.PI * 0.62, opacity:
|
|
113
|
-
{ scale: 0.64, start: Math.PI * 0.85, span: Math.PI * 0.2, opacity: 0.
|
|
116
|
+
{ scale: 1, start: -0.25, span: Math.PI * 0.62, opacity: 1 },
|
|
117
|
+
{ scale: 0.64, start: Math.PI * 0.85, span: Math.PI * 0.2, opacity: 0.82 },
|
|
114
118
|
],
|
|
115
119
|
core: true,
|
|
120
|
+
coreOpacity: 0.42,
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
const FOCUS_PLANET: DialTier = {
|
|
@@ -121,10 +126,12 @@ const FOCUS_PLANET: DialTier = {
|
|
|
121
126
|
{ scale: 0.52, opacity: 0.26 },
|
|
122
127
|
],
|
|
123
128
|
tickCount: 28,
|
|
129
|
+
tickOpacity: 0.58,
|
|
124
130
|
arcs: [
|
|
125
131
|
{ scale: 1, start: -0.25, span: Math.PI * 0.5, opacity: 0.9 },
|
|
126
132
|
],
|
|
127
133
|
core: true,
|
|
134
|
+
coreOpacity: 0.26,
|
|
128
135
|
};
|
|
129
136
|
|
|
130
137
|
/** The five non-facing shell faces: an outline and a faint arc only, so
|
|
@@ -132,8 +139,10 @@ const FOCUS_PLANET: DialTier = {
|
|
|
132
139
|
const SUPPORT: DialTier = {
|
|
133
140
|
rings: [{ scale: 1, opacity: 0.28 }],
|
|
134
141
|
tickCount: 0,
|
|
142
|
+
tickOpacity: 0,
|
|
135
143
|
arcs: [{ scale: 1, start: -0.25, span: Math.PI * 0.62, opacity: 0.34 }],
|
|
136
144
|
core: false,
|
|
145
|
+
coreOpacity: 0,
|
|
137
146
|
};
|
|
138
147
|
|
|
139
148
|
function focusTierFor(role: "star" | "planet"): DialTier {
|
|
@@ -174,7 +183,7 @@ function makeFaceDial(
|
|
|
174
183
|
"position",
|
|
175
184
|
new THREE.Float32BufferAttribute(ticks, 3),
|
|
176
185
|
),
|
|
177
|
-
lineMaterial(color, offline ? 0.
|
|
186
|
+
lineMaterial(color, offline ? tier.tickOpacity * 0.2 : tier.tickOpacity),
|
|
178
187
|
));
|
|
179
188
|
tickMarks.name = "fleet-hologram-face-dial-ticks";
|
|
180
189
|
dial.add(tickMarks);
|
|
@@ -195,7 +204,7 @@ function makeFaceDial(
|
|
|
195
204
|
new THREE.MeshBasicMaterial({
|
|
196
205
|
color,
|
|
197
206
|
transparent: true,
|
|
198
|
-
opacity: offline ? 0.
|
|
207
|
+
opacity: offline ? tier.coreOpacity * 0.3 : tier.coreOpacity,
|
|
199
208
|
side: THREE.DoubleSide,
|
|
200
209
|
depthWrite: false,
|
|
201
210
|
blending: THREE.AdditiveBlending,
|
|
@@ -240,21 +249,25 @@ function makeDiscShell(
|
|
|
240
249
|
/** A stack of concentric circles along the Z axis: fat at the equator,
|
|
241
250
|
* tapering to thin rings at the poles (user direction 2026-10-31 — this
|
|
242
251
|
* is the second of the two fused structures; it rounds the shell into a
|
|
243
|
-
* ball and keeps every side view a set of nested circles).
|
|
252
|
+
* ball and keeps every side view a set of nested circles). `brightness`
|
|
253
|
+
* scales the ring opacities so a main core's ball burns brighter than a
|
|
254
|
+
* node's without adding clutter (2026-10-31 follow-up). */
|
|
244
255
|
function makeRingSpindle(
|
|
245
256
|
bodyRadius: number,
|
|
246
257
|
color: THREE.Color,
|
|
247
258
|
offline: boolean,
|
|
248
259
|
count: number,
|
|
260
|
+
brightness = 1,
|
|
249
261
|
): THREE.Group {
|
|
250
262
|
const spindle = new THREE.Group();
|
|
251
263
|
spindle.name = "fleet-hologram-ring-spindle";
|
|
252
264
|
for (let i = 0; i < count; i += 1) {
|
|
253
265
|
const t = count === 1 ? 0 : -0.97 + (1.94 * i) / (count - 1);
|
|
254
266
|
const radius = bodyRadius * Math.sqrt(Math.max(0, 1 - t * t)) * 0.98;
|
|
267
|
+
const lit = Math.min(1, (0.08 + (1 - Math.abs(t)) * 0.12) * brightness);
|
|
255
268
|
const loop = unpickable(new THREE.LineLoop(
|
|
256
269
|
xyCircleGeometry(radius, 160),
|
|
257
|
-
lineMaterial(color, offline ?
|
|
270
|
+
lineMaterial(color, offline ? lit * 0.2 : lit),
|
|
258
271
|
));
|
|
259
272
|
loop.name = "fleet-hologram-ring-spindle-circle";
|
|
260
273
|
loop.position.z = bodyRadius * t;
|
|
@@ -371,11 +384,14 @@ export function makeFleetHologram(
|
|
|
371
384
|
mesh.name = "fleet-hologram-core";
|
|
372
385
|
|
|
373
386
|
const shell = makeDiscShell(input.radius, color, offline);
|
|
387
|
+
// A main core's ball BURNS brighter as well as denser (2026-10-31
|
|
388
|
+
// follow-up): 1.5× spindle brightness on top of the richer dial tier.
|
|
374
389
|
shell.add(makeRingSpindle(
|
|
375
390
|
input.radius,
|
|
376
391
|
color,
|
|
377
392
|
offline,
|
|
378
393
|
input.role === "star" ? 9 : 6,
|
|
394
|
+
input.role === "star" ? 1.5 : 1,
|
|
379
395
|
));
|
|
380
396
|
group.add(shell);
|
|
381
397
|
|
|
@@ -538,6 +538,36 @@ describe("buildFleetScene teardown", () => {
|
|
|
538
538
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
539
539
|
});
|
|
540
540
|
|
|
541
|
+
it("burns the large bodies' halos brighter than the node baseline", () => {
|
|
542
|
+
const engine = stubEngine();
|
|
543
|
+
const handle = buildFleetScene(engine, callbacks());
|
|
544
|
+
// A hub (star tier) plus a gateway (planet tier, no halo at all).
|
|
545
|
+
handle.update(buildFleetSceneSpec(ROWS), { "hub-a": false });
|
|
546
|
+
|
|
547
|
+
// User direction 2026-10-31 follow-up: the LARGE ones were too dim —
|
|
548
|
+
// the star halo carries the "burning" read, so pin its level.
|
|
549
|
+
const starGlow = engine.registered.get("hub-a")!.object
|
|
550
|
+
.children.find((c): c is THREE.Sprite => c instanceof THREE.Sprite)!;
|
|
551
|
+
expect((starGlow.material as THREE.SpriteMaterial).opacity).toBeCloseTo(0.5, 5);
|
|
552
|
+
const planetSprites = engine.registered.get("edge-1")!.object
|
|
553
|
+
.children.filter((c) => c instanceof THREE.Sprite);
|
|
554
|
+
expect(planetSprites).toHaveLength(0);
|
|
555
|
+
|
|
556
|
+
// The registry core is the largest body on the page: its halo reads
|
|
557
|
+
// brightest of all.
|
|
558
|
+
handle.update(buildFleetSceneSpec([{ node_id: "edge-1", tier: 2, station_count: 0 }]), {});
|
|
559
|
+
const core = engine.registered.get("__star-system-core__")!.object;
|
|
560
|
+
const coreGlow = core.children.find((c): c is THREE.Sprite => c instanceof THREE.Sprite)!;
|
|
561
|
+
expect((coreGlow.material as THREE.SpriteMaterial).opacity).toBeCloseTo(0.55, 5);
|
|
562
|
+
|
|
563
|
+
// An offline hub dims its halo with it (0.24 = the offline twin).
|
|
564
|
+
handle.update(buildFleetSceneSpec(ROWS), { "hub-a": true });
|
|
565
|
+
const darkGlow = engine.registered.get("hub-a")!.object
|
|
566
|
+
.children.find((c): c is THREE.Sprite => c instanceof THREE.Sprite)!;
|
|
567
|
+
expect((darkGlow.material as THREE.SpriteMaterial).opacity).toBeCloseTo(0.24, 5);
|
|
568
|
+
handle.dispose();
|
|
569
|
+
});
|
|
570
|
+
|
|
541
571
|
it("frees real HUD textures when a body generation is replaced", () => {
|
|
542
572
|
const gradient = fakeAs<CanvasGradient>({ addColorStop: vi.fn() });
|
|
543
573
|
const context = fakeAs<CanvasRenderingContext2D>({
|
|
@@ -201,7 +201,9 @@ export function buildFleetScene(
|
|
|
201
201
|
map: glowTexture,
|
|
202
202
|
color,
|
|
203
203
|
transparent: true,
|
|
204
|
-
|
|
204
|
+
// A hub's halo carries its "burning" read (2026-10-31 follow-up:
|
|
205
|
+
// the large ones were too dim next to their denser dials).
|
|
206
|
+
opacity: offline ? 0.24 : 0.5,
|
|
205
207
|
blending: THREE.AdditiveBlending,
|
|
206
208
|
depthWrite: false,
|
|
207
209
|
}),
|
|
@@ -260,7 +262,9 @@ export function buildFleetScene(
|
|
|
260
262
|
map: glowTexture,
|
|
261
263
|
color: 0xffe9b8,
|
|
262
264
|
transparent: true,
|
|
263
|
-
|
|
265
|
+
// The registry core is the largest body on the page — its halo
|
|
266
|
+
// reads brightest (2026-10-31 follow-up).
|
|
267
|
+
opacity: 0.55,
|
|
264
268
|
blending: THREE.AdditiveBlending,
|
|
265
269
|
depthWrite: false,
|
|
266
270
|
}),
|