@ahrowe/ui 0.41.2 → 0.42.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.
Files changed (62) hide show
  1. package/dist/cjs-types/common/floorPlan/floorPlan.graph.d.ts +10 -3
  2. package/dist/cjs-types/common/floorPlan/floorPlan.plan.d.ts +5 -0
  3. package/dist/cjs-types/common/floorPlan/floorPlan.rooms.d.ts +6 -2
  4. package/dist/cjs-types/common/floorPlan/floorPlan.snap.d.ts +1 -3
  5. package/dist/cjs-types/common/floorPlan/index.d.ts +1 -3
  6. package/dist/cjs-types/common/planCanvas/planCanvas.types.d.ts +1 -1
  7. package/dist/cjs-types/common/roomDrawer/roomDrawer.types.d.ts +29 -8
  8. package/dist/cjs-types/common/roomViewer/roomViewer.types.d.ts +4 -2
  9. package/dist/esm/common/floorPlan/floorPlan.faces.mjs +1 -1
  10. package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -1
  11. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +1 -1
  12. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -1
  13. package/dist/esm/common/floorPlan/floorPlan.graph.mjs +1 -1
  14. package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -1
  15. package/dist/esm/common/floorPlan/floorPlan.hit.mjs +1 -1
  16. package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -1
  17. package/dist/esm/common/floorPlan/floorPlan.json.mjs +1 -1
  18. package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -1
  19. package/dist/esm/common/floorPlan/floorPlan.mesh.mjs +1 -1
  20. package/dist/esm/common/floorPlan/floorPlan.mesh.mjs.map +1 -1
  21. package/dist/esm/common/floorPlan/floorPlan.path.mjs +1 -1
  22. package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -1
  23. package/dist/esm/common/floorPlan/floorPlan.plan.mjs +1 -1
  24. package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -1
  25. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +1 -1
  26. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -1
  27. package/dist/esm/common/floorPlan/floorPlan.snap.mjs +1 -1
  28. package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -1
  29. package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs +1 -1
  30. package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs.map +1 -1
  31. package/dist/esm/common/floorPlan/floorPlan.types.mjs +1 -1
  32. package/dist/esm/common/floorPlan/floorPlan.types.mjs.map +1 -1
  33. package/dist/esm/common/planCanvas/planCanvas.mjs +1 -1
  34. package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -1
  35. package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
  36. package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
  37. package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
  38. package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
  39. package/dist/esm/floor-plan.mjs +1 -0
  40. package/dist/esm/index.mjs +1 -1
  41. package/dist/floor-plan.cjs +1 -0
  42. package/dist/floorPlan-DLe5XAx6.cjs +2 -0
  43. package/dist/floorPlan-DLe5XAx6.cjs.map +1 -0
  44. package/dist/index.cjs +3 -3
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/types/common/floorPlan/floorPlan.graph.d.ts +10 -3
  47. package/dist/types/common/floorPlan/floorPlan.plan.d.ts +5 -0
  48. package/dist/types/common/floorPlan/floorPlan.rooms.d.ts +6 -2
  49. package/dist/types/common/floorPlan/floorPlan.snap.d.ts +1 -3
  50. package/dist/types/common/floorPlan/index.d.ts +1 -3
  51. package/dist/types/common/planCanvas/planCanvas.types.d.ts +1 -1
  52. package/dist/types/common/roomDrawer/roomDrawer.types.d.ts +29 -8
  53. package/dist/types/common/roomViewer/roomViewer.types.d.ts +4 -2
  54. package/docs/CLAUDE.md +3 -1
  55. package/docs/FloorPlan.md +18 -14
  56. package/docs/RoomDrawer.md +42 -13
  57. package/docs/RoomViewer.md +1 -14
  58. package/package.json +18 -1
  59. package/dist/cjs-types/common/floorPlan/legacySvg.d.ts +0 -27
  60. package/dist/esm/common/floorPlan/legacySvg.mjs +0 -2
  61. package/dist/esm/common/floorPlan/legacySvg.mjs.map +0 -1
  62. package/dist/types/common/floorPlan/legacySvg.d.ts +0 -27
@@ -5,6 +5,13 @@ import { PlanEdit, PlanFloor, PlanIssue, PlanNode, PlanOpening, PlanPoint, PlanW
5
5
  * reproducible.
6
6
  */
7
7
  export declare function newId(prefix: string): string;
8
+ /**
9
+ * The first of `nameFor(start)`, `nameFor(start + 1)`, … that `used` does not hold.
10
+ *
11
+ * Bounded because `nameFor` is the caller's: one that ignores its number would never reach a
12
+ * free name. An injective one always finds one within `used.size + 1` tries.
13
+ */
14
+ export declare function freshName(nameFor: (n: number) => string, start: number, used: ReadonlySet<string>): string;
8
15
  /** Test-only: makes generated ids reproducible within a single test. */
9
16
  export declare function __resetIdSeq(): void;
10
17
  declare function wallLength(floor: PlanFloor, wall: PlanWall): number;
@@ -157,9 +164,9 @@ export declare function transformPlan(floor: PlanFloor, dx: number, dy: number):
157
164
  /**
158
165
  * Multiplies every coordinate and dimension by `factor`.
159
166
  *
160
- * The one place a scale factor belongs: converting a unitless legacy plan into the
161
- * module's millimetre invariant, once, at the import boundary. The caller picks the
162
- * factor, because nothing in the old data says what it should be.
167
+ * The one place a scale factor belongs: converting geometry drawn in another unit into
168
+ * the module's millimetre invariant, once, where it enters. The caller picks the factor,
169
+ * because nothing in the data says what it should be.
163
170
  */
164
171
  export declare function scalePlan(floor: PlanFloor, factor: number): PlanFloor;
165
172
  export declare function addOpening(floor: PlanFloor, opening: Omit<PlanOpening, 'id'>): PlanEdit<{
@@ -34,6 +34,11 @@ export declare function floorIndex(plan: FloorPlan, floorId: string): number;
34
34
  export declare function floorBelow(plan: FloorPlan, floorId: string): PlanFloor | null;
35
35
  export interface AddFloorOptions {
36
36
  label?: string;
37
+ /**
38
+ * Names the floor when `label` is not given. `n` is the floor count plus one, moved past
39
+ * any number whose name is taken. Default `` (n) => `Floor ${n}` ``.
40
+ */
41
+ nameFor?: (n: number) => string;
37
42
  /** Millimetres. Defaults to the storey height of the floor it is added above. */
38
43
  height?: number;
39
44
  /** Where to put it. Defaults to directly above `aboveFloorId`, else on top. */
@@ -22,8 +22,12 @@ export declare function derivePlan(floor: PlanFloor): PlanDerivation;
22
22
  /** Test-only: makes the room sequence counter reproducible. */
23
23
  export declare function __resetRoomSeq(): void;
24
24
  export interface CommitDerivationOptions {
25
- /** Names new rooms. Receives the 1-based index among rooms created in this call. */
26
- nameFor?: (index: number, face: DerivedRoom) => string;
25
+ /**
26
+ * Names a new room. `n` is the floor's room count plus one, moved past any number whose
27
+ * name is taken, so `` (n) => `Raum ${n}` `` numbers rooms the way the default does.
28
+ * Default `` (n) => `Room ${n}` ``.
29
+ */
30
+ nameFor?: (n: number, face: DerivedRoom) => string;
27
31
  }
28
32
  /**
29
33
  * Mints a `PlanRoom` for every face that matched nothing.
@@ -35,7 +35,7 @@ export interface SnapOptions {
35
35
  export declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
36
36
  export interface SnapContext {
37
37
  floor: PlanFloor;
38
- /** World units per screen pixel at the current zoom. See `screenScale`. */
38
+ /** World units per screen pixel at the current zoom: `1 / view.zoom`. */
39
39
  pxToWorld: number;
40
40
  /** The anchored point. Required for angle lock; also enables the length readout. */
41
41
  origin?: PlanPoint;
@@ -70,8 +70,6 @@ export interface SnapResult {
70
70
  */
71
71
  angleRelativeTo?: 'world' | 'reference';
72
72
  }
73
- /** World units per screen pixel, from the element's current transform. */
74
- export declare function screenScale(svg: SVGSVGElement): number;
75
73
  /**
76
74
  * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.
77
75
  *
@@ -18,7 +18,7 @@ export { HIT_PX, HIT_PX_COARSE, hitTest, selectionsEqual, worldTolerances } from
18
18
  export type { HitTestOptions, HitTolerances } from './floorPlan.hit.js';
19
19
  export { DEFAULT_WALL_ATTRS, addOpening, addRect, addWall, cleanupPlan, detachWall, dissolveNode, mergeNodes, moveNode, removeOpening, removeWall, scalePlan, splitWall, transformPlan, updateOpening, validatePlan, wallLength, weldShortWalls, } from './floorPlan.graph.js';
20
20
  export type { DetachWallResult, DissolveNodeOptions, ResolveNodeResult, SplitWallResult, WallAttrs, } from './floorPlan.graph.js';
21
- export { DEFAULT_SNAP_OPTIONS, SnapKind, clientToWorld, resolveSnap, screenScale } from './floorPlan.snap.js';
21
+ export { DEFAULT_SNAP_OPTIONS, SnapKind, clientToWorld, resolveSnap } from './floorPlan.snap.js';
22
22
  export type { SnapContext, SnapGuide, SnapOptions, SnapResult } from './floorPlan.snap.js';
23
23
  export { buildWallPaths, openingGeometry, wallCentrelineSpans } from './floorPlan.path.js';
24
24
  export type { BuildWallPathsOptions, WallCapLine, WallCapReason, WallPathGroup, WallPathResult, } from './floorPlan.path.js';
@@ -26,5 +26,3 @@ export { PLAN_FORMAT_VERSION, planFromJson, planToJson } from './floorPlan.json.
26
26
  export type { PlanParseResult } from './floorPlan.json.js';
27
27
  export { buildPlanMesh } from './floorPlan.mesh.js';
28
28
  export type { BuildPlanMeshOptions, PlanMesh, PlanMeshGroup, PlanMeshPart } from './floorPlan.mesh.js';
29
- export { planFromLegacySvg } from './legacySvg.js';
30
- export type { LegacyImportOptions } from './legacySvg.js';
@@ -33,7 +33,7 @@ export interface PlanCanvasProps {
33
33
  area: number;
34
34
  faceId: string;
35
35
  }) => ReactNode;
36
- /** Accessible name for a room. */
36
+ /** Accessible name for an interactive room. RoomViewer supplies the default, from its labels. */
37
37
  getRoomLabel?: (room: PlanRoom | null, context: {
38
38
  area: number;
39
39
  }) => string;
@@ -27,7 +27,10 @@ export interface RoomDrawerChangeMeta {
27
27
  /** Ids of whatever the change touched, for syncing an external selection. */
28
28
  targetIds: string[];
29
29
  }
30
- /** Every string the drawer's own chrome renders, for localisation. */
30
+ /**
31
+ * Every string the drawer produces of its own, for localisation: its chrome, and the names it
32
+ * gives the rooms and floors it creates, which are stored in the plan.
33
+ */
31
34
  export interface RoomDrawerLabels {
32
35
  /** The toolbar, read out by screen readers. */
33
36
  tools?: string;
@@ -54,12 +57,21 @@ export interface RoomDrawerLabels {
54
57
  floorHeight?: string;
55
58
  moveFloorUp?: string;
56
59
  moveFloorDown?: string;
60
+ /** Name of a new room. `n` counts the floor's rooms, skipping names already taken. */
61
+ newRoomName?: (n: number) => string;
62
+ /** Name of a floor added with the `+` button. `n` counts the floors, skipping names already taken. */
63
+ newFloorName?: (n: number) => string;
64
+ /** Name of a copied floor, from the name of the floor it copies. */
65
+ copiedFloorName?: (name: string) => string;
66
+ /** Name of the one floor an empty plan starts with, uncontrolled or after `clear()`. */
67
+ initialFloorName?: string;
57
68
  /** The inspector's noun for a floor, shown when nothing is selected. */
58
69
  selectedFloor?: string;
59
70
  /** Shown in the hint line when an opened file could not be read at all. */
60
71
  importFailed?: string;
61
72
  snap?: string;
62
- angleLock?: string;
73
+ /** The angle lock toggle, given `constrainAngle`. */
74
+ angleLock?: (angle: number) => string;
63
75
  grid?: string;
64
76
  zoomIn?: string;
65
77
  zoomOut?: string;
@@ -69,22 +81,27 @@ export interface RoomDrawerLabels {
69
81
  thickness?: string;
70
82
  width?: string;
71
83
  sill?: string;
72
- head?: string;
73
84
  deleteSelection?: string;
74
- /** Shown when several things are selected. `{count}` is replaced with the number. */
75
- selectedCount?: string;
85
+ /** What the inspector calls several things selected at once. */
86
+ selectedCount?: (count: number) => string;
76
87
  /** What the inspector calls the current selection. */
77
- selectedNothing?: string;
78
88
  selectedRoom?: string;
79
89
  selectedWall?: string;
80
90
  selectedCorner?: string;
81
- /** Overrides the opening's own kind, which is used by default. */
82
- selectedOpening?: string;
91
+ selectedDoor?: string;
92
+ selectedWindow?: string;
93
+ selectedArchway?: string;
94
+ selectedGap?: string;
95
+ /** Hint for the select tool with `multiSelect` on. */
83
96
  hintSelect?: string;
97
+ /** Hint for the select tool with `multiSelect` off. */
98
+ hintSelectSingle?: string;
84
99
  hintWall?: string;
85
100
  hintRect?: string;
86
101
  hintMeasure?: string;
87
102
  hintOpening?: string;
103
+ /** A drawing angle measured from the previous wall, given the formatted angle. */
104
+ relativeAngle?: (angle: string) => string;
88
105
  }
89
106
  /** Every icon in the chrome is overridable. */
90
107
  export interface RoomDrawerIcons {
@@ -259,6 +276,10 @@ export interface RoomDrawerProps extends Omit<HtmlProps<HTMLDivElement>, 'onChan
259
276
  showMeasurements?: boolean;
260
277
  formatLength?: (lengthMm: number) => string;
261
278
  formatArea?: (areaMm2: number) => string;
279
+ /** A drawing angle, in degrees from -180 to 180. */
280
+ formatAngle?: (degrees: number) => string;
281
+ /** The zoom readout. `zoom` is screen pixels per millimetre, so `1` reads as 100%. */
282
+ formatZoom?: (zoom: number) => string;
262
283
  showToolbar?: boolean;
263
284
  showInspector?: boolean;
264
285
  showHint?: boolean;
@@ -10,8 +10,10 @@ export interface RoomViewerLabels {
10
10
  zoomOut?: string;
11
11
  fit?: string;
12
12
  empty?: string;
13
- /** Announced when a room is selected. `{name}` and `{area}` are replaced. */
14
- selectedRoom?: string;
13
+ /** Stands in for the name of a room that has none. */
14
+ unnamedRoom?: string;
15
+ /** Announced when a room is selected, given its name and its formatted area. */
16
+ selectedRoom?: (name: string, area: string) => string;
15
17
  /** Announced when the selection is cleared. */
16
18
  noRoomSelected?: string;
17
19
  }
package/docs/CLAUDE.md CHANGED
@@ -39,7 +39,9 @@ enums and slot names. Do not infer props from a component's name.
39
39
  Input, Textarea, NumberInput and SearchInput hand out their inner `<input>`. Timer, Flip, Tilt,
40
40
  Ripple, VirtualList, RoomDrawer and RoomViewer expose an imperative handle instead; its type is
41
41
  exported next to the props.
42
- - Every module is `'use client'`; nothing renders on the server. No right-to-left support.
42
+ - Every module is `'use client'`; nothing renders on the server. The exception is
43
+ `@ahrowe/ui/floor-plan`, the plan model (FloorPlan.md) without React, for server code that reads
44
+ or validates a plan. No right-to-left support.
43
45
  `prefers-reduced-motion` is honoured. Built for Chrome and Edge 111, Firefox 114, Safari 16.4
44
46
  and newer, with no polyfills. The package is 0.x: a minor can break, a patch cannot.
45
47
 
package/docs/FloorPlan.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Keywords:** blueprint, cad, grundriss, apartment, building, storey, partition, area calculation, planar graph, half-edge, extrusion
6
6
 
7
- **Import:** `import { emptyFloorPlan, addWall, derivePlan } from '@ahrowe/ui'`
7
+ **Import:** `import { emptyFloorPlan, addWall, derivePlan } from '@ahrowe/ui'`, or from `'@ahrowe/ui/floor-plan'` where there is no React
8
8
  **Types:** `import type { FloorPlan, PlanFloor, PlanNode, PlanWall, PlanOpening, PlanRoom, DerivedRoom, PlanPoint, PlanView, PlanSelection } from '@ahrowe/ui'`
9
9
 
10
10
  `PlanPoint`, `PlanView` and `PlanSelection` are the ones that also appear in `RoomDrawer` and `RoomViewer` props. Every exported function's own option and result types come with it — `WallAttrs`, `SplitWallResult`, `WallPathResult` and the rest.
@@ -80,7 +80,7 @@ const { floor: refreshed, rooms, issues } = derivePlan(floor);
80
80
 
81
81
  `derivePlan` extracts the faces and attaches stored metadata. It is pure and **never creates or deletes a `PlanRoom`**, so it is safe to call on every render. Memoise it on `plan.revision`, which every mutation bumps.
82
82
 
83
- `commitDerivation(floor)` mints a `PlanRoom` for each face that matched nothing — call it once per committed edit, not per render.
83
+ `commitDerivation(floor, options?)` mints a `PlanRoom` for each face that matched nothing — call it once per committed edit, not per render. It names them `Room 1`, `Room 2`, … counting the floor's rooms and skipping any name already taken; `nameFor: (n, face) => string` replaces the name with the same numbering, so `` (n) => `Raum ${n}` `` gives `Raum 3` on a floor with two rooms. `addFloor` takes the same `nameFor` for `Floor 2`, `Floor 3`, …, and `duplicateFloor` a `label` in place of `<name> copy`.
84
84
 
85
85
  Each `DerivedRoom` carries the face (`outer`, `holes`, `area`, `perimeter`, `wallIds`), the matched `meta`, and an `anchor` guaranteed to be **inside** the polygon (an area centroid can fall outside a concave room, so it is computed, not averaged).
86
86
 
@@ -172,6 +172,22 @@ else if (issues.length) console.warn(issues); // loaded, but repaired
172
172
 
173
173
  `revision` is bumped on load, since `derivePlan` memoises on it and the repaired plan is not the one the file described.
174
174
 
175
+ ### On a server
176
+
177
+ `@ahrowe/ui/floor-plan` is the whole model, and only the model: every function and type on this page, with no React, react-dom or FontAwesome behind it and no `'use client'` in front of it. That is what lets a Node server check a plan with the same code the client drew it with, instead of storing whatever it was sent.
178
+
179
+ ```ts
180
+ import { planFromJson, validatePlan } from '@ahrowe/ui/floor-plan';
181
+
182
+ export function readPlan(json: string) {
183
+ const { plan, issues } = planFromJson(json);
184
+ if (!plan) throw new Error(issues.join('; '));
185
+ return { plan, repaired: issues, broken: plan.floors.flatMap((floor) => validatePlan(floor)) };
186
+ }
187
+ ```
188
+
189
+ `@ahrowe/ui` exports the same functions, so client code can keep importing from there.
190
+
175
191
  ## Snapping and pointer input
176
192
 
177
193
  ```tsx
@@ -195,18 +211,6 @@ const issues = validatePlan(floor); // O(walls²) — development and tests, not
195
211
 
196
212
  The invariant that matters: **no two walls may cross without a node at the intersection, and no node may sit on a wall's interior without that wall being split there.** Break either and the face walk silently merges two rooms into one figure-eight face with no error anywhere, which is why `UnsplitCrossing` and `UnsplitNodeOnWall` exist as explicit issue kinds. Everything built through `addWall` maintains this automatically.
197
213
 
198
- ## Importing older plans
199
-
200
- ```tsx
201
- import { planFromLegacySvg } from '@ahrowe/ui';
202
-
203
- // The old canvas was unitless; pick the factor that makes it millimetres.
204
- // A 400-unit-wide export meant to be a 12m house wants scale: 30.
205
- const plan = planFromLegacySvg(svgText, { scale: 30 });
206
- ```
207
-
208
- Feeding every legacy polygon edge through `addWall` is what collapses the old duplicated walls: the second room's copy of a shared edge resolves to the same two nodes and is recognised as already walled. Room names carry across, and doors are recovered where the old swing-arc path is recognisable; anything that is not is skipped rather than guessed at.
209
-
210
214
  ## Ready for 3D
211
215
 
212
216
  `buildPlanMesh(floor, options?)` turns a storey into triangles: wall solids with their openings cut out, and a slab under each room.
@@ -246,7 +246,7 @@ ref.current?.deleteSelection();
246
246
  | `wallThickness` / `wallHeight` | `number` | Millimetres, applied to newly drawn walls |
247
247
  | `openingDefaults` | `Partial<Record<OpeningKind, { width?, sill?, head? }>>` | Size of newly placed openings |
248
248
  | `showMeasurements` | `boolean` | Live length, angle and area readouts while drawing (default `true`) |
249
- | `formatLength` / `formatArea` | `(mm) => string` | Override the readouts |
249
+ | `formatLength` / `formatArea` / `formatAngle` / `formatZoom` | `(value) => string` | Override the readouts: millimetres, mm², degrees from -180 to 180, and zoom in screen px per mm. See [Localisation](#localisation) |
250
250
  | `showToolbar` / `showInspector` / `showHint` / `showHistoryControls` / `showFileControls` / `showFloorBar` | `boolean` | Hide parts of the chrome |
251
251
  | `floorId` / `defaultFloorId` / `onFloorChange` | `string` / `string` / `(id) => void` | Which storey is being edited |
252
252
  | `showUnderlay` | `boolean` | Trace the storey below behind this one (default `true`) |
@@ -258,7 +258,7 @@ ref.current?.deleteSelection();
258
258
  | `disabled` | `boolean` | No interaction at all |
259
259
  | `canvasHeight` | `string` | Any CSS length (default `'420px'`) |
260
260
  | `getRoomColor` / `renderRoomLabel` | | Per-room fill colour and label content |
261
- | `labels` / `icons` | `RoomDrawerLabels` / `RoomDrawerIcons` | Localise or re-icon the chrome |
261
+ | `labels` / `icons` | `RoomDrawerLabels` / `RoomDrawerIcons` | Localise or re-icon the chrome, and name new rooms and floors |
262
262
 
263
263
  `RoomDrawerProps` extends the full native `div` attribute set plus `data-*`, so `id`, `title`, `aria-*` and the rest pass through to the root.
264
264
 
@@ -292,7 +292,7 @@ Floors are part of the plan document, so they survive a download and reopen with
292
292
 
293
293
  A bar under the canvas shows what is selected and what can be done with it: a name for a room, thickness for a wall, width and sill for an opening, and delete for any of them. It is **always there and always the same height**, including when nothing is selected — it used to appear on selection and size itself to its contents, which moved the canvas out from under the pointer by up to 116px as you clicked between a corner and a wall.
294
294
 
295
- Each selection is named on the left (`Room`, `Wall`, `Door`, `Corner`, `3 selected`), which is the whole content of the bar for a corner: a corner has nothing to edit that dragging it does not already do. Delete sits at the far end, apart from the fields, since it is the one action that applies to every kind of selection and the only destructive one. Override any of the names through `labels` (`selectedRoom`, `selectedWall`, `selectedCorner`, `selectedOpening`, `selectedCount`, `selectedNothing`), and hide the bar with `showInspector={false}`.
295
+ Each selection is named on the left (`Room`, `Wall`, `Door`, `Corner`, `3 selected`), which is the whole content of the bar for a corner: a corner has nothing to edit that dragging it does not already do. Delete sits at the far end, apart from the fields, since it is the one action that applies to every kind of selection and the only destructive one. Override any of the names through `labels` (`selectedRoom`, `selectedWall`, `selectedCorner`, `selectedDoor`, `selectedWindow`, `selectedArchway`, `selectedGap`, `selectedCount`), and hide the bar with `showInspector={false}`.
296
296
 
297
297
  **Sill**, on an opening, is how far above the floor it starts: 0 for a door, around 900 for a window. It changes nothing in the plan, because a plan is a horizontal cut and every opening looks the same from above. It is stored so the same plan can drive a 3D view later without anyone having to supply the heights again — as are `head` (the top of the opening), `PlanWall.height` and `PlanFloor.elevation`.
298
298
 
@@ -358,20 +358,49 @@ Stated plainly: **a free-form drawing canvas cannot be fully operated from a key
358
358
 
359
359
  The root is `role="application"` (justified here, since single-letter keys are intercepted), the toolbar is a real `role="toolbar"` of `<button>`s with `aria-pressed`, and room labels render with a halo so they stay legible over any fill colour a consumer sets.
360
360
 
361
- ## Global defaults
361
+ ## Localisation
362
362
 
363
- Adopts `ConfigProvider`, e.g. `defaultProps={{ RoomDrawer: { gridSize: 250, showGrid: false } }}`. See [ConfigProvider.md](ConfigProvider.md).
363
+ Every string the drawer produces goes through `labels`, and every number it prints through a `format*` prop. Set them once, app-wide: labels merge per key, so an instance that overrides one string keeps the rest.
364
+
365
+ Some of those strings end up **in the plan**, not only on screen: the names of the rooms and floors the drawer creates. A plan saved before they were translated keeps "Room 1" and "Ground floor" for good, so set them from the start.
364
366
 
365
- ## Migration from the previous version
367
+ ```tsx
368
+ const number = new Intl.NumberFormat('de-AT', { maximumFractionDigits: 1 });
369
+ const percent = new Intl.NumberFormat('de-AT', { style: 'percent', maximumFractionDigits: 1 });
370
+
371
+ <AhroweProvider
372
+ defaultProps={{
373
+ RoomDrawer: {
374
+ labels: {
375
+ newRoomName: (n) => `Raum ${n}`,
376
+ newFloorName: (n) => `Etage ${n}`,
377
+ copiedFloorName: (name) => `${name} (Kopie)`,
378
+ initialFloorName: 'Erdgeschoss',
379
+ angleLock: (angle) => `Auf ${angle}°-Schritte einrasten`,
380
+ selectedCount: (count) => `${count} ausgewählt`,
381
+ selectedDoor: 'Tür',
382
+ hintSelect: 'Ecke ziehen zum Umformen · …',
383
+ hintSelectSingle: 'Ecke ziehen zum Umformen · …',
384
+ },
385
+ formatLength: (mm) => `${number.format(mm / 1000)} m`,
386
+ formatArea: (mm2) => `${number.format(mm2 / 1e6)} m²`,
387
+ formatAngle: (degrees) => `${number.format(degrees)}°`,
388
+ formatZoom: (zoom) => percent.format(zoom),
389
+ },
390
+ }}
391
+ >
392
+ <App />
393
+ </AhroweProvider>
394
+ ```
366
395
 
367
- The previous `RoomDrawer` had no data contract at all: `RoomDrawerProps` was `{ className }`, and the only way data left the component was a browser file download. Everything below is new rather than changed, so the old usage still renders, it just starts empty.
396
+ A label that carries a value is a function of it: `angleLock` gets `constrainAngle`, `selectedCount` the count, `copiedFloorName` the name of the floor being copied, and `newRoomName` and `newFloorName` the next number whose name is not taken yet. `hintSelect` is the hint with `multiSelect` on and `hintSelectSingle` the one with it off. `formatZoom` gets screen pixels per millimetre, which is the fraction `Intl`'s `percent` style expects.
368
397
 
369
- - `Tool`, `RoomData`, `DoorData`, `Point` are gone. Use `RoomDrawerTool` and the `FloorPlan` model.
370
- - `selection` is an array rather than a single entry, since the drawer supports multi-select. A single selection is `[entry]`, and nothing selected is `[]` rather than `null`.
371
- - The SVG download and upload buttons are gone. The plan JSON is the interchange format now. To read an SVG exported by the old version, use `planFromLegacySvg` (see [FloorPlan.md](FloorPlan.md)), which also collapses each duplicated room boundary into the single shared wall it should have been.
372
- - The `polygon` tool is now `wall`, which draws open chains as well as closed ones.
373
- - Removing a corner was a double-click, which was undiscoverable and impossible on touch. Select it and press `Delete` instead.
398
+ Two texts are not in `labels`. The root's accessible name is the `aria-label` prop (default "Floor plan editor"), which `defaultProps` can set as well. The repair report after opening a file is `planFromJson`'s list of English sentences, shown as it comes; `onImport` receives it too, if you would rather show your own.
399
+
400
+ ## Global defaults
401
+
402
+ Adopts `ConfigProvider`, e.g. `defaultProps={{ RoomDrawer: { gridSize: 250, showGrid: false } }}`. See [ConfigProvider.md](ConfigProvider.md).
374
403
 
375
404
  **Slots:** `root` `toolbar` `toolButton` `toolIcon` `viewControls` `viewButton` `zoomLabel` `canvasWrapper` `canvas` `grid` `rooms` `roomFill` `roomLabel` `walls` `wallBody` `openings` `opening` `overlay` `measureLine` `floorBar` `floorTab` `inspector` `inspectorLabel` `inspectorField` `hint`
376
405
 
377
- **Labels:** every string the drawer's own chrome renders, including `tools`, `storeyHeightHint` and `sillHeightHint`. Type: `RoomDrawerLabels`.
406
+ **Labels:** every string the drawer's own chrome renders, including `tools`, `storeyHeightHint` and `sillHeightHint`, and the names it gives new rooms and floors. Type: `RoomDrawerLabels`.
@@ -72,7 +72,7 @@ Walls are drawn as mass rather than as ink: the body is `var(--text-color)` mixe
72
72
  | `getRoomColor` / `renderRoomLabel` / `getRoomLabel` / `formatArea` | functions | Colour, label and area rendering |
73
73
  | `height` | `number \| string` | Canvas height (default `360`) |
74
74
  | `emptyState` | `ReactNode` | Shown when there is no plan, or nothing in it |
75
- | `labels` / `icons` | objects | Every string and icon in the chrome |
75
+ | `labels` / `icons` | objects | Every string and icon in the chrome. `selectedRoom` is `(name, area) => string`, and `unnamedRoom` stands in for a room with no name |
76
76
 
77
77
  **Imperative handle:** `fitToContent(options?)`, `getView()`, `setView(view)`, `zoomBy(factor)`, `select(roomId)`, `getFloorId()`, `setFloorId(id)`, `getSvgElement()`.
78
78
 
@@ -81,16 +81,3 @@ Walls are drawn as mass rather than as ink: the body is `var(--text-color)` mixe
81
81
  ## Accessibility, honestly
82
82
 
83
83
  A free-form plan cannot be fully keyboard operated, and this does not pretend otherwise. What it does give you: every room is a focusable `role="button"` with an accessible name, `Enter`/`Space` select, the floor tabs are real buttons, the zoom controls are real buttons, and the current selection is announced. Panning and zooming are pointer-only; the zoom buttons and `fitToContent()` are the keyboard path.
84
-
85
- ## Migration from the previous version
86
-
87
- The old viewer took `data: { label, data: ReactNode }[]` — an array of **pre-rendered SVG elements** — and found rooms by looking for `id="room…"`, highlighting them by mutating `classList`. That is gone, and there is no shim: the old `data` was a React element rather than a string, so there is nothing to parse at runtime.
88
-
89
- | Before | Now |
90
- |--------|-----|
91
- | `data={[{ label, data: <svg/> }]}` | `plan={plan}` — one `FloorPlan`, floors and all |
92
- | `id="room12"` convention | `PlanRoom` ids in the model |
93
- | Highlighting via `classList` | `selectedRoomId` / `hoveredRoomId` |
94
- | No selection callback | `onSelect(roomId, room, floorId)` |
95
-
96
- To bring an old SVG across, run it through `planFromLegacySvg` once and keep the resulting plan. See [FloorPlan](./FloorPlan.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.41.2",
3
+ "version": "0.42.0",
4
4
  "description": "A React UI component library with theming, CSS Modules, and TypeScript support",
5
5
  "keywords": [
6
6
  "agent",
@@ -39,6 +39,13 @@
39
39
  "main": "./dist/index.cjs",
40
40
  "module": "./dist/esm/index.mjs",
41
41
  "types": "./dist/types/index.d.ts",
42
+ "typesVersions": {
43
+ "*": {
44
+ "floor-plan": [
45
+ "./dist/types/common/floorPlan/index.d.ts"
46
+ ]
47
+ }
48
+ },
42
49
  "exports": {
43
50
  ".": {
44
51
  "import": {
@@ -50,6 +57,16 @@
50
57
  "default": "./dist/index.cjs"
51
58
  }
52
59
  },
60
+ "./floor-plan": {
61
+ "import": {
62
+ "types": "./dist/types/common/floorPlan/index.d.ts",
63
+ "default": "./dist/esm/floor-plan.mjs"
64
+ },
65
+ "require": {
66
+ "types": "./dist/cjs-types/common/floorPlan/index.d.ts",
67
+ "default": "./dist/floor-plan.cjs"
68
+ }
69
+ },
53
70
  "./style.css": "./dist/style.css",
54
71
  "./package.json": "./package.json"
55
72
  },
@@ -1,27 +0,0 @@
1
- import { scalePlan } from './floorPlan.graph.js';
2
- import { FloorPlan } from './floorPlan.types.js';
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 };
@@ -1,2 +0,0 @@
1
- "use client";import{OpeningKind as e,WallType as t}from"./floorPlan.types.mjs";import{DEFAULT_WALL_ATTRS as n,addOpening as r,addWall as i}from"./floorPlan.graph.mjs";import{emptyFloorPlan as a}from"./floorPlan.plan.mjs";import{commitDerivation as o,derivePlan as s}from"./floorPlan.rooms.mjs";function c(e){let t=[];for(let n of e.matchAll(/[ML]\s*(-?[\d.]+)[,\s]+(-?[\d.]+)/gi))t.push({x:parseFloat(n[1]),y:parseFloat(n[2])});return t}function l(e){let t=e.match(/^\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*L\s*(-?[\d.]+),(-?[\d.]+)\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*A\s/i);return t?{p1:{x:parseFloat(t[1]),y:parseFloat(t[2])},p2:{x:parseFloat(t[3]),y:parseFloat(t[4])}}:null}function u(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`),n=[],r=[];return t.querySelectorAll(`[id^="room"]`).forEach(e=>{let t=e.querySelector(`[id="background"]`),i=t?.getAttribute(`d`);if(!i)return;let a=c(i);a.length<3||(n.push({name:e.querySelector(`text`)?.textContent?.trim()??``,points:a}),e.querySelectorAll(`path`).forEach(e=>{if(e===t)return;let n=l(e.getAttribute(`d`)??``);n&&r.push(n)}))}),{rooms:n,doors:r}}function d(c,l={}){let{scale:d=1,thickness:p=n.thickness,weldTolerance:m,label:h}=l,{rooms:g,doors:_}=u(c),v=a({floorLabel:h}),y=v.floors[0],b=m??Math.max(1,d*2);for(let e of g){let n=e.points.map(e=>({x:e.x*d,y:e.y*d}));for(let e=0;e<n.length;e++){let r=n[e],a=n[(e+1)%n.length];y=i(y,r,a,{thickness:p,type:t.Interior},b).floor}}for(let t of _){let n={x:t.p1.x*d,y:t.p1.y*d},i={x:t.p2.x*d,y:t.p2.y*d},a={x:(n.x+i.x)/2,y:(n.y+i.y)/2},o=Math.hypot(i.x-n.x,i.y-n.y);if(o<=0)continue;let s=null;for(let e of Object.values(y.walls)){let t=y.nodes[e.a],n=y.nodes[e.b];if(!t||!n)continue;let r=Math.hypot(n.x-t.x,n.y-t.y);if(r<1e-6)continue;let i=((a.x-t.x)*(n.x-t.x)+(a.y-t.y)*(n.y-t.y))/(r*r),o=Math.min(1,Math.max(0,i)),c=t.x+(n.x-t.x)*o,l=t.y+(n.y-t.y)*o,u=Math.hypot(a.x-c,a.y-l);u<=b&&(!s||u<s.dist)&&(s={wallId:e.id,offset:o*r,dist:u})}s&&(y=r(y,{wallId:s.wallId,kind:e.Door,offset:s.offset,width:o,sill:0,head:2100}).floor)}y=o(y).floor;let x=s(y).rooms;for(let e of g){if(!e.name)continue;let t={x:e.points.reduce((e,t)=>e+t.x,0)/e.points.length*d,y:e.points.reduce((e,t)=>e+t.y,0)/e.points.length*d},n=x.find(e=>e.roomId&&e.outer.length>2&&f(t,e.outer)&&y.rooms[e.roomId]?.name.startsWith(`Room `));n?.roomId&&(y={...y,rooms:{...y.rooms,[n.roomId]:{...y.rooms[n.roomId],name:e.name}}})}return{...v,floors:[y]}}function f(e,t){let n=!1;for(let r=0,i=t.length-1;r<t.length;i=r++){let a=t[r].x,o=t[r].y,s=t[i].x,c=t[i].y;o>e.y!=c>e.y&&e.x<(s-a)*(e.y-o)/(c-o)+a&&(n=!n)}return n}export{d as planFromLegacySvg};
2
- //# sourceMappingURL=legacySvg.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"legacySvg.mjs","names":[],"sources":["../../../../package/common/floorPlan/legacySvg.ts"],"sourcesContent":["/**\n * Imports floor plans exported by the previous `RoomDrawer` into the current model.\n *\n * The old format stored each room as its own closed polygon, so a wall between two\n * rooms appeared twice, once per room. Feeding every polygon edge through `addWall` is\n * what converts that: coincident edges resolve to the same nodes and the second one is\n * recognised as already walled, so the pair collapses into the single shared wall the\n * new model needs.\n *\n * Import only. Nothing writes this format any more. See LEGACY_SVG_FORMAT.md in this\n * folder for the specification of what is accepted.\n */\n\nimport { DEFAULT_WALL_ATTRS, addOpening, addWall, scalePlan } from './floorPlan.graph';\nimport { commitDerivation, derivePlan } from './floorPlan.rooms';\nimport { emptyFloorPlan } from './floorPlan.plan';\nimport { OpeningKind, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface LegacyImportOptions {\n /**\n * Multiplies every imported coordinate, converting the old unitless canvas into the\n * module's millimetre invariant.\n *\n * There is no correct automatic value: the old format records no scale at all. The\n * default of 1 keeps the numbers as they were, which makes a typical 400x300 export\n * a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.\n */\n scale?: number;\n /** Wall thickness for imported walls, in millimetres after scaling. */\n thickness?: number;\n /** Tolerance for welding coincident endpoints, in millimetres after scaling. */\n weldTolerance?: number;\n label?: string;\n}\n\ninterface LegacyRoom {\n name: string;\n points: PlanPoint[];\n}\n\ninterface LegacyDoor {\n p1: PlanPoint;\n p2: PlanPoint;\n}\n\n/** Reads the absolute M/L commands the old exporter wrote. */\nfunction parseClosedPath(d: string): PlanPoint[] {\n const points: PlanPoint[] = [];\n for (const m of d.matchAll(/[ML]\\s*(-?[\\d.]+)[,\\s]+(-?[\\d.]+)/gi)) {\n points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });\n }\n return points;\n}\n\n/**\n * Recovers a door from the old swing-arc path: `M p1 L p2 M p1 A w,w 0 0,1 end`.\n *\n * Position and width follow from p1 and p2. Anything that does not match this shape is\n * skipped and the wall is left solid, rather than guessed at.\n */\nfunction parseDoorPath(d: string): LegacyDoor | null {\n const match = d.match(/^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i);\n if (!match) return null;\n return {\n p1: { x: parseFloat(match[1]), y: parseFloat(match[2]) },\n p2: { x: parseFloat(match[3]), y: parseFloat(match[4]) },\n };\n}\n\nfunction parseLegacyDocument(svg: string): { rooms: LegacyRoom[]; doors: LegacyDoor[] } {\n const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');\n const rooms: LegacyRoom[] = [];\n const doors: LegacyDoor[] = [];\n\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const background = group.querySelector('[id=\"background\"]');\n const d = background?.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n rooms.push({ name: group.querySelector('text')?.textContent?.trim() ?? '', points });\n\n group.querySelectorAll('path').forEach((path) => {\n if (path === background) return;\n const door = parseDoorPath(path.getAttribute('d') ?? '');\n if (door) doors.push(door);\n });\n });\n\n return { rooms, doors };\n}\n\n/**\n * Parses a legacy exported SVG into a single-floor `FloorPlan`.\n *\n * Rooms keep their names. Doors are recovered where the old swing-arc path is\n * recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that\n * format, so nothing is lost by not looking for them.\n */\nexport function planFromLegacySvg(svg: string, options: LegacyImportOptions = {}): FloorPlan {\n const { scale = 1, thickness = DEFAULT_WALL_ATTRS.thickness, weldTolerance, label } = options;\n const { rooms, doors } = parseLegacyDocument(svg);\n\n const plan = emptyFloorPlan({ floorLabel: label });\n let floor: PlanFloor = plan.floors[0];\n // Generous relative to the old canvas, where coordinates were snapped to a grid of 5.\n const tol = weldTolerance ?? Math.max(1, scale * 2);\n\n for (const room of rooms) {\n const points = room.points.map((p) => ({ x: p.x * scale, y: p.y * scale }));\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n // The welding step: an edge an adjacent room already drew resolves to the same\n // two nodes, and addWall declines to duplicate it.\n floor = addWall(floor, a, b, { thickness, type: WallType.Interior }, tol).floor;\n }\n }\n\n for (const door of doors) {\n const p1 = { x: door.p1.x * scale, y: door.p1.y * scale };\n const p2 = { x: door.p2.x * scale, y: door.p2.y * scale };\n const centre = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n if (width <= 0) continue;\n\n // Find the wall the door sits on now that edges have been welded.\n let best: { wallId: string; offset: number; dist: number } | null = null;\n for (const wall of Object.values(floor.walls)) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n if (len < 1e-6) continue;\n const t = ((centre.x - a.x) * (b.x - a.x) + (centre.y - a.y) * (b.y - a.y)) / (len * len);\n const clamped = Math.min(1, Math.max(0, t));\n const px = a.x + (b.x - a.x) * clamped;\n const py = a.y + (b.y - a.y) * clamped;\n const d = Math.hypot(centre.x - px, centre.y - py);\n if (d <= tol && (!best || d < best.dist)) {\n best = { wallId: wall.id, offset: clamped * len, dist: d };\n }\n }\n if (!best) continue;\n\n floor = addOpening(floor, {\n wallId: best.wallId,\n kind: OpeningKind.Door,\n offset: best.offset,\n width,\n sill: 0,\n head: 2100,\n }).floor;\n }\n\n // Name the derived faces from the polygons they came from, matching each face to the\n // legacy room whose own centroid falls inside it.\n const committed = commitDerivation(floor);\n floor = committed.floor;\n\n const derived = derivePlan(floor).rooms;\n for (const legacy of rooms) {\n if (!legacy.name) continue;\n const centroid = {\n x: (legacy.points.reduce((s, p) => s + p.x, 0) / legacy.points.length) * scale,\n y: (legacy.points.reduce((s, p) => s + p.y, 0) / legacy.points.length) * scale,\n };\n const match = derived.find(\n (face) =>\n face.roomId &&\n face.outer.length > 2 &&\n pointInsideRing(centroid, face.outer) &&\n floor.rooms[face.roomId]?.name.startsWith('Room '),\n );\n if (match?.roomId) {\n floor = {\n ...floor,\n rooms: { ...floor.rooms, [match.roomId]: { ...floor.rooms[match.roomId], name: legacy.name } },\n };\n }\n }\n\n return { ...plan, floors: [floor] };\n}\n\nfunction pointInsideRing(p: PlanPoint, ring: PlanPoint[]): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const xi = ring[i].x;\n const yi = ring[i].y;\n const xj = ring[j].x;\n const yj = ring[j].y;\n if (yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n return inside;\n}\n\nexport { scalePlan };\n"],"mappings":"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"}
@@ -1,27 +0,0 @@
1
- import { scalePlan } from './floorPlan.graph.js';
2
- import { FloorPlan } from './floorPlan.types.js';
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 };