@reekon-tools/boldr-cad 1.3.0 → 1.5.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 (252) hide show
  1. package/README.md +21 -4
  2. package/dist/app/edit.d.ts +3 -3
  3. package/dist/app/edit.js +26 -1
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +3 -5
  6. package/dist/app/state.js +0 -1
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/sync.d.ts +1 -1
  9. package/dist/data/sync.js +7 -2
  10. package/dist/data/sync.js.map +1 -1
  11. package/dist/editor/actions.d.ts +8 -0
  12. package/dist/editor/actions.js +149 -169
  13. package/dist/editor/actions.js.map +1 -1
  14. package/dist/editor/elevation.d.ts +4 -1
  15. package/dist/editor/elevation.js +16 -17
  16. package/dist/editor/elevation.js.map +1 -1
  17. package/dist/editor/gestures.d.ts +21 -0
  18. package/dist/editor/gestures.js +92 -14
  19. package/dist/editor/gestures.js.map +1 -1
  20. package/dist/editor/keys.js +2 -2
  21. package/dist/editor/keys.js.map +1 -1
  22. package/dist/editor/linkActions.d.ts +22 -3
  23. package/dist/editor/linkActions.js +95 -13
  24. package/dist/editor/linkActions.js.map +1 -1
  25. package/dist/editor/mode.js +4 -3
  26. package/dist/editor/mode.js.map +1 -1
  27. package/dist/editor/project.js +1 -2
  28. package/dist/editor/project.js.map +1 -1
  29. package/dist/editor/scaleGate.d.ts +13 -0
  30. package/dist/editor/scaleGate.js +27 -0
  31. package/dist/editor/scaleGate.js.map +1 -0
  32. package/dist/editor/selection.js +13 -9
  33. package/dist/editor/selection.js.map +1 -1
  34. package/dist/editor/tooltips.js +9 -0
  35. package/dist/editor/tooltips.js.map +1 -1
  36. package/dist/editor/view3d.d.ts +4 -1
  37. package/dist/editor/view3d.js +6 -2
  38. package/dist/editor/view3d.js.map +1 -1
  39. package/dist/export/pdf.d.ts +5 -2
  40. package/dist/export/pdf.js +12 -8
  41. package/dist/export/pdf.js.map +1 -1
  42. package/dist/export/plan.d.ts +1 -1
  43. package/dist/export/plan.js +14 -9
  44. package/dist/export/plan.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/model/demo.d.ts +1 -0
  49. package/dist/model/demo.js +2 -1
  50. package/dist/model/demo.js.map +1 -1
  51. package/dist/model/elevation.d.ts +1 -1
  52. package/dist/model/elevation.js +3 -3
  53. package/dist/model/elevation.js.map +1 -1
  54. package/dist/model/history.js +4 -1
  55. package/dist/model/history.js.map +1 -1
  56. package/dist/model/links.d.ts +4 -1
  57. package/dist/model/links.js +9 -2
  58. package/dist/model/links.js.map +1 -1
  59. package/dist/model/measurement.d.ts +2 -2
  60. package/dist/model/measurement.js +3 -3
  61. package/dist/model/measurement.js.map +1 -1
  62. package/dist/model/openings.d.ts +10 -0
  63. package/dist/model/openings.js +18 -0
  64. package/dist/model/openings.js.map +1 -1
  65. package/dist/model/planGeom.d.ts +37 -2
  66. package/dist/model/planGeom.js +72 -9
  67. package/dist/model/planGeom.js.map +1 -1
  68. package/dist/model/project.js +5 -2
  69. package/dist/model/project.js.map +1 -1
  70. package/dist/model/rooms.d.ts +18 -5
  71. package/dist/model/rooms.js +178 -73
  72. package/dist/model/rooms.js.map +1 -1
  73. package/dist/model/scale.d.ts +30 -0
  74. package/dist/model/scale.js +120 -0
  75. package/dist/model/scale.js.map +1 -0
  76. package/dist/model/settings.d.ts +0 -1
  77. package/dist/model/settings.js +1 -1
  78. package/dist/model/settings.js.map +1 -1
  79. package/dist/model/sketch.d.ts +21 -1
  80. package/dist/model/sketch.js +58 -4
  81. package/dist/model/sketch.js.map +1 -1
  82. package/dist/model/stroke.js +2 -2
  83. package/dist/model/types.d.ts +38 -7
  84. package/dist/react/BottomBar.d.ts +2 -1
  85. package/dist/react/BottomBar.js +94 -25
  86. package/dist/react/BottomBar.js.map +1 -1
  87. package/dist/react/Drawer.js +3 -2
  88. package/dist/react/Drawer.js.map +1 -1
  89. package/dist/react/Duck.js.map +1 -1
  90. package/dist/react/KeypadSheet.js.map +1 -1
  91. package/dist/react/MarkupPanel.js +1 -1
  92. package/dist/react/PlanCanvas.native.js +54 -7
  93. package/dist/react/PlanCanvas.native.js.map +1 -1
  94. package/dist/react/Popovers.js +14 -3
  95. package/dist/react/Popovers.js.map +1 -1
  96. package/dist/react/Rail.js +1 -1
  97. package/dist/react/Sheet.d.ts +2 -2
  98. package/dist/react/Sheet.js.map +1 -1
  99. package/dist/react/barScroll.d.ts +23 -0
  100. package/dist/react/barScroll.js +47 -0
  101. package/dist/react/barScroll.js.map +1 -0
  102. package/dist/react/corners.d.ts +6 -2
  103. package/dist/react/corners.js.map +1 -1
  104. package/dist/react/host.d.ts +7 -0
  105. package/dist/react/host.js +14 -1
  106. package/dist/react/host.js.map +1 -1
  107. package/dist/react/motion.d.ts +1 -1
  108. package/dist/react/motion.js +1 -1
  109. package/dist/react/motion.js.map +1 -1
  110. package/dist/render/draw.d.ts +21 -4
  111. package/dist/render/draw.js +184 -149
  112. package/dist/render/draw.js.map +1 -1
  113. package/dist/render/elevation.js +3 -3
  114. package/dist/render/elevation.js.map +1 -1
  115. package/dist/render/figmaIcons.d.ts +6 -0
  116. package/dist/render/figmaIcons.js +16 -1
  117. package/dist/render/figmaIcons.js.map +1 -1
  118. package/dist/render/hit.d.ts +8 -1
  119. package/dist/render/hit.js +23 -4
  120. package/dist/render/hit.js.map +1 -1
  121. package/dist/render/icons.js +27 -20
  122. package/dist/render/icons.js.map +1 -1
  123. package/dist/render/lens.d.ts +27 -0
  124. package/dist/render/lens.js +41 -0
  125. package/dist/render/lens.js.map +1 -0
  126. package/dist/render/linkTile.d.ts +20 -2
  127. package/dist/render/linkTile.js +476 -246
  128. package/dist/render/linkTile.js.map +1 -1
  129. package/dist/render/snap.d.ts +5 -1
  130. package/dist/render/snap.js +3 -1
  131. package/dist/render/snap.js.map +1 -1
  132. package/dist/render/svgPath.d.ts +29 -0
  133. package/dist/render/svgPath.js +114 -0
  134. package/dist/render/svgPath.js.map +1 -0
  135. package/dist/render/tileIcons.d.ts +18 -0
  136. package/dist/render/tileIcons.js +67 -0
  137. package/dist/render/tileIcons.js.map +1 -0
  138. package/dist/render/view.d.ts +18 -1
  139. package/dist/render/view.js +31 -4
  140. package/dist/render/view.js.map +1 -1
  141. package/dist/skia/SkiaDrawCtx.d.ts +8 -0
  142. package/dist/skia/SkiaDrawCtx.js +27 -5
  143. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  144. package/dist/solver/equations.d.ts +6 -0
  145. package/dist/solver/equations.js +23 -6
  146. package/dist/solver/equations.js.map +1 -1
  147. package/dist/solver/gauss-newton.js +6 -1
  148. package/dist/solver/gauss-newton.js.map +1 -1
  149. package/dist/tools/annotate.d.ts +0 -2
  150. package/dist/tools/annotate.js +1 -22
  151. package/dist/tools/annotate.js.map +1 -1
  152. package/dist/tools/chamfer.js +4 -0
  153. package/dist/tools/chamfer.js.map +1 -1
  154. package/dist/tools/dim.d.ts +3 -3
  155. package/dist/tools/dim.js +26 -65
  156. package/dist/tools/dim.js.map +1 -1
  157. package/dist/tools/fillet.js +4 -0
  158. package/dist/tools/fillet.js.map +1 -1
  159. package/dist/tools/freehand.js +1 -4
  160. package/dist/tools/freehand.js.map +1 -1
  161. package/dist/tools/line.js +15 -11
  162. package/dist/tools/line.js.map +1 -1
  163. package/dist/tools/modify.js +6 -1
  164. package/dist/tools/modify.js.map +1 -1
  165. package/dist/tools/registry.d.ts +5 -0
  166. package/dist/tools/registry.js +6 -1
  167. package/dist/tools/registry.js.map +1 -1
  168. package/dist/tools/select.d.ts +19 -0
  169. package/dist/tools/select.js +116 -17
  170. package/dist/tools/select.js.map +1 -1
  171. package/dist/tools/shapes.d.ts +1 -1
  172. package/dist/tools/shapes.js +12 -5
  173. package/dist/tools/shapes.js.map +1 -1
  174. package/dist/tools/survey.d.ts +6 -0
  175. package/dist/tools/survey.js +28 -0
  176. package/dist/tools/survey.js.map +1 -1
  177. package/dist/view3d/build.js +9 -8
  178. package/dist/view3d/build.js.map +1 -1
  179. package/package.json +9 -9
  180. package/src/app/edit.ts +25 -1
  181. package/src/app/state.ts +3 -5
  182. package/src/data/sync.ts +7 -2
  183. package/src/editor/actions.ts +126 -112
  184. package/src/editor/elevation.ts +13 -16
  185. package/src/editor/gestures.ts +83 -13
  186. package/src/editor/keys.ts +2 -2
  187. package/src/editor/linkActions.ts +90 -13
  188. package/src/editor/mode.ts +4 -3
  189. package/src/editor/project.ts +1 -2
  190. package/src/editor/scaleGate.ts +32 -0
  191. package/src/editor/selection.ts +13 -7
  192. package/src/editor/tooltips.ts +8 -0
  193. package/src/editor/view3d.ts +6 -2
  194. package/src/export/pdf.ts +7 -4
  195. package/src/export/plan.ts +14 -10
  196. package/src/index.ts +1 -1
  197. package/src/model/demo.ts +2 -1
  198. package/src/model/elevation.ts +4 -4
  199. package/src/model/history.ts +4 -1
  200. package/src/model/links.ts +8 -2
  201. package/src/model/measurement.ts +3 -3
  202. package/src/model/openings.ts +18 -0
  203. package/src/model/planGeom.ts +70 -10
  204. package/src/model/project.ts +6 -2
  205. package/src/model/rooms.ts +151 -63
  206. package/src/model/scale.ts +105 -0
  207. package/src/model/settings.ts +1 -2
  208. package/src/model/sketch.ts +48 -4
  209. package/src/model/stroke.ts +2 -2
  210. package/src/model/types.ts +38 -7
  211. package/src/react/BottomBar.tsx +114 -29
  212. package/src/react/Drawer.tsx +14 -10
  213. package/src/react/Duck.tsx +3 -3
  214. package/src/react/KeypadSheet.tsx +2 -2
  215. package/src/react/MarkupPanel.tsx +1 -1
  216. package/src/react/PlanCanvas.native.tsx +60 -6
  217. package/src/react/Popovers.tsx +24 -9
  218. package/src/react/Rail.tsx +1 -1
  219. package/src/react/Sheet.tsx +3 -3
  220. package/src/react/barScroll.ts +57 -0
  221. package/src/react/corners.ts +6 -3
  222. package/src/react/host.ts +14 -1
  223. package/src/react/motion.tsx +6 -6
  224. package/src/render/draw.ts +156 -116
  225. package/src/render/elevation.ts +5 -4
  226. package/src/render/figmaIcons.ts +17 -1
  227. package/src/render/hit.ts +21 -4
  228. package/src/render/icons.ts +27 -20
  229. package/src/render/lens.ts +52 -0
  230. package/src/render/linkTile.ts +436 -190
  231. package/src/render/snap.ts +7 -2
  232. package/src/render/svgPath.ts +95 -0
  233. package/src/render/tileIcons.ts +75 -0
  234. package/src/render/view.ts +37 -5
  235. package/src/skia/SkiaDrawCtx.ts +26 -4
  236. package/src/solver/equations.ts +22 -6
  237. package/src/solver/gauss-newton.ts +4 -1
  238. package/src/styles.css +3 -2
  239. package/src/tools/annotate.ts +1 -18
  240. package/src/tools/chamfer.ts +3 -0
  241. package/src/tools/dim.ts +26 -52
  242. package/src/tools/fillet.ts +3 -0
  243. package/src/tools/freehand.ts +1 -3
  244. package/src/tools/line.ts +12 -11
  245. package/src/tools/modify.ts +5 -1
  246. package/src/tools/registry.ts +10 -1
  247. package/src/tools/select.ts +99 -18
  248. package/src/tools/shapes.ts +9 -4
  249. package/src/tools/survey.ts +21 -0
  250. package/src/ui/ctx.ts +4 -3
  251. package/src/ui/drawer.ts +8 -2
  252. package/src/view3d/build.ts +11 -10
@@ -15,6 +15,7 @@ import { mainT, requestDraw, requestOverlayDraw, S } from '../render/view';
15
15
  import { fmtLen } from '../units';
16
16
  import { anchorMark, applyPending, commitPoint, lengthPreview, PT, rememberHover, wallBand, type PEv } from './registry';
17
17
  import { toast, updateHint } from '../editor/ports';
18
+ import { noScaleYet } from '../editor/scaleGate';
18
19
  import { wallBounds } from '../editor/elevation';
19
20
  import { drawsWalls } from '../editor/mode';
20
21
  import { splitWallsAtEnds } from './modify';
@@ -93,13 +94,15 @@ export const rectTool = {
93
94
  // (a wall corner placed partway along another wall splits that one there)
94
95
  if (wall) for (const id of sides) splitWallsAtEnds(sk, id);
95
96
  applyPending(a.cons); applyPending(c.cons);
96
- for (const k of [{ type: 'h' as const, a: a.id, b }, { type: 'v' as const, a: b, b: c.id }, { type: 'h' as const, a: d, b: c.id }, { type: 'v' as const, a: a.id, b: d }]) tryAddConstraint(k, { silent: true });
97
+ // its sides held level and plumb only with auto-constrain on, as any drawing's are (the user's call, 2026-10-26:
98
+ // "if auto constraint is disabled - it should not work on squares either"); off, it's four lines like any others
99
+ if (settings.autoCons) for (const k of [{ type: 'h' as const, a: a.id, b }, { type: 'v' as const, a: b, b: c.id }, { type: 'h' as const, a: d, b: c.id }, { type: 'v' as const, a: a.id, b: d }]) tryAddConstraint(k, { silent: true });
97
100
  this.reset();
98
101
  // (drawn, not selected: nothing new is highlighted — tap it to select it)
99
102
  ed.sel = [];
100
103
  afterEdit();
101
104
  },
102
- hint() { return this.st.p1 ? 'Tap the opposite corner.' : 'Tap or drag two opposite corners. Edges stay square.'; },
105
+ hint() { return this.st.p1 ? 'Tap the opposite corner.' : 'Tap or drag two opposite corners.' + (settings.autoCons ? ' Edges stay square.' : ''); },
103
106
  };
104
107
 
105
108
  export const circleTool = {
@@ -128,7 +131,8 @@ export const circleTool = {
128
131
  c.restore();
129
132
  // centre marker: crosshair + dot, always visible
130
133
  anchorMark(c, x, y);
131
- chip(c, 'Ø ' + fmtLen(2 * r), x, y - r * T.s - 16, { bg: '#fff', fg: '#0066FF', border: ['#0066FF', 1.5] });
134
+ // (its diameter once the drawing has its scale: before, only a placeholder)
135
+ if (!noScaleYet()) chip(c, 'Ø ' + fmtLen(2 * r), x, y - r * T.s - 16, { bg: '#fff', fg: '#0066FF', border: ['#0066FF', 1.5] });
132
136
  };
133
137
  } else ed.preview = null;
134
138
  requestOverlayDraw();
@@ -151,7 +155,8 @@ export const circleTool = {
151
155
  applyPending(cp.cons);
152
156
  const id = uid('ci');
153
157
  SK().circles[id] = { c: cp.id, r, ...asDrawn() };
154
- if (s.kind === 'point') tryAddConstraint({ type: 'oncircle', p: s.ref!, c: id }, { silent: true });
158
+ // (its edge on a point: held there with auto-constrain on, as a snap is when drawing)
159
+ if (s.kind === 'point' && settings.autoCons) tryAddConstraint({ type: 'oncircle', p: s.ref!, c: id }, { silent: true });
155
160
  this.reset();
156
161
  ed.sel = [];
157
162
  afterEdit();
@@ -256,5 +256,26 @@ export function takeSurveyPoint(x: number, y: number, capture?: unknown): boolea
256
256
  return true;
257
257
  }
258
258
 
259
+ /**
260
+ * Close the open run from the host's device (a button that means "close": BOLDR's T1R's D), as the bar's Close shape
261
+ * does. No point is taken: the last edge goes back to the first point from wherever the run ended. False when there's
262
+ * no run of 3 points or more to close, or the drawing can't take it now (the toast says which).
263
+ */
264
+ export function closeSurveyRun(): boolean {
265
+ if (ed.readOnly) return false;
266
+ if (elevOpen()) { toast('Back out of the wall view to close the shape.', { warn: true }); return false; }
267
+ if (keypadOpen()) { toast('Close the keypad, then close the shape again.', { warn: true }); return false; }
268
+ // (a run ends when another tool is picked up, or on Annotate: its chain is kept only for the next pick-up to clear)
269
+ const n = ed.tool === 'survey' ? liveChain().length : 0;
270
+ if (n < 3) {
271
+ toast(n ? 'Take ' + (n === 2 ? 'one more point' : '2 more points') + ', then close the shape.' : 'No shape to close — take points with your templator first.', { warn: true });
272
+ return false;
273
+ }
274
+ // (the 3D tab keeps the run, as it keeps the tool in hand: closed on the plan, as a point there would be)
275
+ if (is3d()) close3d();
276
+ surveyTool.closeShape();
277
+ return true;
278
+ }
279
+
259
280
  // a new session (another drawing): every page's frame and the run start again
260
281
  onSessionReset(() => { surveyTool.st = fresh(); });
package/src/ui/ctx.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ed } from '../app/state';
2
2
  import { markupTarget } from '../editor/markup';
3
- import { applyCtxAction, barItems, deleteSelection, plusAction, plusLabel, plusVisible } from '../editor/actions';
3
+ import { applyCtxAction, barItems, deleteSelection, iconAlone, plusAction, plusLabel, plusVisible } from '../editor/actions';
4
4
  import { selInfo, selectionHint } from '../editor/selection';
5
5
  import { $ } from './dom';
6
6
  import { icon } from './icons';
@@ -16,10 +16,11 @@ export function updateCtx(): void {
16
16
  if (it === 'sep') { const d = document.createElement('div'); d.className = 'sep'; box.appendChild(d); continue; }
17
17
  if ('lbl' in it) { const d = document.createElement('div'); d.className = 'lbl'; d.textContent = it.lbl; box.appendChild(d); continue; }
18
18
  const b = document.createElement('button');
19
- b.className = 'cb' + (it.on ? ' on' : '') + (it.locked ? ' locked' : '');
19
+ b.className = 'cb' + (it.on ? ' on' : '') + (it.locked ? ' locked' : '') + (iconAlone(it) ? ' lead' : '');
20
+ // (the bar's lead — Lock, or Open — and Lock wherever it is: its icon alone, its name its label for a screen reader)
20
21
  b.innerHTML = (it.ic === 'color'
21
22
  ? `<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8" fill="${markupTarget().color}" stroke="#fff" stroke-width="2"/></svg>`
22
- : icon(it.ic)) + '<span>' + it.label + '</span>';
23
+ : icon(it.ic)) + (iconAlone(it) ? '' : '<span>' + it.label + '</span>');
23
24
  b.setAttribute('aria-label', it.label);
24
25
  if (it.on != null) b.setAttribute('aria-pressed', String(!!it.on));
25
26
  if (it.locked != null) b.setAttribute('aria-pressed', String(it.locked));
package/src/ui/drawer.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { addPage, deletePage, deletePending, duplicatePage, focusSpace, renamePage, renameProject, renameSpace, spaceSchedule, switchPage as switchPageIn } from '../editor/project';
2
2
  import { allPages, page, store } from '../model/store';
3
+ import { noScaleYet } from '../editor/scaleGate';
3
4
  import { drawThumb } from '../render/draw';
4
5
  import { fmtArea, fmtLen } from '../units';
5
6
  import { $, esc } from './dom';
@@ -57,11 +58,16 @@ export function renderDrawer(): void {
57
58
  } else {
58
59
  const sched = spaceSchedule(), total = sched.total;
59
60
  if (!sched.rows.length) { body.innerHTML = '<p class="empty">No spaces on this page yet. Close a loop of walls or lines, or pick the Space tool and tap inside a closed shape.</p>'; return; }
61
+ // (a sketch's sizes are placeholders until its first length: no areas until then)
62
+ const sketchOnly = noScaleYet();
60
63
  let h = '';
61
64
  for (const r of sched.rows) {
62
- h += `<div class="roomrow"><input class="field" data-room="${r.id}" value="${esc(r.name)}" aria-label="Space name"><div class="nums"><span><b>${fmtArea(r.area)}</b> floor</span><span><b>${fmtLen(r.per)}</b> perimeter</span></div></div>`;
65
+ const nums = sketchOnly ? '' : `<div class="nums"><span><b>${fmtArea(r.area)}</b> floor</span><span><b>${fmtLen(r.per)}</b> perimeter</span></div>`;
66
+ h += `<div class="roomrow"><input class="field" data-room="${r.id}" value="${esc(r.name)}" aria-label="Space name">${nums}</div>`;
63
67
  }
64
- h += `<div class="row"><div><b>Total</b><small>Measured to inside wall faces</small></div><b>${fmtArea(total)}</b></div>`;
68
+ h += sketchOnly
69
+ ? '<p class="empty">Areas show once the drawing has a length. Select a line and tap + to enter one.</p>'
70
+ : `<div class="row"><div><b>Total</b><small>Measured to inside wall faces</small></div><b>${fmtArea(total)}</b></div>`;
65
71
  body.innerHTML = h;
66
72
  body.querySelectorAll<HTMLInputElement>('[data-room]').forEach(i => {
67
73
  i.onchange = () => renameSpace(i.dataset.room!, i.value);
@@ -2,12 +2,12 @@ import { openingHeights, toU, wallHeight } from '../model/elevation';
2
2
  import { faceKeyParts } from '../model/faces';
3
3
  import { faceFrame, UPPER_Z } from '../model/facesolve';
4
4
  import { hyp, normAng } from '../model/geom';
5
- import { edgeS, openingSpan } from '../model/openings';
5
+ import { edgeS, hingeEdge, openingSpan } from '../model/openings';
6
6
  import { curveFrame, wallThick } from '../model/planGeom';
7
7
  import { roomGeom } from '../model/rooms';
8
8
  import { settings } from '../model/settings';
9
9
  import { roomValid, withLineIndex } from '../model/sketch';
10
- import type { Opening, Sketch, XY } from '../model/types';
10
+ import type { Opening, OpeningEdge, Sketch, XY } from '../model/types';
11
11
  import { wallPieces } from '../model/wallshape';
12
12
  import { boxLines, cellLines, mergeIv, raiseIn, type Cell3 } from './lines';
13
13
  import { PartBuilder, type Mat3, type Part3, type V3 } from './mesh';
@@ -59,7 +59,7 @@ export function inFront(cut: Cut3, m: XY, n: XY): boolean {
59
59
 
60
60
  interface Wall3 { id: string; A: XY; B: XY; u: XY; L: number; t: number; h: number; full: number }
61
61
 
62
- interface Opening3 { id: string; type: Opening['type']; s0: number; s1: number; sill: number; head: number; flip: boolean }
62
+ interface Opening3 { id: string; type: Opening['type']; s0: number; s1: number; sill: number; head: number; flip: boolean; hinge: OpeningEdge }
63
63
 
64
64
  /** The plan's straight walls, each at its height (cut down when it's in front of a cutaway). */
65
65
  function straightWalls(sk: Sketch, cut: Cut3 | null, lowered: string[]): Map<string, Wall3> {
@@ -150,7 +150,7 @@ function openingsIn(sk: Sketch, w: Wall3, mine: [string, Opening][]): Opening3[]
150
150
  const s0 = Math.max(0, Math.min(a, b)), s1 = Math.min(w.L, Math.max(a, b));
151
151
  if (s1 - s0 < 0.01) continue;
152
152
  const { sill, head } = openingHeights(o);
153
- out.push({ id, type: o.type, s0, s1, sill: Math.min(sill, w.h), head: Math.min(head, w.h), flip: !!o.flip });
153
+ out.push({ id, type: o.type, s0, s1, sill: Math.min(sill, w.h), head: Math.min(head, w.h), flip: !!o.flip, hinge: hingeEdge(o) });
154
154
  }
155
155
  return out.sort((p, q) => p.s0 - q.s0);
156
156
  }
@@ -243,8 +243,8 @@ const FRAME = 0.75, CASING = 2.5, CASING_OUT = 0.625, LEAF = 1.375, OPEN = (30 *
243
243
 
244
244
  /**
245
245
  * A door, so it reads as one: the floor run on through the doorway, a frame lining its sides and head, casings round
246
- * it on both faces (as high as the wall goes), and its leaf — hinged at its first side on the face it swings to, as the
247
- * plan draws it, and open a little. Its outlines go on `lines`.
246
+ * it on both faces (as high as the wall goes), and its leaf — hung on its hinge's jamb (Opening.hinge) on the face it
247
+ * swings to, as the plan draws it, and open a little. Its outlines go on `lines`.
248
248
  */
249
249
  function doorPart(w: Wall3, o: Opening3, lines: number[]): PartBuilder {
250
250
  const b = new PartBuilder('opening', o.id), half = w.t / 2, head = Math.min(o.head, w.h), nx = -w.u.y, ny = w.u.x;
@@ -267,12 +267,13 @@ function doorPart(w: Wall3, o: Opening3, lines: number[]): PartBuilder {
267
267
  box(rect(o.s1, o.s1 + CASING, k0, k1), 0, top, 'frame');
268
268
  box(rect(o.s0, o.s1, k0, k1), head, top, 'frame');
269
269
  }
270
- const f = o.flip ? -1 : 1, W = o.s1 - o.s0 - 2 * FRAME, H = head - FRAME - 0.5;
270
+ const f = o.flip ? -1 : 1, m = o.hinge === 's1' ? -1 : 1, W = o.s1 - o.s0 - 2 * FRAME, H = head - FRAME - 0.5;
271
271
  if (W > 2 && H > 2) {
272
- // (turned about its hinge, at the frame on the face it swings to, toward that face)
273
- const hs = o.s0 + FRAME, hk = f * half, c = Math.cos(f * OPEN), sn = Math.sin(f * OPEN);
272
+ // (turned about its hinge, at the frame on the face it swings to, toward that face; it runs from its hinge's jamb
273
+ // to the other: along the wall, or back along it, `m`)
274
+ const hs = m > 0 ? o.s0 + FRAME : o.s1 - FRAME, hk = f * half, c = Math.cos(m * f * OPEN), sn = Math.sin(m * f * OPEN);
274
275
  const R = (s: number, k: number): XY => P(hs + (s - hs) * c - (k - hk) * sn, hk + (s - hs) * sn + (k - hk) * c);
275
- box([R(hs, hk), R(hs + W, hk), R(hs + W, hk - f * LEAF), R(hs, hk - f * LEAF)], 0.5, 0.5 + H, 'door');
276
+ box([R(hs, hk), R(hs + m * W, hk), R(hs + m * W, hk - f * LEAF), R(hs, hk - f * LEAF)], 0.5, 0.5 + H, 'door');
276
277
  }
277
278
  return b;
278
279
  }