jsgui3-server 0.0.134 → 0.0.136

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 (36) 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/14d) window, canvas globe/ARCBALL-README.md +146 -0
  7. package/examples/controls/14d) window, canvas globe/Clipping.js +0 -0
  8. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +280 -799
  9. package/examples/controls/14d) window, canvas globe/RenderingPipeline.js +43 -0
  10. package/examples/controls/14d) window, canvas globe/arcball-drag-behaviour.js +250 -0
  11. package/examples/controls/14d) window, canvas globe/arcball-drag-behaviour.test.js +141 -0
  12. package/examples/controls/14d) window, canvas globe/drag-behaviour-base.js +70 -0
  13. package/examples/controls/14d) window, canvas globe/drag-controller.js +181 -0
  14. package/examples/controls/14d) window, canvas globe/math.test.js +281 -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/module.js +3 -1
  26. package/package.json +10 -4
  27. package/resources/_old_website-resource.js +10 -2
  28. package/resources/jsbuilder/babel/deep_iterate/deep_iterate_babel.js +37 -0
  29. package/resources/local-server-info-resource.js +1 -1
  30. package/serve-factory.js +221 -0
  31. package/serve-helpers.js +95 -0
  32. package/server.js +93 -7
  33. package/tests/cli.test.js +66 -0
  34. package/tests/dummy-client.js +10 -0
  35. package/tests/serve.test.js +210 -0
  36. package/.vscode/settings.json +0 -13
@@ -0,0 +1,153 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ class ShadeSphereStage extends BaseStage {
4
+ /**
5
+ * Per-pixel shading of the sphere into an offscreen buffer, then composited.
6
+ * Performance note: inner loop kept monolithic to encourage engine inlining
7
+ * and avoid function call overhead per pixel.
8
+ * @param {RenderState} rs
9
+ */
10
+ execute(rs) {
11
+ const rInst = this.r;
12
+
13
+ const q = rInst.opts.quality * (
14
+ rs.interactive ? rInst.opts.interactiveQualityScale : 1
15
+ );
16
+ const d = Math.max(2, (2 * rs.radius * q) | 0);
17
+ const off = rInst._getOff(d, d);
18
+
19
+ if (rInst._mapSize !== d) {
20
+ rInst._buildNormalMap(d);
21
+ }
22
+ if (!rInst._imgData || rInst._imgData.width !== d) {
23
+ rInst._imgData = off.ctx.createImageData(d, d);
24
+ }
25
+
26
+ const data = rInst._imgData.data;
27
+ const base = rInst.opts.baseColor;
28
+ const amb = rInst.opts.ambient;
29
+ const kd = rInst.opts.diffuse;
30
+ const ks = rInst.opts.specular;
31
+ const shin = rInst.opts.shininess;
32
+ const termSoft = rInst.opts.terminatorSoftness;
33
+ const atm = rInst.opts.atmosphere;
34
+ const back = rInst.opts.backlight;
35
+ const Lx = rInst.sun[0], Ly = rInst.sun[1], Lz = rInst.sun[2];
36
+
37
+ // Half vector (approx: light + view(0,0,1))
38
+ let Hx = Lx, Hy = Ly, Hz = Lz + 1;
39
+ {
40
+ const invH = 1 / Math.hypot(Hx, Hy, Hz);
41
+ Hx *= invH; Hy *= invH; Hz *= invH;
42
+ }
43
+
44
+ const Rt = rInst.Rt;
45
+ const Rt00 = Rt[0], Rt01 = Rt[1], Rt02 = Rt[2];
46
+ const Rt10 = Rt[3], Rt11 = Rt[4], Rt12 = Rt[5];
47
+ const Rt20 = Rt[6], Rt21 = Rt[7], Rt22 = Rt[8];
48
+
49
+ const Nx = rInst._Nx, Ny = rInst._Ny, Nz = rInst._Nz, A8 = rInst._A8;
50
+ const texAlb = rInst.tex.albedo, texWater = rInst.tex.water, texIce = rInst.tex.ice;
51
+ const needTex = !!(texAlb || texWater || texIce);
52
+
53
+ const toneLUT = rInst._toneLUT;
54
+ const sLinToSRGB = rInst._lin2sU8;
55
+ const toneScale = (toneLUT.length - 1) / 4;
56
+
57
+ const nPix = d * d;
58
+ let p = 0;
59
+ for (let i = 0; i < nPix; i++) { // hot loop
60
+ const nz = Nz[i];
61
+ if (nz < 0) {
62
+ data[p] = data[p + 1] = data[p + 2] = data[p + 3] = 0;
63
+ p += 4;
64
+ continue;
65
+ }
66
+
67
+ const nx = Nx[i];
68
+ const ny = Ny[i];
69
+ const NL = nx * Lx + ny * Ly + nz * Lz;
70
+ const NV = nz; // view = (0,0,1)
71
+ const NH = nx * Hx + ny * Hy + nz * Hz;
72
+
73
+ let t = (NL + termSoft) / (2 * termSoft);
74
+ if (t < 0) t = 0; else if (t > 1) t = 1;
75
+ const dayMask = (3 - 2 * t) * t * t * (NL > 0 ? NL : 0);
76
+
77
+ let albR = base[0], albG = base[1], albB = base[2];
78
+ let oceanW = 1, iceW = 0;
79
+
80
+ if (needTex) {
81
+ const Wx = Rt00 * nx + Rt01 * ny + Rt02 * nz;
82
+ const Wy = Rt10 * nx + Rt11 * ny + Rt12 * nz;
83
+ const Wz = Rt20 * nx + Rt21 * ny + Rt22 * nz;
84
+ const lon = Math.atan2(Wx, Wz);
85
+ const WyC = Wy < -1 ? -1 : (Wy > 1 ? 1 : Wy);
86
+ const lat = Math.asin(WyC);
87
+ if (texAlb) {
88
+ const s = rInst._sampleSRGB(texAlb, lon, lat);
89
+ const lut = rInst._s2l;
90
+ albR = lut[s.r]; albG = lut[s.g]; albB = lut[s.b];
91
+ }
92
+ if (texWater) {
93
+ const sw = rInst._sampleSRGB(texWater, lon, lat);
94
+ oceanW = (sw.r + sw.g + sw.b) / 765;
95
+ }
96
+ if (texIce) {
97
+ const si = rInst._sampleSRGB(texIce, lon, lat);
98
+ iceW = (si.r + si.g + si.b) / 765;
99
+ }
100
+ }
101
+
102
+ const diff = kd * dayMask;
103
+ const specW = oceanW * (1 - 0.9 * iceW);
104
+ const spec = NL > 0 ? ks * specW * Math.pow(NH > 0 ? NH : 0, shin) : 0;
105
+ const NVc = NV < 0 ? 0 : (NV > 1 ? 1 : NV);
106
+ const rimDay = atm * Math.pow(1 - NVc, 2.4) * (NL + 0.15 > 0 ? NL + 0.15 : 0);
107
+ let rimBack = 0;
108
+ if (Lz < 0 && NL < 0) {
109
+ rimBack = back * Math.pow(1 - NVc, 3.2) * (-NL);
110
+ }
111
+
112
+ let rLin = albR * (amb + diff) + spec + 0.40 * rimDay + 0.25 * rimBack;
113
+ let gLin = albG * (amb + diff) + spec + 0.65 * rimDay + 0.40 * rimBack;
114
+ let bLin = albB * (amb + diff) + spec + 1.00 * rimDay + 0.80 * rimBack;
115
+
116
+ if (rLin < 0) rLin = 0; else if (rLin > 4) rLin = 4;
117
+ if (gLin < 0) gLin = 0; else if (gLin > 4) gLin = 4;
118
+ if (bLin < 0) bLin = 0; else if (bLin > 4) bLin = 4;
119
+
120
+ rLin = toneLUT[(rLin * toneScale) | 0];
121
+ gLin = toneLUT[(gLin * toneScale) | 0];
122
+ bLin = toneLUT[(bLin * toneScale) | 0];
123
+
124
+ if (rLin > 1) rLin = 1;
125
+ if (gLin > 1) gLin = 1;
126
+ if (bLin > 1) bLin = 1;
127
+
128
+ data[p] = sLinToSRGB[(rLin * 4095) | 0];
129
+ data[p + 1] = sLinToSRGB[(gLin * 4095) | 0];
130
+ data[p + 2] = sLinToSRGB[(bLin * 4095) | 0];
131
+ data[p + 3] = A8[i];
132
+ p += 4;
133
+ }
134
+
135
+ off.ctx.putImageData(rInst._imgData, 0, 0);
136
+ const drawSize = d / q;
137
+ const ctx = rInst.ctx;
138
+ ctx.save();
139
+ ctx.beginPath();
140
+ ctx.arc(rs.cx, rs.cy, rs.radius, 0, Math.PI * 2);
141
+ ctx.clip();
142
+ ctx.drawImage(
143
+ off.canvas,
144
+ rs.cx - drawSize / 2,
145
+ rs.cy - drawSize / 2,
146
+ drawSize,
147
+ drawSize
148
+ );
149
+ ctx.restore();
150
+ }
151
+ }
152
+
153
+ module.exports = ShadeSphereStage;
@@ -0,0 +1,23 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ class TransformStage extends BaseStage {
4
+ /** @param {RenderState} rs */
5
+ execute(rs) {
6
+ const r = this.r;
7
+ r._updateRot();
8
+
9
+ const rect = r.canvas.getBoundingClientRect();
10
+ const width = rect.width || r.canvas.width / r.opts.dpr;
11
+ const height = rect.height || r.canvas.height / r.opts.dpr;
12
+ rs.width = width;
13
+ rs.height = height;
14
+
15
+ const pad = r.opts.padding;
16
+ const minSide = Math.min(width, height);
17
+ rs.radius = Math.max(1, minSide / 2 - pad);
18
+ rs.cx = width * 0.5;
19
+ rs.cy = height * 0.5;
20
+ }
21
+ }
22
+
23
+ module.exports = TransformStage;
@@ -0,0 +1,46 @@
1
+ // FrontFaceStrategy: current simple hemisphere clipping (z >= 0 fully)
2
+ // Exports a process(renderer, continent, R) function.
3
+
4
+ function ensureTriangulated(continent) {
5
+ if (continent.tri) return;
6
+ const polyXYZ = continent.polyXYZ;
7
+ const n = polyXYZ.length / 3;
8
+ if (n < 3) { continent.tri = null; return; }
9
+ // Simple average normal basis projection + ear clipping (duplicated lightweight version)
10
+ let nx=0, ny=0, nz=0; for (let i=0;i<n;i++){ const k=i*3; nx+=polyXYZ[k]; ny+=polyXYZ[k+1]; nz+=polyXYZ[k+2]; }
11
+ const nlen = Math.hypot(nx,ny,nz) || 1; nx/=nlen; ny/=nlen; nz/=nlen;
12
+ let ax = Math.abs(nx) < 0.8 ? 1 : 0; let ay = ax?0:1; let az = 0;
13
+ let ux = ny*az - nz*ay, uy = nz*ax - nx*az, uz = nx*ay - ny*ax; const ulen = Math.hypot(ux,uy,uz)||1; ux/=ulen; uy/=ulen; uz/=ulen;
14
+ let vx = ny*uz - nz*uy, vy = nz*ux - nx*uz, vz = nx*uy - ny*ux;
15
+ const px = new Array(n), py = new Array(n);
16
+ for (let i=0;i<n;i++){ const k=i*3; const x=polyXYZ[k], y=polyXYZ[k+1], z=polyXYZ[k+2]; px[i]=x*ux+y*uy+z*uz; py[i]=x*vx+y*vy+z*vz; }
17
+ const V = []; for (let i=0;i<n;i++) V.push(i);
18
+ function area(){ let a=0; for (let i=0;i<V.length;i++){ const i0=V[i], i1=V[(i+1)%V.length]; a+=px[i0]*py[i1]-px[i1]*py[i0]; } return a*0.5; }
19
+ const orientCW = area()<0;
20
+ function isConvex(a,b,c){ const ax=px[a], ay=py[a], bx=px[b], by=py[b], cx=px[c], cy=py[c]; const cross=(bx-ax)*(cy-ay)-(by-ay)*(cx-ax); return orientCW? cross<0: cross>0; }
21
+ function inTri(a,b,c,p){ const x=px[p], y=py[p]; const ax=px[a], ay=py[a], bx=px[b], by=py[b], cx=px[c], cy=py[c]; const v0x=cx-ax,v0y=cy-ay,v1x=bx-ax,v1y=by-ay,v2x=x-ax,v2y=y-ay; const den=v0x*v1y-v1x*v0y; if(Math.abs(den)<1e-12) return false; const inv=1/den; const A=(v2x*v1y-v1x*v2y)*inv; const B=(v0x*v2y-v2x*v0y)*inv; const C=1-A-B; return A>0&&B>0&&C>0; }
22
+ const out=[]; let guard=0;
23
+ while (V.length>3 && guard<10000){ let ear=false; for (let i=0;i<V.length;i++){ const iPrev=V[(i+V.length-1)%V.length], iCurr=V[i], iNext=V[(i+1)%V.length]; if(!isConvex(iPrev,iCurr,iNext)) continue; let inside=false; for (let k=0;k<V.length;k++){ const idx=V[k]; if(idx===iPrev||idx===iCurr||idx===iNext) continue; if(inTri(iPrev,iCurr,iNext,idx)){ inside=true; break; } } if(inside) continue; out.push(iPrev,iCurr,iNext); V.splice(i,1); ear=true; break; } if(!ear) break; guard++; }
24
+ if (V.length===3) out.push(V[0],V[1],V[2]);
25
+ continent.tri = new Uint16Array(out);
26
+ }
27
+
28
+ function process(renderer, continent, R){
29
+ const poly = continent.polyXYZ; if(!poly || poly.length < 9) { continent._visTrisLen=0; continent._strokeRuns=[]; return; }
30
+ ensureTriangulated(continent);
31
+ const nVerts = poly.length/3;
32
+ if(!continent._rotated || continent._rotated.length !== poly.length){ continent._rotated = new Float32Array(poly.length); }
33
+ const rot = continent._rotated;
34
+ const R00=R[0],R01=R[3],R02=R[6]; const R10=R[1],R11=R[4],R12=R[7]; const R20=R[2],R21=R[5],R22=R[8];
35
+ for(let i=0;i<poly.length;i+=3){ const x=poly[i], y=poly[i+1], z=poly[i+2]; rot[i]=R00*x+R01*y+R02*z; rot[i+1]=R10*x+R11*y+R12*z; rot[i+2]=R20*x+R21*y+R22*z; }
36
+ // Mask
37
+ let mask = continent._frontMask; if(!mask || mask.length!==nVerts) mask = continent._frontMask = new Uint8Array(nVerts);
38
+ let front=0; for(let v=0,off=2; v<nVerts; v++, off+=3){ const f = rot[off] >= 0 ? 1:0; mask[v]=f; front+=f; }
39
+ if(front===0){ continent._visTrisLen=0; continent._strokeRuns=[]; return; }
40
+ const allFront = front===nVerts; const tri = continent.tri;
41
+ if(tri){ if(!continent._visTris || continent._visTris.length !== tri.length) continent._visTris = new Uint16Array(tri.length); if(allFront){ continent._visTris.set(tri); continent._visTrisLen=tri.length; } else { let w=0; for(let i=0;i<tri.length;i+=3){ const a=tri[i], b=tri[i+1], c=tri[i+2]; if(mask[a]&mask[b]&mask[c]){ continent._visTris[w++]=a; continent._visTris[w++]=b; continent._visTris[w++]=c; } } continent._visTrisLen=w; } }
42
+ if(allFront){ continent._strokeRuns=[{ idx: Array.from({length:nVerts}, (_,i)=>i) }]; return; }
43
+ const runs=[]; let curr=null; for(let i=0;i<=nVerts;i++){ const idx=i % nVerts; if(mask[idx]){ if(!curr) curr={idx:[]}; curr.idx.push(idx); } else if(curr){ if(curr.idx.length>1) runs.push(curr); curr=null; } } if(curr && curr.idx.length>1) runs.push(curr); continent._strokeRuns=runs;
44
+ }
45
+
46
+ module.exports = { process, name:'frontFace' };
@@ -0,0 +1,339 @@
1
+ // PlaneClipStrategy: spherical clipping of a polygon against the visible hemisphere (z >= 0)
2
+ // Produces clipped fill triangles and a stroke polyline in rotated (view) space.
3
+
4
+ // Helper math
5
+ function norm3(x, y, z) {
6
+ const l = Math.hypot(x, y, z) || 1; return [x / l, y / l, z / l];
7
+ }
8
+ function slerp(A, B, t) {
9
+ const [ax, ay, az] = norm3(A[0], A[1], A[2]);
10
+ const [bx, by, bz] = norm3(B[0], B[1], B[2]);
11
+ let dot = ax * bx + ay * by + az * bz; dot = Math.max(-1, Math.min(1, dot));
12
+ const th = Math.acos(dot);
13
+ if (th === 0) return [ax, ay, az];
14
+ const s = Math.sin(th), s1 = Math.sin((1 - t) * th) / s, s2 = Math.sin(t * th) / s;
15
+ return [ax * s1 + bx * s2, ay * s1 + by * s2, az * s1 + bz * s2];
16
+ }
17
+ // Sample a great-circle arc between A and B with optional horizon-aware refinement
18
+ function sampleGreatCircle(A, B, stepRad = 0.03, refineFn) {
19
+ const [ax, ay, az] = norm3(A[0],A[1],A[2]), [bx, by, bz] = norm3(B[0],B[1],B[2]);
20
+ let dot = ax*bx + ay*by + az*bz; dot = Math.max(-1, Math.min(1, dot));
21
+ const th = Math.acos(dot);
22
+ if (th === 0) return [[ax, ay, az]];
23
+ const n = Math.max(1, Math.ceil(th / stepRad));
24
+ const out = [];
25
+ let prev = null;
26
+ for (let i = 0; i <= n; i++) {
27
+ const t = i / n;
28
+ const p = slerp([ax,ay,az],[bx,by,bz], t);
29
+ if (refineFn && prev) {
30
+ const toAdd = refineFn(prev, p);
31
+ if (toAdd && toAdd.length) out.push(...toAdd);
32
+ }
33
+ out.push(p);
34
+ prev = p;
35
+ }
36
+ return out;
37
+ }
38
+ function intersectHorizon(A, B) {
39
+ const fz = (t) => slerp(A, B, t)[2];
40
+ let t0 = 0, t1 = 1, z0 = fz(t0), z1 = fz(t1);
41
+ if (z0 === 0) return slerp(A, B, 0);
42
+ if (z1 === 0) return slerp(A, B, 1);
43
+ if ((z0 > 0) === (z1 > 0)) return null;
44
+ for (let i = 0; i < 30; i++) {
45
+ const tm = 0.5 * (t0 + t1), zm = fz(tm);
46
+ if (Math.abs(zm) < 1e-8) return norm3(...slerp(A, B, tm));
47
+ if ((z0 > 0) === (zm > 0)) { t0 = tm; z0 = zm; } else { t1 = tm; z1 = zm; }
48
+ }
49
+ return norm3(...slerp(A, B, 0.5 * (t0 + t1)));
50
+ }
51
+
52
+ function clipPolygonHemisphere_SH(poly3) {
53
+ if (!poly3 || poly3.length < 3) return [];
54
+ const isInside = (p) => p[2] >= 0;
55
+ const out = [];
56
+ for (let i = 0; i < poly3.length; i++) {
57
+ const A = poly3[i];
58
+ const B = poly3[(i + 1) % poly3.length];
59
+ const Ain = isInside(A), Bin = isInside(B);
60
+ if (Ain && Bin) {
61
+ out.push(B);
62
+ } else if (Ain && !Bin) {
63
+ const X = intersectHorizon(A, B); if (X) out.push(X);
64
+ } else if (!Ain && Bin) {
65
+ const X = intersectHorizon(A, B); if (X) out.push(X);
66
+ out.push(B);
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+
72
+ // Tesselate polygon into small spherical triangles and cull to hemisphere
73
+ function tessellateClipHemisphere(poly3, opts = {}) {
74
+ const stepRad = opts.stepRad || 0.05; // densify edges before triangulation
75
+ const maxDepth = opts.maxDepth || 6;
76
+ const horizonRefine = opts.horizonRefine || { enabled: true, zThresh: 0.15, midSubdiv: 1 };
77
+ if (!poly3 || poly3.length < 3) return [];
78
+
79
+ // Densify the ring to improve initial triangulation
80
+ const dense = [];
81
+ const refineFn = horizonRefine && horizonRefine.enabled ? (P, Q) => {
82
+ const zP = P[2], zQ = Q[2];
83
+ if (Math.abs(zP) < horizonRefine.zThresh || Math.abs(zQ) < horizonRefine.zThresh || (zP > 0) !== (zQ > 0)) {
84
+ const mids = [];
85
+ for (let k = 1; k <= horizonRefine.midSubdiv; k++) {
86
+ const t = k / (horizonRefine.midSubdiv + 1);
87
+ mids.push(norm3(...slerp(P, Q, t)));
88
+ }
89
+ return mids;
90
+ }
91
+ return null;
92
+ } : null;
93
+
94
+ for (let i = 0; i < poly3.length; i++) {
95
+ const A = poly3[i], B = poly3[(i + 1) % poly3.length];
96
+ const seg = sampleGreatCircle(A, B, stepRad, refineFn);
97
+ if (i > 0 && dense.length) seg.shift(); // avoid duplicates between consecutive segments
98
+ dense.push(...seg);
99
+ }
100
+ if (dense.length && (dense[0] !== dense[dense.length - 1])) dense.push(dense[0]);
101
+
102
+ // Build a fan from a stable anchor (first vertex) to avoid degenerate centroid artifacts
103
+ const anchor = dense[0];
104
+ const tris = [];
105
+ for (let i = 1; i < dense.length - 1; i++) {
106
+ const A = dense[i], B = dense[i + 1];
107
+ tris.push([anchor, A, B]);
108
+ }
109
+
110
+ const result = [];
111
+ const allSign = (tri) => tri.every(p => p[2] >= 0) ? 1 : (tri.every(p => p[2] < 0) ? -1 : 0);
112
+
113
+ const subdivide = (tri, depth) => {
114
+ const sign = allSign(tri);
115
+ if (sign === 1) { result.push(tri); return; }
116
+ if (sign === -1) { return; }
117
+ if (depth >= maxDepth) {
118
+ const clipped = clipPolygonHemisphere_SH(tri);
119
+ if (clipped.length >= 3) {
120
+ for (let i = 1; i < clipped.length - 1; i++) {
121
+ result.push([clipped[0], clipped[i], clipped[i+1]]);
122
+ }
123
+ }
124
+ return;
125
+ }
126
+ const [A, B, C] = tri;
127
+ const AB = norm3(...slerp(A, B, 0.5));
128
+ const BC = norm3(...slerp(B, C, 0.5));
129
+ const CA = norm3(...slerp(C, A, 0.5));
130
+ subdivide([A, AB, CA], depth + 1);
131
+ subdivide([AB, B, BC], depth + 1);
132
+ subdivide([CA, BC, C], depth + 1);
133
+ subdivide([AB, BC, CA], depth + 1);
134
+ };
135
+
136
+ for (const t of tris) subdivide(t, 0);
137
+ return result; // array of triangles (arrays of 3 pts)
138
+ }
139
+
140
+ // Simple 2D ear clipping on XY projection (for polygons entirely within hemisphere)
141
+ function triangulateXY(poly) {
142
+ const n = poly.length; if (n < 3) return [];
143
+ const px = new Array(n), py = new Array(n);
144
+ for (let i = 0; i < n; i++) { px[i] = poly[i][0]; py[i] = poly[i][1]; }
145
+ const V = []; for (let i = 0; i < n; i++) V.push(i);
146
+ function area() { let a = 0; for (let i = 0; i < V.length; i++) { const i0 = V[i], i1 = V[(i + 1) % V.length]; a += px[i0] * py[i1] - px[i1] * py[i0]; } return a * 0.5; }
147
+ const orientCW = area() < 0;
148
+ function isConvex(a, b, c) { const ax = px[a], ay = py[a], bx = px[b], by = py[b], cx = px[c], cy = py[c]; const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); return orientCW ? cross < 0 : cross > 0; }
149
+ function inTri(a, b, c, p) { const x = px[p], y = py[p]; const ax = px[a], ay = py[a], bx = px[b], by = py[b], cx = px[c], cy = py[c]; const v0x = cx - ax, v0y = cy - ay, v1x = bx - ax, v1y = by - ay, v2x = x - ax, v2y = y - ay; const den = v0x * v1y - v1x * v0y; if (Math.abs(den) < 1e-12) return false; const inv = 1 / den; const A = (v2x * v1y - v1x * v2y) * inv; const B = (v0x * v2y - v2x * v0y) * inv; const C = 1 - A - B; return A > 0 && B > 0 && C > 0; }
150
+ const out = [];
151
+ let guard = 0;
152
+ while (V.length > 3 && guard < 10000) {
153
+ let ear = false;
154
+ for (let i = 0; i < V.length; i++) {
155
+ const iPrev = V[(i + V.length - 1) % V.length], iCurr = V[i], iNext = V[(i + 1) % V.length];
156
+ if (!isConvex(iPrev, iCurr, iNext)) continue;
157
+ let inside = false;
158
+ for (let k = 0; k < V.length; k++) { const idx = V[k]; if (idx === iPrev || idx === iCurr || idx === iNext) continue; if (inTri(iPrev, iCurr, iNext, idx)) { inside = true; break; } }
159
+ if (inside) continue;
160
+ out.push(iPrev, iCurr, iNext);
161
+ V.splice(i, 1);
162
+ ear = true;
163
+ break;
164
+ }
165
+ if (!ear) break; guard++;
166
+ }
167
+ if (V.length === 3) out.push(V[0], V[1], V[2]);
168
+ return out; // indices into poly
169
+ }
170
+
171
+ // Remove duplicate consecutive vertices and nearly collinear points (XY plane)
172
+ function cleanRingXY(pts, eps = 1e-10) {
173
+ if (!pts || pts.length < 3) return pts || [];
174
+ const out = [];
175
+ // remove consecutive duplicates
176
+ for (let i = 0; i < pts.length; i++) {
177
+ const p = pts[i];
178
+ if (!out.length) { out.push(p); continue; }
179
+ const q = out[out.length - 1];
180
+ if (Math.hypot(p[0] - q[0], p[1] - q[1]) > eps) out.push(p);
181
+ }
182
+ // if first equals last, drop last
183
+ if (out.length >= 2) {
184
+ const a = out[0], b = out[out.length - 1];
185
+ if (Math.hypot(a[0] - b[0], a[1] - b[1]) <= eps) out.pop();
186
+ }
187
+ if (out.length < 3) return out;
188
+ // remove collinear points by checking cross product with previous anchor
189
+ const res = [];
190
+ for (let i = 0; i < out.length; i++) {
191
+ const A = out[(i + out.length - 1) % out.length];
192
+ const B = out[i];
193
+ const C = out[(i + 1) % out.length];
194
+ const cross = (B[0] - A[0]) * (C[1] - A[1]) - (B[1] - A[1]) * (C[0] - A[0]);
195
+ if (Math.abs(cross) > eps) res.push(B);
196
+ }
197
+ return res;
198
+ }
199
+
200
+ function process(renderer, continent, R) {
201
+ const poly = continent.polyXYZ; if (!poly || poly.length < 9) { continent._clipFillTris = null; continent._clipStrokeXY = null; continent._visTrisLen = 0; return; }
202
+ // Rotate vertices to view space
203
+ if (!continent._rotated || continent._rotated.length !== poly.length) continent._rotated = new Float32Array(poly.length);
204
+ const rot = continent._rotated;
205
+ const R00 = R[0], R01 = R[3], R02 = R[6]; const R10 = R[1], R11 = R[4], R12 = R[7]; const R20 = R[2], R21 = R[5], R22 = R[8];
206
+ for (let i = 0; i < poly.length; i += 3) { const x = poly[i], y = poly[i + 1], z = poly[i + 2]; rot[i] = R00 * x + R01 * y + R02 * z; rot[i + 1] = R10 * x + R11 * y + R12 * z; rot[i + 2] = R20 * x + R21 * y + R22 * z; }
207
+
208
+ // Build polygon in rotated space
209
+ const n = poly.length / 3; const ring = new Array(n);
210
+ for (let i = 0; i < n; i++) { const k = i * 3; ring[i] = [rot[k], rot[k + 1], rot[k + 2]]; }
211
+
212
+ // Detect if entire polygon is strictly in front (including along edges)
213
+ const isAllFront = (() => {
214
+ const step = 0.15; // coarse check
215
+ for (let i = 0, n = ring.length; i < n; i++) {
216
+ const A = ring[i], B = ring[(i + 1) % n];
217
+ const seg = sampleGreatCircle(A, B, step);
218
+ for (const p of seg) if (p[2] < 0) return false;
219
+ }
220
+ return true;
221
+ })();
222
+
223
+ if (isAllFront) {
224
+ // Fast path: ear-clip the original ring in XY
225
+ const stepRad = 0.05;
226
+ const dense = [];
227
+ for (let i = 0; i < ring.length; i++) {
228
+ const A = ring[i], B = ring[(i + 1) % ring.length];
229
+ const seg = sampleGreatCircle(A, B, stepRad);
230
+ if (i > 0 && dense.length) seg.shift();
231
+ dense.push(...seg);
232
+ }
233
+ let poly2 = cleanRingXY(dense.map(p => [p[0], p[1], p[2]]), 1e-12);
234
+ // Provide a clean fill path to avoid AA seams
235
+ continent._clipFillPathXY = new Float32Array(poly2.length * 2);
236
+ for (let i = 0, j = 0; i < poly2.length; i++) { continent._clipFillPathXY[j++] = poly2[i][0]; continent._clipFillPathXY[j++] = poly2[i][1]; }
237
+ const triIdx = triangulateXY(poly2);
238
+ if (triIdx && triIdx.length >= 3 && (triIdx.length % 3) === 0) {
239
+ if (!continent._clipFillTris || continent._clipFillTris.length !== triIdx.length * 2) continent._clipFillTris = new Float32Array(triIdx.length * 2);
240
+ let w = 0; for (let i = 0; i < triIdx.length; i++) { const p = poly2[triIdx[i]]; continent._clipFillTris[w++] = p[0]; continent._clipFillTris[w++] = p[1]; }
241
+ continent._clipFillTrisLen = triIdx.length;
242
+ } else {
243
+ continent._clipFillTris = null; continent._clipFillTrisLen = 0;
244
+ }
245
+ // Single stroke run around full boundary
246
+ const run = new Float32Array(dense.length * 2);
247
+ for (let i = 0, j = 0; i < dense.length; i++) { run[j++] = dense[i][0]; run[j++] = dense[i][1]; }
248
+ continent._clipStrokeRuns = [run];
249
+ continent._visTrisLen = 0;
250
+ return;
251
+ }
252
+
253
+ // Partial visibility: clip to hemisphere and ear-clip the resulting polygon in XY
254
+ let clipped = clipPolygonHemisphere_SH(ring);
255
+ if (!clipped || clipped.length < 3) { continent._clipFillTris = null; continent._clipStrokeRuns = []; continent._visTrisLen = 0; return; }
256
+ const stepRadFill = 0.025;
257
+ const dens = [];
258
+ for (let i = 0; i < clipped.length; i++) {
259
+ const A = clipped[i], B = clipped[(i + 1) % clipped.length];
260
+ const seg = sampleGreatCircle(A, B, stepRadFill, (P, Q) => {
261
+ const zP = P[2], zQ = Q[2];
262
+ if ((zP >= 0 && zQ >= 0) && (Math.abs(zP) < 0.12 || Math.abs(zQ) < 0.12)) {
263
+ // bias sampling toward the horizon for visible edges
264
+ return [norm3(...slerp(P, Q, 0.33)), norm3(...slerp(P, Q, 0.66))];
265
+ }
266
+ if ((zP > 0) !== (zQ > 0)) {
267
+ // crossing: ensure mid sample for stability
268
+ return [norm3(...slerp(P, Q, 0.5))];
269
+ }
270
+ return null;
271
+ });
272
+ if (i > 0 && dens.length) seg.shift();
273
+ dens.push(...seg);
274
+ }
275
+ let poly2 = cleanRingXY(dens.map(p => [p[0], p[1], p[2]]), 1e-12);
276
+ // Provide a clean fill path for partial visibility as well
277
+ continent._clipFillPathXY = new Float32Array(poly2.length * 2);
278
+ for (let i = 0, j = 0; i < poly2.length; i++) { continent._clipFillPathXY[j++] = poly2[i][0]; continent._clipFillPathXY[j++] = poly2[i][1]; }
279
+ let triIdx = triangulateXY(poly2);
280
+ if (!triIdx || triIdx.length < 3 || (triIdx.length % 3) !== 0) {
281
+ // Final fallback: spherical tessellation
282
+ const smallTris = tessellateClipHemisphere(ring, { stepRad: 0.03, maxDepth: 6, horizonRefine: { enabled: true, zThresh: 0.1, midSubdiv: 1 } });
283
+ if (!smallTris || !smallTris.length) { continent._clipFillTris = null; continent._clipStrokeRuns = []; continent._visTrisLen = 0; }
284
+ else {
285
+ const idxCount = smallTris.length * 3;
286
+ const floats = idxCount * 2;
287
+ if (!continent._clipFillTris || continent._clipFillTris.length !== floats) continent._clipFillTris = new Float32Array(floats);
288
+ let w = 0; for (const tri of smallTris) { for (let k = 0; k < 3; k++) { const p = tri[k]; continent._clipFillTris[w++] = p[0]; continent._clipFillTris[w++] = p[1]; } }
289
+ continent._clipFillTrisLen = idxCount;
290
+ }
291
+ } else {
292
+ if (!continent._clipFillTris || continent._clipFillTris.length !== triIdx.length * 2) continent._clipFillTris = new Float32Array(triIdx.length * 2);
293
+ let w = 0; for (let i = 0; i < triIdx.length; i++) { const p = poly2[triIdx[i]]; continent._clipFillTris[w++] = p[0]; continent._clipFillTris[w++] = p[1]; }
294
+ continent._clipFillTrisLen = triIdx.length;
295
+ }
296
+
297
+ // Build visible coastline stroke runs
298
+ const runs = [];
299
+ let curr = [];
300
+ const pushXY = (pt) => {
301
+ const x = pt[0], y = pt[1];
302
+ const L = curr.length;
303
+ if (L >= 2) { const dx = x - curr[L-2], dy = y - curr[L-1]; if (dx*dx + dy*dy < 1e-12) return; }
304
+ curr.push(x, y);
305
+ };
306
+ for (let i = 0, n = ring.length; i < n; i++) {
307
+ const A = ring[i], B = ring[(i + 1) % n];
308
+ const Ain = A[2] >= 0, Bin = B[2] >= 0;
309
+ if (Ain && Bin) {
310
+ const seg = sampleGreatCircle(A, B, stepRadFill);
311
+ if (!curr.length && seg.length) pushXY(seg[0]);
312
+ for (let s = 1; s < seg.length; s++) pushXY(seg[s]);
313
+ } else if (Ain && !Bin) {
314
+ const X = intersectHorizon(A, B); if (X) {
315
+ const seg = sampleGreatCircle(A, X, stepRadFill);
316
+ if (!curr.length && seg.length) pushXY(seg[0]);
317
+ for (let s = 1; s < seg.length; s++) pushXY(seg[s]);
318
+ }
319
+ if (curr.length >= 4) runs.push(new Float32Array(curr));
320
+ curr = [];
321
+ } else if (!Ain && Bin) {
322
+ const X = intersectHorizon(A, B); if (X) {
323
+ const seg = sampleGreatCircle(X, B, stepRadFill);
324
+ curr = [];
325
+ if (seg.length) { pushXY(seg[0]); for (let s = 1; s < seg.length; s++) pushXY(seg[s]); }
326
+ }
327
+ } else {
328
+ if (curr.length >= 4) runs.push(new Float32Array(curr));
329
+ curr = [];
330
+ }
331
+ }
332
+ if (curr.length >= 4) runs.push(new Float32Array(curr));
333
+ continent._clipStrokeRuns = runs;
334
+
335
+ // Disable legacy front-face fill for this continent (handled via _clipFillTris)
336
+ continent._visTrisLen = 0;
337
+ }
338
+
339
+ module.exports = { process, name: 'planeClip' };
package/module.js CHANGED
@@ -16,7 +16,9 @@ jsgui.controls.Active_HTML_Document = require('./controls/Active_HTML_Document')
16
16
 
17
17
  //const Resource_Publisher = require('./publishing/http-resource-publisher');
18
18
  //jsgui.Resource_Publisher = Resource_Publisher;
19
- jsgui.Server = require('./server');
19
+ const Server = require('./server');
20
+ jsgui.Server = Server;
21
+ jsgui.serve = Server.serve;
20
22
  jsgui.fs2 = require('./fs2');
21
23
  //jsgui.Resource = Resource;
22
24
  //console.log('pre scs');
package/package.json CHANGED
@@ -7,14 +7,15 @@
7
7
  "@babel/generator": "^7.28.3",
8
8
  "@babel/parser": "^7.28.4",
9
9
  "cookies": "^0.9.1",
10
- "esbuild": "^0.25.9",
10
+ "esbuild": "^0.25.11",
11
11
  "fnl": "^0.0.36",
12
12
  "fnlfs": "^0.0.33",
13
13
  "jsgui3-client": "^0.0.120",
14
- "jsgui3-html": "^0.0.166",
14
+ "jsgui3-html": "^0.0.168",
15
15
  "jsgui3-webpage": "^0.0.8",
16
16
  "jsgui3-website": "^0.0.8",
17
- "lang-tools": "^0.0.36",
17
+ "lang-tools": "^0.0.39",
18
+ "mocha": "^11.7.4",
18
19
  "multiparty": "^4.2.3",
19
20
  "ncp": "^2.0.0",
20
21
  "obext": "^0.0.31",
@@ -38,5 +39,10 @@
38
39
  "type": "git",
39
40
  "url": "https://github.com/metabench/jsgui3-server.git"
40
41
  },
41
- "version": "0.0.134"
42
+ "version": "0.0.136",
43
+ "scripts": {
44
+ "cli": "node cli.js",
45
+ "serve": "node cli.js serve",
46
+ "test": "mocha tests/**/*.test.js"
47
+ }
42
48
  }
@@ -440,6 +440,14 @@ class Website_Resource extends Resource {
440
440
  var remoteAddress = req.connection.remoteAddress;
441
441
  var router = this.router;
442
442
  var res_process = router.process(req, res);
443
+ var processed_handled = false;
444
+ if (res_process) {
445
+ if (typeof res_process === 'object') {
446
+ processed_handled = !!res_process.handled;
447
+ } else if (res_process === true) {
448
+ processed_handled = true;
449
+ }
450
+ }
443
451
 
444
452
  // Likely will need to make this more advanced, possibly to suit a spec.
445
453
  // Website_Resource_Publisher may be the best place to deal with this.
@@ -448,7 +456,7 @@ class Website_Resource extends Resource {
448
456
  // Possibly we need a Control_Publisher?
449
457
  // To publish a Control at an address on the web.
450
458
 
451
- console.log('Website_Resource !!res_process', !!res_process);
459
+ console.log('Website_Resource !!res_process', processed_handled);
452
460
 
453
461
  // Need more HTTP request to response handlers.
454
462
  // Or just HTTP handlers really.
@@ -463,7 +471,7 @@ class Website_Resource extends Resource {
463
471
 
464
472
 
465
473
 
466
- if (res_process === false) {
474
+ if (!processed_handled) {
467
475
  if (req.url === "/") {
468
476
  // Seems like too much of a special case.
469
477