@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.
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/dist/index.js +3735 -0
- package/dist/index.js.map +1 -0
- package/dist/types/model/authoring.d.ts +38 -0
- package/dist/types/model/boundaries.d.ts +41 -0
- package/dist/types/model/coverage.d.ts +8 -0
- package/dist/types/model/document.d.ts +9 -0
- package/dist/types/model/factory.d.ts +5 -0
- package/dist/types/model/geometry.d.ts +99 -0
- package/dist/types/model/inference.d.ts +46 -0
- package/dist/types/model/live.d.ts +60 -0
- package/dist/types/model/materials.d.ts +28 -0
- package/dist/types/model/mutations.d.ts +178 -0
- package/dist/types/model/navigation.d.ts +70 -0
- package/dist/types/model/ontology.d.ts +43 -0
- package/dist/types/model/passages.d.ts +2 -0
- package/dist/types/model/portals.d.ts +9 -0
- package/dist/types/model/precision.d.ts +11 -0
- package/dist/types/model/project.d.ts +8 -0
- package/dist/types/model/roomNavigation.d.ts +15 -0
- package/dist/types/model/routeClearance.d.ts +11 -0
- package/dist/types/model/spaces.d.ts +46 -0
- package/dist/types/model/topology.d.ts +14 -0
- package/dist/types/model/types.d.ts +369 -0
- package/dist/types/model/validate.d.ts +38 -0
- package/dist/types/model/vertical.d.ts +145 -0
- package/dist/types/schema/index.d.ts +25 -0
- package/package.json +39 -4
|
@@ -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;
|