@aethervtt/shard-structure 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/bench.d.ts +15 -0
- package/dist/bench.js +112 -0
- package/dist/bricks.d.ts +27 -0
- package/dist/bricks.js +98 -0
- package/dist/compile.d.ts +292 -0
- package/dist/compile.js +1602 -0
- package/dist/components.d.ts +156 -0
- package/dist/components.js +294 -0
- package/dist/contact-shade.d.ts +4 -0
- package/dist/contact-shade.js +76 -0
- package/dist/contact.d.ts +65 -0
- package/dist/contact.js +343 -0
- package/dist/curve.d.ts +50 -0
- package/dist/curve.js +224 -0
- package/dist/fixtures.d.ts +77 -0
- package/dist/fixtures.js +327 -0
- package/dist/geometry.d.ts +178 -0
- package/dist/geometry.js +926 -0
- package/dist/harness.d.ts +27 -0
- package/dist/harness.js +74 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +189 -0
- package/dist/interior-plugin.d.ts +206 -0
- package/dist/interior-plugin.js +988 -0
- package/dist/interior.d.ts +159 -0
- package/dist/interior.js +645 -0
- package/dist/planar.d.ts +61 -0
- package/dist/planar.js +105 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +119 -0
- package/package.json +49 -4
package/dist/interior.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COVER_SCALE,
|
|
3
|
+
COVER_ZERO,
|
|
4
|
+
LINK_X,
|
|
5
|
+
LINK_Z,
|
|
6
|
+
ROW_CLEAR,
|
|
7
|
+
ROW_SCALE,
|
|
8
|
+
ROW_ZERO,
|
|
9
|
+
VISIBILITY_MAX
|
|
10
|
+
} from "@aethervtt/shard-render";
|
|
11
|
+
class FieldLayer {
|
|
12
|
+
/** Cover top y over the layer's base, per texel; Infinity where uncovered. */
|
|
13
|
+
cover;
|
|
14
|
+
/** LINK_X | LINK_Z: flux to the +x and +z neighbour is blocked. */
|
|
15
|
+
links;
|
|
16
|
+
/** The solve's visibility, before the blur: a region re-solve starts from it. */
|
|
17
|
+
value;
|
|
18
|
+
/** What uploads: blurred visibility, links and cover, packed (see `packTexel`). */
|
|
19
|
+
packed;
|
|
20
|
+
/** The y cover heights count from: the level's elevation (the ground level's is 0). */
|
|
21
|
+
base = 0;
|
|
22
|
+
constructor(width, height) {
|
|
23
|
+
const n = width * height;
|
|
24
|
+
this.cover = new Float32Array(n).fill(Number.POSITIVE_INFINITY);
|
|
25
|
+
this.links = new Uint8Array(n);
|
|
26
|
+
this.value = new Float32Array(n).fill(1);
|
|
27
|
+
this.packed = new Uint32Array(n);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class SolveScratch {
|
|
31
|
+
blurred = new Float32Array(0);
|
|
32
|
+
/** Per texel: neighbours reached, and 1 / (their count + κ). */
|
|
33
|
+
mask = new Uint8Array(0);
|
|
34
|
+
inv = new Float32Array(0);
|
|
35
|
+
/** Per tile: how far its texels moved in the last sweep, and in this one. */
|
|
36
|
+
moved = new Float32Array(0);
|
|
37
|
+
movedNext = new Float32Array(0);
|
|
38
|
+
crossings = new Float64Array(64);
|
|
39
|
+
/** Aggregation (the coarse pass): aggregate per texel, and per aggregate its sums. */
|
|
40
|
+
agg = new Int32Array(0);
|
|
41
|
+
aggValue = new Float64Array(0);
|
|
42
|
+
aggDiag = new Float64Array(0);
|
|
43
|
+
aggRhs = new Float64Array(0);
|
|
44
|
+
/** Coarse couplings as CSR. */
|
|
45
|
+
rowStart = new Int32Array(0);
|
|
46
|
+
colIndex = new Int32Array(0);
|
|
47
|
+
weight = new Float64Array(0);
|
|
48
|
+
stack = new Int32Array(0);
|
|
49
|
+
ensure(n) {
|
|
50
|
+
if (this.blurred.length < n) this.blurred = new Float32Array(n);
|
|
51
|
+
if (this.mask.length < n) {
|
|
52
|
+
this.mask = new Uint8Array(n);
|
|
53
|
+
this.inv = new Float32Array(n);
|
|
54
|
+
}
|
|
55
|
+
if (this.agg.length < n) {
|
|
56
|
+
this.agg = new Int32Array(n);
|
|
57
|
+
this.stack = new Int32Array(n);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function clipRect(grid, r) {
|
|
62
|
+
return {
|
|
63
|
+
x0: Math.max(0, r.x0),
|
|
64
|
+
z0: Math.max(0, r.z0),
|
|
65
|
+
x1: Math.min(grid.width, r.x1),
|
|
66
|
+
z1: Math.min(grid.height, r.z1)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function texelsOf(grid, b, grow) {
|
|
70
|
+
const t = grid.texel;
|
|
71
|
+
return clipRect(grid, {
|
|
72
|
+
x0: Math.floor((b.minX - grow - grid.ox) / t - 0.5),
|
|
73
|
+
z0: Math.floor((b.minZ - grow - grid.oz) / t - 0.5),
|
|
74
|
+
x1: Math.ceil((b.maxX + grow - grid.ox) / t - 0.5) + 1,
|
|
75
|
+
z1: Math.ceil((b.maxZ + grow - grid.oz) / t - 0.5) + 1
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function rasterCover(grid, layer, cover, rect, base, scratch) {
|
|
79
|
+
const f = cover.shape;
|
|
80
|
+
const t = grid.texel;
|
|
81
|
+
const z0 = Math.max(rect.z0, Math.floor((f.minZ - grid.oz) / t - 0.5));
|
|
82
|
+
const z1 = Math.min(rect.z1, Math.ceil((f.maxZ - grid.oz) / t - 0.5) + 1);
|
|
83
|
+
const rings = f.rings.length - 1;
|
|
84
|
+
for (let z = z0; z < z1; z++) {
|
|
85
|
+
const zc = grid.oz + (z + 0.5) * t;
|
|
86
|
+
let n = 0;
|
|
87
|
+
for (let r = 0; r < rings; r++) {
|
|
88
|
+
if (r > 0 && cover.closed.includes(r)) continue;
|
|
89
|
+
n = crossings(f.points, f.rings[r], f.rings[r + 1], zc, scratch, n);
|
|
90
|
+
}
|
|
91
|
+
fillSpans(grid, layer, f, rect, z, zc, base, scratch.crossings, n);
|
|
92
|
+
for (const r of cover.closed) {
|
|
93
|
+
const m = crossings(f.points, f.rings[r], f.rings[r + 1], zc, scratch, 0);
|
|
94
|
+
fillSpans(grid, layer, f, rect, z, zc, base, scratch.crossings, m);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function crossings(p, start, end, zc, scratch, n) {
|
|
99
|
+
for (let i = start, j = end - 1; i < end; j = i++) {
|
|
100
|
+
const zi = p[i * 2 + 1];
|
|
101
|
+
const zj = p[j * 2 + 1];
|
|
102
|
+
if (zi > zc === zj > zc) continue;
|
|
103
|
+
const xi = p[i * 2];
|
|
104
|
+
const xj = p[j * 2];
|
|
105
|
+
if (n >= scratch.crossings.length) {
|
|
106
|
+
const grown = new Float64Array(scratch.crossings.length * 2);
|
|
107
|
+
grown.set(scratch.crossings);
|
|
108
|
+
scratch.crossings = grown;
|
|
109
|
+
}
|
|
110
|
+
scratch.crossings[n++] = xi + (zc - zi) * (xj - xi) / (zj - zi);
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
function fillSpans(grid, layer, f, rect, z, zc, base, xs, n) {
|
|
115
|
+
const list = xs.subarray(0, n).sort();
|
|
116
|
+
const t = grid.texel;
|
|
117
|
+
const w = grid.width;
|
|
118
|
+
for (let k = 0; k + 1 < n; k += 2) {
|
|
119
|
+
const a = Math.max(rect.x0, Math.ceil((list[k] - grid.ox) / t - 0.5));
|
|
120
|
+
const b = Math.min(rect.x1, Math.ceil((list[k + 1] - grid.ox) / t - 0.5));
|
|
121
|
+
for (let x = a; x < b; x++) {
|
|
122
|
+
const xc = grid.ox + (x + 0.5) * t;
|
|
123
|
+
const y = f.elevation + f.slope * (xc * f.rx + zc * f.rz - f.d0) - base;
|
|
124
|
+
const i = z * w + x;
|
|
125
|
+
if (y < layer.cover[i]) layer.cover[i] = y;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function rasterBarrier(grid, layer, s, rect) {
|
|
130
|
+
const t = grid.texel;
|
|
131
|
+
const w = grid.width;
|
|
132
|
+
const eps = 1e-6;
|
|
133
|
+
const dx = s.bx - s.ax;
|
|
134
|
+
const dz = s.bz - s.az;
|
|
135
|
+
if (Math.abs(dz) > 1e-12) {
|
|
136
|
+
const lo = Math.min(s.az, s.bz) - eps;
|
|
137
|
+
const hi = Math.max(s.az, s.bz) + eps;
|
|
138
|
+
const z0 = Math.max(rect.z0, Math.ceil((lo - grid.oz) / t - 0.5));
|
|
139
|
+
const z1 = Math.min(rect.z1 - 1, Math.floor((hi - grid.oz) / t - 0.5));
|
|
140
|
+
for (let z = z0; z <= z1; z++) {
|
|
141
|
+
const zc = grid.oz + (z + 0.5) * t;
|
|
142
|
+
const u = Math.min(1, Math.max(0, (zc - s.az) / dz));
|
|
143
|
+
const x = s.ax + u * dx;
|
|
144
|
+
const f = (x - grid.ox) / t - 0.5;
|
|
145
|
+
let i = Math.floor(f);
|
|
146
|
+
markX(layer, rect, w, i, z);
|
|
147
|
+
if (Math.abs(f - Math.round(f)) < 1e-9) {
|
|
148
|
+
i = Math.round(f);
|
|
149
|
+
markX(layer, rect, w, i - 1, z);
|
|
150
|
+
markX(layer, rect, w, i, z);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (Math.abs(dx) > 1e-12) {
|
|
155
|
+
const lo = Math.min(s.ax, s.bx) - eps;
|
|
156
|
+
const hi = Math.max(s.ax, s.bx) + eps;
|
|
157
|
+
const x0 = Math.max(rect.x0, Math.ceil((lo - grid.ox) / t - 0.5));
|
|
158
|
+
const x1 = Math.min(rect.x1 - 1, Math.floor((hi - grid.ox) / t - 0.5));
|
|
159
|
+
for (let x = x0; x <= x1; x++) {
|
|
160
|
+
const xc = grid.ox + (x + 0.5) * t;
|
|
161
|
+
const u = Math.min(1, Math.max(0, (xc - s.ax) / dx));
|
|
162
|
+
const z = s.az + u * dz;
|
|
163
|
+
const f = (z - grid.oz) / t - 0.5;
|
|
164
|
+
let j = Math.floor(f);
|
|
165
|
+
markZ(layer, rect, w, x, j);
|
|
166
|
+
if (Math.abs(f - Math.round(f)) < 1e-9) {
|
|
167
|
+
j = Math.round(f);
|
|
168
|
+
markZ(layer, rect, w, x, j - 1);
|
|
169
|
+
markZ(layer, rect, w, x, j);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function markX(layer, rect, w, x, z) {
|
|
175
|
+
if (x >= rect.x0 && x < rect.x1 && z >= rect.z0 && z < rect.z1) layer.links[z * w + x] |= LINK_X;
|
|
176
|
+
}
|
|
177
|
+
function markZ(layer, rect, w, x, z) {
|
|
178
|
+
if (x >= rect.x0 && x < rect.x1 && z >= rect.z0 && z < rect.z1) layer.links[z * w + x] |= LINK_Z;
|
|
179
|
+
}
|
|
180
|
+
function clearRect(grid, layer, rect) {
|
|
181
|
+
const w = grid.width;
|
|
182
|
+
for (let z = rect.z0; z < rect.z1; z++) {
|
|
183
|
+
layer.cover.fill(Number.POSITIVE_INFINITY, z * w + rect.x0, z * w + rect.x1);
|
|
184
|
+
layer.links.fill(0, z * w + rect.x0, z * w + rect.x1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function screening(texel, reach) {
|
|
188
|
+
return texel * texel / Math.max(reach * reach, 1e-12);
|
|
189
|
+
}
|
|
190
|
+
function relaxation(kappa) {
|
|
191
|
+
const rho = 4 / (4 + kappa);
|
|
192
|
+
return 2 / (1 + Math.sqrt(1 - rho * rho));
|
|
193
|
+
}
|
|
194
|
+
const TILE = 16;
|
|
195
|
+
function solveRect(grid, layer, rect, options, scratch) {
|
|
196
|
+
const w = grid.width;
|
|
197
|
+
const h = grid.height;
|
|
198
|
+
const kappa = screening(grid.texel, options.reach);
|
|
199
|
+
const omega = relaxation(kappa);
|
|
200
|
+
const tolerance = options.tolerance ?? 1e-4;
|
|
201
|
+
const maxSweeps = options.maxSweeps ?? 2e3;
|
|
202
|
+
const v = layer.value;
|
|
203
|
+
const cover = layer.cover;
|
|
204
|
+
const links = layer.links;
|
|
205
|
+
scratch.ensure(w * h);
|
|
206
|
+
const mask = scratch.mask;
|
|
207
|
+
const inv = scratch.inv;
|
|
208
|
+
const linkX = LINK_X;
|
|
209
|
+
const linkZ = LINK_Z;
|
|
210
|
+
let texels = 0;
|
|
211
|
+
for (let z = rect.z0; z < rect.z1; z++) {
|
|
212
|
+
for (let x = rect.x0; x < rect.x1; x++) {
|
|
213
|
+
const i = z * w + x;
|
|
214
|
+
if (cover[i] === Number.POSITIVE_INFINITY) {
|
|
215
|
+
v[i] = 1;
|
|
216
|
+
mask[i] = 0;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
texels++;
|
|
220
|
+
let m = 16;
|
|
221
|
+
let n = 0;
|
|
222
|
+
if (x + 1 < w && (links[i] & linkX) === 0) {
|
|
223
|
+
m |= 1;
|
|
224
|
+
n++;
|
|
225
|
+
}
|
|
226
|
+
if (x > 0 && (links[i - 1] & linkX) === 0) {
|
|
227
|
+
m |= 2;
|
|
228
|
+
n++;
|
|
229
|
+
}
|
|
230
|
+
if (z + 1 < h && (links[i] & linkZ) === 0) {
|
|
231
|
+
m |= 4;
|
|
232
|
+
n++;
|
|
233
|
+
}
|
|
234
|
+
if (z > 0 && (links[i - w] & linkZ) === 0) {
|
|
235
|
+
m |= 8;
|
|
236
|
+
n++;
|
|
237
|
+
}
|
|
238
|
+
mask[i] = m;
|
|
239
|
+
inv[i] = 1 / (n + kappa);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let coarseSweeps = 0;
|
|
243
|
+
if (options.coarse && texels > 0)
|
|
244
|
+
coarseSweeps = coarsePass(grid, layer, rect, kappa, options.coarseCells ?? 4, scratch);
|
|
245
|
+
let sweeps = 0;
|
|
246
|
+
if (texels > 0) {
|
|
247
|
+
const tx = Math.ceil((rect.x1 - rect.x0) / TILE);
|
|
248
|
+
const tz = Math.ceil((rect.z1 - rect.z0) / TILE);
|
|
249
|
+
const tiles = tx * tz;
|
|
250
|
+
if (scratch.moved.length < tiles) {
|
|
251
|
+
scratch.moved = new Float32Array(tiles);
|
|
252
|
+
scratch.movedNext = new Float32Array(tiles);
|
|
253
|
+
}
|
|
254
|
+
let moved = scratch.moved;
|
|
255
|
+
let next = scratch.movedNext;
|
|
256
|
+
moved.fill(1, 0, tiles);
|
|
257
|
+
for (; sweeps < maxSweeps; sweeps++) {
|
|
258
|
+
let most = 0;
|
|
259
|
+
for (let b = 0; b < tz; b++) {
|
|
260
|
+
for (let a = 0; a < tx; a++) {
|
|
261
|
+
const t = b * tx + a;
|
|
262
|
+
if (moved[t] < tolerance && (a === 0 || moved[t - 1] < tolerance) && (a + 1 === tx || moved[t + 1] < tolerance) && (b === 0 || moved[t - tx] < tolerance) && (b + 1 === tz || moved[t + tx] < tolerance)) {
|
|
263
|
+
next[t] = 0;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
let tileMost = 0;
|
|
267
|
+
const x0 = rect.x0 + a * TILE;
|
|
268
|
+
const x1 = Math.min(rect.x1, x0 + TILE);
|
|
269
|
+
const z0 = rect.z0 + b * TILE;
|
|
270
|
+
const z1 = Math.min(rect.z1, z0 + TILE);
|
|
271
|
+
for (let z = z0; z < z1; z++) {
|
|
272
|
+
for (let i = z * w + x0, end = z * w + x1; i < end; i++) {
|
|
273
|
+
const m = mask[i];
|
|
274
|
+
if (m === 0) continue;
|
|
275
|
+
let sum = 0;
|
|
276
|
+
if (m & 1) sum += v[i + 1];
|
|
277
|
+
if (m & 2) sum += v[i - 1];
|
|
278
|
+
if (m & 4) sum += v[i + w];
|
|
279
|
+
if (m & 8) sum += v[i - w];
|
|
280
|
+
const old = v[i];
|
|
281
|
+
let value = old + omega * (sum * inv[i] - old);
|
|
282
|
+
if (value < 0) value = 0;
|
|
283
|
+
else if (value > 1) value = 1;
|
|
284
|
+
v[i] = value;
|
|
285
|
+
const d = value > old ? value - old : old - value;
|
|
286
|
+
if (d > tileMost) tileMost = d;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
next[t] = tileMost;
|
|
290
|
+
if (tileMost > most) most = tileMost;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const swap = moved;
|
|
294
|
+
moved = next;
|
|
295
|
+
next = swap;
|
|
296
|
+
if (most < tolerance) {
|
|
297
|
+
sweeps++;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
scratch.moved = moved;
|
|
302
|
+
scratch.movedNext = next;
|
|
303
|
+
}
|
|
304
|
+
return { texels, sweeps, coarseSweeps };
|
|
305
|
+
}
|
|
306
|
+
function coarsePass(grid, layer, rect, kappa, cells, scratch) {
|
|
307
|
+
const w = grid.width;
|
|
308
|
+
const n = w * grid.height;
|
|
309
|
+
scratch.ensure(n);
|
|
310
|
+
const agg = scratch.agg;
|
|
311
|
+
const cover = layer.cover;
|
|
312
|
+
const links = layer.links;
|
|
313
|
+
const v = layer.value;
|
|
314
|
+
const stack = scratch.stack;
|
|
315
|
+
for (let z = rect.z0; z < rect.z1; z++) agg.fill(-1, z * w + rect.x0, z * w + rect.x1);
|
|
316
|
+
let count = 0;
|
|
317
|
+
for (let z = rect.z0; z < rect.z1; z++) {
|
|
318
|
+
for (let x = rect.x0; x < rect.x1; x++) {
|
|
319
|
+
const seed = z * w + x;
|
|
320
|
+
if (agg[seed] !== -1 || cover[seed] === Number.POSITIVE_INFINITY) continue;
|
|
321
|
+
const bx = Math.floor((x - rect.x0) / cells);
|
|
322
|
+
const bz = Math.floor((z - rect.z0) / cells);
|
|
323
|
+
const id = count++;
|
|
324
|
+
let top = 0;
|
|
325
|
+
stack[top++] = seed;
|
|
326
|
+
agg[seed] = id;
|
|
327
|
+
while (top > 0) {
|
|
328
|
+
const i = stack[--top];
|
|
329
|
+
const ix = i % w;
|
|
330
|
+
const iz = (i - ix) / w;
|
|
331
|
+
for (let k = 0; k < 4; k++) {
|
|
332
|
+
let j;
|
|
333
|
+
let open;
|
|
334
|
+
if (k === 0) {
|
|
335
|
+
j = i + 1;
|
|
336
|
+
open = ix + 1 < rect.x1 && (links[i] & LINK_X) === 0;
|
|
337
|
+
} else if (k === 1) {
|
|
338
|
+
j = i - 1;
|
|
339
|
+
open = ix > rect.x0 && (links[i - 1] & LINK_X) === 0;
|
|
340
|
+
} else if (k === 2) {
|
|
341
|
+
j = i + w;
|
|
342
|
+
open = iz + 1 < rect.z1 && (links[i] & LINK_Z) === 0;
|
|
343
|
+
} else {
|
|
344
|
+
j = i - w;
|
|
345
|
+
open = iz > rect.z0 && (links[i - w] & LINK_Z) === 0;
|
|
346
|
+
}
|
|
347
|
+
if (!open || agg[j] !== -1 || cover[j] === Number.POSITIVE_INFINITY) continue;
|
|
348
|
+
const jx = j % w;
|
|
349
|
+
const jz = (j - jx) / w;
|
|
350
|
+
if (Math.floor((jx - rect.x0) / cells) !== bx || Math.floor((jz - rect.z0) / cells) !== bz)
|
|
351
|
+
continue;
|
|
352
|
+
agg[j] = id;
|
|
353
|
+
stack[top++] = j;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (count === 0) return 0;
|
|
359
|
+
if (scratch.aggValue.length < count) {
|
|
360
|
+
scratch.aggValue = new Float64Array(count * 2);
|
|
361
|
+
scratch.aggDiag = new Float64Array(count * 2);
|
|
362
|
+
scratch.aggRhs = new Float64Array(count * 2);
|
|
363
|
+
scratch.rowStart = new Int32Array(count * 2 + 1);
|
|
364
|
+
}
|
|
365
|
+
const diag = scratch.aggDiag;
|
|
366
|
+
const rhs = scratch.aggRhs;
|
|
367
|
+
const value = scratch.aggValue;
|
|
368
|
+
diag.fill(0, 0, count);
|
|
369
|
+
rhs.fill(0, 0, count);
|
|
370
|
+
const scale = 1 / cells;
|
|
371
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
372
|
+
const h = grid.height;
|
|
373
|
+
const aggIn = (i) => {
|
|
374
|
+
const x = i % w;
|
|
375
|
+
const z = (i - x) / w;
|
|
376
|
+
return x >= rect.x0 && x < rect.x1 && z >= rect.z0 && z < rect.z1 ? agg[i] : -1;
|
|
377
|
+
};
|
|
378
|
+
const pair = (i, j) => {
|
|
379
|
+
const a = aggIn(i);
|
|
380
|
+
const b = aggIn(j);
|
|
381
|
+
if (a === b) return;
|
|
382
|
+
if (a >= 0 && b >= 0) {
|
|
383
|
+
diag[a] = diag[a] + scale;
|
|
384
|
+
diag[b] = diag[b] + scale;
|
|
385
|
+
const key = a < b ? a * count + b : b * count + a;
|
|
386
|
+
pairs.set(key, (pairs.get(key) ?? 0) + scale);
|
|
387
|
+
} else if (a >= 0) {
|
|
388
|
+
diag[a] = diag[a] + scale;
|
|
389
|
+
rhs[a] = rhs[a] + scale * v[j];
|
|
390
|
+
} else {
|
|
391
|
+
diag[b] = diag[b] + scale;
|
|
392
|
+
rhs[b] = rhs[b] + scale * v[i];
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
for (let z = Math.max(0, rect.z0 - 1); z < Math.min(h, rect.z1 + 1); z++) {
|
|
396
|
+
for (let x = Math.max(0, rect.x0 - 1); x < Math.min(w, rect.x1 + 1); x++) {
|
|
397
|
+
const i = z * w + x;
|
|
398
|
+
const a = aggIn(i);
|
|
399
|
+
if (a >= 0) diag[a] = diag[a] + kappa;
|
|
400
|
+
if (x + 1 < w && (links[i] & LINK_X) === 0) pair(i, i + 1);
|
|
401
|
+
if (z + 1 < h && (links[i] & LINK_Z) === 0) pair(i, i + w);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const rowStart = scratch.rowStart;
|
|
405
|
+
rowStart.fill(0, 0, count + 1);
|
|
406
|
+
for (const key of pairs.keys()) {
|
|
407
|
+
const a = Math.floor(key / count);
|
|
408
|
+
const b = key - a * count;
|
|
409
|
+
rowStart[a + 1]++;
|
|
410
|
+
rowStart[b + 1]++;
|
|
411
|
+
}
|
|
412
|
+
for (let a = 0; a < count; a++) rowStart[a + 1] = rowStart[a + 1] + rowStart[a];
|
|
413
|
+
const nnz = rowStart[count];
|
|
414
|
+
if (scratch.colIndex.length < nnz) {
|
|
415
|
+
scratch.colIndex = new Int32Array(nnz * 2);
|
|
416
|
+
scratch.weight = new Float64Array(nnz * 2);
|
|
417
|
+
}
|
|
418
|
+
const col = scratch.colIndex;
|
|
419
|
+
const weight = scratch.weight;
|
|
420
|
+
const fill = new Int32Array(count);
|
|
421
|
+
for (const [key, g] of pairs) {
|
|
422
|
+
const a = Math.floor(key / count);
|
|
423
|
+
const b = key - a * count;
|
|
424
|
+
let o = rowStart[a] + fill[a]++;
|
|
425
|
+
col[o] = b;
|
|
426
|
+
weight[o] = g;
|
|
427
|
+
o = rowStart[b] + fill[b]++;
|
|
428
|
+
col[o] = a;
|
|
429
|
+
weight[o] = g;
|
|
430
|
+
}
|
|
431
|
+
value.fill(0, 0, count);
|
|
432
|
+
const coarseKappa = kappa * cells * cells;
|
|
433
|
+
const omega = relaxation(coarseKappa);
|
|
434
|
+
let sweeps = 0;
|
|
435
|
+
for (; sweeps < 500; sweeps++) {
|
|
436
|
+
let moved = 0;
|
|
437
|
+
for (let a = 0; a < count; a++) {
|
|
438
|
+
let sum = rhs[a];
|
|
439
|
+
for (let o = rowStart[a]; o < rowStart[a + 1]; o++) sum += weight[o] * value[col[o]];
|
|
440
|
+
const old = value[a];
|
|
441
|
+
const next = Math.min(1, Math.max(0, old + omega * (sum / diag[a] - old)));
|
|
442
|
+
value[a] = next;
|
|
443
|
+
const d = Math.abs(next - old);
|
|
444
|
+
if (d > moved) moved = d;
|
|
445
|
+
}
|
|
446
|
+
if (moved < 1e-5) break;
|
|
447
|
+
}
|
|
448
|
+
for (let z = rect.z0; z < rect.z1; z++)
|
|
449
|
+
for (let x = rect.x0; x < rect.x1; x++) {
|
|
450
|
+
const i = z * w + x;
|
|
451
|
+
if (agg[i] >= 0) v[i] = value[agg[i]];
|
|
452
|
+
}
|
|
453
|
+
return sweeps + 1;
|
|
454
|
+
}
|
|
455
|
+
function packRect(grid, layer, rect, scratch) {
|
|
456
|
+
const w = grid.width;
|
|
457
|
+
const h = grid.height;
|
|
458
|
+
scratch.ensure(w * h);
|
|
459
|
+
const v = layer.value;
|
|
460
|
+
const links = layer.links;
|
|
461
|
+
const cover = layer.cover;
|
|
462
|
+
const packed = layer.packed;
|
|
463
|
+
const visMax = VISIBILITY_MAX;
|
|
464
|
+
const coverScale = COVER_SCALE;
|
|
465
|
+
const coverZero = COVER_ZERO;
|
|
466
|
+
const linkX = LINK_X;
|
|
467
|
+
const linkZ = LINK_Z;
|
|
468
|
+
for (let z = rect.z0; z < rect.z1; z++) {
|
|
469
|
+
for (let x = rect.x0; x < rect.x1; x++) {
|
|
470
|
+
const i = z * w + x;
|
|
471
|
+
const c = cover[i];
|
|
472
|
+
if (c === Number.POSITIVE_INFINITY) {
|
|
473
|
+
packed[i] = visMax << 2 | links[i] & 3;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
let out;
|
|
477
|
+
{
|
|
478
|
+
let sum = 4 * v[i];
|
|
479
|
+
let n = 4;
|
|
480
|
+
if (x + 1 < w && (links[i] & linkX) === 0) {
|
|
481
|
+
sum += v[i + 1];
|
|
482
|
+
n++;
|
|
483
|
+
}
|
|
484
|
+
if (x > 0 && (links[i - 1] & linkX) === 0) {
|
|
485
|
+
sum += v[i - 1];
|
|
486
|
+
n++;
|
|
487
|
+
}
|
|
488
|
+
if (z + 1 < h && (links[i] & linkZ) === 0) {
|
|
489
|
+
sum += v[i + w];
|
|
490
|
+
n++;
|
|
491
|
+
}
|
|
492
|
+
if (z > 0 && (links[i - w] & linkZ) === 0) {
|
|
493
|
+
sum += v[i - w];
|
|
494
|
+
n++;
|
|
495
|
+
}
|
|
496
|
+
out = sum / n;
|
|
497
|
+
}
|
|
498
|
+
const vis = Math.min(1, Math.max(0, out)) * visMax + 0.5 | 0;
|
|
499
|
+
let code = Math.round(c * coverScale) + coverZero;
|
|
500
|
+
code = code < 1 ? 1 : code > 65535 ? 65535 : code;
|
|
501
|
+
packed[i] = (code << 16 | vis << 2 | links[i] & 3) >>> 0;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function packTexel(visibility, links, cover) {
|
|
506
|
+
const vis = Math.round(Math.min(1, Math.max(0, visibility)) * VISIBILITY_MAX);
|
|
507
|
+
const code = cover === Number.POSITIVE_INFINITY ? 0 : Math.min(65535, Math.max(1, Math.round(cover * COVER_SCALE) + COVER_ZERO));
|
|
508
|
+
return (code << 16 | vis << 2 | links & 3) >>> 0;
|
|
509
|
+
}
|
|
510
|
+
function unpackVisibility(texel) {
|
|
511
|
+
return ((texel & 65535) >>> 2) / VISIBILITY_MAX;
|
|
512
|
+
}
|
|
513
|
+
const directions = /* @__PURE__ */ new Map();
|
|
514
|
+
function binOf(x, z) {
|
|
515
|
+
const t = z / (Math.abs(x) + Math.abs(z));
|
|
516
|
+
const p = x < 0 ? 2 - t : t >= 0 ? t : 4 + t;
|
|
517
|
+
const u = p * 0.25 + 0.5;
|
|
518
|
+
return u - Math.floor(u);
|
|
519
|
+
}
|
|
520
|
+
function directionAt(u, out, k, sin) {
|
|
521
|
+
let p = (u - 0.5) * 4;
|
|
522
|
+
if (p < 0) p += 4;
|
|
523
|
+
let x;
|
|
524
|
+
let z;
|
|
525
|
+
if (p < 1) {
|
|
526
|
+
z = p;
|
|
527
|
+
x = 1 - p;
|
|
528
|
+
} else if (p < 3) {
|
|
529
|
+
z = 2 - p;
|
|
530
|
+
x = -(1 - Math.abs(z));
|
|
531
|
+
} else {
|
|
532
|
+
z = p - 4;
|
|
533
|
+
x = 1 - Math.abs(z);
|
|
534
|
+
}
|
|
535
|
+
const len = Math.sqrt(x * x + z * z);
|
|
536
|
+
out[k] = x / len;
|
|
537
|
+
sin[k] = z / len;
|
|
538
|
+
}
|
|
539
|
+
function binDirections(bins) {
|
|
540
|
+
let d = directions.get(bins);
|
|
541
|
+
if (!d) {
|
|
542
|
+
d = { cos: new Float64Array(bins), sin: new Float64Array(bins) };
|
|
543
|
+
for (let k = 0; k < bins; k++) directionAt((k + 0.5) / bins, d.cos, k, d.sin);
|
|
544
|
+
directions.set(bins, d);
|
|
545
|
+
}
|
|
546
|
+
return d;
|
|
547
|
+
}
|
|
548
|
+
class RowScratch {
|
|
549
|
+
distance = new Float64Array(0);
|
|
550
|
+
top = new Float64Array(0);
|
|
551
|
+
ensure(bins) {
|
|
552
|
+
if (this.distance.length < bins) {
|
|
553
|
+
this.distance = new Float64Array(bins);
|
|
554
|
+
this.top = new Float64Array(bins);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function rowBias(thickness) {
|
|
559
|
+
return Math.min(thickness / 4, 0.05);
|
|
560
|
+
}
|
|
561
|
+
function buildRow(out, x, y, z, range, barriers, count, scratch) {
|
|
562
|
+
const bins = out.length;
|
|
563
|
+
scratch.ensure(bins);
|
|
564
|
+
const dist = scratch.distance;
|
|
565
|
+
const top = scratch.top;
|
|
566
|
+
dist.fill(Number.POSITIVE_INFINITY, 0, bins);
|
|
567
|
+
const { cos, sin } = binDirections(bins);
|
|
568
|
+
for (let n = 0; n < count; n++) {
|
|
569
|
+
const s = barriers[n];
|
|
570
|
+
const ax = s.ax - x;
|
|
571
|
+
const az = s.az - z;
|
|
572
|
+
const ex = s.bx - s.ax;
|
|
573
|
+
const ez = s.bz - s.az;
|
|
574
|
+
const l2 = ex * ex + ez * ez;
|
|
575
|
+
if (l2 < 1e-12) continue;
|
|
576
|
+
const u0 = Math.min(1, Math.max(0, -(ax * ex + az * ez) / l2));
|
|
577
|
+
const px = ax + u0 * ex;
|
|
578
|
+
const pz = az + u0 * ez;
|
|
579
|
+
const near = Math.sqrt(px * px + pz * pz);
|
|
580
|
+
if (near > range || near < 1e-6) continue;
|
|
581
|
+
const ua = binOf(ax, az);
|
|
582
|
+
const ub = binOf(ax + ex, az + ez);
|
|
583
|
+
let span = ub - ua;
|
|
584
|
+
if (span > 0.5) span -= 1;
|
|
585
|
+
else if (span < -0.5) span += 1;
|
|
586
|
+
const start = span >= 0 ? ua : ub;
|
|
587
|
+
const k0 = Math.ceil(start * bins - 0.5);
|
|
588
|
+
const k1 = Math.floor((start + Math.abs(span)) * bins - 0.5);
|
|
589
|
+
const bias = rowBias(s.thickness);
|
|
590
|
+
for (let kk = k0; kk <= k1; kk++) {
|
|
591
|
+
const k = (kk % bins + bins) % bins;
|
|
592
|
+
const dx = cos[k];
|
|
593
|
+
const dz = sin[k];
|
|
594
|
+
const denom = dx * ez - dz * ex;
|
|
595
|
+
if (Math.abs(denom) < 1e-12) continue;
|
|
596
|
+
const t = (ax * ez - az * ex) / denom;
|
|
597
|
+
const u = (ax * dz - az * dx) / denom;
|
|
598
|
+
if (t <= 0 || t > range || u < -1e-9 || u > 1 + 1e-9) continue;
|
|
599
|
+
const r = t + bias;
|
|
600
|
+
if (r < dist[k]) {
|
|
601
|
+
dist[k] = r;
|
|
602
|
+
top[k] = s.top;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
for (let k = 0; k < bins; k++) {
|
|
607
|
+
const r = dist[k];
|
|
608
|
+
if (r === Number.POSITIVE_INFINITY) {
|
|
609
|
+
out[k] = ROW_CLEAR;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const d = Math.min(65534, Math.round(r * ROW_SCALE));
|
|
613
|
+
const h = Math.min(65535, Math.max(0, Math.round((top[k] - y) * ROW_SCALE) + ROW_ZERO));
|
|
614
|
+
out[k] = (h << 16 | d) >>> 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function unpackBin(bin) {
|
|
618
|
+
const d = bin & 65535;
|
|
619
|
+
return {
|
|
620
|
+
distance: d === 65535 ? Number.POSITIVE_INFINITY : d / ROW_SCALE,
|
|
621
|
+
top: ((bin >>> 16) - ROW_ZERO) / ROW_SCALE
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export {
|
|
625
|
+
FieldLayer,
|
|
626
|
+
LINK_X,
|
|
627
|
+
LINK_Z,
|
|
628
|
+
RowScratch,
|
|
629
|
+
SolveScratch,
|
|
630
|
+
binOf,
|
|
631
|
+
buildRow,
|
|
632
|
+
clearRect,
|
|
633
|
+
clipRect,
|
|
634
|
+
packRect,
|
|
635
|
+
packTexel,
|
|
636
|
+
rasterBarrier,
|
|
637
|
+
rasterCover,
|
|
638
|
+
relaxation,
|
|
639
|
+
rowBias,
|
|
640
|
+
screening,
|
|
641
|
+
solveRect,
|
|
642
|
+
texelsOf,
|
|
643
|
+
unpackBin,
|
|
644
|
+
unpackVisibility
|
|
645
|
+
};
|
package/dist/planar.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type WallCurve } from './curve.js';
|
|
2
|
+
export type Channel = 'none' | 'normal';
|
|
3
|
+
/** A light channel (0069): `sight`, or absent, follows the sight channel. */
|
|
4
|
+
export type LightChannel = Channel | 'sight';
|
|
5
|
+
export interface PlanarWall {
|
|
6
|
+
id: string;
|
|
7
|
+
a: readonly [number, number];
|
|
8
|
+
b: readonly [number, number];
|
|
9
|
+
/** Curved walls (0066); straight when absent. */
|
|
10
|
+
shape?: WallCurve;
|
|
11
|
+
bow?: number;
|
|
12
|
+
c0?: readonly [number, number];
|
|
13
|
+
c1?: readonly [number, number];
|
|
14
|
+
/** The level it stands on (0067); none or null is the ground level. */
|
|
15
|
+
level?: string | null;
|
|
16
|
+
/** Whether its spans block light (0069); absent or `sight` follows sight (a wall's always blocks). */
|
|
17
|
+
light?: LightChannel;
|
|
18
|
+
}
|
|
19
|
+
export interface PlanarOptions {
|
|
20
|
+
/** Chord tolerance for curved walls: StructureSettings.curveTolerance. Default 0.01. */
|
|
21
|
+
tolerance?: number;
|
|
22
|
+
/** Only this level's walls (null: the ground level), each segment tagged with it (0067). */
|
|
23
|
+
level?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface PlanarOpening {
|
|
26
|
+
id: string;
|
|
27
|
+
/** The host wall's id. */
|
|
28
|
+
wall: string;
|
|
29
|
+
kind: 'door' | 'window';
|
|
30
|
+
offset: number;
|
|
31
|
+
width: number;
|
|
32
|
+
/** Doors only; an open door blocks nothing. */
|
|
33
|
+
state?: 'closed' | 'open' | 'locked';
|
|
34
|
+
sight: Channel;
|
|
35
|
+
movement: Channel;
|
|
36
|
+
/** Whether it blocks light when closed (0069); absent or `sight` follows sight. */
|
|
37
|
+
light?: LightChannel;
|
|
38
|
+
}
|
|
39
|
+
export interface Segment {
|
|
40
|
+
/** `struct:<wall>:<start>` for wall spans, `struct-opening:<opening>` for openings. */
|
|
41
|
+
id: string;
|
|
42
|
+
sourceWallId: string;
|
|
43
|
+
openingId?: string;
|
|
44
|
+
a: [number, number];
|
|
45
|
+
b: [number, number];
|
|
46
|
+
sight: Channel;
|
|
47
|
+
movement: Channel;
|
|
48
|
+
/** Present when its wall or opening sets a light channel (0069); absent follows sight. */
|
|
49
|
+
light?: Channel;
|
|
50
|
+
/** Its wall's level: present when the wall names one or `level` was asked for (0067). */
|
|
51
|
+
level?: string | null;
|
|
52
|
+
}
|
|
53
|
+
/** Whether a segment blocks light (0069): its light channel, or its sight channel when unset. */
|
|
54
|
+
export declare function lightChannel(segment: Segment): Channel;
|
|
55
|
+
/**
|
|
56
|
+
* Splits walls into barrier segments: solid spans block both channels; an opening's span carries
|
|
57
|
+
* its own channels, and an open door's carries neither. Openings sort by offset, then id; offsets
|
|
58
|
+
* are arc lengths on curved walls. A curved span becomes one segment per sample interval: the
|
|
59
|
+
* first keeps the span's id, the rest append `~1`, `~2`, … With `level`, only that level's walls.
|
|
60
|
+
*/
|
|
61
|
+
export declare function planarBarriers(walls: readonly PlanarWall[], openings: readonly PlanarOpening[], options?: PlanarOptions): Segment[];
|