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,281 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const {
5
+ qIdentity,
6
+ qNormalize,
7
+ qMul,
8
+ qFromAxisAngle,
9
+ qFromVectors,
10
+ mat3FromQuat,
11
+ mat3Transpose
12
+ } = require('./math');
13
+
14
+ describe('Quaternion Math Functions', function() {
15
+
16
+ // Helper to check if two numbers are approximately equal
17
+ function approx_equal(a, b, epsilon = 1e-10) {
18
+ return Math.abs(a - b) < epsilon;
19
+ }
20
+
21
+ // Helper to check if two arrays are approximately equal
22
+ function approx_equal_array(a, b, epsilon = 1e-10) {
23
+ if (a.length !== b.length) return false;
24
+ for (let i = 0; i < a.length; i++) {
25
+ if (!approx_equal(a[i], b[i], epsilon)) return false;
26
+ }
27
+ return true;
28
+ }
29
+
30
+ // Helper to compute quaternion magnitude
31
+ function q_magnitude(q) {
32
+ return Math.sqrt(q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]);
33
+ }
34
+
35
+ describe('qIdentity', function() {
36
+ it('should return identity quaternion [0, 0, 0, 1]', function() {
37
+ const q = qIdentity();
38
+ assert.deepStrictEqual(q, [0, 0, 0, 1]);
39
+ });
40
+ });
41
+
42
+ describe('qNormalize', function() {
43
+ it('should normalize a quaternion to unit length', function() {
44
+ const q = [1, 2, 3, 4];
45
+ const normalized = qNormalize(q);
46
+ const mag = q_magnitude(normalized);
47
+ assert(approx_equal(mag, 1.0), `Magnitude should be 1.0, got ${mag}`);
48
+ });
49
+
50
+ it('should handle zero quaternion gracefully', function() {
51
+ const q = [0, 0, 0, 0];
52
+ const normalized = qNormalize(q);
53
+ // Should normalize to [0,0,0,0] since magnitude is 0
54
+ assert.deepStrictEqual(normalized, [0, 0, 0, 0]);
55
+ });
56
+
57
+ it('should preserve already normalized quaternions', function() {
58
+ const q = qIdentity();
59
+ const normalized = qNormalize(q);
60
+ assert(approx_equal_array(normalized, [0, 0, 0, 1]));
61
+ });
62
+ });
63
+
64
+ describe('qMul', function() {
65
+ it('should multiply two quaternions correctly', function() {
66
+ // Identity * anything = anything
67
+ const q1 = qIdentity();
68
+ const q2 = [1, 2, 3, 4];
69
+ const result = qMul(q1, q2);
70
+ assert(approx_equal_array(result, q2, 1e-9));
71
+ });
72
+
73
+ it('should be non-commutative (q1*q2 != q2*q1 in general)', function() {
74
+ const q1 = qFromAxisAngle(1, 0, 0, Math.PI / 4);
75
+ const q2 = qFromAxisAngle(0, 1, 0, Math.PI / 4);
76
+ const r1 = qMul(q1, q2);
77
+ const r2 = qMul(q2, q1);
78
+
79
+ // These should not be equal
80
+ const are_equal = approx_equal_array(r1, r2, 1e-6);
81
+ assert(!are_equal, 'Quaternion multiplication should be non-commutative');
82
+ });
83
+
84
+ it('should compose rotations correctly', function() {
85
+ // 90° rotation around Z-axis
86
+ const q1 = qFromAxisAngle(0, 0, 1, Math.PI / 2);
87
+ // Another 90° rotation around Z-axis
88
+ const q2 = qFromAxisAngle(0, 0, 1, Math.PI / 2);
89
+ // Combined should be 180° around Z-axis
90
+ const result = qNormalize(qMul(q1, q2));
91
+ const expected = qFromAxisAngle(0, 0, 1, Math.PI);
92
+
93
+ // Allow for both q and -q (they represent same rotation)
94
+ const match = approx_equal_array(result, expected, 1e-9) ||
95
+ approx_equal_array(result, [-expected[0], -expected[1], -expected[2], -expected[3]], 1e-9);
96
+ assert(match, `Expected ${expected}, got ${result}`);
97
+ });
98
+ });
99
+
100
+ describe('qFromAxisAngle', function() {
101
+ it('should create quaternion from axis-angle', function() {
102
+ // 180° rotation around X-axis
103
+ const q = qFromAxisAngle(1, 0, 0, Math.PI);
104
+ const mag = q_magnitude(q);
105
+ assert(approx_equal(mag, 1.0), 'Quaternion should be unit length');
106
+ assert(approx_equal(q[0], 1.0, 1e-9), 'X component should be ~1');
107
+ assert(approx_equal(q[3], 0.0, 1e-9), 'W component should be ~0');
108
+ });
109
+
110
+ it('should handle zero angle (identity)', function() {
111
+ const q = qFromAxisAngle(1, 0, 0, 0);
112
+ assert(approx_equal_array(q, [0, 0, 0, 1], 1e-9));
113
+ });
114
+
115
+ it('should normalize axis internally', function() {
116
+ // Non-unit axis
117
+ const q = qFromAxisAngle(3, 4, 0, Math.PI / 2);
118
+ const mag = q_magnitude(q);
119
+ assert(approx_equal(mag, 1.0), 'Result should be unit quaternion');
120
+ });
121
+ });
122
+
123
+ describe('qFromVectors', function() {
124
+ it('should create quaternion rotating u to v', function() {
125
+ const u = [1, 0, 0];
126
+ const v = [0, 1, 0];
127
+ const q = qFromVectors(u, v);
128
+
129
+ // Verify it's a unit quaternion
130
+ const mag = q_magnitude(q);
131
+ assert(approx_equal(mag, 1.0), 'Should be unit quaternion');
132
+ });
133
+
134
+ it('should handle identical vectors (no rotation)', function() {
135
+ const u = [1, 0, 0];
136
+ const v = [1, 0, 0];
137
+ const q = qFromVectors(u, v);
138
+
139
+ // Should be identity or very close
140
+ assert(approx_equal_array(q, [0, 0, 0, 1], 1e-9));
141
+ });
142
+
143
+ it('should handle opposite vectors (180° rotation)', function() {
144
+ const u = [1, 0, 0];
145
+ const v = [-1, 0, 0];
146
+ const q = qFromVectors(u, v);
147
+
148
+ // Should be a 180° rotation
149
+ const mag = q_magnitude(q);
150
+ assert(approx_equal(mag, 1.0), 'Should be unit quaternion');
151
+ // W component should be ~0 for 180° rotation
152
+ assert(approx_equal(Math.abs(q[3]), 0.0, 1e-9));
153
+ });
154
+
155
+ it('should handle small angle rotations robustly', function() {
156
+ const u = [1, 0, 0];
157
+ const v = [0.99999, 0.00001, 0];
158
+ // Normalize v
159
+ const vn = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
160
+ v[0] /= vn; v[1] /= vn; v[2] /= vn;
161
+
162
+ const q = qFromVectors(u, v);
163
+ const mag = q_magnitude(q);
164
+ assert(approx_equal(mag, 1.0), 'Should be unit quaternion');
165
+ });
166
+ });
167
+
168
+ describe('mat3FromQuat', function() {
169
+ it('should create rotation matrix from quaternion', function() {
170
+ const q = qIdentity();
171
+ const m = mat3FromQuat(q);
172
+
173
+ // Identity quaternion should give identity matrix
174
+ const identity = [
175
+ 1, 0, 0,
176
+ 0, 1, 0,
177
+ 0, 0, 1
178
+ ];
179
+ assert(approx_equal_array(m, identity, 1e-9));
180
+ });
181
+
182
+ it('should create correct matrix for 90° Z rotation', function() {
183
+ const q = qFromAxisAngle(0, 0, 1, Math.PI / 2);
184
+ const m = mat3FromQuat(q);
185
+
186
+ // 90° Z rotation matrix (column-major)
187
+ const expected = [
188
+ 0, 1, 0,
189
+ -1, 0, 0,
190
+ 0, 0, 1
191
+ ];
192
+ assert(approx_equal_array(m, expected, 1e-9));
193
+ });
194
+
195
+ it('should produce orthogonal matrix', function() {
196
+ const q = qNormalize([1, 2, 3, 4]);
197
+ const m = mat3FromQuat(q);
198
+
199
+ // Check if M * M^T = I (orthogonality test)
200
+ const mt = mat3Transpose(m);
201
+
202
+ // Multiply M * M^T (both are column-major)
203
+ const result = [];
204
+ for (let row = 0; row < 3; row++) {
205
+ for (let col = 0; col < 3; col++) {
206
+ let sum = 0;
207
+ for (let k = 0; k < 3; k++) {
208
+ // m is column-major: element at (row, k) is m[k*3 + row]
209
+ // mt is column-major: element at (k, col) is mt[col*3 + k]
210
+ sum += m[k * 3 + row] * mt[col * 3 + k];
211
+ }
212
+ result.push(sum);
213
+ }
214
+ }
215
+
216
+ const identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
217
+ assert(approx_equal_array(result, identity, 1e-9));
218
+ });
219
+ });
220
+
221
+ describe('mat3Transpose', function() {
222
+ it('should transpose a 3x3 matrix', function() {
223
+ const m = [
224
+ 1, 2, 3,
225
+ 4, 5, 6,
226
+ 7, 8, 9
227
+ ];
228
+ const mt = mat3Transpose(m);
229
+ const expected = [
230
+ 1, 4, 7,
231
+ 2, 5, 8,
232
+ 3, 6, 9
233
+ ];
234
+ assert.deepStrictEqual(mt, expected);
235
+ });
236
+
237
+ it('should be self-inverse (transpose twice = original)', function() {
238
+ const m = [1, 2, 3, 4, 5, 6, 7, 8, 9];
239
+ const mt = mat3Transpose(m);
240
+ const mtt = mat3Transpose(mt);
241
+ assert.deepStrictEqual(mtt, m);
242
+ });
243
+ });
244
+
245
+ describe('Integration Tests', function() {
246
+ it('should rotate a vector correctly using quaternion', function() {
247
+ // Rotate point [1,0,0] by 90° around Z-axis
248
+ // Should get [0,1,0]
249
+ const q = qFromAxisAngle(0, 0, 1, Math.PI / 2);
250
+ const m = mat3FromQuat(q);
251
+
252
+ const v = [1, 0, 0];
253
+ const result = [
254
+ m[0] * v[0] + m[3] * v[1] + m[6] * v[2],
255
+ m[1] * v[0] + m[4] * v[1] + m[7] * v[2],
256
+ m[2] * v[0] + m[5] * v[1] + m[8] * v[2]
257
+ ];
258
+
259
+ assert(approx_equal_array(result, [0, 1, 0], 1e-9));
260
+ });
261
+
262
+ it('should compose multiple rotations correctly', function() {
263
+ // Rotate 90° around X, then 90° around Y
264
+ const qx = qFromAxisAngle(1, 0, 0, Math.PI / 2);
265
+ const qy = qFromAxisAngle(0, 1, 0, Math.PI / 2);
266
+ const qCombined = qNormalize(qMul(qy, qx));
267
+
268
+ // Apply to point [1, 0, 0]
269
+ const m = mat3FromQuat(qCombined);
270
+ const v = [1, 0, 0];
271
+ const result = [
272
+ m[0] * v[0] + m[3] * v[1] + m[6] * v[2],
273
+ m[1] * v[0] + m[4] * v[1] + m[7] * v[2],
274
+ m[2] * v[0] + m[5] * v[1] + m[8] * v[2]
275
+ ];
276
+
277
+ // Should get [0, 0, -1] (or close to it)
278
+ assert(approx_equal_array(result, [0, 0, -1], 1e-9));
279
+ });
280
+ });
281
+ });
@@ -0,0 +1,46 @@
1
+
2
+ /**
3
+ * @typedef {Object} RenderState
4
+ * @property {CanvasRenderingContext2D} ctx - Destination 2D context.
5
+ * @property {number} width - Canvas width in CSS pixels.
6
+ * @property {number} height - Canvas height in CSS pixels.
7
+ * @property {number} radius - Globe screen-space radius (px).
8
+ * @property {number} cx - Globe center X in canvas coordinates.
9
+ * @property {number} cy - Globe center Y in canvas coordinates.
10
+ * @property {boolean} interactive - True if frame triggered by interaction (may raise quality scale).
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} Continent
15
+ * @property {string} name
16
+ * @property {Float32Array} polyXYZ - Original great-circle densified polygon vertices (triplets).
17
+ * @property {Uint16Array|null} tri - Triangulated index list (ear clipping) referencing polygon vertex order.
18
+ * @property {Float32Array} [_rotated] - Per-frame rotated vertices (camera space).
19
+ * @property {Uint16Array} [_visTris] - Front-facing triangle indices (subset of tri).
20
+ * @property {number} [_visTrisLen] - Number of used indices in _visTris.
21
+ * @property {{idx:number[]}[]} [_strokeRuns] - Contiguous front-facing edge runs for outline stroking.
22
+ * @property {string|null} [fill]
23
+ * @property {string|null} [stroke]
24
+ * @property {number|null} [lineWidth]
25
+ */
26
+
27
+ /**
28
+ * BaseStage
29
+ * Lightweight interface each pipeline stage implements. Stages may override
30
+ * prepare (optional) and must implement execute. Keeping the interface tiny
31
+ * avoids overhead inside the render loop.
32
+ */
33
+ class BaseStage {
34
+ /**
35
+ * @param {any} r - Renderer instance.
36
+ */
37
+ constructor(r) {
38
+ this.r = r;
39
+ }
40
+ /** @param {RenderState} _rs */
41
+ prepare(_rs) {}
42
+ /** @param {RenderState} _rs */
43
+ execute(_rs) {}
44
+ }
45
+
46
+ module.exports = BaseStage;
@@ -0,0 +1,29 @@
1
+ const BaseStage = require('./BaseStage');
2
+ const frontFace = require('./clipping/FrontFaceStrategy');
3
+ const planeClip = require('./clipping/PlaneClipStrategy');
4
+
5
+ // Strategy registry (extensible)
6
+ const strategies = {
7
+ frontFace,
8
+ planeClip
9
+ };
10
+
11
+ class ClippingStage extends BaseStage {
12
+ constructor(r){
13
+ super(r);
14
+ }
15
+ /** @param {RenderState} _rs */
16
+ execute(_rs){
17
+ const r = this.r;
18
+ if(!r._continents) return;
19
+ // Choose best default strategy; allow override via opts.clipping.mode
20
+ const mode = r.opts.clipping?.mode || 'planeClip';
21
+ const strat = strategies[mode] || strategies.frontFace;
22
+ const R = r.R;
23
+ for(const c of r._continents){
24
+ strat.process(r, c, R);
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = ClippingStage;
@@ -0,0 +1,15 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ class ComposeStage extends BaseStage {
4
+ /** @param {RenderState} rs */
5
+ execute(rs) {
6
+ const ctx = rs.ctx;
7
+ ctx.beginPath();
8
+ ctx.arc(rs.cx, rs.cy, rs.radius, 0, Math.PI * 2);
9
+ ctx.strokeStyle = 'rgba(0,0,0,0.14)';
10
+ ctx.lineWidth = 1.1;
11
+ ctx.stroke();
12
+ }
13
+ }
14
+
15
+ module.exports = ComposeStage;
@@ -0,0 +1,116 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ class ContinentsStage extends BaseStage {
4
+ /** @param {RenderState} rs */
5
+ execute(rs) {
6
+ const rInst = this.r;
7
+ if (!rInst._continents) return;
8
+ const ctx = rInst.ctx;
9
+ const clippingEnabled = !!rInst.opts.clipping.enabled; // currently front-face only
10
+
11
+ for (const c of rInst._continents) {
12
+ const rot = c._rotated;
13
+ if (!rot) continue;
14
+
15
+ const fillStyle = c.fill || rInst.opts.antarcticaFill;
16
+ const strokeStyle = c.stroke || rInst.opts.antarcticaStroke;
17
+ const lw = c.lineWidth || rInst.opts.antarcticaLineWidth;
18
+
19
+ // Fill: draw a solid path when available (prevents triangle AA seams)
20
+ if (c._clipFillPathXY && c._clipFillPathXY.length >= 6) {
21
+ const path = c._clipFillPathXY;
22
+ ctx.save();
23
+ ctx.fillStyle = fillStyle;
24
+ ctx.beginPath();
25
+ ctx.moveTo(rs.cx + rs.radius * path[0], rs.cy - rs.radius * path[1]);
26
+ for (let i = 2; i < path.length; i += 2) {
27
+ const x = path[i], y = path[i+1];
28
+ ctx.lineTo(rs.cx + rs.radius * x, rs.cy - rs.radius * y);
29
+ }
30
+ ctx.closePath();
31
+ ctx.fill();
32
+ ctx.restore();
33
+ } else if (c._clipFillTris && c._clipFillTrisLen) {
34
+ const buf = c._clipFillTris;
35
+ ctx.save();
36
+ ctx.fillStyle = fillStyle;
37
+ for (let i = 0; i < c._clipFillTrisLen * 2; i += 6) {
38
+ const ax = buf[i], ay = buf[i + 1];
39
+ const bx = buf[i + 2], by = buf[i + 3];
40
+ const cx = buf[i + 4], cy = buf[i + 5];
41
+ ctx.beginPath();
42
+ ctx.moveTo(rs.cx + rs.radius * ax, rs.cy - rs.radius * ay);
43
+ ctx.lineTo(rs.cx + rs.radius * bx, rs.cy - rs.radius * by);
44
+ ctx.lineTo(rs.cx + rs.radius * cx, rs.cy - rs.radius * cy);
45
+ ctx.closePath();
46
+ ctx.fill();
47
+ }
48
+ ctx.restore();
49
+ } else if (c._visTrisLen) {
50
+ ctx.save();
51
+ ctx.fillStyle = fillStyle;
52
+ ctx.beginPath();
53
+ const len = c._visTrisLen;
54
+ for (let i = 0; i < len; i += 3) {
55
+ const ia = c._visTris[i];
56
+ const ib = c._visTris[i + 1];
57
+ const ic = c._visTris[i + 2];
58
+ const a = ia * 3, b = ib * 3, d = ic * 3;
59
+ const ax = rot[a], ay = rot[a + 1];
60
+ const bx = rot[b], by = rot[b + 1];
61
+ const cx = rot[d], cy = rot[d + 1];
62
+ ctx.moveTo(rs.cx + rs.radius * ax, rs.cy - rs.radius * ay);
63
+ ctx.lineTo(rs.cx + rs.radius * bx, rs.cy - rs.radius * by);
64
+ ctx.lineTo(rs.cx + rs.radius * cx, rs.cy - rs.radius * cy);
65
+ }
66
+ ctx.fill();
67
+ ctx.restore();
68
+ }
69
+
70
+ // Stroke: prefer clipped runs if present, else legacy runs, else legacy polyline
71
+ if (c._clipStrokeRuns && c._clipStrokeRuns.length) {
72
+ ctx.save();
73
+ ctx.strokeStyle = strokeStyle;
74
+ ctx.lineWidth = lw;
75
+ for (const buf of c._clipStrokeRuns) {
76
+ if (!buf || buf.length < 4) continue;
77
+ ctx.beginPath();
78
+ let sx = rs.cx + rs.radius * buf[0];
79
+ let sy = rs.cy - rs.radius * buf[1];
80
+ ctx.moveTo(sx, sy);
81
+ for (let i = 2; i < buf.length; i += 2) {
82
+ sx = rs.cx + rs.radius * buf[i];
83
+ sy = rs.cy - rs.radius * buf[i + 1];
84
+ ctx.lineTo(sx, sy);
85
+ }
86
+ ctx.stroke();
87
+ }
88
+ ctx.restore();
89
+ } else if (c._strokeRuns && c._strokeRuns.length) {
90
+ ctx.save();
91
+ ctx.strokeStyle = strokeStyle;
92
+ ctx.lineWidth = lw;
93
+ for (const run of c._strokeRuns) {
94
+ if (run.idx.length < 2) continue;
95
+ ctx.beginPath();
96
+ for (let i = 0; i < run.idx.length; i++) {
97
+ const vi = run.idx[i] * 3;
98
+ const x = rot[vi];
99
+ const y = rot[vi + 1];
100
+ const sx = rs.cx + rs.radius * x;
101
+ const sy = rs.cy - rs.radius * y;
102
+ if (!i) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy);
103
+ }
104
+ ctx.stroke();
105
+ }
106
+ ctx.restore();
107
+ }
108
+
109
+ // Placeholder: if future partial clipping is added, use original + rot arrays here
110
+ void clippingEnabled;
111
+ }
112
+ }
113
+ // Triangulation handled in ClippingStage
114
+ }
115
+
116
+ module.exports = ContinentsStage;
@@ -0,0 +1,105 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ // Precompute constant to avoid repeated division inside loops.
4
+ const DEG = Math.PI / 180;
5
+
6
+ class GridStage extends BaseStage {
7
+ /** @param {RenderState} rs */
8
+ execute(rs) {
9
+ const rInst = this.r;
10
+ if (!rInst.opts.grid?.enabled) return;
11
+
12
+ const g = rInst.opts.grid;
13
+ const params = rInst._gridCache?.params;
14
+
15
+ const unchanged = (
16
+ params &&
17
+ params.stepLat === g.stepLat &&
18
+ params.stepLon === g.stepLon &&
19
+ params.sampleStepDeg === g.sampleStepDeg
20
+ );
21
+
22
+ if (!unchanged) {
23
+ const parallels = [];
24
+ for (let lat = -80; lat <= 80; lat += g.stepLat) {
25
+ const pts = [];
26
+ for (let lon = -180; lon <= 180; lon += g.sampleStepDeg) {
27
+ const φ = lat * DEG;
28
+ const λ = lon * DEG;
29
+ const cφ = Math.cos(φ), sφ = Math.sin(φ);
30
+ const sλ = Math.sin(λ), cλ = Math.cos(λ);
31
+ pts.push(cφ * sλ, sφ, cφ * cλ);
32
+ }
33
+ parallels.push(new Float32Array(pts));
34
+ }
35
+
36
+ const meridians = [];
37
+ for (let lon = -180; lon < 180; lon += g.stepLon) {
38
+ const pts = [];
39
+ for (let lat = -90; lat <= 90; lat += g.sampleStepDeg) {
40
+ const φ = lat * DEG;
41
+ const λ = lon * DEG;
42
+ const cφ = Math.cos(φ), sφ = Math.sin(φ);
43
+ const sλ = Math.sin(λ), cλ = Math.cos(λ);
44
+ pts.push(cφ * sλ, sφ, cφ * cλ);
45
+ }
46
+ meridians.push(new Float32Array(pts));
47
+ }
48
+
49
+ rInst._gridCache = {
50
+ parallels,
51
+ meridians,
52
+ params: {
53
+ stepLat: g.stepLat,
54
+ stepLon: g.stepLon,
55
+ sampleStepDeg: g.sampleStepDeg
56
+ }
57
+ };
58
+ }
59
+
60
+ const cache = rInst._gridCache;
61
+ if (!cache) return;
62
+ const ctx = rInst.ctx;
63
+ const R = rInst.R;
64
+ const R00 = R[0], R01 = R[3], R02 = R[6];
65
+ const R10 = R[1], R11 = R[4], R12 = R[7];
66
+ const R20 = R[2], R21 = R[5], R22 = R[8];
67
+
68
+ ctx.save();
69
+ ctx.strokeStyle = g.color;
70
+ ctx.globalAlpha = g.alpha;
71
+ ctx.lineWidth = g.lineWidth;
72
+ ctx.lineCap = 'round';
73
+
74
+ const drawLines = (lines) => {
75
+ for (const L of lines) {
76
+ let drawing = false;
77
+ ctx.beginPath();
78
+ for (let i = 0; i < L.length; i += 3) {
79
+ const vx = L[i], vy = L[i + 1], vz = L[i + 2];
80
+ const xw = R00 * vx + R01 * vy + R02 * vz;
81
+ const yw = R10 * vx + R11 * vy + R12 * vz;
82
+ const zw = R20 * vx + R21 * vy + R22 * vz;
83
+ if (zw >= 0) {
84
+ const sx = rs.cx + rs.radius * xw;
85
+ const sy = rs.cy - rs.radius * yw;
86
+ if (!drawing) { ctx.moveTo(sx, sy); drawing = true; }
87
+ else ctx.lineTo(sx, sy);
88
+ } else if (drawing) {
89
+ ctx.stroke();
90
+ ctx.beginPath();
91
+ drawing = false;
92
+ }
93
+ }
94
+ if (drawing) ctx.stroke();
95
+ }
96
+ };
97
+
98
+ // Parallels first (visual layering), then meridians.
99
+ drawLines(cache.parallels);
100
+ drawLines(cache.meridians);
101
+ ctx.restore();
102
+ }
103
+ }
104
+
105
+ module.exports = GridStage;
@@ -0,0 +1,10 @@
1
+ const BaseStage = require('./BaseStage');
2
+
3
+ class HUDStage extends BaseStage {
4
+ /** @param {RenderState} _rs */
5
+ execute(_rs) {
6
+ if (this.r.opts.showFPS) this.r._drawFPS();
7
+ }
8
+ }
9
+
10
+ module.exports = HUDStage;