@aethervtt/shard-structure 0.0.0-stage → 0.2.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/contact.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { pointAt } from "./curve.js";
|
|
2
|
+
import { chunkKey } from "./geometry.js";
|
|
3
|
+
const CONTACT_STRIDE = 20;
|
|
4
|
+
const CONTACT_STEP = 1.8;
|
|
5
|
+
const CONTACT_LIFT = 2e-3;
|
|
6
|
+
const SIDE_PROBE = 0.05;
|
|
7
|
+
const CORNER_INSET = 0.2;
|
|
8
|
+
class ContactQuads {
|
|
9
|
+
data = new Float64Array(CONTACT_STRIDE * 8);
|
|
10
|
+
count = 0;
|
|
11
|
+
reset() {
|
|
12
|
+
this.count = 0;
|
|
13
|
+
}
|
|
14
|
+
/** Room for one more quad; returns its offset. */
|
|
15
|
+
next() {
|
|
16
|
+
const o = this.count * CONTACT_STRIDE;
|
|
17
|
+
if (o + CONTACT_STRIDE > this.data.length) {
|
|
18
|
+
const grown = new Float64Array(this.data.length * 2);
|
|
19
|
+
grown.set(this.data);
|
|
20
|
+
this.data = grown;
|
|
21
|
+
}
|
|
22
|
+
this.count++;
|
|
23
|
+
return o;
|
|
24
|
+
}
|
|
25
|
+
/** Whether `other` holds the same quads (an edit that changed nothing here). */
|
|
26
|
+
equals(other) {
|
|
27
|
+
if (other.count !== this.count) return false;
|
|
28
|
+
for (let i = 0; i < this.count * CONTACT_STRIDE; i++)
|
|
29
|
+
if (this.data[i] !== other.data[i]) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
copy(other) {
|
|
33
|
+
if (this.data.length < other.data.length) this.data = new Float64Array(other.data.length);
|
|
34
|
+
this.data.set(other.data.subarray(0, other.count * CONTACT_STRIDE));
|
|
35
|
+
this.count = other.count;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const at = new Float64Array(4);
|
|
39
|
+
const jointOut = new Float64Array(3);
|
|
40
|
+
function sideFloor(w, s, sgn, env) {
|
|
41
|
+
pointAt(w.line, s, at);
|
|
42
|
+
const off = (w.thickness / 2 + SIDE_PROBE) * sgn;
|
|
43
|
+
return env.floorTop(at[0] + at[2] * off, at[1] + at[3] * off, w.elevation);
|
|
44
|
+
}
|
|
45
|
+
function quad(out, nx, ny, nz, c) {
|
|
46
|
+
const o = out.next();
|
|
47
|
+
const d = out.data;
|
|
48
|
+
d[o] = nx;
|
|
49
|
+
d[o + 1] = ny;
|
|
50
|
+
d[o + 2] = nz;
|
|
51
|
+
d[o + 3] = 0;
|
|
52
|
+
for (let i = 0; i < 16; i++) d[o + 4 + i] = c[i];
|
|
53
|
+
}
|
|
54
|
+
const corners = new Array(16).fill(0);
|
|
55
|
+
function floorStrip(w, s0, s1, sgn, y, reach, out) {
|
|
56
|
+
const line = w.line;
|
|
57
|
+
const half = w.thickness / 2;
|
|
58
|
+
let a = s0;
|
|
59
|
+
let k = 1;
|
|
60
|
+
while (a < s1 - 1e-9) {
|
|
61
|
+
while (k < line.count - 1 && line.s[k] <= a + 1e-9) k++;
|
|
62
|
+
const b = k < line.count - 1 ? Math.min(s1, line.s[k]) : s1;
|
|
63
|
+
pointAt(line, a, at);
|
|
64
|
+
const ax = at[0];
|
|
65
|
+
const az = at[1];
|
|
66
|
+
const anx = at[2] * sgn;
|
|
67
|
+
const anz = at[3] * sgn;
|
|
68
|
+
pointAt(line, b, at);
|
|
69
|
+
const bx = at[0];
|
|
70
|
+
const bz = at[1];
|
|
71
|
+
const bnx = at[2] * sgn;
|
|
72
|
+
const bnz = at[3] * sgn;
|
|
73
|
+
const c = corners;
|
|
74
|
+
c[0] = ax + anx * half;
|
|
75
|
+
c[1] = y;
|
|
76
|
+
c[2] = az + anz * half;
|
|
77
|
+
c[3] = 0;
|
|
78
|
+
c[4] = bx + bnx * half;
|
|
79
|
+
c[5] = y;
|
|
80
|
+
c[6] = bz + bnz * half;
|
|
81
|
+
c[7] = 0;
|
|
82
|
+
c[8] = bx + bnx * (half + reach);
|
|
83
|
+
c[9] = y;
|
|
84
|
+
c[10] = bz + bnz * (half + reach);
|
|
85
|
+
c[11] = 1;
|
|
86
|
+
c[12] = ax + anx * (half + reach);
|
|
87
|
+
c[13] = y;
|
|
88
|
+
c[14] = az + anz * (half + reach);
|
|
89
|
+
c[15] = 1;
|
|
90
|
+
quad(out, 0, 1, 0, c);
|
|
91
|
+
a = b;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function cornerStrip(w, from, to, covered, sgn, y0, y1, reach, out) {
|
|
95
|
+
const line = w.line;
|
|
96
|
+
const off = w.thickness / 2 + CONTACT_LIFT;
|
|
97
|
+
const lo = Math.min(from, to);
|
|
98
|
+
const hi = Math.max(from, to);
|
|
99
|
+
let a = lo;
|
|
100
|
+
let k = 1;
|
|
101
|
+
while (a < hi - 1e-9) {
|
|
102
|
+
while (k < line.count - 1 && line.s[k] <= a + 1e-9) k++;
|
|
103
|
+
const b = k < line.count - 1 ? Math.min(hi, line.s[k]) : hi;
|
|
104
|
+
pointAt(line, a, at);
|
|
105
|
+
const ax = at[0] + at[2] * off * sgn;
|
|
106
|
+
const az = at[1] + at[3] * off * sgn;
|
|
107
|
+
const nx = at[2] * sgn;
|
|
108
|
+
const nz = at[3] * sgn;
|
|
109
|
+
pointAt(line, b, at);
|
|
110
|
+
const bx = at[0] + at[2] * off * sgn;
|
|
111
|
+
const bz = at[1] + at[3] * off * sgn;
|
|
112
|
+
const fa = (Math.abs(a - from) - covered) / reach;
|
|
113
|
+
const fb = (Math.abs(b - from) - covered) / reach;
|
|
114
|
+
const c = corners;
|
|
115
|
+
c[0] = ax;
|
|
116
|
+
c[1] = y0;
|
|
117
|
+
c[2] = az;
|
|
118
|
+
c[3] = fa;
|
|
119
|
+
c[4] = bx;
|
|
120
|
+
c[5] = y0;
|
|
121
|
+
c[6] = bz;
|
|
122
|
+
c[7] = fb;
|
|
123
|
+
c[8] = bx;
|
|
124
|
+
c[9] = y1;
|
|
125
|
+
c[10] = bz;
|
|
126
|
+
c[11] = fb;
|
|
127
|
+
c[12] = ax;
|
|
128
|
+
c[13] = y1;
|
|
129
|
+
c[14] = az;
|
|
130
|
+
c[15] = fa;
|
|
131
|
+
quad(out, (nx + at[2] * sgn) / 2, 0, (nz + at[3] * sgn) / 2, c);
|
|
132
|
+
a = b;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function contactQuads(w, openings, reach, env, out) {
|
|
136
|
+
out.reset();
|
|
137
|
+
const length = w.line.length;
|
|
138
|
+
if (length < 1e-6 || w.height <= 0) return;
|
|
139
|
+
if (reach.floorReach > 1e-6) {
|
|
140
|
+
let cursor = 0;
|
|
141
|
+
for (let i = 0; i <= openings.length; i++) {
|
|
142
|
+
const o = openings[i];
|
|
143
|
+
if (o && o.kind !== "door") continue;
|
|
144
|
+
const end = o ? Math.max(cursor, Math.min(length, o.offset)) : length;
|
|
145
|
+
const span2 = end - cursor;
|
|
146
|
+
if (span2 > 1e-6) {
|
|
147
|
+
const n = Math.max(1, Math.ceil(span2 / CONTACT_STEP));
|
|
148
|
+
for (let p = 0; p < n; p++) {
|
|
149
|
+
const s0 = cursor + span2 * p / n;
|
|
150
|
+
const s1 = cursor + span2 * (p + 1) / n;
|
|
151
|
+
for (let sgn = 1; sgn >= -1; sgn -= 2) {
|
|
152
|
+
const top = sideFloor(w, (s0 + s1) / 2, sgn, env);
|
|
153
|
+
if (Number.isNaN(top)) continue;
|
|
154
|
+
floorStrip(w, s0, s1, sgn, top + CONTACT_LIFT, reach.floorReach, out);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (o) cursor = Math.max(cursor, Math.min(length, o.offset + o.width));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (reach.cornerReach > 1e-6) {
|
|
162
|
+
for (let end = 0; end < 2; end++) {
|
|
163
|
+
if (!env.joint(end, jointOut)) continue;
|
|
164
|
+
const y0 = jointOut[0] + CORNER_INSET;
|
|
165
|
+
const y1 = jointOut[1] - CORNER_INSET;
|
|
166
|
+
const covered = Math.max(0, jointOut[2]);
|
|
167
|
+
if (y1 <= y0) continue;
|
|
168
|
+
const from = end === 0 ? 0 : length;
|
|
169
|
+
const run = Math.min(length, covered + reach.cornerReach);
|
|
170
|
+
const to = end === 0 ? run : length - run;
|
|
171
|
+
const probe = Math.min(length / 2, covered + reach.cornerReach / 2);
|
|
172
|
+
for (let sgn = 1; sgn >= -1; sgn -= 2) {
|
|
173
|
+
const top = sideFloor(w, end === 0 ? probe : length - probe, sgn, env);
|
|
174
|
+
if (Number.isNaN(top)) continue;
|
|
175
|
+
cornerStrip(w, from, to, covered, sgn, y0, y1, reach.cornerReach, out);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const polyA = new Float64Array(36);
|
|
181
|
+
const polyB = new Float64Array(36);
|
|
182
|
+
function clipEdge(src, n, dst, axis, edge, sign) {
|
|
183
|
+
if (n === 0) return 0;
|
|
184
|
+
let count = 0;
|
|
185
|
+
let p = (n - 1) * 3;
|
|
186
|
+
let pd = sign * (src[p + (axis === 0 ? 0 : 1)] - edge);
|
|
187
|
+
for (let i = 0; i < n; i++) {
|
|
188
|
+
const c = i * 3;
|
|
189
|
+
const d = sign * (src[c + (axis === 0 ? 0 : 1)] - edge);
|
|
190
|
+
if (d >= 0 !== pd >= 0) {
|
|
191
|
+
const t = pd / (pd - d);
|
|
192
|
+
dst[count * 3] = src[p] + (src[c] - src[p]) * t;
|
|
193
|
+
dst[count * 3 + 1] = src[p + 1] + (src[c + 1] - src[p + 1]) * t;
|
|
194
|
+
dst[count * 3 + 2] = src[p + 2] + (src[c + 2] - src[p + 2]) * t;
|
|
195
|
+
count++;
|
|
196
|
+
}
|
|
197
|
+
if (d >= 0) {
|
|
198
|
+
dst[count * 3] = src[c];
|
|
199
|
+
dst[count * 3 + 1] = src[c + 1];
|
|
200
|
+
dst[count * 3 + 2] = src[c + 2];
|
|
201
|
+
count++;
|
|
202
|
+
}
|
|
203
|
+
p = c;
|
|
204
|
+
pd = d;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
function clipFloorQuad(d, o, minX, minZ, maxX, maxZ) {
|
|
209
|
+
for (let k = 0; k < 4; k++) {
|
|
210
|
+
polyA[k * 3] = d[o + 4 + k * 4];
|
|
211
|
+
polyA[k * 3 + 1] = d[o + 6 + k * 4];
|
|
212
|
+
polyA[k * 3 + 2] = d[o + 7 + k * 4];
|
|
213
|
+
}
|
|
214
|
+
let n = clipEdge(polyA, 4, polyB, 0, minX, 1);
|
|
215
|
+
n = clipEdge(polyB, n, polyA, 0, maxX, -1);
|
|
216
|
+
n = clipEdge(polyA, n, polyB, 1, minZ, 1);
|
|
217
|
+
n = clipEdge(polyB, n, polyA, 1, maxZ, -1);
|
|
218
|
+
if (n < 3) return 0;
|
|
219
|
+
let area = 0;
|
|
220
|
+
for (let i = 0; i < n; i++) {
|
|
221
|
+
const j = (i + 1) % n;
|
|
222
|
+
area += polyA[i * 3] * polyA[j * 3 + 1] - polyA[j * 3] * polyA[i * 3 + 1];
|
|
223
|
+
}
|
|
224
|
+
return Math.abs(area) > 1e-9 ? n : 0;
|
|
225
|
+
}
|
|
226
|
+
const span = new Float64Array(2);
|
|
227
|
+
function clipCornerQuad(d, o, minX, minZ, maxX, maxZ) {
|
|
228
|
+
const ax = d[o + 4];
|
|
229
|
+
const az = d[o + 6];
|
|
230
|
+
const dx = d[o + 8] - ax;
|
|
231
|
+
const dz = d[o + 10] - az;
|
|
232
|
+
let t0 = 0;
|
|
233
|
+
let t1 = 1;
|
|
234
|
+
for (let k = 0; k < 4; k++) {
|
|
235
|
+
const p = k === 0 ? -dx : k === 1 ? dx : k === 2 ? -dz : dz;
|
|
236
|
+
const q = k === 0 ? ax - minX : k === 1 ? maxX - ax : k === 2 ? az - minZ : maxZ - az;
|
|
237
|
+
if (Math.abs(p) < 1e-12) {
|
|
238
|
+
if (q < 0 || q === 0 && (k === 1 || k === 3)) return false;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const t = q / p;
|
|
242
|
+
if (p < 0) {
|
|
243
|
+
if (t > t1) return false;
|
|
244
|
+
if (t > t0) t0 = t;
|
|
245
|
+
} else {
|
|
246
|
+
if (t < t0) return false;
|
|
247
|
+
if (t < t1) t1 = t;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if ((t1 - t0) * Math.sqrt(dx * dx + dz * dz) <= 1e-9) return false;
|
|
251
|
+
span[0] = t0;
|
|
252
|
+
span[1] = t1;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
function contactChunks(quads, size, out) {
|
|
256
|
+
const d = quads.data;
|
|
257
|
+
for (let q = 0; q < quads.count; q++) {
|
|
258
|
+
const o = q * CONTACT_STRIDE;
|
|
259
|
+
let x0 = Infinity;
|
|
260
|
+
let z0 = Infinity;
|
|
261
|
+
let x1 = -Infinity;
|
|
262
|
+
let z1 = -Infinity;
|
|
263
|
+
for (let k = 0; k < 4; k++) {
|
|
264
|
+
const x = d[o + 4 + k * 4];
|
|
265
|
+
const z = d[o + 6 + k * 4];
|
|
266
|
+
if (x < x0) x0 = x;
|
|
267
|
+
if (x > x1) x1 = x;
|
|
268
|
+
if (z < z0) z0 = z;
|
|
269
|
+
if (z > z1) z1 = z;
|
|
270
|
+
}
|
|
271
|
+
const vertical = d[o + 1] === 0;
|
|
272
|
+
for (let cx = Math.floor(x0 / size); cx <= Math.floor(x1 / size); cx++)
|
|
273
|
+
for (let cz = Math.floor(z0 / size); cz <= Math.floor(z1 / size); cz++) {
|
|
274
|
+
const key = chunkKey(cx, cz);
|
|
275
|
+
if (out.has(key)) continue;
|
|
276
|
+
const minX = cx * size;
|
|
277
|
+
const minZ = cz * size;
|
|
278
|
+
const hit = vertical ? clipCornerQuad(d, o, minX, minZ, minX + size, minZ + size) : clipFloorQuad(d, o, minX, minZ, minX + size, minZ + size) > 0;
|
|
279
|
+
if (hit) out.add(key);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function emitContact(quads, minX, minZ, maxX, maxZ, m) {
|
|
284
|
+
const d = quads.data;
|
|
285
|
+
let any = false;
|
|
286
|
+
for (let q = 0; q < quads.count; q++) {
|
|
287
|
+
const o = q * CONTACT_STRIDE;
|
|
288
|
+
const nx = d[o];
|
|
289
|
+
const ny = d[o + 1];
|
|
290
|
+
const nz = d[o + 2];
|
|
291
|
+
if (ny !== 0) {
|
|
292
|
+
const n = clipFloorQuad(d, o, minX, minZ, maxX, maxZ);
|
|
293
|
+
if (n === 0) continue;
|
|
294
|
+
any = true;
|
|
295
|
+
const y = d[o + 5];
|
|
296
|
+
const first2 = m.vertexCount;
|
|
297
|
+
for (let k = 0; k < n; k++)
|
|
298
|
+
m.vertex(polyA[k * 3], y, polyA[k * 3 + 1], 0, 1, 0, polyA[k * 3 + 2], 0, 1, 0, 0, 1);
|
|
299
|
+
m.fan(first2, n, 0, 1, 0);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!clipCornerQuad(d, o, minX, minZ, maxX, maxZ)) continue;
|
|
303
|
+
any = true;
|
|
304
|
+
const first = m.vertexCount;
|
|
305
|
+
for (let e = 0; e < 4; e++) {
|
|
306
|
+
const t = e === 0 || e === 3 ? span[0] : span[1];
|
|
307
|
+
const y = e < 2 ? d[o + 5] : d[o + 13];
|
|
308
|
+
const x = d[o + 4] + (d[o + 8] - d[o + 4]) * t;
|
|
309
|
+
const z = d[o + 6] + (d[o + 10] - d[o + 6]) * t;
|
|
310
|
+
const fade = d[o + 7] + (d[o + 11] - d[o + 7]) * t;
|
|
311
|
+
m.vertex(x, y, z, nx, 0, nz, fade, 0, -nz, 0, nx, 1);
|
|
312
|
+
}
|
|
313
|
+
m.fan(first, 4, nx, 0, nz);
|
|
314
|
+
}
|
|
315
|
+
return any;
|
|
316
|
+
}
|
|
317
|
+
const CONTACT_CORE = 0.1;
|
|
318
|
+
const CONTACT_CORE_BOOST = 0.55;
|
|
319
|
+
function smoothstep(a, b, x) {
|
|
320
|
+
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
321
|
+
return t * t * (3 - 2 * t);
|
|
322
|
+
}
|
|
323
|
+
function contactAlpha(fade, n, look) {
|
|
324
|
+
const wobble = Math.min(1, Math.max(0, look.wobble));
|
|
325
|
+
const f = Math.min(1, Math.max(0, fade * (1 + wobble * (2 * n - 1))));
|
|
326
|
+
const falloff = 1 - smoothstep(0, 1, f);
|
|
327
|
+
const core = 1 - smoothstep(0, CONTACT_CORE, f);
|
|
328
|
+
const edge = 1 - smoothstep(0.8, 1, fade);
|
|
329
|
+
const alpha = look.opacity * (falloff * (0.8 + 0.2 * falloff) + core * CONTACT_CORE_BOOST) * (0.94 + 0.12 * n);
|
|
330
|
+
return Math.min(look.maxAlpha, alpha) * edge;
|
|
331
|
+
}
|
|
332
|
+
export {
|
|
333
|
+
CONTACT_CORE,
|
|
334
|
+
CONTACT_CORE_BOOST,
|
|
335
|
+
CONTACT_LIFT,
|
|
336
|
+
CONTACT_STEP,
|
|
337
|
+
CONTACT_STRIDE,
|
|
338
|
+
ContactQuads,
|
|
339
|
+
contactAlpha,
|
|
340
|
+
contactChunks,
|
|
341
|
+
contactQuads,
|
|
342
|
+
emitContact
|
|
343
|
+
};
|
package/dist/curve.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type WallCurve = 'straight' | 'arc' | 'bezier';
|
|
2
|
+
/** What a centreline is made from, in world (x, z). */
|
|
3
|
+
export interface CurveInput {
|
|
4
|
+
a: ArrayLike<number>;
|
|
5
|
+
b: ArrayLike<number>;
|
|
6
|
+
shape?: WallCurve;
|
|
7
|
+
/** Arc: how far the midpoint bows off the line a → b; positive bows to the left of a → b. */
|
|
8
|
+
bow?: number;
|
|
9
|
+
/** Bézier: the cubic's control points. */
|
|
10
|
+
c0?: ArrayLike<number>;
|
|
11
|
+
c1?: ArrayLike<number>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A sampled centreline: points, the arc length at each, and the unit normal to the left of the
|
|
15
|
+
* direction of travel. `radius` is the smallest radius of curvature (Infinity when straight).
|
|
16
|
+
*/
|
|
17
|
+
export interface Centerline {
|
|
18
|
+
count: number;
|
|
19
|
+
x: Float64Array;
|
|
20
|
+
z: Float64Array;
|
|
21
|
+
s: Float64Array;
|
|
22
|
+
nx: Float64Array;
|
|
23
|
+
nz: Float64Array;
|
|
24
|
+
length: number;
|
|
25
|
+
radius: number;
|
|
26
|
+
}
|
|
27
|
+
/** Default chord tolerance, metres (`StructureSettings.curveTolerance`). */
|
|
28
|
+
export declare const CURVE_TOLERANCE = 0.01;
|
|
29
|
+
/** A circular arc's centre, radius and sweep (signed radians, from a), or undefined if straight. */
|
|
30
|
+
export declare function arcOf(input: CurveInput): {
|
|
31
|
+
cx: number;
|
|
32
|
+
cz: number;
|
|
33
|
+
r: number;
|
|
34
|
+
from: number;
|
|
35
|
+
sweep: number;
|
|
36
|
+
} | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Samples a wall's centreline so no chord strays more than `tolerance` from the curve. Straight
|
|
39
|
+
* walls give their two ends; arcs even angles; Béziers adaptive subdivision. Reuses `out`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sampleWall(input: CurveInput, tolerance?: number, out?: Centerline): Centerline;
|
|
42
|
+
/** Index of the sample interval containing arc length `s` (clamped). */
|
|
43
|
+
export declare function intervalAt(c: Centerline, s: number): number;
|
|
44
|
+
/** The centreline point and left normal at arc length `s`, interpolated between samples. */
|
|
45
|
+
export declare function pointAt(c: Centerline, s: number, out: Float64Array): Float64Array;
|
|
46
|
+
/** A quadratic Bézier (one control point q) as the same curve in cubic form, exactly. */
|
|
47
|
+
export declare function quadraticToCubic(a: ArrayLike<number>, q: ArrayLike<number>, b: ArrayLike<number>): {
|
|
48
|
+
c0: [number, number];
|
|
49
|
+
c1: [number, number];
|
|
50
|
+
};
|
package/dist/curve.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const CURVE_TOLERANCE = 0.01;
|
|
2
|
+
function alloc(n) {
|
|
3
|
+
return {
|
|
4
|
+
count: 0,
|
|
5
|
+
x: new Float64Array(n),
|
|
6
|
+
z: new Float64Array(n),
|
|
7
|
+
s: new Float64Array(n),
|
|
8
|
+
nx: new Float64Array(n),
|
|
9
|
+
nz: new Float64Array(n),
|
|
10
|
+
length: 0,
|
|
11
|
+
radius: Infinity
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function push(c, x, z, tx, tz) {
|
|
15
|
+
if (c.count === c.x.length) {
|
|
16
|
+
const grow = (a) => {
|
|
17
|
+
const next = new Float64Array(a.length * 2);
|
|
18
|
+
next.set(a);
|
|
19
|
+
return next;
|
|
20
|
+
};
|
|
21
|
+
c.x = grow(c.x);
|
|
22
|
+
c.z = grow(c.z);
|
|
23
|
+
c.s = grow(c.s);
|
|
24
|
+
c.nx = grow(c.nx);
|
|
25
|
+
c.nz = grow(c.nz);
|
|
26
|
+
}
|
|
27
|
+
const i = c.count++;
|
|
28
|
+
c.x[i] = x;
|
|
29
|
+
c.z[i] = z;
|
|
30
|
+
c.s[i] = i === 0 ? 0 : c.s[i - 1] + Math.hypot(x - c.x[i - 1], z - c.z[i - 1]);
|
|
31
|
+
const l = Math.sqrt(tx * tx + tz * tz) || 1;
|
|
32
|
+
c.nx[i] = -tz / l;
|
|
33
|
+
c.nz[i] = tx / l;
|
|
34
|
+
}
|
|
35
|
+
function arcOf(input) {
|
|
36
|
+
const bow = input.bow ?? 0;
|
|
37
|
+
const ax = input.a[0];
|
|
38
|
+
const az = input.a[1];
|
|
39
|
+
const bx = input.b[0];
|
|
40
|
+
const bz = input.b[1];
|
|
41
|
+
const dx = bx - ax;
|
|
42
|
+
const dz = bz - az;
|
|
43
|
+
const chord = Math.sqrt(dx * dx + dz * dz);
|
|
44
|
+
if (chord < 1e-9 || Math.abs(bow) < 1e-9) return void 0;
|
|
45
|
+
const nx = -dz / chord;
|
|
46
|
+
const nz = dx / chord;
|
|
47
|
+
const r = chord * chord / 4 / (2 * Math.abs(bow)) + Math.abs(bow) / 2;
|
|
48
|
+
const mx = (ax + bx) / 2 + nx * bow;
|
|
49
|
+
const mz = (az + bz) / 2 + nz * bow;
|
|
50
|
+
const sign = Math.sign(bow);
|
|
51
|
+
const cx = mx - nx * sign * r;
|
|
52
|
+
const cz = mz - nz * sign * r;
|
|
53
|
+
const from = Math.atan2(az - cz, ax - cx);
|
|
54
|
+
const to = Math.atan2(bz - cz, bx - cx);
|
|
55
|
+
const mid = Math.atan2(mz - cz, mx - cx);
|
|
56
|
+
let ccw = to - from;
|
|
57
|
+
while (ccw <= 0) ccw += Math.PI * 2;
|
|
58
|
+
let m = mid - from;
|
|
59
|
+
while (m < 0) m += Math.PI * 2;
|
|
60
|
+
const sweep = m <= ccw ? ccw : ccw - Math.PI * 2;
|
|
61
|
+
return { cx, cz, r, from, sweep };
|
|
62
|
+
}
|
|
63
|
+
function bezier(p, t, out) {
|
|
64
|
+
const u = 1 - t;
|
|
65
|
+
const b0 = u * u * u;
|
|
66
|
+
const b1 = 3 * u * u * t;
|
|
67
|
+
const b2 = 3 * u * t * t;
|
|
68
|
+
const b3 = t * t * t;
|
|
69
|
+
out[0] = b0 * p[0] + b1 * p[2] + b2 * p[4] + b3 * p[6];
|
|
70
|
+
out[1] = b0 * p[1] + b1 * p[3] + b2 * p[5] + b3 * p[7];
|
|
71
|
+
const d0 = 3 * u * u;
|
|
72
|
+
const d1 = 6 * u * t;
|
|
73
|
+
const d2 = 3 * t * t;
|
|
74
|
+
out[2] = d0 * (p[2] - p[0]) + d1 * (p[4] - p[2]) + d2 * (p[6] - p[4]);
|
|
75
|
+
out[3] = d0 * (p[3] - p[1]) + d1 * (p[5] - p[3]) + d2 * (p[7] - p[5]);
|
|
76
|
+
}
|
|
77
|
+
function flatness(p, t0, t1, scratch) {
|
|
78
|
+
const q = scratch;
|
|
79
|
+
const sub = (t, i) => {
|
|
80
|
+
bezier(p, t, q);
|
|
81
|
+
q[8 + i * 2] = q[0];
|
|
82
|
+
q[9 + i * 2] = q[1];
|
|
83
|
+
};
|
|
84
|
+
sub(t0, 0);
|
|
85
|
+
sub(t1, 3);
|
|
86
|
+
bezier(p, t0, q);
|
|
87
|
+
const d0x = q[2] * ((t1 - t0) / 3);
|
|
88
|
+
const d0z = q[3] * ((t1 - t0) / 3);
|
|
89
|
+
bezier(p, t1, q);
|
|
90
|
+
const d1x = q[2] * ((t1 - t0) / 3);
|
|
91
|
+
const d1z = q[3] * ((t1 - t0) / 3);
|
|
92
|
+
const ax = q[8];
|
|
93
|
+
const az = q[9];
|
|
94
|
+
const bx = q[14];
|
|
95
|
+
const bz = q[15];
|
|
96
|
+
const c0x = ax + d0x;
|
|
97
|
+
const c0z = az + d0z;
|
|
98
|
+
const c1x = bx - d1x;
|
|
99
|
+
const c1z = bz - d1z;
|
|
100
|
+
const dx = bx - ax;
|
|
101
|
+
const dz = bz - az;
|
|
102
|
+
const len = Math.sqrt(dx * dx + dz * dz);
|
|
103
|
+
const dist = (x, z) => len < 1e-12 ? Math.hypot(x - ax, z - az) : Math.abs((x - ax) * dz - (z - az) * dx) / len;
|
|
104
|
+
return Math.max(dist(c0x, c0z), dist(c1x, c1z));
|
|
105
|
+
}
|
|
106
|
+
function bezierRadius(p, t) {
|
|
107
|
+
const u = 1 - t;
|
|
108
|
+
const dx = 3 * u * u * (p[2] - p[0]) + 6 * u * t * (p[4] - p[2]) + 3 * t * t * (p[6] - p[4]);
|
|
109
|
+
const dz = 3 * u * u * (p[3] - p[1]) + 6 * u * t * (p[5] - p[3]) + 3 * t * t * (p[7] - p[5]);
|
|
110
|
+
const ddx = 6 * u * (p[4] - 2 * p[2] + p[0]) + 6 * t * (p[6] - 2 * p[4] + p[2]);
|
|
111
|
+
const ddz = 6 * u * (p[5] - 2 * p[3] + p[1]) + 6 * t * (p[7] - 2 * p[5] + p[3]);
|
|
112
|
+
const cross = Math.abs(dx * ddz - dz * ddx);
|
|
113
|
+
const speed = Math.sqrt(dx * dx + dz * dz);
|
|
114
|
+
return cross < 1e-12 ? Infinity : speed * speed * speed / cross;
|
|
115
|
+
}
|
|
116
|
+
function sampleWall(input, tolerance = CURVE_TOLERANCE, out) {
|
|
117
|
+
const c = out ?? alloc(8);
|
|
118
|
+
c.count = 0;
|
|
119
|
+
c.radius = Infinity;
|
|
120
|
+
const ax = input.a[0];
|
|
121
|
+
const az = input.a[1];
|
|
122
|
+
const bx = input.b[0];
|
|
123
|
+
const bz = input.b[1];
|
|
124
|
+
const shape = input.shape ?? "straight";
|
|
125
|
+
const arc = shape === "arc" ? arcOf(input) : void 0;
|
|
126
|
+
if (arc) {
|
|
127
|
+
const { cx, cz, r, from, sweep } = arc;
|
|
128
|
+
const step = 2 * Math.acos(Math.max(-1, 1 - tolerance / r));
|
|
129
|
+
const n = Math.max(1, Math.ceil(Math.abs(sweep) / Math.max(step, 1e-6)));
|
|
130
|
+
for (let k = 0; k <= n; k++) {
|
|
131
|
+
const a = from + sweep * k / n;
|
|
132
|
+
const cos = Math.cos(a);
|
|
133
|
+
const sin = Math.sin(a);
|
|
134
|
+
const x = k === 0 ? ax : k === n ? bx : cx + r * cos;
|
|
135
|
+
const z = k === 0 ? az : k === n ? bz : cz + r * sin;
|
|
136
|
+
push(c, x, z, -sin * Math.sign(sweep), cos * Math.sign(sweep));
|
|
137
|
+
}
|
|
138
|
+
for (let k = 0; k <= n; k++) c.s[k] = r * Math.abs(sweep) * k / n;
|
|
139
|
+
c.radius = r;
|
|
140
|
+
} else if (shape === "bezier" && input.c0 && input.c1) {
|
|
141
|
+
const p = new Float64Array([
|
|
142
|
+
ax,
|
|
143
|
+
az,
|
|
144
|
+
input.c0[0],
|
|
145
|
+
input.c0[1],
|
|
146
|
+
input.c1[0],
|
|
147
|
+
input.c1[1],
|
|
148
|
+
bx,
|
|
149
|
+
bz
|
|
150
|
+
]);
|
|
151
|
+
const q = new Float64Array(16);
|
|
152
|
+
const ts = [0];
|
|
153
|
+
const stack = [[0, 1]];
|
|
154
|
+
const done = [];
|
|
155
|
+
while (stack.length > 0) {
|
|
156
|
+
const [t0, t1] = stack.pop();
|
|
157
|
+
if (t1 - t0 > 1 / 4096 && (t1 - t0 > 0.25 || flatness(p, t0, t1, q) > tolerance)) {
|
|
158
|
+
const m = (t0 + t1) / 2;
|
|
159
|
+
stack.push([m, t1], [t0, m]);
|
|
160
|
+
} else done.push([t0, t1]);
|
|
161
|
+
}
|
|
162
|
+
for (const [, t1] of done) ts.push(t1);
|
|
163
|
+
for (const t of ts) {
|
|
164
|
+
bezier(p, t, q);
|
|
165
|
+
const x = t === 0 ? ax : t === 1 ? bx : q[0];
|
|
166
|
+
const z = t === 0 ? az : t === 1 ? bz : q[1];
|
|
167
|
+
let tx = q[2];
|
|
168
|
+
let tz = q[3];
|
|
169
|
+
if (tx * tx + tz * tz < 1e-18) {
|
|
170
|
+
bezier(p, t === 0 ? 1e-4 : 1 - 1e-4, q);
|
|
171
|
+
tx = q[2];
|
|
172
|
+
tz = q[3];
|
|
173
|
+
}
|
|
174
|
+
push(c, x, z, tx, tz);
|
|
175
|
+
c.radius = Math.min(c.radius, bezierRadius(p, t));
|
|
176
|
+
}
|
|
177
|
+
for (let i = 0; i + 1 < ts.length; i++)
|
|
178
|
+
c.radius = Math.min(c.radius, bezierRadius(p, (ts[i] + ts[i + 1]) / 2));
|
|
179
|
+
} else {
|
|
180
|
+
push(c, ax, az, bx - ax, bz - az);
|
|
181
|
+
push(c, bx, bz, bx - ax, bz - az);
|
|
182
|
+
}
|
|
183
|
+
c.length = c.s[c.count - 1];
|
|
184
|
+
return c;
|
|
185
|
+
}
|
|
186
|
+
function intervalAt(c, s) {
|
|
187
|
+
let lo = 0;
|
|
188
|
+
let hi = c.count - 2;
|
|
189
|
+
if (hi <= 0) return 0;
|
|
190
|
+
while (lo < hi) {
|
|
191
|
+
const mid = lo + hi + 1 >> 1;
|
|
192
|
+
if (c.s[mid] <= s) lo = mid;
|
|
193
|
+
else hi = mid - 1;
|
|
194
|
+
}
|
|
195
|
+
return lo;
|
|
196
|
+
}
|
|
197
|
+
function pointAt(c, s, out) {
|
|
198
|
+
const i = intervalAt(c, s);
|
|
199
|
+
const s0 = c.s[i];
|
|
200
|
+
const s1 = c.s[i + 1] ?? s0;
|
|
201
|
+
const t = s1 > s0 ? Math.min(1, Math.max(0, (s - s0) / (s1 - s0))) : 0;
|
|
202
|
+
out[0] = c.x[i] + (c.x[i + 1] - c.x[i]) * t;
|
|
203
|
+
out[1] = c.z[i] + (c.z[i + 1] - c.z[i]) * t;
|
|
204
|
+
const nx = c.nx[i] + (c.nx[i + 1] - c.nx[i]) * t;
|
|
205
|
+
const nz = c.nz[i] + (c.nz[i + 1] - c.nz[i]) * t;
|
|
206
|
+
const l = Math.sqrt(nx * nx + nz * nz) || 1;
|
|
207
|
+
out[2] = nx / l;
|
|
208
|
+
out[3] = nz / l;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function quadraticToCubic(a, q, b) {
|
|
212
|
+
return {
|
|
213
|
+
c0: [a[0] + 2 / 3 * (q[0] - a[0]), a[1] + 2 / 3 * (q[1] - a[1])],
|
|
214
|
+
c1: [b[0] + 2 / 3 * (q[0] - b[0]), b[1] + 2 / 3 * (q[1] - b[1])]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export {
|
|
218
|
+
CURVE_TOLERANCE,
|
|
219
|
+
arcOf,
|
|
220
|
+
intervalAt,
|
|
221
|
+
pointAt,
|
|
222
|
+
quadraticToCubic,
|
|
223
|
+
sampleWall
|
|
224
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type AssetRef, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import { type Mirror } from '@aethervtt/shard-mirror';
|
|
3
|
+
import { type Image } from '@aethervtt/shard-texture';
|
|
4
|
+
import { type FloorDoc, LIMITS, type MaterialDoc, maxScene, type OpeningDoc, type PropDoc, type SceneDocs, type SceneGrid, shadowStress, type TokenDoc, type WallDoc } from '../fixtures/structure/index.mjs';
|
|
5
|
+
export { LIMITS, maxScene, type SceneDocs, shadowStress };
|
|
6
|
+
/**
|
|
7
|
+
* The fixed visual scale from Aether pixels to world units: 70 px (a cell) is 1.5 m. It never
|
|
8
|
+
* depends on the grid's game distance ("5 ft per square").
|
|
9
|
+
*/
|
|
10
|
+
export declare const PX_TO_WORLD: number;
|
|
11
|
+
/** Resolves a host's texture asset ref to a decoded image (Aether: `asset:<sha256>`). */
|
|
12
|
+
export type ImageSource = (ref: string) => Image | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Aether's scene material as a `StandardMaterial` value (0066). `texture` makes (or finds) the
|
|
15
|
+
* texture for a ref and a use; `packed` is the packed roughness/metalness texture, if the material
|
|
16
|
+
* has either map. Repeat is pixels per tile, so a slot's scale is 1 / (repeat × pxToWorld) tiles per
|
|
17
|
+
* metre on structure's metre UVs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sceneMaterialValue(doc: MaterialDoc, pxToWorld: number, texture: (ref: string, usage: 'color' | 'normal' | 'data') => AssetRef<'Texture'> | null, packed?: AssetRef<'Texture'> | null): Record<string, unknown>;
|
|
20
|
+
/** What one `sync` of every mirror did. */
|
|
21
|
+
export interface HostSyncCounts {
|
|
22
|
+
spawned: number;
|
|
23
|
+
applied: number;
|
|
24
|
+
removed: number;
|
|
25
|
+
resized: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A reference host adapter for Aether-shaped scenes (the one Aether ships lives in Aether): each
|
|
29
|
+
* document list is mirrored onto entities (0055), in world units through one fixed visual scale.
|
|
30
|
+
* Tokens are a logical root with a flat disc as a visual child; token footprints and props are
|
|
31
|
+
* sized in cells, so the grid's pixel size resizes them, and nothing else about the grid does.
|
|
32
|
+
*/
|
|
33
|
+
export declare class HostScene {
|
|
34
|
+
readonly world: World;
|
|
35
|
+
readonly k: number;
|
|
36
|
+
readonly materials: Mirror<MaterialDoc>;
|
|
37
|
+
readonly walls: Mirror<WallDoc>;
|
|
38
|
+
readonly openings: Mirror<OpeningDoc>;
|
|
39
|
+
readonly floors: Mirror<FloorDoc>;
|
|
40
|
+
readonly props: Mirror<PropDoc>;
|
|
41
|
+
readonly tokens: Mirror<TokenDoc>;
|
|
42
|
+
/** Token root → its disc. */
|
|
43
|
+
readonly discs: Map<number, number>;
|
|
44
|
+
grid: SceneGrid | undefined;
|
|
45
|
+
private readonly materialRefs;
|
|
46
|
+
private readonly propMeshes;
|
|
47
|
+
private readonly tokenDocs;
|
|
48
|
+
private readonly propDocs;
|
|
49
|
+
private discMesh;
|
|
50
|
+
private tokenMaterial;
|
|
51
|
+
/** Textures made from the host's images, by ref and use. */
|
|
52
|
+
private readonly textureRefs;
|
|
53
|
+
private readonly images;
|
|
54
|
+
constructor(world: World, pxToWorld?: number, images?: ImageSource);
|
|
55
|
+
/** A scene material's value: its textures made from the host's images on first use. */
|
|
56
|
+
private materialValue;
|
|
57
|
+
/** World size of one grid cell: the grid's pixel size through the visual scale. */
|
|
58
|
+
get cell(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Applies a full host state. Only documents whose revision changed are applied. A grid whose
|
|
61
|
+
* pixel size changed resizes cell-sized things (token footprints, props); its game distance,
|
|
62
|
+
* unit and diagonal rule place nothing.
|
|
63
|
+
*/
|
|
64
|
+
sync(docs: SceneDocs): HostSyncCounts;
|
|
65
|
+
private sizeToken;
|
|
66
|
+
private placeProp;
|
|
67
|
+
private disc;
|
|
68
|
+
private tokenMat;
|
|
69
|
+
/**
|
|
70
|
+
* One mesh per prop asset: spheres of about 1,950 triangles, so 256 props come to Aether's
|
|
71
|
+
* 500k prop triangles.
|
|
72
|
+
*/
|
|
73
|
+
private propMesh;
|
|
74
|
+
}
|
|
75
|
+
/** Mirrors Aether-shaped documents onto `world` (see `HostScene`). */
|
|
76
|
+
export declare function hostScene(world: World, pxToWorld?: number, images?: ImageSource): HostScene;
|
|
77
|
+
export { type BrickOptions, brickMaterial, brickTextures } from './bricks.js';
|