@ahrowe/ui 0.25.2 → 0.26.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/dist/esm/common/floorPlan/floorPlan.faces.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.hit.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.json.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.path.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.plan.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.snap.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -0
- package/dist/esm/common/floorPlan/floorPlan.types.mjs +2 -0
- package/dist/esm/common/floorPlan/floorPlan.types.mjs.map +1 -0
- package/dist/esm/common/floorPlan/legacySvg.mjs +2 -0
- package/dist/esm/common/floorPlan/legacySvg.mjs.map +1 -0
- package/dist/esm/common/planCanvas/planCanvas.mjs +2 -0
- package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -0
- package/dist/esm/common/planCanvas/planCanvas.module.mjs +2 -0
- package/dist/esm/common/planCanvas/planCanvas.module.mjs.map +1 -0
- package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.module.mjs +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.module.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/usePlanHistory.mjs +2 -0
- package/dist/esm/common/roomDrawer/usePlanHistory.mjs.map +1 -0
- package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
- package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
- package/dist/esm/common/roomViewer/roomViewer.module.mjs +1 -1
- package/dist/esm/common/roomViewer/roomViewer.module.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +3 -7
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/configProvider/configProvider.types.d.ts +4 -0
- package/dist/types/package/common/floorPlan/floorPlan.faces.d.ts +33 -0
- package/dist/types/package/common/floorPlan/floorPlan.fixtures.d.ts +35 -0
- package/dist/types/package/common/floorPlan/floorPlan.geometry.d.ts +111 -0
- package/dist/types/package/common/floorPlan/floorPlan.graph.d.ts +139 -0
- package/dist/types/package/common/floorPlan/floorPlan.hit.d.ts +26 -0
- package/dist/types/package/common/floorPlan/floorPlan.json.d.ts +23 -0
- package/dist/types/package/common/floorPlan/floorPlan.path.d.ts +59 -0
- package/dist/types/package/common/floorPlan/floorPlan.plan.d.ts +98 -0
- package/dist/types/package/common/floorPlan/floorPlan.rooms.d.ts +47 -0
- package/dist/types/package/common/floorPlan/floorPlan.snap.d.ts +101 -0
- package/dist/types/package/common/floorPlan/floorPlan.types.d.ts +276 -0
- package/dist/types/package/common/floorPlan/index.d.ts +28 -0
- package/dist/types/package/common/floorPlan/legacySvg.d.ts +27 -0
- package/dist/types/package/common/planCanvas/index.d.ts +2 -0
- package/dist/types/package/common/planCanvas/planCanvas.d.ts +7 -0
- package/dist/types/package/common/planCanvas/planCanvas.types.d.ts +62 -0
- package/dist/types/package/common/roomDrawer/roomDrawer.d.ts +3 -3
- package/dist/types/package/common/roomDrawer/roomDrawer.types.d.ts +295 -15
- package/dist/types/package/common/roomDrawer/usePlanHistory.d.ts +24 -0
- package/dist/types/package/common/roomViewer/index.d.ts +1 -0
- package/dist/types/package/common/roomViewer/roomViewer.d.ts +9 -2
- package/dist/types/package/common/roomViewer/roomViewer.types.d.ts +88 -9
- package/dist/types/package/index.d.ts +1 -0
- package/docs/CLAUDE.md +1 -0
- package/docs/ConfigProvider.md +1 -0
- package/docs/FloorPlan.md +214 -0
- package/docs/RoomDrawer.md +330 -30
- package/docs/RoomViewer.md +80 -27
- package/package.json +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs +0 -6
- package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs.map +0 -1
- package/dist/types/package/common/roomDrawer/roomDrawer.utils.d.ts +0 -32
- package/docs/room-drawing-analysis.md +0 -337
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { PlanFloor, PlanPoint } from './floorPlan.types';
|
|
2
|
+
export declare enum SnapKind {
|
|
3
|
+
None = "none",
|
|
4
|
+
Node = "node",
|
|
5
|
+
/** A point projected onto a wall's centreline. */
|
|
6
|
+
Wall = "wall",
|
|
7
|
+
/** The crossing of two walls, or of a wall with the active angle ray. */
|
|
8
|
+
Intersection = "intersection",
|
|
9
|
+
/** On the infinite line continuing an existing wall. */
|
|
10
|
+
Extension = "extension",
|
|
11
|
+
/** Shares an x or y coordinate with an existing node. */
|
|
12
|
+
Alignment = "alignment",
|
|
13
|
+
/** On a ray at a multiple of `angleStepDeg` from `origin`. */
|
|
14
|
+
Angle = "angle",
|
|
15
|
+
Grid = "grid"
|
|
16
|
+
}
|
|
17
|
+
export interface SnapGuide {
|
|
18
|
+
kind: SnapKind;
|
|
19
|
+
a: PlanPoint;
|
|
20
|
+
b: PlanPoint;
|
|
21
|
+
/** Render as an unbounded construction line rather than a segment. */
|
|
22
|
+
infinite?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SnapOptions {
|
|
25
|
+
/** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */
|
|
26
|
+
enabled: SnapKind[];
|
|
27
|
+
/** Millimetres. 0 disables grid snap. */
|
|
28
|
+
gridSize: number;
|
|
29
|
+
/** Degrees. 45 gives 0/45/90. 0 disables angle lock. */
|
|
30
|
+
angleStepDeg: number;
|
|
31
|
+
toleranceScreenPx: number;
|
|
32
|
+
/** Nodes are the most valuable snap, so they get a little more room. */
|
|
33
|
+
nodeToleranceScreenPx?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
|
|
36
|
+
export interface SnapContext {
|
|
37
|
+
floor: PlanFloor;
|
|
38
|
+
/** World units per screen pixel at the current zoom. See `screenScale`. */
|
|
39
|
+
pxToWorld: number;
|
|
40
|
+
/** The anchored point. Required for angle lock; also enables the length readout. */
|
|
41
|
+
origin?: PlanPoint;
|
|
42
|
+
/**
|
|
43
|
+
* A direction in radians that angle lock measures from, in ADDITION to the world axes.
|
|
44
|
+
*
|
|
45
|
+
* Pass the direction of the wall being continued and the lock offers perpendicular and
|
|
46
|
+
* parallel to that wall, not just to the world. Without it, angle lock is only useful
|
|
47
|
+
* on a building that happens to be aligned to the page.
|
|
48
|
+
*/
|
|
49
|
+
referenceAngle?: number;
|
|
50
|
+
excludeWallIds?: ReadonlySet<string>;
|
|
51
|
+
excludeNodeIds?: ReadonlySet<string>;
|
|
52
|
+
options?: Partial<SnapOptions>;
|
|
53
|
+
}
|
|
54
|
+
export interface SnapResult {
|
|
55
|
+
point: PlanPoint;
|
|
56
|
+
kind: SnapKind;
|
|
57
|
+
nodeId?: string;
|
|
58
|
+
wallId?: string;
|
|
59
|
+
/** Parameter along `wallId`, for `SnapKind.Wall`. */
|
|
60
|
+
t?: number;
|
|
61
|
+
/** Everything an overlay needs to draw to explain the snap. */
|
|
62
|
+
guides: SnapGuide[];
|
|
63
|
+
/** Radians from `origin` to `point`, when an origin was given. */
|
|
64
|
+
angle?: number;
|
|
65
|
+
/** Distance from `origin` to `point`, when an origin was given. */
|
|
66
|
+
distance?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Set when angle lock produced the result, saying what it locked against: the world
|
|
69
|
+
* axes, or the reference direction that was passed in.
|
|
70
|
+
*/
|
|
71
|
+
angleRelativeTo?: 'world' | 'reference';
|
|
72
|
+
}
|
|
73
|
+
/** World units per screen pixel, from the element's current transform. */
|
|
74
|
+
export declare function screenScale(svg: SVGSVGElement): number;
|
|
75
|
+
/**
|
|
76
|
+
* Client (screen) coordinates to world coordinates. Pure conversion, no snapping.
|
|
77
|
+
*
|
|
78
|
+
* Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than
|
|
79
|
+
* `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it
|
|
80
|
+
* untestable.
|
|
81
|
+
*/
|
|
82
|
+
export declare function clientToWorld(clientX: number, clientY: number, rect: {
|
|
83
|
+
left: number;
|
|
84
|
+
top: number;
|
|
85
|
+
}, view: {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
zoom: number;
|
|
89
|
+
}): PlanPoint;
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a raw pointer position to the point the user meant.
|
|
92
|
+
*
|
|
93
|
+
* Two stages, because half of these snaps are constraints rather than points. A hard
|
|
94
|
+
* snap (node, intersection, wall) fixes both degrees of freedom, so the first match
|
|
95
|
+
* wins. A constraint (angle lock, extension, alignment) removes only one, so a flat
|
|
96
|
+
* "first match wins" list would kick the point off the angle ray the moment an
|
|
97
|
+
* alignment also matched. Two constraints intersect; one constraint is projected onto
|
|
98
|
+
* and then slid ALONG to the nearest grid position, which is what lets angle lock and
|
|
99
|
+
* grid coexist instead of fighting.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floor plan model: a planar wall graph whose rooms are derived, not stored.
|
|
3
|
+
*
|
|
4
|
+
* Nodes and walls are first-class and shared, so the wall between two rooms is one
|
|
5
|
+
* wall. An opening therefore cuts it exactly once, which is what the previous
|
|
6
|
+
* room-polygon model could not express.
|
|
7
|
+
*
|
|
8
|
+
* GEOMETRY INVARIANT: every coordinate, length, thickness, offset and height in this
|
|
9
|
+
* module is in MILLIMETRES. There is deliberately no per-plan scale factor: one would
|
|
10
|
+
* have to be threaded through every geometry function, and a single missed call site
|
|
11
|
+
* is a silent 1000x error that no type can catch. `unitSystem` below drives display
|
|
12
|
+
* formatting only and is never read by geometry.
|
|
13
|
+
*/
|
|
14
|
+
/** A point in plan space, in millimetres. y increases downwards, as in SVG. */
|
|
15
|
+
export interface PlanPoint {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
}
|
|
19
|
+
export declare enum PlanUnitSystem {
|
|
20
|
+
/** Display as m / cm / mm. */
|
|
21
|
+
Metric = "metric",
|
|
22
|
+
/** Display as feet and inches. Storage stays millimetres. */
|
|
23
|
+
Imperial = "imperial"
|
|
24
|
+
}
|
|
25
|
+
export declare enum WallType {
|
|
26
|
+
/** Load-bearing envelope wall. */
|
|
27
|
+
Exterior = "exterior",
|
|
28
|
+
/** Load-bearing interior wall. */
|
|
29
|
+
Interior = "interior",
|
|
30
|
+
/** Non-load-bearing partition. */
|
|
31
|
+
Partition = "partition",
|
|
32
|
+
/**
|
|
33
|
+
* A room boundary with no physical wall, for dividing open-plan space. It still
|
|
34
|
+
* closes a face, so it produces two rooms, but renders as a thin dashed line and
|
|
35
|
+
* extrudes to nothing in 3D.
|
|
36
|
+
*/
|
|
37
|
+
Virtual = "virtual"
|
|
38
|
+
}
|
|
39
|
+
export declare enum OpeningKind {
|
|
40
|
+
/** A real door leaf, with a swing and a threshold. */
|
|
41
|
+
Door = "door",
|
|
42
|
+
/** Glazed opening with a sill above floor level. */
|
|
43
|
+
Window = "window",
|
|
44
|
+
/** Structural opening with a head but no leaf. */
|
|
45
|
+
Archway = "archway",
|
|
46
|
+
/**
|
|
47
|
+
* A plain hole in the wall with no jamb detailing. Distinct from "no wall at all":
|
|
48
|
+
* a Gap is a marked span of a wall that exists, which is what lets a gap-style
|
|
49
|
+
* rendering stay unambiguous.
|
|
50
|
+
*/
|
|
51
|
+
Gap = "gap"
|
|
52
|
+
}
|
|
53
|
+
export declare enum DoorSwing {
|
|
54
|
+
None = "none",
|
|
55
|
+
/** Hinged at the opening's start, opening towards the wall's left normal. */
|
|
56
|
+
StartLeft = "startLeft",
|
|
57
|
+
StartRight = "startRight",
|
|
58
|
+
EndLeft = "endLeft",
|
|
59
|
+
EndRight = "endRight",
|
|
60
|
+
/** Two leaves, hinged at both ends. */
|
|
61
|
+
Double = "double",
|
|
62
|
+
Sliding = "sliding"
|
|
63
|
+
}
|
|
64
|
+
/** A graph vertex. Structurally assignable to `PlanPoint`. */
|
|
65
|
+
export interface PlanNode {
|
|
66
|
+
id: string;
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* An undirected edge between two nodes. `a` and `b` define the wall's own direction,
|
|
72
|
+
* which is what opening offsets and door swings are measured against.
|
|
73
|
+
*
|
|
74
|
+
* The wall is centred on the segment a..b, extending `thickness / 2` to each side.
|
|
75
|
+
* There is no eccentric offset in this version.
|
|
76
|
+
*/
|
|
77
|
+
export interface PlanWall {
|
|
78
|
+
id: string;
|
|
79
|
+
a: string;
|
|
80
|
+
b: string;
|
|
81
|
+
/** Millimetres. Must be greater than 0. */
|
|
82
|
+
thickness: number;
|
|
83
|
+
/** Millimetres above this wall's base. Omit to inherit `PlanFloor.height`. */
|
|
84
|
+
height?: number;
|
|
85
|
+
/** Millimetres above the floor's finished level. Defaults to 0. */
|
|
86
|
+
baseHeight?: number;
|
|
87
|
+
type: WallType;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* An opening cut into exactly one wall. Because a shared wall is one wall, an opening
|
|
91
|
+
* between two rooms exists exactly once and needs no de-duplication anywhere.
|
|
92
|
+
*/
|
|
93
|
+
export interface PlanOpening {
|
|
94
|
+
id: string;
|
|
95
|
+
wallId: string;
|
|
96
|
+
kind: OpeningKind;
|
|
97
|
+
/**
|
|
98
|
+
* Distance in millimetres from the wall's node `a` to the opening's CENTRE, measured
|
|
99
|
+
* along the centreline.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately absolute rather than a 0..1 parameter: with a parameter, stretching a
|
|
102
|
+
* wall by dragging one of its nodes would rescale every door on it. Absolute offsets
|
|
103
|
+
* are clamped instead when a wall shortens.
|
|
104
|
+
*/
|
|
105
|
+
offset: number;
|
|
106
|
+
/** Millimetres, along the wall. */
|
|
107
|
+
width: number;
|
|
108
|
+
/**
|
|
109
|
+
* Height of the opening's BOTTOM above the floor, in millimetres: 0 for a door, around
|
|
110
|
+
* 900 for a window. Nothing in the 2D plan reads it, since a plan is a horizontal cut
|
|
111
|
+
* and every opening looks the same from above. It is here so the same plan can drive a
|
|
112
|
+
* 3D view later without the heights having to be invented or asked for again.
|
|
113
|
+
*/
|
|
114
|
+
sill: number;
|
|
115
|
+
/** Height of the opening's TOP above the floor, in millimetres. Typically 2100. */
|
|
116
|
+
head: number;
|
|
117
|
+
/** Only meaningful for `OpeningKind.Door`. */
|
|
118
|
+
swing?: DoorSwing;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Room METADATA. This is not a geometric object: the polygon comes from face
|
|
122
|
+
* extraction. The trailing `wallIds` / `area` fields are a match cache maintained by
|
|
123
|
+
* `derivePlan`, and are the reason a room's name survives edits.
|
|
124
|
+
*/
|
|
125
|
+
export interface PlanRoom {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
/** Any CSS colour. Omit to use the theme default. */
|
|
129
|
+
colour?: string;
|
|
130
|
+
/** Free-form usage tag ("kitchen", "bath"), for later fixture and 3D defaults. */
|
|
131
|
+
usage?: string;
|
|
132
|
+
/**
|
|
133
|
+
* A point that lay inside the room's polygon when it was last written. Used as the
|
|
134
|
+
* label position and as the secondary identity signal during matching.
|
|
135
|
+
*/
|
|
136
|
+
anchor: PlanPoint;
|
|
137
|
+
/** Set once the user drags the label; `derivePlan` then stops moving it. */
|
|
138
|
+
anchorPinned: boolean;
|
|
139
|
+
/** Monotonic creation counter, used as a deterministic tie-break when matching. */
|
|
140
|
+
seq: number;
|
|
141
|
+
/** Match cache: the wall ids on this room's boundary at the last successful match. */
|
|
142
|
+
wallIds: string[];
|
|
143
|
+
/** Match cache: area in mm² at the last successful match. */
|
|
144
|
+
area: number;
|
|
145
|
+
}
|
|
146
|
+
export interface PlanFloor {
|
|
147
|
+
id: string;
|
|
148
|
+
label: string;
|
|
149
|
+
/** Finished floor level of this storey, millimetres above datum. */
|
|
150
|
+
elevation: number;
|
|
151
|
+
/** Default storey height in millimetres, used when a wall omits its own `height`. */
|
|
152
|
+
height: number;
|
|
153
|
+
/**
|
|
154
|
+
* Keyed by id for O(1) lookup. Ids must not be numeric strings ('n_12', never '12'),
|
|
155
|
+
* so that JavaScript preserves insertion order and iteration stays reproducible.
|
|
156
|
+
*/
|
|
157
|
+
nodes: Record<string, PlanNode>;
|
|
158
|
+
walls: Record<string, PlanWall>;
|
|
159
|
+
openings: Record<string, PlanOpening>;
|
|
160
|
+
rooms: Record<string, PlanRoom>;
|
|
161
|
+
}
|
|
162
|
+
export interface FloorPlan {
|
|
163
|
+
id: string;
|
|
164
|
+
name?: string;
|
|
165
|
+
/** Schema version, for future migrations. */
|
|
166
|
+
version: 1;
|
|
167
|
+
/** Display formatting only. Never read by geometry. */
|
|
168
|
+
unitSystem: PlanUnitSystem;
|
|
169
|
+
floors: PlanFloor[];
|
|
170
|
+
/**
|
|
171
|
+
* Bumped by every mutating operation. Use it as the memo key for `derivePlan`, so a
|
|
172
|
+
* gesture that does not actually change the graph does not trigger a re-derive.
|
|
173
|
+
*/
|
|
174
|
+
revision: number;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A raw face from the planar walk, before room metadata is attached.
|
|
178
|
+
*
|
|
179
|
+
* WINDING: `outer` is positively wound under the shoelace formula, which in SVG's
|
|
180
|
+
* y-down space means it reads as visually CLOCKWISE. Anything downstream that assumes
|
|
181
|
+
* counter-clockwise (extruded slab normals, most triangulators) must flip it.
|
|
182
|
+
*/
|
|
183
|
+
export interface PlanFace {
|
|
184
|
+
/**
|
|
185
|
+
* Derived from the boundary itself, so it is stable across derives for as long as
|
|
186
|
+
* the geometry is unchanged. Safe to use as a React key.
|
|
187
|
+
*/
|
|
188
|
+
id: string;
|
|
189
|
+
/** Outer ring, closed implicitly: the first vertex is not repeated at the end. */
|
|
190
|
+
outer: PlanPoint[];
|
|
191
|
+
/** Inner rings from nested components, wound opposite to `outer`. */
|
|
192
|
+
holes: PlanPoint[][];
|
|
193
|
+
/** mm², always positive, with holes already subtracted. */
|
|
194
|
+
area: number;
|
|
195
|
+
/** mm, outer ring only. */
|
|
196
|
+
perimeter: number;
|
|
197
|
+
/**
|
|
198
|
+
* Wall ids on the boundary, in traversal order. A dangling wall inside this face
|
|
199
|
+
* appears TWICE, once per direction, so consumers needing unique ids must dedupe.
|
|
200
|
+
*/
|
|
201
|
+
wallIds: string[];
|
|
202
|
+
nodeIds: string[];
|
|
203
|
+
}
|
|
204
|
+
/** A face with its matched metadata. This is what the components render. */
|
|
205
|
+
export interface DerivedRoom extends PlanFace {
|
|
206
|
+
/** `null` when no `PlanRoom` matched this face during this derive. */
|
|
207
|
+
roomId: string | null;
|
|
208
|
+
meta: PlanRoom | null;
|
|
209
|
+
/** Guaranteed inside `outer` and outside every hole. The label position. */
|
|
210
|
+
anchor: PlanPoint;
|
|
211
|
+
}
|
|
212
|
+
export declare enum PlanIssueKind {
|
|
213
|
+
ZeroLengthWall = "zeroLengthWall",
|
|
214
|
+
SelfLoopWall = "selfLoopWall",
|
|
215
|
+
DuplicateWall = "duplicateWall",
|
|
216
|
+
CoincidentNodes = "coincidentNodes",
|
|
217
|
+
/** Two walls cross with no node at the intersection. Corrupts face extraction. */
|
|
218
|
+
UnsplitCrossing = "unsplitCrossing",
|
|
219
|
+
/** A node sits on a wall's interior but that wall was never split there. */
|
|
220
|
+
UnsplitNodeOnWall = "unsplitNodeOnWall",
|
|
221
|
+
/** Two walls leave a node in the same direction, tying the angular sort. */
|
|
222
|
+
OverlappingWalls = "overlappingWalls",
|
|
223
|
+
OrphanNode = "orphanNode",
|
|
224
|
+
DanglingOpening = "danglingOpening",
|
|
225
|
+
/** A `PlanRoom` matched no face this derive. It is kept, never deleted. */
|
|
226
|
+
UnmatchedRoom = "unmatchedRoom",
|
|
227
|
+
/** An opening was moved or dropped because its wall no longer has room for it. */
|
|
228
|
+
OpeningDropped = "openingDropped"
|
|
229
|
+
}
|
|
230
|
+
export interface PlanIssue {
|
|
231
|
+
kind: PlanIssueKind;
|
|
232
|
+
message: string;
|
|
233
|
+
nodeIds?: string[];
|
|
234
|
+
wallIds?: string[];
|
|
235
|
+
roomIds?: string[];
|
|
236
|
+
openingIds?: string[];
|
|
237
|
+
}
|
|
238
|
+
export interface PlanDerivation {
|
|
239
|
+
/** The floor with room match caches and anchors refreshed. Pure: a new object. */
|
|
240
|
+
floor: PlanFloor;
|
|
241
|
+
rooms: DerivedRoom[];
|
|
242
|
+
issues: PlanIssue[];
|
|
243
|
+
}
|
|
244
|
+
/** The uniform result of every graph mutation. All of them are pure. */
|
|
245
|
+
export interface PlanEdit<T = void> {
|
|
246
|
+
floor: PlanFloor;
|
|
247
|
+
result: T;
|
|
248
|
+
issues: PlanIssue[];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The viewport: which world point sits at the canvas's top-left corner, and how many
|
|
252
|
+
* screen pixels one world millimetre occupies.
|
|
253
|
+
*
|
|
254
|
+
* A 6m plan filling a 600px canvas is `zoom: 0.1`. Note this is a transform on a group,
|
|
255
|
+
* not an SVG `viewBox`: handles, labels and hit tolerances are then sized in screen
|
|
256
|
+
* pixels and stay constant as you zoom, instead of scaling with the drawing.
|
|
257
|
+
*/
|
|
258
|
+
export interface PlanView {
|
|
259
|
+
x: number;
|
|
260
|
+
y: number;
|
|
261
|
+
zoom: number;
|
|
262
|
+
}
|
|
263
|
+
/** What is currently selected or hovered. */
|
|
264
|
+
export type PlanSelection = {
|
|
265
|
+
type: 'room';
|
|
266
|
+
id: string;
|
|
267
|
+
} | {
|
|
268
|
+
type: 'wall';
|
|
269
|
+
id: string;
|
|
270
|
+
} | {
|
|
271
|
+
type: 'opening';
|
|
272
|
+
id: string;
|
|
273
|
+
} | {
|
|
274
|
+
type: 'node';
|
|
275
|
+
id: string;
|
|
276
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the floor plan model.
|
|
3
|
+
*
|
|
4
|
+
* Explicit named exports, NOT `export *`: `scripts/gen-barrel.ts` forwards this folder
|
|
5
|
+
* straight into `package/index.ts`, so anything listed here becomes part of the
|
|
6
|
+
* published `@ahrowe/ui` API. Internal primitives (`cross`, `lerp`, the dart graph)
|
|
7
|
+
* stay unexported and are imported by path within this module.
|
|
8
|
+
*/
|
|
9
|
+
export type { DerivedRoom, FloorPlan, PlanDerivation, PlanEdit, PlanFace, PlanFloor, PlanIssue, PlanNode, PlanOpening, PlanPoint, PlanRoom, PlanSelection, PlanView, PlanWall, } from './floorPlan.types';
|
|
10
|
+
export { DoorSwing, OpeningKind, PlanIssueKind, PlanUnitSystem, WallType } from './floorPlan.types';
|
|
11
|
+
export { addFloor, duplicateFloor, emptyFloor, emptyFloorPlan, floorBelow, floorBounds, floorIndex, getFloor, MIN_FLOOR_HEIGHT, moveFloor, removeFloor, renameFloor, setFloor, setFloorHeight, updateFloor, } from './floorPlan.plan';
|
|
12
|
+
export type { AddFloorOptions, EmptyFloorPlanOptions, PlanBounds } from './floorPlan.plan';
|
|
13
|
+
export { MIN_OPENING_WIDTH, MIN_WALL_LENGTH } from './floorPlan.geometry';
|
|
14
|
+
export { deriveFaces, nodeDegrees } from './floorPlan.faces';
|
|
15
|
+
export { commitDerivation, derivePlan, pruneUnmatchedRooms, roomAtPoint } from './floorPlan.rooms';
|
|
16
|
+
export type { CommitDerivationOptions } from './floorPlan.rooms';
|
|
17
|
+
export { HIT_PX, HIT_PX_COARSE, hitTest, selectionsEqual, worldTolerances } from './floorPlan.hit';
|
|
18
|
+
export type { HitTestOptions, HitTolerances } from './floorPlan.hit';
|
|
19
|
+
export { DEFAULT_WALL_ATTRS, addOpening, addRect, addWall, cleanupPlan, dissolveNode, mergeNodes, moveNode, removeOpening, removeWall, scalePlan, splitWall, transformPlan, updateOpening, validatePlan, wallLength, } from './floorPlan.graph';
|
|
20
|
+
export type { DissolveNodeOptions, ResolveNodeResult, SplitWallResult, WallAttrs, } from './floorPlan.graph';
|
|
21
|
+
export { DEFAULT_SNAP_OPTIONS, SnapKind, clientToWorld, resolveSnap, screenScale } from './floorPlan.snap';
|
|
22
|
+
export type { SnapContext, SnapGuide, SnapOptions, SnapResult } from './floorPlan.snap';
|
|
23
|
+
export { buildWallPaths, openingGeometry, wallCentrelineSpans } from './floorPlan.path';
|
|
24
|
+
export type { BuildWallPathsOptions, WallCapLine, WallCapReason, WallPathGroup, WallPathResult, } from './floorPlan.path';
|
|
25
|
+
export { PLAN_FORMAT_VERSION, planFromJson, planToJson } from './floorPlan.json';
|
|
26
|
+
export type { PlanParseResult } from './floorPlan.json';
|
|
27
|
+
export { planFromLegacySvg } from './legacySvg';
|
|
28
|
+
export type { LegacyImportOptions } from './legacySvg';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { scalePlan } from './floorPlan.graph';
|
|
2
|
+
import { FloorPlan } from './floorPlan.types';
|
|
3
|
+
export interface LegacyImportOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Multiplies every imported coordinate, converting the old unitless canvas into the
|
|
6
|
+
* module's millimetre invariant.
|
|
7
|
+
*
|
|
8
|
+
* There is no correct automatic value: the old format records no scale at all. The
|
|
9
|
+
* default of 1 keeps the numbers as they were, which makes a typical 400x300 export
|
|
10
|
+
* a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.
|
|
11
|
+
*/
|
|
12
|
+
scale?: number;
|
|
13
|
+
/** Wall thickness for imported walls, in millimetres after scaling. */
|
|
14
|
+
thickness?: number;
|
|
15
|
+
/** Tolerance for welding coincident endpoints, in millimetres after scaling. */
|
|
16
|
+
weldTolerance?: number;
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parses a legacy exported SVG into a single-floor `FloorPlan`.
|
|
21
|
+
*
|
|
22
|
+
* Rooms keep their names. Doors are recovered where the old swing-arc path is
|
|
23
|
+
* recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that
|
|
24
|
+
* format, so nothing is lost by not looking for them.
|
|
25
|
+
*/
|
|
26
|
+
export declare function planFromLegacySvg(svg: string, options?: LegacyImportOptions): FloorPlan;
|
|
27
|
+
export { scalePlan };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlanCanvasProps } from './planCanvas.types';
|
|
2
|
+
/**
|
|
3
|
+
* The ref lands on the `<svg>` itself, which is what a consumer wants for measuring or
|
|
4
|
+
* screenshotting. Both the drawer and the viewer expose it as `getSvgElement()`.
|
|
5
|
+
*/
|
|
6
|
+
declare const PlanCanvas: import('react').ForwardRefExoticComponent<PlanCanvasProps & import('react').RefAttributes<SVGSVGElement>>;
|
|
7
|
+
export default PlanCanvas;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { DerivedRoom, PlanFloor, PlanRoom, PlanSelection, PlanView } from '../floorPlan';
|
|
3
|
+
import { SlotClassNames, SlotStyles } from '../types/slots.types';
|
|
4
|
+
export type PlanCanvasSlots = 'canvas' | 'grid' | 'underlay' | 'rooms' | 'roomFill' | 'roomLabel' | 'walls' | 'wallBody' | 'openings' | 'opening' | 'overlay';
|
|
5
|
+
export interface PlanCanvasProps {
|
|
6
|
+
floor: PlanFloor;
|
|
7
|
+
/**
|
|
8
|
+
* A floor traced faintly behind this one, normally the storey below, so walls can be
|
|
9
|
+
* lined up between storeys. Drawn as wall centrelines only: it is a reference, not a
|
|
10
|
+
* second plan, and it takes no pointer events.
|
|
11
|
+
*/
|
|
12
|
+
underlay?: PlanFloor | null;
|
|
13
|
+
/**
|
|
14
|
+
* Derived rooms, passed in rather than computed here so the parent controls
|
|
15
|
+
* memoisation and both components share one derivation per render.
|
|
16
|
+
*/
|
|
17
|
+
rooms: DerivedRoom[];
|
|
18
|
+
view: PlanView;
|
|
19
|
+
/** Everything currently selected. A single selection is a one-element array. */
|
|
20
|
+
selection?: readonly PlanSelection[];
|
|
21
|
+
hovered?: PlanSelection | null;
|
|
22
|
+
/** Makes room fills focusable, clickable and screen-reader labelled. */
|
|
23
|
+
interactiveRooms?: boolean;
|
|
24
|
+
interactiveOpenings?: boolean;
|
|
25
|
+
showGrid?: boolean;
|
|
26
|
+
/** Grid spacing in millimetres. */
|
|
27
|
+
gridSize?: number;
|
|
28
|
+
showRoomLabels?: boolean;
|
|
29
|
+
/** Door leaves, swing arcs and window panes. `false` leaves plain gaps. */
|
|
30
|
+
showOpeningSymbols?: boolean;
|
|
31
|
+
getRoomColor?: (room: PlanRoom, area: number) => string | undefined;
|
|
32
|
+
renderRoomLabel?: (room: PlanRoom | null, context: {
|
|
33
|
+
area: number;
|
|
34
|
+
faceId: string;
|
|
35
|
+
}) => ReactNode;
|
|
36
|
+
/** Accessible name for a room. */
|
|
37
|
+
getRoomLabel?: (room: PlanRoom | null, context: {
|
|
38
|
+
area: number;
|
|
39
|
+
}) => string;
|
|
40
|
+
formatArea?: (areaMm2: number) => string;
|
|
41
|
+
onRoomActivate?: (roomId: string) => void;
|
|
42
|
+
onOpeningActivate?: (openingId: string) => void;
|
|
43
|
+
onBackgroundActivate?: () => void;
|
|
44
|
+
onHoverChange?: (target: PlanSelection | null) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Extra SVG drawn above everything, in SCREEN space: handles, previews, badges.
|
|
47
|
+
* The drawer supplies its editing chrome through this.
|
|
48
|
+
*/
|
|
49
|
+
overlay?: ReactNode;
|
|
50
|
+
/** Extra SVG drawn in WORLD space, below the screen-space overlay. */
|
|
51
|
+
worldOverlay?: ReactNode;
|
|
52
|
+
/** Width and height of the canvas in CSS pixels, measured by the parent. */
|
|
53
|
+
size: {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
'aria-label'?: string;
|
|
58
|
+
className?: string;
|
|
59
|
+
style?: CSSProperties;
|
|
60
|
+
classNames?: SlotClassNames<PlanCanvasSlots>;
|
|
61
|
+
styles?: SlotStyles<PlanCanvasSlots>;
|
|
62
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export default
|
|
1
|
+
import { RoomDrawerHandle, RoomDrawerProps } from './roomDrawer.types';
|
|
2
|
+
declare const RoomDrawer: import('react').ForwardRefExoticComponent<RoomDrawerProps & import('react').RefAttributes<RoomDrawerHandle>>;
|
|
3
|
+
export default RoomDrawer;
|