@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.
Files changed (74) hide show
  1. package/dist/esm/common/floorPlan/floorPlan.faces.mjs +2 -0
  2. package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -0
  3. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +2 -0
  4. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -0
  5. package/dist/esm/common/floorPlan/floorPlan.graph.mjs +2 -0
  6. package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -0
  7. package/dist/esm/common/floorPlan/floorPlan.hit.mjs +2 -0
  8. package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -0
  9. package/dist/esm/common/floorPlan/floorPlan.json.mjs +2 -0
  10. package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -0
  11. package/dist/esm/common/floorPlan/floorPlan.path.mjs +2 -0
  12. package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -0
  13. package/dist/esm/common/floorPlan/floorPlan.plan.mjs +2 -0
  14. package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -0
  15. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +2 -0
  16. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -0
  17. package/dist/esm/common/floorPlan/floorPlan.snap.mjs +2 -0
  18. package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -0
  19. package/dist/esm/common/floorPlan/floorPlan.types.mjs +2 -0
  20. package/dist/esm/common/floorPlan/floorPlan.types.mjs.map +1 -0
  21. package/dist/esm/common/floorPlan/legacySvg.mjs +2 -0
  22. package/dist/esm/common/floorPlan/legacySvg.mjs.map +1 -0
  23. package/dist/esm/common/planCanvas/planCanvas.mjs +2 -0
  24. package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -0
  25. package/dist/esm/common/planCanvas/planCanvas.module.mjs +2 -0
  26. package/dist/esm/common/planCanvas/planCanvas.module.mjs.map +1 -0
  27. package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
  28. package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
  29. package/dist/esm/common/roomDrawer/roomDrawer.module.mjs +1 -1
  30. package/dist/esm/common/roomDrawer/roomDrawer.module.mjs.map +1 -1
  31. package/dist/esm/common/roomDrawer/usePlanHistory.mjs +2 -0
  32. package/dist/esm/common/roomDrawer/usePlanHistory.mjs.map +1 -0
  33. package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
  34. package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
  35. package/dist/esm/common/roomViewer/roomViewer.module.mjs +1 -1
  36. package/dist/esm/common/roomViewer/roomViewer.module.mjs.map +1 -1
  37. package/dist/esm/index.mjs +1 -1
  38. package/dist/index.cjs +3 -7
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/style.css +1 -1
  41. package/dist/types/package/common/configProvider/configProvider.types.d.ts +4 -0
  42. package/dist/types/package/common/floorPlan/floorPlan.faces.d.ts +33 -0
  43. package/dist/types/package/common/floorPlan/floorPlan.fixtures.d.ts +35 -0
  44. package/dist/types/package/common/floorPlan/floorPlan.geometry.d.ts +111 -0
  45. package/dist/types/package/common/floorPlan/floorPlan.graph.d.ts +139 -0
  46. package/dist/types/package/common/floorPlan/floorPlan.hit.d.ts +26 -0
  47. package/dist/types/package/common/floorPlan/floorPlan.json.d.ts +23 -0
  48. package/dist/types/package/common/floorPlan/floorPlan.path.d.ts +59 -0
  49. package/dist/types/package/common/floorPlan/floorPlan.plan.d.ts +98 -0
  50. package/dist/types/package/common/floorPlan/floorPlan.rooms.d.ts +47 -0
  51. package/dist/types/package/common/floorPlan/floorPlan.snap.d.ts +101 -0
  52. package/dist/types/package/common/floorPlan/floorPlan.types.d.ts +276 -0
  53. package/dist/types/package/common/floorPlan/index.d.ts +28 -0
  54. package/dist/types/package/common/floorPlan/legacySvg.d.ts +27 -0
  55. package/dist/types/package/common/planCanvas/index.d.ts +2 -0
  56. package/dist/types/package/common/planCanvas/planCanvas.d.ts +7 -0
  57. package/dist/types/package/common/planCanvas/planCanvas.types.d.ts +62 -0
  58. package/dist/types/package/common/roomDrawer/roomDrawer.d.ts +3 -3
  59. package/dist/types/package/common/roomDrawer/roomDrawer.types.d.ts +295 -15
  60. package/dist/types/package/common/roomDrawer/usePlanHistory.d.ts +24 -0
  61. package/dist/types/package/common/roomViewer/index.d.ts +1 -0
  62. package/dist/types/package/common/roomViewer/roomViewer.d.ts +9 -2
  63. package/dist/types/package/common/roomViewer/roomViewer.types.d.ts +88 -9
  64. package/dist/types/package/index.d.ts +1 -0
  65. package/docs/CLAUDE.md +1 -0
  66. package/docs/ConfigProvider.md +1 -0
  67. package/docs/FloorPlan.md +214 -0
  68. package/docs/RoomDrawer.md +330 -30
  69. package/docs/RoomViewer.md +80 -27
  70. package/package.json +1 -1
  71. package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs +0 -6
  72. package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs.map +0 -1
  73. package/dist/types/package/common/roomDrawer/roomDrawer.utils.d.ts +0 -32
  74. package/docs/room-drawing-analysis.md +0 -337
@@ -1,41 +1,94 @@
1
1
  # RoomViewer
2
2
 
3
- **When to use:** Display read-only SVG floor plans with a floor selector — venue maps, seating layouts, building directories. Pair with `RoomDrawer` when you also need editing capability.
3
+ **When to use:** to show a floor plan someone can look at and click, without letting them change it — a building directory, a booking screen, a dashboard beside a room list.
4
4
 
5
- **Keywords:** floor plan viewer
6
-
7
- **Import:** `import { RoomViewer } from '@ahrowe/ui'`
8
- **Types:** `import type { RoomViewerFloor, RoomViewerProps } from '@ahrowe/ui'`
5
+ **Keywords:** map, site plan, wayfinding, hotspot, clickable regions, blueprint, storey, apartment, read-only
9
6
 
10
7
  ```tsx
11
8
  import { RoomViewer } from '@ahrowe/ui';
12
- import type { RoomViewerFloor } from '@ahrowe/ui';
13
-
14
- const floors: RoomViewerFloor[] = [
15
- {
16
- label: 'Ground Floor',
17
- data: <GroundFloorSvg />,
18
- },
19
- {
20
- label: 'First Floor',
21
- data: <FirstFloorSvg />,
22
- },
23
- ];
24
-
25
- <RoomViewer data={floors} />
9
+ import type { RoomViewerProps, RoomViewerHandle } from '@ahrowe/ui';
26
10
  ```
27
11
 
28
- **RoomViewerFloor:**
12
+ It renders a **`FloorPlan`** — the same document [RoomDrawer](./RoomDrawer.md) edits and downloads, drawn on the same canvas — so the two can never drift apart. See [FloorPlan](./FloorPlan.md) for the model.
13
+
14
+ ```tsx
15
+ <RoomViewer plan={plan} onSelect={(roomId, room) => console.log(roomId, room?.area)} />
16
+ ```
17
+
18
+ ## Selecting rooms
19
+
20
+ Rooms are clickable and keyboard-reachable: each is a `role="button"` with `aria-pressed`, a `<title>` naming it, and `Enter` / `Space` to select. Because clicking a shape is silent to a screen reader, the selection is also announced through an `aria-live` region.
21
+
22
+ Selection, hover and the active floor are each controlled or uncontrolled:
29
23
 
30
- ```ts
31
- interface RoomViewerFloor {
32
- label: string; // Tab/selector label shown above the plan
33
- data: React.ReactNode; // SVG floor plan element
34
- }
24
+ ```tsx
25
+ // Uncontrolled
26
+ <RoomViewer plan={plan} defaultSelectedRoomId={roomId} onSelect={setRoomId} />
27
+
28
+ // Controlled, which is what a room list beside the plan needs
29
+ <RoomViewer
30
+ plan={plan}
31
+ selectedRoomId={selectedId}
32
+ onSelect={(id) => setSelectedId(id)}
33
+ hoveredRoomId={hoveredId}
34
+ onHoverChange={setHoveredId}
35
+ />
35
36
  ```
36
37
 
37
- **Key props:**
38
+ `onSelect(roomId, room, floorId)` hands back the derived room, so its `area`, `outer` ring and `wallIds` are there without a second lookup. `roomId` is `null` when the selection is cleared by clicking away.
39
+
40
+ `interactive={false}` makes it a picture: no focus stops, no hit targets, no callbacks.
41
+
42
+ **Hover tints the room's floor; selection colours the walls that bound it** — two channels, so a hovered room and a selected one can be told apart at a glance. Hover answers *which room is this* and a fill is instant; a wall is shared, so colouring one is ambiguous between the rooms on either side. Selection is state you work inside, where a wash over the room would hide its doors and label. Override with `--plan-room-fill-hover` and `--plan-room-edge-selected`, or style either through `classNames.roomFill` and its `data-state` attribute.
43
+
44
+ ## Floors
45
+
46
+ A plan holds a stack of storeys and the viewer shows one at a time, with a tab per floor listed **top first**, the way a building reads in section. The strip hides itself for a single-storey plan.
47
+
48
+ ```tsx
49
+ <RoomViewer plan={plan} activeFloorId={floorId} onFloorChange={setFloorId} />
50
+ <RoomViewer plan={plan} showFloorBar={false} />
51
+ ```
52
+
53
+ ## Panning and zooming
54
+
55
+ Drag to pan, wheel or pinch to zoom, and the controls in the corner zoom and fit. The plan fits itself to the viewport on mount, and refits when the floor or the canvas size changes — **until the viewer pans or zooms**, after which the view is theirs and the component stops moving it. `pannable={false}` freezes it.
56
+
57
+ ## Key props
38
58
 
39
59
  | Prop | Type | Description |
40
60
  |------|------|-------------|
41
- | `data` | `RoomViewerFloor[]` | Floor definitions to display |
61
+ | `plan` | `FloorPlan` | The plan to show. Omit for the empty state |
62
+ | `activeFloorId` / `defaultActiveFloorId` / `onFloorChange` | `string` / `string` / `(id) => void` | Which storey is showing |
63
+ | `selectedRoomId` / `defaultSelectedRoomId` / `onSelect` | `string \| null` / … / `(roomId, room, floorId) => void` | Selection |
64
+ | `hoveredRoomId` / `onHoverChange` | `string \| null` / `(roomId) => void` | Hover, for cross-highlighting |
65
+ | `view` / `defaultView` / `onViewChange` | `PlanView` | Pan and zoom |
66
+ | `interactive` | `boolean` | Rooms respond to pointer and keyboard (default `true`) |
67
+ | `pannable` | `boolean` | Drag to pan, wheel and pinch to zoom (default `true`) |
68
+ | `fitOnMount` | `boolean` | Fit the plan into view (default `true`) |
69
+ | `showFloorBar` / `showViewControls` / `showGrid` / `showRoomLabels` / `showOpeningSymbols` | `boolean` | Chrome and layers |
70
+ | `getRoomColor` / `renderRoomLabel` / `getRoomLabel` / `formatArea` | functions | Colour, label and area rendering |
71
+ | `height` | `number \| string` | Canvas height (default `360`) |
72
+ | `emptyState` | `ReactNode` | Shown when there is no plan, or nothing in it |
73
+ | `labels` / `icons` | objects | Every string and icon in the chrome |
74
+
75
+ **Imperative handle:** `fitToContent(options?)`, `getView()`, `setView(view)`, `zoomBy(factor)`, `select(roomId)`, `getFloorId()`, `setFloorId(id)`, `getSvgElement()`.
76
+
77
+ **Slots:** `root` `floorBar` `floorTab` `canvasWrapper` `canvas` `grid` `rooms` `roomFill` `roomLabel` `walls` `wallBody` `openings` `opening` `viewControls` `viewButton` `empty`
78
+
79
+ ## Accessibility, honestly
80
+
81
+ 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.
82
+
83
+ ## Migration from the previous version
84
+
85
+ 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.
86
+
87
+ | Before | Now |
88
+ |--------|-----|
89
+ | `data={[{ label, data: <svg/> }]}` | `plan={plan}` — one `FloorPlan`, floors and all |
90
+ | `id="room12"` convention | `PlanRoom` ids in the model |
91
+ | Highlighting via `classList` | `selectedRoomId` / `hoveredRoomId` |
92
+ | No selection callback | `onSelect(roomId, room, floorId)` |
93
+
94
+ 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.25.2",
3
+ "version": "0.26.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +0,0 @@
1
- function e(e){return Math.round(e/5)*5}function t(t){return{x:e(t.x),y:e(t.y)}}function n(e,n,r){let i=r.createSVGPoint();i.x=e,i.y=n;let a=r.getScreenCTM();if(!a)return{x:0,y:0};let o=i.matrixTransform(a.inverse());return t({x:o.x,y:o.y})}function r(e,t){return Math.sqrt((e.x-t.x)**2+(e.y-t.y)**2)}function i(e,t,n){let i=n.x-t.x,a=n.y-t.y,o=i*i+a*a;if(o===0)return{dist:r(e,t),t:0};let s=Math.max(0,Math.min(1,((e.x-t.x)*i+(e.y-t.y)*a)/o)),c=t.x+s*i,l=t.y+s*a;return{dist:Math.sqrt((e.x-c)**2+(e.y-l)**2),t:s}}function a(e,t){let n=null;for(let r of t){let t=r.points;for(let a=0;a<t.length;a++){let o=t[a],s=t[(a+1)%t.length],{dist:c,t:l}=i(e,o,s);c<=8&&(!n||c<n.dist)&&(n={roomId:r.id,wallIndex:a,t:l,dist:c})}}return n}function o(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}function s(e){return{x:e.reduce((e,t)=>e+t.x,0)/e.length,y:e.reduce((e,t)=>e+t.y,0)/e.length}}function c(e,t,n,r){return[{x:e,y:t},{x:e+n,y:t},{x:e+n,y:t+r},{x:e,y:t+r}]}function l(e,t,n,r){let i=t.x-e.x,a=t.y-e.y,o=Math.sqrt(i*i+a*a);if(o<.001)return null;let s=i/o,c=a/o,l=e.x+i*n,u=e.y+a*n,d=Math.min(r/2,o/2-.001);return{p1:{x:l-s*d,y:u-c*d},p2:{x:l+s*d,y:u+c*d},perp:{x:-c,y:s}}}function u(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`)}function d(e){return e.map((e,t)=>`${t===0?`M`:`L`} ${e.x.toFixed(2)},${e.y.toFixed(2)}`).join(` `)+` Z`}function f(e,t){if(e.length<2)return[];if(t.length===0)return[e.map((e,t)=>`${t===0?`M`:`L`} ${e.x.toFixed(2)},${e.y.toFixed(2)}`).join(` `)+` Z`];let n=e.length,r=[],i=[`M ${e[0].x.toFixed(2)},${e[0].y.toFixed(2)}`];for(let a=0;a<n;a++){let o=e[a],s=e[(a+1)%n],c=s.x-o.x,l=s.y-o.y,u=Math.sqrt(c*c+l*l),d=t.filter(e=>e.wallIndex===a).sort((e,t)=>e.t-t.t);if(d.length===0)i.push(`L ${s.x.toFixed(2)},${s.y.toFixed(2)}`);else{for(let e of d){let t=e.width/2,n=Math.max(0,e.t-t/u),a=Math.min(1,e.t+t/u),s={x:o.x+c*n,y:o.y+l*n},d={x:o.x+c*a,y:o.y+l*a};i.push(`L ${s.x.toFixed(2)},${s.y.toFixed(2)}`),r.push(i),i=[`M ${d.x.toFixed(2)},${d.y.toFixed(2)}`]}i.push(`L ${s.x.toFixed(2)},${s.y.toFixed(2)}`)}}return r[0]=[...i,...r[0].slice(1)],r.map(e=>e.join(` `))}function p(e,t,n){let r=l(e,t,n.t,n.width);if(!r)return``;let{p1:i,p2:a,perp:o}=r,s={x:i.x+o.x*n.width,y:i.y+o.y*n.width};return[`M ${i.x.toFixed(2)},${i.y.toFixed(2)} L ${a.x.toFixed(2)},${a.y.toFixed(2)}`,`M ${i.x.toFixed(2)},${i.y.toFixed(2)} A ${n.width},${n.width} 0 0,1 ${s.x.toFixed(2)},${s.y.toFixed(2)}`].join(` `)}function m(e,t){let n=1/0,r=1/0,i=-1/0,a=-1/0;for(let t of e)for(let e of t.points)e.x<n&&(n=e.x),e.y<r&&(r=e.y),e.x>i&&(i=e.x),e.y>a&&(a=e.y);isFinite(n)||(n=0,r=0,i=400,a=300);let o=[(n-5).toFixed(2),(r-5).toFixed(2),(i-n+10).toFixed(2),(a-r+10).toFixed(2)].join(` `),c=e.map(e=>{let n=t.filter(t=>t.roomId===e.id),r=f(e.points,n),i=d(e.points),a=s(e.points),o=r.map(e=>` <path d="${e}" />`).join(`
2
- `),c=n.map(t=>{let n=e.points[t.wallIndex],r=e.points[(t.wallIndex+1)%e.points.length],i=p(n,r,t);return i?` <path d="${i}" fill="none" />`:``}).filter(Boolean).join(`
3
- `),l=e.name?` <text fontSize="5" x="${a.x.toFixed(1)}" y="${a.y.toFixed(1)}">${u(e.name)}</text>`:``;return[` <g id="${e.id}">`,o,` <path id="background" d="${i}" />`,c,l,` </g>`].filter(Boolean).join(`
4
- `)});return[`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="${o}">`,` <g id="layer1">`,...c,` </g>`,` <use id="use" href="" />`,`</svg>`].join(`
5
- `)}function h(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 g(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`),n=[];return t.querySelectorAll(`[id^="room"]`).forEach(e=>{let t=e.querySelector(`[id="background"]`);if(!t)return;let r=t.getAttribute(`d`);if(!r)return;let i=h(r);if(i.length<3)return;let a=e.querySelector(`text`)?.textContent?.trim()??``;n.push({id:e.id,name:a,points:i})}),n}function _(e,t){let n=m(e,t),r=new Blob([n],{type:`image/svg+xml`}),i=URL.createObjectURL(r),a=document.createElement(`a`);a.href=i,a.download=`room-plan.svg`,a.click(),URL.revokeObjectURL(i)}export{f as buildWallSegments,s as centroid,n as clientToSVG,r as dist,l as doorEndpoints,_ as downloadSVG,a as nearestWall,g as parseSVGRooms,o as pointInPolygon,i as pointToSegment,c as rectToPoints,t as snapPt};
6
- //# sourceMappingURL=roomDrawer.utils.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"roomDrawer.utils.mjs","names":[],"sources":["../../../../package/common/roomDrawer/roomDrawer.utils.ts"],"sourcesContent":["import type { Point, RoomData, DoorData } from './roomDrawer.types';\n\nexport const CANVAS_W = 400;\nexport const CANVAS_H = 300;\nexport const GRID = 5;\nexport const DOOR_WIDTH = 12;\nexport const CLOSE_THRESHOLD = 8; // px in SVG units to close a polygon\nexport const WALL_TOLERANCE = 8; // max distance to snap to a wall for door placement\n\n// ─── Coordinate helpers ────────────────────────────────────────────────────\n\nexport function snap(v: number): number {\n return Math.round(v / GRID) * GRID;\n}\n\nexport function snapPt(p: Point): Point {\n return { x: snap(p.x), y: snap(p.y) };\n}\n\nexport function clientToSVG(clientX: number, clientY: number, svg: SVGSVGElement): Point {\n const pt = svg.createSVGPoint();\n pt.x = clientX;\n pt.y = clientY;\n const m = svg.getScreenCTM();\n if (!m) return { x: 0, y: 0 };\n const raw = pt.matrixTransform(m.inverse());\n return snapPt({ x: raw.x, y: raw.y });\n}\n\n// ─── Geometry ──────────────────────────────────────────────────────────────\n\nexport function dist(a: Point, b: Point): number {\n return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n}\n\nexport function pointToSegment(p: Point, a: Point, b: Point): { dist: number; t: number } {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len2 = dx * dx + dy * dy;\n if (len2 === 0) return { dist: dist(p, a), t: 0 };\n const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));\n const px = a.x + t * dx;\n const py = a.y + t * dy;\n return { dist: Math.sqrt((p.x - px) ** 2 + (p.y - py) ** 2), t };\n}\n\nexport function nearestWall(pt: Point, rooms: RoomData[]): { roomId: string; wallIndex: number; t: number } | null {\n let best: { roomId: string; wallIndex: number; t: number; dist: number } | null = null;\n for (const room of rooms) {\n const pts = room.points;\n for (let i = 0; i < pts.length; i++) {\n const a = pts[i];\n const b = pts[(i + 1) % pts.length];\n const { dist: d, t } = pointToSegment(pt, a, b);\n if (d <= WALL_TOLERANCE && (!best || d < best.dist)) {\n best = { roomId: room.id, wallIndex: i, t, dist: d };\n }\n }\n }\n return best;\n}\n\nexport function pointInPolygon(pt: Point, poly: Point[]): boolean {\n let inside = false;\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const xi = poly[i].x, yi = poly[i].y;\n const xj = poly[j].x, yj = poly[j].y;\n if ((yi > pt.y) !== (yj > pt.y) && pt.x < ((xj - xi) * (pt.y - yi)) / (yj - yi) + xi) {\n inside = !inside;\n }\n }\n return inside;\n}\n\nexport function centroid(pts: Point[]): Point {\n return {\n x: pts.reduce((s, p) => s + p.x, 0) / pts.length,\n y: pts.reduce((s, p) => s + p.y, 0) / pts.length,\n };\n}\n\nexport function rectToPoints(x: number, y: number, w: number, h: number): Point[] {\n return [\n { x, y },\n { x: x + w, y },\n { x: x + w, y: y + h },\n { x, y: y + h },\n ];\n}\n\n// Returns the two endpoints of the door opening and a perpendicular unit vector (for the swing arc).\nexport function doorEndpoints(\n a: Point,\n b: Point,\n t: number,\n width: number,\n): { p1: Point; p2: Point; perp: Point } | null {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.sqrt(dx * dx + dy * dy);\n if (len < 0.001) return null;\n const nx = dx / len;\n const ny = dy / len;\n const cx = a.x + dx * t;\n const cy = a.y + dy * t;\n const hw = Math.min(width / 2, len / 2 - 0.001);\n return {\n p1: { x: cx - nx * hw, y: cy - ny * hw },\n p2: { x: cx + nx * hw, y: cy + ny * hw },\n perp: { x: -ny, y: nx },\n };\n}\n\n// ─── SVG export ────────────────────────────────────────────────────────────\n\nfunction escapeSVG(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction ptsToClosedPath(pts: Point[]): string {\n return pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') + ' Z';\n}\n\n// Returns one path string per wall segment between door gaps.\n// 0 doors → [closed path] | N doors → N open paths\nexport function buildWallSegments(pts: Point[], wallDoors: DoorData[]): string[] {\n if (pts.length < 2) return [];\n\n if (wallDoors.length === 0) {\n return [pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') + ' Z'];\n }\n\n const n = pts.length;\n const segments: string[][] = [];\n let current: string[] = [`M ${pts[0].x.toFixed(2)},${pts[0].y.toFixed(2)}`];\n\n for (let i = 0; i < n; i++) {\n const a = pts[i];\n const b = pts[(i + 1) % n];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.sqrt(dx * dx + dy * dy);\n const segs = wallDoors.filter((d) => d.wallIndex === i).sort((x, y) => x.t - y.t);\n\n if (segs.length === 0) {\n current.push(`L ${b.x.toFixed(2)},${b.y.toFixed(2)}`);\n } else {\n for (const door of segs) {\n const hw = door.width / 2;\n const t1 = Math.max(0, door.t - hw / len);\n const t2 = Math.min(1, door.t + hw / len);\n const gs = { x: a.x + dx * t1, y: a.y + dy * t1 };\n const ge = { x: a.x + dx * t2, y: a.y + dy * t2 };\n current.push(`L ${gs.x.toFixed(2)},${gs.y.toFixed(2)}`);\n segments.push(current);\n current = [`M ${ge.x.toFixed(2)},${ge.y.toFixed(2)}`];\n }\n current.push(`L ${b.x.toFixed(2)},${b.y.toFixed(2)}`);\n }\n }\n\n // Wrap-around: the last segment ends at pts[0] where segments[0] started —\n // merge them so each segment is one continuous open path.\n segments[0] = [...current, ...segments[0].slice(1)];\n\n return segments.map((s) => s.join(' '));\n}\n\nfunction buildDoorArcPath(a: Point, b: Point, door: DoorData): string {\n const ep = doorEndpoints(a, b, door.t, door.width);\n if (!ep) return '';\n const { p1, p2, perp } = ep;\n const swingEnd = { x: p1.x + perp.x * door.width, y: p1.y + perp.y * door.width };\n return [\n `M ${p1.x.toFixed(2)},${p1.y.toFixed(2)} L ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`,\n `M ${p1.x.toFixed(2)},${p1.y.toFixed(2)} A ${door.width},${door.width} 0 0,1 ${swingEnd.x.toFixed(2)},${swingEnd.y.toFixed(2)}`,\n ].join(' ');\n}\n\nexport function generateSVG(rooms: RoomData[], doors: DoorData[]): string {\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const r of rooms) {\n for (const p of r.points) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n }\n if (!isFinite(minX)) { minX = 0; minY = 0; maxX = CANVAS_W; maxY = CANVAS_H; }\n\n const pad = 5;\n const vb = [\n (minX - pad).toFixed(2),\n (minY - pad).toFixed(2),\n (maxX - minX + pad * 2).toFixed(2),\n (maxY - minY + pad * 2).toFixed(2),\n ].join(' ');\n\n const roomEls = rooms.map((room) => {\n const roomDoors = doors.filter((d) => d.roomId === room.id);\n const wallSegs = buildWallSegments(room.points, roomDoors);\n const bgPath = ptsToClosedPath(room.points);\n const c = centroid(room.points);\n\n const wallPaths = wallSegs.map((d) => ` <path d=\"${d}\" />`).join('\\n');\n\n const doorPaths = roomDoors\n .map((door) => {\n const a = room.points[door.wallIndex];\n const b = room.points[(door.wallIndex + 1) % room.points.length];\n const d = buildDoorArcPath(a, b, door);\n return d ? ` <path d=\"${d}\" fill=\"none\" />` : '';\n })\n .filter(Boolean)\n .join('\\n');\n\n const labelEl = room.name\n ? ` <text fontSize=\"5\" x=\"${c.x.toFixed(1)}\" y=\"${c.y.toFixed(1)}\">${escapeSVG(room.name)}</text>`\n : '';\n\n return [` <g id=\"${room.id}\">`, wallPaths, ` <path id=\"background\" d=\"${bgPath}\" />`, doorPaths, labelEl, ` </g>`]\n .filter(Boolean)\n .join('\\n');\n });\n\n return [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"${vb}\">`,\n ` <g id=\"layer1\">`,\n ...roomEls,\n ` </g>`,\n ` <use id=\"use\" href=\"\" />`,\n `</svg>`,\n ].join('\\n');\n}\n\n// ─── SVG import ────────────────────────────────────────────────────────────\n\nfunction parseClosedPath(d: string): Point[] {\n const points: Point[] = [];\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\nexport function parseSVGRooms(svgString: string): RoomData[] {\n const doc = new DOMParser().parseFromString(svgString, 'image/svg+xml');\n const rooms: RoomData[] = [];\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const bg = group.querySelector('[id=\"background\"]');\n if (!bg) return;\n const d = bg.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n const name = group.querySelector('text')?.textContent?.trim() ?? '';\n rooms.push({ id: group.id, name, points });\n });\n return rooms;\n}\n\nexport function downloadSVG(rooms: RoomData[], doors: DoorData[]): void {\n const svg = generateSVG(rooms, doors);\n const blob = new Blob([svg], { type: 'image/svg+xml' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'room-plan.svg';\n a.click();\n URL.revokeObjectURL(url);\n}\n"],"mappings":"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"}
@@ -1,32 +0,0 @@
1
- import { Point, RoomData, DoorData } from './roomDrawer.types';
2
- export declare const CANVAS_W = 400;
3
- export declare const CANVAS_H = 300;
4
- export declare const GRID = 5;
5
- export declare const DOOR_WIDTH = 12;
6
- export declare const CLOSE_THRESHOLD = 8;
7
- export declare const WALL_TOLERANCE = 8;
8
- export declare function snap(v: number): number;
9
- export declare function snapPt(p: Point): Point;
10
- export declare function clientToSVG(clientX: number, clientY: number, svg: SVGSVGElement): Point;
11
- export declare function dist(a: Point, b: Point): number;
12
- export declare function pointToSegment(p: Point, a: Point, b: Point): {
13
- dist: number;
14
- t: number;
15
- };
16
- export declare function nearestWall(pt: Point, rooms: RoomData[]): {
17
- roomId: string;
18
- wallIndex: number;
19
- t: number;
20
- } | null;
21
- export declare function pointInPolygon(pt: Point, poly: Point[]): boolean;
22
- export declare function centroid(pts: Point[]): Point;
23
- export declare function rectToPoints(x: number, y: number, w: number, h: number): Point[];
24
- export declare function doorEndpoints(a: Point, b: Point, t: number, width: number): {
25
- p1: Point;
26
- p2: Point;
27
- perp: Point;
28
- } | null;
29
- export declare function buildWallSegments(pts: Point[], wallDoors: DoorData[]): string[];
30
- export declare function generateSVG(rooms: RoomData[], doors: DoorData[]): string;
31
- export declare function parseSVGRooms(svgString: string): RoomData[];
32
- export declare function downloadSVG(rooms: RoomData[], doors: DoorData[]): void;
@@ -1,337 +0,0 @@
1
- # RoomViewer — Deep Analysis
2
-
3
- This document reverse-engineers every convention in the existing SVG room data so we can build a room-drawing tool that produces fully compatible output.
4
-
5
- ---
6
-
7
- ## 1. The Data Model
8
-
9
- Each floor is an object `{ id: string, label: string, data: ReactElement }`.
10
- `data` is a JSX SVG element. The component renders it inside a `<div>` wrapper and queries the DOM after render to attach click handlers.
11
-
12
- ```ts
13
- interface FloorData {
14
- id: string;
15
- label: string; // shown as the floor heading
16
- data: ReactElement; // the <svg> JSX element
17
- }
18
- ```
19
-
20
- The array can hold 1..N floors. When length > 1 the component renders chevron navigation.
21
-
22
- ---
23
-
24
- ## 2. SVG Coordinate System
25
-
26
- ### ViewBox
27
-
28
- ```
29
- viewBox="minX minY width height"
30
- ```
31
-
32
- | Floor | viewBox | x-range | y-range |
33
- |-------|---------|---------|---------|
34
- | Ground (floor 1) | `0.9 0.9 165.1 103.8` | 0.9 → 166.0 | 0.9 → 104.7 |
35
- | Upper (floor 2) | `22 46 167 105` | 22 → 189 | 46 → 151 |
36
-
37
- **Key observation**: the viewBox does NOT need to start at origin. Floor 2 starts at `(22, 46)` because the actual room coordinates in that SVG happen to begin around those values. The SVG renderer maps the viewBox onto the container's display size automatically.
38
-
39
- The SVG itself has no `width` or `height` attributes — it fills whatever container it is placed in (the component styles it with CSS).
40
-
41
- ### Y-axis direction
42
-
43
- SVG uses a **top-left origin** with Y increasing downward (opposite to Cartesian). This affects how you think about "up" and "down" in path commands: `V 1` is near the top edge, `V 103` is near the bottom.
44
-
45
- ### Stroke width
46
-
47
- All paths are rendered with `stroke-width: 1px` (set in CSS, not SVG). This means wall thickness is purely visual — the coordinate defines the centre-line of the wall.
48
-
49
- ---
50
-
51
- ## 3. Room Definition — Two Formats
52
-
53
- ### Format A: Group with background path
54
-
55
- ```xml
56
- <g id="roomKitchen">
57
- <path id="path923" d="..." /> <!-- decorative outline segment(s) -->
58
- <path id="path925" d="..." /> <!-- more outline -->
59
- <path id="background" d="..." /> <!-- closed filled polygon -->
60
- </g>
61
- ```
62
-
63
- - The `<g>` gets the `id="roomXxx"` that makes it interactive.
64
- - The `background` path is a **closed polygon** (`Z` at end) covering the room's entire floor area. CSS sets it `fill: transparent` by default and the highlight classes colour it.
65
- - The other paths are partial outlines (wall segments). They do **not** need to form a closed shape together — they are supplementary wall lines for visual completeness.
66
- - The group can also contain `<text>` for room label rendering (floor 2 only).
67
-
68
- ### Format B: Bare path
69
-
70
- ```xml
71
- <path id="roomToilet" d="..." />
72
- ```
73
-
74
- - A single `<path>` element whose `id` starts with `room`.
75
- - The path itself is the room outline. It is typically **open** (no `Z`) — just the visible wall segments.
76
- - There is no separate background path. When highlighted, the path element itself gets both the `selected` (stroke colour) and `tint` (fill) classes directly.
77
- - This format has no text label support.
78
-
79
- ### Comparison
80
-
81
- | | Format A (group) | Format B (bare path) |
82
- |--|--|--|
83
- | Hit area | `path[id="background"]` | the path element itself |
84
- | Outline | multiple partial paths | single path |
85
- | Text label | supported (`<text>` inside `<g>`) | not supported |
86
- | CSS tint target | `background` child | the path itself |
87
- | CSS stroke target | the `<g>` and its `path` children | the path itself |
88
-
89
- ---
90
-
91
- ## 4. The `id="background"` Convention
92
-
93
- Inside a Format A group, the background path:
94
-
95
- - Must have `id="background"` (exact string, queried with `querySelector('[id="background"]')`).
96
- - Must be a **closed polygon** (path ends with `Z`) covering the whole room area.
97
- - Is the actual clickable surface when the group is a `<g>` — CSS sets `cursor: pointer` on it.
98
- - Gets the tint class on selection.
99
- - Multiple rooms can each have their own `id="background"` child (duplicate IDs in different subtrees — technically invalid HTML but SVG renders it fine and querySelector on a parent scoped to the group returns the right one).
100
-
101
- ---
102
-
103
- ## 5. Path Anatomy — How Rooms Are Drawn
104
-
105
- SVG path `d` attributes use these commands in the sample data:
106
-
107
- | Command | Meaning |
108
- |---------|---------|
109
- | `M x,y` | Move to absolute point — starts a new sub-path |
110
- | `m dx,dy` | Move to relative point |
111
- | `H x` | Horizontal line to absolute x |
112
- | `h dx` | Horizontal line by relative dx |
113
- | `V y` | Vertical line to absolute y |
114
- | `v dy` | Vertical line by relative dy |
115
- | `L x,y` | Line to absolute point |
116
- | `l dx,dy` | Line to relative point |
117
- | `Z` | Close path (line back to start of sub-path) |
118
-
119
- All rooms in the sample data use **only axis-aligned segments** (H, V) with the exception of `L` / `l` for diagonal closes on the background polygons where the start and end points aren't perfectly aligned. In practice all rooms are **rectilinear polygons** (right-angle corners only).
120
-
121
- ### Example: roomKitchen background (Format A)
122
-
123
- ```
124
- m 70.283619,51.634041 → start at (70.28, 51.63)
125
- h 95.082671 → right to (165.37, 51.63)
126
- l 0.0972,52.109689 → slightly right+down to (165.46, 103.74) ← border-align correction
127
- H 69.930275 → left to (69.93, 103.74)
128
- Z → close back to (70.28, 51.63)
129
- ```
130
-
131
- This is a near-perfect rectangle at x: 70–165, y: 52–104.
132
- The tiny `l 0.0972,52.11` instead of a pure `V` is because the border path itself has a slight diagonal — the background traces the outer border exactly.
133
-
134
- ### Example: roomToilet (Format B, open path)
135
-
136
- ```
137
- M 41.312537,31.672271 → start at (41.31, 31.67)
138
- V 27.96255 → up to (41.31, 27.96) top-left corner
139
- H 1.5913974 → left to (1.59, 27.96) top-right corner (at left wall)
140
- V 48.100984 → down to (1.59, 48.10) bottom-right corner
141
- H 41.135885 → right to (41.14, 48.10) bottom-left corner
142
- v -6.53616 → up to (41.14, 41.56) partial right wall
143
- ```
144
-
145
- No `Z` — the path is open. It draws three walls (top, left outer wall, bottom) and part of the right wall, leaving the door gap open. The room outline is **not** self-contained; it shares walls with the outer border and adjacent rooms.
146
-
147
- ### Example: roomEntryHall (Format B, staircase shape)
148
-
149
- ```
150
- M 47.848693,81.841692 → (47.85, 81.84)
151
- V 73.715669 → up to (47.85, 73.72)
152
- H 25.774869 → left to (25.77, 73.72)
153
- V 68.698633 → up to (25.77, 68.70) ← step in wall
154
- H 1.6667594 → left to (1.67, 68.70)
155
- l -0.07538,35.045097 → down to (1.59, 103.75) outer bottom-left
156
- H 48.201992 → right to (48.20, 103.75)
157
- v -9.094696 → up to (48.20, 94.65)
158
- ```
159
-
160
- This L-shaped room has a **step** in its top-right corner at `(25.77, 73.72)→(25.77, 68.70)` — a 5-unit indentation where the wall jogs inward. This is the staircase landing or hallway notch.
161
-
162
- ### The `nope` path
163
-
164
- ```
165
- id="nope" — NOT prefixed with "room", therefore not interactive
166
- M 47.848693,73.715669
167
- H 25.774869
168
- V 57.331599 → up to y=57.33
169
- 68.698633 → implicit V 68.70 (continuation) — BACK DOWN to y=68.70
170
- H 1.6667594 → left
171
- L 1.5913794,48.100984 → up
172
- H 47.848679 → right
173
- Z
174
- ```
175
-
176
- This is the hallway/corridor space between Toilet, Office, and EntryHall. It is drawn but not selectable. The double V value (`V 57.33 68.70`) creates a zigzag that draws the staircase step outline that is shared with the EntryHall.
177
-
178
- ---
179
-
180
- ## 6. Wall Sharing
181
-
182
- Rooms do **not** each define their complete closed boundary. They only draw the wall segments that are their "own" walls — the walls they share with other rooms or the outer border are not repeated.
183
-
184
- Examples from floor 1:
185
-
186
- - The left outer wall (`x ≈ 1.59`) is drawn by the `border` path, not by individual rooms.
187
- - The wall at `x ≈ 70` separating Toilet/Office/EntryHall from Kitchen/Livingroom is partially drawn by each room on that side.
188
- - The horizontal wall at `y ≈ 27.96` separating Toilet and Office is drawn by both their paths, but only the outer edge is stroked.
189
-
190
- This means the SVG viewer relies on **overlapping strokes** to form complete wall appearances — you don't get gaps because each side draws at least one stroke along the shared wall.
191
-
192
- **Implication for a drawing tool**: rooms should be stored as complete closed polygons internally, and at render time the tool can either:
193
- 1. Render each room as a complete closed shape (simpler), or
194
- 2. Compute shared edges and only render each wall segment once (more authentic to the existing format, matches the component's CSS expectations).
195
-
196
- ---
197
-
198
- ## 7. The `<use>` Highlight Mechanism
199
-
200
- Every SVG must contain exactly one `<use id="use" href="">` element, placed **after** all room elements (so it renders on top in SVG painter's order):
201
-
202
- ```xml
203
- <use id="use" href="" />
204
- ```
205
-
206
- When a room is clicked, `onRoomClicked` does:
207
- ```js
208
- use.setAttribute('href', `#${roomId}`);
209
- ```
210
-
211
- This makes `<use>` render a visual copy of the selected room's element at the same position. Because `<use>` is last in the DOM, its copy renders on top of everything else.
212
-
213
- The `<use>` element inherits the CSS classes of the element it references (via SVG shadow DOM), so the green stroke/tint applied to the original group also shows on the `<use>` copy. The effect is a highlighted outline drawn over all other overlapping paths — this is why the selection visually "pops" even when rooms share wall lines.
214
-
215
- **Critical**: without `<use id="use">` in the SVG the component will throw when trying to `setAttribute` on `undefined`.
216
-
217
- ---
218
-
219
- ## 8. CSS Highlight Classes
220
-
221
- The component applies classes from the CSS module to DOM elements directly via `classList`. Three classes are involved:
222
-
223
- ### `.roomViewerSelected`
224
- Applied to: the room's top element (`<g>` or `<path>`)
225
- Effect:
226
- ```css
227
- stroke: #18f810 !important; /* green stroke on the group/path */
228
- path { stroke: #18f810 !important; fill: none !important; }
229
- path[id="background"] { stroke: none !important; fill: #20f81013 !important; }
230
- ```
231
-
232
- ### `.roomViewerTint`
233
- Applied to: the `path[id="background"]` child, or the room element itself if no background exists
234
- Effect:
235
- ```css
236
- fill: #20f81013 !important; /* translucent green fill */
237
- path { fill: #20f81013 !important; }
238
- text { stroke: none; fill: none; } /* hide text when tinted */
239
- ```
240
-
241
- ### Deselection
242
- Both classes are removed from the previously selected room before applying to the new one. The `<use href>` is updated too.
243
-
244
- ### Note on `.roomViewerActive`
245
- The CSS defines an `-active` class with identical styling to `-selected` but it is **never set by the component code**. It appears to be dead CSS, possibly from an older version.
246
-
247
- ---
248
-
249
- ## 9. Non-Interactive Elements
250
-
251
- These elements appear in the SVG but are deliberately not rooms:
252
-
253
- | `id` | Role |
254
- |------|------|
255
- | `border` | outer building outline — pure decoration |
256
- | `layer1` | top-level `<g>` grouping all content |
257
- | `background` | fill area inside a Format A room — selectable but not a room itself |
258
- | `nope` | corridor/void area explicitly excluded from interaction |
259
- | `path923`, `path925`, etc. | decorative wall segments inside Format A groups |
260
- | `use` | the highlight overlay — must exist but is not a room |
261
-
262
- The selector `[id^="room"]` in both the JavaScript (`querySelectorAll`) and CSS (`*[id^="room"]`) correctly targets only room elements.
263
-
264
- ---
265
-
266
- ## 10. Transform Attribute
267
-
268
- Floor 1's `roomLivingroom` has:
269
- ```xml
270
- <g id="roomLivingroom" transform="translate(-0.41843424,-0.03740732)">
271
- ```
272
-
273
- This tiny sub-pixel shift aligns the Livingroom paths with the outer border precisely (the border has very slight diagonal lines rather than perfect axis-aligned coordinates). The drawing tool should either:
274
- - Support a `transform` per room for micro-alignment corrections, or
275
- - Guarantee perfectly aligned coordinates so no correction is needed.
276
-
277
- ---
278
-
279
- ## 11. Summary: What a Drawing Tool Needs to Produce
280
-
281
- To produce SVG data compatible with RoomViewer, every exported SVG must satisfy these rules:
282
-
283
- ### Required structure
284
- ```xml
285
- <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="minX minY width height">
286
- <g id="layer1">
287
- <!-- outer border (optional but conventional) -->
288
- <path id="border" d="..." />
289
-
290
- <!-- zero or more room elements -->
291
- <g id="roomXxx"> <!-- Format A -->
292
- <path d="..." /> <!-- wall segment(s) -->
293
- <path id="background" d="..." /> <!-- closed polygon, must end with Z -->
294
- <text ...>Label</text> <!-- optional -->
295
- </g>
296
-
297
- <path id="roomYyy" d="..." /> <!-- Format B, for simple rooms -->
298
- </g>
299
-
300
- <use id="use" href="" /> <!-- REQUIRED, must be last -->
301
- </svg>
302
- ```
303
-
304
- ### Room ID convention
305
- - Must start with `room` (case-sensitive)
306
- - Remainder is the "pure room ID" — stored in state without the `room` prefix
307
- - Examples: `roomLivingroom`, `roomBath`, `roomBedroomOne`
308
-
309
- ### Background path rules
310
- - Must be a **closed polygon** — path `d` must end with `Z`
311
- - Must have exactly `id="background"` (the querySelector targets this exact string)
312
- - Should cover the entire floor area of the room
313
- - For rectilinear rooms: trace all four (or more) corners and close
314
-
315
- ### `<use>` element rules
316
- - Must have `id="use"`
317
- - Must be the last element in the SVG (so it renders on top)
318
- - `href=""` initially empty
319
-
320
- ### Coordinate system for a drawing tool
321
- - Use a fixed grid (e.g. 200×150 units) as the design canvas
322
- - Store rooms as arrays of `{x, y}` corner points (all axis-aligned → only right-angle corners)
323
- - Generate path `d` for wall outlines: open path tracing the room's perimeter segments
324
- - Generate path `d` for background: closed polygon — `M x0,y0 H x1 V y1 H x0 Z` (for a rectangle)
325
- - viewBox should be computed from the bounding box of all content with a small margin
326
- - Keep coordinates as floating-point with 2–6 decimal places (the existing SVGs use 6)
327
-
328
- ### Data format to emit
329
- ```ts
330
- {
331
- id: string, // e.g. 'floorOne'
332
- label: string, // e.g. 'First floor'
333
- data: ReactElement // the <svg> JSX
334
- }
335
- ```
336
-
337
- The drawing tool should render rooms as JSX (using `React.createElement` or a JSX template string evaluated at export time).