@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/compile.js
ADDED
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildOf,
|
|
3
|
+
Derived,
|
|
4
|
+
defineResource,
|
|
5
|
+
defineSystem,
|
|
6
|
+
onRemove,
|
|
7
|
+
ShardError
|
|
8
|
+
} from "@aethervtt/shard-core";
|
|
9
|
+
import { Mesh } from "@aethervtt/shard-mesh";
|
|
10
|
+
import {
|
|
11
|
+
ComputedVisibility,
|
|
12
|
+
Cutaway,
|
|
13
|
+
GpuAssetsResource,
|
|
14
|
+
Materials,
|
|
15
|
+
Mesh3d,
|
|
16
|
+
Meshes,
|
|
17
|
+
MeshMaterial,
|
|
18
|
+
NotShadowCaster,
|
|
19
|
+
RenderStats,
|
|
20
|
+
ShadowWhenHidden
|
|
21
|
+
} from "@aethervtt/shard-render";
|
|
22
|
+
import { FrameDemand, LogResource, Time } from "@aethervtt/shard-runtime";
|
|
23
|
+
import { Transform } from "@aethervtt/shard-transform";
|
|
24
|
+
import {
|
|
25
|
+
ContactMesh,
|
|
26
|
+
Cutout,
|
|
27
|
+
DoorLeaf,
|
|
28
|
+
defaultContact,
|
|
29
|
+
Floor,
|
|
30
|
+
Level,
|
|
31
|
+
Opening,
|
|
32
|
+
Roof,
|
|
33
|
+
StructureChunk,
|
|
34
|
+
StructureSettings,
|
|
35
|
+
Wall,
|
|
36
|
+
WindowPane
|
|
37
|
+
} from "./components.js";
|
|
38
|
+
import { ContactQuads, contactChunks, contactQuads, emitContact } from "./contact.js";
|
|
39
|
+
import { pointAt, sampleWall } from "./curve.js";
|
|
40
|
+
import {
|
|
41
|
+
ClipScratch,
|
|
42
|
+
chunkKey,
|
|
43
|
+
chunkX,
|
|
44
|
+
chunkZ,
|
|
45
|
+
emitFloor,
|
|
46
|
+
emitWall,
|
|
47
|
+
floorChunks,
|
|
48
|
+
floorShape,
|
|
49
|
+
insideRing,
|
|
50
|
+
MeshBuilder,
|
|
51
|
+
ringChunks,
|
|
52
|
+
ringInside,
|
|
53
|
+
ringShape,
|
|
54
|
+
ringsOverlap,
|
|
55
|
+
slabY,
|
|
56
|
+
wallChunks,
|
|
57
|
+
wallLength,
|
|
58
|
+
wallPieces
|
|
59
|
+
} from "./geometry.js";
|
|
60
|
+
const FRAME_KEY = "structure:frame";
|
|
61
|
+
const GROUP_STRIDE = 4294967296;
|
|
62
|
+
const WALL_KEY = "#wall/";
|
|
63
|
+
function materialOf(key) {
|
|
64
|
+
return key.startsWith(WALL_KEY) ? key.slice(WALL_KEY.length) : key;
|
|
65
|
+
}
|
|
66
|
+
const DOOR_DEMAND = "structure/doors";
|
|
67
|
+
class StructureState {
|
|
68
|
+
walls = /* @__PURE__ */ new Map();
|
|
69
|
+
slabs = /* @__PURE__ */ new Map();
|
|
70
|
+
openings = /* @__PURE__ */ new Map();
|
|
71
|
+
cutouts = /* @__PURE__ */ new Map();
|
|
72
|
+
/** Openings by host wall entity (including hosts that don't exist yet). */
|
|
73
|
+
byWall = /* @__PURE__ */ new Map();
|
|
74
|
+
/** Cutouts by host floor or roof. */
|
|
75
|
+
byHost = /* @__PURE__ */ new Map();
|
|
76
|
+
/** Every Level's elevation, to tell a move from an edit that moves nothing. */
|
|
77
|
+
levels = /* @__PURE__ */ new Map();
|
|
78
|
+
/** Keyed by (group, chunk): see `chunkOf`. */
|
|
79
|
+
chunks = /* @__PURE__ */ new Map();
|
|
80
|
+
materials = /* @__PURE__ */ new Map();
|
|
81
|
+
/** Roofs by the chunks their footprints' bounds overlap (`roofAt`). */
|
|
82
|
+
roofIndex = /* @__PURE__ */ new Map();
|
|
83
|
+
/** Group entities by slot; the ground level is slot 0. */
|
|
84
|
+
groups = [];
|
|
85
|
+
groupSlots = /* @__PURE__ */ new Map();
|
|
86
|
+
/** Doors and hatches swinging. */
|
|
87
|
+
moving = /* @__PURE__ */ new Set();
|
|
88
|
+
/** The ground level's group: pieces without a Level draw under it. */
|
|
89
|
+
ground = -1;
|
|
90
|
+
chunkSize = 0;
|
|
91
|
+
curveTolerance = 0;
|
|
92
|
+
/** The shape each wall last reported a too-tight curve for (reported once per shape). */
|
|
93
|
+
warned = /* @__PURE__ */ new Map();
|
|
94
|
+
/** The error each cutout last reported (reported once until it changes). */
|
|
95
|
+
cutoutWarned = /* @__PURE__ */ new Map();
|
|
96
|
+
last = {
|
|
97
|
+
dirtyChunks: [],
|
|
98
|
+
dirtyGroups: [],
|
|
99
|
+
chunksRebuilt: 0,
|
|
100
|
+
meshesRebuilt: 0,
|
|
101
|
+
contactRebuilt: 0,
|
|
102
|
+
contactChunks: [],
|
|
103
|
+
ms: 0
|
|
104
|
+
};
|
|
105
|
+
/** Totals since start. */
|
|
106
|
+
chunksRebuilt = 0;
|
|
107
|
+
compiles = 0;
|
|
108
|
+
defaultMaterial;
|
|
109
|
+
glassMaterial;
|
|
110
|
+
leafMesh;
|
|
111
|
+
removedWalls = [];
|
|
112
|
+
removedSlabs = [];
|
|
113
|
+
removedOpenings = [];
|
|
114
|
+
removedCutouts = [];
|
|
115
|
+
removedLevels = [];
|
|
116
|
+
dirtyWalls = /* @__PURE__ */ new Set();
|
|
117
|
+
dirtySlabs = /* @__PURE__ */ new Set();
|
|
118
|
+
dirtyChunks = /* @__PURE__ */ new Set();
|
|
119
|
+
/** (group, chunk) keys whose contact mesh alone rebuilds. */
|
|
120
|
+
dirtyContact = /* @__PURE__ */ new Set();
|
|
121
|
+
/** Walls whose contact quads are recomputed after this compile's walls and slabs. */
|
|
122
|
+
contactWalls = /* @__PURE__ */ new Set();
|
|
123
|
+
/** Contact settings as last applied. */
|
|
124
|
+
contact = defaultContact();
|
|
125
|
+
/** StructureSettings.cutawayWalls as last applied (0070). */
|
|
126
|
+
cutawayWalls = false;
|
|
127
|
+
/** The `StructureSettings.contact` object last merged (a patch replaces it). */
|
|
128
|
+
contactSeen;
|
|
129
|
+
contactMaterial;
|
|
130
|
+
contactBuilder = new MeshBuilder();
|
|
131
|
+
contactScratch = new ContactQuads();
|
|
132
|
+
builders = /* @__PURE__ */ new Map();
|
|
133
|
+
scratch = new ClipScratch();
|
|
134
|
+
overlap = /* @__PURE__ */ new Set();
|
|
135
|
+
region = /* @__PURE__ */ new Set();
|
|
136
|
+
sortedOpenings = [];
|
|
137
|
+
world;
|
|
138
|
+
/** Interior lighting's section of `describe` (0069), while interiorLightingPlugin is installed. */
|
|
139
|
+
describeInterior;
|
|
140
|
+
constructor(world) {
|
|
141
|
+
this.world = world;
|
|
142
|
+
}
|
|
143
|
+
/** A material's key, registering its ref (the default material for none). */
|
|
144
|
+
materialKey(ref) {
|
|
145
|
+
if (!ref) return "";
|
|
146
|
+
const key = ref.guid ?? ref.path ?? "";
|
|
147
|
+
if (key && !this.materials.has(key)) this.materials.set(key, ref);
|
|
148
|
+
return key;
|
|
149
|
+
}
|
|
150
|
+
materialRef(key) {
|
|
151
|
+
return (key ? this.materials.get(key) : void 0) ?? this.defaultMaterial;
|
|
152
|
+
}
|
|
153
|
+
builder(key) {
|
|
154
|
+
let b = this.builders.get(key);
|
|
155
|
+
if (!b) {
|
|
156
|
+
b = new MeshBuilder();
|
|
157
|
+
this.builders.set(key, b);
|
|
158
|
+
}
|
|
159
|
+
return b;
|
|
160
|
+
}
|
|
161
|
+
/** A group's slot (assigned on first use). */
|
|
162
|
+
groupSlot(group) {
|
|
163
|
+
let slot = this.groupSlots.get(group);
|
|
164
|
+
if (slot === void 0) {
|
|
165
|
+
slot = this.groups.length;
|
|
166
|
+
this.groups.push(group);
|
|
167
|
+
this.groupSlots.set(group, slot);
|
|
168
|
+
}
|
|
169
|
+
return slot;
|
|
170
|
+
}
|
|
171
|
+
/** The (group, chunk) key of chunk `key` in `group`. The ground level's are plain chunk keys. */
|
|
172
|
+
chunkOf(group, key) {
|
|
173
|
+
return this.groupSlot(group) * GROUP_STRIDE + key;
|
|
174
|
+
}
|
|
175
|
+
chunk(composite) {
|
|
176
|
+
let c = this.chunks.get(composite);
|
|
177
|
+
if (!c) {
|
|
178
|
+
const key = composite % GROUP_STRIDE;
|
|
179
|
+
c = {
|
|
180
|
+
group: this.groups[Math.floor(composite / GROUP_STRIDE)],
|
|
181
|
+
x: chunkX(key),
|
|
182
|
+
z: chunkZ(key),
|
|
183
|
+
walls: /* @__PURE__ */ new Set(),
|
|
184
|
+
slabs: /* @__PURE__ */ new Set(),
|
|
185
|
+
meshes: /* @__PURE__ */ new Map(),
|
|
186
|
+
contacts: /* @__PURE__ */ new Set(),
|
|
187
|
+
contact: null
|
|
188
|
+
};
|
|
189
|
+
this.chunks.set(composite, c);
|
|
190
|
+
}
|
|
191
|
+
return c;
|
|
192
|
+
}
|
|
193
|
+
/** Where a piece on `level` draws: the Level, or the ground level when it has none. */
|
|
194
|
+
groupOf(level) {
|
|
195
|
+
const world = this.world;
|
|
196
|
+
return level !== null && world.isAlive(level) && world.has(level, Level) ? level : this.ground;
|
|
197
|
+
}
|
|
198
|
+
/** A group's elevation: its Level's, or 0. */
|
|
199
|
+
baseOf(group) {
|
|
200
|
+
return group === this.ground ? 0 : this.world.tryGet(group, Level)?.elevation ?? 0;
|
|
201
|
+
}
|
|
202
|
+
/** Curved walls and how finely each is sampled (0066). */
|
|
203
|
+
curves() {
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const [wall, rec] of this.walls)
|
|
206
|
+
if (rec.shape.line.count > 2)
|
|
207
|
+
out.push({ wall, samples: rec.shape.line.count, length: rec.shape.line.length });
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/** Every group: its kind, level index, chunks and meshes, and whether it's hidden. */
|
|
211
|
+
describeGroups() {
|
|
212
|
+
const world = this.world;
|
|
213
|
+
const out = [];
|
|
214
|
+
const counts = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const c of this.chunks.values()) {
|
|
216
|
+
const n = counts.get(c.group) ?? { chunks: 0, meshes: 0, contact: 0 };
|
|
217
|
+
n.chunks++;
|
|
218
|
+
n.meshes += c.meshes.size;
|
|
219
|
+
if (c.contact) n.contact++;
|
|
220
|
+
counts.set(c.group, n);
|
|
221
|
+
}
|
|
222
|
+
for (const group of this.groups) {
|
|
223
|
+
const n = counts.get(group);
|
|
224
|
+
if (!n && group !== this.ground) continue;
|
|
225
|
+
const roof = this.slabs.get(group)?.kind === "roof";
|
|
226
|
+
const level = world.isAlive(group) ? world.tryGet(group, Level) : void 0;
|
|
227
|
+
out.push({
|
|
228
|
+
group,
|
|
229
|
+
kind: group === this.ground ? "ground" : roof ? "roof" : "level",
|
|
230
|
+
index: level?.index ?? 0,
|
|
231
|
+
chunks: n?.chunks ?? 0,
|
|
232
|
+
meshes: n?.meshes ?? 0,
|
|
233
|
+
contactMeshes: n?.contact ?? 0,
|
|
234
|
+
hidden: world.isAlive(group) && world.tryGet(group, ComputedVisibility)?.visible === false
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
/** Counts for `structure.describe`. */
|
|
240
|
+
describe() {
|
|
241
|
+
const size = this.chunkSize;
|
|
242
|
+
let meshes = 0;
|
|
243
|
+
let minX = Infinity;
|
|
244
|
+
let minZ = Infinity;
|
|
245
|
+
let maxX = -Infinity;
|
|
246
|
+
let maxZ = -Infinity;
|
|
247
|
+
const perChunk = [];
|
|
248
|
+
let contactMeshes = 0;
|
|
249
|
+
let contactTriangles = 0;
|
|
250
|
+
for (const c of this.chunks.values()) {
|
|
251
|
+
meshes += c.meshes.size;
|
|
252
|
+
if (c.contact) {
|
|
253
|
+
contactMeshes++;
|
|
254
|
+
contactTriangles += c.contact.triangles;
|
|
255
|
+
}
|
|
256
|
+
if (c.x < minX) minX = c.x;
|
|
257
|
+
if (c.z < minZ) minZ = c.z;
|
|
258
|
+
if (c.x > maxX) maxX = c.x;
|
|
259
|
+
if (c.z > maxZ) maxZ = c.z;
|
|
260
|
+
perChunk.push({
|
|
261
|
+
group: c.group,
|
|
262
|
+
chunk: [c.x, c.z],
|
|
263
|
+
walls: c.walls.size,
|
|
264
|
+
floors: c.slabs.size,
|
|
265
|
+
meshes: c.meshes.size,
|
|
266
|
+
contactTriangles: c.contact?.triangles ?? 0
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
perChunk.sort((a, b) => a.group - b.group || a.chunk[0] - b.chunk[0] || a.chunk[1] - b.chunk[1]);
|
|
270
|
+
let floors = 0;
|
|
271
|
+
for (const s of this.slabs.values()) if (s.kind === "floor") floors++;
|
|
272
|
+
return {
|
|
273
|
+
walls: this.walls.size,
|
|
274
|
+
openings: this.openings.size,
|
|
275
|
+
floors,
|
|
276
|
+
roofs: this.slabs.size - floors,
|
|
277
|
+
cutouts: this.cutouts.size,
|
|
278
|
+
levels: this.levels.size,
|
|
279
|
+
chunkSize: size,
|
|
280
|
+
chunks: this.chunks.size,
|
|
281
|
+
meshes,
|
|
282
|
+
grid: this.chunks.size > 0 ? {
|
|
283
|
+
min: [minX, minZ],
|
|
284
|
+
max: [maxX, maxZ],
|
|
285
|
+
bounds: [minX * size, minZ * size, (maxX + 1) * size, (maxZ + 1) * size]
|
|
286
|
+
} : null,
|
|
287
|
+
groups: this.describeGroups(),
|
|
288
|
+
perChunk,
|
|
289
|
+
contact: {
|
|
290
|
+
enabled: this.contact.enabled,
|
|
291
|
+
meshes: contactMeshes,
|
|
292
|
+
triangles: contactTriangles
|
|
293
|
+
},
|
|
294
|
+
doorsMoving: this.moving.size,
|
|
295
|
+
curves: this.curves(),
|
|
296
|
+
warnings: [...this.warned.keys(), ...this.cutoutWarned.keys()],
|
|
297
|
+
lastCompile: this.last,
|
|
298
|
+
totals: { compiles: this.compiles, chunksRebuilt: this.chunksRebuilt },
|
|
299
|
+
...this.describeInterior ? { interior: this.describeInterior() } : {}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const Structure = defineResource("structure/State", {
|
|
304
|
+
description: "Walls, openings, floors, roofs and cutouts, and the chunk meshes structure compile keeps for each group."
|
|
305
|
+
});
|
|
306
|
+
function observeRemovals(world, state) {
|
|
307
|
+
world.observe(onRemove(Wall), ({ entity }) => {
|
|
308
|
+
state.removedWalls.push(entity);
|
|
309
|
+
});
|
|
310
|
+
world.observe(onRemove(Floor), ({ entity }) => {
|
|
311
|
+
state.removedSlabs.push(entity);
|
|
312
|
+
});
|
|
313
|
+
world.observe(onRemove(Roof), ({ entity }) => {
|
|
314
|
+
state.removedSlabs.push(entity);
|
|
315
|
+
});
|
|
316
|
+
world.observe(onRemove(Opening), ({ entity }) => {
|
|
317
|
+
state.removedOpenings.push(entity);
|
|
318
|
+
});
|
|
319
|
+
world.observe(onRemove(Cutout), ({ entity }) => {
|
|
320
|
+
state.removedCutouts.push(entity);
|
|
321
|
+
});
|
|
322
|
+
world.observe(onRemove(Level), ({ entity }) => {
|
|
323
|
+
state.removedLevels.push(entity);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function wallShape(value, base, tolerance) {
|
|
327
|
+
return {
|
|
328
|
+
ax: value.a[0],
|
|
329
|
+
az: value.a[1],
|
|
330
|
+
bx: value.b[0],
|
|
331
|
+
bz: value.b[1],
|
|
332
|
+
height: value.height,
|
|
333
|
+
thickness: value.thickness,
|
|
334
|
+
elevation: base + value.elevation,
|
|
335
|
+
line: sampleWall(value, tolerance)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function log(state, level, err) {
|
|
339
|
+
state.world.tryResource(LogResource)?.log(level, err.message, {
|
|
340
|
+
code: err.code,
|
|
341
|
+
path: err.path,
|
|
342
|
+
hint: err.hint
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function checkCurve(state, entity, value, shape) {
|
|
346
|
+
if (shape.line.radius >= value.thickness / 2) {
|
|
347
|
+
state.warned.delete(entity);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
const signature = `${value.shape}/${value.bow}/${value.c0}/${value.c1}/${value.thickness}`;
|
|
351
|
+
const arc = value.shape === "arc";
|
|
352
|
+
if (state.warned.get(entity) !== signature) {
|
|
353
|
+
state.warned.set(entity, signature);
|
|
354
|
+
log(
|
|
355
|
+
state,
|
|
356
|
+
arc ? "error" : "warn",
|
|
357
|
+
new ShardError(
|
|
358
|
+
arc ? "structure/wall-too-tight" : "structure/wall-folds",
|
|
359
|
+
arc ? `Wall ${entity}: an arc of radius ${shape.line.radius.toFixed(3)} m can't hold a wall ${value.thickness} m thick` : `Wall ${entity}: the curve bends tighter than half the wall's thickness, so its inner face folds`,
|
|
360
|
+
{
|
|
361
|
+
path: `/entities/${entity}/structure/Wall/${arc ? "bow" : "c0"}`,
|
|
362
|
+
hint: arc ? "Bow it less, or make the wall thinner: the radius must be at least half the thickness." : "Move the control points apart, or make the wall thinner."
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return !arc;
|
|
368
|
+
}
|
|
369
|
+
function markChunks(state, chunks) {
|
|
370
|
+
for (const key of chunks) state.dirtyChunks.add(key);
|
|
371
|
+
}
|
|
372
|
+
function groupKeys(state, group, out) {
|
|
373
|
+
out.clear();
|
|
374
|
+
for (const key of state.overlap) out.add(state.chunkOf(group, key));
|
|
375
|
+
state.overlap.clear();
|
|
376
|
+
}
|
|
377
|
+
function parentTo(state, leaf, group) {
|
|
378
|
+
const world = state.world;
|
|
379
|
+
if (leaf === null || !world.isAlive(leaf)) return;
|
|
380
|
+
if (world.tryGet(leaf, ChildOf)?.parent !== group) world.add(leaf, ChildOf, { parent: group });
|
|
381
|
+
}
|
|
382
|
+
function evaluateWall(state, entity) {
|
|
383
|
+
const world = state.world;
|
|
384
|
+
const old = state.walls.get(entity);
|
|
385
|
+
if (old) {
|
|
386
|
+
queueContactNear(state, old.chunks);
|
|
387
|
+
markChunks(state, old.chunks);
|
|
388
|
+
for (const key of old.chunks) state.chunks.get(key)?.walls.delete(entity);
|
|
389
|
+
}
|
|
390
|
+
const value = world.tryGet(entity, Wall);
|
|
391
|
+
if (!value) {
|
|
392
|
+
state.walls.delete(entity);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const level = value.level ?? null;
|
|
396
|
+
const group = state.groupOf(level);
|
|
397
|
+
const shape = wallShape(value, state.baseOf(group), state.curveTolerance);
|
|
398
|
+
if (!checkCurve(state, entity, value, shape)) {
|
|
399
|
+
state.walls.delete(entity);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const material = state.materialKey(value.material);
|
|
403
|
+
const list = state.sortedOpenings;
|
|
404
|
+
list.length = 0;
|
|
405
|
+
const ids = [];
|
|
406
|
+
for (const o of state.byWall.get(entity) ?? []) {
|
|
407
|
+
const r = state.openings.get(o);
|
|
408
|
+
if (r) ids.push(o);
|
|
409
|
+
}
|
|
410
|
+
ids.sort((p, q) => state.openings.get(p).offset - state.openings.get(q).offset || p - q);
|
|
411
|
+
for (const o of ids) list.push(state.openings.get(o));
|
|
412
|
+
const rec = old ?? {
|
|
413
|
+
shape,
|
|
414
|
+
material,
|
|
415
|
+
pieces: [],
|
|
416
|
+
count: 0,
|
|
417
|
+
pieceMaterials: [],
|
|
418
|
+
chunks: /* @__PURE__ */ new Set(),
|
|
419
|
+
level,
|
|
420
|
+
group,
|
|
421
|
+
contact: new ContactQuads(),
|
|
422
|
+
contactChunks: /* @__PURE__ */ new Set()
|
|
423
|
+
};
|
|
424
|
+
rec.shape = shape;
|
|
425
|
+
rec.material = material;
|
|
426
|
+
rec.level = level;
|
|
427
|
+
rec.group = group;
|
|
428
|
+
rec.count = wallPieces(shape, list, rec.pieces);
|
|
429
|
+
rec.pieceMaterials.length = rec.count;
|
|
430
|
+
for (let i = 0; i < rec.count; i++) {
|
|
431
|
+
const p = rec.pieces[i];
|
|
432
|
+
if (!p.frame) rec.pieceMaterials[i] = material;
|
|
433
|
+
else {
|
|
434
|
+
const frame = list[p.source].frameMaterial;
|
|
435
|
+
rec.pieceMaterials[i] = frame || FRAME_KEY;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
wallChunks(shape, rec.pieces, rec.count, state.chunkSize, state.overlap, state.scratch);
|
|
439
|
+
groupKeys(state, group, rec.chunks);
|
|
440
|
+
for (const key of rec.chunks) state.chunk(key).walls.add(entity);
|
|
441
|
+
markChunks(state, rec.chunks);
|
|
442
|
+
state.walls.set(entity, rec);
|
|
443
|
+
state.contactWalls.add(entity);
|
|
444
|
+
queueContactNear(state, rec.chunks);
|
|
445
|
+
for (const o of ids) {
|
|
446
|
+
const r = state.openings.get(o);
|
|
447
|
+
if (r.leaf === null || !world.isAlive(r.leaf)) spawnLeaf(state, o, r);
|
|
448
|
+
else parentTo(state, r.leaf, group);
|
|
449
|
+
placeLeaf(state, o);
|
|
450
|
+
dressLeaf(state, o);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function rejectCutout(state, entity, code, other) {
|
|
454
|
+
const signature = `${code}/${other ?? ""}`;
|
|
455
|
+
if (state.cutoutWarned.get(entity) === signature) return;
|
|
456
|
+
state.cutoutWarned.set(entity, signature);
|
|
457
|
+
const rec = state.cutouts.get(entity);
|
|
458
|
+
log(
|
|
459
|
+
state,
|
|
460
|
+
"error",
|
|
461
|
+
new ShardError(
|
|
462
|
+
code,
|
|
463
|
+
code === "structure/cutout-outside" ? `Cutout ${entity}: its outline isn't inside its host ${rec.host}` : `Cutout ${entity}: its outline overlaps cutout ${other} on the same host`,
|
|
464
|
+
{
|
|
465
|
+
path: `/entities/${entity}/structure/Cutout/points`,
|
|
466
|
+
hint: code === "structure/cutout-outside" ? "Keep every point inside the floor or roof, off its edge; a hole across the edge is a different outline." : "Move one of them, or merge the two into one outline."
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function evaluateSlab(state, entity) {
|
|
472
|
+
const world = state.world;
|
|
473
|
+
const old = state.slabs.get(entity);
|
|
474
|
+
const oldChunks = old ? [...old.chunks] : [];
|
|
475
|
+
const oldShape = old?.shape;
|
|
476
|
+
const oldCutouts = old?.cutouts ?? [];
|
|
477
|
+
const oldRings = old?.rings;
|
|
478
|
+
if (old?.kind === "floor") queueContactArea(state, old.group, old.shape);
|
|
479
|
+
if (old) {
|
|
480
|
+
for (const key of old.chunks) state.chunks.get(key)?.slabs.delete(entity);
|
|
481
|
+
for (const key of old.index) state.roofIndex.get(key)?.delete(entity);
|
|
482
|
+
}
|
|
483
|
+
const floor = world.tryGet(entity, Floor);
|
|
484
|
+
const roof = floor ? void 0 : world.tryGet(entity, Roof);
|
|
485
|
+
const value = floor ?? roof;
|
|
486
|
+
if (!value || value.points.length < 3) {
|
|
487
|
+
for (const key of oldChunks) state.dirtyChunks.add(key);
|
|
488
|
+
state.slabs.delete(entity);
|
|
489
|
+
for (const c of state.byHost.get(entity) ?? []) dropLeaf(state, c);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const level = value.level ?? null;
|
|
493
|
+
const levelGroup = state.groupOf(level);
|
|
494
|
+
const group = roof ? entity : levelGroup;
|
|
495
|
+
const base = state.baseOf(levelGroup);
|
|
496
|
+
const outline = new Float64Array(value.points.length * 2);
|
|
497
|
+
for (let i = 0; i < value.points.length; i++) {
|
|
498
|
+
outline[i * 2] = value.points[i][0];
|
|
499
|
+
outline[i * 2 + 1] = value.points[i][1];
|
|
500
|
+
}
|
|
501
|
+
const ids = [...state.byHost.get(entity) ?? []].filter((c) => state.cutouts.has(c));
|
|
502
|
+
const rank = (c) => {
|
|
503
|
+
const i = oldCutouts.indexOf(c);
|
|
504
|
+
return i < 0 ? oldCutouts.length : i;
|
|
505
|
+
};
|
|
506
|
+
ids.sort((a, b) => rank(a) - rank(b) || a - b);
|
|
507
|
+
const accepted = [];
|
|
508
|
+
const holes = [];
|
|
509
|
+
const frames = [];
|
|
510
|
+
const frameMaterials = [];
|
|
511
|
+
const rings = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const c of ids) {
|
|
513
|
+
const cut2 = state.cutouts.get(c);
|
|
514
|
+
cut2.ring = -1;
|
|
515
|
+
if (cut2.points.length < 6) continue;
|
|
516
|
+
if (!ringInside(cut2.points, outline)) {
|
|
517
|
+
rejectCutout(state, c, "structure/cutout-outside");
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const clash = accepted.find((a) => ringsOverlap(cut2.points, state.cutouts.get(a).points));
|
|
521
|
+
if (clash !== void 0) {
|
|
522
|
+
rejectCutout(state, c, "structure/cutout-overlap", clash);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
state.cutoutWarned.delete(c);
|
|
526
|
+
accepted.push(c);
|
|
527
|
+
cut2.ring = accepted.length;
|
|
528
|
+
const ring = [];
|
|
529
|
+
for (let i = 0; i < cut2.points.length; i += 2) ring.push([cut2.points[i], cut2.points[i + 1]]);
|
|
530
|
+
holes.push(ring);
|
|
531
|
+
const framed = cut2.frameWidth > 1e-9;
|
|
532
|
+
if (framed) {
|
|
533
|
+
frames.push({ ring: cut2.ring, width: cut2.frameWidth, depth: cut2.frameDepth });
|
|
534
|
+
frameMaterials.push(cut2.frameMaterial || FRAME_KEY);
|
|
535
|
+
}
|
|
536
|
+
rings.set(
|
|
537
|
+
c,
|
|
538
|
+
framed ? `${cut2.points}|${cut2.frameWidth}|${cut2.frameDepth}|${cut2.frameMaterial}` : `${cut2.points}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const material = state.materialKey(value.material);
|
|
542
|
+
const signature = roof ? `roof|${outline}|${base + roof.height}|${roof.thickness}|${roof.pitch}|${roof.ridge}|${material}|${group}` : `floor|${outline}|${base + floor.elevation}|${floor.thickness}|${material}|${group}`;
|
|
543
|
+
const shape = floorShape(
|
|
544
|
+
value.points,
|
|
545
|
+
roof ? base + roof.height : base + floor.elevation,
|
|
546
|
+
roof ? {
|
|
547
|
+
holes,
|
|
548
|
+
frames,
|
|
549
|
+
thickness: roof.thickness,
|
|
550
|
+
pitch: roof.pitch,
|
|
551
|
+
ridge: roof.ridge
|
|
552
|
+
} : { holes, frames, thickness: floor.thickness }
|
|
553
|
+
);
|
|
554
|
+
const rec = old ?? {
|
|
555
|
+
kind: roof ? "roof" : "floor",
|
|
556
|
+
shape,
|
|
557
|
+
material: "",
|
|
558
|
+
chunks: /* @__PURE__ */ new Set(),
|
|
559
|
+
level,
|
|
560
|
+
group,
|
|
561
|
+
cutouts: [],
|
|
562
|
+
frameMaterials: [],
|
|
563
|
+
shadowWhenHidden: false,
|
|
564
|
+
cutaway: false,
|
|
565
|
+
index: [],
|
|
566
|
+
signature: "",
|
|
567
|
+
rings: /* @__PURE__ */ new Map()
|
|
568
|
+
};
|
|
569
|
+
rec.kind = roof ? "roof" : "floor";
|
|
570
|
+
rec.shape = shape;
|
|
571
|
+
rec.material = material;
|
|
572
|
+
rec.level = level;
|
|
573
|
+
rec.group = group;
|
|
574
|
+
rec.cutouts = accepted;
|
|
575
|
+
rec.frameMaterials = frameMaterials;
|
|
576
|
+
const shadow = roof?.shadowWhenHidden ?? false;
|
|
577
|
+
if (old && old.shadowWhenHidden !== shadow) {
|
|
578
|
+
for (const key of old.chunks)
|
|
579
|
+
for (const cm of state.chunks.get(key)?.meshes.values() ?? [])
|
|
580
|
+
setShadowWhenHidden(state, cm, shadow);
|
|
581
|
+
}
|
|
582
|
+
rec.shadowWhenHidden = shadow;
|
|
583
|
+
const cut = roof?.cutaway ?? false;
|
|
584
|
+
if (old && old.cutaway !== cut) {
|
|
585
|
+
for (const key of old.chunks)
|
|
586
|
+
for (const cm of state.chunks.get(key)?.meshes.values() ?? [])
|
|
587
|
+
setTag(state, cm.entity, Cutaway, cut);
|
|
588
|
+
for (const c of old.cutouts) setTag(state, state.cutouts.get(c)?.leaf ?? null, Cutaway, cut);
|
|
589
|
+
}
|
|
590
|
+
rec.cutaway = cut;
|
|
591
|
+
floorChunks(shape, state.chunkSize, state.overlap, state.scratch);
|
|
592
|
+
groupKeys(state, group, rec.chunks);
|
|
593
|
+
for (const key of rec.chunks) state.chunk(key).slabs.add(entity);
|
|
594
|
+
if (!old || old.signature !== signature) {
|
|
595
|
+
for (const key of oldChunks) state.dirtyChunks.add(key);
|
|
596
|
+
markChunks(state, rec.chunks);
|
|
597
|
+
} else {
|
|
598
|
+
const region = state.region;
|
|
599
|
+
region.clear();
|
|
600
|
+
const size = state.chunkSize;
|
|
601
|
+
for (const c of /* @__PURE__ */ new Set([...oldRings.keys(), ...rings.keys()])) {
|
|
602
|
+
if (oldRings.get(c) === rings.get(c)) continue;
|
|
603
|
+
const was = oldCutouts.indexOf(c);
|
|
604
|
+
if (was >= 0) ringChunks(oldShape, was + 1, size, region, state.scratch);
|
|
605
|
+
const now = accepted.indexOf(c);
|
|
606
|
+
if (now >= 0) ringChunks(shape, now + 1, size, region, state.scratch);
|
|
607
|
+
}
|
|
608
|
+
const before = new Set(oldChunks);
|
|
609
|
+
for (const key of region) {
|
|
610
|
+
const composite = state.chunkOf(group, key);
|
|
611
|
+
if (before.has(composite) || rec.chunks.has(composite)) state.dirtyChunks.add(composite);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
rec.signature = signature;
|
|
615
|
+
rec.rings = rings;
|
|
616
|
+
rec.index.length = 0;
|
|
617
|
+
if (roof) {
|
|
618
|
+
const size = state.chunkSize;
|
|
619
|
+
for (let x = Math.floor(shape.minX / size); x <= Math.floor(shape.maxX / size); x++)
|
|
620
|
+
for (let z = Math.floor(shape.minZ / size); z <= Math.floor(shape.maxZ / size); z++) {
|
|
621
|
+
const key = chunkKey(x, z);
|
|
622
|
+
rec.index.push(key);
|
|
623
|
+
let set = state.roofIndex.get(key);
|
|
624
|
+
if (!set) {
|
|
625
|
+
set = /* @__PURE__ */ new Set();
|
|
626
|
+
state.roofIndex.set(key, set);
|
|
627
|
+
}
|
|
628
|
+
set.add(entity);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
state.slabs.set(entity, rec);
|
|
632
|
+
if (floor) queueContactArea(state, group, shape);
|
|
633
|
+
for (const c of ids) {
|
|
634
|
+
const cut2 = state.cutouts.get(c);
|
|
635
|
+
if (cut2.ring < 0) dropLeaf(state, c);
|
|
636
|
+
else buildLeaf(state, c, rec);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function setShadowWhenHidden(state, cm, on) {
|
|
640
|
+
setTag(state, cm.entity, ShadowWhenHidden, on);
|
|
641
|
+
}
|
|
642
|
+
function setTag(state, entity, tag, on) {
|
|
643
|
+
const world = state.world;
|
|
644
|
+
if (entity === null || !world.isAlive(entity) || world.has(entity, tag) === on) return;
|
|
645
|
+
if (on) world.add(entity, tag);
|
|
646
|
+
else world.remove(entity, tag);
|
|
647
|
+
}
|
|
648
|
+
function forgetOpening(state, entity) {
|
|
649
|
+
const rec = state.openings.get(entity);
|
|
650
|
+
if (!rec) return;
|
|
651
|
+
state.byWall.get(rec.wall)?.delete(entity);
|
|
652
|
+
if (state.walls.has(rec.wall)) state.dirtyWalls.add(rec.wall);
|
|
653
|
+
if (rec.leaf !== null && state.world.isAlive(rec.leaf)) state.world.despawn(rec.leaf);
|
|
654
|
+
state.openings.delete(entity);
|
|
655
|
+
state.moving.delete(entity);
|
|
656
|
+
}
|
|
657
|
+
function readOpening(state, entity) {
|
|
658
|
+
const world = state.world;
|
|
659
|
+
const v = world.get(entity, Opening);
|
|
660
|
+
const wall = v.wall ?? -1;
|
|
661
|
+
const frameMaterial = state.materialKey(v.frameMaterial);
|
|
662
|
+
const target = v.kind === "door" && v.state === "open" ? 1 : 0;
|
|
663
|
+
let rec = state.openings.get(entity);
|
|
664
|
+
if (!rec) {
|
|
665
|
+
rec = {
|
|
666
|
+
wall,
|
|
667
|
+
kind: v.kind,
|
|
668
|
+
offset: v.offset,
|
|
669
|
+
width: v.width,
|
|
670
|
+
height: v.height,
|
|
671
|
+
sill: v.sill,
|
|
672
|
+
frameWidth: v.frameWidth,
|
|
673
|
+
frameDepth: v.frameDepth,
|
|
674
|
+
frameMaterial,
|
|
675
|
+
hinge: v.hinge,
|
|
676
|
+
swing: v.swing,
|
|
677
|
+
state: v.state,
|
|
678
|
+
leaf: null,
|
|
679
|
+
open: target,
|
|
680
|
+
target
|
|
681
|
+
};
|
|
682
|
+
state.openings.set(entity, rec);
|
|
683
|
+
hostSet(state.byWall, wall).add(entity);
|
|
684
|
+
spawnLeaf(state, entity, rec);
|
|
685
|
+
if (state.walls.has(wall) || world.isAlive(wall)) state.dirtyWalls.add(wall);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const geometry = rec.wall !== wall || rec.kind !== v.kind || rec.offset !== v.offset || rec.width !== v.width || rec.height !== v.height || rec.sill !== v.sill || rec.frameWidth !== v.frameWidth || rec.frameDepth !== v.frameDepth || rec.frameMaterial !== frameMaterial;
|
|
689
|
+
const kindChanged = rec.kind !== v.kind;
|
|
690
|
+
if (geometry) {
|
|
691
|
+
state.dirtyWalls.add(rec.wall);
|
|
692
|
+
if (rec.wall !== wall) {
|
|
693
|
+
state.byWall.get(rec.wall)?.delete(entity);
|
|
694
|
+
hostSet(state.byWall, wall).add(entity);
|
|
695
|
+
state.dirtyWalls.add(wall);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
rec.wall = wall;
|
|
699
|
+
rec.kind = v.kind;
|
|
700
|
+
rec.offset = v.offset;
|
|
701
|
+
rec.width = v.width;
|
|
702
|
+
rec.height = v.height;
|
|
703
|
+
rec.sill = v.sill;
|
|
704
|
+
rec.frameWidth = v.frameWidth;
|
|
705
|
+
rec.frameDepth = v.frameDepth;
|
|
706
|
+
rec.frameMaterial = frameMaterial;
|
|
707
|
+
const placement = geometry || rec.hinge !== v.hinge || rec.swing !== v.swing;
|
|
708
|
+
rec.hinge = v.hinge;
|
|
709
|
+
rec.swing = v.swing;
|
|
710
|
+
rec.state = v.state;
|
|
711
|
+
if (kindChanged) {
|
|
712
|
+
if (rec.leaf !== null && world.isAlive(rec.leaf)) world.despawn(rec.leaf);
|
|
713
|
+
rec.leaf = null;
|
|
714
|
+
spawnLeaf(state, entity, rec);
|
|
715
|
+
} else if (placement) {
|
|
716
|
+
placeLeaf(state, entity);
|
|
717
|
+
dressLeaf(state, entity);
|
|
718
|
+
}
|
|
719
|
+
if (rec.target !== target) {
|
|
720
|
+
rec.target = target;
|
|
721
|
+
if (state.world.resource(StructureSettings).reducedMotion) {
|
|
722
|
+
rec.open = target;
|
|
723
|
+
placeLeaf(state, entity);
|
|
724
|
+
} else state.moving.add(entity);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function hostSet(map, host) {
|
|
728
|
+
let set = map.get(host);
|
|
729
|
+
if (!set) {
|
|
730
|
+
set = /* @__PURE__ */ new Set();
|
|
731
|
+
map.set(host, set);
|
|
732
|
+
}
|
|
733
|
+
return set;
|
|
734
|
+
}
|
|
735
|
+
function spawnLeaf(state, opening, rec) {
|
|
736
|
+
const world = state.world;
|
|
737
|
+
const material = rec.kind === "window" ? state.glassMaterial : state.defaultMaterial;
|
|
738
|
+
const leaf = world.spawn(
|
|
739
|
+
[Mesh3d, { mesh: state.leafMesh }],
|
|
740
|
+
[MeshMaterial, { material }],
|
|
741
|
+
[DoorLeaf, { opening, angle: rec.open }],
|
|
742
|
+
[ChildOf, { parent: state.walls.get(rec.wall)?.group ?? state.ground }],
|
|
743
|
+
Transform,
|
|
744
|
+
Derived
|
|
745
|
+
);
|
|
746
|
+
if (rec.kind === "window") {
|
|
747
|
+
world.add(leaf, WindowPane);
|
|
748
|
+
world.add(leaf, NotShadowCaster);
|
|
749
|
+
}
|
|
750
|
+
if (state.cutawayWalls) world.add(leaf, Cutaway);
|
|
751
|
+
rec.leaf = leaf;
|
|
752
|
+
placeLeaf(state, opening);
|
|
753
|
+
dressLeaf(state, opening);
|
|
754
|
+
}
|
|
755
|
+
const HALF_PI = Math.PI / 2;
|
|
756
|
+
const chordA = new Float64Array(4);
|
|
757
|
+
const chordB = new Float64Array(4);
|
|
758
|
+
function placeLeaf(state, opening) {
|
|
759
|
+
const rec = state.openings.get(opening);
|
|
760
|
+
const world = state.world;
|
|
761
|
+
if (!rec || rec.leaf === null || !world.isAlive(rec.leaf)) return;
|
|
762
|
+
const wall = state.walls.get(rec.wall);
|
|
763
|
+
const leaf = rec.leaf;
|
|
764
|
+
const table = world.entityTable(leaf);
|
|
765
|
+
const row = world.entityRow(leaf);
|
|
766
|
+
const scale = table.column(Transform, "scale");
|
|
767
|
+
if (!wall) {
|
|
768
|
+
scale[row * 3] = scale[row * 3 + 1] = scale[row * 3 + 2] = 0;
|
|
769
|
+
table.markChanged(Transform, row);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const s = wall.shape;
|
|
773
|
+
const len = wallLength(s);
|
|
774
|
+
const o0 = Math.max(0, Math.min(len, rec.offset));
|
|
775
|
+
const o1 = Math.max(o0, Math.min(len, rec.offset + rec.width));
|
|
776
|
+
const fw = Math.max(0, Math.min(rec.frameWidth, (o1 - o0) / 2));
|
|
777
|
+
const head = Math.min(s.height, rec.sill + rec.height);
|
|
778
|
+
const bottom = rec.sill + (rec.kind === "window" ? fw : 0);
|
|
779
|
+
const height = Math.max(0, head - fw - bottom);
|
|
780
|
+
const thick = rec.kind === "window" ? Math.min(0.02, s.thickness * 0.2) : s.thickness * 0.4;
|
|
781
|
+
const end = rec.kind === "door" && rec.hinge === "end";
|
|
782
|
+
const p0 = pointAt(s.line, o0 + fw, chordA);
|
|
783
|
+
const p1 = pointAt(s.line, Math.max(o0 + fw, o1 - fw), chordB);
|
|
784
|
+
const cx = p1[0] - p0[0];
|
|
785
|
+
const cz = p1[1] - p0[1];
|
|
786
|
+
const width = Math.sqrt(cx * cx + cz * cz);
|
|
787
|
+
const dx = (end ? -cx : cx) / (width || 1);
|
|
788
|
+
const dz = (end ? -cz : cz) / (width || 1);
|
|
789
|
+
const hinge = end ? p1 : p0;
|
|
790
|
+
const yaw = Math.atan2(-dz, dx);
|
|
791
|
+
const sign = rec.swing === "left" !== end ? 1 : -1;
|
|
792
|
+
const angle = yaw + (rec.kind === "door" ? sign * rec.open * HALF_PI : 0);
|
|
793
|
+
const translation = table.column(Transform, "translation");
|
|
794
|
+
const rotation = table.column(Transform, "rotation");
|
|
795
|
+
translation[row * 3] = hinge[0];
|
|
796
|
+
translation[row * 3 + 1] = s.elevation + bottom;
|
|
797
|
+
translation[row * 3 + 2] = hinge[1];
|
|
798
|
+
rotation[row * 4] = 0;
|
|
799
|
+
rotation[row * 4 + 1] = Math.sin(angle / 2);
|
|
800
|
+
rotation[row * 4 + 2] = 0;
|
|
801
|
+
rotation[row * 4 + 3] = Math.cos(angle / 2);
|
|
802
|
+
scale[row * 3] = width;
|
|
803
|
+
scale[row * 3 + 1] = height;
|
|
804
|
+
scale[row * 3 + 2] = thick;
|
|
805
|
+
table.markChanged(Transform, row);
|
|
806
|
+
const angles = table.column(DoorLeaf, "angle");
|
|
807
|
+
angles[row] = rec.open;
|
|
808
|
+
}
|
|
809
|
+
function dressLeaf(state, opening) {
|
|
810
|
+
const rec = state.openings.get(opening);
|
|
811
|
+
const world = state.world;
|
|
812
|
+
if (rec?.kind !== "door" || rec.leaf === null || !world.isAlive(rec.leaf)) return;
|
|
813
|
+
const material = state.materialRef(rec.frameMaterial || FRAME_KEY);
|
|
814
|
+
const current = world.get(rec.leaf, MeshMaterial).material;
|
|
815
|
+
if (current?.guid !== material.guid || current?.path !== material.path)
|
|
816
|
+
world.set(rec.leaf, MeshMaterial, { material });
|
|
817
|
+
}
|
|
818
|
+
function forgetCutout(state, entity) {
|
|
819
|
+
const rec = state.cutouts.get(entity);
|
|
820
|
+
if (!rec) return;
|
|
821
|
+
dropLeaf(state, entity);
|
|
822
|
+
state.byHost.get(rec.host)?.delete(entity);
|
|
823
|
+
if (state.slabs.has(rec.host)) state.dirtySlabs.add(rec.host);
|
|
824
|
+
state.cutouts.delete(entity);
|
|
825
|
+
state.cutoutWarned.delete(entity);
|
|
826
|
+
}
|
|
827
|
+
function readCutout(state, entity) {
|
|
828
|
+
const world = state.world;
|
|
829
|
+
const v = world.get(entity, Cutout);
|
|
830
|
+
const host = v.host ?? -1;
|
|
831
|
+
const frameMaterial = state.materialKey(v.frameMaterial);
|
|
832
|
+
const target = v.kind === "hatch" && v.state === "open" ? 1 : 0;
|
|
833
|
+
const points = new Float64Array(v.points.length * 2);
|
|
834
|
+
for (let i = 0; i < v.points.length; i++) {
|
|
835
|
+
points[i * 2] = v.points[i][0];
|
|
836
|
+
points[i * 2 + 1] = v.points[i][1];
|
|
837
|
+
}
|
|
838
|
+
let rec = state.cutouts.get(entity);
|
|
839
|
+
if (!rec) {
|
|
840
|
+
rec = {
|
|
841
|
+
host,
|
|
842
|
+
kind: v.kind,
|
|
843
|
+
points,
|
|
844
|
+
frameWidth: v.frameWidth,
|
|
845
|
+
frameDepth: v.frameDepth,
|
|
846
|
+
frameMaterial,
|
|
847
|
+
hinge: v.hinge,
|
|
848
|
+
state: v.state,
|
|
849
|
+
ring: -1,
|
|
850
|
+
leaf: null,
|
|
851
|
+
mesh: null,
|
|
852
|
+
meshRef: null,
|
|
853
|
+
pivot: [0, 0, 0, 1, 0, 0, 1],
|
|
854
|
+
open: target,
|
|
855
|
+
target
|
|
856
|
+
};
|
|
857
|
+
state.cutouts.set(entity, rec);
|
|
858
|
+
hostSet(state.byHost, host).add(entity);
|
|
859
|
+
state.dirtySlabs.add(host);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
let samePoints = rec.points.length === points.length;
|
|
863
|
+
for (let i = 0; samePoints && i < points.length; i++) samePoints = rec.points[i] === points[i];
|
|
864
|
+
const geometry = rec.host !== host || rec.kind !== v.kind || !samePoints || rec.frameWidth !== v.frameWidth || rec.frameDepth !== v.frameDepth || rec.frameMaterial !== frameMaterial;
|
|
865
|
+
if (geometry) {
|
|
866
|
+
state.dirtySlabs.add(rec.host);
|
|
867
|
+
if (rec.host !== host) {
|
|
868
|
+
state.byHost.get(rec.host)?.delete(entity);
|
|
869
|
+
hostSet(state.byHost, host).add(entity);
|
|
870
|
+
state.dirtySlabs.add(host);
|
|
871
|
+
}
|
|
872
|
+
if (rec.kind !== v.kind) dropLeaf(state, entity);
|
|
873
|
+
}
|
|
874
|
+
const rehinge = !geometry && rec.hinge !== v.hinge;
|
|
875
|
+
rec.host = host;
|
|
876
|
+
rec.kind = v.kind;
|
|
877
|
+
rec.points = points;
|
|
878
|
+
rec.frameWidth = v.frameWidth;
|
|
879
|
+
rec.frameDepth = v.frameDepth;
|
|
880
|
+
rec.frameMaterial = frameMaterial;
|
|
881
|
+
rec.hinge = v.hinge;
|
|
882
|
+
rec.state = v.state;
|
|
883
|
+
if (rehinge) {
|
|
884
|
+
const slab = state.slabs.get(host);
|
|
885
|
+
if (slab && rec.ring > 0) buildLeaf(state, entity, slab);
|
|
886
|
+
}
|
|
887
|
+
if (rec.target !== target) {
|
|
888
|
+
rec.target = target;
|
|
889
|
+
if (state.world.resource(StructureSettings).reducedMotion) {
|
|
890
|
+
rec.open = target;
|
|
891
|
+
placeHatch(state, entity);
|
|
892
|
+
} else state.moving.add(entity);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function dropLeaf(state, entity) {
|
|
896
|
+
const rec = state.cutouts.get(entity);
|
|
897
|
+
if (!rec) return;
|
|
898
|
+
const world = state.world;
|
|
899
|
+
if (rec.leaf !== null && world.isAlive(rec.leaf)) world.despawn(rec.leaf);
|
|
900
|
+
if (rec.mesh) {
|
|
901
|
+
world.tryResource(GpuAssetsResource)?.releaseMesh(rec.mesh);
|
|
902
|
+
world.resource(Meshes).delete(rec.meshRef.guid);
|
|
903
|
+
}
|
|
904
|
+
rec.leaf = null;
|
|
905
|
+
rec.mesh = null;
|
|
906
|
+
rec.meshRef = null;
|
|
907
|
+
state.moving.delete(entity);
|
|
908
|
+
}
|
|
909
|
+
const leafBuilder = new MeshBuilder();
|
|
910
|
+
function buildLeaf(state, entity, host) {
|
|
911
|
+
const rec = state.cutouts.get(entity);
|
|
912
|
+
if (rec.kind === "hole") {
|
|
913
|
+
dropLeaf(state, entity);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const world = state.world;
|
|
917
|
+
const f = host.shape;
|
|
918
|
+
const hatch = rec.kind === "hatch";
|
|
919
|
+
const leafShape = hatch ? ringShape(f, rec.ring, f.thickness > 0 ? Math.min(f.thickness, 0.06) : 0.04) : ringShape(f, rec.ring, 0, f.thickness > 0 ? Math.min(f.thickness / 2, 0.02) : 5e-3);
|
|
920
|
+
const b = leafBuilder;
|
|
921
|
+
b.reset();
|
|
922
|
+
emitFloor(leafShape, -Infinity, -Infinity, Infinity, Infinity, b, state.scratch);
|
|
923
|
+
const count = rec.points.length / 2;
|
|
924
|
+
const i = rec.hinge % count;
|
|
925
|
+
const j = (i + 1) % count;
|
|
926
|
+
const hx = rec.points[i * 2];
|
|
927
|
+
const hz = rec.points[i * 2 + 1];
|
|
928
|
+
const hy = slabY(f, hx, hz);
|
|
929
|
+
const pivot = rec.pivot;
|
|
930
|
+
if (hatch) {
|
|
931
|
+
const ex = rec.points[j * 2] - hx;
|
|
932
|
+
const ez = rec.points[j * 2 + 1] - hz;
|
|
933
|
+
const ey = slabY(f, hx + ex, hz + ez) - hy;
|
|
934
|
+
const el = Math.sqrt(ex * ex + ey * ey + ez * ez) || 1;
|
|
935
|
+
let cx = 0;
|
|
936
|
+
let cz = 0;
|
|
937
|
+
for (let k = 0; k < count; k++) {
|
|
938
|
+
cx += rec.points[k * 2] - hx;
|
|
939
|
+
cz += rec.points[k * 2 + 1] - hz;
|
|
940
|
+
}
|
|
941
|
+
const lift = ez / el * cx - ex / el * cz;
|
|
942
|
+
pivot[0] = hx;
|
|
943
|
+
pivot[1] = hy;
|
|
944
|
+
pivot[2] = hz;
|
|
945
|
+
pivot[3] = ex / el;
|
|
946
|
+
pivot[4] = ey / el;
|
|
947
|
+
pivot[5] = ez / el;
|
|
948
|
+
pivot[6] = lift >= 0 ? 1 : -1;
|
|
949
|
+
const p = b.positions;
|
|
950
|
+
for (let k = 0; k < b.vertexCount; k++) {
|
|
951
|
+
p[k * 3] = p[k * 3] - hx;
|
|
952
|
+
p[k * 3 + 1] = p[k * 3 + 1] - hy;
|
|
953
|
+
p[k * 3 + 2] = p[k * 3 + 2] - hz;
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
pivot[0] = pivot[1] = pivot[2] = 0;
|
|
957
|
+
pivot[3] = 1;
|
|
958
|
+
pivot[4] = pivot[5] = 0;
|
|
959
|
+
pivot[6] = 1;
|
|
960
|
+
}
|
|
961
|
+
const data = meshData(b);
|
|
962
|
+
if (rec.mesh) rec.mesh.update(data);
|
|
963
|
+
else {
|
|
964
|
+
rec.mesh = Mesh.create(data);
|
|
965
|
+
rec.meshRef = world.resource(Meshes).add(rec.mesh, `structure:${rec.kind}/${entity}`);
|
|
966
|
+
}
|
|
967
|
+
const material = hatch ? state.materialRef(rec.frameMaterial || FRAME_KEY) : state.glassMaterial;
|
|
968
|
+
if (rec.leaf === null || !world.isAlive(rec.leaf)) {
|
|
969
|
+
rec.leaf = world.spawn(
|
|
970
|
+
[Mesh3d, { mesh: rec.meshRef }],
|
|
971
|
+
[MeshMaterial, { material }],
|
|
972
|
+
[DoorLeaf, { opening: entity, angle: rec.open }],
|
|
973
|
+
[ChildOf, { parent: host.group }],
|
|
974
|
+
Transform,
|
|
975
|
+
Derived
|
|
976
|
+
);
|
|
977
|
+
if (!hatch) {
|
|
978
|
+
world.add(rec.leaf, WindowPane);
|
|
979
|
+
world.add(rec.leaf, NotShadowCaster);
|
|
980
|
+
}
|
|
981
|
+
if (host.cutaway) world.add(rec.leaf, Cutaway);
|
|
982
|
+
} else {
|
|
983
|
+
parentTo(state, rec.leaf, host.group);
|
|
984
|
+
const current = world.get(rec.leaf, MeshMaterial).material;
|
|
985
|
+
if (current?.guid !== material.guid || current?.path !== material.path)
|
|
986
|
+
world.set(rec.leaf, MeshMaterial, { material });
|
|
987
|
+
}
|
|
988
|
+
placeHatch(state, entity);
|
|
989
|
+
}
|
|
990
|
+
function placeHatch(state, entity) {
|
|
991
|
+
const rec = state.cutouts.get(entity);
|
|
992
|
+
const world = state.world;
|
|
993
|
+
if (!rec || rec.leaf === null || !world.isAlive(rec.leaf)) return;
|
|
994
|
+
const table = world.entityTable(rec.leaf);
|
|
995
|
+
const row = world.entityRow(rec.leaf);
|
|
996
|
+
const p = rec.pivot;
|
|
997
|
+
const half = p[6] * rec.open * HALF_PI / 2;
|
|
998
|
+
const s = Math.sin(half);
|
|
999
|
+
const translation = table.column(Transform, "translation");
|
|
1000
|
+
const rotation = table.column(Transform, "rotation");
|
|
1001
|
+
translation[row * 3] = p[0];
|
|
1002
|
+
translation[row * 3 + 1] = p[1];
|
|
1003
|
+
translation[row * 3 + 2] = p[2];
|
|
1004
|
+
rotation[row * 4] = p[3] * s;
|
|
1005
|
+
rotation[row * 4 + 1] = p[4] * s;
|
|
1006
|
+
rotation[row * 4 + 2] = p[5] * s;
|
|
1007
|
+
rotation[row * 4 + 3] = Math.cos(half);
|
|
1008
|
+
table.markChanged(Transform, row);
|
|
1009
|
+
const angles = table.column(DoorLeaf, "angle");
|
|
1010
|
+
angles[row] = rec.open;
|
|
1011
|
+
}
|
|
1012
|
+
function rebuildChunk(state, key) {
|
|
1013
|
+
const world = state.world;
|
|
1014
|
+
const chunk = state.chunks.get(key);
|
|
1015
|
+
if (!chunk) return 0;
|
|
1016
|
+
const size = state.chunkSize;
|
|
1017
|
+
const minX = chunk.x * size;
|
|
1018
|
+
const minZ = chunk.z * size;
|
|
1019
|
+
const maxX = minX + size;
|
|
1020
|
+
const maxZ = minZ + size;
|
|
1021
|
+
for (const b of state.builders.values()) b.reset();
|
|
1022
|
+
const wallKey = state.cutawayWalls ? WALL_KEY : "";
|
|
1023
|
+
for (const entity of chunk.walls) {
|
|
1024
|
+
const rec = state.walls.get(entity);
|
|
1025
|
+
if (!rec) continue;
|
|
1026
|
+
emitWall(
|
|
1027
|
+
rec.shape,
|
|
1028
|
+
rec.pieces,
|
|
1029
|
+
rec.count,
|
|
1030
|
+
minX,
|
|
1031
|
+
minZ,
|
|
1032
|
+
maxX,
|
|
1033
|
+
maxZ,
|
|
1034
|
+
(i) => state.builder(wallKey + rec.pieceMaterials[i]),
|
|
1035
|
+
state.scratch
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
let shadowWhenHidden = false;
|
|
1039
|
+
let cutawayRoof = false;
|
|
1040
|
+
for (const entity of chunk.slabs) {
|
|
1041
|
+
const rec = state.slabs.get(entity);
|
|
1042
|
+
if (!rec) continue;
|
|
1043
|
+
if (rec.shadowWhenHidden) shadowWhenHidden = true;
|
|
1044
|
+
if (rec.cutaway) cutawayRoof = true;
|
|
1045
|
+
emitFloor(
|
|
1046
|
+
rec.shape,
|
|
1047
|
+
minX,
|
|
1048
|
+
minZ,
|
|
1049
|
+
maxX,
|
|
1050
|
+
maxZ,
|
|
1051
|
+
state.builder(rec.material),
|
|
1052
|
+
state.scratch,
|
|
1053
|
+
(k) => state.builder(rec.frameMaterials[k])
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
let rebuilt = 0;
|
|
1057
|
+
const meshes = world.resource(Meshes);
|
|
1058
|
+
for (const [material, b] of state.builders) {
|
|
1059
|
+
const existing = chunk.meshes.get(material);
|
|
1060
|
+
if (b.vertexCount === 0) continue;
|
|
1061
|
+
const data = meshData(b);
|
|
1062
|
+
if (existing && world.isAlive(existing.entity)) {
|
|
1063
|
+
existing.mesh.update(data);
|
|
1064
|
+
existing.triangles = b.indexCount / 3;
|
|
1065
|
+
} else {
|
|
1066
|
+
if (existing) dropChunkMesh(world, existing);
|
|
1067
|
+
const mesh = Mesh.create(data);
|
|
1068
|
+
const ref = meshes.add(
|
|
1069
|
+
mesh,
|
|
1070
|
+
`structure:chunk/${chunk.group}/${chunk.x},${chunk.z}/${material || "default"}`
|
|
1071
|
+
);
|
|
1072
|
+
const entity = world.spawn(
|
|
1073
|
+
[Mesh3d, { mesh: ref }],
|
|
1074
|
+
[MeshMaterial, { material: state.materialRef(materialOf(material)) }],
|
|
1075
|
+
[StructureChunk, { group: chunk.group, x: chunk.x, z: chunk.z }],
|
|
1076
|
+
[ChildOf, { parent: chunk.group }],
|
|
1077
|
+
Transform,
|
|
1078
|
+
Derived
|
|
1079
|
+
);
|
|
1080
|
+
if (shadowWhenHidden) world.add(entity, ShadowWhenHidden);
|
|
1081
|
+
if (cutawayRoof || material.startsWith(WALL_KEY)) world.add(entity, Cutaway);
|
|
1082
|
+
chunk.meshes.set(material, { entity, mesh, ref, triangles: b.indexCount / 3 });
|
|
1083
|
+
}
|
|
1084
|
+
rebuilt++;
|
|
1085
|
+
}
|
|
1086
|
+
for (const [material, cm] of chunk.meshes) {
|
|
1087
|
+
if ((state.builders.get(material)?.vertexCount ?? 0) > 0) continue;
|
|
1088
|
+
dropChunkMesh(world, cm);
|
|
1089
|
+
chunk.meshes.delete(material);
|
|
1090
|
+
}
|
|
1091
|
+
rebuildContact(state, chunk);
|
|
1092
|
+
dropIfEmpty(state, key, chunk);
|
|
1093
|
+
return rebuilt;
|
|
1094
|
+
}
|
|
1095
|
+
function dropIfEmpty(state, key, chunk) {
|
|
1096
|
+
if (chunk.walls.size === 0 && chunk.slabs.size === 0 && chunk.meshes.size === 0 && chunk.contacts.size === 0 && chunk.contact === null)
|
|
1097
|
+
state.chunks.delete(key);
|
|
1098
|
+
}
|
|
1099
|
+
function queueContactNear(state, keys) {
|
|
1100
|
+
if (!state.contact.enabled) return;
|
|
1101
|
+
for (const key of keys) {
|
|
1102
|
+
const chunk = state.chunks.get(key);
|
|
1103
|
+
if (chunk) for (const w of chunk.walls) state.contactWalls.add(w);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function queueContactArea(state, group, f) {
|
|
1107
|
+
if (!state.contact.enabled) return;
|
|
1108
|
+
const size = state.chunkSize;
|
|
1109
|
+
const r = state.contact.floorReach + 0.1;
|
|
1110
|
+
for (let x = Math.floor((f.minX - r) / size); x <= Math.floor((f.maxX + r) / size); x++)
|
|
1111
|
+
for (let z = Math.floor((f.minZ - r) / size); z <= Math.floor((f.maxZ + r) / size); z++) {
|
|
1112
|
+
const chunk = state.chunks.get(state.chunkOf(group, chunkKey(x, z)));
|
|
1113
|
+
if (chunk) for (const w of chunk.walls) state.contactWalls.add(w);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const env = {
|
|
1117
|
+
state: void 0,
|
|
1118
|
+
entity: -1,
|
|
1119
|
+
rec: void 0,
|
|
1120
|
+
seen: /* @__PURE__ */ new Set(),
|
|
1121
|
+
keys: [0, 0, 0, 0],
|
|
1122
|
+
floorTop(x, z, base) {
|
|
1123
|
+
const st = env.state;
|
|
1124
|
+
const size = st.chunkSize;
|
|
1125
|
+
const chunk = st.chunks.get(
|
|
1126
|
+
st.chunkOf(env.rec.group, chunkKey(Math.floor(x / size), Math.floor(z / size)))
|
|
1127
|
+
);
|
|
1128
|
+
if (!chunk) return Number.NaN;
|
|
1129
|
+
let best = Number.NaN;
|
|
1130
|
+
for (const e of chunk.slabs) {
|
|
1131
|
+
const slab = st.slabs.get(e);
|
|
1132
|
+
if (slab?.kind !== "floor") continue;
|
|
1133
|
+
const f = slab.shape;
|
|
1134
|
+
if (x < f.minX || x > f.maxX || z < f.minZ || z > f.maxZ) continue;
|
|
1135
|
+
if (!insideRing(f.points, 0, f.rings[1], x, z, false)) continue;
|
|
1136
|
+
let hole = false;
|
|
1137
|
+
for (let r = 1; r + 1 < f.rings.length && !hole; r++)
|
|
1138
|
+
hole = insideRing(f.points, f.rings[r], f.rings[r + 1], x, z, true);
|
|
1139
|
+
if (hole) continue;
|
|
1140
|
+
const top = slabY(f, x, z);
|
|
1141
|
+
if (Number.isNaN(best) || Math.abs(top - base) < Math.abs(best - base)) best = top;
|
|
1142
|
+
}
|
|
1143
|
+
return best;
|
|
1144
|
+
},
|
|
1145
|
+
joint(end, out) {
|
|
1146
|
+
const st = env.state;
|
|
1147
|
+
const w = env.rec.shape;
|
|
1148
|
+
const line = w.line;
|
|
1149
|
+
const i = end === 0 ? 0 : line.count - 1;
|
|
1150
|
+
const ex = line.x[i];
|
|
1151
|
+
const ez = line.z[i];
|
|
1152
|
+
const tx = line.nz[i];
|
|
1153
|
+
const tz = -line.nx[i];
|
|
1154
|
+
const size = st.chunkSize;
|
|
1155
|
+
let y0 = Infinity;
|
|
1156
|
+
let y1 = -Infinity;
|
|
1157
|
+
let covered = 0;
|
|
1158
|
+
const seen = env.seen;
|
|
1159
|
+
seen.clear();
|
|
1160
|
+
seen.add(env.entity);
|
|
1161
|
+
for (let k = 0; k < 4; k++) {
|
|
1162
|
+
const key = st.chunkOf(
|
|
1163
|
+
env.rec.group,
|
|
1164
|
+
chunkKey(
|
|
1165
|
+
Math.floor((ex + (k & 1 ? 0.05 : -0.05)) / size),
|
|
1166
|
+
Math.floor((ez + (k & 2 ? 0.05 : -0.05)) / size)
|
|
1167
|
+
)
|
|
1168
|
+
);
|
|
1169
|
+
env.keys[k] = key;
|
|
1170
|
+
if (env.keys.indexOf(key) < k) continue;
|
|
1171
|
+
const chunk = st.chunks.get(key);
|
|
1172
|
+
if (!chunk) continue;
|
|
1173
|
+
for (const o of chunk.walls) {
|
|
1174
|
+
if (seen.has(o)) continue;
|
|
1175
|
+
seen.add(o);
|
|
1176
|
+
const other = st.walls.get(o);
|
|
1177
|
+
if (!other) continue;
|
|
1178
|
+
const ol = other.shape.line;
|
|
1179
|
+
const half = other.shape.thickness / 2;
|
|
1180
|
+
let best = Infinity;
|
|
1181
|
+
let sx = 0;
|
|
1182
|
+
let sz = 0;
|
|
1183
|
+
for (let j = 0; j + 1 < ol.count; j++) {
|
|
1184
|
+
const ax = ol.x[j];
|
|
1185
|
+
const az = ol.z[j];
|
|
1186
|
+
const dx = ol.x[j + 1] - ax;
|
|
1187
|
+
const dz = ol.z[j + 1] - az;
|
|
1188
|
+
const l2 = dx * dx + dz * dz;
|
|
1189
|
+
const t = l2 > 0 ? Math.min(1, Math.max(0, ((ex - ax) * dx + (ez - az) * dz) / l2)) : 0;
|
|
1190
|
+
const px = ax + dx * t - ex;
|
|
1191
|
+
const pz = az + dz * t - ez;
|
|
1192
|
+
const d = Math.sqrt(px * px + pz * pz);
|
|
1193
|
+
if (d < best) {
|
|
1194
|
+
best = d;
|
|
1195
|
+
const l = Math.sqrt(l2) || 1;
|
|
1196
|
+
sx = dx / l;
|
|
1197
|
+
sz = dz / l;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (best > half + 0.02) continue;
|
|
1201
|
+
const sin = Math.abs(tx * sz - tz * sx);
|
|
1202
|
+
if (sin < 0.2) continue;
|
|
1203
|
+
const lo = Math.max(w.elevation, other.shape.elevation);
|
|
1204
|
+
const hi = Math.min(w.elevation + w.height, other.shape.elevation + other.shape.height);
|
|
1205
|
+
if (hi <= lo) continue;
|
|
1206
|
+
if (lo < y0) y0 = lo;
|
|
1207
|
+
if (hi > y1) y1 = hi;
|
|
1208
|
+
covered = Math.max(covered, (half - best) / sin);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (y1 <= y0) return false;
|
|
1212
|
+
out[0] = y0;
|
|
1213
|
+
out[1] = y1;
|
|
1214
|
+
out[2] = covered;
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
function updateContacts(state) {
|
|
1219
|
+
if (!state.contact.enabled) {
|
|
1220
|
+
state.contactWalls.clear();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const scratch = state.contactScratch;
|
|
1224
|
+
const list = state.sortedOpenings;
|
|
1225
|
+
env.state = state;
|
|
1226
|
+
for (const e of state.contactWalls) {
|
|
1227
|
+
const rec = state.walls.get(e);
|
|
1228
|
+
if (!rec) continue;
|
|
1229
|
+
list.length = 0;
|
|
1230
|
+
for (const o of state.byWall.get(e) ?? []) {
|
|
1231
|
+
const r = state.openings.get(o);
|
|
1232
|
+
if (r) list.push(r);
|
|
1233
|
+
}
|
|
1234
|
+
list.sort((p, q) => p.offset - q.offset);
|
|
1235
|
+
env.entity = e;
|
|
1236
|
+
env.rec = rec;
|
|
1237
|
+
contactQuads(rec.shape, list, state.contact, env, scratch);
|
|
1238
|
+
if (rec.contact.equals(scratch) && sameGroup(state, rec)) continue;
|
|
1239
|
+
for (const key of rec.contactChunks) {
|
|
1240
|
+
state.dirtyContact.add(key);
|
|
1241
|
+
state.chunks.get(key)?.contacts.delete(e);
|
|
1242
|
+
}
|
|
1243
|
+
rec.contact.copy(scratch);
|
|
1244
|
+
contactChunks(rec.contact, state.chunkSize, state.overlap);
|
|
1245
|
+
groupKeys(state, rec.group, rec.contactChunks);
|
|
1246
|
+
for (const key of rec.contactChunks) {
|
|
1247
|
+
state.chunk(key).contacts.add(e);
|
|
1248
|
+
state.dirtyContact.add(key);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
state.contactWalls.clear();
|
|
1252
|
+
}
|
|
1253
|
+
function sameGroup(state, rec) {
|
|
1254
|
+
for (const key of rec.contactChunks)
|
|
1255
|
+
return state.groups[Math.floor(key / GROUP_STRIDE)] === rec.group;
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
function rebuildContact(state, chunk) {
|
|
1259
|
+
const world = state.world;
|
|
1260
|
+
const b = state.contactBuilder;
|
|
1261
|
+
b.reset();
|
|
1262
|
+
if (state.contact.enabled) {
|
|
1263
|
+
const size = state.chunkSize;
|
|
1264
|
+
const minX = chunk.x * size;
|
|
1265
|
+
const minZ = chunk.z * size;
|
|
1266
|
+
for (const e of chunk.contacts) {
|
|
1267
|
+
const rec = state.walls.get(e);
|
|
1268
|
+
if (rec) emitContact(rec.contact, minX, minZ, minX + size, minZ + size, b);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const cm = chunk.contact;
|
|
1272
|
+
if (b.vertexCount === 0) {
|
|
1273
|
+
if (cm) dropChunkMesh(world, cm);
|
|
1274
|
+
chunk.contact = null;
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const data = meshData(b);
|
|
1278
|
+
if (cm && world.isAlive(cm.entity)) {
|
|
1279
|
+
cm.mesh.update(data);
|
|
1280
|
+
cm.triangles = b.indexCount / 3;
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (cm) dropChunkMesh(world, cm);
|
|
1284
|
+
const mesh = Mesh.create(data);
|
|
1285
|
+
const ref = world.resource(Meshes).add(mesh, `structure:contact/${chunk.group}/${chunk.x},${chunk.z}`);
|
|
1286
|
+
const entity = world.spawn(
|
|
1287
|
+
[Mesh3d, { mesh: ref }],
|
|
1288
|
+
[MeshMaterial, { material: state.contactMaterial }],
|
|
1289
|
+
[StructureChunk, { group: chunk.group, x: chunk.x, z: chunk.z }],
|
|
1290
|
+
[ChildOf, { parent: chunk.group }],
|
|
1291
|
+
ContactMesh,
|
|
1292
|
+
NotShadowCaster,
|
|
1293
|
+
Transform,
|
|
1294
|
+
Derived
|
|
1295
|
+
);
|
|
1296
|
+
chunk.contact = { entity, mesh, ref, triangles: b.indexCount / 3 };
|
|
1297
|
+
}
|
|
1298
|
+
function dropContacts(state) {
|
|
1299
|
+
for (const [key, chunk] of state.chunks) {
|
|
1300
|
+
if (chunk.contact) dropChunkMesh(state.world, chunk.contact);
|
|
1301
|
+
chunk.contact = null;
|
|
1302
|
+
chunk.contacts.clear();
|
|
1303
|
+
dropIfEmpty(state, key, chunk);
|
|
1304
|
+
}
|
|
1305
|
+
for (const rec of state.walls.values()) {
|
|
1306
|
+
rec.contact.reset();
|
|
1307
|
+
rec.contactChunks.clear();
|
|
1308
|
+
}
|
|
1309
|
+
state.dirtyContact.clear();
|
|
1310
|
+
state.contactWalls.clear();
|
|
1311
|
+
}
|
|
1312
|
+
function contactSettingsChanged(state, settings) {
|
|
1313
|
+
let c = settings.contact;
|
|
1314
|
+
if (c !== state.contactSeen) {
|
|
1315
|
+
c = { ...state.contact, ...c };
|
|
1316
|
+
settings.contact = c;
|
|
1317
|
+
state.contactSeen = c;
|
|
1318
|
+
}
|
|
1319
|
+
const a = state.contact;
|
|
1320
|
+
return c.enabled !== a.enabled || c.floorReach !== a.floorReach || c.cornerReach !== a.cornerReach || c.opacity !== a.opacity || c.maxAlpha !== a.maxAlpha || c.wobble !== a.wobble || c.color[0] !== a.color[0] || c.color[1] !== a.color[1] || c.color[2] !== a.color[2];
|
|
1321
|
+
}
|
|
1322
|
+
function applyContactSettings(state, next) {
|
|
1323
|
+
const prev = state.contact;
|
|
1324
|
+
state.contact = { ...next, color: [next.color[0], next.color[1], next.color[2]] };
|
|
1325
|
+
const material = state.contactMaterial ? state.world.resource(Materials).get(state.contactMaterial) : void 0;
|
|
1326
|
+
material?.set({
|
|
1327
|
+
opacity: next.opacity,
|
|
1328
|
+
maxAlpha: next.maxAlpha,
|
|
1329
|
+
color: [next.color[0], next.color[1], next.color[2]],
|
|
1330
|
+
wobble: next.wobble
|
|
1331
|
+
});
|
|
1332
|
+
if (!next.enabled) {
|
|
1333
|
+
if (prev.enabled) dropContacts(state);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (!prev.enabled || prev.floorReach !== next.floorReach || prev.cornerReach !== next.cornerReach)
|
|
1337
|
+
for (const e of state.walls.keys()) state.contactWalls.add(e);
|
|
1338
|
+
}
|
|
1339
|
+
function dropChunkMesh(world, cm) {
|
|
1340
|
+
if (world.isAlive(cm.entity)) world.despawn(cm.entity);
|
|
1341
|
+
world.tryResource(GpuAssetsResource)?.releaseMesh(cm.mesh);
|
|
1342
|
+
world.resource(Meshes).delete(cm.ref.guid);
|
|
1343
|
+
}
|
|
1344
|
+
function meshData(b) {
|
|
1345
|
+
const v = b.vertexCount;
|
|
1346
|
+
return {
|
|
1347
|
+
positions: b.positions.slice(0, v * 3),
|
|
1348
|
+
normals: b.normals.slice(0, v * 3),
|
|
1349
|
+
uvs: b.uvs.slice(0, v * 2),
|
|
1350
|
+
tangents: b.tangents.slice(0, v * 4),
|
|
1351
|
+
indices: v < 65536 ? Uint16Array.from(b.indices.subarray(0, b.indexCount)) : b.indices.slice(0, b.indexCount)
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function applyCutawayWalls(state, on) {
|
|
1355
|
+
state.cutawayWalls = on;
|
|
1356
|
+
for (const [key, chunk] of state.chunks) if (chunk.walls.size > 0) state.dirtyChunks.add(key);
|
|
1357
|
+
for (const rec of state.openings.values()) setTag(state, rec.leaf, Cutaway, on);
|
|
1358
|
+
}
|
|
1359
|
+
function resetChunks(state) {
|
|
1360
|
+
for (const chunk of state.chunks.values()) {
|
|
1361
|
+
for (const cm of chunk.meshes.values()) dropChunkMesh(state.world, cm);
|
|
1362
|
+
if (chunk.contact) dropChunkMesh(state.world, chunk.contact);
|
|
1363
|
+
}
|
|
1364
|
+
state.chunks.clear();
|
|
1365
|
+
state.dirtyContact.clear();
|
|
1366
|
+
for (const [entity, rec] of state.walls) {
|
|
1367
|
+
rec.chunks.clear();
|
|
1368
|
+
rec.contact.reset();
|
|
1369
|
+
rec.contactChunks.clear();
|
|
1370
|
+
state.dirtyWalls.add(entity);
|
|
1371
|
+
}
|
|
1372
|
+
for (const [entity, rec] of state.slabs) {
|
|
1373
|
+
rec.chunks.clear();
|
|
1374
|
+
state.dirtySlabs.add(entity);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function dirtyLevel(state, level) {
|
|
1378
|
+
for (const [e, rec] of state.walls) if (rec.level === level) state.dirtyWalls.add(e);
|
|
1379
|
+
for (const [e, rec] of state.slabs) if (rec.level === level) state.dirtySlabs.add(e);
|
|
1380
|
+
}
|
|
1381
|
+
const compileStructure = defineSystem({
|
|
1382
|
+
name: "structure/compile",
|
|
1383
|
+
description: "Rebuilds the chunk meshes that changed walls, openings, floors, roofs and cutouts overlap (old and new geometry), per group.",
|
|
1384
|
+
setup: (world) => ({
|
|
1385
|
+
walls: world.query({ with: [Wall] }),
|
|
1386
|
+
floors: world.query({ with: [Floor] }),
|
|
1387
|
+
roofs: world.query({ with: [Roof] }),
|
|
1388
|
+
openings: world.query({ with: [Opening] }),
|
|
1389
|
+
cutouts: world.query({ with: [Cutout] }),
|
|
1390
|
+
levels: world.query({ with: [Level] })
|
|
1391
|
+
}),
|
|
1392
|
+
run: ({ walls, floors, roofs, openings, cutouts, levels }, world, ctx) => {
|
|
1393
|
+
const state = world.resource(Structure);
|
|
1394
|
+
const since = ctx.lastRunTick;
|
|
1395
|
+
const settings = world.resource(StructureSettings);
|
|
1396
|
+
const size = settings.chunkSize;
|
|
1397
|
+
const tolerance = settings.curveTolerance;
|
|
1398
|
+
const contact = contactSettingsChanged(state, settings);
|
|
1399
|
+
const cutawayWalls = settings.cutawayWalls === true;
|
|
1400
|
+
let any = contact || cutawayWalls !== state.cutawayWalls || state.removedWalls.length + state.removedSlabs.length + state.removedOpenings.length + state.removedCutouts.length + state.removedLevels.length > 0 || size !== state.chunkSize || tolerance !== state.curveTolerance;
|
|
1401
|
+
if (!any) {
|
|
1402
|
+
for (let t = 0; t < walls.tables.length && !any; t++)
|
|
1403
|
+
if (walls.tables[t].lastChanged(Wall) > since) any = true;
|
|
1404
|
+
for (let t = 0; t < floors.tables.length && !any; t++)
|
|
1405
|
+
if (floors.tables[t].lastChanged(Floor) > since) any = true;
|
|
1406
|
+
for (let t = 0; t < roofs.tables.length && !any; t++)
|
|
1407
|
+
if (roofs.tables[t].lastChanged(Roof) > since) any = true;
|
|
1408
|
+
for (let t = 0; t < openings.tables.length && !any; t++)
|
|
1409
|
+
if (openings.tables[t].lastChanged(Opening) > since) any = true;
|
|
1410
|
+
for (let t = 0; t < cutouts.tables.length && !any; t++)
|
|
1411
|
+
if (cutouts.tables[t].lastChanged(Cutout) > since) any = true;
|
|
1412
|
+
for (let t = 0; t < levels.tables.length && !any; t++)
|
|
1413
|
+
if (levels.tables[t].lastChanged(Level) > since) any = true;
|
|
1414
|
+
}
|
|
1415
|
+
if (!any) return;
|
|
1416
|
+
const start = performance.now();
|
|
1417
|
+
if (contact) applyContactSettings(state, settings.contact);
|
|
1418
|
+
if (cutawayWalls !== state.cutawayWalls) applyCutawayWalls(state, cutawayWalls);
|
|
1419
|
+
if (size !== state.chunkSize || tolerance !== state.curveTolerance) {
|
|
1420
|
+
state.chunkSize = size;
|
|
1421
|
+
state.curveTolerance = tolerance;
|
|
1422
|
+
resetChunks(state);
|
|
1423
|
+
}
|
|
1424
|
+
for (const e of state.removedOpenings) forgetOpening(state, e);
|
|
1425
|
+
state.removedOpenings.length = 0;
|
|
1426
|
+
for (const e of state.removedCutouts) forgetCutout(state, e);
|
|
1427
|
+
state.removedCutouts.length = 0;
|
|
1428
|
+
for (const e of state.removedWalls) {
|
|
1429
|
+
const rec = state.walls.get(e);
|
|
1430
|
+
if (!rec) continue;
|
|
1431
|
+
markChunks(state, rec.chunks);
|
|
1432
|
+
queueContactNear(state, rec.chunks);
|
|
1433
|
+
for (const key of rec.chunks) state.chunks.get(key)?.walls.delete(e);
|
|
1434
|
+
for (const key of rec.contactChunks) {
|
|
1435
|
+
state.dirtyContact.add(key);
|
|
1436
|
+
state.chunks.get(key)?.contacts.delete(e);
|
|
1437
|
+
}
|
|
1438
|
+
state.walls.delete(e);
|
|
1439
|
+
for (const o of state.byWall.get(e) ?? []) placeLeaf(state, o);
|
|
1440
|
+
}
|
|
1441
|
+
state.removedWalls.length = 0;
|
|
1442
|
+
for (const e of state.removedSlabs) {
|
|
1443
|
+
const rec = state.slabs.get(e);
|
|
1444
|
+
if (!rec) continue;
|
|
1445
|
+
markChunks(state, rec.chunks);
|
|
1446
|
+
if (rec.kind === "floor") queueContactArea(state, rec.group, rec.shape);
|
|
1447
|
+
for (const key of rec.chunks) state.chunks.get(key)?.slabs.delete(e);
|
|
1448
|
+
for (const key of rec.index) state.roofIndex.get(key)?.delete(e);
|
|
1449
|
+
state.slabs.delete(e);
|
|
1450
|
+
for (const c of state.byHost.get(e) ?? []) dropLeaf(state, c);
|
|
1451
|
+
}
|
|
1452
|
+
state.removedSlabs.length = 0;
|
|
1453
|
+
for (const e of state.removedLevels) {
|
|
1454
|
+
state.levels.delete(e);
|
|
1455
|
+
dirtyLevel(state, e);
|
|
1456
|
+
}
|
|
1457
|
+
state.removedLevels.length = 0;
|
|
1458
|
+
for (const table of levels.tables) {
|
|
1459
|
+
if (table.lastChanged(Level) <= since) continue;
|
|
1460
|
+
const ticks = table.changedTicks(Level);
|
|
1461
|
+
const elevations = table.column(Level, "elevation");
|
|
1462
|
+
for (let row = 0; row < table.count; row++) {
|
|
1463
|
+
if (ticks[row] <= since) continue;
|
|
1464
|
+
const e = table.entities[row];
|
|
1465
|
+
const elevation = elevations[row];
|
|
1466
|
+
if (state.levels.get(e) === elevation) continue;
|
|
1467
|
+
state.levels.set(e, elevation);
|
|
1468
|
+
dirtyLevel(state, e);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
for (const table of openings.tables) {
|
|
1472
|
+
if (table.lastChanged(Opening) <= since) continue;
|
|
1473
|
+
const ticks = table.changedTicks(Opening);
|
|
1474
|
+
for (let row = 0; row < table.count; row++)
|
|
1475
|
+
if (ticks[row] > since) readOpening(state, table.entities[row]);
|
|
1476
|
+
}
|
|
1477
|
+
for (const table of cutouts.tables) {
|
|
1478
|
+
if (table.lastChanged(Cutout) <= since) continue;
|
|
1479
|
+
const ticks = table.changedTicks(Cutout);
|
|
1480
|
+
for (let row = 0; row < table.count; row++)
|
|
1481
|
+
if (ticks[row] > since) readCutout(state, table.entities[row]);
|
|
1482
|
+
}
|
|
1483
|
+
for (const table of walls.tables) {
|
|
1484
|
+
if (table.lastChanged(Wall) <= since) continue;
|
|
1485
|
+
const ticks = table.changedTicks(Wall);
|
|
1486
|
+
for (let row = 0; row < table.count; row++)
|
|
1487
|
+
if (ticks[row] > since) state.dirtyWalls.add(table.entities[row]);
|
|
1488
|
+
}
|
|
1489
|
+
for (const q of [floors, roofs]) {
|
|
1490
|
+
const def = q === floors ? Floor : Roof;
|
|
1491
|
+
for (const table of q.tables) {
|
|
1492
|
+
if (table.lastChanged(def) <= since) continue;
|
|
1493
|
+
const ticks = table.changedTicks(def);
|
|
1494
|
+
for (let row = 0; row < table.count; row++)
|
|
1495
|
+
if (ticks[row] > since) state.dirtySlabs.add(table.entities[row]);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
for (const e of state.dirtyWalls)
|
|
1499
|
+
if (world.isAlive(e) && world.has(e, Wall)) evaluateWall(state, e);
|
|
1500
|
+
state.dirtyWalls.clear();
|
|
1501
|
+
for (const e of state.dirtySlabs)
|
|
1502
|
+
if (world.isAlive(e) && (world.has(e, Floor) || world.has(e, Roof))) evaluateSlab(state, e);
|
|
1503
|
+
state.dirtySlabs.clear();
|
|
1504
|
+
updateContacts(state);
|
|
1505
|
+
if (state.dirtyChunks.size === 0 && state.dirtyContact.size === 0) return;
|
|
1506
|
+
const dirty = [];
|
|
1507
|
+
const groups = [];
|
|
1508
|
+
let meshes = 0;
|
|
1509
|
+
const keys = [...state.dirtyChunks].sort((a, b) => a - b);
|
|
1510
|
+
for (const key of keys) {
|
|
1511
|
+
const group = state.groups[Math.floor(key / GROUP_STRIDE)];
|
|
1512
|
+
meshes += rebuildChunk(state, key);
|
|
1513
|
+
const plain = key % GROUP_STRIDE;
|
|
1514
|
+
dirty.push([chunkX(plain), chunkZ(plain)]);
|
|
1515
|
+
groups.push(group);
|
|
1516
|
+
}
|
|
1517
|
+
const contactDirty = [];
|
|
1518
|
+
for (const key of state.dirtyContact) {
|
|
1519
|
+
if (state.dirtyChunks.has(key)) continue;
|
|
1520
|
+
const chunk = state.chunks.get(key);
|
|
1521
|
+
if (!chunk) continue;
|
|
1522
|
+
rebuildContact(state, chunk);
|
|
1523
|
+
dropIfEmpty(state, key, chunk);
|
|
1524
|
+
contactDirty.push([chunk.x, chunk.z]);
|
|
1525
|
+
}
|
|
1526
|
+
state.dirtyContact.clear();
|
|
1527
|
+
state.dirtyChunks.clear();
|
|
1528
|
+
state.last = {
|
|
1529
|
+
dirtyChunks: dirty,
|
|
1530
|
+
dirtyGroups: groups,
|
|
1531
|
+
chunksRebuilt: dirty.length,
|
|
1532
|
+
meshesRebuilt: meshes,
|
|
1533
|
+
contactRebuilt: contactDirty.length,
|
|
1534
|
+
contactChunks: contactDirty,
|
|
1535
|
+
ms: performance.now() - start
|
|
1536
|
+
};
|
|
1537
|
+
state.chunksRebuilt += dirty.length;
|
|
1538
|
+
state.compiles++;
|
|
1539
|
+
const stats = world.tryResource(RenderStats);
|
|
1540
|
+
if (stats) stats.current.chunksRebuilt += dirty.length;
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
function roofAt(world, x, z, level) {
|
|
1544
|
+
const state = world.resource(Structure);
|
|
1545
|
+
const size = state.chunkSize || world.resource(StructureSettings).chunkSize;
|
|
1546
|
+
const set = state.roofIndex.get(chunkKey(Math.floor(x / size), Math.floor(z / size)));
|
|
1547
|
+
if (!set) return void 0;
|
|
1548
|
+
let best;
|
|
1549
|
+
for (const e of set) {
|
|
1550
|
+
if (best !== void 0 && e > best) continue;
|
|
1551
|
+
const rec = state.slabs.get(e);
|
|
1552
|
+
if (!rec) continue;
|
|
1553
|
+
if (level !== void 0) {
|
|
1554
|
+
const on = state.groupOf(rec.level) === state.ground ? null : rec.level;
|
|
1555
|
+
if (on !== level) continue;
|
|
1556
|
+
}
|
|
1557
|
+
const p = rec.shape.points;
|
|
1558
|
+
if (insideRing(p, 0, rec.shape.rings[1], x, z, true)) best = e;
|
|
1559
|
+
}
|
|
1560
|
+
return best;
|
|
1561
|
+
}
|
|
1562
|
+
let swingState;
|
|
1563
|
+
let swingStep = 0;
|
|
1564
|
+
function swingOne(entity) {
|
|
1565
|
+
const state = swingState;
|
|
1566
|
+
const rec = state.openings.get(entity) ?? state.cutouts.get(entity);
|
|
1567
|
+
if (!rec) {
|
|
1568
|
+
state.moving.delete(entity);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
rec.open = rec.target > rec.open ? Math.min(rec.target, rec.open + swingStep) : Math.max(rec.target, rec.open - swingStep);
|
|
1572
|
+
if (state.openings.has(entity)) placeLeaf(state, entity);
|
|
1573
|
+
else placeHatch(state, entity);
|
|
1574
|
+
if (rec.open === rec.target) state.moving.delete(entity);
|
|
1575
|
+
}
|
|
1576
|
+
const swingDoors = defineSystem({
|
|
1577
|
+
name: "structure/doors",
|
|
1578
|
+
description: "Animates door and hatch leaves toward open or closed, holding a frame demand while they move.",
|
|
1579
|
+
run: (_, world) => {
|
|
1580
|
+
const state = world.resource(Structure);
|
|
1581
|
+
const demand = world.tryResource(FrameDemand);
|
|
1582
|
+
if (state.moving.size === 0) {
|
|
1583
|
+
demand?.set(DOOR_DEMAND, false);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const settings = world.resource(StructureSettings);
|
|
1587
|
+
swingState = state;
|
|
1588
|
+
swingStep = settings.reducedMotion ? 1 : world.resource(Time).delta / Math.max(1e-3, settings.doorSwingMs / 1e3);
|
|
1589
|
+
state.moving.forEach(swingOne);
|
|
1590
|
+
demand?.set(DOOR_DEMAND, state.moving.size > 0);
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
export {
|
|
1594
|
+
DOOR_DEMAND,
|
|
1595
|
+
FRAME_KEY,
|
|
1596
|
+
Structure,
|
|
1597
|
+
StructureState,
|
|
1598
|
+
compileStructure,
|
|
1599
|
+
observeRemovals,
|
|
1600
|
+
roofAt,
|
|
1601
|
+
swingDoors
|
|
1602
|
+
};
|