@reekon-tools/boldr-cad 1.4.0 → 1.6.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 (311) hide show
  1. package/README.md +13 -3
  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 +6 -8
  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 +255 -181
  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 +4 -3
  21. package/dist/editor/keys.js.map +1 -1
  22. package/dist/editor/laser.d.ts +4 -2
  23. package/dist/editor/laser.js +4 -2
  24. package/dist/editor/laser.js.map +1 -1
  25. package/dist/editor/linkActions.d.ts +49 -12
  26. package/dist/editor/linkActions.js +164 -35
  27. package/dist/editor/linkActions.js.map +1 -1
  28. package/dist/editor/links.d.ts +13 -0
  29. package/dist/editor/links.js +7 -1
  30. package/dist/editor/links.js.map +1 -1
  31. package/dist/editor/mode.d.ts +2 -1
  32. package/dist/editor/mode.js +8 -5
  33. package/dist/editor/mode.js.map +1 -1
  34. package/dist/editor/project.js +1 -2
  35. package/dist/editor/project.js.map +1 -1
  36. package/dist/editor/scaleGate.d.ts +13 -0
  37. package/dist/editor/scaleGate.js +27 -0
  38. package/dist/editor/scaleGate.js.map +1 -0
  39. package/dist/editor/selection.js +13 -9
  40. package/dist/editor/selection.js.map +1 -1
  41. package/dist/editor/tooltips.js +10 -0
  42. package/dist/editor/tooltips.js.map +1 -1
  43. package/dist/editor/view3d.d.ts +4 -1
  44. package/dist/editor/view3d.js +6 -2
  45. package/dist/editor/view3d.js.map +1 -1
  46. package/dist/export/pdf.d.ts +5 -2
  47. package/dist/export/pdf.js +12 -8
  48. package/dist/export/pdf.js.map +1 -1
  49. package/dist/export/plan.d.ts +1 -1
  50. package/dist/export/plan.js +14 -9
  51. package/dist/export/plan.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/model/constraints.js +2 -0
  56. package/dist/model/constraints.js.map +1 -1
  57. package/dist/model/demo.d.ts +1 -0
  58. package/dist/model/demo.js +2 -1
  59. package/dist/model/demo.js.map +1 -1
  60. package/dist/model/elevation.d.ts +1 -1
  61. package/dist/model/elevation.js +3 -3
  62. package/dist/model/elevation.js.map +1 -1
  63. package/dist/model/geom.d.ts +2 -0
  64. package/dist/model/geom.js +11 -0
  65. package/dist/model/geom.js.map +1 -1
  66. package/dist/model/history.js +4 -1
  67. package/dist/model/history.js.map +1 -1
  68. package/dist/model/links.d.ts +10 -2
  69. package/dist/model/links.js +15 -3
  70. package/dist/model/links.js.map +1 -1
  71. package/dist/model/measurement.d.ts +2 -2
  72. package/dist/model/measurement.js +3 -3
  73. package/dist/model/measurement.js.map +1 -1
  74. package/dist/model/openings.d.ts +10 -0
  75. package/dist/model/openings.js +18 -0
  76. package/dist/model/openings.js.map +1 -1
  77. package/dist/model/planGeom.d.ts +37 -2
  78. package/dist/model/planGeom.js +72 -9
  79. package/dist/model/planGeom.js.map +1 -1
  80. package/dist/model/project.js +5 -2
  81. package/dist/model/project.js.map +1 -1
  82. package/dist/model/rooms.d.ts +18 -5
  83. package/dist/model/rooms.js +178 -73
  84. package/dist/model/rooms.js.map +1 -1
  85. package/dist/model/scale.d.ts +30 -0
  86. package/dist/model/scale.js +123 -0
  87. package/dist/model/scale.js.map +1 -0
  88. package/dist/model/settings.d.ts +0 -1
  89. package/dist/model/settings.js +1 -1
  90. package/dist/model/settings.js.map +1 -1
  91. package/dist/model/sketch.d.ts +26 -2
  92. package/dist/model/sketch.js +77 -22
  93. package/dist/model/sketch.js.map +1 -1
  94. package/dist/model/stroke.js +2 -2
  95. package/dist/model/trim.d.ts +54 -0
  96. package/dist/model/trim.js +219 -0
  97. package/dist/model/trim.js.map +1 -0
  98. package/dist/model/types.d.ts +62 -9
  99. package/dist/react/BottomBar.d.ts +2 -1
  100. package/dist/react/BottomBar.js +144 -46
  101. package/dist/react/BottomBar.js.map +1 -1
  102. package/dist/react/CadEditor.js +18 -9
  103. package/dist/react/CadEditor.js.map +1 -1
  104. package/dist/react/Drawer.js +6 -4
  105. package/dist/react/Drawer.js.map +1 -1
  106. package/dist/react/Duck.js.map +1 -1
  107. package/dist/react/KeypadSheet.js.map +1 -1
  108. package/dist/react/MarkupPanel.js +1 -1
  109. package/dist/react/PlanCanvas.native.js +54 -7
  110. package/dist/react/PlanCanvas.native.js.map +1 -1
  111. package/dist/react/PlanCanvas.web.js +17 -5
  112. package/dist/react/PlanCanvas.web.js.map +1 -1
  113. package/dist/react/Popovers.js +14 -3
  114. package/dist/react/Popovers.js.map +1 -1
  115. package/dist/react/Rail.js +1 -1
  116. package/dist/react/ShapeMenu.js +3 -2
  117. package/dist/react/ShapeMenu.js.map +1 -1
  118. package/dist/react/Sheet.d.ts +2 -2
  119. package/dist/react/Sheet.js.map +1 -1
  120. package/dist/react/ToolTip.js +3 -2
  121. package/dist/react/ToolTip.js.map +1 -1
  122. package/dist/react/TopBar.js +4 -3
  123. package/dist/react/TopBar.js.map +1 -1
  124. package/dist/react/barScroll.d.ts +23 -0
  125. package/dist/react/barScroll.js +47 -0
  126. package/dist/react/barScroll.js.map +1 -0
  127. package/dist/react/corners.d.ts +6 -2
  128. package/dist/react/corners.js.map +1 -1
  129. package/dist/react/editorBox.d.ts +8 -0
  130. package/dist/react/editorBox.js +10 -0
  131. package/dist/react/editorBox.js.map +1 -0
  132. package/dist/react/host.d.ts +1 -1
  133. package/dist/react/host.js +1 -1
  134. package/dist/react/host.js.map +1 -1
  135. package/dist/react/motion.d.ts +1 -1
  136. package/dist/react/motion.js +1 -1
  137. package/dist/react/motion.js.map +1 -1
  138. package/dist/react/pageLock.native.d.ts +2 -0
  139. package/dist/react/pageLock.native.js +2 -0
  140. package/dist/react/pageLock.native.js.map +1 -1
  141. package/dist/react/pageLock.web.d.ts +6 -0
  142. package/dist/react/pageLock.web.js +12 -3
  143. package/dist/react/pageLock.web.js.map +1 -1
  144. package/dist/react/ports.js +8 -3
  145. package/dist/react/ports.js.map +1 -1
  146. package/dist/react/saving.d.ts +7 -1
  147. package/dist/react/saving.js +10 -2
  148. package/dist/react/saving.js.map +1 -1
  149. package/dist/react/store.d.ts +5 -2
  150. package/dist/react/store.js +4 -1
  151. package/dist/react/store.js.map +1 -1
  152. package/dist/render/draw.d.ts +21 -4
  153. package/dist/render/draw.js +203 -151
  154. package/dist/render/draw.js.map +1 -1
  155. package/dist/render/elevation.js +3 -3
  156. package/dist/render/elevation.js.map +1 -1
  157. package/dist/render/figmaIcons.d.ts +6 -0
  158. package/dist/render/figmaIcons.js +16 -1
  159. package/dist/render/figmaIcons.js.map +1 -1
  160. package/dist/render/hit.d.ts +8 -1
  161. package/dist/render/hit.js +23 -4
  162. package/dist/render/hit.js.map +1 -1
  163. package/dist/render/icons.js +27 -20
  164. package/dist/render/icons.js.map +1 -1
  165. package/dist/render/lens.d.ts +27 -0
  166. package/dist/render/lens.js +41 -0
  167. package/dist/render/lens.js.map +1 -0
  168. package/dist/render/linkTile.d.ts +20 -2
  169. package/dist/render/linkTile.js +476 -246
  170. package/dist/render/linkTile.js.map +1 -1
  171. package/dist/render/snap.d.ts +10 -1
  172. package/dist/render/snap.js +87 -15
  173. package/dist/render/snap.js.map +1 -1
  174. package/dist/render/svgPath.d.ts +29 -0
  175. package/dist/render/svgPath.js +114 -0
  176. package/dist/render/svgPath.js.map +1 -0
  177. package/dist/render/tileIcons.d.ts +18 -0
  178. package/dist/render/tileIcons.js +67 -0
  179. package/dist/render/tileIcons.js.map +1 -0
  180. package/dist/render/view.d.ts +18 -1
  181. package/dist/render/view.js +38 -6
  182. package/dist/render/view.js.map +1 -1
  183. package/dist/skia/SkiaDrawCtx.d.ts +8 -0
  184. package/dist/skia/SkiaDrawCtx.js +27 -5
  185. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  186. package/dist/solver/equations.d.ts +6 -0
  187. package/dist/solver/equations.js +53 -22
  188. package/dist/solver/equations.js.map +1 -1
  189. package/dist/solver/gauss-newton.js +6 -1
  190. package/dist/solver/gauss-newton.js.map +1 -1
  191. package/dist/tools/annotate.d.ts +0 -2
  192. package/dist/tools/annotate.js +1 -22
  193. package/dist/tools/annotate.js.map +1 -1
  194. package/dist/tools/chamfer.js +4 -0
  195. package/dist/tools/chamfer.js.map +1 -1
  196. package/dist/tools/dim.d.ts +3 -3
  197. package/dist/tools/dim.js +26 -65
  198. package/dist/tools/dim.js.map +1 -1
  199. package/dist/tools/fillet.js +4 -0
  200. package/dist/tools/fillet.js.map +1 -1
  201. package/dist/tools/freehand.js +1 -4
  202. package/dist/tools/freehand.js.map +1 -1
  203. package/dist/tools/line.js +15 -11
  204. package/dist/tools/line.js.map +1 -1
  205. package/dist/tools/modify.d.ts +32 -4
  206. package/dist/tools/modify.js +253 -98
  207. package/dist/tools/modify.js.map +1 -1
  208. package/dist/tools/registry.d.ts +7 -0
  209. package/dist/tools/registry.js +14 -1
  210. package/dist/tools/registry.js.map +1 -1
  211. package/dist/tools/select.d.ts +30 -8
  212. package/dist/tools/select.js +165 -34
  213. package/dist/tools/select.js.map +1 -1
  214. package/dist/tools/shapes.d.ts +1 -1
  215. package/dist/tools/shapes.js +12 -5
  216. package/dist/tools/shapes.js.map +1 -1
  217. package/dist/view3d/build.js +9 -8
  218. package/dist/view3d/build.js.map +1 -1
  219. package/package.json +9 -9
  220. package/src/app/edit.ts +25 -1
  221. package/src/app/state.ts +6 -8
  222. package/src/data/sync.ts +7 -2
  223. package/src/editor/actions.ts +213 -122
  224. package/src/editor/elevation.ts +13 -16
  225. package/src/editor/gestures.ts +83 -13
  226. package/src/editor/keys.ts +3 -2
  227. package/src/editor/laser.ts +7 -4
  228. package/src/editor/linkActions.ts +169 -31
  229. package/src/editor/links.ts +17 -1
  230. package/src/editor/mode.ts +8 -5
  231. package/src/editor/project.ts +1 -2
  232. package/src/editor/scaleGate.ts +32 -0
  233. package/src/editor/selection.ts +13 -7
  234. package/src/editor/tooltips.ts +9 -0
  235. package/src/editor/view3d.ts +6 -2
  236. package/src/export/pdf.ts +7 -4
  237. package/src/export/plan.ts +14 -10
  238. package/src/index.ts +1 -0
  239. package/src/model/constraints.ts +1 -0
  240. package/src/model/demo.ts +2 -1
  241. package/src/model/elevation.ts +4 -4
  242. package/src/model/geom.ts +10 -0
  243. package/src/model/history.ts +4 -1
  244. package/src/model/links.ts +14 -3
  245. package/src/model/measurement.ts +3 -3
  246. package/src/model/openings.ts +18 -0
  247. package/src/model/planGeom.ts +70 -10
  248. package/src/model/project.ts +6 -2
  249. package/src/model/rooms.ts +151 -63
  250. package/src/model/scale.ts +107 -0
  251. package/src/model/settings.ts +1 -2
  252. package/src/model/sketch.ts +62 -11
  253. package/src/model/stroke.ts +2 -2
  254. package/src/model/trim.ts +188 -0
  255. package/src/model/types.ts +57 -9
  256. package/src/react/BottomBar.tsx +177 -52
  257. package/src/react/CadEditor.tsx +19 -9
  258. package/src/react/Drawer.tsx +17 -12
  259. package/src/react/Duck.tsx +3 -3
  260. package/src/react/KeypadSheet.tsx +2 -2
  261. package/src/react/MarkupPanel.tsx +1 -1
  262. package/src/react/PlanCanvas.native.tsx +60 -6
  263. package/src/react/PlanCanvas.web.tsx +17 -5
  264. package/src/react/Popovers.tsx +24 -9
  265. package/src/react/Rail.tsx +1 -1
  266. package/src/react/ShapeMenu.tsx +3 -2
  267. package/src/react/Sheet.tsx +3 -3
  268. package/src/react/ToolTip.tsx +3 -2
  269. package/src/react/TopBar.tsx +4 -3
  270. package/src/react/barScroll.ts +57 -0
  271. package/src/react/corners.ts +6 -3
  272. package/src/react/editorBox.ts +20 -0
  273. package/src/react/host.ts +1 -1
  274. package/src/react/motion.tsx +6 -6
  275. package/src/react/pageLock.native.ts +2 -0
  276. package/src/react/pageLock.web.ts +12 -3
  277. package/src/react/ports.ts +7 -2
  278. package/src/react/saving.ts +10 -2
  279. package/src/react/store.ts +5 -2
  280. package/src/render/draw.ts +169 -118
  281. package/src/render/elevation.ts +5 -4
  282. package/src/render/figmaIcons.ts +17 -1
  283. package/src/render/hit.ts +21 -4
  284. package/src/render/icons.ts +27 -20
  285. package/src/render/lens.ts +52 -0
  286. package/src/render/linkTile.ts +436 -190
  287. package/src/render/snap.ts +87 -17
  288. package/src/render/svgPath.ts +95 -0
  289. package/src/render/tileIcons.ts +75 -0
  290. package/src/render/view.ts +43 -7
  291. package/src/skia/SkiaDrawCtx.ts +26 -4
  292. package/src/solver/equations.ts +52 -22
  293. package/src/solver/gauss-newton.ts +4 -1
  294. package/src/styles.css +3 -2
  295. package/src/tools/annotate.ts +1 -18
  296. package/src/tools/chamfer.ts +3 -0
  297. package/src/tools/dim.ts +26 -52
  298. package/src/tools/fillet.ts +3 -0
  299. package/src/tools/freehand.ts +1 -3
  300. package/src/tools/line.ts +12 -11
  301. package/src/tools/modify.ts +210 -70
  302. package/src/tools/registry.ts +18 -1
  303. package/src/tools/select.ts +144 -36
  304. package/src/tools/shapes.ts +9 -4
  305. package/src/ui/ctx.ts +4 -3
  306. package/src/ui/drawer.ts +8 -2
  307. package/src/view3d/build.ts +11 -10
  308. package/dist/react/LaserTray.d.ts +0 -4
  309. package/dist/react/LaserTray.js +0 -26
  310. package/dist/react/LaserTray.js.map +0 -1
  311. package/src/react/LaserTray.tsx +0 -42
@@ -20,10 +20,42 @@ type DFn = (id: string) => number[];
20
20
  type RVFn = (X: Vec, cid: string) => number;
21
21
  type RDFn = (cid: string) => number[];
22
22
 
23
+ /**
24
+ * The side of its line a tangent keeps the curve's centre on (1: left of the line's a → b): the side it was made
25
+ * tangent from (`sign`: the fillet tool and the Tangent action set it), or, for one saved before tangents had a side,
26
+ * the side it's on now.
27
+ */
28
+ export function tanSide(sk: Sketch, c: Constraint): 1 | -1 {
29
+ if (c.sign) return c.sign < 0 ? -1 : 1;
30
+ const l = c.l ? sk.lines[c.l] : undefined, ci = curveInfo(sk, c.c);
31
+ const A = l && sk.points[l.a], B = l && sk.points[l.b], o = ci && sk.points[ci.o.c];
32
+ if (!A || !B || !o) return 1;
33
+ return (B.x - A.x) * (o.y - A.y) - (B.y - A.y) * (o.x - A.x) < 0 ? -1 : 1;
34
+ }
35
+
23
36
  /** Residual equations for one constraint, each with the variable indices it depends on. */
24
37
  export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD: RDFn): [number[], EqFn][] {
25
38
  const L = (id: string | undefined) => (id ? sk.lines[id] : undefined);
26
39
  const out: [number[], EqFn][] = [];
40
+ /**
41
+ * Line `id`'s length, and what it depends on — for a wall's `face`, along that face corner to corner: the walls at its
42
+ * ends cut it short (an inside corner) or carry it on (an outside one), so they're part of it too.
43
+ */
44
+ const len = (id: string, face?: number): { deps: number[]; f: EqFn } => {
45
+ const l = sk.lines[id];
46
+ if (face && l.kind === 'wall') {
47
+ const ends = faceEnds(sk, id), side = face < 0 ? -1 : 1;
48
+ return {
49
+ deps: [...D(l.a), ...D(l.b), ...[...ends.a, ...ends.b].flatMap(e => D(e.far ?? e.arc!.c))],
50
+ f: X => {
51
+ const at = (p: string) => { const q = P(X, p); return { x: q[0], y: q[1] }; };
52
+ const fs = faceSpan(at, at(l.a), at(l.b), l.thick, side, ends);
53
+ return fs ? fs.s1 - fs.s0 : 0;
54
+ },
55
+ };
56
+ }
57
+ return { deps: [...D(l.a), ...D(l.b)], f: X => { const a = P(X, l.a), b = P(X, l.b); return hyp(b[0] - a[0], b[1] - a[1]); } };
58
+ };
27
59
  const unit = (a: [number, number], b: [number, number]): [number, number] => {
28
60
  const x = b[0] - a[0], y = b[1] - a[1], n = hyp(x, y) || 1e-12;
29
61
  return [x / n, y / n];
@@ -49,24 +81,27 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
49
81
  }
50
82
  case 'tan': {
51
83
  // line tangent to a circle/arc: the centre's distance from the (infinite) line equals the radius,
52
- // on the side the centre started on (sign), so it never flips across the line
84
+ // on the side the centre started on (tanSide), so it never flips across the line
53
85
  const l = L(c.l), r = rad(c.c);
54
86
  if (!l || !r) break;
87
+ const s = tanSide(sk, c);
55
88
  // an arc that ends on one of the line's ends (a fillet, a smooth join): tangent there is its radius to that end
56
- // standing square to the line. (The distance form has no slope at that point, so the solver couldn't tell a
57
- // second such tangent from one that's already true.)
89
+ // standing square to the line, from its own side. (The distance form has no slope at that point, so the solver
90
+ // couldn't tell a second such tangent from one that's already true.) Square alone is true of the arc's mirror
91
+ // image across the line too, which a fast drag could land on — and the arc, drawn counter-clockwise from its
92
+ // start to its end, then went the long way round. So it's the angle from the line's normal on the centre's side
93
+ // to the radius: a slope at the answer, as square had, and half a turn out on the wrong side, never zero.
58
94
  const ci = curveInfo(sk, c.c);
59
95
  const joint = ci && ci.kind === 'arc' ? [ci.o.a, ci.o.b].find(p => p === l.a || p === l.b) : undefined;
60
96
  if (ci && joint) {
61
97
  const o = ci.o.c;
62
98
  out.push([[...D(l.a), ...D(l.b), ...D(o), ...D(joint)], X => {
63
99
  const a = P(X, l.a), b = P(X, l.b), m = P(X, o), e = P(X, joint);
64
- const ux = b[0] - a[0], uy = b[1] - a[1], vx = e[0] - m[0], vy = e[1] - m[1];
65
- return (ux * vx + uy * vy) / ((hyp(ux, uy) || 1e-12) * (hyp(vx, vy) || 1e-12));
100
+ const ux = b[0] - a[0], uy = b[1] - a[1], wx = m[0] - e[0], wy = m[1] - e[1];
101
+ return Math.atan2(ux * wx + uy * wy, s * (ux * wy - uy * wx));
66
102
  }]);
67
103
  break;
68
104
  }
69
- const s = c.sign || 1;
70
105
  out.push([[...D(l.a), ...D(l.b), ...D(r.c), ...r.deps], X => {
71
106
  const o = P(X, r.c), a = P(X, l.a), b = P(X, l.b), n = hyp(b[0] - a[0], b[1] - a[1]) || 1e-12;
72
107
  return (s * ((b[0] - a[0]) * (o[1] - a[1]) - (b[1] - a[1]) * (o[0] - a[0]))) / n - r.f(X);
@@ -86,12 +121,10 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
86
121
  break;
87
122
  }
88
123
  case 'eq': {
89
- const l1 = L(c.l1), l2 = L(c.l2);
90
- if (!l1 || !l2) break;
91
- out.push([[...D(l1.a), ...D(l1.b), ...D(l2.a), ...D(l2.b)], X => {
92
- const a = P(X, l1.a), b = P(X, l1.b), p = P(X, l2.a), q = P(X, l2.b);
93
- return hyp(b[0] - a[0], b[1] - a[1]) - hyp(q[0] - p[0], q[1] - p[1]);
94
- }]);
124
+ if (!L(c.l1) || !L(c.l2)) break;
125
+ // (a wall's edge picked: that face's length — faces)
126
+ const a = len(c.l1!, c.faces?.[0]), b = len(c.l2!, c.faces?.[1]);
127
+ out.push([[...a.deps, ...b.deps], X => a.f(X) - b.f(X)]);
95
128
  break;
96
129
  }
97
130
  case 'eqr': {
@@ -111,9 +144,12 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
111
144
  case 'online': {
112
145
  const l = L(c.l);
113
146
  if (!l) break;
147
+ // on one face of a wall: half its thickness to that side of the centreline — read when solved, so a wall made
148
+ // thicker carries what's on its face with it
149
+ const faceOff = () => (c.face && l.kind === 'wall' ? (c.face * l.thick) / 2 : 0);
114
150
  out.push([[...D(c.p!), ...D(l.a), ...D(l.b)], X => {
115
151
  const p = P(X, c.p!), a = P(X, l.a), b = P(X, l.b), dx = b[0] - a[0], dy = b[1] - a[1], n = hyp(dx, dy) || 1e-12;
116
- return ((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / n;
152
+ return ((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / n + faceOff();
117
153
  }]);
118
154
  break;
119
155
  }
@@ -126,15 +162,9 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
126
162
  case 'dist': {
127
163
  const fw = c.face && c.line && (!c.kind || c.kind === 'al') ? L(c.line) : undefined;
128
164
  if (fw && fw.kind === 'wall') {
129
- // along one face of a wall, corner to corner: the walls at its ends cut it short (an inside corner) or carry it
130
- // on (an outside one), so they're part of it too
131
- const ends = faceEnds(sk, c.line!), side = c.face!;
132
- const deps = [...D(fw.a), ...D(fw.b), ...[...ends.a, ...ends.b].flatMap(e => D(e.far ?? e.arc!.c))];
133
- out.push([deps, X => {
134
- const at = (id: string) => { const q = P(X, id); return { x: q[0], y: q[1] }; };
135
- const f = faceSpan(at, at(fw.a), at(fw.b), fw.thick, side, ends);
136
- return (f ? f.s1 - f.s0 : 0) - c.value!;
137
- }]);
165
+ // along one face of a wall, corner to corner
166
+ const f = len(c.line!, c.face);
167
+ out.push([f.deps, X => f.f(X) - c.value!]);
138
168
  break;
139
169
  }
140
170
  const deps = [...D(c.a!), ...D(c.b!)], s = c.sign || 1;
@@ -1,7 +1,7 @@
1
1
  import { isDim, measure, relatedCons } from '../model/constraints';
2
2
  import { addCons, restorePos, snapshotPos } from '../model/sketch';
3
3
  import type { Analysis, Constraint, Sketch, XY } from '../model/types';
4
- import { buildSys, getX, type System, type Vec } from './equations';
4
+ import { buildSys, getX, tanSide, type System, type Vec } from './equations';
5
5
  import { blocks, compactRows, envAt, envelopeOf, jac, rref, rrefTol, solveDense, solveEnvelope, sq, type Envelope, type Row } from './linalg';
6
6
  import type { AddResult, SolveResult, Solver } from './types';
7
7
 
@@ -180,6 +180,9 @@ export class GaussNewtonSolver implements Solver {
180
180
  }
181
181
 
182
182
  drag(sk: Sketch, ids: string[], targets: XY[]): boolean {
183
+ // (a tangent saved before tangents kept a side keeps the one it's on now, read before the dragged points jump to
184
+ // the pointer: read after, a moved end of its line could put it on the wrong one)
185
+ for (const c of Object.values(sk.cons)) if (c.type === 'tan' && !c.sign) c.sign = tanSide(sk, c);
183
186
  const snap = snapshotPos(sk);
184
187
  ids.forEach((id, i) => { sk.points[id].x = targets[i].x; sk.points[id].y = targets[i].y; });
185
188
  const s = this.solve(sk, ids);
package/src/styles.css CHANGED
@@ -96,7 +96,7 @@ input{font:inherit}
96
96
 
97
97
  /* bottom contextual bar */
98
98
  .bottom{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;align-items:center;gap:12px;z-index:24;max-width:calc(100% - 180px)}
99
- .ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:33px;padding:6px;overflow-x:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26);max-width:100%}
99
+ .ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:30px;min-height:60px;padding:3px 6px;overflow-x:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26);max-width:100%}
100
100
  .ctx::-webkit-scrollbar{display:none}
101
101
  .cb{min-width:58px;height:54px;border-radius:27px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#fff;font-size:10.5px;font-weight:500;padding:0 8px;flex-shrink:0;white-space:nowrap;transition:background .12s}
102
102
  .cb:hover{background:rgba(255,255,255,.08)}
@@ -104,9 +104,10 @@ input{font:inherit}
104
104
  .cb[disabled]{opacity:.35;pointer-events:none}
105
105
  .cb.warn{color:#FFB4AE}
106
106
  .cb.locked{color:var(--red)}
107
+ .cb.lead{min-width:48px;width:48px;height:48px;border-radius:50%;padding:0}
107
108
  .ctx .sep{width:1px;height:32px;background:var(--g3);margin:0 4px;flex-shrink:0}
108
109
  .ctx .lbl{font-size:12px;color:var(--lg1);padding:0 10px;white-space:nowrap;flex-shrink:0}
109
- .addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px;margin-right:3px}
110
+ .addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px}
110
111
  /* the + stays pinned at the end of the bar while the other actions scroll under it (phone) */
111
112
  .ctx .addbtn{position:sticky;right:0;z-index:1;box-shadow:0 0 0 6px var(--g2),-16px 0 10px -2px var(--g2)}
112
113
  .ctx:has(.addbtn){padding-right:6px}
@@ -1,10 +1,9 @@
1
1
  import { targetImage } from '../editor/project';
2
2
  import { imageById } from '../model/images';
3
- import { strokeToLines } from './freehand';
4
3
  import { afterEdit } from '../app/edit';
5
4
  import { ed } from '../app/state';
6
5
  import { hyp } from '../model/geom';
7
- import { dropHistory, pushHistory } from '../model/history';
6
+ import { pushHistory } from '../model/history';
8
7
  import { uid } from '../model/ids';
9
8
  import { page, SK } from '../model/store';
10
9
  import { dashFor, DEFAULT_NOTE_WIDTH, NOTE_TEXT_IN, noteLook, shapeOutline } from '../model/markup';
@@ -16,7 +15,6 @@ import { fitView, mainT, requestDraw, requestOverlayDraw, S, s2w, underlayH } fr
16
15
  import { tools, type PEv, type Tool } from './registry';
17
16
  import { hitAny } from '../render/hit';
18
17
  import { selectTool } from './select';
19
- import { setMode } from '../editor/mode';
20
18
  import { hideNoteInput, openKeypad, setCursor, showNoteInput, toast, updateHint } from '../editor/ports';
21
19
  import { selectionHint } from '../editor/selection';
22
20
 
@@ -194,21 +192,6 @@ export const calibTool = {
194
192
  hint() { return ['Tap the first end of something you know the length of.', 'Tap the other end.'][this.st.pts.length] || ''; },
195
193
  };
196
194
 
197
- /** Turn a pen stroke into straight lines, squaring near-horizontal/vertical segments. */
198
- export function convertNote(id: string): void {
199
- const sk = SK();
200
- const n = sk.notes.find(n => n.id === id);
201
- if (!n || !n.pts) return;
202
- pushHistory();
203
- const r = strokeToLines(n.pts.map(p => ({ x: p[0], y: p[1] })));
204
- if (!r) { dropHistory(); toast('That stroke is too small to turn into lines.', { warn: true }); return; }
205
- sk.notes = sk.notes.filter(x => x.id !== id);
206
- setMode('draft');
207
- ed.sel = r.lines.map(id => ({ type: 'line' as const, id }));
208
- afterEdit();
209
- toast('Turned into ' + r.lines.length + ' line' + (r.lines.length > 1 ? 's' : '') + '. Select a line and tap + to add its length.');
210
- }
211
-
212
195
  /**
213
196
  * Lets a markup drawing tool (pen, arrow, cloud) reshape what was just drawn: a press on a handle of the selected
214
197
  * markup is handed to Select markup (reshape); anywhere else draws as usual.
@@ -8,6 +8,7 @@ import { requestDraw } from '../render/view';
8
8
  import { solver } from '../solver';
9
9
  import { fmtLen } from '../units';
10
10
  import { closeKeypad, openKeypad, toast, updateCtx } from '../editor/ports';
11
+ import { waitForScale } from '../editor/scaleGate';
11
12
  import { cornerAt, cutCorner } from './corner';
12
13
 
13
14
  /*
@@ -33,6 +34,8 @@ function showSide(lid: string | null): void { ed.sel = lid ? [{ type: 'line', id
33
34
  export function chamferPrompt(pid: string): void {
34
35
  const sk = SK(), k = cornerAt(sk, pid);
35
36
  if (!k) { toast('Select a corner where two lines meet to cut it off.', { warn: true }); return; }
37
+ // (a distance typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
38
+ if (waitForScale()) return;
36
39
  const most = Math.min(k.len1, k.len2), start = Math.round(Math.min(6, most * 0.5) * 16) / 16;
37
40
  openKeypad({
38
41
  title: 'Chamfer', sub: 'Back from the corner along each side — up to ' + fmtLen(most), initial: fmtLen(start), value: start, selectAll: true, laser: true,
package/src/tools/dim.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { afterEdit, holdGroups, tryAddConstraint } from '../app/edit';
2
2
  import { ed } from '../app/state';
3
3
  import { measure } from '../model/constraints';
4
- import { hyp, norm2, pointInPoly, projT } from '../model/geom';
4
+ import { hyp, norm2, projT } from '../model/geom';
5
5
  import { dropHistory, pushHistory } from '../model/history';
6
- import { addCons, lengthDimOf, lineLen, roomValid } from '../model/sketch';
6
+ import { lengthDimOf, lineLen } from '../model/sketch';
7
7
  import { SK } from '../model/store';
8
8
  import type { Constraint, LinkTarget, OpeningEdge, Sketch, WallFace, XY } from '../model/types';
9
9
  import { askLinkedChange, changesValue } from '../editor/linkWrite';
@@ -25,6 +25,7 @@ import type { PEv } from './registry';
25
25
  import { keypadFresh, keypadOpen, keypadSetReading } from '../editor/keypad';
26
26
  import { cornerLines } from '../editor/selection';
27
27
  import { closeKeypad, kpTyped, openKeypad, showConflict, toast, updateCtx, updateHint, type KeypadSpec } from '../editor/ports';
28
+ import { noScaleYet, pendingLabel, startingValue } from '../editor/scaleGate';
28
29
 
29
30
  export const dimTool = {
30
31
  /** angle: the first pick is a corner whose angle keypad is open (the next tap closes it) */
@@ -66,7 +67,7 @@ export const dimTool = {
66
67
  move(ev: PEv) { this.hover(ev); },
67
68
  up(ev: PEv) {
68
69
  const h = pickForDim(ev);
69
- const print = ed.dimLayer === 'print', st = this.st;
70
+ const st = this.st;
70
71
  // a corner's angle keypad is open: the next tap closes it and goes on (a second point measures from the corner)
71
72
  if (st.angle) { st.angle = false; if (keypadOpen()) closeKeypad(); }
72
73
  if (!h || h.type === 'room') { st.first = null; ed.pickedAnchor = null; ed.sel = []; ed.preview = null; updateHint(); requestDraw(); return; }
@@ -74,7 +75,6 @@ export const dimTool = {
74
75
  const picked = ed.pickedAnchor;
75
76
  if (h.type === 'oanchor') {
76
77
  const edge = h.part as OpeningEdge;
77
- if (print) { toast('Print dimensions can’t attach to doors or windows yet. Switch to Driving in the bar below.', { warn: true }); return; }
78
78
  if (st.first) {
79
79
  const p = st.first; st.first = null; ed.sel = [];
80
80
  addOpeningDim(h.id, h.weak ? nearerEdge(SK(), h.id, p) : edge, { pt: p });
@@ -101,10 +101,10 @@ export const dimTool = {
101
101
  if (!st.first) {
102
102
  st.first = h.id; ed.sel = [{ type: 'point', id: h.id }];
103
103
  // a corner: its angle straight away (close the keypad to measure from the corner to another point instead)
104
- if (!print) st.angle = cornerAngle(h.id, () => { this.st.first = null; this.st.angle = false; ed.sel = []; updateHint(); requestDraw(); }, true);
104
+ st.angle = cornerAngle(h.id, () => { this.st.first = null; this.st.angle = false; ed.sel = []; updateHint(); requestDraw(); }, true);
105
105
  updateHint(); requestDraw(); return;
106
106
  }
107
- if (h.id !== st.first) dimPoints(st.first, h.id, print);
107
+ if (h.id !== st.first) dimPoints(st.first, h.id);
108
108
  st.first = null; updateHint();
109
109
  return;
110
110
  }
@@ -113,14 +113,14 @@ export const dimTool = {
113
113
  if (h.type === 'wface') {
114
114
  const [lid, side] = edgeOf(h.id);
115
115
  if (!SK().lines[lid]) { dimCurve(lid, side); return; }
116
- if (st.first) { const p = st.first; st.first = null; if (print) dimPointLine(p, lid); else dimEdgePoint(lid, side, p); updateHint(); return; }
117
- dimLine(lid, print, side);
116
+ if (st.first) { const p = st.first; st.first = null; dimEdgePoint(lid, side, p); updateHint(); return; }
117
+ dimLine(lid, side);
118
118
  return;
119
119
  }
120
120
  if (h.type === 'line') {
121
121
  if (h.side) ed.wallTap = { id: h.id, side: h.side };
122
122
  if (st.first) { dimPointLine(st.first, h.id); st.first = null; updateHint(); return; }
123
- dimLine(h.id, print);
123
+ dimLine(h.id);
124
124
  return;
125
125
  }
126
126
  if (h.type === 'circle' || h.type === 'arc') {
@@ -131,7 +131,7 @@ export const dimTool = {
131
131
  hint() {
132
132
  if (ed.pickedAnchor) return 'Tap a corner or point to measure from. The door or window locks at that distance.';
133
133
  if (this.st.first) return 'Tap a second point, a door or window edge, or a line for the offset distance.';
134
- return ed.dimLayer === 'print' ? 'Print dimensions are labels for the crew — they never move the drawing. Tap a line or two points.' : 'Tap a line, a circle, or two points — door and window edges count as points. Then type the value.';
134
+ return 'Tap a line, a circle, or two points — door and window edges count as points. Then type the value.';
135
135
  },
136
136
  };
137
137
 
@@ -153,34 +153,15 @@ function pickForDim(ev: PEv): Hit | null {
153
153
  return a ? { type: 'oanchor', id: a.id, part: a.edge, weak: true } : h;
154
154
  }
155
155
 
156
- /** +1 or -1: the side of a line that faces away from any room (for print dimension offsets). */
157
- function sideAwayFromRooms(lid: string): number {
158
- const sk = SK(), l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
159
- const n = norm2(-(b.y - a.y), b.x - a.x);
160
- const m = { x: (a.x + b.x) / 2 + n[0] * 12, y: (a.y + b.y) / 2 + n[1] * 12 };
161
- for (const id in sk.rooms) { const r = sk.rooms[id]; if (roomValid(sk, r) && pointInPoly(m, r.loop.map(p => sk.points[p]))) return -1; }
162
- return 1;
163
- }
164
-
165
156
  /**
166
157
  * Tap on a line (or its + chip): open the keypad for its length, or edit the existing length. A wall's is measured on
167
158
  * `side`, the edge picked — or, picked whole, on the side a space is on (the side it was tapped on, between two), and
168
159
  * along its centerline where there's no space; the keypad can switch it.
169
160
  */
170
- export function dimLine(lid: string, print: boolean, side?: 1 | -1): void {
161
+ export function dimLine(lid: string, side?: 1 | -1): void {
171
162
  const sk = SK(), l = sk.lines[lid];
172
163
  if (!l) return;
173
164
  const wall = faced(sk, lid);
174
- if (print) {
175
- pushHistory();
176
- // (on a wall's edge: that face's length, labelled on that side)
177
- const f = wall ? side : undefined;
178
- const off = (36 / V().s) * (f ?? sideAwayFromRooms(lid));
179
- addCons(sk, { type: 'dist', a: l.a, b: l.b, kind: 'al', value: f ? faceLength(sk, lid, f) : lineLen(sk, l), line: lid, driving: false, layer: 'print', off, ...(f ? { face: f } : {}) });
180
- afterEdit();
181
- toast('Print dimension added. Drag it to move it.');
182
- return;
183
- }
184
165
  const ex = lengthDimOf(sk, lid);
185
166
  if (ex) { editDim(ex, undefined, false, wall && side ? { face: side } : undefined); return; }
186
167
  let face: 0 | 1 | -1 = wall ? side ?? defaultFace(sk, lid, ed.wallTap?.id === lid ? ed.wallTap.side : undefined) ?? 0 : 0;
@@ -190,10 +171,10 @@ export function dimLine(lid: string, print: boolean, side?: 1 | -1): void {
190
171
  const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(lid, face) }] : [{ type: 'line', id: lid }]; requestDraw(); };
191
172
  show();
192
173
  const spec: KeypadSpec = {
193
- title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', initial: fmtLen(len()), value: len(), selectAll: true, laser: true,
174
+ title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', ...startingValue(len()), selectAll: true, laser: true,
194
175
  extras: wall ? () => lengthChoices(sk, lid).map(ch => ({
195
176
  label: ch.label!, on: ch.face === face,
196
- onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh()) keypadSetReading(len()); },
177
+ onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh() && !noScaleYet()) keypadSetReading(len()); },
197
178
  })) : undefined,
198
179
  onClose: () => { ed.sel = was; updateCtx(); requestDraw(); },
199
180
  onDone: (v, capture) => { if (v > 0) addLength(v, capture !== undefined ? { capture } : {}); },
@@ -305,7 +286,7 @@ export function faceChoices(sk: Sketch, c: Constraint): FaceChoice[] {
305
286
  const l = sk.lines[c.l], a = sk.points[l.a], b = sk.points[l.b], H = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? 'H' : '';
306
287
  return [
307
288
  { label: 'Inside faces', short: 'Inside', icon: 'spanIn' + H, faces: k(1) },
308
- { label: 'Centerlines', short: 'Centers', icon: 'spanMid' + H, faces: [0, 0] },
289
+ { label: 'Centerlines', short: 'Center', icon: 'spanMid' + H, faces: [0, 0] },
309
290
  { label: 'Outside faces', short: 'Outside', icon: 'spanOut' + H, faces: k(-1) },
310
291
  ];
311
292
  }
@@ -408,22 +389,16 @@ export function dimEdges(items: { type: string; id: string }[], thickness: (lid:
408
389
  else if (other.type === 'line') dimTwoLines(lid, other.id, { [lid]: side });
409
390
  }
410
391
 
411
- export function dimPoints(a: string, b: string, print: boolean): void {
392
+ export function dimPoints(a: string, b: string): void {
412
393
  const sk = SK(), A = sk.points[a], B = sk.points[b];
413
394
  const L = hyp(B.x - A.x, B.y - A.y);
414
- if (print) {
415
- pushHistory();
416
- addCons(sk, { type: 'dist', a, b, kind: 'al', value: L, driving: false, layer: 'print', off: 30 / V().s });
417
- afterEdit();
418
- return;
419
- }
420
395
  ed.sel = [];
421
396
  const n = norm2(-(B.y - A.y), B.x - A.x);
422
- ed.preview = (c, T) => drawPendingDim(c, T, { from: A, to: B, a: A, b: B, nx: n[0], ny: n[1], off: 0, label: fmtLen(kpTyped() ?? L) });
397
+ ed.preview = (c, T) => drawPendingDim(c, T, { from: A, to: B, a: A, b: B, nx: n[0], ny: n[1], off: 0, label: pendingLabel(kpTyped(), L) });
423
398
  requestDraw();
424
399
  openKeypad({
425
400
  onClose: () => { ed.preview = null; requestDraw(); },
426
- title: 'Distance', initial: fmtLen(L), value: L, selectAll: true, laser: true,
401
+ title: 'Distance', ...startingValue(L), selectAll: true, laser: true,
427
402
  onDone: (v, capture) => {
428
403
  pushHistory();
429
404
  const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0, ...(capture !== undefined ? { capture } : {}) });
@@ -449,7 +424,7 @@ export function dimTwoLines(l1: string, l2: string, sides: Record<string, 0 | 1
449
424
  const kind = parallel ? 'pldist' : 'angle';
450
425
  const existing = Object.keys(sk.cons).find(id => {
451
426
  const c = sk.cons[id];
452
- if (c.type !== kind || c.layer === 'print') return false;
427
+ if (c.type !== kind) return false;
453
428
  if (kind === 'angle') return (c.l1 === l1 && c.l2 === l2) || (c.l1 === l2 && c.l2 === l1);
454
429
  return (c.l === l1 && (c.p === L2.a || c.p === L2.b)) || (c.l === l2 && (c.p === L1.a || c.p === L1.b));
455
430
  });
@@ -487,15 +462,15 @@ export function dimPointLine(p: string, lid: string, opts: { keepParallel?: stri
487
462
  applyFace(c, { faces: opts.faces ?? [faceToward(sk, lid, P), 0] });
488
463
  const choices = faceChoices(sk, c);
489
464
  ed.sel = [];
490
- ed.preview = (cx, T) => { const e = dimEnds(sk, c); if (e) drawPendingDim(cx, T, { from: e.a, to: e.b, a: e.a, b: e.b, nx: 0, ny: 0, off: 0, label: fmtLen(kpTyped() ?? measure(sk, c)) }); };
465
+ ed.preview = (cx, T) => { const e = dimEnds(sk, c); if (e) drawPendingDim(cx, T, { from: e.a, to: e.b, a: e.a, b: e.b, nx: 0, ny: 0, off: 0, label: pendingLabel(kpTyped(), measure(sk, c)) }); };
491
466
  requestDraw();
492
467
  const spec: KeypadSpec = {
493
468
  onClose: () => { ed.preview = null; requestDraw(); },
494
469
  // (which faces it's measured between: the choices beside the keys)
495
- title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', initial: fmtLen(measure(sk, c)), value: measure(sk, c), selectAll: true,
470
+ title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', ...startingValue(measure(sk, c)), selectAll: true,
496
471
  extras: choices.length ? () => choices.map(ch => ({
497
472
  label: ch.label!, on: onChoice(c, ch),
498
- onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh()) keypadSetReading(measure(sk, c)); },
473
+ onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh() && !noScaleYet()) keypadSetReading(measure(sk, c)); },
499
474
  })) : undefined,
500
475
  onDone: v => {
501
476
  pushHistory();
@@ -525,10 +500,10 @@ export function dimCurve(id: string, side?: 1 | -1): void {
525
500
  const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(id, face) }] : [{ type, id }]; requestDraw(); };
526
501
  show();
527
502
  const spec: KeypadSpec = {
528
- title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', initial: fmtLen(now()), value: now(), selectAll: true,
503
+ title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', ...startingValue(now()), selectAll: true,
529
504
  extras: wall ? () => curveChoices(sk, id).map(ch => ({
530
505
  label: ch.label!, on: ch.face === face,
531
- onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh()) keypadSetReading(now()); },
506
+ onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh() && !noScaleYet()) keypadSetReading(now()); },
532
507
  })) : undefined,
533
508
  onClose: () => { ed.sel = was; requestDraw(); },
534
509
  onDone: v => {
@@ -549,7 +524,7 @@ export function cornerAngle(pid: string, done?: () => void, modeless = false): b
549
524
  const sk = SK(), pair = cornerLines(sk, pid);
550
525
  if (!pair) return false;
551
526
  // edit the corner's existing angle dimension rather than stacking a second one
552
- const ex = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'angle' && c.layer !== 'print' && ((c.l1 === pair[0] && c.l2 === pair[1]) || (c.l1 === pair[1] && c.l2 === pair[0])); });
527
+ const ex = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'angle' && ((c.l1 === pair[0] && c.l2 === pair[1]) || (c.l1 === pair[1] && c.l2 === pair[0])); });
553
528
  if (ex) editDim(ex, done, modeless); else dimAngle(pair[0], pair[1], done, modeless);
554
529
  return true;
555
530
  }
@@ -605,7 +580,7 @@ export function editDim(id: string, done?: () => void, modeless = false, pick?:
605
580
  // (which faces it's measured on: the choices beside the keys)
606
581
  const spec: KeypadSpec = {
607
582
  title,
608
- sub: c.driving === false ? (c.layer === 'print' ? 'Print dimension (label only)' : 'Reference — not driving the shape') : 'Driving dimension',
583
+ sub: c.driving === false ? 'Reference — not driving the shape' : 'Driving dimension',
609
584
  numeric: isAng, initial: isAng ? String(Math.round(now() * 10) / 10) : fmtLen(now()), selectAll: true, laser: !isAng, modeless,
610
585
  // (just the faces it's measured on: Reference and Delete are the bar's, for a dimension selected)
611
586
  extras: () => faceChoices(sk, withFace(c, face)).map(ch => ({
@@ -615,7 +590,7 @@ export function editDim(id: string, done?: () => void, modeless = false, pick?:
615
590
  onDone: (v, capture) => { setDimValue(id, v * k, face ?? undefined, { capture }); done?.(); },
616
591
  // a line's length or an angle: or one of the job's, from its list — the dimension takes it, linked to it (a
617
592
  // dimension already linked to another, re-linked)
618
- measurements: ((c.type === 'dist' && c.line) || isAng) && c.layer !== 'print' && offersJobMeasurements() ? {
593
+ measurements: ((c.type === 'dist' && c.line) || isAng) && offersJobMeasurements() ? {
619
594
  kind: isAng ? 'angle' : 'length', pick: t => {
620
595
  const v = jobValue(t, isAng ? 'angle' : 'length');
621
596
  if (v == null) return;
@@ -671,7 +646,6 @@ function takeLink(c: Constraint, o: DimValueOpts): void {
671
646
  export function setDimValue(id: string, v: number, face?: FaceChoice, opts: DimValueOpts = {}): boolean {
672
647
  const sk = SK(), c = sk.cons[id];
673
648
  if (!c) return false;
674
- if (c.layer === 'print') { toast('Print dimensions follow the drawing. Change a driving dimension instead.', { warn: true }); return false; }
675
649
  if (c.measurementRef && opts.link === undefined && !opts.asked && !opts.quiet && changesValue(c, v)) {
676
650
  askLinkedChange(c, v, unlink => setDimValue(id, v, face, { ...opts, asked: true, ...(unlink ? { link: null } : {}) }), opts.capture);
677
651
  return false;
@@ -9,6 +9,7 @@ import { requestDraw } from '../render/view';
9
9
  import { solver } from '../solver';
10
10
  import { fmtLen } from '../units';
11
11
  import { openKeypad, toast, updateCtx } from '../editor/ports';
12
+ import { waitForScale } from '../editor/scaleGate';
12
13
  import { cornerAt, cutCorner } from './corner';
13
14
 
14
15
  /*
@@ -33,6 +34,8 @@ export function maxFillet(sk: Sketch, pid: string): number {
33
34
  export function filletPrompt(pid: string): void {
34
35
  const sk = SK();
35
36
  if (!cornerAt(sk, pid)) { toast('Select a corner where two lines meet to round it off.', { warn: true }); return; }
37
+ // (a radius typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
38
+ if (waitForScale()) return;
36
39
  const most = maxFillet(sk, pid), start = Math.min(6, most * 0.5);
37
40
  openKeypad({
38
41
  title: 'Fillet radius', sub: 'Up to ' + fmtLen(most) + ' fits this corner', initial: fmtLen(Math.round(start * 16) / 16), value: Math.round(start * 16) / 16, selectAll: true, laser: true,
@@ -3,7 +3,6 @@ import { ed } from '../app/state';
3
3
  import { wallBand, type PEv } from './registry';
4
4
  import { hyp } from '../model/geom';
5
5
  import { dropHistory, pushHistory } from '../model/history';
6
- import { maybeRoom } from '../model/rooms';
7
6
  import { settings } from '../model/settings';
8
7
  import { addLine, addPoint } from '../model/sketch';
9
8
  import { deg, JOIN_PX, simplifyStroke, SQUARE_DEG, squareUp, turn } from '../model/stroke';
@@ -20,7 +19,6 @@ import { drawsWalls } from '../editor/mode';
20
19
  simplified, corners cleaned up, closed if it ends where it began, every segment within 15° of level, plumb or a
21
20
  45° diagonal snapped exactly to it (most site sketches are 90s and 45s), joined to points it starts or ends on,
22
21
  and (with auto-constrain on) level/plumb segments and square corners constrained, ready to adjust.
23
- The same conversion turns a markup pen stroke into lines (convertNote).
24
22
  */
25
23
 
26
24
  /**
@@ -57,7 +55,7 @@ export function strokeToLines(stroke: XY[], opts: { constr?: boolean; kind?: Lin
57
55
  if (Math.abs(turn(p, q, r) - 90) < SQUARE_DEG) add({ type: 'perp', l1: lines[j], l2: lines[i] });
58
56
  }
59
57
  }
60
- if (closed && !opts.constr) maybeRoom(sk, ids);
58
+ // (a closed shape is only a shape: the Space tool makes it a space, as the line tool's)
61
59
  return { lines, closed };
62
60
  }
63
61
 
package/src/tools/line.ts CHANGED
@@ -2,15 +2,14 @@ import { afterEdit, analyze, tryAddConstraint } from '../app/edit';
2
2
  import { ed, type Guide, type Snap } from '../app/state';
3
3
  import { hyp, norm2 } from '../model/geom';
4
4
  import { pushHistory } from '../model/history';
5
- import { roomGeom, maybeRoom } from '../model/rooms';
6
5
  import { settings } from '../model/settings';
7
6
  import { addLine, gc, pinnedPoints } from '../model/sketch';
8
- import { SK, store } from '../model/store';
7
+ import { isUnscaled, setScale } from '../model/scale';
8
+ import { page, SK, store } from '../model/store';
9
9
  import type { LineKind } from '../model/types';
10
10
  import { TH } from '../render/theme';
11
11
  import { shiftSnap, snap45, snapAt } from '../render/snap';
12
12
  import { requestDraw, requestOverlayDraw, S } from '../render/view';
13
- import { fmtArea } from '../units';
14
13
  import { commitPoint, applyPending, lengthPreview, PT, rememberHover, wallBand, type PEv, type Tool } from './registry';
15
14
  import { openKeypad, toast, updateCtx, updateHint } from '../editor/ports';
16
15
  import type { KeyIn } from '../editor/keys';
@@ -138,19 +137,17 @@ export function makeLineTool(): LineTool {
138
137
  const closed = b.id === st.first && st.chain.length >= 3;
139
138
  st.chain.push(b.id); st.prev = lid; st.start = b.id;
140
139
  pinnedPoints.clear(); pinnedPoints.add(b.id);
141
- if (closed) {
142
- const loop = st.chain.slice(0, -1);
143
- const sk = SK();
144
- const rid = kindNow() === 'wall' || !ed.drawConstr ? maybeRoom(sk, loop) : null;
145
- this.reset();
146
- if (rid) toast(sk.rooms[rid].name + ' added — ' + fmtArea(roomGeom(sk, sk.rooms[rid]).area));
147
- }
140
+ // a closed shape is only a shape: it's made a space by the Space tool, when the person picks it (the user's call,
141
+ // 2026-10-26: "By default i dont want spaces create at all")
142
+ if (closed) this.reset();
148
143
  },
149
144
  typeLength(initial: string) {
150
145
  const st = this.st;
151
146
  if (!st.start) return;
152
147
  const dirs: [string, [number, number]][] = [['→', [1, 0]], ['←', [-1, 0]], ['↑', [0, 1]], ['↓', [0, -1]]];
153
148
  let dir = st.dir || [1, 0];
149
+ // how long the segment is drawn so far (to the pointer: nothing, on a touch screen, after a tap)
150
+ const from = PT(st.start), drawn = ed.snapState ? hyp(ed.snapState.x - from.x, ed.snapState.y - from.y) : 0;
154
151
  openKeypad({
155
152
  title: 'Segment length', sub: 'Direction sets where it goes', initial: initial || '',
156
153
  extras: () => dirs.map(([label, d]) => ({
@@ -161,11 +158,15 @@ export function makeLineTool(): LineTool {
161
158
  })),
162
159
  onDone: v => {
163
160
  if (v <= 0) return;
161
+ pushHistory();
162
+ // the drawing's first length: the sketch is scaled so the segment, as drawn, is that long — as it is, when
163
+ // nothing was drawn to go by (model/scale.ts). Before it's read where the segment starts: that moves too.
164
+ const pg = page();
165
+ if (isUnscaled(pg.sk) && SK() === pg.sk) { setScale(pg, drawn > 1e-6 ? v / drawn : 1); requestDraw(); }
164
166
  const a = PT(st.start!);
165
167
  const guides: Guide[] = [];
166
168
  if (Math.abs(dir[1]) < 1e-9) guides.push({ kind: 'H', from: st.start! });
167
169
  else if (Math.abs(dir[0]) < 1e-9) guides.push({ kind: 'V', from: st.start! });
168
- pushHistory();
169
170
  this.segmentTo({ x: a.x + dir[0] * v, y: a.y + dir[1] * v, kind: 'free', guides }, v);
170
171
  afterEdit();
171
172
  },