@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @aethervtt/shard-structure
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1,15 @@
1
+ import type { GpuContext } from '@aethervtt/shard-gpu';
2
+ /** One measurement against its budget (0055's acceptance criteria). */
3
+ export interface BenchResult {
4
+ name: string;
5
+ value: number;
6
+ unit: 'ms' | 'bytes' | 'chunks' | 'maps';
7
+ budget: number;
8
+ pass: boolean;
9
+ }
10
+ /**
11
+ * Runs the structure fixtures (Aether's shadow-stress and max scenes) and reports each budget:
12
+ * mirror sync, max build, wall edit compile, token move and door swing uploads, and shadow maps
13
+ * on a still frame. `shard bench structure` prints these.
14
+ */
15
+ export declare function runStructureBench(gpu: GpuContext): Promise<BenchResult[]>;
package/dist/bench.js ADDED
@@ -0,0 +1,112 @@
1
+ import { createMirror } from "@aethervtt/shard-mirror";
2
+ import { RenderStats } from "@aethervtt/shard-render";
3
+ import { settle } from "@aethervtt/shard-render/testing";
4
+ import { Structure } from "./compile.js";
5
+ import { DoorLeaf } from "./components.js";
6
+ import { maxScene, shadowStress } from "./fixtures.js";
7
+ import { rig } from "./harness.js";
8
+ const median = (values) => [...values].sort((a, b) => a - b)[values.length >> 1];
9
+ function result(name, value, unit, budget) {
10
+ return { name, value, unit, budget, pass: value <= budget };
11
+ }
12
+ function empty(docs) {
13
+ return { ...docs, materials: [], walls: [], openings: [], floors: [], props: [], tokens: [] };
14
+ }
15
+ async function runStructureBench(gpu) {
16
+ const out = [];
17
+ {
18
+ const r = await rig(gpu, { shadows: false });
19
+ const docs = shadowStress();
20
+ const mirror = createMirror(r.app.world, {
21
+ key: (d) => d.id,
22
+ rev: (d) => d.rev,
23
+ spawn: (_d, w) => w.spawn(),
24
+ apply() {
25
+ }
26
+ });
27
+ mirror.sync(docs.walls);
28
+ for (let i = 0; i < 200; i++) mirror.sync(docs.walls);
29
+ const times = [];
30
+ for (let i = 0; i < 400; i++) {
31
+ const t0 = performance.now();
32
+ mirror.sync(docs.walls);
33
+ times.push(performance.now() - t0);
34
+ }
35
+ out.push(result("mirror.sync of 5,000 unchanged walls", median(times), "ms", 0.2));
36
+ await r.dispose();
37
+ }
38
+ {
39
+ const r = await rig(gpu, { shadows: false });
40
+ const docs = maxScene();
41
+ const times = [];
42
+ for (let i = 0; i < 4; i++) {
43
+ const t0 = performance.now();
44
+ r.host.sync(docs);
45
+ r.frame();
46
+ times.push(performance.now() - t0);
47
+ r.host.sync(empty(docs));
48
+ r.frame();
49
+ }
50
+ times.shift();
51
+ out.push(result("max fixture build (sync, compile, first frame)", median(times), "ms", 300));
52
+ await r.dispose();
53
+ }
54
+ {
55
+ const r = await rig(gpu, { shadowUpdate: "on-change" });
56
+ const docs = shadowStress();
57
+ r.host.sync(docs);
58
+ r.look([10, 20, 24], [10, 0, 8]);
59
+ await settle(r.app);
60
+ for (let i = 0; i < 4; i++) r.frame();
61
+ const world = r.app.world;
62
+ const stats = world.resource(RenderStats);
63
+ const state = world.resource(Structure);
64
+ r.frame();
65
+ out.push(
66
+ result("shadow maps drawn on a still frame", stats.lastFrame.shadowMapsRendered, "maps", 0)
67
+ );
68
+ const token = docs.tokens[0];
69
+ docs.tokens[0] = { ...token, rev: token.rev + 1, x: token.x + 70 };
70
+ r.host.sync(docs);
71
+ r.frame();
72
+ out.push(result("token move: scene bytes", stats.lastFrame.sceneBytes, "bytes", 112));
73
+ r.frame();
74
+ const door = docs.openings[0];
75
+ docs.openings[0] = { ...door, rev: door.rev + 1, state: "open" };
76
+ r.host.sync(docs);
77
+ let worstBytes = 0;
78
+ let rebuilt = 0;
79
+ const leaf = world.query({ with: [DoorLeaf] }).entities().find((e) => world.get(e, DoorLeaf).opening === r.host.openings.entity(door.id));
80
+ for (let i = 0; i < 40 && world.get(leaf, DoorLeaf).angle < 1; i++) {
81
+ r.frame();
82
+ worstBytes = Math.max(worstBytes, stats.lastFrame.sceneBytes);
83
+ rebuilt += stats.lastFrame.chunksRebuilt;
84
+ }
85
+ out.push(result("door swing: chunks rebuilt", rebuilt, "chunks", 0));
86
+ out.push(result("door swing: scene bytes per frame", worstBytes, "bytes", 112));
87
+ const k = r.host.k;
88
+ const times = [];
89
+ for (let i = 0; i < 12; i++) {
90
+ const index = 100 + i * 211;
91
+ const old = docs.walls[index];
92
+ const ax = old.a.x + 35;
93
+ const ay = old.a.y + 20;
94
+ const angle = i * 0.61;
95
+ docs.walls[index] = {
96
+ ...old,
97
+ rev: old.rev + 1,
98
+ a: { x: ax, y: ay },
99
+ b: { x: ax + Math.cos(angle) * 3 / k, y: ay + Math.sin(angle) * 3 / k }
100
+ };
101
+ r.host.sync(docs);
102
+ r.frame();
103
+ times.push(state.last.ms);
104
+ }
105
+ out.push(result("3 m wall edit: compile", median(times), "ms", 4));
106
+ await r.dispose();
107
+ }
108
+ return out;
109
+ }
110
+ export {
111
+ runStructureBench
112
+ };
@@ -0,0 +1,27 @@
1
+ import type { AssetRef, World } from '@aethervtt/shard-core';
2
+ import { Texture } from '@aethervtt/shard-texture';
3
+ export interface BrickOptions {
4
+ seed?: number;
5
+ /** Brick length and height, metres. Default 0.4 × 0.2 (a VTT-scale block). */
6
+ brick?: [number, number];
7
+ /** Mortar joint width, metres. Default 0.025. */
8
+ mortar?: number;
9
+ /** Brick colour (linear) and how much each brick varies from it. */
10
+ color?: [number, number, number];
11
+ variation?: number;
12
+ mortarColor?: [number, number, number];
13
+ /** Texels across one tile. Default 512. */
14
+ resolution?: number;
15
+ }
16
+ /** The brick textures (albedo, normal, roughness), and the tile's size in metres. */
17
+ export declare function brickTextures(options?: BrickOptions): {
18
+ albedo: Texture;
19
+ normal: Texture;
20
+ metallicRoughness: Texture;
21
+ tile: [number, number];
22
+ };
23
+ /**
24
+ * A brick material: its textures in the world's Textures store, tiled so one brick is
25
+ * `options.brick` metres on structure's metre UVs.
26
+ */
27
+ export declare function brickMaterial(world: World, options?: BrickOptions): AssetRef<'Material'>;
package/dist/bricks.js ADDED
@@ -0,0 +1,98 @@
1
+ import { MaterialAsset, Materials } from "@aethervtt/shard-render";
2
+ import { packMetallicRoughness, Texture, Textures } from "@aethervtt/shard-texture";
3
+ function hash(x, y, seed) {
4
+ let h = x * 374761393 + y * 668265263 + seed * 2246822519 >>> 0;
5
+ h = Math.imul(h ^ h >>> 13, 1274126177) >>> 0;
6
+ return ((h ^ h >>> 16) >>> 0) / 4294967296;
7
+ }
8
+ const toSrgb = (c) => Math.round(255 * Math.min(1, c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055));
9
+ function brickTextures(options = {}) {
10
+ const seed = options.seed ?? 1;
11
+ const [bw, bh] = options.brick ?? [0.4, 0.2];
12
+ const mortar = options.mortar ?? 0.025;
13
+ const color = options.color ?? [0.36, 0.13, 0.07];
14
+ const variation = options.variation ?? 0.25;
15
+ const mortarColor = options.mortarColor ?? [0.42, 0.4, 0.36];
16
+ const res = options.resolution ?? 512;
17
+ const tileW = bw * 2;
18
+ const tileH = bh * 4;
19
+ const w = res;
20
+ const h = Math.max(4, Math.round(res * tileH / tileW));
21
+ const height = new Float32Array(w * h);
22
+ const albedo = new Uint8Array(w * h * 4);
23
+ const rough = new Uint8Array(w * h * 4);
24
+ for (let row = 0; row < h; row++) {
25
+ const y = (h - row - 0.5) / h * tileH;
26
+ const course = Math.floor(y / bh);
27
+ const shift = course % 2 === 1 ? bw / 2 : 0;
28
+ for (let col = 0; col < w; col++) {
29
+ const x = (col + 0.5) / w * tileW;
30
+ const bx = x + shift;
31
+ const brick = Math.floor(bx / bw);
32
+ const fx = bx - brick * bw;
33
+ const fy = y - course * bh;
34
+ const edge = Math.min(fx, bw - fx, fy, bh - fy) - mortar / 2;
35
+ const inBrick = edge > 0;
36
+ const bevel = Math.min(1, Math.max(0, edge / (mortar * 0.8)));
37
+ const noise = hash(col, row, seed + 7) * 0.08;
38
+ const i = row * w + col;
39
+ height[i] = inBrick ? 0.6 + 0.4 * bevel + noise * bevel : hash(col, row, seed + 11) * 0.05;
40
+ const id = (brick % 2 + 2) % 2;
41
+ const tint = 1 + (hash(id, course % 4, seed) - 0.5) * 2 * variation;
42
+ const speck = 1 + (hash(col, row, seed + 3) - 0.5) * 0.12;
43
+ const c = inBrick ? color.map((v) => v * tint * speck) : mortarColor.map((v) => v * speck);
44
+ albedo[i * 4] = toSrgb(c[0]);
45
+ albedo[i * 4 + 1] = toSrgb(c[1]);
46
+ albedo[i * 4 + 2] = toSrgb(c[2]);
47
+ albedo[i * 4 + 3] = 255;
48
+ const r = inBrick ? 0.78 + noise : 0.95;
49
+ rough[i * 4] = Math.round(255 * Math.min(1, r));
50
+ }
51
+ }
52
+ const normal = new Uint8Array(w * h * 4);
53
+ const strength = 2.5;
54
+ const at = (c, r) => height[(r + h) % h * w + (c + w) % w];
55
+ for (let row = 0; row < h; row++) {
56
+ for (let col = 0; col < w; col++) {
57
+ const dx = (at(col + 1, row) - at(col - 1, row)) * strength;
58
+ const dUp = (at(col, row - 1) - at(col, row + 1)) * strength;
59
+ const l = Math.sqrt(dx * dx + dUp * dUp + 1);
60
+ const i = (row * w + col) * 4;
61
+ normal[i] = Math.round((-dx / l * 0.5 + 0.5) * 255);
62
+ normal[i + 1] = Math.round((-dUp / l * 0.5 + 0.5) * 255);
63
+ normal[i + 2] = Math.round((1 / l * 0.5 + 0.5) * 255);
64
+ normal[i + 3] = 255;
65
+ }
66
+ }
67
+ return {
68
+ albedo: Texture.create({ width: w, height: h, usage: "color", mips: [albedo], mipmaps: true }),
69
+ normal: Texture.create({ width: w, height: h, usage: "normal", mips: [normal], mipmaps: true }),
70
+ metallicRoughness: packMetallicRoughness({ width: w, height: h, kind: "u8", data: rough }),
71
+ tile: [tileW, tileH]
72
+ };
73
+ }
74
+ function brickMaterial(world, options = {}) {
75
+ const t = brickTextures(options);
76
+ const textures = world.resource(Textures);
77
+ const name = `bricks/${options.seed ?? 1}`;
78
+ const slot = (texture, suffix) => ({
79
+ texture: textures.add(texture, `${name}/${suffix}`),
80
+ scale: [1 / t.tile[0], 1 / t.tile[1]],
81
+ wrap: "repeat"
82
+ });
83
+ return world.resource(Materials).add(
84
+ new MaterialAsset({
85
+ baseColor: [1, 1, 1, 1],
86
+ roughness: 1,
87
+ metallic: 0,
88
+ baseColorTexture: slot(t.albedo, "albedo"),
89
+ normalTexture: slot(t.normal, "normal"),
90
+ metallicRoughnessTexture: slot(t.metallicRoughness, "roughness")
91
+ }),
92
+ name
93
+ );
94
+ }
95
+ export {
96
+ brickMaterial,
97
+ brickTextures
98
+ };
@@ -0,0 +1,292 @@
1
+ import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
2
+ import { Mesh } from '@aethervtt/shard-mesh';
3
+ import { type ContactSettings } from './components.js';
4
+ import { ContactQuads } from './contact.js';
5
+ import { ClipScratch, type FloorShape, MeshBuilder, type OpeningShape, type Piece, type WallShape } from './geometry.js';
6
+ type MaterialRef = AssetRef<'Material'>;
7
+ /** The key of the plain frame material: frames and door leaves without one of their own. */
8
+ export declare const FRAME_KEY = "structure:frame";
9
+ interface WallRecord {
10
+ shape: WallShape;
11
+ material: string;
12
+ pieces: Piece[];
13
+ count: number;
14
+ /** Material key of each piece. */
15
+ pieceMaterials: string[];
16
+ /** (group, chunk) keys its geometry overlaps. */
17
+ chunks: Set<number>;
18
+ /** The Level it names (null: none), and the group it draws into (the ground level if the
19
+ * entity it names isn't a Level). */
20
+ level: Entity | null;
21
+ group: Entity;
22
+ /** Contact shade quads (0068), and the (group, chunk) keys they have area in. */
23
+ contact: ContactQuads;
24
+ contactChunks: Set<number>;
25
+ }
26
+ /** A floor or a roof: an outline with its cutouts as holes (0067). */
27
+ interface SlabRecord {
28
+ kind: 'floor' | 'roof';
29
+ shape: FloorShape;
30
+ material: string;
31
+ /** (group, chunk) keys. */
32
+ chunks: Set<number>;
33
+ level: Entity | null;
34
+ group: Entity;
35
+ /** Accepted cutouts in ring order (ring i + 1), and the material key of each framed one. */
36
+ cutouts: Entity[];
37
+ frameMaterials: string[];
38
+ /** Roofs: keeps casting while hidden. */
39
+ shadowWhenHidden: boolean;
40
+ /** Roofs: its meshes are Cutaway (0070). */
41
+ cutaway: boolean;
42
+ /** Roofs: chunk keys of its footprint's bounds, for `roofAt`. */
43
+ index: number[];
44
+ /** Everything but its cutouts, and each accepted cutout's outline and frame: when only cutouts
45
+ * change, only the chunks they cover rebuild. */
46
+ signature: string;
47
+ rings: Map<Entity, string>;
48
+ }
49
+ interface OpeningRecord {
50
+ wall: Entity;
51
+ kind: 'door' | 'window';
52
+ offset: number;
53
+ width: number;
54
+ height: number;
55
+ sill: number;
56
+ frameWidth: number;
57
+ frameDepth: number;
58
+ frameMaterial: string;
59
+ hinge: 'start' | 'end';
60
+ swing: 'left' | 'right';
61
+ state: 'closed' | 'open' | 'locked';
62
+ /** The door leaf or window pane. */
63
+ leaf: Entity | null;
64
+ /** How far open the leaf is (0 to 1), and where it's heading. */
65
+ open: number;
66
+ target: number;
67
+ }
68
+ interface CutoutRecord {
69
+ host: Entity;
70
+ kind: 'hole' | 'hatch' | 'skylight';
71
+ /** The outline, flat (x, z) pairs. */
72
+ points: Float64Array;
73
+ frameWidth: number;
74
+ frameDepth: number;
75
+ frameMaterial: string;
76
+ hinge: number;
77
+ state: 'closed' | 'open' | 'locked';
78
+ /** Its ring in its host's shape, or -1 when its host skipped it. */
79
+ ring: number;
80
+ /** The hatch leaf or skylight pane, and its mesh. */
81
+ leaf: Entity | null;
82
+ mesh: Mesh | null;
83
+ meshRef: AssetRef<'Mesh'> | null;
84
+ /** Hatches: the hinge (x, y, z), its axis (unit), and the swing's sign. */
85
+ pivot: [number, number, number, number, number, number, number];
86
+ open: number;
87
+ target: number;
88
+ }
89
+ interface ChunkMesh {
90
+ entity: Entity;
91
+ mesh: Mesh;
92
+ ref: AssetRef<'Mesh'>;
93
+ triangles: number;
94
+ }
95
+ interface ChunkRecord {
96
+ group: Entity;
97
+ x: number;
98
+ z: number;
99
+ walls: Set<Entity>;
100
+ slabs: Set<Entity>;
101
+ meshes: Map<string, ChunkMesh>;
102
+ /** Walls with contact shade here, and the chunk's contact mesh (0068). */
103
+ contacts: Set<Entity>;
104
+ contact: ChunkMesh | null;
105
+ }
106
+ /** What the last compile that did something did (`structure.describe`). */
107
+ export interface CompileReport {
108
+ /** Chunks rebuilt, as `[x, z]`, and the group of each. */
109
+ dirtyChunks: [number, number][];
110
+ dirtyGroups: Entity[];
111
+ chunksRebuilt: number;
112
+ meshesRebuilt: number;
113
+ /** Chunks whose contact mesh alone was rebuilt (0068): a floor edit, a contact setting. */
114
+ contactRebuilt: number;
115
+ /** Those chunks, as `[x, z]`. */
116
+ contactChunks: [number, number][];
117
+ ms: number;
118
+ }
119
+ /** Frame demand held while a door or hatch swings (0052). */
120
+ export declare const DOOR_DEMAND = "structure/doors";
121
+ /**
122
+ * Structure's state: the geometry of every wall, opening, floor, roof and cutout, which (group,
123
+ * chunk) pairs each overlaps, and the per-material meshes of every one. `structure/compile` keeps
124
+ * it current. A group is a Level, a Roof, or the ground level (`ground`); its chunk meshes are its
125
+ * children, so hiding it is one Visibility write (0067).
126
+ */
127
+ export declare class StructureState {
128
+ readonly walls: Map<number, WallRecord>;
129
+ readonly slabs: Map<number, SlabRecord>;
130
+ readonly openings: Map<number, OpeningRecord>;
131
+ readonly cutouts: Map<number, CutoutRecord>;
132
+ /** Openings by host wall entity (including hosts that don't exist yet). */
133
+ readonly byWall: Map<number, Set<number>>;
134
+ /** Cutouts by host floor or roof. */
135
+ readonly byHost: Map<number, Set<number>>;
136
+ /** Every Level's elevation, to tell a move from an edit that moves nothing. */
137
+ readonly levels: Map<number, number>;
138
+ /** Keyed by (group, chunk): see `chunkOf`. */
139
+ readonly chunks: Map<number, ChunkRecord>;
140
+ readonly materials: Map<string, MaterialRef>;
141
+ /** Roofs by the chunks their footprints' bounds overlap (`roofAt`). */
142
+ readonly roofIndex: Map<number, Set<number>>;
143
+ /** Group entities by slot; the ground level is slot 0. */
144
+ readonly groups: Entity[];
145
+ readonly groupSlots: Map<number, number>;
146
+ /** Doors and hatches swinging. */
147
+ readonly moving: Set<number>;
148
+ /** The ground level's group: pieces without a Level draw under it. */
149
+ ground: Entity;
150
+ chunkSize: number;
151
+ curveTolerance: number;
152
+ /** The shape each wall last reported a too-tight curve for (reported once per shape). */
153
+ readonly warned: Map<number, string>;
154
+ /** The error each cutout last reported (reported once until it changes). */
155
+ readonly cutoutWarned: Map<number, string>;
156
+ last: CompileReport;
157
+ /** Totals since start. */
158
+ chunksRebuilt: number;
159
+ compiles: number;
160
+ defaultMaterial: MaterialRef | undefined;
161
+ glassMaterial: MaterialRef | undefined;
162
+ leafMesh: AssetRef<'Mesh'> | undefined;
163
+ readonly removedWalls: Entity[];
164
+ readonly removedSlabs: Entity[];
165
+ readonly removedOpenings: Entity[];
166
+ readonly removedCutouts: Entity[];
167
+ readonly removedLevels: Entity[];
168
+ readonly dirtyWalls: Set<number>;
169
+ readonly dirtySlabs: Set<number>;
170
+ readonly dirtyChunks: Set<number>;
171
+ /** (group, chunk) keys whose contact mesh alone rebuilds. */
172
+ readonly dirtyContact: Set<number>;
173
+ /** Walls whose contact quads are recomputed after this compile's walls and slabs. */
174
+ readonly contactWalls: Set<number>;
175
+ /** Contact settings as last applied. */
176
+ contact: ContactSettings;
177
+ /** StructureSettings.cutawayWalls as last applied (0070). */
178
+ cutawayWalls: boolean;
179
+ /** The `StructureSettings.contact` object last merged (a patch replaces it). */
180
+ contactSeen: object | undefined;
181
+ contactMaterial: MaterialRef | undefined;
182
+ readonly contactBuilder: MeshBuilder;
183
+ readonly contactScratch: ContactQuads;
184
+ readonly builders: Map<string, MeshBuilder>;
185
+ readonly scratch: ClipScratch;
186
+ readonly overlap: Set<number>;
187
+ readonly region: Set<number>;
188
+ readonly sortedOpenings: OpeningShape[];
189
+ readonly world: World;
190
+ /** Interior lighting's section of `describe` (0069), while interiorLightingPlugin is installed. */
191
+ describeInterior: (() => unknown) | undefined;
192
+ constructor(world: World);
193
+ /** A material's key, registering its ref (the default material for none). */
194
+ materialKey(ref: MaterialRef | null): string;
195
+ materialRef(key: string): MaterialRef;
196
+ builder(key: string): MeshBuilder;
197
+ /** A group's slot (assigned on first use). */
198
+ groupSlot(group: Entity): number;
199
+ /** The (group, chunk) key of chunk `key` in `group`. The ground level's are plain chunk keys. */
200
+ chunkOf(group: Entity, key: number): number;
201
+ chunk(composite: number): ChunkRecord;
202
+ /** Where a piece on `level` draws: the Level, or the ground level when it has none. */
203
+ groupOf(level: Entity | null): Entity;
204
+ /** A group's elevation: its Level's, or 0. */
205
+ baseOf(group: Entity): number;
206
+ /** Curved walls and how finely each is sampled (0066). */
207
+ private curves;
208
+ /** Every group: its kind, level index, chunks and meshes, and whether it's hidden. */
209
+ private describeGroups;
210
+ /** Counts for `structure.describe`. */
211
+ describe(): {
212
+ walls: number;
213
+ openings: number;
214
+ floors: number;
215
+ roofs: number;
216
+ cutouts: number;
217
+ levels: number;
218
+ chunkSize: number;
219
+ chunks: number;
220
+ meshes: number;
221
+ grid: {
222
+ min: number[];
223
+ max: number[];
224
+ bounds: number[];
225
+ } | null;
226
+ groups: {
227
+ group: Entity;
228
+ kind: 'ground' | 'level' | 'roof';
229
+ index: number;
230
+ chunks: number;
231
+ meshes: number;
232
+ contactMeshes: number;
233
+ hidden: boolean;
234
+ }[];
235
+ perChunk: {
236
+ group: Entity;
237
+ chunk: [number, number];
238
+ walls: number;
239
+ floors: number;
240
+ meshes: number;
241
+ contactTriangles: number;
242
+ }[];
243
+ contact: {
244
+ enabled: boolean;
245
+ meshes: number;
246
+ triangles: number;
247
+ };
248
+ doorsMoving: number;
249
+ curves: {
250
+ wall: Entity;
251
+ samples: number;
252
+ length: number;
253
+ }[];
254
+ warnings: number[];
255
+ lastCompile: CompileReport;
256
+ totals: {
257
+ compiles: number;
258
+ chunksRebuilt: number;
259
+ };
260
+ interior?: unknown;
261
+ };
262
+ }
263
+ export declare const Structure: import("@aethervtt/shard-core").ResourceDef<StructureState>;
264
+ /** Records despawns and removals for the next compile (observers fire before the value goes). */
265
+ export declare function observeRemovals(world: World, state: StructureState): void;
266
+ /**
267
+ * The compile: gathers walls, openings, floors, roofs, cutouts and levels changed since its last
268
+ * run (plus despawns), marks the (group, chunk) pairs their old and new geometry overlap, and
269
+ * rebuilds exactly those. A door's or hatch's `state` marks nothing. Allocates nothing when
270
+ * nothing changed.
271
+ */
272
+ export declare const compileStructure: import("@aethervtt/shard-core").SystemDef<{
273
+ walls: import("@aethervtt/shard-core").Query;
274
+ floors: import("@aethervtt/shard-core").Query;
275
+ roofs: import("@aethervtt/shard-core").Query;
276
+ openings: import("@aethervtt/shard-core").Query;
277
+ cutouts: import("@aethervtt/shard-core").Query;
278
+ levels: import("@aethervtt/shard-core").Query;
279
+ }>;
280
+ /**
281
+ * The roof whose footprint contains (x, z), through the roofs indexed at that chunk: on an edge
282
+ * counts, and a point over one of its holes still counts. With `level` (null: the ground level),
283
+ * only roofs on that level. Ties go to the lowest entity. A host hides the roofs its viewer's
284
+ * tokens stand under.
285
+ */
286
+ export declare function roofAt(world: World, x: number, z: number, level?: Entity | null): Entity | undefined;
287
+ /**
288
+ * Swings door and hatch leaves toward their state over `doorSwingMs` (at once with
289
+ * `reducedMotion`), holding a frame demand while any moves.
290
+ */
291
+ export declare const swingDoors: import("@aethervtt/shard-core").SystemDef<undefined>;
292
+ export {};