jsgui3-server 0.0.134 → 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.
- 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/Clipping.js +0 -0
- package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +319 -762
- package/examples/controls/14d) window, canvas globe/RenderingPipeline.js +43 -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 +9 -3
- package/resources/_old_website-resource.js +10 -2
- package/resources/jsbuilder/babel/deep_iterate/deep_iterate_babel.js +37 -0
- 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,43 @@
|
|
|
1
|
+
// RenderingPipeline.js (modular version)
|
|
2
|
+
// Always loads fresh (removed previous idempotent caching to allow stage changes like ClippingStage).
|
|
3
|
+
|
|
4
|
+
const BaseStage = require('./pipeline/BaseStage');
|
|
5
|
+
const TransformStage = require('./pipeline/TransformStage');
|
|
6
|
+
const ShadeSphereStage = require('./pipeline/ShadeSphereStage');
|
|
7
|
+
const GridStage = require('./pipeline/GridStage');
|
|
8
|
+
const ClippingStage = require('./pipeline/ClippingStage');
|
|
9
|
+
const ContinentsStage = require('./pipeline/ContinentsStage');
|
|
10
|
+
const ComposeStage = require('./pipeline/ComposeStage');
|
|
11
|
+
const HUDStage = require('./pipeline/HUDStage');
|
|
12
|
+
|
|
13
|
+
class RenderingPipeline {
|
|
14
|
+
constructor(renderer, stages){
|
|
15
|
+
this.r=renderer;
|
|
16
|
+
this.setStages(stages && stages.length ? stages : [
|
|
17
|
+
new TransformStage(renderer),
|
|
18
|
+
new ShadeSphereStage(renderer),
|
|
19
|
+
new GridStage(renderer),
|
|
20
|
+
new ClippingStage(renderer),
|
|
21
|
+
new ContinentsStage(renderer),
|
|
22
|
+
new ComposeStage(renderer),
|
|
23
|
+
new HUDStage(renderer)
|
|
24
|
+
]);
|
|
25
|
+
this._rs={ renderer, ctx:renderer.ctx, width:0,height:0,cx:0,cy:0,radius:0,interactive:false,time:0,qualityScale:1 };
|
|
26
|
+
}
|
|
27
|
+
setStages(stages){ this.stages=stages; }
|
|
28
|
+
run({interactive=false}={}){
|
|
29
|
+
const r=this.r; const ctx=r.ctx; const rect=r.canvas.getBoundingClientRect();
|
|
30
|
+
const width=rect.width||r.canvas.width/r.opts.dpr; const height=rect.height||r.canvas.height/r.opts.dpr;
|
|
31
|
+
ctx.clearRect(0,0,width,height);
|
|
32
|
+
if(r.opts.background){ ctx.fillStyle=r.opts.background; ctx.fillRect(0,0,width,height); }
|
|
33
|
+
const rs=this._rs; rs.interactive=interactive; rs.time=performance.now(); rs.ctx=ctx; rs.width=width; rs.height=height;
|
|
34
|
+
for(const s of this.stages) if(s.prepare) s.prepare(rs);
|
|
35
|
+
for(const s of this.stages) s.execute(rs);
|
|
36
|
+
}
|
|
37
|
+
insertStageBefore(stage, Type){ const i=this.stages.findIndex(s=>s instanceof Type); if(i===-1) this.stages.push(stage); else this.stages.splice(i,0,stage); }
|
|
38
|
+
removeStage(Type){ const i=this.stages.findIndex(s=>s instanceof Type); if(i!==-1) this.stages.splice(i,1); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const exportsObj = { RenderingPipeline, BaseStage, TransformStage, ShadeSphereStage, GridStage, ClippingStage, ContinentsStage, ComposeStage, HUDStage };
|
|
42
|
+
if (typeof globalThis !== 'undefined') globalThis.__EG_RENDERING_PIPELINE_EXPORTS__ = exportsObj; // still update for debugging
|
|
43
|
+
module.exports = exportsObj;
|
|
@@ -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,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' };
|