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.
- package/AGENTS.md +6 -0
- package/README.md +114 -18
- package/TODO.md +81 -0
- package/cli.js +96 -0
- package/docs/simple-server-api-design.md +702 -0
- package/examples/controls/14d) window, canvas globe/ARCBALL-README.md +146 -0
- package/examples/controls/14d) window, canvas globe/Clipping.js +0 -0
- package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +280 -799
- package/examples/controls/14d) window, canvas globe/RenderingPipeline.js +43 -0
- package/examples/controls/14d) window, canvas globe/arcball-drag-behaviour.js +250 -0
- package/examples/controls/14d) window, canvas globe/arcball-drag-behaviour.test.js +141 -0
- package/examples/controls/14d) window, canvas globe/drag-behaviour-base.js +70 -0
- package/examples/controls/14d) window, canvas globe/drag-controller.js +181 -0
- package/examples/controls/14d) window, canvas globe/math.test.js +281 -0
- package/examples/controls/14d) window, canvas globe/pipeline/BaseStage.js +46 -0
- package/examples/controls/14d) window, canvas globe/pipeline/ClippingStage.js +29 -0
- package/examples/controls/14d) window, canvas globe/pipeline/ComposeStage.js +15 -0
- package/examples/controls/14d) window, canvas globe/pipeline/ContinentsStage.js +116 -0
- package/examples/controls/14d) window, canvas globe/pipeline/GridStage.js +105 -0
- package/examples/controls/14d) window, canvas globe/pipeline/HUDStage.js +10 -0
- package/examples/controls/14d) window, canvas globe/pipeline/ShadeSphereStage.js +153 -0
- package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +23 -0
- package/examples/controls/14d) window, canvas globe/pipeline/clipping/FrontFaceStrategy.js +46 -0
- package/examples/controls/14d) window, canvas globe/pipeline/clipping/PlaneClipStrategy.js +339 -0
- package/module.js +3 -1
- package/package.json +10 -4
- package/resources/_old_website-resource.js +10 -2
- package/resources/jsbuilder/babel/deep_iterate/deep_iterate_babel.js +37 -0
- package/resources/local-server-info-resource.js +1 -1
- package/serve-factory.js +221 -0
- package/serve-helpers.js +95 -0
- package/server.js +93 -7
- package/tests/cli.test.js +66 -0
- package/tests/dummy-client.js +10 -0
- package/tests/serve.test.js +210 -0
- 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;
|