@kerros/schema 0.0.0-stage → 0.1.0-rc.2

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.
@@ -0,0 +1,9 @@
1
+ import { type Portal, type ProjectDocument } from './types';
2
+ /** A shared virtual edge explicitly declares an unwalled connection. Read its two sides directly;
3
+ * sampling inset room outlines loses narrow openings near the ends of adjoining walls. */
4
+ export declare function sharedBoundaryPortals(project: ProjectDocument): Portal[];
5
+ /** The readable portal layer, including explicit shared openings in older saved plans. A stored
6
+ * portal takes precedence, including sealed and one-way passage. No document mutation is needed.
7
+ * Once both spaces use shared boundaries, an inferred open connection also disappears when its
8
+ * virtual edge is replaced by a wall. Legacy independent outlines retain their inferred portals. */
9
+ export declare function effectivePortals(project: ProjectDocument): Portal[];
@@ -0,0 +1,11 @@
1
+ /** Numerical tolerances are independent of the user's positioning grid. All values are metres. */
2
+ export declare const GEOMETRY_EPS = 0.000001;
3
+ export declare const JOIN_EPS = 0.001;
4
+ export declare const MIN_WALL_LENGTH = 0.01;
5
+ export declare const MIN_RING_EDGE = 0.001;
6
+ /** Usable floor area, after subtracting holes and physical walls, in square metres. */
7
+ export declare const MIN_SPACE_AREA = 1;
8
+ /** Topological faces and holes may be smaller than a labelled space. Square metres. */
9
+ export declare const MIN_FACE_AREA = 1e-8;
10
+ /** Only derived polygon-clipping inputs use this precision; junctions retain their coordinates. */
11
+ export declare const CLIP_SCALE = 10000;
@@ -0,0 +1,8 @@
1
+ import type { ProjectDocument, SiteObject } from './types';
2
+ export declare function copyProject(project: ProjectDocument): ProjectDocument;
3
+ /** Apply dimensions in the object's own axes, then rotate and translate its polygon. */
4
+ export declare function transformObject(object: SiteObject, patch: Partial<SiteObject>): void;
5
+ /** The floor a viewer should open on: the document's own `initialFloorId` when it names a floor that
6
+ * still exists, otherwise the ground floor, otherwise the lowest one. An explicit `null` is a real
7
+ * answer — it means the outdoor site — so it is distinguished from the field being absent. */
8
+ export declare function openingFloorId(project: ProjectDocument): string | null;
@@ -0,0 +1,15 @@
1
+ import type { NavEdge, NavNode, ProjectDocument } from './types';
2
+ /** Bind a graph node to the room whose walkable floor it can enter. Use separate nodes on the
3
+ * sides of a controlled portal and join those with a directed/door edge in `graph`. */
4
+ export interface RoomAccess {
5
+ spaceId: string;
6
+ nodeId: string;
7
+ }
8
+ export interface RoomNavigationGraph {
9
+ nodes: NavNode[];
10
+ edges: NavEdge[];
11
+ }
12
+ /** Build the geometric walking graph inside rooms. Explicit access points describe connectivity;
13
+ * room outlines, holes, pools and wall thickness describe where a person can walk. The input
14
+ * document/graph are not mutated. Supplied door/vertical/directed edges are kept verbatim. */
15
+ export declare function buildRoomNavigation(project: ProjectDocument, access: readonly RoomAccess[], graph?: RoomNavigationGraph): RoomNavigationGraph;
@@ -0,0 +1,11 @@
1
+ import type { Point, ProjectDocument } from './types';
2
+ /** Extra room for shoulders and for the route marking, in plan metres. */
3
+ export declare const ROUTE_CLEARANCE = 0.4;
4
+ type Bounds = [number, number, number, number];
5
+ export declare const routeBounds: (points: Point[], pad?: number) => Bounds;
6
+ /** A geometric guard for walking connections. It checks the whole segment, not samples along it.
7
+ * Walls retain their thickness; doors/gates cut their actual apertures. Pools and floor holes are
8
+ * obstacles even when a containing room's outer ring includes them. Build once for a document. */
9
+ export declare function createRouteClearance(project: ProjectDocument): (floorId: string | null, extent?: Bounds) => (a: Point, b: Point) => boolean;
10
+ export declare function routeClearance(project: ProjectDocument): (floorId: string | null, extent?: Bounds) => (a: Point, b: Point) => boolean;
11
+ export {};
@@ -0,0 +1,46 @@
1
+ import { type Point, type ProjectDocument, type Ring, type SiteObject } from './types';
2
+ /** The enclosed region a point stands in, read off the walls around it.
3
+ *
4
+ * A floor's open area is its extent minus the bodies of its walls, and that difference falls apart
5
+ * into exactly the regions the walls divide it into — so the room under a click is whichever of
6
+ * those contains the click. No tracing, no flood fill, no tolerance to tune: the walls say where
7
+ * the rooms are, and this reads the answer.
8
+ *
9
+ * Doorways do not leak, because a door is an object ON a continuous wall rather than a hole in the
10
+ * run of it. A doorless passage does leak, and should: the drawing said those two places flow into
11
+ * one another. Returns null when the point is not enclosed — outside the building, or inside a
12
+ * boundary with a gap in it, where the region reaches the edge of the extent. */
13
+ export declare function enclosedRegion(project: ProjectDocument, floorId: string | null, point: Point): Ring | null;
14
+ /** Every enclosed region on a floor, largest first.
15
+ *
16
+ * Outer rings only: a region with a walled island inside it comes back with the island filled in,
17
+ * so its area overstates the floor you could stand on by the island's footprint. Carrying the holes
18
+ * through as further rings is the complete answer and changes this signature; until then the island
19
+ * is its own region in the list, and enclosedRegion returns it for a click inside it.
20
+ *
21
+ * The same difference as `enclosedRegion`, read whole rather than probed: one subtraction gives all
22
+ * the rooms the walls describe. Regions touching the extent are open to the outside and dropped,
23
+ * and slivers below `minArea` — the gap between two walls that nearly meet — with them. */
24
+ export declare function enclosedRegions(project: ProjectDocument, floorId: string | null, minArea?: number): Ring[];
25
+ /** Re-fit the rooms that the walls were defining, after those walls have moved.
26
+ *
27
+ * A room stores its own outline, which is what lets a space exist where no wall does — a zone over
28
+ * an open floor, a parcel, an area someone drew freehand. The cost is that moving a wall used to
29
+ * leave the room it bounded exactly where it was, so the two drifted apart and had to be kept in
30
+ * step by hand.
31
+ *
32
+ * They do not have to be. Pass the regions as they stood BEFORE the edit: a room whose outline was
33
+ * one of them was being defined by the walls, so it takes the region that now stands in its place.
34
+ * A room that matched nothing was drawn rather than enclosed, and is left alone. Identity is never
35
+ * touched — the name, colour, bindings and id are the room; only its shape came from the walls. */
36
+ export declare function refitEnclosedRooms(project: ProjectDocument, floorId: string | null, before: Ring[]): number;
37
+ /** Spaces are objects you can stand in — the areas already in the document, plus lifts and stairs.
38
+ * Nothing new is stored for them: a room *is* a space, and zones and portals reference it by id. */
39
+ export declare const spaces: (project: ProjectDocument) => SiteObject[];
40
+ /** Where a space sits for routing. Its footprint's centroid, falling back to its placed position. */
41
+ export declare function spacePoint(project: ProjectDocument, space: SiteObject): Point;
42
+ /** True when `point` falls inside a space's footprint — inside its outline and outside its holes. */
43
+ export declare function inSpace(space: SiteObject, point: Point): boolean;
44
+ /** The space a point falls in: the *smallest* one containing it, since a room inside a zone inside a
45
+ * floor plate are all true and only the innermost is useful. */
46
+ export declare function spaceAt(project: ProjectDocument, floorId: string | null, point: Point): SiteObject | null;
@@ -0,0 +1,14 @@
1
+ import { type NavEdge, type NavNode } from './types';
2
+ import type { ProjectDocument } from './types';
3
+ /** Node id for a space. Prefixed so a derived graph can never collide with an authored entity id. */
4
+ export declare const spaceNodeId: (spaceId: string) => string;
5
+ export declare function derivedGraph(project: ProjectDocument): {
6
+ nodes: NavNode[];
7
+ edges: NavEdge[];
8
+ };
9
+ export declare function topology(project: ProjectDocument): {
10
+ nodes: NavNode[];
11
+ edges: NavEdge[];
12
+ };
13
+ /** Straight-line length of a derived edge, for cost comparisons against an authored graph. */
14
+ export declare const edgeSpan: (nodes: Map<string, NavNode>, edge: NavEdge) => number;
@@ -0,0 +1,369 @@
1
+ import type { ExteriorPreset } from './materials';
2
+ export type Point = [number, number];
3
+ /** Geographic anchor of the local tangent frame — [lng, lat] in WGS84 degrees; an optional third
4
+ * element rotates the site clockwise from true north (degrees). */
5
+ export type Origin = [number, number] | [number, number, number];
6
+ export type Ring = Point[];
7
+ /** The built-in taxonomy of drawable things, spanning the toolkit's use-cases (spaces, openings,
8
+ * devices, logistics, safety). It is intentionally broad rather than tied to one domain; model
9
+ * host-specific subtypes (e.g. a turnstile variety, a sensor class) as `category`/`metadata` on the
10
+ * object rather than by extending this union. Runtime tuple so the type and the document validator
11
+ * share one source of truth. */
12
+ export declare const OBJECT_KINDS: readonly ["zone", "room", "parcel", "building", "office", "container", "storage", "door", "window", "gate", "turnstile", "reader", "camera", "elevator", "stairs", "poi", "fixture", "light", "landscape", "sensor", "alarm", "equipment", "evacuation"];
13
+ export type ObjectKind = (typeof OBJECT_KINDS)[number];
14
+ export type MaterialKind = 'brick' | 'stone' | 'plaster' | 'timber' | 'oak' | 'tile' | 'terrazzo' | 'grass' | 'paving' | 'carpet' | 'wallpaper';
15
+ export type ModelKind = 'bed' | 'sofa' | 'dining' | 'cabinet' | 'counter' | 'toilet' | 'basin' | 'shower' | 'sauna' | 'tree' | 'shrub' | 'lamp' | 'car' | 'chimney' | 'vent' | 'post';
16
+ export interface RoofSection {
17
+ footprint: Ring;
18
+ ridge: [Point, Point];
19
+ eave: number;
20
+ peak: number;
21
+ }
22
+ export interface BuildingRoof {
23
+ floorId: string;
24
+ color: string;
25
+ sections: RoofSection[];
26
+ }
27
+ /** mezzanine marks an intermediate (entresol) level: the plan draws it in context of the levels around it. */
28
+ export interface Floor {
29
+ id: string;
30
+ buildingId: string;
31
+ name: string;
32
+ elevation: number;
33
+ height: number;
34
+ mezzanine?: boolean;
35
+ code?: string;
36
+ /** The artificial light this level burns — colour temperature and how brightly. Absent means a
37
+ * fluorescent ceiling at working brightness, which is what is actually overhead in most of the
38
+ * buildings anyone draws. A building's own lighting does not follow the sun, so this is what
39
+ * keeps a storey legible in a plan opened after dark. */
40
+ light?: InteriorLight;
41
+ /** What this level sounds like from inside it — the default for every space on it that does not
42
+ * say otherwise. Absent is silence. */
43
+ ambience?: Ambience;
44
+ }
45
+ /** The sound of a space, heard at eye level: a named soundscape or musical preset,
46
+ * and how loud, 0 to 1 (absent is 1). Kept in
47
+ * the document rather than the renderer for the same reason light is: what a room sounds like
48
+ * describes the room. */
49
+ export interface Ambience {
50
+ preset: AmbiencePreset;
51
+ level?: number;
52
+ }
53
+ export type AmbiencePreset = 'silent' | 'office' | 'backrooms' | 'plant' | 'baths' | 'elevator';
54
+ export declare const AMBIENCE_PRESETS: AmbiencePreset[];
55
+ /** Colour temperature in kelvin (2700 tungsten, 4000 fluorescent, 6500 daylight) and how brightly,
56
+ * 0 to 1. Kept here rather than in the renderer: what a space is lit by describes the space. */
57
+ export interface InteriorLight {
58
+ kelvin: number;
59
+ level: number;
60
+ /** Optional colour cast from the fittings or reflected surroundings. */
61
+ tint?: string;
62
+ }
63
+ /** A ceiling luminaire. Object height is the mounting height above the floor, intensity is candela,
64
+ * range is metres, and flicker is the optional 0..1 depth of intermittent ballast dimming. */
65
+ export interface LightFixture {
66
+ /** Optional mounting elevation above the floor; negative values place a lamp inside a pool. */
67
+ mountHeight?: number;
68
+ kelvin: number;
69
+ intensity: number;
70
+ range: number;
71
+ flicker?: number;
72
+ }
73
+ export interface Building {
74
+ id: string;
75
+ name: string;
76
+ roof?: BuildingRoof;
77
+ /** Provenance id of the basemap footprint a building was adopted from — the value of the host's
78
+ * configured footprints.idField (a string or number, depending on the tile source); absent for
79
+ * hand-drawn buildings. */
80
+ sourceId?: string | number;
81
+ exteriorPreset?: ExteriorPreset;
82
+ }
83
+ export interface Junction {
84
+ id: string;
85
+ floorId: string | null;
86
+ position: Point;
87
+ }
88
+ /** An unwalled space boundary. Physical boundaries use Barrier IDs and endpoints directly. */
89
+ export interface VirtualBoundary {
90
+ id: string;
91
+ floorId: string | null;
92
+ startId: string;
93
+ endId: string;
94
+ }
95
+ export interface BoundaryUse {
96
+ edgeId: string;
97
+ /** Traverse from endId to startId. Absent means startId to endId. */
98
+ reversed?: boolean;
99
+ }
100
+ export type SpaceGeometry = {
101
+ mode: 'independent';
102
+ } | {
103
+ mode: 'boundaries';
104
+ loops: BoundaryUse[][];
105
+ };
106
+ export interface Barrier {
107
+ id: string;
108
+ floorId: string | null;
109
+ startId: string;
110
+ endId: string;
111
+ kind: 'wall' | 'fence';
112
+ thickness: number;
113
+ height: number;
114
+ name: string;
115
+ color?: string;
116
+ material?: MaterialKind;
117
+ category?: string;
118
+ metadata?: Record<string, unknown>;
119
+ }
120
+ /** A sloped area: the footprint falls linearly from `high` at `axis[0]` to `low` at `axis[1]`, and
121
+ * stays flat beyond either end — so a footprint longer than its axis keeps level aprons where it
122
+ * meets the decks it joins. Elevations are absolute metres in the same frame as Floor.elevation, so a
123
+ * ramp can span floors (a garage ramp, a loading incline, a sloped plaza). Any area object may carry
124
+ * one; without it an area is flat. The axis always runs downhill: `high` >= `low`. */
125
+ export interface Slope {
126
+ axis: [Point, Point];
127
+ high: number;
128
+ low: number;
129
+ }
130
+ export interface SiteObject {
131
+ id: string;
132
+ kind: ObjectKind;
133
+ floorId: string | null;
134
+ name: string;
135
+ position: Point;
136
+ rotation: number;
137
+ rings?: Ring[];
138
+ /** Boundary-driven spaces keep rings and dimensions as a validated, generated cache.
139
+ * Absent is an independent outline, preserving existing/imported projects without guessing. */
140
+ geometry?: SpaceGeometry;
141
+ /** Makes this area a sloped plane rather than a flat plate; see Slope. */
142
+ slope?: Slope;
143
+ /** Clear ceiling height above this floor's datum; omitted uses the storey height.
144
+ * A room spanning storeys uses this with holes in the plates above it. */
145
+ ceilingHeight?: number;
146
+ /** A basin below this area's walking surface, filled with clear water. */
147
+ water?: {
148
+ depth: number;
149
+ ripple?: number;
150
+ };
151
+ /** Open water-slide centreline, in metres relative to the object's position and rotation.
152
+ * The third coordinate is height above its floor. */
153
+ slide?: {
154
+ path: [number, number, number][];
155
+ radius: number;
156
+ };
157
+ /** Bottom of a raised fixture, metres above the floor surface (e.g. a doorway lintel). */
158
+ baseHeight?: number;
159
+ width: number;
160
+ depth: number;
161
+ height: number;
162
+ parentId?: string;
163
+ barrierId?: string;
164
+ offset?: number;
165
+ /** Door hinge at the wall start (left, default) or end (right). */
166
+ doorHinge?: 'left' | 'right';
167
+ /** Door mechanism. Omitted means a conventional single hinged leaf. */
168
+ doorType?: 'hinged' | 'sliding' | 'double';
169
+ /** Door opens to the left (+1, default) or right (-1) of the directed wall. */
170
+ doorSwing?: 1 | -1;
171
+ feedId?: string;
172
+ symbol?: 'personnel' | 'service' | 'driveway' | 'parking' | 'assembly' | 'info' | 'aed' | 'extinguisher' | 'firstaid' | 'exit' | 'firealarm';
173
+ servedFloorIds?: string[];
174
+ /** Stairs only: which geometry this climb has.
175
+ *
176
+ * `straight` is one run. `switchback` turns back on itself around a half-landing — the half-turn
177
+ * stair that fits a storey into a short core box, and what most building stairs actually are.
178
+ * `dogleg` turns a quarter at its landing, for a stair in the corner of a plan. `spiral` winds
179
+ * around a pole and can land on every level it passes. `escalator` is a machine: a fixed 30° pitch
180
+ * and a flat comb plate at each end.
181
+ *
182
+ * All of them connect the same floors and route identically, which is why this is one field rather
183
+ * than five kinds — the difference is what it looks like and how much run it needs. Absent picks
184
+ * the shape that fits: a straight run where the footprint allows a civil pitch, a switchback where
185
+ * it does not, since a plan that gives a 4.5 m box to a 4.4 m storey means a stair that turns, not
186
+ * a ladder. */
187
+ stairModel?: 'straight' | 'switchback' | 'dogleg' | 'spiral' | 'escalator';
188
+ /** Stairs with the same group on a level share one rectangular floor/ceiling opening. */
189
+ wellGroup?: string;
190
+ /** Escalators only: which way the machine carries you.
191
+ *
192
+ * It is a property of the machine, not of the drawing and not of its name: an escalator runs one
193
+ * way, a bank of them runs alternate ways, and which way this one runs is the first thing anyone
194
+ * asks of it. The same vocabulary as `Zone.connects`, so the described layer and the object never
195
+ * need translating between them. Absent means up, which is what a lone escalator nearly always
196
+ * does. A stair that is not an escalator ignores it — a stair carries you both ways.
197
+ *
198
+ * An imported escalator arrives once per storey it is drawn on; the twin standing lowest is the
199
+ * one the model draws and routes from, so that is the one to author. */
200
+ travel?: 'up' | 'down';
201
+ /** Elevators only: which faces of the car open, relative to the object's own rotation. A through-car
202
+ * opens front and back; a corner lobby car opens front and one side. Absent means the front alone,
203
+ * which is what almost every lift does. Which FLOORS it opens on is `servedFloorIds` — a level the
204
+ * shaft passes without serving has no doors on any side. */
205
+ doorSides?: ('front' | 'back' | 'left' | 'right')[];
206
+ coverageAngle?: number;
207
+ coverageRange?: number;
208
+ color?: string;
209
+ /** Camera: ids of the doors, rooms and elevators this camera observes (event footage lookup). */
210
+ watchedIds?: string[];
211
+ material?: MaterialKind;
212
+ model?: ModelKind;
213
+ light?: LightFixture;
214
+ /** A space's own sound, overriding its floor's. See Floor.ambience. */
215
+ ambience?: Ambience;
216
+ /** Host-defined subtype/category (e.g. 'meeting-room', 'speedgate', 'cold-store') for styling, filtering and event handlers. */
217
+ category?: string;
218
+ /** Arbitrary host metadata; ignored by the core, available to style hooks and event handlers. */
219
+ metadata?: Record<string, unknown>;
220
+ }
221
+ export interface Drawing {
222
+ id: string;
223
+ floorId: string | null;
224
+ assetId: string;
225
+ name: string;
226
+ width: number;
227
+ height: number;
228
+ origin: Point;
229
+ scale: number;
230
+ rotation: number;
231
+ opacity: number;
232
+ visible: boolean;
233
+ locked: boolean;
234
+ }
235
+ /** A named set of spaces — the area objects listed in `spaceIds`.
236
+ *
237
+ * A zone is a *membership*, not a shape: it has no geometry of its own, which is exactly what lets it
238
+ * span floors, skip buildings, and overlap other zones freely. (Contrast `SiteObject.parentId`, which
239
+ * is the strict single-parent, same-floor, geometrically-contained tree.) A lift shaft is a zone of
240
+ * its per-floor lift spaces; a security area is a zone of the rooms inside it. */
241
+ export interface Zone {
242
+ id: string;
243
+ name: string;
244
+ spaceIds: string[];
245
+ /** Zones may contain zones: a DAG, not a tree. Cycles are rejected; several parents are fine.
246
+ * Membership does NOT imply access inheritance — that is policy, and policy is the host's. */
247
+ childZoneIds?: string[];
248
+ /** How the spaces within this zone reach each other, when they are not joined by portals.
249
+ * 'all' — every space mutually reachable: a lift serving all of its landings.
250
+ * 'adjacent' — reachable in elevation order, both ways: a stair, passing each level in turn.
251
+ * 'up'/'down'— adjacent but one-way: an escalator, which carries you in a single direction.
252
+ * omitted — no implied connectivity; the zone is a grouping only. */
253
+ connects?: 'all' | 'adjacent' | 'up' | 'down';
254
+ /** Host vocabulary ('security', 'evacuation', 'hvac', …). The core never interprets it. */
255
+ purpose?: string;
256
+ metadata?: Record<string, unknown>;
257
+ }
258
+ /** A traversable connection between exactly two spaces.
259
+ *
260
+ * Direction is deliberately not baked in: a door's entry and exit sides are relative to whichever
261
+ * area you are asking about, so the two sides are stored symmetrically and direction is derived
262
+ * (see `entryInto`). Applications that attach a rule per direction of travel split a two-way door into
263
+ * two objects; that is the right shape for an application layer and the wrong shape for a topology one. */
264
+ export interface Portal {
265
+ id: string;
266
+ /** The opening object (door/gate/turnstile) crossed here; absent for a virtual portal — an open
267
+ * boundary between two spaces with no physical separator, which malls and open-plan floors need.
268
+ * Kept as a reference rather than folded in, so two portals can be known to share one door leaf. */
269
+ openingId?: string;
270
+ /** The two sides. Order is storage, not meaning — ask `entryInto` rather than reading it. */
271
+ a: string;
272
+ b: string;
273
+ /** Which way passage is possible at all. Default 'both'. */
274
+ passage?: 'both' | 'a-to-b' | 'b-to-a' | 'none';
275
+ /** How far a crossing here can be trusted to have actually happened.
276
+ * 'confirmed' — a sensor saw it (a door monitor opening and closing).
277
+ * 'assumed' — inferred from a grant; nobody watched. The default for a plain door.
278
+ * 'none' — the crossing is a *request*, not an event: a lift floor button, where the
279
+ * passenger may go anywhere. Anything counting crossings must ignore these. */
280
+ attests?: 'confirmed' | 'assumed' | 'none';
281
+ name?: string;
282
+ metadata?: Record<string, unknown>;
283
+ }
284
+ /** An explicit set of portals, for sets that are not a zone boundary — "all loading-bay shutters",
285
+ * "every door with a reader". Where a set *is* a zone boundary, derive it instead: see `perimeter`. */
286
+ export interface PortalGroup {
287
+ id: string;
288
+ name: string;
289
+ portalIds: string[];
290
+ metadata?: Record<string, unknown>;
291
+ }
292
+ /** Authored route-graph node for indoor navigation. objectId binds it to a SiteObject (door, elevator, stairs, room, poi) so routing can anchor at that object and instructions can name it. */
293
+ export interface NavNode {
294
+ id: string;
295
+ floorId: string | null;
296
+ position: Point;
297
+ objectId?: string;
298
+ }
299
+ export type NavEdgeKind = 'walk' | 'door' | 'stairs' | 'elevator';
300
+ /** Undirected route-graph edge. walk stays on one floor; door stays on one floor or steps outside (one end floorId null); stairs/elevator join different floors and bind the serving object via objectId. weight overrides the derived metres-equivalent cost. */
301
+ export interface NavEdge {
302
+ id: string;
303
+ kind: NavEdgeKind;
304
+ aId: string;
305
+ bId: string;
306
+ /** Passable from `aId` to `bId` only. Escalators and one-way doors; absent means both ways. */
307
+ directed?: boolean;
308
+ objectId?: string;
309
+ weight?: number;
310
+ }
311
+ export interface ProjectDocument {
312
+ schemaVersion: 1;
313
+ id: string;
314
+ name: string;
315
+ description: string;
316
+ updatedAt: string;
317
+ origin: Origin;
318
+ datum: string;
319
+ buildings: Building[];
320
+ floors: Floor[];
321
+ junctions: Junction[];
322
+ barriers: Barrier[];
323
+ virtualBoundaries?: VirtualBoundary[];
324
+ objects: SiteObject[];
325
+ drawings: Drawing[];
326
+ referenceNote?: string;
327
+ /** The floor a viewer should open on — the document's own front door, so a project that is really
328
+ * about its offices does not always land on the street. `null` means the outdoor site; omit it
329
+ * entirely to let the viewer pick (ground floor, else the lowest). Hosts can still override per
330
+ * session via ViewState.floor / FloorViewer's `floorId`. */
331
+ initialFloorId?: string | null;
332
+ /** The spatial ontology layered over the geometry: which spaces group together, and what connects
333
+ * to what. Spaces themselves are the ringed area objects in `objects` — nothing is duplicated. */
334
+ zones?: Zone[];
335
+ portals?: Portal[];
336
+ portalGroups?: PortalGroup[];
337
+ /** Optional authored navigation graph. When absent, the graph is derived from spaces and portals
338
+ * (see model/topology.ts) — the plan is then the single source of truth and cannot drift out of
339
+ * step with a hand-kept copy of itself. */
340
+ navNodes?: NavNode[];
341
+ navEdges?: NavEdge[];
342
+ }
343
+ export interface ProjectSummary {
344
+ id: string;
345
+ name: string;
346
+ updatedAt: string;
347
+ }
348
+ export interface ProjectRepository {
349
+ list(): Promise<ProjectSummary[]>;
350
+ load(id: string): Promise<ProjectDocument | null>;
351
+ save(project: ProjectDocument): Promise<void>;
352
+ delete(id: string): Promise<void>;
353
+ }
354
+ export interface AssetRepository {
355
+ get(id: string): Promise<Blob | undefined>;
356
+ put(id: string, asset: Blob): Promise<void>;
357
+ delete(id: string): Promise<void>;
358
+ }
359
+ export type Tool = 'select' | 'pan' | 'wall' | 'boundary' | 'partition' | 'fence' | 'zone' | 'room' | 'rectangle' | 'enclose' | 'hole' | 'door' | 'window' | 'gate' | 'turnstile' | 'reader' | 'camera' | 'elevator' | 'stairs' | 'office' | 'container' | 'storage' | 'poi' | 'measure' | 'adopt' | 'split' | 'route' | 'sensor' | 'light' | 'alarm' | 'equipment' | 'evacuation';
360
+ /** A fresh entity id. A bare UUIDv4 (no type prefix) so ids drop straight into a database `uuid`
361
+ * column and validate there; the entity's type is already given by which collection it lives in. */
362
+ export declare const uid: () => `${string}-${string}-${string}-${string}-${string}`;
363
+ export declare const isArea: (kind: ObjectKind) => boolean;
364
+ export declare const isDevice: (kind: ObjectKind) => boolean;
365
+ export declare const isOpening: (kind: ObjectKind) => kind is "door" | "window" | "gate";
366
+ /** A *space* is somewhere you can stand — the unit the ontology groups and connects. Areas qualify,
367
+ * and so do lifts and stairs: the inside of a car is as much a place as the lobby it opens onto, and
368
+ * the through-car case depends on being able to say so. */
369
+ export declare const isSpace: (kind: ObjectKind) => boolean;
@@ -0,0 +1,38 @@
1
+ import type { ProjectDocument, Ring } from './types';
2
+ /** How far from the site origin a coordinate may sit, in metres. A guard against corrupt data, not a
3
+ * modelling limit: no site is 100 km across, but a NaN that became 1e15 through arithmetic would
4
+ * otherwise sail through every finite() check and break projection math much later and much worse. */
5
+ export declare const COORD_LIMIT = 100000;
6
+ /** Geometric sanity of an area's rings: a real outer boundary, no self-crossings, holes inside and
7
+ * disjoint. Returns an error message or null. */
8
+ export declare function validateRings(rings: Ring[], minEdge?: number, minArea?: number): string | null;
9
+ /** Spatial relationships between entities: ring sanity per object, parent containment without
10
+ * cycles, and openings that actually fit the barriers they sit in. */
11
+ export declare function validateRelationships(project: ProjectDocument): string | null;
12
+ /** Semantic validation of the navigation graph — authored or derived; structural checks (ids,
13
+ * uniqueness) live in validateProject. Returns an error message or null. */
14
+ export declare function validateNavigation(p: ProjectDocument): string | null;
15
+ /** Parse and validate an unknown value into a ProjectDocument; throws with a reason on any failure.
16
+ * Every rule the schema has lives behind this one entry point. */
17
+ export declare function validateProject(value: unknown): ProjectDocument;
18
+ /** Deep-freeze a document in place and return it. Writes to a frozen document throw (modules run in
19
+ * strict mode), so an out-of-band mutation — one that dodged `transact` — fails at the assignment
20
+ * that would have corrupted the document, not three renders later. */
21
+ export declare function freezeProject(project: ProjectDocument): ProjectDocument;
22
+ export type TransactResult = {
23
+ ok: true;
24
+ project: ProjectDocument;
25
+ } | {
26
+ ok: false;
27
+ error: string;
28
+ };
29
+ /** Apply a change atomically: work on a clone, run every rule, and only then hand the result back —
30
+ * frozen, so nothing can edit it except the next transaction. If the change throws, or the outcome
31
+ * breaks any rule, the original is untouched and the reason comes back as `error`; there is no
32
+ * state in which a caller holds a partly-applied or invalid document.
33
+ *
34
+ * `updatedAt` is deliberately not stamped here — that belongs to whoever owns persistence and
35
+ * history (see commitHistory), which also stamps undo and redo. */
36
+ export declare function transact(project: ProjectDocument, change: (draft: ProjectDocument) => void, options?: {
37
+ freeze?: boolean;
38
+ }): TransactResult;