jsgui3-server 0.0.133 → 0.0.135

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.
Files changed (41) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +114 -18
  3. package/TODO.md +81 -0
  4. package/cli.js +96 -0
  5. package/docs/simple-server-api-design.md +702 -0
  6. package/examples/controls/14) window, canvas/client.js +238 -0
  7. package/examples/controls/14) window, canvas/server.js +21 -0
  8. package/examples/controls/14b) window, canvas (improved renderer)/client.js +391 -0
  9. package/examples/controls/14b) window, canvas (improved renderer)/server.js +21 -0
  10. package/examples/controls/14d) window, canvas globe/Clipping.js +0 -0
  11. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +435 -0
  12. package/examples/controls/14d) window, canvas globe/RenderingPipeline.js +43 -0
  13. package/examples/controls/14d) window, canvas globe/client.js +95 -0
  14. package/examples/controls/14d) window, canvas globe/math.js +76 -0
  15. package/examples/controls/14d) window, canvas globe/pipeline/BaseStage.js +46 -0
  16. package/examples/controls/14d) window, canvas globe/pipeline/ClippingStage.js +29 -0
  17. package/examples/controls/14d) window, canvas globe/pipeline/ComposeStage.js +15 -0
  18. package/examples/controls/14d) window, canvas globe/pipeline/ContinentsStage.js +116 -0
  19. package/examples/controls/14d) window, canvas globe/pipeline/GridStage.js +105 -0
  20. package/examples/controls/14d) window, canvas globe/pipeline/HUDStage.js +10 -0
  21. package/examples/controls/14d) window, canvas globe/pipeline/ShadeSphereStage.js +153 -0
  22. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +23 -0
  23. package/examples/controls/14d) window, canvas globe/pipeline/clipping/FrontFaceStrategy.js +46 -0
  24. package/examples/controls/14d) window, canvas globe/pipeline/clipping/PlaneClipStrategy.js +339 -0
  25. package/examples/controls/14d) window, canvas globe/server.js +21 -0
  26. package/examples/controls/14e) window, canvas multithreaded/client.js +948 -0
  27. package/examples/controls/14e) window, canvas multithreaded/server.js +21 -0
  28. package/examples/controls/14f) window, canvas polyglobe/client.js +569 -0
  29. package/examples/controls/14f) window, canvas polyglobe/math.js +137 -0
  30. package/examples/controls/14f) window, canvas polyglobe/server.js +21 -0
  31. package/module.js +3 -1
  32. package/package.json +12 -6
  33. package/resources/_old_website-resource.js +10 -2
  34. package/resources/jsbuilder/babel/deep_iterate/deep_iterate_babel.js +37 -0
  35. package/serve-factory.js +221 -0
  36. package/serve-helpers.js +95 -0
  37. package/server.js +93 -7
  38. package/tests/cli.test.js +66 -0
  39. package/tests/dummy-client.js +10 -0
  40. package/tests/serve.test.js +210 -0
  41. package/.vscode/settings.json +0 -6
@@ -0,0 +1,21 @@
1
+ const jsgui = require('./client');
2
+ const {Demo_UI} = jsgui.controls;
3
+ const Server = require('../../../server');
4
+ if (require.main === module) {
5
+ const server = new Server({
6
+ Ctrl: Demo_UI,
7
+ debug: true,
8
+ 'src_path_client_js': require.resolve('./client.js'),
9
+ });
10
+ console.log('waiting for server ready event');
11
+ server.one('ready', () => {
12
+ console.log('server ready');
13
+ server.start(52000, function (err, cb_start) {
14
+ if (err) {
15
+ throw err;
16
+ } else {
17
+ console.log('server started');
18
+ }
19
+ });
20
+ })
21
+ }
@@ -0,0 +1,569 @@
1
+
2
+ const jsgui = require('jsgui3-client');
3
+ const {controls} = jsgui;
4
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
5
+
6
+ // Shared math helpers
7
+ const M = require('./math'); // <- math.js in the same folder
8
+
9
+ // ----------------------------------------------------------------------------
10
+ // HexPentGlobeRenderer
11
+ // Renders a "soccer-ball" style Earth made of hexagons + 12 pentagons:
12
+ // 1) Build a geodesic sphere by subdividing an icosahedron (levels = 0..4 typical)
13
+ // 2) Project vertices to unit sphere
14
+ // 3) Build the dual mesh: one cell per original vertex; cell vertices are the
15
+ // centers of faces incident to that vertex (sorted around the vertex).
16
+ // 4) Shade each cell with simple directional lighting.
17
+ // ----------------------------------------------------------------------------
18
+ class HexPentGlobeRenderer {
19
+ constructor(canvas, opts = {}) {
20
+ this.canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas;
21
+ if (!(this.canvas instanceof HTMLCanvasElement)) throw new Error('Pass a canvas element or its id');
22
+ this.ctx = this.canvas.getContext('2d', { alpha: true });
23
+
24
+ // -------- Options --------
25
+ const visual = opts.visual || {};
26
+ const stroke = visual.stroke || {};
27
+ this.opts = {
28
+ padding: opts.padding ?? 8,
29
+ background: opts.background ?? '#081019',
30
+
31
+ // Mesh detail: levels=0 gives raw icosahedron; each level doubles edge frequency (n = 2^levels).
32
+ levels: Math.max(0, Math.min(5, opts.levels ?? 3)),
33
+
34
+ // Lighting
35
+ ambient: opts.ambient ?? 0.15,
36
+ diffuse: opts.diffuse ?? 0.95,
37
+ specular: opts.specular ?? 0.25,
38
+ shininess: opts.shininess ?? 60,
39
+
40
+ // Colors
41
+ baseColor: opts.baseColor ?? [0.13, 0.42, 0.86], // linear-ish RGB 0..1
42
+ pentagonTint: opts.pentagonTint ?? [0.08, 0.10, 0.14], // extra tint for 12 pentagons
43
+ cellJitter: opts.cellJitter ?? 0.06, // subtle per-cell brightness randomization
44
+
45
+ // Strokes
46
+ visual: {
47
+ fillAlpha: visual.fillAlpha ?? 1.0,
48
+ strokeAlpha: visual.strokeAlpha ?? 0.22,
49
+ strokeWidth: stroke.width ?? 0.8,
50
+ strokeColor: stroke.color ?? '#FFFFFF'
51
+ },
52
+
53
+ // Interaction
54
+ inertiaFriction: opts.inertiaFriction ?? 2.6,
55
+ inertiaMinSpeed: opts.inertiaMinSpeed ?? 0.05,
56
+ dragInvertX: opts.dragInvertX ?? false,
57
+ dragInvertY: opts.dragInvertY ?? false,
58
+ dragSwapAxes: opts.dragSwapAxes ?? false,
59
+ dragSensitivity: opts.dragSensitivity ?? 1.0,
60
+
61
+ dpr: window.devicePixelRatio || 1
62
+ };
63
+
64
+ // Sun (unit, camera frame)
65
+ this.sun = M.vec3.norm([-0.45, 0.55, 0.65]);
66
+
67
+ // Orientation (Arcball quaternion) and drag state
68
+ this.Q = [0,0,0,1]; // [x,y,z,w]
69
+ this._dragging = false;
70
+ this._pickObj = null; // object-space unit vector of picked point
71
+ this._updateR();
72
+
73
+ // Build geodesic-dual mesh once
74
+ this._buildCells();
75
+
76
+ this._attachPointerHandlers();
77
+
78
+ // Initial draw
79
+ this.resize();
80
+ this.render();
81
+
82
+ this._onResize = () => { this.resize(); this.render(); };
83
+ window.addEventListener('resize', this._onResize, { passive: true });
84
+ }
85
+
86
+ destroy() {
87
+ window.removeEventListener('resize', this._onResize);
88
+ this._detachPointerHandlers();
89
+ if (this._animHandle) cancelAnimationFrame(this._animHandle);
90
+ }
91
+
92
+ // -------- Public API --------
93
+ setSunDirection(vec3) { this.sun = M.vec3.norm(vec3); this.render(); }
94
+ setSunFromSpherical(lonDeg, latDeg) {
95
+ const lon = M.deg2rad(lonDeg), lat = M.deg2rad(latDeg);
96
+ const cx = Math.cos(lat), sx = Math.sin(lat), sz = Math.sin(lon), cz = Math.cos(lon);
97
+ this.setSunDirection([ cx*sz, sx, cx*cz ]);
98
+ }
99
+ setRotation(yawDeg, pitchDeg) {
100
+ // Preserve API: map yaw/pitch to quaternion for convenience
101
+ const yaw = M.deg2rad(yawDeg), pitch = M.deg2rad(pitchDeg);
102
+ const qy = M.quat.fromAxisAngle(0,1,0, yaw);
103
+ const qx = M.quat.fromAxisAngle(1,0,0, pitch);
104
+ this.Q = M.quat.normalize(M.quat.mul(qx, qy));
105
+ this._updateR(); this.render();
106
+ }
107
+
108
+ resize() {
109
+ const dpr = (this.opts.dpr = window.devicePixelRatio || 1);
110
+ const rect = this.canvas.getBoundingClientRect();
111
+ const displayWidth = rect.width || this.canvas.width;
112
+ const displayHeight = rect.height || this.canvas.height;
113
+ const w = Math.max(1, Math.round(displayWidth * dpr));
114
+ const h = Math.max(1, Math.round(displayHeight * dpr));
115
+ if (this.canvas.width !== w || this.canvas.height !== h) {
116
+ this.canvas.width = w; this.canvas.height = h;
117
+ }
118
+ this.ctx.setTransform(1,0,0,1,0,0);
119
+ this.ctx.scale(dpr, dpr);
120
+ }
121
+
122
+ // -------- Core rendering --------
123
+ render() {
124
+ const ctx = this.ctx;
125
+ const rect = this.canvas.getBoundingClientRect();
126
+ const width = rect.width || this.canvas.width / this.opts.dpr;
127
+ const height = rect.height || this.canvas.height / this.opts.dpr;
128
+
129
+ // Background
130
+ ctx.clearRect(0, 0, width, height);
131
+ if (this.opts.background) {
132
+ ctx.fillStyle = this.opts.background;
133
+ ctx.fillRect(0, 0, width, height);
134
+ }
135
+
136
+ const pad = this.opts.padding;
137
+ const r = Math.max(1, Math.min(width, height) / 2 - pad);
138
+ const cx = width / 2, cy = height / 2;
139
+
140
+ // Clip to disc
141
+ ctx.save();
142
+ ctx.beginPath();
143
+ ctx.arc(cx, cy, r, 0, Math.PI*2);
144
+ ctx.clip();
145
+
146
+ // Prepare transforms + lighting
147
+ this._updateR();
148
+ const R = this._R;
149
+ const L = this.sun;
150
+
151
+ // Rotate all face centers once per frame
152
+ const C = this._faceCenters; // [Nf][3]
153
+ const Cr = this._rotCenters; // rotated centers cached per frame
154
+ const nF = C.length;
155
+ for (let i = 0; i < nF; i++) {
156
+ const c = C[i];
157
+ Cr[i][0] = R[0]*c[0] + R[1]*c[1] + R[2]*c[2];
158
+ Cr[i][1] = R[3]*c[0] + R[4]*c[1] + R[5]*c[2];
159
+ Cr[i][2] = R[6]*c[0] + R[7]*c[1] + R[8]*c[2];
160
+ }
161
+
162
+ // For each cell (one per original vertex) draw polygon
163
+ const cells = this._cells; // {v:[x,y,z], idxs:[faceIndices...], pent:boolean, jitter:number}
164
+ const fillAlpha = this.opts.visual.fillAlpha;
165
+ const strokeAlpha = this.opts.visual.strokeAlpha;
166
+ const strokeColor = this.opts.visual.strokeColor;
167
+ const strokeWidth = this.opts.visual.strokeWidth;
168
+
169
+ ctx.lineWidth = strokeWidth;
170
+ ctx.strokeStyle = strokeColor;
171
+ ctx.globalAlpha = 1;
172
+
173
+ // Sort cells back-to-front by depth of cell center (painter's algorithm within clipped disk)
174
+ // Using rotated vertex (cell center) depth
175
+ const sortTmp = this._sorted;
176
+ for (let i = 0; i < cells.length; i++) {
177
+ const v = cells[i].v; // original unit vertex
178
+ const vx = R[0]*v[0] + R[1]*v[1] + R[2]*v[2];
179
+ const vy = R[3]*v[0] + R[4]*v[1] + R[5]*v[2];
180
+ const vz = R[6]*v[0] + R[7]*v[1] + R[8]*v[2];
181
+ sortTmp[i].z = vz;
182
+ sortTmp[i].i = i;
183
+ // Store rotated center for shading
184
+ cells[i]._vr = [vx, vy, vz];
185
+ }
186
+ sortTmp.sort((a,b)=> b.z - a.z); // draw far to near would overdraw wrongly; we clip anyway.
187
+ // We'll actually draw near-to-far so closer edges sit above. Use ascending z:
188
+ sortTmp.reverse();
189
+
190
+ for (let si = 0; si < sortTmp.length; si++) {
191
+ const cidx = sortTmp[si].i;
192
+ const cell = cells[cidx];
193
+ const vr = cell._vr;
194
+
195
+ // Cull back hemisphere by center
196
+ if (vr[2] <= 0) continue;
197
+
198
+ // Lighting for the whole cell (use cell normal ~ vr)
199
+ const NL = Math.max(0, vr[0]*L[0] + vr[1]*L[1] + vr[2]*L[2]);
200
+ const diff = this.opts.diffuse * NL;
201
+ const amb = this.opts.ambient;
202
+ const spec = this.opts.specular * Math.pow(Math.max(0, NL), this.opts.shininess/100); // subtle
203
+
204
+ // Base color (+ pentagon tint + per-cell jitter)
205
+ let rLin = this.opts.baseColor[0];
206
+ let gLin = this.opts.baseColor[1];
207
+ let bLin = this.opts.baseColor[2];
208
+
209
+ if (cell.pent) {
210
+ rLin += this.opts.pentagonTint[0];
211
+ gLin += this.opts.pentagonTint[1];
212
+ bLin += this.opts.pentagonTint[2];
213
+ }
214
+
215
+ const jit = cell.jitter;
216
+ rLin = Math.max(0, rLin + jit); gLin = Math.max(0, gLin + jit); bLin = Math.max(0, bLin + jit);
217
+
218
+ // Final lighting and simple tone map
219
+ rLin = M.toneMap(rLin*(amb + diff) + spec);
220
+ gLin = M.toneMap(gLin*(amb + diff) + spec);
221
+ bLin = M.toneMap(bLin*(amb + diff) + spec);
222
+
223
+ const Rsrgb = Math.round(M.toSRGB_linear(rLin)*255);
224
+ const Gsrgb = Math.round(M.toSRGB_linear(gLin)*255);
225
+ const Bsrgb = Math.round(M.toSRGB_linear(bLin)*255);
226
+
227
+ // Fill color
228
+ this.ctx.fillStyle = 'rgba(' + Rsrgb + ',' + Gsrgb + ',' + Bsrgb + ',' + fillAlpha + ')';
229
+ this.ctx.globalAlpha = 1;
230
+
231
+ // Build polygon path from rotated face-centers
232
+ const poly = cell.idxs;
233
+ this.ctx.beginPath();
234
+ for (let k = 0; k < poly.length; k++) {
235
+ const P = Cr[poly[k]];
236
+ const x = cx + r * P[0];
237
+ const y = cy - r * P[1];
238
+ if (k === 0) this.ctx.moveTo(x, y); else this.ctx.lineTo(x, y);
239
+ }
240
+ this.ctx.closePath();
241
+ this.ctx.fill();
242
+
243
+ // Stroke (draw after fill)
244
+ this.ctx.save();
245
+ this.ctx.globalAlpha = strokeAlpha * (cell.pent ? 1.25 : 1.0);
246
+ this.ctx.stroke();
247
+ this.ctx.restore();
248
+ }
249
+
250
+ ctx.restore();
251
+
252
+ // Subtle globe outline
253
+ ctx.beginPath();
254
+ ctx.arc(cx, cy, r, 0, Math.PI*2);
255
+ ctx.strokeStyle = 'rgba(255,255,255,0.08)';
256
+ ctx.lineWidth = 1.25;
257
+ ctx.stroke();
258
+ }
259
+
260
+ // -------- Mesh generation (geodesic icosahedron → dual) --------
261
+ _buildCells() {
262
+ // 1) Icosahedron
263
+ const ico = this._icosahedron();
264
+ let V = ico.vertices; // Array<[x,y,z]>
265
+ let F = ico.faces; // Array<[i,j,k]>
266
+
267
+ // 2) Subdivide by levels (Loop-like midpoint; each iteration splits tri into 4)
268
+ for (let l = 0; l < this.opts.levels; l++) {
269
+ const nextFaces = [];
270
+ const midCache = new Map(); // key "a,b" with a<b -> vertex index
271
+ const getKey = (a,b)=> a<b ? (a+'_'+b) : (b+'_'+a);
272
+ const midpoint = (a,b)=>{
273
+ const key = getKey(a,b);
274
+ if (midCache.has(key)) return midCache.get(key);
275
+ const pa = V[a], pb = V[b];
276
+ const m = M.vec3.norm([ (pa[0]+pb[0]), (pa[1]+pb[1]), (pa[2]+pb[2]) ]);
277
+ const idx = V.push(m) - 1;
278
+ midCache.set(key, idx);
279
+ return idx;
280
+ };
281
+ for (let fi = 0; fi < F.length; fi++) {
282
+ const a = F[fi][0], b = F[fi][1], c = F[fi][2];
283
+ const ab = midpoint(a,b);
284
+ const bc = midpoint(b,c);
285
+ const ca = midpoint(c,a);
286
+ nextFaces.push([a, ab, ca]);
287
+ nextFaces.push([b, bc, ab]);
288
+ nextFaces.push([c, ca, bc]);
289
+ nextFaces.push([ab, bc, ca]);
290
+ }
291
+ F = nextFaces;
292
+ // vertices already normalized via midpoint step; keep them on unit sphere
293
+ }
294
+
295
+ // Ensure exact normalization (avoid drift)
296
+ for (let i = 0; i < V.length; i++) {
297
+ V[i] = M.vec3.norm(V[i]);
298
+ }
299
+
300
+ // 3) Face centers (normalized)
301
+ const faceCenters = new Array(F.length);
302
+ for (let i = 0; i < F.length; i++) {
303
+ const a = V[F[i][0]], b = V[F[i][1]], c = V[F[i][2]];
304
+ const s = [ (a[0]+b[0]+c[0])/3, (a[1]+b[1]+c[1])/3, (a[2]+b[2]+c[2])/3 ];
305
+ faceCenters[i] = M.vec3.norm(s);
306
+ }
307
+
308
+ // 4) Build incident face list per vertex
309
+ const inc = new Array(V.length);
310
+ for (let i = 0; i < V.length; i++) inc[i] = [];
311
+ for (let fi = 0; fi < F.length; fi++) {
312
+ const tri = F[fi];
313
+ inc[tri[0]].push(fi);
314
+ inc[tri[1]].push(fi);
315
+ inc[tri[2]].push(fi);
316
+ }
317
+
318
+ // 5) For each vertex, sort its incident face-centers around the vertex to form a polygon
319
+ const cells = [];
320
+ const rotCenters = new Array(faceCenters.length);
321
+ for (let i = 0; i < faceCenters.length; i++) rotCenters[i] = [0,0,0];
322
+
323
+ // local tangent basis per vertex for angle sort
324
+ for (let vi = 0; vi < V.length; vi++) {
325
+ const v = V[vi];
326
+ const facesAround = inc[vi];
327
+
328
+ // Build local basis (u,w) perp to v
329
+ let ref = Math.abs(v[1]) < 0.9 ? [0,1,0] : [1,0,0];
330
+ const u = M.vec3.norm(M.vec3.cross(ref, v));
331
+ const w = M.vec3.cross(v, u); // already unit since u and v unit & perpendicular
332
+
333
+ // compute angle of each incident face center around v
334
+ const withAngles = new Array(facesAround.length);
335
+ for (let k = 0; k < facesAround.length; k++) {
336
+ const fc = faceCenters[facesAround[k]];
337
+ // project onto tangent plane
338
+ const dot_v = fc[0]*v[0] + fc[1]*v[1] + fc[2]*v[2];
339
+ const t = [ fc[0] - dot_v*v[0], fc[1] - dot_v*v[1], fc[2] - dot_v*v[2] ];
340
+ const x = t[0]*u[0] + t[1]*u[1] + t[2]*u[2];
341
+ const y = t[0]*w[0] + t[1]*w[1] + t[2]*w[2];
342
+ const ang = Math.atan2(y, x);
343
+ withAngles[k] = { fi: facesAround[k], ang };
344
+ }
345
+ withAngles.sort((a,b)=> a.ang - b.ang);
346
+ const idxs = withAngles.map(o => o.fi);
347
+
348
+ // Identify pentagons (valence 5 at the 12 original icosahedron vertices)
349
+ const isPent = idxs.length === 5;
350
+
351
+ // Per-cell color jitter (consistent pseudo-random from index)
352
+ const jitter = (hashInt(vi) * 2 - 1) * this.opts.cellJitter;
353
+
354
+ cells.push({ v: v, idxs: idxs, pent: isPent, jitter: jitter, _vr: [0,0,0] });
355
+ }
356
+
357
+ // Sort helper buffer (reused each frame)
358
+ const sorted = new Array(cells.length);
359
+ for (let i = 0; i < sorted.length; i++) sorted[i] = { z: 0, i: 0 };
360
+
361
+ this._verts = V;
362
+ this._faces = F;
363
+ this._faceCenters = faceCenters;
364
+ this._rotCenters = rotCenters;
365
+ this._cells = cells;
366
+ this._sorted = sorted;
367
+ }
368
+
369
+ _icosahedron() {
370
+ // Returns {vertices, faces} for a unit icosahedron (vertices normalized)
371
+ const t = (1 + Math.sqrt(5)) / 2; // golden ratio
372
+ const raw = [
373
+ [-1, t, 0], [ 1, t, 0], [-1, -t, 0], [ 1, -t, 0],
374
+ [ 0, -1, t], [ 0, 1, t], [ 0, -1, -t], [ 0, 1, -t],
375
+ [ t, 0, -1], [ t, 0, 1], [-t, 0, -1], [-t, 0, 1]
376
+ ];
377
+ const vertices = raw.map(M.vec3.norm);
378
+ const faces = [
379
+ [0,11,5],[0,5,1],[0,1,7],[0,7,10],[0,10,11],
380
+ [1,5,9],[5,11,4],[11,10,2],[10,7,6],[7,1,8],
381
+ [3,9,4],[3,4,2],[3,2,6],[3,6,8],[3,8,9],
382
+ [4,9,5],[2,4,11],[6,2,10],[8,6,7],[9,8,1]
383
+ ];
384
+ return { vertices, faces };
385
+ }
386
+
387
+ // -------- Rotation / math --------
388
+ _updateR() {
389
+ // Rotation matrix from quaternion
390
+ this._R = M.mat3.fromQuat(this.Q);
391
+ }
392
+
393
+ // -------- Interaction --------
394
+ _attachPointerHandlers() {
395
+ const el = this.canvas;
396
+ if (getComputedStyle(el).touchAction !== 'none') el.style.touchAction = 'none';
397
+
398
+ const onDown = (ev)=>{
399
+ const { cx, cy, r } = this._viewGeo();
400
+ const v0 = M.screenToArcball(ev.clientX, ev.clientY, cx, cy, r);
401
+ // Ignore presses far outside the disc
402
+ if (v0[2] === 0) {
403
+ const dx = (ev.clientX - cx) / r, dy = (cy - ev.clientY) / r;
404
+ if (dx*dx + dy*dy > 1.5*1.5) return;
405
+ }
406
+ this._updateR();
407
+ const R = this._R;
408
+ // pickObj = R^T * v0 (row-major R)
409
+ const px = R[0]*v0[0] + R[3]*v0[1] + R[6]*v0[2];
410
+ const py = R[1]*v0[0] + R[4]*v0[1] + R[7]*v0[2];
411
+ const pz = R[2]*v0[0] + R[5]*v0[1] + R[8]*v0[2];
412
+ const invLen = 1/Math.max(1e-12, Math.hypot(px,py,pz));
413
+ this._pickObj = [px*invLen, py*invLen, pz*invLen];
414
+ this._dragging = true;
415
+ try { el.setPointerCapture(ev.pointerId); } catch {}
416
+ };
417
+ const onMove = (ev)=>{
418
+ if (!this._dragging) return;
419
+ const { cx, cy, r } = this._viewGeo();
420
+ const v1 = M.screenToArcball(ev.clientX, ev.clientY, cx, cy, r);
421
+ if (!this._pickObj) return;
422
+ this._updateR();
423
+ const R = this._R;
424
+ // current camera-space position of picked object point
425
+ const cxp = R[0]*this._pickObj[0] + R[1]*this._pickObj[1] + R[2]*this._pickObj[2];
426
+ const cyp = R[3]*this._pickObj[0] + R[4]*this._pickObj[1] + R[5]*this._pickObj[2];
427
+ const czp = R[6]*this._pickObj[0] + R[7]*this._pickObj[1] + R[8]*this._pickObj[2];
428
+ const clen = Math.max(1e-12, Math.hypot(cxp,cyp,czp));
429
+ const c = [cxp/clen, cyp/clen, czp/clen];
430
+ // Compute stable axis/angle for c -> v1
431
+ let dot = c[0]*v1[0] + c[1]*v1[1] + c[2]*v1[2];
432
+ dot = Math.max(-1, Math.min(1, dot));
433
+ let angle = Math.acos(dot);
434
+ let axis = [ c[1]*v1[2] - c[2]*v1[1], c[2]*v1[0] - c[0]*v1[2], c[0]*v1[1] - c[1]*v1[0] ];
435
+ let an = Math.hypot(axis[0], axis[1], axis[2]);
436
+ if (an < 1e-8) {
437
+ // 0° or 180°: choose a stable axis orthogonal to c
438
+ const ref = Math.abs(c[1]) < 0.9 ? [0,1,0] : [1,0,0];
439
+ axis = [ c[1]*ref[2] - c[2]*ref[1], c[2]*ref[0] - c[0]*ref[2], c[0]*ref[1] - c[1]*ref[0] ];
440
+ an = Math.hypot(axis[0], axis[1], axis[2]);
441
+ if (an < 1e-8) axis = [0,1,0]; else { axis[0]/=an; axis[1]/=an; axis[2]/=an; }
442
+ } else {
443
+ axis[0]/=an; axis[1]/=an; axis[2]/=an;
444
+ }
445
+
446
+ // Clamp large steps to avoid overshoot on slow frames
447
+ const MAX_STEP_ANGLE = 0.35; // ~20° per event
448
+ if (angle > MAX_STEP_ANGLE) angle = MAX_STEP_ANGLE;
449
+
450
+ if (angle < 1e-6) { return; }
451
+ const dq = M.quat.fromAxisAngle(axis[0], axis[1], axis[2], angle);
452
+ const qNew = M.quat.norm(M.quat.mul(dq, this.Q));
453
+ this.Q = [qNew[0], qNew[1], qNew[2], qNew[3]];
454
+ this._updateR();
455
+ this.render();
456
+ };
457
+ const onUp = (ev)=>{
458
+ this._dragging = false;
459
+ try { el.releasePointerCapture(ev.pointerId); } catch {}
460
+ this._pickObj = null;
461
+ };
462
+
463
+ el.addEventListener('pointerdown', onDown);
464
+ el.addEventListener('pointermove', onMove);
465
+ el.addEventListener('pointerup', onUp);
466
+ el.addEventListener('pointercancel', onUp);
467
+ this._ptr = { onDown, onMove, onUp };
468
+ }
469
+ _detachPointerHandlers(){
470
+ if (!this._ptr) return;
471
+ const el = this.canvas;
472
+ el.removeEventListener('pointerdown', this._ptr.onDown);
473
+ el.removeEventListener('pointermove', this._ptr.onMove);
474
+ el.removeEventListener('pointerup', this._ptr.onUp);
475
+ el.removeEventListener('pointercancel', this._ptr.onUp);
476
+ this._ptr = null;
477
+ }
478
+ // no inertia with arcball; rotation is directly under the cursor
479
+
480
+ _viewGeo() {
481
+ // center (in client px), radius in client px
482
+ const rect = this.canvas.getBoundingClientRect();
483
+ const cx = rect.left + rect.width/2;
484
+ const cy = rect.top + rect.height/2;
485
+ const r = Math.max(1, Math.min(rect.width, rect.height)/2 - (this.opts.padding||0));
486
+ return { cx, cy, r };
487
+ }
488
+
489
+ // (no arcball helpers needed for yaw/pitch)
490
+ }
491
+
492
+ // Simple integer hash → [0,1)
493
+ function hashInt(i){
494
+ // xorshift-ish; stable across runs for visual jitter
495
+ let x = (i>>>0) + 0x9e3779b9;
496
+ x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
497
+ return ((x>>>0) % 1000) / 1000;
498
+ }
499
+
500
+ // ----------------------------------------------------------------------------
501
+ // Demo UI wiring (kept minimal; same structure you used previously)
502
+ // ----------------------------------------------------------------------------
503
+ class Demo_UI extends Active_HTML_Document {
504
+ constructor(spec = {}) {
505
+ spec.__type_name = spec.__type_name || 'demo_ui';
506
+ super(spec);
507
+ const {context} = this;
508
+ if (typeof this.body.add_class === 'function') this.body.add_class('demo-ui');
509
+
510
+ const compose = () => {
511
+ const windowCtrl = new controls.Window({
512
+ context,
513
+ title: 'Hex/Pent Globe',
514
+ pos: [5, 5]
515
+ });
516
+ windowCtrl.size = [1000, 1000];
517
+
518
+ const canvas = new controls.canvas({ context });
519
+ canvas.dom.attributes.id = 'globeCanvas';
520
+ canvas.size = [900, 900];
521
+
522
+ windowCtrl.inner.add(canvas);
523
+ this.body.add(windowCtrl);
524
+
525
+ this._ctrl_fields = this._ctrl_fields || {};
526
+ this._ctrl_fields.canvas = this.canvas = canvas;
527
+ };
528
+
529
+ if (!spec.el) compose();
530
+ }
531
+
532
+ activate() {
533
+ if (!this.__active) {
534
+ super.activate();
535
+ const {context} = this;
536
+ console.log('activate Demo_UI');
537
+
538
+ const globe = new HexPentGlobeRenderer('globeCanvas', {
539
+ background: '#081019',
540
+ // Increase polygons: each level ~x4 cells; 5 -> ~10242 cells (~16x from 3)
541
+ levels: 4,
542
+ ambient: 0.18,
543
+ diffuse: 0.95,
544
+ specular: 0.22,
545
+ shininess: 70,
546
+ visual: {
547
+ fillAlpha: 1.0,
548
+ strokeAlpha: 0.22,
549
+ stroke: { width: 0.8, color: '#FFFFFF' }
550
+ }
551
+ });
552
+
553
+ globe.setSunFromSpherical(-35, 25);
554
+
555
+ context.on('window-resize', () => {
556
+ globe.resize();
557
+ globe.render();
558
+ });
559
+ }
560
+ }
561
+ }
562
+ Demo_UI.css = `
563
+ *{margin:0;padding:0}
564
+ body{overflow-x:hidden;overflow-y:hidden;background-color:#E0E0E0}
565
+ .demo-ui{}
566
+ `;
567
+
568
+ controls.Demo_UI = Demo_UI;
569
+ module.exports = jsgui;