@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floorPlan.snap.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.snap.ts"],"sourcesContent":["/**\n * Snapping, and the screen-to-world conversion it depends on.\n *\n * Two things here are deliberately different from the previous implementation:\n *\n * 1. Conversion and snapping are separate. `clientToWorld` only converts; `resolveSnap`\n * is the only thing that decides. The old `clientToSVG` called `snapPt` internally,\n * which made grid snap unconditional and left no way to hold Alt to disable it.\n *\n * 2. Every tolerance is in SCREEN PIXELS, converted through the current zoom at the\n * point of use. A fixed world-unit tolerance (the old `WALL_TOLERANCE = 8`) means\n * snapping covers a smaller and smaller part of the screen as you zoom in, until it\n * stops working.\n */\n\nimport {\n dist,\n pointToSegment,\n projectOnSegment,\n segmentIntersection,\n} from './floorPlan.geometry';\nimport type { PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport enum SnapKind {\n None = 'none',\n Node = 'node',\n /** A point projected onto a wall's centreline. */\n Wall = 'wall',\n /** The crossing of two walls, or of a wall with the active angle ray. */\n Intersection = 'intersection',\n /** On the infinite line continuing an existing wall. */\n Extension = 'extension',\n /** Shares an x or y coordinate with an existing node. */\n Alignment = 'alignment',\n /** On a ray at a multiple of `angleStepDeg` from `origin`. */\n Angle = 'angle',\n Grid = 'grid',\n}\n\nexport interface SnapGuide {\n kind: SnapKind;\n a: PlanPoint;\n b: PlanPoint;\n /** Render as an unbounded construction line rather than a segment. */\n infinite?: boolean;\n}\n\nexport interface SnapOptions {\n /** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */\n enabled: SnapKind[];\n /** Millimetres. 0 disables grid snap. */\n gridSize: number;\n /** Degrees. 45 gives 0/45/90. 0 disables angle lock. */\n angleStepDeg: number;\n toleranceScreenPx: number;\n /** Nodes are the most valuable snap, so they get a little more room. */\n nodeToleranceScreenPx?: number;\n}\n\nexport const DEFAULT_SNAP_OPTIONS: SnapOptions = {\n enabled: [\n SnapKind.Node,\n SnapKind.Intersection,\n SnapKind.Wall,\n SnapKind.Angle,\n SnapKind.Extension,\n SnapKind.Alignment,\n SnapKind.Grid,\n ],\n gridSize: 100,\n angleStepDeg: 0,\n toleranceScreenPx: 10,\n nodeToleranceScreenPx: 12,\n};\n\nexport interface SnapContext {\n floor: PlanFloor;\n /** World units per screen pixel at the current zoom. See `screenScale`. */\n pxToWorld: number;\n /** The anchored point. Required for angle lock; also enables the length readout. */\n origin?: PlanPoint;\n /**\n * A direction in radians that angle lock measures from, in ADDITION to the world axes.\n *\n * Pass the direction of the wall being continued and the lock offers perpendicular and\n * parallel to that wall, not just to the world. Without it, angle lock is only useful\n * on a building that happens to be aligned to the page.\n */\n referenceAngle?: number;\n excludeWallIds?: ReadonlySet<string>;\n excludeNodeIds?: ReadonlySet<string>;\n options?: Partial<SnapOptions>;\n}\n\nexport interface SnapResult {\n point: PlanPoint;\n kind: SnapKind;\n nodeId?: string;\n wallId?: string;\n /** Parameter along `wallId`, for `SnapKind.Wall`. */\n t?: number;\n /** Everything an overlay needs to draw to explain the snap. */\n guides: SnapGuide[];\n /** Radians from `origin` to `point`, when an origin was given. */\n angle?: number;\n /** Distance from `origin` to `point`, when an origin was given. */\n distance?: number;\n /**\n * Set when angle lock produced the result, saying what it locked against: the world\n * axes, or the reference direction that was passed in.\n */\n angleRelativeTo?: 'world' | 'reference';\n}\n\n/** An infinite line as a point and a unit direction. */\ninterface Line {\n kind: SnapKind;\n point: PlanPoint;\n dir: PlanPoint;\n residual: number;\n /** Angle lock only: which basis the ray was measured from. */\n relativeTo?: 'world' | 'reference';\n}\n\n/** World units per screen pixel, from the element's current transform. */\nexport function screenScale(svg: SVGSVGElement): number {\n const ctm = svg.getScreenCTM();\n if (!ctm) return 1;\n // Measured rather than read off ctm.a, so a non-uniform or rotated transform still\n // gives a sensible scalar.\n const scale = Math.hypot(ctm.a, ctm.b);\n return scale === 0 ? 1 : 1 / scale;\n}\n\n/**\n * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.\n *\n * Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than\n * `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it\n * untestable.\n */\nexport function clientToWorld(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number },\n view: { x: number; y: number; zoom: number },\n): PlanPoint {\n return {\n x: view.x + (clientX - rect.left) / view.zoom,\n y: view.y + (clientY - rect.top) / view.zoom,\n };\n}\n\nfunction perpendicularDistanceToLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): number {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n return Math.abs(vx * line.dir.y - vy * line.dir.x);\n}\n\nfunction projectOnLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): PlanPoint {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n const along = vx * line.dir.x + vy * line.dir.y;\n return { x: line.point.x + line.dir.x * along, y: line.point.y + line.dir.y * along };\n}\n\nfunction intersectLines(l1: Line, l2: Line): PlanPoint | null {\n const denom = l1.dir.x * l2.dir.y - l1.dir.y * l2.dir.x;\n if (Math.abs(denom) < 1e-9) return null;\n const dx = l2.point.x - l1.point.x;\n const dy = l2.point.y - l1.point.y;\n const t = (dx * l2.dir.y - dy * l2.dir.x) / denom;\n return { x: l1.point.x + l1.dir.x * t, y: l1.point.y + l1.dir.y * t };\n}\n\nfunction angleBetween(l1: Line, l2: Line): number {\n const dot = Math.abs(l1.dir.x * l2.dir.x + l1.dir.y * l2.dir.y);\n return Math.acos(Math.min(1, dot));\n}\n\nfunction roundTo(value: number, step: number): number {\n return step > 0 ? Math.round(value / step) * step : value;\n}\n\n/**\n * Resolves a raw pointer position to the point the user meant.\n *\n * Two stages, because half of these snaps are constraints rather than points. A hard\n * snap (node, intersection, wall) fixes both degrees of freedom, so the first match\n * wins. A constraint (angle lock, extension, alignment) removes only one, so a flat\n * \"first match wins\" list would kick the point off the angle ray the moment an\n * alignment also matched. Two constraints intersect; one constraint is projected onto\n * and then slid ALONG to the nearest grid position, which is what lets angle lock and\n * grid coexist instead of fighting.\n */\nexport function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult {\n const options: SnapOptions = { ...DEFAULT_SNAP_OPTIONS, ...ctx.options };\n const enabled = new Set(options.enabled);\n\n /**\n * Measures the resolved point back against the origin, so every branch reports the\n * same angle and distance without each one having to compute them. Taking them from\n * the final point rather than from whichever branch produced it means they always\n * describe what the user actually gets.\n */\n const finish = (result: SnapResult): SnapResult => {\n if (!ctx.origin) return result;\n const dx = result.point.x - ctx.origin.x;\n const dy = result.point.y - ctx.origin.y;\n return { ...result, angle: Math.atan2(dy, dx), distance: Math.hypot(dx, dy) };\n };\n\n if (enabled.size === 0) return finish({ point: raw, kind: SnapKind.None, guides: [] });\n\n const tol = options.toleranceScreenPx * ctx.pxToWorld;\n const nodeTol = (options.nodeToleranceScreenPx ?? options.toleranceScreenPx) * ctx.pxToWorld;\n const { floor } = ctx;\n\n /**\n * Angle lock, when the caller has asked for it, is a HARD constraint rather than\n * another proximity snap: `angleStepDeg > 0` only happens because the user latched the\n * lock or held the modifier. Gating it by tolerance like the ambient snaps would mean\n * it silently stopped applying as soon as you dragged more than a few pixels off the\n * ray, which is the opposite of what a lock is for.\n */\n const lockedRay: Line | null = (() => {\n if (!enabled.has(SnapKind.Angle) || !ctx.origin || options.angleStepDeg <= 0) return null;\n const step = (options.angleStepDeg * Math.PI) / 180;\n const rawAngle = Math.atan2(raw.y - ctx.origin.y, raw.x - ctx.origin.x);\n\n // Measure from the world axes, and also from the reference direction when one was\n // given, so \"square to the wall I am continuing\" is available as well as \"square to\n // the page\". Whichever ray the pointer is closest to wins.\n const bases: Array<{ angle: number; relativeTo: 'world' | 'reference' }> = [\n { angle: 0, relativeTo: 'world' },\n ];\n if (ctx.referenceAngle !== undefined) {\n bases.push({ angle: ctx.referenceAngle, relativeTo: 'reference' });\n }\n\n let best: Line | null = null;\n for (const base of bases) {\n const snapped = base.angle + Math.round((rawAngle - base.angle) / step) * step;\n const dir = { x: Math.cos(snapped), y: Math.sin(snapped) };\n const line: Line = {\n kind: SnapKind.Angle,\n point: ctx.origin,\n dir,\n residual: 0,\n relativeTo: base.relativeTo,\n };\n line.residual = perpendicularDistanceToLine(raw, line);\n if (!best || line.residual < best.residual) best = line;\n }\n return best;\n })();\n\n /**\n * While the lock is on, a hard snap is only allowed to win if it sits ON the ray.\n * Otherwise \"hold the angle\" would be quietly overridden by any nearby corner, which\n * is exactly the case the lock exists to prevent.\n */\n const allowedByLock = (p: PlanPoint): boolean =>\n !lockedRay || perpendicularDistanceToLine(p, lockedRay) <= tol;\n\n // ── Stage A: hard snaps ──────────────────────────────────────────────────────\n\n if (enabled.has(SnapKind.Node)) {\n let best: { id: string; point: PlanPoint; d: number } | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const d = dist(raw, node);\n if (d <= nodeTol && allowedByLock(node) && (!best || d < best.d)) {\n best = { id: node.id, point: { x: node.x, y: node.y }, d };\n }\n }\n if (best) {\n return finish({ point: best.point, kind: SnapKind.Node, nodeId: best.id, guides: [] });\n }\n }\n\n const walls = Object.values(floor.walls).filter((w) => !ctx.excludeWallIds?.has(w.id));\n\n if (enabled.has(SnapKind.Intersection)) {\n let best: { point: PlanPoint; d: number } | null = null;\n for (let i = 0; i < walls.length; i++) {\n for (let j = i + 1; j < walls.length; j++) {\n const a = floor.nodes[walls[i].a];\n const b = floor.nodes[walls[i].b];\n const c = floor.nodes[walls[j].a];\n const d2 = floor.nodes[walls[j].b];\n if (!a || !b || !c || !d2) continue;\n const hit = segmentIntersection(a, b, c, d2);\n if (!hit.point) continue;\n const d = dist(raw, hit.point);\n if (d <= tol && allowedByLock(hit.point) && (!best || d < best.d)) {\n best = { point: hit.point, d };\n }\n }\n }\n if (best) return finish({ point: best.point, kind: SnapKind.Intersection, guides: [] });\n }\n\n if (enabled.has(SnapKind.Wall)) {\n let best: { wallId: string; point: PlanPoint; t: number; d: number } | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const r = pointToSegment(raw, a, b);\n if (r.dist <= tol && allowedByLock(r.point) && (!best || r.dist < best.d)) {\n // Project exactly: intent is allowed to be approximate, the model is not.\n best = { wallId: wall.id, point: projectOnSegment(raw, a, b), t: r.t, d: r.dist };\n }\n }\n if (best) {\n return finish({\n point: best.point,\n kind: SnapKind.Wall,\n wallId: best.wallId,\n t: best.t,\n guides: [],\n });\n }\n }\n\n // ── Stage B: constraints ─────────────────────────────────────────────────────\n\n const constraints: Line[] = [];\n\n if (enabled.has(SnapKind.Extension)) {\n let best: Line | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = dist(a, b);\n if (len < 1e-6) continue;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const line: Line = { kind: SnapKind.Extension, point: { x: a.x, y: a.y }, dir, residual: 0 };\n line.residual = perpendicularDistanceToLine(raw, line);\n // Only beyond the segment itself; on the segment, the wall snap already applies.\n const onSegment = pointToSegment(raw, a, b).dist <= tol;\n if (line.residual <= tol && !onSegment && (!best || line.residual < best.residual)) {\n best = line;\n }\n }\n if (best) constraints.push(best);\n }\n\n if (enabled.has(SnapKind.Alignment)) {\n let bestV: Line | null = null;\n let bestH: Line | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const dx = Math.abs(raw.x - node.x);\n if (dx <= tol && (!bestV || dx < bestV.residual)) {\n bestV = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 0, y: 1 },\n residual: dx,\n };\n }\n const dy = Math.abs(raw.y - node.y);\n if (dy <= tol && (!bestH || dy < bestH.residual)) {\n bestH = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 1, y: 0 },\n residual: dy,\n };\n }\n }\n if (bestV) constraints.push(bestV);\n if (bestH) constraints.push(bestH);\n }\n\n const guideFor = (line: Line, through: PlanPoint): SnapGuide => ({\n kind: line.kind,\n a: { x: through.x - line.dir.x * 1e5, y: through.y - line.dir.y * 1e5 },\n b: { x: through.x + line.dir.x * 1e5, y: through.y + line.dir.y * 1e5 },\n infinite: true,\n });\n\n if (lockedRay) {\n // The ray is fixed; the only freedom left is where along it the point sits. Another\n // constraint that genuinely crosses it pins that down, which is the useful\n // \"45 degrees AND level with that corner\" case.\n const crossing = constraints\n .filter((c) => c.residual <= tol && angleBetween(lockedRay, c) > (15 * Math.PI) / 180)\n .sort((a, b) => a.residual - b.residual)[0];\n\n if (crossing) {\n const point = intersectLines(lockedRay, crossing);\n if (point && dist(point, raw) <= tol * 4) {\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point), guideFor(crossing, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n }\n\n let point = projectOnLine(raw, lockedRay);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the ray to a round length, rather than rounding both coordinates,\n // which would take the point straight back off it.\n const along =\n (point.x - lockedRay.point.x) * lockedRay.dir.x +\n (point.y - lockedRay.point.y) * lockedRay.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: lockedRay.point.x + lockedRay.dir.x * snappedAlong,\n y: lockedRay.point.y + lockedRay.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n\n if (constraints.length >= 2) {\n const sorted = [...constraints].sort((a, b) => a.residual - b.residual);\n const [l1, l2] = sorted;\n const point = intersectLines(l1, l2);\n // Two nearly parallel constraints intersect somewhere useless and far away.\n if (point && angleBetween(l1, l2) > (15 * Math.PI) / 180 && dist(point, raw) <= tol * 2) {\n return finish({\n point,\n kind: SnapKind.Intersection,\n guides: [guideFor(l1, point), guideFor(l2, point)],\n });\n }\n }\n\n if (constraints.length >= 1) {\n const line = [...constraints].sort((a, b) => a.residual - b.residual)[0];\n let point = projectOnLine(raw, line);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the constraint to a round distance, rather than rounding both\n // coordinates, which would take the point back off the line.\n const along =\n (point.x - line.point.x) * line.dir.x + (point.y - line.point.y) * line.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: line.point.x + line.dir.x * snappedAlong,\n y: line.point.y + line.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: line.kind,\n guides: [guideFor(line, point)],\n angleRelativeTo: line.relativeTo,\n });\n }\n\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n return finish({\n point: { x: roundTo(raw.x, options.gridSize), y: roundTo(raw.y, options.gridSize) },\n kind: SnapKind.Grid,\n guides: [],\n });\n }\n\n return finish({ point: raw, kind: SnapKind.None, guides: [] });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e=function(e){return e.Metric=`metric`,e.Imperial=`imperial`,e}({}),t=function(e){return e.Exterior=`exterior`,e.Interior=`interior`,e.Partition=`partition`,e.Virtual=`virtual`,e}({}),n=function(e){return e.Door=`door`,e.Window=`window`,e.Archway=`archway`,e.Gap=`gap`,e}({}),r=function(e){return e.None=`none`,e.StartLeft=`startLeft`,e.StartRight=`startRight`,e.EndLeft=`endLeft`,e.EndRight=`endRight`,e.Double=`double`,e.Sliding=`sliding`,e}({}),i=function(e){return e.ZeroLengthWall=`zeroLengthWall`,e.SelfLoopWall=`selfLoopWall`,e.DuplicateWall=`duplicateWall`,e.CoincidentNodes=`coincidentNodes`,e.UnsplitCrossing=`unsplitCrossing`,e.UnsplitNodeOnWall=`unsplitNodeOnWall`,e.OverlappingWalls=`overlappingWalls`,e.OrphanNode=`orphanNode`,e.DanglingOpening=`danglingOpening`,e.UnmatchedRoom=`unmatchedRoom`,e.OpeningDropped=`openingDropped`,e}({});export{r as DoorSwing,n as OpeningKind,i as PlanIssueKind,e as PlanUnitSystem,t as WallType};
2
+ //# sourceMappingURL=floorPlan.types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floorPlan.types.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.types.ts"],"sourcesContent":["/**\n * Floor plan model: a planar wall graph whose rooms are derived, not stored.\n *\n * Nodes and walls are first-class and shared, so the wall between two rooms is one\n * wall. An opening therefore cuts it exactly once, which is what the previous\n * room-polygon model could not express.\n *\n * GEOMETRY INVARIANT: every coordinate, length, thickness, offset and height in this\n * module is in MILLIMETRES. There is deliberately no per-plan scale factor: one would\n * have to be threaded through every geometry function, and a single missed call site\n * is a silent 1000x error that no type can catch. `unitSystem` below drives display\n * formatting only and is never read by geometry.\n */\n\n/** A point in plan space, in millimetres. y increases downwards, as in SVG. */\nexport interface PlanPoint {\n x: number;\n y: number;\n}\n\nexport enum PlanUnitSystem {\n /** Display as m / cm / mm. */\n Metric = 'metric',\n /** Display as feet and inches. Storage stays millimetres. */\n Imperial = 'imperial',\n}\n\nexport enum WallType {\n /** Load-bearing envelope wall. */\n Exterior = 'exterior',\n /** Load-bearing interior wall. */\n Interior = 'interior',\n /** Non-load-bearing partition. */\n Partition = 'partition',\n /**\n * A room boundary with no physical wall, for dividing open-plan space. It still\n * closes a face, so it produces two rooms, but renders as a thin dashed line and\n * extrudes to nothing in 3D.\n */\n Virtual = 'virtual',\n}\n\nexport enum OpeningKind {\n /** A real door leaf, with a swing and a threshold. */\n Door = 'door',\n /** Glazed opening with a sill above floor level. */\n Window = 'window',\n /** Structural opening with a head but no leaf. */\n Archway = 'archway',\n /**\n * A plain hole in the wall with no jamb detailing. Distinct from \"no wall at all\":\n * a Gap is a marked span of a wall that exists, which is what lets a gap-style\n * rendering stay unambiguous.\n */\n Gap = 'gap',\n}\n\nexport enum DoorSwing {\n None = 'none',\n /** Hinged at the opening's start, opening towards the wall's left normal. */\n StartLeft = 'startLeft',\n StartRight = 'startRight',\n EndLeft = 'endLeft',\n EndRight = 'endRight',\n /** Two leaves, hinged at both ends. */\n Double = 'double',\n Sliding = 'sliding',\n}\n\n/** A graph vertex. Structurally assignable to `PlanPoint`. */\nexport interface PlanNode {\n id: string;\n x: number;\n y: number;\n}\n\n/**\n * An undirected edge between two nodes. `a` and `b` define the wall's own direction,\n * which is what opening offsets and door swings are measured against.\n *\n * The wall is centred on the segment a..b, extending `thickness / 2` to each side.\n * There is no eccentric offset in this version.\n */\nexport interface PlanWall {\n id: string;\n a: string;\n b: string;\n /** Millimetres. Must be greater than 0. */\n thickness: number;\n /** Millimetres above this wall's base. Omit to inherit `PlanFloor.height`. */\n height?: number;\n /** Millimetres above the floor's finished level. Defaults to 0. */\n baseHeight?: number;\n type: WallType;\n}\n\n/**\n * An opening cut into exactly one wall. Because a shared wall is one wall, an opening\n * between two rooms exists exactly once and needs no de-duplication anywhere.\n */\nexport interface PlanOpening {\n id: string;\n wallId: string;\n kind: OpeningKind;\n /**\n * Distance in millimetres from the wall's node `a` to the opening's CENTRE, measured\n * along the centreline.\n *\n * Deliberately absolute rather than a 0..1 parameter: with a parameter, stretching a\n * wall by dragging one of its nodes would rescale every door on it. Absolute offsets\n * are clamped instead when a wall shortens.\n */\n offset: number;\n /** Millimetres, along the wall. */\n width: number;\n /**\n * Height of the opening's BOTTOM above the floor, in millimetres: 0 for a door, around\n * 900 for a window. Nothing in the 2D plan reads it, since a plan is a horizontal cut\n * and every opening looks the same from above. It is here so the same plan can drive a\n * 3D view later without the heights having to be invented or asked for again.\n */\n sill: number;\n /** Height of the opening's TOP above the floor, in millimetres. Typically 2100. */\n head: number;\n /** Only meaningful for `OpeningKind.Door`. */\n swing?: DoorSwing;\n}\n\n/**\n * Room METADATA. This is not a geometric object: the polygon comes from face\n * extraction. The trailing `wallIds` / `area` fields are a match cache maintained by\n * `derivePlan`, and are the reason a room's name survives edits.\n */\nexport interface PlanRoom {\n id: string;\n name: string;\n /** Any CSS colour. Omit to use the theme default. */\n colour?: string;\n /** Free-form usage tag (\"kitchen\", \"bath\"), for later fixture and 3D defaults. */\n usage?: string;\n /**\n * A point that lay inside the room's polygon when it was last written. Used as the\n * label position and as the secondary identity signal during matching.\n */\n anchor: PlanPoint;\n /** Set once the user drags the label; `derivePlan` then stops moving it. */\n anchorPinned: boolean;\n /** Monotonic creation counter, used as a deterministic tie-break when matching. */\n seq: number;\n /** Match cache: the wall ids on this room's boundary at the last successful match. */\n wallIds: string[];\n /** Match cache: area in mm² at the last successful match. */\n area: number;\n}\n\nexport interface PlanFloor {\n id: string;\n label: string;\n /** Finished floor level of this storey, millimetres above datum. */\n elevation: number;\n /** Default storey height in millimetres, used when a wall omits its own `height`. */\n height: number;\n /**\n * Keyed by id for O(1) lookup. Ids must not be numeric strings ('n_12', never '12'),\n * so that JavaScript preserves insertion order and iteration stays reproducible.\n */\n nodes: Record<string, PlanNode>;\n walls: Record<string, PlanWall>;\n openings: Record<string, PlanOpening>;\n rooms: Record<string, PlanRoom>;\n}\n\nexport interface FloorPlan {\n id: string;\n name?: string;\n /** Schema version, for future migrations. */\n version: 1;\n /** Display formatting only. Never read by geometry. */\n unitSystem: PlanUnitSystem;\n floors: PlanFloor[];\n /**\n * Bumped by every mutating operation. Use it as the memo key for `derivePlan`, so a\n * gesture that does not actually change the graph does not trigger a re-derive.\n */\n revision: number;\n}\n\n/**\n * A raw face from the planar walk, before room metadata is attached.\n *\n * WINDING: `outer` is positively wound under the shoelace formula, which in SVG's\n * y-down space means it reads as visually CLOCKWISE. Anything downstream that assumes\n * counter-clockwise (extruded slab normals, most triangulators) must flip it.\n */\nexport interface PlanFace {\n /**\n * Derived from the boundary itself, so it is stable across derives for as long as\n * the geometry is unchanged. Safe to use as a React key.\n */\n id: string;\n /** Outer ring, closed implicitly: the first vertex is not repeated at the end. */\n outer: PlanPoint[];\n /** Inner rings from nested components, wound opposite to `outer`. */\n holes: PlanPoint[][];\n /** mm², always positive, with holes already subtracted. */\n area: number;\n /** mm, outer ring only. */\n perimeter: number;\n /**\n * Wall ids on the boundary, in traversal order. A dangling wall inside this face\n * appears TWICE, once per direction, so consumers needing unique ids must dedupe.\n */\n wallIds: string[];\n nodeIds: string[];\n}\n\n/** A face with its matched metadata. This is what the components render. */\nexport interface DerivedRoom extends PlanFace {\n /** `null` when no `PlanRoom` matched this face during this derive. */\n roomId: string | null;\n meta: PlanRoom | null;\n /** Guaranteed inside `outer` and outside every hole. The label position. */\n anchor: PlanPoint;\n}\n\nexport enum PlanIssueKind {\n ZeroLengthWall = 'zeroLengthWall',\n SelfLoopWall = 'selfLoopWall',\n DuplicateWall = 'duplicateWall',\n CoincidentNodes = 'coincidentNodes',\n /** Two walls cross with no node at the intersection. Corrupts face extraction. */\n UnsplitCrossing = 'unsplitCrossing',\n /** A node sits on a wall's interior but that wall was never split there. */\n UnsplitNodeOnWall = 'unsplitNodeOnWall',\n /** Two walls leave a node in the same direction, tying the angular sort. */\n OverlappingWalls = 'overlappingWalls',\n OrphanNode = 'orphanNode',\n DanglingOpening = 'danglingOpening',\n /** A `PlanRoom` matched no face this derive. It is kept, never deleted. */\n UnmatchedRoom = 'unmatchedRoom',\n /** An opening was moved or dropped because its wall no longer has room for it. */\n OpeningDropped = 'openingDropped',\n}\n\nexport interface PlanIssue {\n kind: PlanIssueKind;\n message: string;\n nodeIds?: string[];\n wallIds?: string[];\n roomIds?: string[];\n openingIds?: string[];\n}\n\nexport interface PlanDerivation {\n /** The floor with room match caches and anchors refreshed. Pure: a new object. */\n floor: PlanFloor;\n rooms: DerivedRoom[];\n issues: PlanIssue[];\n}\n\n/** The uniform result of every graph mutation. All of them are pure. */\nexport interface PlanEdit<T = void> {\n floor: PlanFloor;\n result: T;\n issues: PlanIssue[];\n}\n\n/**\n * The viewport: which world point sits at the canvas's top-left corner, and how many\n * screen pixels one world millimetre occupies.\n *\n * A 6m plan filling a 600px canvas is `zoom: 0.1`. Note this is a transform on a group,\n * not an SVG `viewBox`: handles, labels and hit tolerances are then sized in screen\n * pixels and stay constant as you zoom, instead of scaling with the drawing.\n */\nexport interface PlanView {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** What is currently selected or hovered. */\nexport type PlanSelection =\n | { type: 'room'; id: string }\n | { type: 'wall'; id: string }\n | { type: 'opening'; id: string }\n | { type: 'node'; id: string };\n"],"mappings":"AAoBA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,OAAA,SAEA,EAAA,SAAA,YACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,SAAA,WAEA,EAAA,SAAA,WAEA,EAAA,UAAA,YAMA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,KAAA,OAEA,EAAA,OAAA,SAEA,EAAA,QAAA,UAMA,EAAA,IAAA,OACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,KAAA,OAEA,EAAA,UAAA,YACA,EAAA,WAAA,aACA,EAAA,QAAA,UACA,EAAA,SAAA,WAEA,EAAA,OAAA,SACA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EA8JY,EAAL,SAAA,EAAA,OACL,GAAA,eAAA,iBACA,EAAA,aAAA,eACA,EAAA,cAAA,gBACA,EAAA,gBAAA,kBAEA,EAAA,gBAAA,kBAEA,EAAA,kBAAA,oBAEA,EAAA,iBAAA,mBACA,EAAA,WAAA,aACA,EAAA,gBAAA,kBAEA,EAAA,cAAA,gBAEA,EAAA,eAAA,kBACF,EAAA,CAAA,CAAA"}
@@ -0,0 +1,2 @@
1
+ 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
@@ -0,0 +1 @@
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(\n /^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i,\n );\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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"}
@@ -0,0 +1,2 @@
1
+ import{OpeningKind as e}from"../floorPlan/floorPlan.types.mjs";import{buildWallPaths as t,openingGeometry as n}from"../floorPlan/floorPlan.path.mjs";import r from"./planCanvas.module.mjs";import{forwardRef as i,useMemo as a}from"react";import o from"classnames";import{Fragment as s,jsx as c,jsxs as l}from"react/jsx-runtime";var u=1,d=[];function f(e){let t=e/1e6;return`${t.toFixed(+(t<10))} m²`}function p(e){return e.length===0?``:`${e.map((e,t)=>`${t===0?`M`:`L`} ${e.x},${e.y}`).join(` `)} Z`}function m(e){return[p(e.outer),...e.holes.map(p)].filter(Boolean).join(` `)}var h=i(function(i,p){let{floor:h,underlay:g,rooms:_,view:v,selection:y=d,hovered:b=null,interactiveRooms:x=!1,interactiveOpenings:S=!1,showGrid:C=!1,gridSize:w=100,showRoomLabels:T=!0,showOpeningSymbols:E=!0,getRoomColor:D,renderRoomLabel:O,getRoomLabel:k,formatArea:A=f,onRoomActivate:j,onOpeningActivate:M,onBackgroundActivate:N,onHoverChange:P,overlay:F,worldOverlay:I,size:L,className:R,style:z,classNames:B,styles:V,"aria-label":H}=i,U=v.zoom>0?1/v.zoom:1,W=u*U,G=a(()=>t(h),[h]),K=a(()=>g?Object.values(g.walls).flatMap(e=>{let t=g.nodes[e.a],n=g.nodes[e.b];return t&&n?[{id:e.id,x1:t.x,y1:t.y,x2:n.x,y2:n.y}]:[]}):[],[g]),q=e=>({x:(e.x-v.x)*v.zoom,y:(e.y-v.y)*v.zoom}),J=a(()=>{if(!C||w<=0)return null;let e=L.width*U,t=L.height*U,n=w*Math.max(1,10**Math.ceil(Math.log10(e/w/60))),r=Math.floor(v.x/n)*n,i=Math.floor(v.y/n)*n,a=[],o=[];for(let t=r;t<=v.x+e;t+=n)a.push(t);for(let e=i;e<=v.y+t;e+=n)o.push(e);return{verticals:a,horizontals:o,step:n,worldW:e,worldH:t}},[C,w,L.width,L.height,U,v.x,v.y]),Y=(e,t)=>y.some(n=>n.type===e&&n.id===t),X=(e,t)=>b?.type===e&&b.id===t,Z=a(()=>{let e=new Set(y.filter(e=>e.type===`room`).map(e=>e.id)),t=new Set;for(let n of _)if(n.roomId&&e.has(n.roomId))for(let e of n.wallIds)t.add(e);return t},[_,y]),Q=a(()=>Z.size>0?t(h,{wallIds:Z}):{groups:[],caps:[]},[h,Z]);function $(t){let i=n(h,t);if(!i)return null;let a=h.walls[t.wallId],s=a?a.thickness/2:50,{p1:u,p2:d,normal:f,dir:p,centre:m}=i,g=Y(`opening`,t.id),_=X(`opening`,t.id),v=Math.hypot(d.x-u.x,d.y-u.y),y=[];if(E&&t.kind===e.Door){let e=t.swing!==`endLeft`&&t.swing!==`endRight`,n=e?u:d,i=e?1:-1,a={x:n.x+f.x*v,y:n.y+f.y*v},o={x:n.x+p.x*v*i,y:n.y+p.y*v*i};y.push(c(`path`,{className:r.planCanvasDoorLeaf,d:`M ${n.x},${n.y} L ${a.x},${a.y}`,strokeWidth:W},`leaf`),c(`path`,{className:r.planCanvasDoorSwing,d:`M ${a.x},${a.y} A ${v},${v} 0 0,${+!e} ${o.x},${o.y}`,strokeWidth:W},`swing`))}E&&t.kind===e.Window&&y.push(c(`path`,{className:r.planCanvasWindowPane,d:`M ${u.x},${u.y} L ${d.x},${d.y}`,strokeWidth:W*2},`pane`));let b=`M ${u.x+f.x*s},${u.y+f.y*s} L ${d.x+f.x*s},${d.y+f.y*s} L ${d.x-f.x*s},${d.y-f.y*s} L ${u.x-f.x*s},${u.y-f.y*s} Z`;return l(`g`,{className:o(g&&r.planCanvasOpeningSelected,_&&r.planCanvasOpeningHovered,B?.opening),style:V?.opening,"data-opening-kind":t.kind,children:[y,c(`path`,{className:o(r.planCanvasOpeningHit,S&&r.planCanvasOpeningHitInteractive),d:b,onClick:S&&M?e=>{e.stopPropagation(),M(t.id)}:void 0,onPointerEnter:P?()=>P({type:`opening`,id:t.id}):void 0,children:S&&c(`title`,{children:t.kind})}),(g||_)&&c(`path`,{className:r.planCanvasOpeningOutline,d:b,vectorEffect:`non-scaling-stroke`}),c(`path`,{className:o(r.planCanvasJamb,Z.has(t.wallId)&&r.planCanvasJambHighlighted),d:`M ${u.x-f.x*s},${u.y-f.y*s} L ${u.x+f.x*s},${u.y+f.y*s} M ${d.x-f.x*s},${d.y-f.y*s} L ${d.x+f.x*s},${d.y+f.y*s}`,strokeWidth:W}),m&&null]},t.id)}return l(`svg`,{ref:p,className:o(r.planCanvas,R,B?.canvas),style:{...z,...V?.canvas},viewBox:`0 0 ${Math.max(1,L.width)} ${Math.max(1,L.height)}`,width:L.width,height:L.height,role:`img`,"aria-label":H,onClick:N?()=>N():void 0,onPointerLeave:P?()=>P(null):void 0,children:[l(`g`,{transform:`translate(${-v.x*v.zoom} ${-v.y*v.zoom}) scale(${v.zoom})`,children:[J&&l(`g`,{className:o(r.planCanvasGrid,B?.grid),style:V?.grid,"aria-hidden":`true`,children:[J.verticals.map(e=>c(`line`,{x1:e,y1:v.y,x2:e,y2:v.y+J.worldH,strokeWidth:U},`v${e}`)),J.horizontals.map(e=>c(`line`,{x1:v.x,y1:e,x2:v.x+J.worldW,y2:e,strokeWidth:U},`h${e}`))]}),K.length>0&&c(`g`,{className:o(r.planCanvasUnderlay,B?.underlay),style:V?.underlay,"aria-hidden":`true`,children:K.map(e=>c(`line`,{x1:e.x1,y1:e.y1,x2:e.x2,y2:e.y2,strokeWidth:1.5},e.id))}),c(`g`,{className:o(r.planCanvasRooms,B?.rooms),style:V?.rooms,children:_.map(e=>{let t=e.roomId?Y(`room`,e.roomId):!1,n=e.roomId?X(`room`,e.roomId):!1,i=e.meta?D?.(e.meta,e.area):void 0,a=k?.(e.meta,{area:e.area})??`${e.meta?.name??`Unnamed room`}, ${A(e.area)}`;return c(`path`,{className:o(r.planCanvasRoomFill,n&&r.planCanvasRoomFillHovered,x&&r.planCanvasRoomFillInteractive,B?.roomFill),"data-state":t?`selected`:n?`hovered`:void 0,style:{...V?.roomFill,...i?{fill:i}:null},d:m(e),fillRule:`evenodd`,tabIndex:x?0:void 0,role:x?`button`:void 0,"aria-pressed":x?t:void 0,onClick:x&&j&&e.roomId?t=>{t.stopPropagation(),j(e.roomId)}:void 0,onKeyDown:x&&j&&e.roomId?t=>{t.key!==`Enter`&&t.key!==` `||(t.preventDefault(),j(e.roomId))}:void 0,onPointerEnter:P&&e.roomId?()=>P({type:`room`,id:e.roomId}):void 0,children:x&&c(`title`,{children:a})},e.id)})}),l(`g`,{className:o(r.planCanvasWalls,B?.walls),style:V?.walls,"aria-hidden":`true`,children:[c(`g`,{className:r.planCanvasWallOutline,children:G.groups.map(e=>c(`path`,{className:B?.wallBody,style:V?.wallBody,d:e.d,strokeWidth:e.thickness},`o${e.thickness}`))}),c(`g`,{className:r.planCanvasWallFill,children:G.groups.map(e=>{let t=e.thickness-2*W;return t<=0?null:c(`path`,{d:e.d,strokeWidth:t},`f${e.thickness}`)})}),l(`g`,{className:r.planCanvasRoomEdgeSelected,children:[Q.groups.map(e=>c(`path`,{className:r.planCanvasRoomEdgeOutline,d:e.d,strokeWidth:e.thickness},`o${e.thickness}`)),Q.groups.map(e=>{let t=e.thickness-2*W;return t<=0?null:c(`path`,{className:r.planCanvasRoomEdgeFill,d:e.d,strokeWidth:t},`f${e.thickness}`)})]}),c(`g`,{className:r.planCanvasWallCaps,children:G.caps.filter(e=>!Z.has(e.wallId)).map((e,t)=>c(`line`,{x1:e.a.x,y1:e.a.y,x2:e.b.x,y2:e.b.y,strokeWidth:W},`${e.wallId}-${e.reason}-${t}`))}),c(`g`,{className:r.planCanvasRoomEdgeCaps,children:Q.caps.map((e,t)=>c(`line`,{x1:e.a.x,y1:e.a.y,x2:e.b.x,y2:e.b.y,strokeWidth:W},`${e.wallId}-${e.reason}-${t}`))})]}),c(`g`,{className:o(r.planCanvasOpenings,B?.openings),style:V?.openings,children:Object.values(h.openings).map($)}),l(`g`,{className:r.planCanvasSelection,"aria-hidden":`true`,children:[y.filter(e=>e.type===`wall`).map(e=>{let t=h.walls[e.id];if(!t)return null;let n=h.nodes[t.a],i=h.nodes[t.b];return!n||!i?null:c(`line`,{x1:n.x,y1:n.y,x2:i.x,y2:i.y,strokeWidth:t.thickness,className:r.planCanvasWallSelected},e.id)}),b?.type===`wall`&&!Y(`wall`,b.id)&&(()=>{let e=h.walls[b.id];if(!e)return null;let t=h.nodes[e.a],n=h.nodes[e.b];return!t||!n?null:c(`line`,{x1:t.x,y1:t.y,x2:n.x,y2:n.y,strokeWidth:e.thickness,className:r.planCanvasWallHovered})})()]}),I]}),l(`g`,{className:o(r.planCanvasScreen,B?.overlay),style:V?.overlay,children:[T&&_.map(e=>{let t=q(e.anchor);if(t.x<-200||t.y<-200||t.x>L.width+200||t.y>L.height+200)return null;let n=O?.(e.meta,{area:e.area,faceId:e.id});return n===null?null:c(`g`,{className:o(r.planCanvasRoomLabel,B?.roomLabel),style:V?.roomLabel,transform:`translate(${t.x} ${t.y})`,"aria-hidden":`true`,children:n===void 0?l(s,{children:[c(`text`,{className:r.planCanvasRoomName,textAnchor:`middle`,children:e.meta?.name??``}),c(`text`,{className:r.planCanvasRoomArea,textAnchor:`middle`,dy:`1.2em`,children:A(e.area)})]}):n},e.id)}),F]})]})});export{h as default};
2
+ //# sourceMappingURL=planCanvas.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planCanvas.mjs","names":[],"sources":["../../../../package/common/planCanvas/planCanvas.tsx"],"sourcesContent":["import { forwardRef, useMemo, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport {\n OpeningKind,\n buildWallPaths,\n openingGeometry,\n type DerivedRoom,\n type PlanOpening,\n type PlanPoint,\n type PlanSelection,\n} from '../floorPlan';\nimport type { PlanCanvasProps } from './planCanvas.types';\nimport styles from './planCanvas.module.pcss';\n\n/**\n * The read-only renderer shared by RoomDrawer and RoomViewer.\n *\n * It owns the projection, the layer stack, and the accessible room shapes. It owns no\n * editing state: the drawer supplies its handles and previews through `overlay`. Having\n * one renderer is the point, so that a plan cannot look different in the editor than it\n * does in the viewer.\n *\n * The viewport is a TRANSFORM on a group, not an SVG `viewBox`. That is what lets the\n * screen-space layer draw labels and handles at a fixed pixel size while the drawing\n * itself scales.\n */\n\nconst DEFAULT_OUTLINE_PX = 1;\n\n/** Stable identity, so a default-valued `selection` prop does not re-run memos. */\nconst EMPTY_SELECTION: readonly PlanSelection[] = [];\n\nfunction formatAreaDefault(areaMm2: number): string {\n const m2 = areaMm2 / 1_000_000;\n return `${m2.toFixed(m2 < 10 ? 1 : 0)} m²`;\n}\n\nfunction ringToPath(ring: PlanPoint[]): string {\n if (ring.length === 0) return '';\n return `${ring.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ')} Z`;\n}\n\n/** Outer ring plus holes as one path; `fill-rule: evenodd` punches the holes out. */\nfunction faceToPath(face: DerivedRoom): string {\n return [ringToPath(face.outer), ...face.holes.map(ringToPath)].filter(Boolean).join(' ');\n}\n\n/**\n * The ref lands on the `<svg>` itself, which is what a consumer wants for measuring or\n * screenshotting. Both the drawer and the viewer expose it as `getSvgElement()`.\n */\nconst PlanCanvas = forwardRef<SVGSVGElement, PlanCanvasProps>(function PlanCanvas(props, ref) {\n const {\n floor,\n underlay,\n rooms,\n view,\n selection = EMPTY_SELECTION,\n hovered = null,\n interactiveRooms = false,\n interactiveOpenings = false,\n showGrid = false,\n gridSize = 100,\n showRoomLabels = true,\n showOpeningSymbols = true,\n getRoomColor,\n renderRoomLabel,\n getRoomLabel,\n formatArea = formatAreaDefault,\n onRoomActivate,\n onOpeningActivate,\n onBackgroundActivate,\n onHoverChange,\n overlay,\n worldOverlay,\n size,\n className,\n style,\n classNames,\n styles: slotStyles,\n 'aria-label': ariaLabel,\n } = props;\n\n const pxToWorld = view.zoom > 0 ? 1 / view.zoom : 1;\n const outlineWidth = DEFAULT_OUTLINE_PX * pxToWorld;\n\n const wallPaths = useMemo(() => buildWallPaths(floor), [floor]);\n\n /**\n * Centrelines only, at one stroke width. The underlay is there to line walls up\n * against, so it wants to read as a tracing rather than compete with the floor being\n * drawn; the two-pass thick-wall treatment would give it equal weight.\n */\n const underlayLines = useMemo(() => {\n if (!underlay) return [];\n return Object.values(underlay.walls).flatMap((wall) => {\n const a = underlay.nodes[wall.a];\n const b = underlay.nodes[wall.b];\n return a && b ? [{ id: wall.id, x1: a.x, y1: a.y, x2: b.x, y2: b.y }] : [];\n });\n }, [underlay]);\n\n /** World point to screen point, for the untransformed layer. */\n const toScreen = (p: PlanPoint) => ({\n x: (p.x - view.x) * view.zoom,\n y: (p.y - view.y) * view.zoom,\n });\n\n // Grid lines are generated for the visible rect only. Deliberately not an SVG\n // <pattern>: a pattern needs an id, and two canvases on one page then collide.\n const gridLines = useMemo(() => {\n if (!showGrid || gridSize <= 0) return null;\n const worldW = size.width * pxToWorld;\n const worldH = size.height * pxToWorld;\n // Keep the line count sane when zoomed far out.\n const step = gridSize * Math.max(1, Math.pow(10, Math.ceil(Math.log10(worldW / gridSize / 60))));\n const startX = Math.floor(view.x / step) * step;\n const startY = Math.floor(view.y / step) * step;\n const verticals: number[] = [];\n const horizontals: number[] = [];\n for (let x = startX; x <= view.x + worldW; x += step) verticals.push(x);\n for (let y = startY; y <= view.y + worldH; y += step) horizontals.push(y);\n return { verticals, horizontals, step, worldW, worldH };\n }, [showGrid, gridSize, size.width, size.height, pxToWorld, view.x, view.y]);\n\n const isSelected = (type: string, id: string) =>\n selection.some((entry) => entry.type === type && entry.id === id);\n const isHovered = (type: string, id: string) => hovered?.type === type && hovered.id === id;\n\n /**\n * The walls bounding the selected rooms. A SELECTED room is marked by colouring its\n * boundary rather than by flooding its floor, which would bury its doors and its label;\n * hover keeps the faint floor tint instead, since the two say different things.\n */\n const highlightedWalls = useMemo(() => {\n const selectedRooms = new Set(\n selection.filter((entry) => entry.type === 'room').map((entry) => entry.id),\n );\n const ids = new Set<string>();\n for (const room of rooms) {\n if (room.roomId && selectedRooms.has(room.roomId)) {\n // A wall shared by two selected rooms belongs to both; the set draws it once.\n for (const id of room.wallIds) ids.add(id);\n }\n }\n return ids;\n }, [rooms, selection]);\n\n const roomEdge = useMemo(\n () =>\n highlightedWalls.size > 0\n ? buildWallPaths(floor, { wallIds: highlightedWalls })\n : { groups: [], caps: [] },\n [floor, highlightedWalls],\n );\n\n function renderOpening(opening: PlanOpening) {\n const geo = openingGeometry(floor, opening);\n if (!geo) return null;\n const wall = floor.walls[opening.wallId];\n const half = wall ? wall.thickness / 2 : 50;\n const { p1, p2, normal, dir, centre } = geo;\n\n const selected = isSelected('opening', opening.id);\n const hoveredNow = isHovered('opening', opening.id);\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n\n const parts: ReactElement[] = [];\n\n if (showOpeningSymbols && opening.kind === OpeningKind.Door) {\n // Leaf plus swing arc, drawn on the wall's left side.\n const hingeAtStart = opening.swing !== 'endLeft' && opening.swing !== 'endRight';\n const hinge = hingeAtStart ? p1 : p2;\n const sweepDir = hingeAtStart ? 1 : -1;\n const leafEnd = { x: hinge.x + normal.x * width, y: hinge.y + normal.y * width };\n const arcEnd = {\n x: hinge.x + dir.x * width * sweepDir,\n y: hinge.y + dir.y * width * sweepDir,\n };\n parts.push(\n <path\n key=\"leaf\"\n className={styles.planCanvasDoorLeaf}\n d={`M ${hinge.x},${hinge.y} L ${leafEnd.x},${leafEnd.y}`}\n strokeWidth={outlineWidth}\n />,\n <path\n key=\"swing\"\n className={styles.planCanvasDoorSwing}\n d={`M ${leafEnd.x},${leafEnd.y} A ${width},${width} 0 0,${\n hingeAtStart ? 0 : 1\n } ${arcEnd.x},${arcEnd.y}`}\n strokeWidth={outlineWidth}\n />,\n );\n }\n\n if (showOpeningSymbols && opening.kind === OpeningKind.Window) {\n // A pane line down the middle of the wall's thickness.\n parts.push(\n <path\n key=\"pane\"\n className={styles.planCanvasWindowPane}\n d={`M ${p1.x},${p1.y} L ${p2.x},${p2.y}`}\n strokeWidth={outlineWidth * 2}\n />,\n );\n }\n\n const hitPath = `M ${p1.x + normal.x * half},${p1.y + normal.y * half} L ${\n p2.x + normal.x * half\n },${p2.y + normal.y * half} L ${p2.x - normal.x * half},${p2.y - normal.y * half} L ${\n p1.x - normal.x * half\n },${p1.y - normal.y * half} Z`;\n\n return (\n <g\n key={opening.id}\n className={cx(\n selected && styles.planCanvasOpeningSelected,\n hoveredNow && styles.planCanvasOpeningHovered,\n classNames?.opening,\n )}\n style={slotStyles?.opening}\n data-opening-kind={opening.kind}\n >\n {parts}\n <path\n className={cx(\n styles.planCanvasOpeningHit,\n interactiveOpenings && styles.planCanvasOpeningHitInteractive,\n )}\n d={hitPath}\n onClick={\n interactiveOpenings && onOpeningActivate\n ? (e) => {\n e.stopPropagation();\n onOpeningActivate(opening.id);\n }\n : undefined\n }\n onPointerEnter={onHoverChange ? () => onHoverChange({ type: 'opening', id: opening.id }) : undefined}\n >\n {interactiveOpenings && <title>{opening.kind}</title>}\n </path>\n {(selected || hoveredNow) && (\n <path className={styles.planCanvasOpeningOutline} d={hitPath} vectorEffect=\"non-scaling-stroke\" />\n )}\n {/* The jamb faces, so an opening reads as a hole with sides rather than a blank. */}\n <path\n className={cx(\n styles.planCanvasJamb,\n // The jamb face belongs to the wall it is cut into, so it follows that wall\n // into the highlight rather than staying white against it.\n highlightedWalls.has(opening.wallId) && styles.planCanvasJambHighlighted,\n )}\n d={`M ${p1.x - normal.x * half},${p1.y - normal.y * half} L ${p1.x + normal.x * half},${\n p1.y + normal.y * half\n } M ${p2.x - normal.x * half},${p2.y - normal.y * half} L ${p2.x + normal.x * half},${\n p2.y + normal.y * half\n }`}\n strokeWidth={outlineWidth}\n />\n {centre && null}\n </g>\n );\n }\n\n return (\n <svg\n ref={ref}\n className={cx(styles.planCanvas, className, classNames?.canvas)}\n style={{ ...style, ...slotStyles?.canvas }}\n viewBox={`0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`}\n width={size.width}\n height={size.height}\n role=\"img\"\n aria-label={ariaLabel}\n onClick={onBackgroundActivate ? () => onBackgroundActivate() : undefined}\n onPointerLeave={onHoverChange ? () => onHoverChange(null) : undefined}\n >\n <g\n transform={`translate(${-view.x * view.zoom} ${-view.y * view.zoom}) scale(${view.zoom})`}\n >\n {/* 1. Grid */}\n {gridLines && (\n <g className={cx(styles.planCanvasGrid, classNames?.grid)} style={slotStyles?.grid} aria-hidden=\"true\">\n {gridLines.verticals.map((x) => (\n <line\n key={`v${x}`}\n x1={x}\n y1={view.y}\n x2={x}\n y2={view.y + gridLines.worldH}\n strokeWidth={pxToWorld}\n />\n ))}\n {gridLines.horizontals.map((y) => (\n <line\n key={`h${y}`}\n x1={view.x}\n y1={y}\n x2={view.x + gridLines.worldW}\n y2={y}\n strokeWidth={pxToWorld}\n />\n ))}\n </g>\n )}\n\n {/* 1b. The storey below, traced */}\n {underlayLines.length > 0 && (\n <g\n className={cx(styles.planCanvasUnderlay, classNames?.underlay)}\n style={slotStyles?.underlay}\n aria-hidden=\"true\"\n >\n {/* Screen units: the rule sets vector-effect: non-scaling-stroke, so the width\n is read in screen pixels rather than world millimetres. */}\n {underlayLines.map((line) => (\n <line\n key={line.id}\n x1={line.x1}\n y1={line.y1}\n x2={line.x2}\n y2={line.y2}\n strokeWidth={1.5}\n />\n ))}\n </g>\n )}\n\n {/* 2. Room fills — the only hit target for room selection */}\n <g className={cx(styles.planCanvasRooms, classNames?.rooms)} style={slotStyles?.rooms}>\n {rooms.map((room) => {\n const selected = room.roomId ? isSelected('room', room.roomId) : false;\n const hoveredNow = room.roomId ? isHovered('room', room.roomId) : false;\n const colour = room.meta ? getRoomColor?.(room.meta, room.area) : undefined;\n const label =\n getRoomLabel?.(room.meta, { area: room.area }) ??\n `${room.meta?.name ?? 'Unnamed room'}, ${formatArea(room.area)}`;\n\n return (\n <path\n key={room.id}\n className={cx(\n styles.planCanvasRoomFill,\n hoveredNow && styles.planCanvasRoomFillHovered,\n interactiveRooms && styles.planCanvasRoomFillInteractive,\n classNames?.roomFill,\n )}\n data-state={selected ? 'selected' : hoveredNow ? 'hovered' : undefined}\n style={{ ...slotStyles?.roomFill, ...(colour ? { fill: colour } : null) }}\n d={faceToPath(room)}\n fillRule=\"evenodd\"\n tabIndex={interactiveRooms ? 0 : undefined}\n role={interactiveRooms ? 'button' : undefined}\n aria-pressed={interactiveRooms ? selected : undefined}\n onClick={\n interactiveRooms && onRoomActivate && room.roomId\n ? (e) => {\n e.stopPropagation();\n onRoomActivate(room.roomId!);\n }\n : undefined\n }\n onKeyDown={\n interactiveRooms && onRoomActivate && room.roomId\n ? (e) => {\n if (e.key !== 'Enter' && e.key !== ' ') return;\n e.preventDefault();\n onRoomActivate(room.roomId!);\n }\n : undefined\n }\n onPointerEnter={\n onHoverChange && room.roomId\n ? () => onHoverChange({ type: 'room', id: room.roomId! })\n : undefined\n }\n >\n {/* Both a <title> and the aria-label: aria-label on SVG shapes is\n unreliable in older Safari/VoiceOver, and <title> doubles as the\n native tooltip. */}\n {interactiveRooms && <title>{label}</title>}\n </path>\n );\n })}\n </g>\n\n {/* 3. Wall bodies — outline pass, then fill pass. Never interleaved: the visible\n outline is what is left of the first pass after the second covers its middle. */}\n <g className={cx(styles.planCanvasWalls, classNames?.walls)} style={slotStyles?.walls} aria-hidden=\"true\">\n <g className={styles.planCanvasWallOutline}>\n {wallPaths.groups.map((group) => (\n <path\n key={`o${group.thickness}`}\n className={classNames?.wallBody}\n style={slotStyles?.wallBody}\n d={group.d}\n strokeWidth={group.thickness}\n />\n ))}\n </g>\n <g className={styles.planCanvasWallFill}>\n {wallPaths.groups.map((group) => {\n const fillWidth = group.thickness - 2 * outlineWidth;\n // Zoomed far out the fill would invert; walls collapsing to solid lines is\n // the right look at that scale.\n if (fillWidth <= 0) return null;\n return <path key={`f${group.thickness}`} d={group.d} strokeWidth={fillWidth} />;\n })}\n </g>\n <g className={styles.planCanvasRoomEdgeSelected}>\n {/* The same two passes the walls themselves use, on the same path data: an\n outline at full thickness, then a fill inside it. Recolouring only the body\n left the wall edged in its normal colour, so it did not read as one object. */}\n {roomEdge.groups.map((group) => (\n <path\n key={`o${group.thickness}`}\n className={styles.planCanvasRoomEdgeOutline}\n d={group.d}\n strokeWidth={group.thickness}\n />\n ))}\n {roomEdge.groups.map((group) => {\n const fillWidth = group.thickness - 2 * outlineWidth;\n if (fillWidth <= 0) return null;\n return (\n <path\n key={`f${group.thickness}`}\n className={styles.planCanvasRoomEdgeFill}\n d={group.d}\n strokeWidth={fillWidth}\n />\n );\n })}\n </g>\n\n <g className={styles.planCanvasWallCaps}>\n {/* A highlighted wall's caps are drawn below in the highlight colour. Skipped\n here rather than painted over: an antialiased hairline is never fully\n covered, and the two would blend into a pale seam. */}\n {wallPaths.caps\n .filter((cap) => !highlightedWalls.has(cap.wallId))\n .map((cap, i) => (\n <line\n key={`${cap.wallId}-${cap.reason}-${i}`}\n x1={cap.a.x}\n y1={cap.a.y}\n x2={cap.b.x}\n y2={cap.b.y}\n strokeWidth={outlineWidth}\n />\n ))}\n </g>\n <g className={styles.planCanvasRoomEdgeCaps}>\n {roomEdge.caps.map((cap, i) => (\n <line\n key={`${cap.wallId}-${cap.reason}-${i}`}\n x1={cap.a.x}\n y1={cap.a.y}\n x2={cap.b.x}\n y2={cap.b.y}\n strokeWidth={outlineWidth}\n />\n ))}\n </g>\n </g>\n\n {/* 4. Openings */}\n <g className={cx(styles.planCanvasOpenings, classNames?.openings)} style={slotStyles?.openings}>\n {Object.values(floor.openings).map(renderOpening)}\n </g>\n\n {/* 5. Selection / hover outlines for walls and nodes */}\n <g className={styles.planCanvasSelection} aria-hidden=\"true\">\n {selection\n .filter((entry) => entry.type === 'wall')\n .map((entry) => {\n const wall = floor.walls[entry.id];\n if (!wall) return null;\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return null;\n return (\n <line\n key={entry.id}\n x1={a.x}\n y1={a.y}\n x2={b.x}\n y2={b.y}\n strokeWidth={wall.thickness}\n className={styles.planCanvasWallSelected}\n />\n );\n })}\n {hovered?.type === 'wall' &&\n !isSelected('wall', hovered.id) &&\n (() => {\n const wall = floor.walls[hovered.id];\n if (!wall) return null;\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return null;\n return (\n <line\n x1={a.x}\n y1={a.y}\n x2={b.x}\n y2={b.y}\n strokeWidth={wall.thickness}\n className={styles.planCanvasWallHovered}\n />\n );\n })()}\n </g>\n\n {worldOverlay}\n </g>\n\n {/* Screen space: sized in pixels, so labels and handles stay constant under zoom */}\n <g className={cx(styles.planCanvasScreen, classNames?.overlay)} style={slotStyles?.overlay}>\n {showRoomLabels &&\n rooms.map((room) => {\n const at = toScreen(room.anchor);\n if (at.x < -200 || at.y < -200 || at.x > size.width + 200 || at.y > size.height + 200) {\n return null;\n }\n const custom = renderRoomLabel?.(room.meta, { area: room.area, faceId: room.id });\n if (custom === null) return null;\n\n return (\n <g\n key={room.id}\n className={cx(styles.planCanvasRoomLabel, classNames?.roomLabel)}\n style={slotStyles?.roomLabel}\n transform={`translate(${at.x} ${at.y})`}\n aria-hidden=\"true\"\n >\n {custom !== undefined ? (\n custom\n ) : (\n <>\n <text className={styles.planCanvasRoomName} textAnchor=\"middle\">\n {room.meta?.name ?? ''}\n </text>\n <text className={styles.planCanvasRoomArea} textAnchor=\"middle\" dy=\"1.2em\">\n {formatArea(room.area)}\n </text>\n </>\n )}\n </g>\n );\n })}\n {overlay}\n </g>\n </svg>\n );\n});\n\nexport default PlanCanvas;\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e={planCanvas:`planCanvas_7XTdG`,"planCanvas-screen":`planCanvas-screen_lk83X`,planCanvasScreen:`planCanvas-screen_lk83X`,"planCanvas-underlay":`planCanvas-underlay_rpzSl`,planCanvasUnderlay:`planCanvas-underlay_rpzSl`,"planCanvas-grid":`planCanvas-grid_49CmH`,planCanvasGrid:`planCanvas-grid_49CmH`,"planCanvas-rooms":`planCanvas-rooms_UyjEC`,planCanvasRooms:`planCanvas-rooms_UyjEC`,"planCanvas-roomFill":`planCanvas-roomFill_LXlOf`,planCanvasRoomFill:`planCanvas-roomFill_LXlOf`,"planCanvas-roomFill-interactive":`planCanvas-roomFill-interactive_JSU1E`,planCanvasRoomFillInteractive:`planCanvas-roomFill-interactive_JSU1E`,"planCanvas-roomFill-hovered":`planCanvas-roomFill-hovered_7ZXgf`,planCanvasRoomFillHovered:`planCanvas-roomFill-hovered_7ZXgf`,"planCanvas-roomLabel":`planCanvas-roomLabel_GrzE3`,planCanvasRoomLabel:`planCanvas-roomLabel_GrzE3`,"planCanvas-roomName":`planCanvas-roomName_kHAtD`,planCanvasRoomName:`planCanvas-roomName_kHAtD`,"planCanvas-roomArea":`planCanvas-roomArea_X1Tal`,planCanvasRoomArea:`planCanvas-roomArea_X1Tal`,"planCanvas-walls":`planCanvas-walls_G9vXB`,planCanvasWalls:`planCanvas-walls_G9vXB`,"planCanvas-wallOutline":`planCanvas-wallOutline_eWEN-`,planCanvasWallOutline:`planCanvas-wallOutline_eWEN-`,"planCanvas-wallFill":`planCanvas-wallFill_iKHyE`,planCanvasWallFill:`planCanvas-wallFill_iKHyE`,"planCanvas-wallCaps":`planCanvas-wallCaps_ZpLG4`,planCanvasWallCaps:`planCanvas-wallCaps_ZpLG4`,"planCanvas-roomEdgeCaps":`planCanvas-roomEdgeCaps_5Y7lq`,planCanvasRoomEdgeCaps:`planCanvas-roomEdgeCaps_5Y7lq`,"planCanvas-roomEdgeSelected":`planCanvas-roomEdgeSelected_wQk6B`,planCanvasRoomEdgeSelected:`planCanvas-roomEdgeSelected_wQk6B`,"planCanvas-roomEdgeOutline":`planCanvas-roomEdgeOutline_Y1ZwL`,planCanvasRoomEdgeOutline:`planCanvas-roomEdgeOutline_Y1ZwL`,"planCanvas-roomEdgeFill":`planCanvas-roomEdgeFill_Qr9p-`,planCanvasRoomEdgeFill:`planCanvas-roomEdgeFill_Qr9p-`,"planCanvas-wallSelected":`planCanvas-wallSelected_vFNw8`,planCanvasWallSelected:`planCanvas-wallSelected_vFNw8`,"planCanvas-wallHovered":`planCanvas-wallHovered_CFIAR`,planCanvasWallHovered:`planCanvas-wallHovered_CFIAR`,"planCanvas-openings":`planCanvas-openings_1fyX4`,planCanvasOpenings:`planCanvas-openings_1fyX4`,"planCanvas-opening-selected":`planCanvas-opening-selected_Wgl4r`,planCanvasOpeningSelected:`planCanvas-opening-selected_Wgl4r`,"planCanvas-doorLeaf":`planCanvas-doorLeaf_1BEkI`,planCanvasDoorLeaf:`planCanvas-doorLeaf_1BEkI`,"planCanvas-doorSwing":`planCanvas-doorSwing_QCc5i`,planCanvasDoorSwing:`planCanvas-doorSwing_QCc5i`,"planCanvas-windowPane":`planCanvas-windowPane_5q9oQ`,planCanvasWindowPane:`planCanvas-windowPane_5q9oQ`,"planCanvas-opening-hovered":`planCanvas-opening-hovered_NGK0O`,planCanvasOpeningHovered:`planCanvas-opening-hovered_NGK0O`,"planCanvas-opening-hit":`planCanvas-opening-hit_frRch`,planCanvasOpeningHit:`planCanvas-opening-hit_frRch`,"planCanvas-opening-hit-interactive":`planCanvas-opening-hit-interactive_D4ZUY`,planCanvasOpeningHitInteractive:`planCanvas-opening-hit-interactive_D4ZUY`,"planCanvas-opening-outline":`planCanvas-opening-outline_HS2wY`,planCanvasOpeningOutline:`planCanvas-opening-outline_HS2wY`,"planCanvas-jamb":`planCanvas-jamb_e-I9r`,planCanvasJamb:`planCanvas-jamb_e-I9r`,"planCanvas-jamb-highlighted":`planCanvas-jamb-highlighted_rWYxZ`,planCanvasJambHighlighted:`planCanvas-jamb-highlighted_rWYxZ`,"planCanvas-selection":`planCanvas-selection_0a31R`,planCanvasSelection:`planCanvas-selection_0a31R`};export{e as default};
2
+ //# sourceMappingURL=planCanvas.module.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planCanvas.module.mjs","names":[],"sources":["../../../../package/common/planCanvas/planCanvas.module.pcss"],"sourcesContent":["/**\n * Every colour comes from a theme variable. The wall fill/outline pair in particular\n * must stay two distinct surfaces, or the two-pass stroke has nothing to reveal.\n */\n\n.planCanvas {\n display: block;\n touch-action: none;\n user-select: none;\n background: var(--plan-canvas-background, var(--background));\n\n &-screen {\n pointer-events: none;\n }\n\n /* The storey below, for lining walls up between floors. Faint and dashed so it reads\n as a tracing under the drawing rather than as part of it, and never picks up a\n pointer: it is not this floor's geometry. */\n &-underlay {\n pointer-events: none;\n\n line {\n stroke: var(--plan-underlay-color, var(--text-dark));\n opacity: 0.28;\n stroke-dasharray: 6 5;\n stroke-linecap: round;\n vector-effect: non-scaling-stroke;\n }\n }\n\n &-grid {\n pointer-events: none;\n\n line {\n stroke: var(--plan-grid-color, var(--text-color));\n opacity: 0.1;\n }\n }\n\n /* ── Rooms ── */\n\n &-rooms {\n pointer-events: auto;\n }\n\n &-roomFill {\n fill: var(--plan-room-fill, var(--background-accent));\n stroke: none;\n transition: fill 0.12s ease-out, opacity 0.12s ease-out;\n\n &-interactive {\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &:focus-visible {\n outline: none;\n stroke: var(--focus-border-color, var(--primary-color));\n stroke-width: 2;\n vector-effect: non-scaling-stroke;\n }\n }\n\n /* Hover tints the floor, selection colours the walls: two channels, so both can be\n read at once. A fill answers \"which room\" instantly, where a shared wall is\n ambiguous between the rooms either side of it. */\n &-hovered {\n fill: var(--plan-room-fill-hover, var(--background-accent-light));\n }\n }\n\n &-roomLabel {\n pointer-events: none;\n }\n\n &-roomName {\n fill: var(--text-color);\n font-size: 13px;\n font-weight: 600;\n /* A halo, so a label stays legible over any room colour a consumer sets. */\n paint-order: stroke;\n stroke: var(--background);\n stroke-width: 3px;\n stroke-linejoin: round;\n }\n\n &-roomArea {\n fill: var(--text-dark);\n font-size: 11px;\n paint-order: stroke;\n stroke: var(--background);\n stroke-width: 3px;\n stroke-linejoin: round;\n }\n\n /* ── Walls ── */\n\n &-walls {\n pointer-events: none;\n }\n\n /*\n * A mitred corner runs to 1 / sin(angle / 2) times the stroke width, so an acute one\n * grows without bound and reads as a spike. 4 keeps a true point down to about 29\n * degrees and caps it at twice the wall thickness, which covers every angle a building\n * plausibly has; SVG bevels anything sharper.\n */\n &-wallOutline path {\n fill: none;\n stroke: var(--plan-wall-outline, var(--text-color));\n stroke-linecap: butt;\n stroke-linejoin: miter;\n stroke-miterlimit: var(--plan-wall-miter-limit, 4);\n }\n\n /* Must match the outline pass exactly: the two are the same shape, and the visible\n outline is what is left of the first after this one covers its middle. */\n &-wallFill path {\n fill: none;\n stroke: var(--plan-wall-fill, var(--text-dark));\n stroke-linecap: butt;\n stroke-linejoin: miter;\n stroke-miterlimit: var(--plan-wall-miter-limit, 4);\n }\n\n &-wallCaps line {\n stroke: var(--plan-wall-outline, var(--text-color));\n stroke-linecap: butt;\n }\n\n /* Drawn from the same geometry as the walls, so it inherits their corners and their\n gaps at doors. */\n &-roomEdgeCaps line {\n stroke: var(--plan-room-edge-selected, var(--primary-color));\n stroke-linecap: butt;\n }\n\n &-roomEdgeSelected path {\n fill: none;\n stroke-linecap: butt;\n stroke-linejoin: miter;\n stroke-miterlimit: var(--plan-wall-miter-limit, 4);\n }\n\n /* Edge darker than body, so the wall keeps the crisp outline every other wall has\n rather than turning into a flat slab, and reads as one highlighted object. */\n &-roomEdgeSelected {\n .planCanvas-roomEdgeOutline {\n stroke: var(--plan-room-edge-selected, var(--primary-color));\n }\n\n .planCanvas-roomEdgeFill {\n stroke: var(--plan-room-edge-selected-fill, var(--primary-lighter));\n }\n }\n\n &-wallSelected {\n stroke: var(--primary-color);\n opacity: 0.45;\n stroke-linecap: butt;\n }\n\n &-wallHovered {\n stroke: var(--primary-lighter);\n opacity: 0.35;\n stroke-linecap: butt;\n }\n\n /* ── Openings ── */\n\n &-openings {\n pointer-events: auto;\n }\n\n &-opening {\n /*\n * State lives on the group so the whole symbol responds, not just the box drawn round\n * it by `-outline`: a door reads as selected when its leaf and swing do, the way the\n * room fill does, rather than by acquiring a rectangle.\n */\n &-selected {\n .planCanvas-doorLeaf,\n .planCanvas-doorSwing,\n .planCanvas-windowPane {\n stroke: var(--primary-color);\n }\n\n .planCanvas-doorSwing {\n opacity: 0.9;\n }\n }\n\n &-hovered {\n .planCanvas-doorLeaf,\n .planCanvas-doorSwing,\n .planCanvas-windowPane {\n stroke: var(--primary-lighter, var(--primary-color));\n }\n }\n\n &-hit {\n fill: transparent;\n stroke: none;\n pointer-events: none;\n\n &-interactive {\n pointer-events: auto;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n }\n\n &-outline {\n fill: none;\n stroke: var(--primary-color);\n stroke-width: 2;\n }\n }\n\n &-jamb {\n stroke: var(--plan-wall-outline, var(--text-color));\n stroke-linecap: butt;\n\n &-highlighted {\n stroke: var(--plan-room-edge-selected, var(--primary-color));\n }\n }\n\n &-doorLeaf {\n fill: none;\n stroke: var(--plan-door-color, var(--text-dark));\n stroke-linecap: butt;\n }\n\n &-doorSwing {\n fill: none;\n stroke: var(--plan-door-color, var(--text-dark));\n stroke-dasharray: 6 4;\n opacity: 0.6;\n }\n\n &-windowPane {\n stroke: var(--plan-window-color, var(--info-color));\n stroke-linecap: butt;\n }\n\n &-selection {\n pointer-events: none;\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- import{buildWallSegments as e,centroid as t,clientToSVG as n,dist as r,doorEndpoints as i,downloadSVG as a,nearestWall as o,parseSVGRooms as s,pointInPolygon as c,pointToSegment as ee,rectToPoints as te,snapPt as l}from"./roomDrawer.utils.mjs";import u from"./roomDrawer.module.mjs";import{useEffect as ne,useLayoutEffect as re,useRef as d,useState as f}from"react";import p from"classnames";import{FontAwesomeIcon as m}from"@fortawesome/react-fontawesome";import{faCheck as ie,faDoorOpen as ae,faDownload as oe,faDrawPolygon as se,faHandPointer as ce,faMagnifyingGlassMinus as le,faMagnifyingGlassPlus as ue,faSquare as de,faTrash as fe,faUpload as pe}from"@fortawesome/free-solid-svg-icons";import{Fragment as me,jsx as h,jsxs as g}from"react/jsx-runtime";var he=6,ge=5,_=1.25,v=.25,y=12,b=0;function x(e){return b+=1,`${e}${b}`}var _e={select:`Drag inside to move · Drag corner to reshape · Click wall to add corner · Dbl-click corner to remove · Delete to erase`,rect:`Drag to draw a rectangular room`,polygon:`Click to place corners · Click first point or Enter to close · Esc to cancel`};function S({className:b}){let[S,C]=f([]),[w,T]=f([]),[E,ve]=f(`rect`),[D,O]=f(null),[k,A]=f(null),[j,M]=f(null),[N,P]=f(null),[F,I]=f([]),[ye,L]=f(null),[R,z]=f(1),[B,V]=f({x:0,y:0}),H=d(null),U=d(null),W=d(null),G=d(null),be=d(null),K=d({rooms:S,doors:w,tool:E,selectedId:D,editingDoorsForRoomId:k,polyPoints:F,zoom:R,pan:B});re(()=>{K.current={rooms:S,doors:w,tool:E,selectedId:D,editingDoorsForRoomId:k,polyPoints:F,zoom:R,pan:B}});function xe(e){e.length<3||(C(t=>[...t,{id:x(`room`),name:`Room ${t.length+1}`,points:e}]),I([]),L(null))}ne(()=>{let e=G.current;if(!e)return;function t(e){e.preventDefault();let{zoom:t,pan:r}=K.current,i=n(e.clientX,e.clientY,G.current);if(!i)return;let a=e.deltaY<0?_:1/_,o=Math.max(v,Math.min(y,t*a));V({x:i.x-(i.x-r.x)*(t/o),y:i.y-(i.y-r.y)*(t/o)}),z(o)}return e.addEventListener(`wheel`,t,{passive:!1}),()=>e.removeEventListener(`wheel`,t)},[]),ne(()=>{function e(e){let{tool:t,selectedId:n,editingDoorsForRoomId:r,polyPoints:i,zoom:a,pan:o}=K.current;if(e.key===`Escape`){if(r){A(null);return}t===`polygon`&&(I([]),L(null)),O(null);return}if(e.key===`Enter`&&t===`polygon`&&i.length>=3){xe(i);return}if((e.key===`+`||e.key===`=`)&&!(e.target instanceof HTMLInputElement)){let e=Math.max(v,Math.min(y,a*_)),t=o.x+400/a/2,n=o.y+300/a/2;z(e),V({x:t-400/e/2,y:n-300/e/2});return}if(e.key===`-`&&!(e.target instanceof HTMLInputElement)){let e=Math.max(v,Math.min(y,a/_)),t=o.x+400/a/2,n=o.y+300/a/2;z(e),V({x:t-400/e/2,y:n-300/e/2});return}if(e.key===`0`&&!(e.target instanceof HTMLInputElement)){z(1),V({x:0,y:0});return}(e.key===`Delete`||e.key===`Backspace`)&&n&&!r&&!(e.target instanceof HTMLInputElement)&&(C(e=>e.filter(e=>e.id!==n)),T(e=>e.filter(e=>e.roomId!==n)),O(null))}return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);function q(e,t=R,n=B){let r=Math.max(v,Math.min(y,t*e)),i=n.x+400/t/2,a=n.y+300/t/2;V({x:i-400/r/2,y:a-300/r/2}),z(r)}function J(e){return G.current?n(e.clientX,e.clientY,G.current):null}function Se(e,t){let n=null;for(let i=0;i<t.points.length;i++){let a=r(e,t.points[i]);a<=he&&(!n||a<n.d)&&(n={index:i,d:a})}return n}function Ce(e,t){let n=null;for(let r=0;r<t.points.length;r++){let i=t.points[r],a=t.points[(r+1)%t.points.length],{dist:o,t:s}=ee(e,i,a);o<=ge&&(!n||o<n.d)&&(n={index:r,t:s,d:o})}return n}function we(e,t,n,r){return C(n=>n.map(n=>{if(n.id!==e)return n;let i=[...n.points];return i.splice(t+1,0,r),{...n,points:i}})),T(r=>r.map(r=>r.roomId===e?r.wallIndex===t?r.t<=n?{...r,t:n>0?r.t/n:0}:{...r,wallIndex:t+1,t:(r.t-n)/(1-n)}:r.wallIndex>t?{...r,wallIndex:r.wallIndex+1}:r:r)),t+1}function Te(e,t){let n=S.find(t=>t.id===e);if(!n||n.points.length<=3)return;let r=n.points.length;C(n=>n.map(n=>n.id===e?{...n,points:n.points.filter((e,n)=>n!==t)}:n)),T(n=>n.map(n=>{if(n.roomId!==e)return n;let i=(t-1+r)%r;return n.wallIndex===i||n.wallIndex===t?{...n,wallIndex:-1}:n.wallIndex>t?{...n,wallIndex:n.wallIndex-1}:n}).filter(e=>e.wallIndex!==-1))}function Ee(e){if(e.button===1){e.preventDefault();let t=G.current;if(t){let n=t.getBoundingClientRect();W.current={startClient:{x:e.clientX,y:e.clientY},origPan:{...B},scaleX:400/R/n.width,scaleY:300/R/n.height}}return}if(e.button!==0)return;e.preventDefault();let t=J(e);if(t){if(k){let e=S.find(e=>e.id===k);if(e){let n=o(t,[e]);n&&T(e=>[...e,{id:x(`door`),roomId:n.roomId,wallIndex:n.wallIndex,t:n.t,width:12}])}return}if(E===`rect`){M(t),P(t);return}if(E===`polygon`){F.length>=3&&r(t,F[0])<=8?xe(F):I(e=>[...e,t]);return}if(E===`select`){let e=S.find(e=>e.id===D)??null;if(e){let n=Se(t,e);if(n){H.current={type:`vertex`,roomId:e.id,index:n.index,start:t,origPt:e.points[n.index]};return}}if(e){let n=Ce(t,e);if(n){let r=e.points[n.index],i=e.points[(n.index+1)%e.points.length],a=l({x:r.x+(i.x-r.x)*n.t,y:r.y+(i.y-r.y)*n.t}),o=we(e.id,n.index,n.t,a);H.current={type:`vertex`,roomId:e.id,index:o,start:t,origPt:a};return}}let n=S.find(e=>c(t,e.points));if(n){O(n.id),H.current={type:`room`,roomId:n.id,start:t,origPoints:[...n.points]};return}O(null)}}}function De(e){if(E!==`select`||k)return;let t=J(e);if(!t)return;let n=S.find(e=>e.id===D);if(!n)return;let r=Se(t,n);r&&(e.preventDefault(),Te(n.id,r.index))}function Oe(e){if(W.current){let{startClient:t,origPan:n,scaleX:r,scaleY:i}=W.current;V({x:n.x-(e.clientX-t.x)*r,y:n.y-(e.clientY-t.y)*i});return}let t=J(e);if(t){if(U.current&&k){let e=S.find(e=>e.id===k);if(e){let n=o(t,[e]);n&&T(e=>e.map(e=>e.id===U.current?{...e,wallIndex:n.wallIndex,t:n.t}:e))}return}if(E===`rect`&&j){P(t);return}if(E===`polygon`){L(t);return}if(E===`select`&&!k&&H.current){let e=H.current;if(e.type===`vertex`){let n=t.x-e.start.x,r=t.y-e.start.y,i=l({x:e.origPt.x+n,y:e.origPt.y+r});C(t=>t.map(t=>{if(t.id!==e.roomId)return t;let n=[...t.points];return n[e.index]=i,{...t,points:n}}))}else{let n=t.x-e.start.x,r=t.y-e.start.y;C(t=>t.map(t=>t.id===e.roomId?{...t,points:e.origPoints.map(e=>({x:e.x+n,y:e.y+r}))}:t))}}}}function ke(e){if(W.current){W.current=null;return}let t=J(e);if(t){if(E===`rect`&&j){let e=Math.min(j.x,t.x),n=Math.min(j.y,t.y),r=Math.abs(t.x-j.x),i=Math.abs(t.y-j.y);r>=10&&i>=10&&C(t=>[...t,{id:x(`room`),name:`Room ${t.length+1}`,points:te(e,n,r,i)}]),M(null),P(null)}U.current=null,H.current=null}}function Ae(e){D&&C(t=>t.map(t=>t.id===D?{...t,name:e}:t))}function je(e,t){let n=parseFloat(t);isNaN(n)||n<=0||T(t=>t.map(t=>t.id===e?{...t,width:n}:t))}function Me(){D&&(C(e=>e.filter(e=>e.id!==D)),T(e=>e.filter(e=>e.roomId!==D)),A(null),O(null))}function Ne(e){T(t=>t.filter(t=>t.id!==e))}function Pe(e){ve(e),A(null),I([]),L(null),M(null),P(null)}function Fe(e){let t=e.target.files?.[0];if(!t)return;e.target.value=``;let n=new FileReader;n.onload=e=>{let t=e.target?.result;if(typeof t!=`string`)return;let n=s(t);n.length!==0&&(C(n.map(e=>({...e,id:x(`room`)}))),T([]),O(null),A(null),z(1),V({x:0,y:0}))},n.readAsText(t)}let Y=E===`rect`&&j&&N?{x:Math.min(j.x,N.x),y:Math.min(j.y,N.y),w:Math.abs(N.x-j.x),h:Math.abs(N.y-j.y)}:null,X=D?S.find(e=>e.id===D)??null:null,Z=k?S.find(e=>e.id===k)??null:null,Q=k?w.filter(e=>e.roomId===k):[],Ie=[{t:`select`,icon:ce,label:`Select & edit`},{t:`rect`,icon:de,label:`Rectangle room`},{t:`polygon`,icon:se,label:`Polygon room`}],Le=400/R,$=300/R,Re=k?u.roomDrawerCanvasDoor:E===`select`?u.roomDrawerCanvasSelect:void 0;return g(`div`,{className:p(u.roomDrawer,b),children:[g(`div`,{className:u.roomDrawerToolbar,children:[Ie.map(({t:e,icon:t,label:n})=>h(`button`,{title:n,className:p(u.roomDrawerToolbarBtn,e===E&&!k&&u.roomDrawerToolbarBtnActive),onClick:()=>Pe(e),children:h(m,{icon:t})},e)),h(`div`,{className:u.roomDrawerToolbarSep}),h(`button`,{title:`Zoom in (+)`,className:u.roomDrawerToolbarBtn,onClick:()=>q(_),disabled:R>=y,children:h(m,{icon:ue})}),g(`button`,{title:`Reset zoom (0) — ${Math.round(R*100)}%`,className:u.roomDrawerToolbarZoomLabel,onClick:()=>{z(1),V({x:0,y:0})},children:[Math.round(R*100),`%`]}),h(`button`,{title:`Zoom out (−)`,className:u.roomDrawerToolbarBtn,onClick:()=>q(1/_),disabled:R<=v,children:h(m,{icon:le})}),h(`div`,{className:u.roomDrawerToolbarSep}),h(`button`,{title:`Upload SVG`,className:u.roomDrawerToolbarBtn,onClick:()=>be.current?.click(),children:h(m,{icon:pe})}),h(`button`,{title:`Download SVG`,className:u.roomDrawerToolbarBtn,onClick:()=>a(S,w),disabled:S.length===0,children:h(m,{icon:oe})}),h(`input`,{ref:be,type:`file`,accept:`.svg,image/svg+xml`,style:{display:`none`},onChange:Fe})]}),g(`svg`,{ref:G,className:p(u.roomDrawerCanvas,Re),viewBox:`${B.x} ${B.y} ${Le} ${$}`,onMouseDown:Ee,onMouseMove:Oe,onMouseUp:ke,onDoubleClick:De,children:[h(`defs`,{children:h(`pattern`,{id:`rd-grid`,width:5,height:5,patternUnits:`userSpaceOnUse`,children:h(`path`,{d:`M 5 0 L 0 0 0 5`,fill:`none`,className:u.gridLine})})}),h(`rect`,{x:B.x,y:B.y,width:Le,height:$,fill:`url(#rd-grid)`}),S.map(n=>{let r=n.points,i=r.map((e,t)=>`${t===0?`M`:`L`} ${e.x},${e.y}`).join(` `)+` Z`,a=e(r,w.filter(e=>e.roomId===n.id)),o=t(r),s=D===n.id,c=k===n.id;return g(`g`,{children:[h(`path`,{d:i,className:p(u.roomFill,(s||c)&&u.roomFillSelected)}),a.map((e,t)=>h(`path`,{d:e,className:p(u.roomStroke,(s||c)&&u.roomStrokeSelected)},t)),n.name&&h(`text`,{x:o.x,y:o.y,className:u.roomText,textAnchor:`middle`,dominantBaseline:`middle`,children:n.name})]},n.id)}),w.map(e=>{let t=S.find(t=>t.id===e.roomId);if(!t)return null;let n=t.points[e.wallIndex],r=t.points[(e.wallIndex+1)%t.points.length],a=i(n,r,e.t,e.width);return a?h(`line`,{x1:a.p1.x,y1:a.p1.y,x2:a.p2.x,y2:a.p2.y,className:u.doorGap},e.id):null}),k&&Q.map(e=>{let t=S.find(t=>t.id===e.roomId);if(!t)return null;let n=t.points[e.wallIndex],r=t.points[(e.wallIndex+1)%t.points.length],a=i(n,r,e.t,e.width);if(!a)return null;let o={x:(a.p1.x+a.p2.x)/2,y:(a.p1.y+a.p2.y)/2};return h(`circle`,{cx:o.x,cy:o.y,r:2.5,className:u.doorMarker,onMouseDown:t=>{t.button===0&&(t.preventDefault(),t.stopPropagation(),U.current=e.id)}},e.id)}),E===`select`&&!k&&X&&X.points.map((e,t)=>h(`circle`,{cx:e.x,cy:e.y,r:3.5,className:u.vertexHandle},t)),Y&&h(`rect`,{x:Y.x,y:Y.y,width:Y.w,height:Y.h,className:u.preview}),E===`polygon`&&F.length>0&&g(me,{children:[h(`polyline`,{points:[...F,ye??F[F.length-1]].map(e=>`${e.x},${e.y}`).join(` `),className:u.preview}),F.length>=3&&h(`circle`,{cx:F[0].x,cy:F[0].y,r:8/2,className:u.polyCloseTarget}),F.map((e,t)=>h(`circle`,{cx:e.x,cy:e.y,r:2,className:u.polyVertex},t))]})]}),X&&!k&&g(`div`,{className:u.roomDrawerInfo,children:[h(`span`,{className:u.roomDrawerInfoLabel,children:`Name`}),h(`input`,{className:u.roomDrawerInfoInput,value:X.name,onChange:e=>Ae(e.target.value),placeholder:`Room name`}),g(`button`,{className:u.roomDrawerInfoDoorsBtn,onClick:()=>A(X.id),title:`Edit doors`,children:[h(m,{icon:ae}),h(`span`,{children:w.filter(e=>e.roomId===X.id).length})]}),h(`button`,{className:u.roomDrawerInfoDelete,onClick:Me,title:`Delete room`,children:h(m,{icon:fe})})]}),k&&Z&&g(`div`,{className:u.roomDrawerDoorPanel,children:[g(`div`,{className:u.roomDrawerDoorPanelHeader,children:[h(m,{icon:ae,className:u.roomDrawerDoorPanelIcon}),h(`span`,{className:u.roomDrawerDoorPanelTitle,children:Z.name?`Doors · ${Z.name}`:`Doors`}),g(`button`,{className:u.roomDrawerDoorPanelDone,onClick:()=>A(null),title:`Done (Esc)`,children:[h(m,{icon:ie}),`Done`]})]}),g(`div`,{className:u.roomDrawerDoorPanelList,children:[Q.length===0&&h(`span`,{className:u.roomDrawerDoorPanelEmpty,children:`Click on a wall to add a door`}),Q.map((e,t)=>g(`div`,{className:u.roomDrawerDoorPanelItem,children:[g(`span`,{className:u.roomDrawerDoorPanelLabel,children:[`Door `,t+1]}),h(`span`,{className:u.roomDrawerDoorPanelWidthLabel,children:`Width`}),h(`input`,{className:u.roomDrawerInfoInput,type:`number`,min:1,value:e.width,onChange:t=>je(e.id,t.target.value)}),h(`button`,{className:u.roomDrawerInfoDelete,onClick:()=>Ne(e.id),title:`Delete door`,children:h(m,{icon:fe})})]},e.id))]})]}),h(`div`,{className:u.roomDrawerHint,children:k?`Click a wall to add a door · Adjust width or delete from the list · Done or Esc to finish`:`${_e[E]} · Scroll to zoom · Middle-drag to pan · 0 to reset`})]})}export{S as default};
1
+ import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{renderIcon as t}from"../utils/renderIcon.mjs";import n from"../actionIcon/actionIcon.mjs";import r from"../hooks/useCoarsePointer.mjs";import i from"../input/input.mjs";import{OpeningKind as a,WallType as o}from"../floorPlan/floorPlan.types.mjs";import{pointToSegment as s}from"../floorPlan/floorPlan.geometry.mjs";import{DEFAULT_WALL_ATTRS as c,addOpening as ee,addRect as te,addWall as ne,dissolveNode as re,mergeNodes as ie,moveNode as ae,removeOpening as oe,removeWall as se,splitWall as ce,updateOpening as le}from"../floorPlan/floorPlan.graph.mjs";import{addFloor as ue,duplicateFloor as de,emptyFloorPlan as fe,floorBelow as pe,floorBounds as me,floorIndex as he,getFloor as ge,moveFloor as _e,removeFloor as ve,renameFloor as ye,setFloor as be,setFloorHeight as xe}from"../floorPlan/floorPlan.plan.mjs";import{commitDerivation as Se,derivePlan as Ce}from"../floorPlan/floorPlan.rooms.mjs";import{HIT_PX as we,HIT_PX_COARSE as Te,hitTest as Ee,selectionsEqual as De,worldTolerances as l}from"../floorPlan/floorPlan.hit.mjs";import{SnapKind as u,clientToWorld as Oe,resolveSnap as ke}from"../floorPlan/floorPlan.snap.mjs";import{planFromJson as Ae,planToJson as je}from"../floorPlan/floorPlan.json.mjs";import Me from"../numberInput/index.mjs";import Ne from"../planCanvas/planCanvas.mjs";import{usePlanHistory as Pe}from"./usePlanHistory.mjs";import d from"./roomDrawer.module.mjs";import{forwardRef as Fe,useCallback as f,useEffect as p,useImperativeHandle as Ie,useMemo as m,useRef as h,useState as g}from"react";import _ from"classnames";import{faAngleDown as Le,faAngleUp as Re,faArrowPointer as ze,faArrowRotateLeft as Be,faArrowRotateRight as Ve,faBorderAll as He,faClone as Ue,faDoorOpen as We,faDownload as Ge,faDraftingCompass as Ke,faExpand as qe,faLayerGroup as Je,faMagnet as Ye,faMagnifyingGlassMinus as Xe,faMagnifyingGlassPlus as Ze,faMinus as Qe,faPlus as $e,faRulerCombined as et,faSquare as tt,faTrash as nt,faUpload as rt,faWindowMaximize as it}from"@fortawesome/free-solid-svg-icons";import{Fragment as at,jsx as v,jsxs as y}from"react/jsx-runtime";var ot=[`select`,`wall`,`rect`,`door`,`window`,`archway`,`measure`],st={v:`select`,w:`wall`,r:`rect`,d:`door`,n:`window`,a:`archway`,m:`measure`},ct={door:a.Door,window:a.Window,archway:a.Archway},lt={[a.Door]:`Door`,[a.Window]:`Window`,[a.Archway]:`Archway`,[a.Gap]:`Gap`},ut={[a.Door]:{width:900,sill:0,head:2100},[a.Window]:{width:1200,sill:900,head:2100},[a.Archway]:{width:1200,sill:0,head:2100},[a.Gap]:{width:1e3,sill:0,head:2500}},dt=1,ft=4,pt=[];function mt(e){return e>=1e3?`${(e/1e3).toFixed(2)} m`:`${Math.round(e)} mm`}function ht(e){let t=e/1e6;return`${t.toFixed(+(t<10))} m²`}function gt(e){let t=e*180/Math.PI;return t=(t%360+360)%360,t>180&&(t-=360),`${Math.abs(t)<.05?0:Math.round(t*10)/10}°`}var b=Fe(function(a,Fe){let{value:b,defaultValue:_t,onChange:x,onChangeEnd:vt,floorId:yt,defaultFloorId:bt,onFloorChange:xt,showFloorBar:St=!0,showUnderlay:Ct=!0,tool:wt,defaultTool:Tt=`select`,onToolChange:Et,tools:Dt=ot,selection:Ot,defaultSelection:kt,onSelectionChange:At,multiSelect:jt=!0,onHoverChange:Mt,view:Nt,defaultView:Pt,onViewChange:Ft,minZoom:It=.005,maxZoom:Lt=2,fitOnMount:Rt=!0,gridSize:S=100,showGrid:zt=!0,snap:Bt=!0,angleLock:Vt=!1,constrainAngle:Ht=45,showSnapGuides:Ut=!0,wallThickness:Wt=c.thickness,wallHeight:Gt,openingDefaults:Kt,showMeasurements:qt=!0,formatLength:Jt=mt,formatArea:Yt=ht,showToolbar:Xt=!0,showInspector:Zt=!0,showHint:Qt=!0,showHistoryControls:$t=!0,showFileControls:en=!0,fileName:tn,onImport:nn,historyMode:rn=`internal`,historyLimit:an=50,onHistoryChange:on,readOnly:sn=!1,disabled:C=!1,canvasHeight:cn=`420px`,getRoomColor:ln,renderRoomLabel:un,labels:w,icons:T,"aria-label":dn,className:fn,style:pn,classNames:E,styles:D,...mn}=e(`RoomDrawer`,a),hn=r(),O=hn?Te:we,gn=h(null),_n=h(null),vn=h(null),yn=h(null),k=h({kind:`none`}),bn=h(new Map),xn=h(null),Sn=h(!1),Cn=h(!1),wn=h(!1),A=Pe(m(()=>_t??b??fe(),[]),an),Tn=b!==void 0,j=Tn?b:A.present;p(()=>{if(!(!Tn||!b)){if(b===yn.current){A.replace(b);return}A.commit(b,`external`)}},[b,Tn]);let[En,Dn]=g(bt),On=yt??En??j.floors[0]?.id,M=m(()=>ge(j,On)??ge(j)??fe().floors[0],[j,On]),[kn,An]=g(Ct);p(()=>An(Ct),[Ct]);let jn=m(()=>kn?pe(j,M.id):null,[kn,j,M.id]),N=f(e=>{e!==M.id&&(yt===void 0&&Dn(e),xt?.(e))},[M.id,yt,xt]),P=m(()=>Ce(M),[M]).rooms,[Mn,Nn]=g(Tt),F=wt??Mn,Pn=f(e=>{wt===void 0&&Nn(e),Et?.(e)},[wt,Et]),[Fn,In]=g(kt??pt),I=Ot===void 0?Fn:Ot,L=h(I);L.current=I;let R=f(e=>{let t=e===null?pt:Array.isArray(e)?e:[e];L.current=t,Ot===void 0&&In(t),At?.([...t])},[Ot,At]),Ln=f(e=>{let t=L.current,n=t.filter(t=>!(t.type===e.type&&t.id===e.id));R(n.length===t.length?[...t,e]:n)},[R]),z=I.length===1?I[0]:null,Rn=f(e=>{let t=new Set;for(let n of e)if(n.type===`node`)t.add(n.id);else if(n.type===`wall`){let e=M.walls[n.id];e&&(t.add(e.a),t.add(e.b))}else if(n.type===`room`){let e=P.find(e=>e.roomId===n.id);for(let n of e?.nodeIds??[])t.add(n)}return t},[M.walls,P]),zn=m(()=>Rn(I),[Rn,I]),[Bn,Vn]=g(null),[Hn,Un]=g(Pt??{x:0,y:0,zoom:.05}),B=Nt??Hn,V=f(e=>{Nt===void 0&&Un(e),Ft?.(e)},[Nt,Ft]),[H,Wn]=g({width:800,height:420}),[U,Gn]=g([]),[W,Kn]=g(null),[qn,Jn]=g(Bt),[Yn,Xn]=g(Vt);p(()=>Jn(Bt),[Bt]),p(()=>Xn(Vt),[Vt]);let[Zn,Qn]=g(zt);p(()=>Qn(zt),[zt]);let $n=h(null),[G,er]=g(null),[K,tr]=g([]),nr=h(0),[rr,ir]=g(null),ar=h(null),or=f(e=>{let t=l(O,B.zoom);for(let n=K.length-1;n>=0;n--){let r=K[n];if(Math.hypot(e.x-r.a.x,e.y-r.a.y)<=t.node)return{id:r.id,end:`a`};if(Math.hypot(e.x-r.b.x,e.y-r.b.y)<=t.node)return{id:r.id,end:`b`}}for(let n=K.length-1;n>=0;n--){let r=K[n];if(s(e,r.a,r.b).dist<=t.wall)return{id:r.id}}return null},[K,O,B.zoom]),[sr,cr]=g(null),q=!sn&&!C,lr=m(()=>{if(U.length<2)return;let e=U[U.length-2],t=U[U.length-1];return Math.atan2(t.y-e.y,t.x-e.x)},[U]);p(()=>{let e=_n.current;if(!e)return;let t=new ResizeObserver(()=>{let t=e.getBoundingClientRect();t.width>0&&t.height>0&&Wn({width:Math.round(t.width),height:Math.round(t.height)})});t.observe(e);let n=e.getBoundingClientRect();return n.width>0&&n.height>0&&Wn({width:Math.round(n.width),height:Math.round(n.height)}),()=>t.disconnect()},[]);let ur=f((e,t)=>Oe(e,t,_n.current?.getBoundingClientRect()??{left:0,top:0},B),[B]),J=f(e=>({x:(e.x-B.x)*B.zoom,y:(e.y-B.y)*B.zoom}),[B]),Y=f((e,t,n,r,i)=>{let a=qn!==Cn.current,o=Yn!==wn.current,s=ke(e,{floor:M,pxToWorld:B.zoom>0?1/B.zoom:1,origin:t,referenceAngle:r,excludeNodeIds:n,excludeWallIds:i,options:{enabled:a?[u.Node,u.Intersection,u.Wall,u.Angle,u.Extension,u.Alignment,u.Grid]:[],gridSize:S,angleStepDeg:o?Ht:0,toleranceScreenPx:hn?16:10}});return $n.current=s,s},[M,B.zoom,qn,Yn,S,Ht,hn]),dr=f(e=>{let t=Object.values(M.walls).filter(t=>t.a===e||t.b===e),n=new Set(t.map(e=>e.id)),r=M.nodes[e];if(!r)return{excludeWallIds:n};let i={x:r.x,y:r.y},a,o=t[0];if(o){let t=o.a===e?o.b:o.a,n=M.nodes[t];n&&(a=Math.atan2(n.y-r.y,n.x-r.x))}return{origin:i,referenceAngle:a,excludeWallIds:n}},[M]),fr=f((e={})=>{let t=e.padding??24,n=me(M);if(!n||n.width<=0||n.height<=0){V({x:-1e3,y:-1e3,zoom:.05});return}let r=Math.min((H.width-t*2)/n.width,(H.height-t*2)/n.height),i=Math.max(It,Math.min(Lt,r));V({x:n.minX-(H.width/i-n.width)/2,y:n.minY-(H.height/i-n.height)/2,zoom:i})},[M,H.width,H.height,It,Lt,V]),pr=h(!1);p(()=>{!Rt||pr.current||Object.keys(M.nodes).length!==0&&(H.width<=1||(pr.current=!0,fr()))},[Rt,M,H.width,fr]);let mr=f((e,t,n,r=[],i={})=>{if(e===M)return j;let a=be(j,e);yn.current=a,n?i.push===!1?A.replace(a):A.commit(a,t,r):A.updateGesture(a);let o={reason:t,committed:n,targetIds:r};return x?.(a,o),n&&vt?.(a,o),a},[M,j,A,x,vt]),X=f((e,t,n=[],r={})=>{let i=Se(e).floor;return mr(i,t,!0,n,r)},[mr]);p(()=>{on?.({canUndo:A.canUndo,canRedo:A.canRedo})},[A.canUndo,A.canRedo,on]);let hr=m(()=>z?.type===`room`?P.find(e=>e.roomId===z.id)??null:null,[z,P]),Z=z?.type===`wall`?M.walls[z.id]:void 0,gr=z?.type===`opening`?M.openings[z.id]:void 0,_r=m(()=>{if(!Z)return null;let e=M.nodes[Z.a],t=M.nodes[Z.b];return!e||!t?null:{x:(e.x+t.x)/2,y:(e.y+t.y)/2}},[Z,M.nodes]),vr=f(()=>{if(!q||!Z)return;let e=ce(M,Z.id,.5);e.result.wallIds&&(X(e.floor,`edit-wall`,[e.result.nodeId]),R({type:`node`,id:e.result.nodeId}))},[q,Z,M,X,R]),yr=f((e,t,n=new Set)=>{if(e.type===`wall`)return{floor:se(t,e.id).floor};if(e.type===`opening`)return{floor:oe(t,e.id).floor};if(e.type===`node`){let n=re(t,e.id,{force:!0});if(n.result)return{floor:n.floor,reselect:{type:`wall`,id:n.result.wallId}};let r=t;for(let n of Object.values(t.walls))(n.a===e.id||n.b===e.id)&&(r=se(r,n.id,{dissolveCollinear:!1}).floor);return{floor:r}}let r=P.find(t=>t.roomId===e.id);if(!r)return{floor:t};let i=new Set;for(let t of P)if(t.roomId!==e.id&&!(t.roomId&&n.has(t.roomId)))for(let e of t.wallIds)i.add(e);let a=t;for(let e of new Set(r.wallIds))i.has(e)||(a=se(a,e,{dissolveCollinear:!1}).floor);let o={...a.rooms};return delete o[e.id],{floor:{...a,rooms:o}}},[P]),br=f(()=>{let e=L.current;if(!q||e.length===0)return;let t={room:0,opening:1,wall:2,node:3},n=[...e].sort((e,n)=>t[e.type]-t[n.type]),r=new Set(n.filter(e=>e.type===`room`).map(e=>e.id)),i=M,a;for(let e of n){let t=yr(e,i,r);i=t.floor,n.length===1&&(a=t.reselect)}i!==M&&(X(i,`delete`,n.map(e=>e.id)),R(a??null))},[q,M,yr,X,R]),xr=f(()=>{Gn([])},[]),Sr=f(e=>{if(C)return;if(gn.current?.focus(),cr(null),bn.current.set(e.pointerId,{x:e.clientX,y:e.clientY}),e.currentTarget.setPointerCapture?.(e.pointerId),bn.current.size===2){let[e,t]=[...bn.current.values()];k.current.kind!==`none`&&k.current.kind!==`pan`&&A.cancelGesture(),k.current={kind:`pinch`,startDist:Math.hypot(t.x-e.x,t.y-e.y),startView:B,startMid:{x:(e.x+t.x)/2,y:(e.y+t.y)/2}};return}if(bn.current.size>2)return;if(xn.current={client:{x:e.clientX,y:e.clientY},moved:!1},e.button===1||Sn.current){k.current={kind:`pan`,startClient:{x:e.clientX,y:e.clientY},startView:B};return}if(e.button!==0)return;let t=ur(e.clientX,e.clientY);if(!q){R(Ee(M,P,t,l(O,B.zoom)));return}if(F===`rect`){let e=Y(t).point;k.current={kind:`rect`,start:e,current:e},A.beginGesture();return}if(F===`measure`){let e=or(t);if(e){let n=K.find(t=>t.id===e.id);k.current=e.end?{kind:`measureEnd`,id:e.id,end:e.end}:{kind:`measureMove`,id:e.id,startWorld:t,origA:n.a,origB:n.b};return}let n=Y(t).point;k.current={kind:`measure`,start:n,current:n};return}if(F===`wall`){let e=U[U.length-1],n=Y(t,e,void 0,lr).point;if(U.length===0){Gn([n]);return}let r=U[U.length-1],i=ne(M,r,n,{thickness:Wt,type:o.Interior,height:Gt},l(O,B.zoom).node);i.result.wallIds.length>0&&X(i.floor,`draw-wall`,i.result.wallIds),U.length>=2&&Math.hypot(n.x-U[0].x,n.y-U[0].y)<l(O,B.zoom).node?xr():Gn(e=>[...e,n]);return}let n=ct[F];if(n){let e=Ee(M,P,t,l(O,B.zoom),{accept:[`wall`]});if(e?.type!==`wall`)return;let r=M.walls[e.id],i=M.nodes[r.a],a=M.nodes[r.b],o=Math.hypot(a.x-i.x,a.y-i.y),s=((t.x-i.x)*(a.x-i.x)+(t.y-i.y)*(a.y-i.y))/(o*o||1),c={...ut[n],...Kt?.[n]},te=ee(M,{wallId:r.id,kind:n,offset:Math.min(Math.max(s,0),1)*o,width:c.width,sill:c.sill,head:c.head});te.result.openingId&&(X(te.floor,`add-opening`,[te.result.openingId]),R({type:`opening`,id:te.result.openingId}));return}let r=l(O,B.zoom);if(_r&&z?.type===`wall`&&Math.hypot(t.x-_r.x,t.y-_r.y)<=r.node){A.beginGesture(),k.current={kind:`wallSplit`,wallId:z.id,at:_r};return}let i=Ee(M,P,t,r),a=jt&&(e.ctrlKey||e.metaKey);if(!i){a||R(null),k.current=jt?{kind:`marquee`,start:t,current:t,additive:a,base:L.current}:{kind:`none`};return}if(a){Ln(i),k.current={kind:`none`};return}if(L.current.some(e=>e.type===i.type&&e.id===i.id)||R(i),A.beginGesture(),L.current.length>1){let e=[...Rn(L.current)],n={};for(let t of e){let e=M.nodes[t];e&&(n[t]={x:e.x,y:e.y})}k.current={kind:`room`,nodeIds:e,startWorld:t,orig:n,moved:!1};return}if(i.type===`node`)k.current={kind:`node`,nodeId:i.id,moved:!1,...dr(i.id)};else if(i.type===`wall`){let e=M.walls[i.id];k.current={kind:`wall`,wallId:i.id,startWorld:t,origA:{...M.nodes[e.a]},origB:{...M.nodes[e.b]},moved:!1}}else if(i.type===`opening`)k.current={kind:`opening`,openingId:i.id,moved:!1};else{let e=P.find(e=>e.roomId===i.id),n=e?[...new Set(e.nodeIds)]:[],r={};for(let e of n){let t=M.nodes[e];t&&(r[e]={x:t.x,y:t.y})}k.current={kind:`room`,nodeIds:n,startWorld:t,orig:r,moved:!1}}},[C,q,B,F,M,P,O,ur,Y,U,Wt,Gt,Kt,X,R,_r,z,lr,dr,Rn,jt,Ln,A,xr,or,K]),Cr=f(e=>{if(C)return;bn.current.has(e.pointerId)&&bn.current.set(e.pointerId,{x:e.clientX,y:e.clientY});let t=k.current;if(t.kind===`pinch`&&bn.current.size>=2){let[e,n]=[...bn.current.values()],r=Math.hypot(n.x-e.x,n.y-e.y);if(t.startDist<=0)return;let i=Math.max(It,Math.min(Lt,t.startView.zoom*(r/t.startDist))),a=_n.current?.getBoundingClientRect()??{left:0,top:0},o={x:(e.x+n.x)/2,y:(e.y+n.y)/2},s=Oe(t.startMid.x,t.startMid.y,a,t.startView);V({x:s.x-(o.x-a.left)/i,y:s.y-(o.y-a.top)/i,zoom:i});return}if(xn.current){let t=e.clientX-xn.current.client.x,n=e.clientY-xn.current.client.y;Math.hypot(t,n)>ft&&(xn.current.moved=!0)}if(t.kind===`pan`){V({x:t.startView.x-(e.clientX-t.startClient.x)/t.startView.zoom,y:t.startView.y-(e.clientY-t.startClient.y)/t.startView.zoom,zoom:t.startView.zoom});return}let n=ur(e.clientX,e.clientY);if(Kn(n),t.kind===`none`){if(!q)return;if(F===`wall`&&U.length>0){let e=Y(n,U[U.length-1],void 0,lr);Kn(e.point),er(e);return}if(F===`select`){let e=Ee(M,P,n,l(O,B.zoom));De(e,Bn)||(Vn(e),Mt?.(e))}F===`measure`&&ir(or(n)?.id??null);return}if(xn.current?.moved){if(t.kind===`measureEnd`){let e=Y(n).point;tr(n=>n.map(n=>n.id===t.id?{...n,[t.end]:e}:n)),Kn(e);return}if(t.kind===`measureMove`){let e=n.x-t.startWorld.x,r=n.y-t.startWorld.y;tr(n=>n.map(n=>n.id===t.id?{...n,a:{x:t.origA.x+e,y:t.origA.y+r},b:{x:t.origB.x+e,y:t.origB.y+r}}:n));return}if(t.kind===`rect`||t.kind===`measure`){k.current={...t,current:Y(n).point},Kn(Y(n).point);return}if(t.kind===`marquee`){k.current={...t,current:n},Kn(n);return}if(t.kind===`wallSplit`){let e=ce(M,t.wallId,t.at);if(!e.result.wallIds){k.current={kind:`none`};return}let r=e.result.nodeId,i=M.walls[t.wallId],a=i?e.floor.nodes[i.a]:void 0,o=i?e.floor.nodes[i.b]:void 0,s={origin:a?{x:a.x,y:a.y}:void 0,referenceAngle:a&&o?Math.atan2(o.y-a.y,o.x-a.x):void 0,excludeWallIds:new Set(e.result.wallIds)},c=Y(n,s.origin,new Set([r]),s.referenceAngle,s.excludeWallIds).point;k.current={kind:`node`,nodeId:r,moved:!0,...s},R({type:`node`,id:r}),er($n.current),mr(ae(e.floor,r,c).floor,`move-node`,!1,[r]);return}if(t.kind===`node`){let e=Y(n,t.origin,new Set([t.nodeId]),t.referenceAngle,t.excludeWallIds).point;er($n.current),k.current={...t,moved:!0},mr(ae(M,t.nodeId,e).floor,`move-node`,!1,[t.nodeId]);return}if(t.kind===`wall`){let e=M.walls[t.wallId];if(!e)return;let r=t.origB.x-t.origA.x,i=t.origB.y-t.origA.y,a=Math.hypot(r,i);if(a<1e-6)return;let o={x:-i/a,y:r/a},s=(n.x-t.startWorld.x)*o.x+(n.y-t.startWorld.y)*o.y,c=Bt!==Cn.current&&S>0?S:1,ee=Math.round(s/c)*c,te=ae(M,e.a,{x:t.origA.x+o.x*ee,y:t.origA.y+o.y*ee}).floor;te=ae(te,e.b,{x:t.origB.x+o.x*ee,y:t.origB.y+o.y*ee}).floor,k.current={...t,moved:!0},mr(te,`move-wall`,!1,[t.wallId]);return}if(t.kind===`opening`){let e=M.openings[t.openingId];if(!e)return;let r=M.walls[e.wallId],i=M.nodes[r.a],a=M.nodes[r.b],o=Math.hypot(a.x-i.x,a.y-i.y),s=((n.x-i.x)*(a.x-i.x)+(n.y-i.y)*(a.y-i.y))/(o*o||1);k.current={...t,moved:!0},mr(le(M,t.openingId,{offset:Math.min(Math.max(s,0),1)*o}).floor,`move-opening`,!1,[t.openingId]);return}if(t.kind===`room`){let e=n.x-t.startWorld.x,r=n.y-t.startWorld.y,i=Bt!==Cn.current&&S>0?S:1,a=Math.round(e/i)*i,o=Math.round(r/i)*i,s=M;for(let e of t.nodeIds){let n=t.orig[e];n&&(s=ae(s,e,{x:n.x+a,y:n.y+o}).floor)}k.current={...t,moved:!0},mr(s,`move-room`,!1,t.nodeIds)}}},[C,q,F,M,P,Bn,O,B.zoom,It,Lt,S,Bt,ur,Y,mr,V,R,U,lr,Mt,or]),wr=f((e,t)=>{bn.current.delete(e.pointerId);let n=k.current;k.current={kind:`none`};let r=xn.current?.moved??!1;if(xn.current=null,er(null),n.kind===`none`||n.kind===`pan`||n.kind===`pinch`)return;if(n.kind===`marquee`){if(t||!r){t&&R([...n.base]);return}let e=Math.min(n.start.x,n.current.x),i=Math.max(n.start.x,n.current.x),a=Math.min(n.start.y,n.current.y),o=Math.max(n.start.y,n.current.y),s=[];for(let t of Object.values(M.nodes))t.x>=e&&t.x<=i&&t.y>=a&&t.y<=o&&s.push({type:`node`,id:t.id});if(n.additive){let e=new Set(n.base.map(e=>`${e.type}:${e.id}`));R([...n.base,...s.filter(t=>!e.has(`${t.type}:${t.id}`))])}else R(s);return}if(t){let e=A.cancelGesture();e&&x?.(e,{reason:`external`,committed:!1,targetIds:[]});return}if(n.kind===`measureEnd`||n.kind===`measureMove`)return;if(n.kind===`measure`){let e=Math.hypot(n.current.x-n.start.x,n.current.y-n.start.y);r&&e>dt&&(nr.current+=1,tr(e=>[...e,{id:nr.current,a:n.start,b:n.current}]));return}if(n.kind===`rect`){if(!r){A.cancelGesture();return}let e=te(M,n.start,n.current,{thickness:Wt,type:o.Interior,height:Gt},l(O,B.zoom).node);if(e.result.wallIds.length===0){A.cancelGesture();return}let t=A.endGesture();X(e.floor,`draw-rect`,e.result.wallIds,{push:!t});return}if(!r){A.cancelGesture();return}if(n.kind===`node`){let e=M.nodes[n.nodeId];if(e){let t=l(O,B.zoom).node,r=Object.values(M.nodes).find(r=>r.id!==n.nodeId&&Math.hypot(r.x-e.x,r.y-e.y)<=t);if(r){let e=A.endGesture();X(ie(M,r.id,n.nodeId).floor,`move-node`,[r.id],{push:!e}),R({type:`node`,id:r.id});return}}}let i=A.endGesture();X(M,n.kind===`wall`?`move-wall`:n.kind===`opening`?`move-opening`:n.kind===`room`?`move-room`:`move-node`,[],{push:!i})},[M,A,O,B.zoom,Wt,Gt,X,R,x]);p(()=>{let e=_n.current;if(!e||C)return;function t(t){t.preventDefault();let n=e.getBoundingClientRect(),r=Math.exp(-t.deltaY*.002);V((()=>{let e=Tr.current,i=Math.max(It,Math.min(Lt,e.zoom*r)),a=Oe(t.clientX,t.clientY,n,e);return{x:a.x-(t.clientX-n.left)/i,y:a.y-(t.clientY-n.top)/i,zoom:i}})())}return e.addEventListener(`wheel`,t,{passive:!1}),()=>e.removeEventListener(`wheel`,t)},[C,It,Lt,V]);let Tr=h(B);Tr.current=B;let Er=f(e=>{let t=Math.max(It,Math.min(Lt,B.zoom*e)),n={x:B.x+H.width/2/B.zoom,y:B.y+H.height/2/B.zoom};V({x:n.x-H.width/2/t,y:n.y-H.height/2/t,zoom:t})},[B,H,It,Lt,V]),Dr=f(()=>{if(rn===`none`)return;let e=A.undo();e&&(yn.current=e,x?.(e,{reason:`undo`,committed:!0,targetIds:[]}),vt?.(e,{reason:`undo`,committed:!0,targetIds:[]}))},[rn,A,x,vt]),Or=f(()=>{if(rn===`none`)return;let e=A.redo();e&&(yn.current=e,x?.(e,{reason:`redo`,committed:!0,targetIds:[]}),vt?.(e,{reason:`redo`,committed:!0,targetIds:[]}))},[rn,A,x,vt]),kr=f(()=>{let e=P.filter(e=>e.roomId).map(e=>({type:`room`,id:e.roomId}));if(e.length>0){R(e);return}R(Object.keys(M.nodes).map(e=>({type:`node`,id:e})))},[P,M.nodes,R]),Ar=f((e,t)=>{if(!q||zn.size===0)return;let n=S>0?S:100,r=wn.current?n*10:n,i=M;for(let n of zn){let a=i.nodes[n];a&&(i=ae(i,n,{x:a.x+e*r,y:a.y+t*r}).floor)}X(i,`nudge`,[...zn])},[q,zn,M,S,X]),jr=f(e=>{if(C)return;let t=e.target;if(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement){e.key===`Escape`&&t.blur();return}if(e.key===`Shift`&&(wn.current=!0),e.key===`Alt`&&(Cn.current=!0),e.key===` `&&!Sn.current){Sn.current=!0,e.preventDefault();return}let n=e.metaKey||e.ctrlKey;if(n&&e.key.toLowerCase()===`z`){e.preventDefault(),e.shiftKey?Or():Dr();return}if(n&&e.key.toLowerCase()===`a`){e.preventDefault(),jt&&kr();return}if(n&&e.key.toLowerCase()===`y`){e.preventDefault(),Or();return}if(e.key===`Escape`){U.length>0?xr():K.length>0?tr([]):R(null);return}if(e.key===`Enter`&&F===`wall`){xr();return}if(e.key===`Delete`||e.key===`Backspace`){if(e.preventDefault(),F===`measure`){rr!==null&&(tr(e=>e.filter(e=>e.id!==rr)),ir(null));return}br();return}if(e.key===`+`||e.key===`=`){Er(1.25);return}if(e.key===`-`){Er(1/1.25);return}if(e.key===`0`){fr();return}if(e.key.startsWith(`Arrow`)){e.preventDefault();let t={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]}[e.key];t&&Ar(t[0],t[1]);return}let r=e.key.toLowerCase();if(r===`s`){Jn(e=>!e);return}if(r===`l`){Xn(e=>!e);return}if(r===`g`){Qn(e=>!e);return}let i=st[e.key.toLowerCase()];i&&q&&Dt.includes(i)&&(Pn(i),xr())},[C,q,Dt,F,U,K.length,rr,Or,Dr,R,xr,br,Er,fr,Ar,Pn,jt,kr]),Q=f((e,t,n)=>{yn.current=e,n?.resetHistory?(A.clearHistory(),A.replace(e)):A.commit(e,t);let r={reason:t,committed:!0,targetIds:[]};x?.(e,r),vt?.(e,r)},[A,x,vt]),Mr=f((e={})=>{let{plan:t,floor:n}=ue(j,{...e,index:he(j,M.id)+1});return Q(t,`add-floor`),N(n.id),n.id},[j,M.id,Q,N]),Nr=f(()=>{let{plan:e,floor:t}=de(j,M.id);return t?(Q(e,`duplicate-floor`),N(t.id),t.id):null},[j,M.id,Q,N]),Pr=f(e=>{let t=e??M.id,n=ve(j,t);if(n!==j&&(Q(n,`remove-floor`),t===M.id)){let e=he(j,t);N(n.floors[Math.max(0,e-1)].id)}},[j,M.id,Q,N]),Fr=f((e,t)=>{let n=_e(j,e,t);n!==j&&Q(n,`move-floor`)},[j,Q]),Ir=f((e,t)=>{let n=xe(j,e,t);n!==j&&Q(n,`edit-floor`)},[j,Q]),Lr=f((e,t)=>{let n=ye(j,e,t);n!==j&&Q(n,`rename-floor`)},[j,Q]),Rr=f(e=>{let t=(e??tn??j.name??``).trim()||`floor-plan`,n=URL.createObjectURL(new Blob([je(j)],{type:`application/json`})),r=document.createElement(`a`);r.href=n,r.download=t.endsWith(`.json`)?t:`${t}.json`,r.click(),setTimeout(()=>URL.revokeObjectURL(n),0)},[tn,j]),zr=f(e=>{let t=Ae(e);return t.plan&&Q(t.plan,`import`),cr(t.plan&&t.issues.length===0?null:t.issues),nn?.(t),t},[nn,Q]),Br=f(e=>{e.text().then(e=>zr(e)).catch(e=>{let t=[`Could not read that file: ${e.message}`];cr(t),nn?.({plan:null,issues:t})})},[zr,nn]),Vr=f(()=>tr([]),[]),Hr=f(e=>{e.key===`Shift`&&(wn.current=!1),e.key===`Alt`&&(Cn.current=!1),e.key===` `&&(Sn.current=!1)},[]);Ie(Fe,()=>({getPlan:()=>j,setPlan:(e,t)=>Q(e,`import`,t),clear:()=>Q(fe(),`clear`),getFloorId:()=>M.id,setFloorId:N,addFloor:e=>Mr(e),duplicateFloor:Nr,removeFloor:Pr,renameFloor:Lr,moveFloor:Fr,setFloorHeight:Ir,clearMeasurements:Vr,download:Rr,loadJson:zr,undo:Dr,redo:Or,canUndo:()=>rn!==`none`&&A.canUndo,canRedo:()=>rn!==`none`&&A.canRedo,clearHistory:A.clearHistory,getSelection:()=>[...L.current],select:R,addToSelection:e=>{let t=Array.isArray(e)?e:[e],n=new Set(L.current.map(e=>`${e.type}:${e.id}`));R([...L.current,...t.filter(e=>!n.has(`${e.type}:${e.id}`))])},toggleSelection:Ln,selectAll:kr,deleteSelection:br,getView:()=>B,setView:V,zoomBy:Er,fitToContent:fr,toWorld:ur,toScreen:J,getSvgElement:()=>vn.current}),[j,A,Q,M,N,Mr,Nr,Pr,Lr,Fr,Ir,Vr,Rr,zr,Dr,Or,rn,R,Ln,kr,br,B,V,Er,fr,ur,J]);let $=k.current,Ur=m(()=>{if(!q)return null;let e=[],t=[...K,...$.kind===`measure`?[{id:-1,a:$.start,b:$.current}]:[]];for(let n of t){let t=J(n.a),r=J(n.b),i=Math.hypot(n.b.x-n.a.x,n.b.y-n.a.y);if(i<dt)continue;let a=Math.atan2(n.b.y-n.a.y,n.b.x-n.a.x),o=-Math.sin(a)*5,s=Math.cos(a)*5,c={x:(t.x+r.x)/2,y:(t.y+r.y)/2};if(e.push(y(`g`,{className:_(d.roomDrawerMeasureLine,rr===n.id&&d.roomDrawerMeasureLineHovered,E?.measureLine),children:[v(`line`,{x1:t.x,y1:t.y,x2:r.x,y2:r.y}),v(`line`,{x1:t.x-o,y1:t.y-s,x2:t.x+o,y2:t.y+s}),v(`line`,{x1:r.x-o,y1:r.y-s,x2:r.x+o,y2:r.y+s})]},`measure-${n.id}`)),F===`measure`&&n.id>=0)for(let[i,a]of[[`a`,t],[`b`,r]])e.push(v(`circle`,{className:d.roomDrawerMeasureGrip,cx:a.x,cy:a.y,r:4},`measure-grip-${n.id}-${i}`));e.push(v(`text`,{className:_(d.roomDrawerMeasure,E?.measureLine),x:c.x,y:c.y-8,textAnchor:`middle`,children:`${Jt(i)} · ${gt(a)}`},`measure-label-${n.id}`))}if(Ut&&G&&(G.guides.forEach((t,n)=>{let r=J(t.a),i=J(t.b);e.push(v(`line`,{className:_(d.roomDrawerGuide,t.kind===u.Angle&&d.roomDrawerGuideAngle),x1:r.x,y1:r.y,x2:i.x,y2:i.y},`guide-${n}`))}),G.kind!==u.None&&G.kind!==u.Grid)){let t=J(G.point);e.push(v(`rect`,{className:d.roomDrawerSnapMarker,x:t.x-4,y:t.y-4,width:8,height:8},`snap-marker`))}if($.kind===`rect`){let t=J($.start),n=J($.current),r=Math.min(t.x,n.x),i=Math.min(t.y,n.y),a=Math.max(t.x,n.x),o=Math.max(t.y,n.y);e.push(v(`rect`,{className:d.roomDrawerPreview,x:r,y:i,width:a-r,height:o-i},`rect-preview`));let s=Math.abs($.current.x-$.start.x),c=Math.abs($.current.y-$.start.y);qt&&(s>0||c>0)&&(e.push(v(`text`,{className:d.roomDrawerMeasure,x:(r+a)/2,y:i-8,textAnchor:`middle`,children:Jt(s)},`rect-width`),v(`text`,{className:d.roomDrawerMeasure,x:r-8,y:(i+o)/2,textAnchor:`end`,dominantBaseline:`central`,children:Jt(c)},`rect-height`)),s>0&&c>0&&e.push(v(`text`,{className:_(d.roomDrawerMeasure,d.roomDrawerMeasureSecondary),x:(r+a)/2,y:(i+o)/2,textAnchor:`middle`,dominantBaseline:`central`,children:Yt(s*c)},`rect-area`)))}if(F===`wall`&&U.length>0){let t=[...U,W].filter(Boolean).map(J);if(e.push(v(`polyline`,{className:d.roomDrawerPreview,points:t.map(e=>`${e.x},${e.y}`).join(` `),fill:`none`},`chain`)),t.forEach((t,n)=>{e.push(v(`circle`,{className:d.roomDrawerHandle,cx:t.x,cy:t.y,r:4},`chain-dot-${n}`))}),qt&&W&&U.length>0){let t=U[U.length-1],n=Math.hypot(W.x-t.x,W.y-t.y),r=J({x:(t.x+W.x)/2,y:(t.y+W.y)/2}),i=G?.kind===u.Angle;e.push(v(`text`,{className:_(d.roomDrawerMeasure,i&&d.roomDrawerMeasureLocked),x:r.x,y:r.y-8,textAnchor:`middle`,children:`${Jt(n)} · ${gt(G?.angleRelativeTo===`reference`&&lr!==void 0?Math.atan2(W.y-t.y,W.x-t.x)-lr:Math.atan2(W.y-t.y,W.x-t.x))}${G?.angleRelativeTo===`reference`?` rel`:``}`},`chain-len`))}}let n=new Set(I.filter(e=>e.type===`node`).map(e=>e.id));for(let t of zn){let r=M.nodes[t];if(!r)continue;let i=J(r);e.push(v(`circle`,{className:_(d.roomDrawerHandle,n.has(t)&&d.roomDrawerHandleActive),cx:i.x,cy:i.y,r:5},`handle-${t}`))}if($.kind===`marquee`&&xn.current?.moved){let t=J($.start),n=J($.current);e.push(v(`rect`,{className:d.roomDrawerMarquee,x:Math.min(t.x,n.x),y:Math.min(t.y,n.y),width:Math.abs(n.x-t.x),height:Math.abs(n.y-t.y)},`marquee`))}if(_r&&$.kind!==`node`){let t=J(_r);e.push(v(`circle`,{className:d.roomDrawerMidHandle,cx:t.x,cy:t.y,r:4},`midpoint`))}if(qt&&$.kind===`room`&&$.moved){let t=new Set($.nodeIds),n=$.nodeIds.find(e=>M.nodes[e]&&$.orig[e]);if(n){let t=M.nodes[n],r=$.orig[n],i=t.x-r.x,a=t.y-r.y,o=z?.type===`room`?P.find(e=>e.roomId===z.id):void 0,s=J(o?o.anchor:t);e.push(v(`text`,{className:_(d.roomDrawerMeasure,d.roomDrawerMeasureOffset),x:s.x,y:s.y-26,textAnchor:`middle`,children:`${Jt(Math.hypot(i,a))} · ${gt(Math.atan2(a,i))}`},`room-offset`))}for(let n of Object.values(M.walls)){if(t.has(n.a)===t.has(n.b))continue;let r=M.nodes[n.a],i=M.nodes[n.b];if(!r||!i)continue;let a=t.has(n.a)?r:i,o=t.has(n.a)?i:r,s=J({x:(r.x+i.x)/2,y:(r.y+i.y)/2});e.push(v(`text`,{className:_(d.roomDrawerMeasure,d.roomDrawerMeasureSecondary),x:s.x,y:s.y-8,textAnchor:`middle`,children:`${Jt(Math.hypot(o.x-a.x,o.y-a.y))} · ${gt(Math.atan2(o.y-a.y,o.x-a.x))}`},`room-stretch-${n.id}`))}}if(qt&&(z?.type===`node`||z?.type===`wall`)){let t=[],n=new Set,r=(e,r,i)=>{if(n.has(e))return;let a=M.walls[e];a&&(n.add(e),t.push({wall:a,from:r,primary:i}))};if(z.type===`node`)for(let e of Object.values(M.walls))(e.a===z.id||e.b===z.id)&&r(e.id,z.id,!0);else{let e=M.walls[z.id];if(e){r(e.id,e.a,!0);for(let t of Object.values(M.walls))if(t.id!==e.id)for(let n of[e.a,e.b])(t.a===n||t.b===n)&&r(t.id,n,!1)}}let i=G?.kind===u.Angle&&$.kind!==`none`;for(let{wall:n,from:r,primary:a}of t){let t=M.nodes[n.a],o=M.nodes[n.b];if(!t||!o)continue;let s=n.a===r?t:o,c=n.a===r?o:t,ee=J({x:(t.x+o.x)/2,y:(t.y+o.y)/2});e.push(v(`text`,{className:_(d.roomDrawerMeasure,!a&&d.roomDrawerMeasureSecondary,a&&i&&d.roomDrawerMeasureLocked),x:ee.x,y:ee.y-8,textAnchor:`middle`,children:`${Jt(Math.hypot(c.x-s.x,c.y-s.y))} · ${gt(Math.atan2(c.y-s.y,c.x-s.x))}`},`measure-${n.id}`))}}return v(at,{children:e})},[q,$,F,U,W,I,M,P,z,zn,_r,qt,Ut,G,lr,Jt,Yt,K,rr,E?.measureLine,J]),Wr={select:{icon:ze,label:w?.select??`Select and edit`,key:`V`},wall:{icon:Qe,label:w?.wall??`Draw walls`,key:`W`},rect:{icon:tt,label:w?.rect??`Draw a room`,key:`R`},door:{icon:We,label:w?.door??`Add a door`,key:`D`},window:{icon:it,label:w?.window??`Add a window`,key:`N`},archway:{icon:qe,label:w?.archway??`Add an archway`,key:`A`},measure:{icon:et,label:w?.measure??`Measure`,key:`M`}},Gr=q?F===`wall`?w?.hintWall??`Click to place corners · Shift for a fixed angle, L to latch it · Click the first corner or press Enter to close`:F===`rect`?w?.hintRect??`Drag to draw a room`:F===`measure`?w?.hintMeasure??`Drag to measure · Drag an end or the line to adjust it · Delete removes one, Escape clears them`:ct[F]?w?.hintOpening??`Click a wall to cut an opening into it`:w?.hintSelect??(jt?`Drag a corner to reshape · Drag a wall to push it, and the room next door moves with it · Ctrl-click or drag a box to select several · Delete to remove`:`Drag a corner to reshape · Drag a wall to push it, and the room next door moves with it · Drag the dot on a selected wall to add a corner · Delete to remove`):``,Kr=I.length===0?w?.selectedFloor??`Floor`:I.length>1?(w?.selectedCount??`{count} selected`).replace(`{count}`,String(I.length)):gr?w?.selectedOpening??lt[gr.kind]:Z?w?.selectedWall??`Wall`:z?.type===`room`?w?.selectedRoom??`Room`:w?.selectedCorner??`Corner`,qr=q?Dt:[];return y(`div`,{...mn,ref:gn,className:_(d.roomDrawer,C&&d.roomDrawerDisabled,fn,E?.root),style:{...pn,...D?.root},role:`application`,"aria-label":dn??`Floor plan editor`,tabIndex:C?-1:0,onKeyDown:jr,onKeyUp:Hr,children:[Xt&&y(`div`,{className:_(d.roomDrawerToolbar,E?.toolbar),style:D?.toolbar,role:`toolbar`,"aria-label":`Drawing tools`,children:[qr.map(e=>{let n=Wr[e];return v(`button`,{type:`button`,className:_(d.roomDrawerToolButton,e===F&&d.roomDrawerToolButtonActive,E?.toolButton),style:D?.toolButton,title:`${n.label} (${n.key})`,"aria-label":n.label,"aria-pressed":e===F,onClick:()=>{Pn(e),xr()},children:t(T?.[e]??n.icon,{className:_(d.roomDrawerToolIcon,E?.toolIcon),style:D?.toolIcon})},e)}),qr.length>0&&v(`span`,{className:d.roomDrawerToolbarSeparator}),q&&y(at,{children:[[{on:qn,toggle:()=>Jn(e=>!e),icon:T?.snap??Ye,label:w?.snap??`Snap to grid and geometry`,key:`S`},{on:Yn,toggle:()=>Xn(e=>!e),icon:T?.angleLock??Ke,label:w?.angleLock??`Lock to ${Ht}° angles`,key:`L`},{on:Zn,toggle:()=>Qn(e=>!e),icon:T?.grid??He,label:w?.grid??`Show grid`,key:`G`},...pe(j,M.id)?[{on:kn,toggle:()=>An(e=>!e),icon:T?.underlay??Je,label:w?.underlay??`Show the floor below`,key:`B`}]:[]].map(e=>v(`button`,{type:`button`,className:_(d.roomDrawerToolButton,e.on&&d.roomDrawerToolButtonOn,E?.toolButton),style:D?.toolButton,title:`${e.label} (${e.key})`,"aria-label":e.label,"aria-pressed":e.on,onClick:e.toggle,children:t(e.icon,{className:_(d.roomDrawerToolIcon,E?.toolIcon),style:D?.toolIcon})},e.key)),v(`span`,{className:d.roomDrawerToolbarSeparator})]}),$t&&rn!==`none`&&q&&y(at,{children:[v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.undo??`Undo`,"aria-label":w?.undo??`Undo`,disabled:!A.canUndo,onClick:Dr,children:t(T?.undo??Be)}),v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.redo??`Redo`,"aria-label":w?.redo??`Redo`,disabled:!A.canRedo,onClick:Or,children:t(T?.redo??Ve)}),v(`span`,{className:d.roomDrawerToolbarSeparator})]}),en&&y(at,{children:[v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.download??`Download plan`,"aria-label":w?.download??`Download plan`,onClick:()=>Rr(),children:t(T?.download??Ge)}),q&&v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.upload??`Open a plan file`,"aria-label":w?.upload??`Open a plan file`,onClick:()=>ar.current?.click(),children:t(T?.upload??rt)}),v(`span`,{className:d.roomDrawerToolbarSeparator})]}),y(`div`,{className:_(d.roomDrawerViewControls,E?.viewControls),style:D?.viewControls,children:[v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.zoomOut??`Zoom out`,"aria-label":w?.zoomOut??`Zoom out`,onClick:()=>Er(1/1.25),children:t(T?.zoomOut??Xe)}),y(`button`,{type:`button`,className:_(d.roomDrawerZoomLabel,E?.zoomLabel),style:D?.zoomLabel,title:w?.fit??`Fit to content`,onClick:()=>fr(),children:[Math.round(B.zoom*1e3)/10,`%`]}),v(`button`,{type:`button`,className:_(d.roomDrawerViewButton,E?.viewButton),style:D?.viewButton,title:w?.zoomIn??`Zoom in`,"aria-label":w?.zoomIn??`Zoom in`,onClick:()=>Er(1.25),children:t(T?.zoomIn??Ze)})]})]}),St&&j.floors.length>0&&y(`div`,{className:_(d.roomDrawerFloorBar,E?.floorBar),style:D?.floorBar,role:`tablist`,"aria-label":w?.selectedFloor??`Floor`,children:[[...j.floors].reverse().map(e=>v(`button`,{type:`button`,role:`tab`,className:_(d.roomDrawerFloorTab,e.id===M.id&&d.roomDrawerFloorTabActive,E?.floorTab),style:D?.floorTab,"aria-selected":e.id===M.id,onClick:()=>N(e.id),children:e.label},e.id)),q&&v(`button`,{type:`button`,className:_(d.roomDrawerFloorTab,d.roomDrawerFloorTabAdd),title:w?.addFloor??`Add a floor above`,"aria-label":w?.addFloor??`Add a floor above`,onClick:()=>Mr(),children:t(T?.addFloor??$e)})]}),v(`div`,{ref:_n,className:_(d.roomDrawerCanvasWrapper,q&&F!==`select`&&d.roomDrawerCanvasWrapperDrawing,E?.canvasWrapper),style:{height:cn,...D?.canvasWrapper},onPointerDown:Sr,onPointerMove:Cr,onPointerUp:e=>wr(e,!1),onPointerCancel:e=>wr(e,!0),children:v(Ne,{ref:vn,floor:M,underlay:jn,rooms:P,view:B,size:H,selection:I,hovered:Bn,interactiveRooms:!1,showGrid:Zn,gridSize:S,showRoomLabels:!0,formatArea:Yt,getRoomColor:ln,renderRoomLabel:un,overlay:Ur,classNames:E,styles:D})}),Zt&&q&&y(`div`,{className:_(d.roomDrawerInspector,E?.inspector),style:D?.inspector,children:[v(`span`,{className:_(d.roomDrawerInspectorKind,I.length===0&&d.roomDrawerInspectorKindEmpty,E?.inspectorLabel),children:Kr}),I.length===0&&y(at,{children:[v(i,{className:_(d.roomDrawerInspectorName,E?.inspectorField),style:D?.inspectorField,label:w?.floorName??`Floor name`,value:M.label,onChange:e=>Lr(M.id,e)}),v(Me,{className:_(d.roomDrawerInspectorField,E?.inspectorField),style:D?.inspectorField,label:w?.floorHeight??`Height (mm)`,title:`Storey height. The floors above move to sit on top of it.`,value:M.height,decimalScale:0,min:500,step:100,showArrows:!0,onChange:e=>{e!==void 0&&Ir(M.id,e)}}),j.floors.length>1&&y(at,{children:[v(`button`,{type:`button`,className:_(d.roomDrawerInspectorAction,E?.inspectorField),style:D?.inspectorField,title:w?.moveFloorUp??`Move this floor up`,"aria-label":w?.moveFloorUp??`Move this floor up`,disabled:he(j,M.id)>=j.floors.length-1,onClick:()=>Fr(M.id,he(j,M.id)+1),children:t(T?.moveFloorUp??Re)}),v(`button`,{type:`button`,className:_(d.roomDrawerInspectorAction,E?.inspectorField),style:D?.inspectorField,title:w?.moveFloorDown??`Move this floor down`,"aria-label":w?.moveFloorDown??`Move this floor down`,disabled:he(j,M.id)<=0,onClick:()=>Fr(M.id,he(j,M.id)-1),children:t(T?.moveFloorDown??Le)})]}),v(`button`,{type:`button`,className:_(d.roomDrawerInspectorAction,E?.inspectorField),style:D?.inspectorField,title:w?.duplicateFloor??`Copy this floor to a new one above`,onClick:()=>Nr(),children:t(T?.duplicateFloor??Ue)}),j.floors.length>1&&v(n,{className:d.roomDrawerInspectorDelete,icon:T?.delete??nt,title:w?.deleteFloor??`Delete this floor`,onClick:()=>Pr()})]}),hr?.meta&&y(at,{children:[v(i,{className:_(d.roomDrawerInspectorName,E?.inspectorField),style:D?.inspectorField,label:w?.roomName??`Room name`,value:hr.meta.name,onChange:e=>{let t=hr.meta;X({...M,rooms:{...M.rooms,[t.id]:{...t,name:e}}},`edit-room`,[t.id])}}),v(`span`,{className:_(d.roomDrawerInspectorLabel,E?.inspectorLabel),children:Yt(hr.area)})]}),Z&&v(`button`,{type:`button`,className:_(d.roomDrawerInspectorAction,E?.inspectorField),style:D?.inspectorField,onClick:vr,title:w?.addCorner??`Add a corner to this wall`,children:w?.addCorner??`Add corner`}),Z&&v(Me,{className:_(d.roomDrawerInspectorField,E?.inspectorField),style:D?.inspectorField,label:w?.thickness??`Thickness (mm)`,value:Z.thickness,decimalScale:0,min:10,showArrows:!0,step:10,onChange:e=>{e!==void 0&&X({...M,walls:{...M.walls,[Z.id]:{...Z,thickness:e}}},`edit-wall`,[Z.id])}}),gr&&y(at,{children:[v(Me,{className:_(d.roomDrawerInspectorField,E?.inspectorField),style:D?.inspectorField,label:w?.width??`Width (mm)`,value:gr.width,decimalScale:0,min:100,step:50,showArrows:!0,onChange:e=>{e!==void 0&&X(le(M,gr.id,{width:e}).floor,`edit-opening`,[gr.id])}}),v(Me,{className:_(d.roomDrawerInspectorField,E?.inspectorField),style:D?.inspectorField,label:w?.sill??`Sill (mm)`,title:`How far above the floor the opening starts. Stored for a 3D view; it does not change the plan.`,showArrows:!0,value:gr.sill,decimalScale:0,min:0,step:50,onChange:e=>{e!==void 0&&X(le(M,gr.id,{sill:e}).floor,`edit-opening`,[gr.id])}})]}),I.length>0&&v(n,{className:d.roomDrawerInspectorDelete,icon:T?.delete??nt,title:w?.deleteSelection??`Delete selection`,onClick:br})]}),en&&q&&v(`input`,{ref:ar,type:`file`,accept:`application/json,.json`,className:d.roomDrawerFileInput,tabIndex:-1,"aria-hidden":`true`,onChange:e=>{let t=e.target.files?.[0];e.target.value=``,t&&Br(t)}}),Qt&&(sr!==null||Gr)&&v(`div`,{className:_(d.roomDrawerHint,sr!==null&&d.roomDrawerHintReport,E?.hint),style:D?.hint,role:sr===null?void 0:`status`,children:sr===null?Gr:sr.length===0?w?.importFailed??`That file could not be read as a floor plan`:sr.join(` · `)})]})});export{b as default};
2
2
  //# sourceMappingURL=roomDrawer.mjs.map