@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
package/docs/RoomViewer.md
CHANGED
|
@@ -1,41 +1,94 @@
|
|
|
1
1
|
# RoomViewer
|
|
2
2
|
|
|
3
|
-
**When to use:**
|
|
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:**
|
|
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 {
|
|
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
|
-
|
|
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
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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 +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,`&`).replace(/</g,`<`).replace(/>/g,`>`)}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, '&').replace(/</g, '<').replace(/>/g, '>');\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).
|