@celestia-island/hikari 0.55.255 → 0.55.257

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.255",
3
+ "version": "0.55.257",
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",
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Source contract for the bond CELLS of the horizontal stepper (2026-10-31
3
- * user rule).
3
+ * user rule; 60 % share since the same evening's second look).
4
4
  *
5
5
  * The row is read as one cell per step plus one cell per bond: the steps
6
6
  * keep their content width, the leftover splits into EQUAL bond cells, and
7
- * each bond paints 80 % of its own cell CENTERED, so both ends keep a 10 %
7
+ * each bond paints 60 % of its own cell CENTERED, so both ends keep a 20 %
8
8
  * air gap. Before that rule the bond filled whatever the step had left after
9
9
  * its label, minus two fixed 4px margins — measured on a real engine at the
10
10
  * provider wizard's 590px row that put the bond 12px off its own label but
@@ -95,7 +95,7 @@ describe("horizontal bond cell contract", () => {
95
95
  // while the bond→node air is margin alone — the 12/4 asymmetry that
96
96
  // made the bond look glued to the next circle.
97
97
  expect(cell).toContain(`margin-inline: calc(-1 * ${GAP}) 0;`);
98
- // The box must not paint itself: the visible 80 % is the ::before.
98
+ // The box must not paint itself: the visible 60 % is the ::before.
99
99
  expect(cell).toContain("background: none;");
100
100
  // The ::before resolves percentage insets against this box.
101
101
  expect(cell).toContain("position: relative;");
@@ -106,7 +106,7 @@ describe("horizontal bond cell contract", () => {
106
106
  expect(blockFor(".hk-timeline-step")).toContain(`gap: ${GAP};`);
107
107
  });
108
108
 
109
- it("paints 80 % of the cell centred with a 10 % inset on each end", () => {
109
+ it("paints 60 % of the cell centred with a 20 % inset on each end", () => {
110
110
  // The parser above only yields top-level rules; the paint lives in the
111
111
  // nested block, so lift it out of the cell rule's body.
112
112
  const paint = /&::before\s*\{([^}]*)\}/.exec(blockFor(BOND_CELL))?.[1] ?? "";
@@ -114,18 +114,31 @@ describe("horizontal bond cell contract", () => {
114
114
  expect(paint).toContain('content: "";');
115
115
  expect(paint).toContain("position: absolute;");
116
116
  expect(paint).toContain("inset-block: 0;");
117
- expect(paint).toContain("inset-inline: 10%;");
117
+ expect(paint).toContain("inset-inline: 20%;");
118
118
  expect(paint).toContain("background: var(--_bond-color);");
119
- // Percentage PADDING would resolve against the step, not the bond.
120
- expect(src).not.toMatch(/padding-inline:\s*10%/);
119
+ // Percentage PADDING would resolve against the step, not the bond —
120
+ // ban the MECHANISM on both boxes, not one pinned value (a stray
121
+ // `padding-inline: 10%` beside the correct inset would widen the paint
122
+ // through the padding box and slip past a value-pinned regex).
123
+ expect(blockFor(BOND_CELL)).not.toMatch(/padding-inline:/);
124
+ expect(paint).not.toMatch(/padding-inline:/);
121
125
  });
122
126
 
123
- it("keeps a visible bond floor the collapse measurement can see", () => {
127
+ it("reserves the two-margin-era collapse threshold through its floor", () => {
124
128
  const cell = blockFor(BOND_CELL);
125
- // A clamped row must still paint a bond, and the row's max-content
126
- // width must keep reserving it — otherwise collapse="auto" measures the
127
- // row narrower than it renders and swaps the window away too early.
128
- expect(cell).toContain(`min-width: calc(0.5rem + ${GAP});`);
129
+ // The floor (half a rem plus two gaps, minus the cancelled gap) reserves
130
+ // the same sixteen outer pixels per bond the old two 4px margins
131
+ // carried, so collapse="auto" measures the row's max-content width at
132
+ // the two-margin-era threshold — never earlier, never later.
133
+ expect(cell).toContain(`min-width: calc(0.5rem + 2 * ${GAP});`);
134
+ // A definite flex-basis reserves NOTHING: engines size a flex item's
135
+ // intrinsic contribution at max(min-width, content) and ignore the
136
+ // basis (measured: a 100px basis on an empty item contributed its 8px
137
+ // floor). The basis must not creep back in place of the floor — nor as
138
+ // the shorthand `flex:`, whose basis component would be equally silent
139
+ // while the literal reads like a reservation.
140
+ expect(cell).not.toContain("flex-basis");
141
+ expect(cell).not.toMatch(/\bflex\s*:/);
129
142
  });
130
143
 
131
144
  it("publishes one bond colour token for every status", () => {
@@ -312,11 +312,13 @@
312
312
  }
313
313
  }
314
314
 
315
- /* ── Bond cells (2026-10-31 user rule) ──────────────────────────────────
315
+ /* ── Bond cells (2026-10-31 user rule; 60 % share the same evening) ────
316
316
  The horizontal row reads as one cell per step plus one cell per bond: the
317
317
  steps keep their content width, the leftover splits into EQUAL bond cells,
318
- and each bond paints 80 % of its cell CENTERED, so both ends keep a 10 %
319
- air gap. Two mechanics make that exact:
318
+ and each bond paints 60 % of its cell CENTERED, so both ends keep a 20 %
319
+ air gap. (The first cut painted 80 %/10 % — centred, but the completed
320
+ bond read as long as ever, so the share was trimmed the same evening.)
321
+ Three mechanics make that exact:
320
322
 
321
323
  1. The bond's box must span the WHOLE cell. Its cell begins where its own
322
324
  label ends, but the step's flex gap already sits in front of it (that
@@ -325,39 +327,48 @@
325
327
  SAME `--hk-timeline-step-gap` token the step's `gap` uses. Before this
326
328
  the gap counted twice and the bond ran 12px off its label but only 4px
327
329
  off the next node: the completed bond read as glued to the next circle.
328
- 2. The painted 80 % cannot be `padding-inline` — a percentage padding
330
+ 2. The painted share cannot be `padding-inline` — a percentage padding
329
331
  resolves against the CONTAINING block (the step), not the bond's own
330
- box, which would inset it by a slice of the step instead of 10 % of the
331
- cell. So the colour moves to a ::before pinned `inset-inline: 10%`,
332
+ box, which would inset it by a slice of the step instead of 20 % of the
333
+ cell. So the colour lives on a ::before pinned `inset-inline: 20%`,
332
334
  which does resolve against the bond. The status rules below therefore
333
335
  publish `--_bond-color`; the vertical rail keeps painting its own box
334
336
  (it is a rail hanging under the node, not a cell-wide bond).
335
-
336
- The floor keeps a squeezed row honest: the cell absorbs the gap on top of
337
- a half-rem of bond, so the narrowest full row still paints a visible line.
338
- Its outer size is that floor MINUS the cancelled gap, i.e. half a rem per
339
- bond — eight of the sixteen pixels the old two 4px margins reserved, so
340
- `collapse="auto"` (HkTimeline.tsx measures this max-content width) fires
341
- about 32px earlier than before across a five-step row, never later.
342
- Bounded cost, accepted: in a row squeezed past the floor (reachable through
343
- `collapse="never"`, or the full-mode frame that a label change and a
344
- hysteresis un-collapse render before re-measuring) the box keeps its floor
345
- and overhangs the next step by a QUARTER of the row's shortfall — ~6px at a
346
- 430px host, ~14px at 400px — so the bond paints under that step's circle
347
- until the overhang outgrows the 24px circle itself (host ≲ 362px), where a
348
- visible stub sits beside it. Shrinking the floor to nothing would trade
349
- this for a bond that vanishes on the narrowest hosts. */
337
+ 3. The row-width RESERVATION is a min-width floor, sized to the two-margin
338
+ era at the stock 8px gap: half a rem plus two gaps, minus the cancelled
339
+ gap, reserves the same sixteen outer pixels per bond the old fixed
340
+ 4px+4px margins carried, so `collapse="auto"` (HkTimeline.tsx measures
341
+ the row's max-content width) switches to the window at exactly the
342
+ threshold the two-margin era had — never earlier, never later. (A host
343
+ that retunes `--hk-timeline-step-gap` shifts the threshold by the
344
+ difference: the old margins were fixed 16px outer while this floor
345
+ reserves gap+8. And a definite `flex-basis` reserves NOTHING here:
346
+ engines size a flex item's intrinsic contribution at
347
+ max(min-width, content) and ignore the basis — measured, a 100px basis
348
+ on an empty item contributed only its 8px floor.) The floor only binds
349
+ when a row is squeezed below its natural width, which `collapse="auto"`
350
+ never lets happen for windowable rows (the window takes over; a ≤3-step
351
+ row stays full-mode at any width, but the floor then behaves exactly as
352
+ both prior eras' floors did); through `collapse="never"` — or the
353
+ full-mode frame a label change renders before re-measuring — the pinned
354
+ 24px box paints 14.4px inset 4.8px, and that paint slides UNDER the
355
+ next step's 24px circle for as long as the overflow stays under 24px
356
+ (host ≳ 380px for the five-step wizard row). Below that a small stub
357
+ shows beside the circle (onset ~381px, ~5px by 360px) — marginally
358
+ tighter than the two-margin geometry's own stub in this manually-forced
359
+ mode (onset ~384px, ~6px by 360px), and bounded below by the width
360
+ where a label reaches the next circle (~400px for this row). */
350
361
  .hk-timeline[data-mode="full"][data-orientation="horizontal"] .hk-timeline-step [data-el="connector"] {
351
362
  position: relative;
352
363
  margin-inline: calc(-1 * var(--hk-timeline-step-gap, var(--space-8, 8px))) 0;
353
- min-width: calc(0.5rem + var(--hk-timeline-step-gap, var(--space-8, 8px)));
364
+ min-width: calc(0.5rem + 2 * var(--hk-timeline-step-gap, var(--space-8, 8px)));
354
365
  background: none;
355
366
 
356
367
  &::before {
357
368
  content: "";
358
369
  position: absolute;
359
370
  inset-block: 0;
360
- inset-inline: 10%;
371
+ inset-inline: 20%;
361
372
  background: var(--_bond-color);
362
373
  }
363
374
  }
@@ -426,7 +437,7 @@
426
437
  }
427
438
 
428
439
  [data-el="connector"] {
429
- // The bonds paint 80 % of their cell through a ::before (see the
440
+ // The bonds paint 60 % of their cell through a ::before (see the
430
441
  // full-mode cell rule); the token is the single source for both that
431
442
  // paint and the vertical rail's own box.
432
443
  --_bond-color: var(--hi-color-primary);
@@ -516,7 +527,7 @@
516
527
 
517
528
  // Status crossfade: the data-status driven colors (indicator fill/border,
518
529
  // label color, connector fill) transition instead of flipping. The
519
- // horizontal bond's paint lives on its ::before (the 80 % cell rule), so
530
+ // horizontal bond's paint lives on its ::before (the 60 % cell rule), so
520
531
  // that box rides the crossfade too — without it a status flip snaps there
521
532
  // while the vertical rail keeps easing.
522
533
  .hk-timeline-step [data-el="indicator"],
@@ -3,6 +3,8 @@ import * as THREE from "three";
3
3
 
4
4
  import { fakeAs } from "../test-utils/fakes";
5
5
  import {
6
+ extractHologramFaces,
7
+ faceInradius,
6
8
  hologramHudText,
7
9
  makeFleetHologram,
8
10
  poseFleetHologram,
@@ -86,4 +88,77 @@ describe("makeFleetHologram", () => {
86
88
  expect(hub.mesh.geometry).toBeInstanceOf(THREE.IcosahedronGeometry);
87
89
  expect(planet.mesh.geometry).toBeInstanceOf(THREE.DodecahedronGeometry);
88
90
  });
91
+
92
+ it("gives every face its own detached inset ring, with no shared vertices", () => {
93
+ mockCanvas();
94
+ for (const [role, expectedFaces] of [["star", 20], ["planet", 12]] as const) {
95
+ const hologram = makeFleetHologram(
96
+ { ...INPUT, role },
97
+ new THREE.Color(),
98
+ false,
99
+ );
100
+ const rings = hologram.mesh.getObjectsByProperty(
101
+ "name",
102
+ "fleet-hologram-face-ring",
103
+ );
104
+ expect(rings, `${role} face rings`).toHaveLength(expectedFaces);
105
+ expect(
106
+ hologram.mesh.getObjectsByProperty("name", "fleet-hologram-polyhedron-edges"),
107
+ `${role} shared wireframe`,
108
+ ).toHaveLength(0);
109
+
110
+ const vertexKeys = new Set<string>();
111
+ let vertices = 0;
112
+ for (const ring of rings) {
113
+ expect(ring).toBeInstanceOf(THREE.LineLoop);
114
+ const positions = (ring as THREE.LineLoop).geometry.getAttribute("position");
115
+ expect(positions.count).toBe(32);
116
+ for (let i = 0; i < positions.count; i += 1) {
117
+ vertexKeys.add(
118
+ `${positions.getX(i).toFixed(5)}:${positions.getY(i).toFixed(5)}:${positions.getZ(i).toFixed(5)}`,
119
+ );
120
+ vertices += 1;
121
+ }
122
+ }
123
+ // Every ring has its own circumference; no ring shares an edge or
124
+ // vertex with a neighbour, so the old connected cage cannot reappear.
125
+ expect(vertexKeys.size).toBe(vertices);
126
+
127
+ // Each ring must stay well INSIDE its own face: map it back to the
128
+ // recovered face polygon, then pin both the 62% radius ratio and the
129
+ // remaining margin. This makes an overflow regression impossible to
130
+ // hide behind the count/name assertions above.
131
+ const faces = extractHologramFaces(hologram.mesh.geometry);
132
+ expect(faces).toHaveLength(expectedFaces);
133
+ const usedFaces = new Set<number>();
134
+ for (const ring of rings) {
135
+ expect(ring.raycast).not.toBe(THREE.LineLoop.prototype.raycast);
136
+ const positions = (ring as THREE.LineLoop).geometry.getAttribute("position");
137
+ const ringCenter = new THREE.Vector3();
138
+ const points: THREE.Vector3[] = [];
139
+ for (let i = 0; i < positions.count; i += 1) {
140
+ const point = new THREE.Vector3(
141
+ positions.getX(i),
142
+ positions.getY(i),
143
+ positions.getZ(i),
144
+ );
145
+ points.push(point);
146
+ ringCenter.add(point);
147
+ }
148
+ ringCenter.multiplyScalar(1 / positions.count);
149
+ const faceIndex = faces.findIndex((face, index) =>
150
+ !usedFaces.has(index) && ringCenter.distanceTo(face.center) < 0.03);
151
+ expect(faceIndex, `${role} ring maps to one face`).toBeGreaterThanOrEqual(0);
152
+ usedFaces.add(faceIndex);
153
+ const face = faces[faceIndex]!;
154
+ const inradius = faceInradius(face);
155
+ const ringRadius = Math.max(...points.map((point) => point.distanceTo(ringCenter)));
156
+ expect(ringRadius / inradius).toBeCloseTo(0.62, 5);
157
+ for (const point of points) {
158
+ expect(point.distanceTo(face.center)).toBeLessThan(inradius);
159
+ }
160
+ }
161
+ expect(usedFaces.size).toBe(expectedFaces);
162
+ }
163
+ });
89
164
  });
@@ -3,10 +3,11 @@
3
3
  * data-bound cylindrical HUD band.
4
4
  *
5
5
  * The visual language is deliberately restrained: a coarse translucent
6
- * polyhedron, two rings for a standard node (three for a hub), one bright
7
- * arc per ring, and a slow ticker made ONLY from real caller-supplied
8
- * fields. Decorative lines and the band never join raycast picking; the
9
- * centre mesh remains the node's pointer and selection target.
6
+ * polyhedron whose FACES each carry an independent in-plane ring (no shared
7
+ * wireframe edges), two orbit rings for a standard node (three for a hub),
8
+ * one bright arc per orbit ring, and a slow ticker made ONLY from real
9
+ * caller-supplied fields. Decorative lines and the band never join raycast
10
+ * picking; the centre mesh remains the node's pointer and selection target.
10
11
  */
11
12
 
12
13
  import * as THREE from "three";
@@ -76,6 +77,125 @@ function unpickable<T extends THREE.Object3D>(object: T): T {
76
77
  return object;
77
78
  }
78
79
 
80
+ /** One flat polygon face recovered from the polyhedron's triangles.
81
+ * Exported for the geometry contract test; not part of the public barrel. */
82
+ export interface HologramFace {
83
+ center: THREE.Vector3;
84
+ normal: THREE.Vector3;
85
+ vertices: THREE.Vector3[];
86
+ }
87
+
88
+ /** Recover the polyhedron's true faces (a dodecahedron face is three
89
+ * coplanar triangles; a pentagon must become ONE ring, not three). Group by
90
+ * normal direction with a dot threshold: Three.js' projected pentagon
91
+ * fragments are nearly coplanar, while adjacent dodecahedron faces differ
92
+ * by tens of degrees. */
93
+ export function extractHologramFaces(geometry: THREE.BufferGeometry): HologramFace[] {
94
+ const position = geometry.getAttribute("position");
95
+ const groups: Array<{ normal: THREE.Vector3; vertices: Map<string, THREE.Vector3> }> = [];
96
+ for (let i = 0; i < position.count; i += 3) {
97
+ const a = new THREE.Vector3(position.getX(i), position.getY(i), position.getZ(i));
98
+ const b = new THREE.Vector3(position.getX(i + 1), position.getY(i + 1), position.getZ(i + 1));
99
+ const c = new THREE.Vector3(position.getX(i + 2), position.getY(i + 2), position.getZ(i + 2));
100
+ const normal = b.clone().sub(a).cross(c.clone().sub(a)).normalize();
101
+ const triangleCenter = a.clone().add(b).add(c).multiplyScalar(1 / 3);
102
+ if (normal.dot(triangleCenter) < 0) normal.negate();
103
+ let group = groups.find((candidate) => candidate.normal.dot(normal) > 0.999);
104
+ if (!group) {
105
+ group = { normal, vertices: new Map() };
106
+ groups.push(group);
107
+ }
108
+ for (const vertex of [a, b, c]) {
109
+ const vertexKey = [vertex.x, vertex.y, vertex.z]
110
+ .map((value) => value.toFixed(5))
111
+ .join(":");
112
+ group.vertices.set(vertexKey, vertex);
113
+ }
114
+ }
115
+
116
+ return groups.map((group) => {
117
+ const vertices = [...group.vertices.values()];
118
+ const center = vertices
119
+ .reduce((sum, vertex) => sum.add(vertex), new THREE.Vector3())
120
+ .multiplyScalar(1 / vertices.length);
121
+ return {
122
+ center,
123
+ normal: group.normal.clone().dot(center) < 0
124
+ ? group.normal.clone().negate()
125
+ : group.normal.clone(),
126
+ vertices,
127
+ };
128
+ });
129
+ }
130
+
131
+ /** Inradius of the face polygon — the largest inset circle that still
132
+ * leaves a visible gap between neighbouring face rings. Exported for the
133
+ * geometry contract test; not part of the public barrel. */
134
+ export function faceInradius(face: HologramFace): number {
135
+ const u = face.vertices[0]!.clone().sub(face.center).normalize();
136
+ const v = face.normal.clone().cross(u).normalize();
137
+ const ordered = [...face.vertices].sort((a, b) => {
138
+ const angle = (point: THREE.Vector3): number => Math.atan2(
139
+ point.clone().sub(face.center).dot(v),
140
+ point.clone().sub(face.center).dot(u),
141
+ );
142
+ return angle(a) - angle(b);
143
+ });
144
+ let inradius = Number.POSITIVE_INFINITY;
145
+ for (let i = 0; i < ordered.length; i += 1) {
146
+ const a = ordered[i]!;
147
+ const b = ordered[(i + 1) % ordered.length]!;
148
+ const edge = b.clone().sub(a);
149
+ const area2 = edge.clone().cross(face.center.clone().sub(a)).length();
150
+ if (edge.lengthSq() > 1e-10) inradius = Math.min(inradius, area2 / edge.length());
151
+ }
152
+ return Number.isFinite(inradius) ? inradius : 0;
153
+ }
154
+
155
+ /** Each face gets its own detached ring. No shared edges, no connected
156
+ * wireframe cage — adjacent rings visibly stop short of every face border. */
157
+ function makeFaceRings(
158
+ geometry: THREE.BufferGeometry,
159
+ bodyRadius: number,
160
+ color: THREE.Color,
161
+ offline: boolean,
162
+ ): THREE.LineLoop[] {
163
+ const lift = bodyRadius * 0.01;
164
+ const segments = 32;
165
+ const rings: THREE.LineLoop[] = [];
166
+ for (const face of extractHologramFaces(geometry)) {
167
+ const radius = Math.max(0.012, faceInradius(face) * 0.62);
168
+ const u = face.vertices[0]!.clone().sub(face.center).normalize();
169
+ const v = face.normal.clone().cross(u).normalize();
170
+ const points: number[] = [];
171
+ for (let i = 0; i < segments; i += 1) {
172
+ const angle = (i / segments) * Math.PI * 2;
173
+ const point = face.center
174
+ .clone()
175
+ .addScaledVector(face.normal, lift)
176
+ .addScaledVector(u, Math.cos(angle) * radius)
177
+ .addScaledVector(v, Math.sin(angle) * radius);
178
+ points.push(point.x, point.y, point.z);
179
+ }
180
+ const ring = unpickable(new THREE.LineLoop(
181
+ new THREE.BufferGeometry().setAttribute(
182
+ "position",
183
+ new THREE.Float32BufferAttribute(points, 3),
184
+ ),
185
+ new THREE.LineBasicMaterial({
186
+ color,
187
+ transparent: true,
188
+ opacity: offline ? 0.16 : 0.62,
189
+ depthWrite: false,
190
+ blending: THREE.AdditiveBlending,
191
+ }),
192
+ ));
193
+ ring.name = "fleet-hologram-face-ring";
194
+ rings.push(ring);
195
+ }
196
+ return rings;
197
+ }
198
+
79
199
  function makeRing(
80
200
  radius: number,
81
201
  index: number,
@@ -212,18 +332,9 @@ export function makeFleetHologram(
212
332
  mesh.name = "fleet-hologram-polyhedron";
213
333
  mesh.rotation.set(0.2, 0, 0.1);
214
334
 
215
- const edges = unpickable(new THREE.LineSegments(
216
- new THREE.EdgesGeometry(geometry, 1),
217
- new THREE.LineBasicMaterial({
218
- color: edgeColor,
219
- transparent: true,
220
- opacity: offline ? 0.26 : 0.78,
221
- depthWrite: false,
222
- blending: THREE.AdditiveBlending,
223
- }),
224
- ));
225
- edges.name = "fleet-hologram-polyhedron-edges";
226
- mesh.add(edges);
335
+ for (const ring of makeFaceRings(geometry, input.radius, edgeColor, offline)) {
336
+ mesh.add(ring);
337
+ }
227
338
  group.add(mesh);
228
339
 
229
340
  const rings = HOLOGRAM_RING_SCALES[input.role].map((scale, index) => {
@@ -669,10 +669,18 @@ describe("buildFleetScene body dressing", () => {
669
669
  expect(ring.getObjectByName("fleet-hologram-ring-base")).toBeDefined();
670
670
  expect(ring.getObjectByName("fleet-hologram-ring-arc")).toBeDefined();
671
671
  }
672
- const edges = bodyMesh(engine, id).getObjectByName(
673
- "fleet-hologram-polyhedron-edges",
672
+ const faceRings = root.getObjectsByProperty("name", "fleet-hologram-face-ring");
673
+ expect(faceRings, `${id} detached face-ring count`).toHaveLength(
674
+ id === "hub-a" ? 20 : 12,
674
675
  );
675
- expect(edges, `${id} polyhedron edges`).toBeInstanceOf(THREE.LineSegments);
676
+ for (const faceRing of faceRings) {
677
+ expect(faceRing).toBeInstanceOf(THREE.LineLoop);
678
+ expect(faceRing).not.toBeInstanceOf(THREE.LineSegments);
679
+ }
680
+ expect(
681
+ root.getObjectsByProperty("name", "fleet-hologram-polyhedron-edges"),
682
+ "the shared wireframe cage is gone",
683
+ ).toHaveLength(0);
676
684
  }
677
685
  handle.dispose();
678
686
  });