@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.263",
3
+ "version": "0.55.264",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -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.72 },
107
- { scale: 0.64, opacity: 0.3 },
108
- { scale: 0.38, opacity: 0.22 },
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: 0.94 },
113
- { scale: 0.64, start: Math.PI * 0.85, span: Math.PI * 0.2, opacity: 0.62 },
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.12 : 0.58),
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.08 : 0.26,
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 ? 0.04 : 0.08 + (1 - Math.abs(t)) * 0.12),
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
- opacity: offline ? 0.16 : 0.34,
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
- opacity: 0.38,
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
  }),