@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
@@ -1,20 +1,23 @@
1
- import { afterEdit, holdFaces, solveOrRevert, tryAddConstraint, undo } from '../app/edit';
2
- import { ed } from '../app/state';
3
- import { splitFaces } from '../model/faces';
4
- import { clamp, hyp, lineCircleT, norm2, projT, segInter } from '../model/geom';
1
+ import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../app/edit';
2
+ import { ed, type Preview } from '../app/state';
3
+ import { faceKey, splitFaces } from '../model/faces';
4
+ import { clamp, hyp, norm2, projT } from '../model/geom';
5
5
  import { dropHistory, pushHistory } from '../model/history';
6
6
  import { uid } from '../model/ids';
7
7
  import { findFace, maybeRoom, roomGeom } from '../model/rooms';
8
- import { addLine, addPoint } from '../model/sketch';
9
- import { SK } from '../model/store';
10
- import type { Constraint, Sketch } from '../model/types';
8
+ import { addCons, addLine, addPoint } from '../model/sketch';
9
+ import { replaceSK, SK } from '../model/store';
10
+ import { distTo, nearestOn, pieceAt, pieceKey, pieceShape, trimItemAt, type Cut, type TrimItem, type TrimKind, type TrimPiece } from '../model/trim';
11
+ import type { Constraint, Sketch, XY } from '../model/types';
11
12
  import { hitAny } from '../render/hit';
12
13
  import { outsideSide } from '../model/planGeom';
13
- import { mainT, requestDraw, s2w } from '../render/view';
14
+ import { TH } from '../render/theme';
15
+ import { mainT, requestDraw, requestOverlayDraw, S, s2w } from '../render/view';
14
16
  import { placeTargets, snapSlide } from '../editor/elevation';
15
17
  import { fmtArea, fmtLen } from '../units';
16
18
  import type { PEv } from './registry';
17
19
  import { openKeypad, toast } from '../editor/ports';
20
+ import { noScaleYet, waitForScale } from '../editor/scaleGate';
18
21
 
19
22
  /** Hover helper: highlight only geometry of the given types. */
20
23
  const hoverOnly = (types: string[]) => (ev: PEv) => {
@@ -23,82 +26,216 @@ const hoverOnly = (types: string[]) => (ev: PEv) => {
23
26
  requestDraw();
24
27
  };
25
28
 
29
+ /*
30
+ Trim (T; the rail, after Arc — on a phone, under Draw to shape): tap a piece of a line, wall, arc or circle and it
31
+ goes, cut back to where something crosses it or meets it on either side (model/trim.ts); drag across several and
32
+ each one the stroke crosses goes when it lifts. What's about to go is shown in red. One undo step a tap or a stroke.
33
+ */
34
+
35
+ /** A piece a tap or a stroke took: where on what it's of (in the drawing), and the piece then. */
36
+ interface TrimMark { at: XY; pc: TrimPiece; key: string }
37
+
38
+ /** How near (inches) a point is to count as on what's trimmed, and cuts to count as one: a couple of pixels. */
39
+ const cutTol = (): number => Math.min(0.5, 2 / mainT().s);
40
+
41
+ /** The piece under screen point (sx, sy). */
42
+ function trimMarkAt(sx: number, sy: number, touch: boolean): TrimMark | null {
43
+ const [x, y] = s2w(sx, sy), sk = SK();
44
+ const it = trimItemAt(sk, { x, y }, (touch ? 20 : 10) / mainT().s);
45
+ if (!it) return null;
46
+ // (where on it: found again there as it's cut, on it or on a piece an earlier cut split off)
47
+ const at = nearestOn(sk, it, { x, y }), pc = pieceAt(sk, it, at, cutTol());
48
+ return { at, pc, key: pieceKey(pc) };
49
+ }
50
+
51
+ /** What's about to be cut away, drawn over the drawing in red. */
52
+ const trimPreview = (pcs: TrimPiece[]): Preview => (c, T) => {
53
+ const sk = SK();
54
+ c.save();
55
+ c.strokeStyle = TH.red; c.lineWidth = 4; c.lineCap = 'round'; c.globalAlpha = 0.85;
56
+ for (const pc of pcs) {
57
+ const g = pieceShape(sk, pc);
58
+ if (!g) continue;
59
+ c.beginPath();
60
+ if ('a' in g) { const [x1, y1] = S(T, g.a.x, g.a.y), [x2, y2] = S(T, g.b.x, g.b.y); c.moveTo(x1, y1); c.lineTo(x2, y2); }
61
+ else { const [x, y] = S(T, g.o.x, g.o.y); c.arc(x, y, g.r * T.s, -g.t0, -g.t1, true); }
62
+ c.stroke();
63
+ }
64
+ c.restore();
65
+ };
66
+
67
+ const freshTrim = () => ({ down: null as XY | null, last: null as XY | null, stroke: false, marks: [] as TrimMark[] });
68
+
26
69
  export const trimTool = {
27
- reset() { ed.hover = null; },
28
- hover: hoverOnly(['line', 'circle']),
29
- down(ev: PEv) { this.hover(ev); },
30
- move(ev: PEv) { this.hover(ev); },
70
+ st: freshTrim(),
71
+ reset() { this.st = freshTrim(); ed.hover = null; ed.preview = null; requestOverlayDraw(); },
72
+ show(marks: TrimMark[]) { ed.preview = marks.length ? trimPreview(marks.map(m => m.pc)) : null; requestOverlayDraw(); },
73
+ hover(ev: PEv) { const m = trimMarkAt(ev.sx, ev.sy, ev.touch); this.show(m ? [m] : []); },
74
+ down(ev: PEv) {
75
+ const m = trimMarkAt(ev.sx, ev.sy, ev.touch);
76
+ this.st = { down: { x: ev.sx, y: ev.sy }, last: { x: ev.sx, y: ev.sy }, stroke: false, marks: m ? [m] : [] };
77
+ this.show(this.st.marks);
78
+ },
79
+ move(ev: PEv) {
80
+ const st = this.st;
81
+ if (!st.down) { this.hover(ev); return; }
82
+ if (!st.stroke && hyp(ev.sx - st.down.x, ev.sy - st.down.y) < 12) return;
83
+ st.stroke = true;
84
+ // every few pixels of the way since the last move, so a quick stroke doesn't skip a short piece
85
+ const from = st.last!, n = Math.max(1, Math.ceil(hyp(ev.sx - from.x, ev.sy - from.y) / 6));
86
+ for (let i = 1; i <= n; i++) {
87
+ const m = trimMarkAt(from.x + ((ev.sx - from.x) * i) / n, from.y + ((ev.sy - from.y) * i) / n, ev.touch);
88
+ if (m && !st.marks.some(k => k.key === m.key)) st.marks.push(m);
89
+ }
90
+ st.last = { x: ev.sx, y: ev.sy };
91
+ this.show(st.marks);
92
+ },
31
93
  up(ev: PEv) {
32
- const h = hitAny(ev.sx, ev.sy, { touch: ev.touch, only: 'geom' });
33
- if (!h) return;
34
- const [wx, wy] = s2w(ev.sx, ev.sy);
35
- if (h.type === 'line') trimLine(h.id, wx, wy);
36
- else if (h.type === 'circle') { pushHistory(); delete SK().circles[h.id]; afterEdit(); }
94
+ const st = this.st, tap = trimMarkAt(ev.sx, ev.sy, ev.touch);
95
+ const marks = st.stroke ? st.marks : tap ? [tap] : [];
96
+ this.reset();
97
+ if (marks.length) trimMarks(marks);
37
98
  },
38
- hint() { return 'Tap the part of a line to remove. It cuts back to the nearest crossing and keeps the rest constrained.'; },
99
+ hint() { return 'Tap the piece to cut away, or drag across several. Each cuts back to where something crosses or meets it.'; },
39
100
  };
40
101
 
41
- /** Cut a line back to the nearest crossings around (wx, wy). Unlike SolveSpace, constraints carry over. */
42
- export function trimLine(lid: string, wx: number, wy: number): void {
43
- const sk = SK(), l = sk.lines[lid];
44
- const A = { ...sk.points[l.a] }, B = { ...sk.points[l.b] };
45
- const tc = projT(A, B, { x: wx, y: wy });
46
- const ts: { t: number; line?: string; circle?: string }[] = [];
47
- for (const id in sk.lines) {
48
- if (id === lid) continue;
49
- const o = sk.lines[id];
50
- const r = segInter(A, B, sk.points[o.a], sk.points[o.b]);
51
- if (r && r.t > 1e-6 && r.t < 1 - 1e-6) ts.push({ t: r.t, line: id });
52
- }
53
- for (const id in sk.circles) { const ci = sk.circles[id]; for (const t of lineCircleT(A, B, sk.points[ci.c], ci.r)) ts.push({ t, circle: id }); }
102
+ /**
103
+ * Cut away a tap's or a stroke's pieces, as one undo step. Each is found again as it's cut — on what it was of, or on a
104
+ * piece an earlier cut split off — so cutting one doesn't upset the next. If the drawing can't solve after, it's put
105
+ * back as it was and says so.
106
+ */
107
+ export function trimMarks(marks: TrimMark[]): void {
108
+ const sk = SK(), was = JSON.stringify(sk), tol = cutTol(), near = 3 * tol;
54
109
  pushHistory();
55
- if (!ts.length) { delete sk.lines[lid]; afterEdit(); toast('Line removed — nothing crossed it.'); return; }
56
- ts.sort((a, b) => a.t - b.t);
57
- let lo: (typeof ts)[number] | null = null, hi: (typeof ts)[number] | null = null;
58
- for (const r of ts) { if (r.t < tc) lo = r; else if (!hi) hi = r; }
59
- const mk = (r: (typeof ts)[number]) => {
60
- const id = addPoint(sk, A.x + (B.x - A.x) * r.t, A.y + (B.y - A.y) * r.t);
61
- if (r.line) tryAddConstraint({ type: 'online', p: id, l: r.line }, { silent: true });
62
- if (r.circle) tryAddConstraint({ type: 'oncircle', p: id, c: r.circle }, { silent: true });
110
+ const made: TrimItem[] = [];
111
+ let cut = 0, whole: TrimKind | null = null;
112
+ for (const m of marks) {
113
+ const it = [m.pc.item, ...made].find(c => distTo(sk, c, m.at) <= near);
114
+ if (!it) continue;
115
+ const pc = pieceAt(sk, it, m.at, tol);
116
+ if (!pc.lo && !pc.hi) whole = it.kind;
117
+ made.push(...trimPiece(sk, pc));
118
+ cut++;
119
+ }
120
+ if (!cut) { dropHistory(); return; }
121
+ if (!solveSketch().ok) {
122
+ replaceSK(JSON.parse(was) as Sketch);
123
+ dropHistory();
124
+ afterEdit();
125
+ toast('Can’t trim that — it conflicts with other constraints.', { warn: true });
126
+ return;
127
+ }
128
+ afterEdit();
129
+ // (a tap that took a whole thing: nothing crossed it, so there was nothing to cut back to)
130
+ if (marks.length === 1 && whole) toast((whole === 'line' ? 'Line' : whole === 'arc' ? 'Arc' : 'Circle') + ' removed — ' + (whole === 'circle' ? 'it takes two crossings to cut a piece out.' : 'nothing crossed it.'));
131
+ }
132
+
133
+ /**
134
+ * Cut piece `pc` away. A line or wall is shortened from an end, or split in two around a middle piece that goes (the
135
+ * two kept in line); an arc the same (the two one circle still); a circle opens into an arc round the rest; with no cut
136
+ * either side, the whole thing goes. Where a cut is a point already there, the new end is that point, so what meets
137
+ * there stays joined; a crossing makes a new point, held on what it crosses. Constraints carry over (product rule 9):
138
+ * a length, a midpoint or Equal on a cut line no longer say what they did and go. Edits the sketch only: the caller
139
+ * pushes history, solves and runs afterEdit. Returns what it made (a split's second piece).
140
+ */
141
+ export function trimPiece(sk: Sketch, pc: TrimPiece): TrimItem[] {
142
+ const { item, lo, hi } = pc, made: TrimItem[] = [];
143
+ /** A cut's point: the one there, or a new one, held on what crosses there. */
144
+ const at = (c: Cut): string => {
145
+ if (c.pt) return c.pt;
146
+ const id = addPoint(sk, c.x, c.y);
147
+ if (c.line) addCons(sk, { type: 'online', p: id, l: c.line });
148
+ if (c.curve) addCons(sk, { type: 'oncircle', p: id, c: c.curve });
63
149
  return id;
64
150
  };
65
- const oldA = l.a, oldB = l.b, L = hyp(B.x - A.x, B.y - A.y);
66
- const isHV = (c: Constraint) => (c.type === 'h' || c.type === 'v') && ((c.a === oldA && c.b === oldB) || (c.a === oldB && c.b === oldA));
67
- const retarget = (na: string, nb: string) => {
151
+ if (item.kind === 'line') trimLinePiece(sk, item.id, lo, hi, at, made);
152
+ else if (item.kind === 'arc') {
153
+ const a = sk.arcs[item.id];
154
+ if (!lo && !hi) delete sk.arcs[item.id];
155
+ else if (!lo) a.a = at(hi!);
156
+ else if (!hi) a.b = at(lo);
157
+ else {
158
+ const p1 = at(lo), p2 = at(hi), oldB = a.b, nid = uid('ar');
159
+ a.b = p1;
160
+ sk.arcs[nid] = { ...a, a: p2, b: oldB };
161
+ // one circle still; a tangent at its old end goes with the piece that ends there now
162
+ addCons(sk, { type: 'eqr', c1: item.id, c2: nid });
163
+ for (const c of Object.values(sk.cons)) if (c.type === 'tan' && c.c === item.id) { const l = sk.lines[c.l!]; if (l && (l.a === oldB || l.b === oldB)) c.c = nid; }
164
+ made.push({ kind: 'arc', id: nid });
165
+ }
166
+ } else {
167
+ const ci = sk.circles[item.id];
168
+ if (!lo || !hi) delete sk.circles[item.id];
169
+ else {
170
+ // what's left runs from the cut after the piece round to the one before: an arc, with the circle's id, so what
171
+ // refers to it (its size, a tangent, Concentric) refers to the arc
172
+ const { c, constr, kind, thick } = ci, a = at(hi), b = at(lo);
173
+ delete sk.circles[item.id];
174
+ sk.arcs[item.id] = { c, a, b, ...(constr ? { constr } : {}), ...(kind ? { kind } : {}), ...(thick ? { thick } : {}) };
175
+ }
176
+ }
177
+ // a point held on a line or an arc that's that line's or arc's end now: an end is on it already
178
+ const ids = new Set([item.id, ...made.map(m => m.id)]);
179
+ for (const id in sk.cons) {
180
+ const c = sk.cons[id], l = c.l ? sk.lines[c.l] : undefined, a = c.c ? sk.arcs[c.c] : undefined;
181
+ if (c.type === 'online' && !c.face && l && ids.has(c.l!) && (l.a === c.p || l.b === c.p)) delete sk.cons[id];
182
+ else if (c.type === 'oncircle' && a && ids.has(c.c!) && (a.a === c.p || a.b === c.p)) delete sk.cons[id];
183
+ }
184
+ return made;
185
+ }
186
+
187
+ function trimLinePiece(sk: Sketch, lid: string, lo: Cut | null, hi: Cut | null, at: (c: Cut) => string, made: TrimItem[]): void {
188
+ const l = sk.lines[lid];
189
+ if (!lo && !hi) { delete sk.lines[lid]; return; }
190
+ const A = sk.points[l.a], B = sk.points[l.b], L = hyp(B.x - A.x, B.y - A.y), oldA = l.a, oldB = l.b;
191
+ // its length, its middle, Equal to another: they no longer say what they did
192
+ for (const id in sk.cons) {
193
+ const c = sk.cons[id];
194
+ if ((c.type === 'dist' && c.line === lid) || (c.type === 'mid' && c.l === lid) || (c.type === 'eq' && (c.l1 === lid || c.l2 === lid))) delete sk.cons[id];
195
+ }
196
+ if (lo && hi) {
197
+ // a middle piece: split at both cuts (splitLineAt carries constraints, spaces, doors and windows, and what's on its
198
+ // faces) and the piece between goes; the two left stay in line
199
+ const p1 = at(lo), p2 = at(hi), mid = splitLineAt(sk, lid, p1), rest = mid && splitLineAt(sk, mid, p2);
200
+ if (!mid || !rest) return;
201
+ delete sk.lines[mid];
68
202
  for (const id in sk.cons) {
69
203
  const c = sk.cons[id];
70
- if (isHV(c)) { c.a = na; c.b = nb; }
71
- if (c.type === 'dist' && c.line === lid) delete sk.cons[id];
72
- if (c.type === 'mid' && c.l === lid) delete sk.cons[id];
204
+ const across = (c.type === 'h' || c.type === 'v') && ((c.a === p1 && c.b === p2) || (c.a === p2 && c.b === p1));
205
+ if (across || (c.type === 'online' && c.p === p2 && c.l === lid)) delete sk.cons[id];
73
206
  }
74
- };
207
+ addCons(sk, { type: 'col', l1: lid, l2: rest });
208
+ made.push({ kind: 'line', id: rest });
209
+ return;
210
+ }
211
+ const hv = (c: Constraint) => (c.type === 'h' || c.type === 'v') && ((c.a === oldA && c.b === oldB) || (c.a === oldB && c.b === oldA));
75
212
  if (!lo) {
76
- const p = mk(hi!);
213
+ // from its start: it starts at the cut now, and what's measured from its start is measured from there
214
+ const p = at(hi!), s = hi!.at * L;
77
215
  l.a = p;
78
- retarget(p, oldB);
79
- for (const id in sk.openings) { const o = sk.openings[id]; if (o.line === lid) o.at -= hi!.t * L; }
80
- } else if (!hi) {
81
- const p = mk(lo);
82
- l.b = p;
83
- retarget(oldA, p);
216
+ for (const c of Object.values(sk.cons)) if (hv(c)) { c.a = p; c.b = oldB; }
217
+ for (const id in sk.openings) { const o = sk.openings[id]; if (o.line !== lid) continue; if (o.at < s) delete sk.openings[id]; else o.at -= s; }
218
+ shiftWallFace(sk, lid, -1, -s);
219
+ for (const side of [1, -1]) for (const e of sk.elev?.[lid + '|' + side] ?? []) { e.a[0] -= s; e.b[0] -= s; }
84
220
  } else {
85
- const p1 = mk(lo), p2 = mk(hi);
86
- l.b = p1;
87
- const nid = addLine(sk, p2, oldB, { kind: l.kind, constr: l.constr, thick: l.thick });
88
- const copies: Constraint[] = [];
89
- for (const id in sk.cons) {
90
- const c = sk.cons[id];
91
- if ((c.type === 'par' || c.type === 'perp' || c.type === 'col') && (c.l1 === lid || c.l2 === lid)) copies.push(Object.assign({}, c, c.l1 === lid ? { l1: nid } : { l2: nid }));
92
- if (isHV(c)) copies.push({ type: c.type, a: p2, b: oldB });
93
- }
94
- retarget(oldA, p1);
95
- for (const c of copies) tryAddConstraint(c, { silent: true });
96
- tryAddConstraint({ type: 'online', p: p2, l: lid }, { silent: true });
97
- tryAddConstraint({ type: 'par', l1: lid, l2: nid }, { silent: true });
98
- for (const id in sk.openings) { const o = sk.openings[id]; if (o.line === lid && o.at > hi.t * L) { o.line = nid; o.at -= hi.t * L; } }
221
+ // to its end: it ends at the cut now
222
+ const p = at(lo), s = lo.at * L;
223
+ l.b = p;
224
+ for (const c of Object.values(sk.cons)) if (hv(c)) { c.a = oldA; c.b = p; }
225
+ for (const id in sk.openings) { const o = sk.openings[id]; if (o.line === lid && o.at > s) delete sk.openings[id]; }
226
+ shiftWallFace(sk, lid, 1, s - L);
99
227
  }
100
- solveOrRevert();
101
- afterEdit();
228
+ }
229
+
230
+ /**
231
+ * What's drawn on one face of wall `lid`, moved across it by dx: the face's x runs from the wall's start seen from side
232
+ * −1, and from its end seen from side 1 (see splitFaces), so a trim moving that end moves it.
233
+ */
234
+ function shiftWallFace(sk: Sketch, lid: string, side: 1 | -1, dx: number): void {
235
+ const face = sk.faces?.[faceKey(lid, side)];
236
+ if (!face || !dx) return;
237
+ delete face.wall; // (its corners are recounted at its next placing)
238
+ for (const p of Object.values(face.points)) if (!p.anchor) p.x += dx;
102
239
  }
103
240
 
104
241
  export const offsetTool = {
@@ -118,6 +255,8 @@ export const offsetTool = {
118
255
  };
119
256
 
120
257
  export function offsetPrompt(lid: string, side: number): void {
258
+ // (a distance typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
259
+ if (waitForScale()) return;
121
260
  ed.sel = [{ type: 'line', id: lid }];
122
261
  requestDraw();
123
262
  openKeypad({ title: 'Offset distance', sub: 'Copies the line, kept parallel', initial: '', onDone: v => { if (!v) return; offsetLine(lid, v * side); } });
@@ -185,7 +324,8 @@ export const roomTool = {
185
324
  if (!id) { dropHistory(); toast('That area is too small for a space.', { warn: true }); return; }
186
325
  ed.sel = [];
187
326
  afterEdit();
188
- toast(sk.rooms[id].name + ' — ' + fmtArea(roomGeom(sk, sk.rooms[id]).area));
327
+ // (its area once the drawing has its scale: before, only a placeholder)
328
+ toast(sk.rooms[id].name + (noScaleYet() ? '' : ' — ' + fmtArea(roomGeom(sk, sk.rooms[id]).area)));
189
329
  },
190
330
  hint() { return 'Tap inside a closed shape to make a space. Area is measured to the inside face of walls.'; },
191
331
  };
@@ -1,6 +1,7 @@
1
1
  import { tryAddConstraint } from '../app/edit';
2
2
  import { ed, type Snap } from '../app/state';
3
3
  import { hyp } from '../model/geom';
4
+ import { edgeOf } from '../model/planGeom';
4
5
  import { settings } from '../model/settings';
5
6
  import { addPoint, WALL_THICK } from '../model/sketch';
6
7
  import { SK } from '../model/store';
@@ -10,6 +11,7 @@ import { requestDraw, S, type Xform } from '../render/view';
10
11
  import { fmtLen } from '../units';
11
12
  import { onSessionReset } from '../model/session';
12
13
  import { updateCtx, updateHint, updateRail } from '../editor/ports';
14
+ import { noScaleYet, toolWaitsForScale, waitForScale } from '../editor/scaleGate';
13
15
  import type { KeyIn } from '../editor/keys';
14
16
  import type { DrawCtx } from '../render/ctx';
15
17
  import { TH } from '../render/theme';
@@ -36,6 +38,11 @@ export interface Tool {
36
38
  hint?(): string;
37
39
  /** something half-done that Esc should cancel first (a line in progress, a picked first point…) */
38
40
  busy?(): boolean;
41
+ /**
42
+ * whether a finger or pencil pressed and moving is placing or dragging something, for a device's magnifier
43
+ * (render/lens.ts). Unset: whenever one is (a drawing tool places at it all the time).
44
+ */
45
+ magnify?(): boolean;
39
46
  }
40
47
 
41
48
  export const tools: Record<string, Tool> = {};
@@ -53,6 +60,8 @@ export function cancelTool(): void {
53
60
  }
54
61
 
55
62
  export function setTool(t: string): void {
63
+ // (doors, windows, cabinets and offsets have real sizes: they wait for the page's first length — editor/scaleGate.ts)
64
+ if (toolWaitsForScale(t) && waitForScale()) return;
56
65
  if (t === ed.tool && t !== 'select' && t !== 'aselect') { cancelTool(); updateCtx(); updateHint(); return; }
57
66
  cancelTool();
58
67
  ed.tool = t;
@@ -73,9 +82,16 @@ export function commitPoint(s: Snap): { id: string; cons: Constraint[] } {
73
82
  else if (s.kind === 'online') cons.push({ type: 'online', p: id, l: s.ref! });
74
83
  else if (s.kind === 'oncircle') cons.push({ type: 'oncircle', p: id, c: s.ref! });
75
84
  else if (s.kind === 'inter' && s.refs) cons.push({ type: 'online', p: id, l: s.refs[0] }, { type: 'online', p: id, l: s.refs[1] });
85
+ else cons.push(...faceSnapCons(s, id));
76
86
  return { id, cons };
77
87
  }
78
88
 
89
+ /** A snap onto a wall's face, or where two faces meet: point `p` on that face (on each), not on the wall's centreline. */
90
+ export function faceSnapCons(s: Snap, p: string): Constraint[] {
91
+ const edges = s.kind === 'onface' && s.ref ? [s.ref] : s.kind === 'facecorner' && s.refs ? s.refs : [];
92
+ return edges.map(e => { const [l, face] = edgeOf(e); return { type: 'online', p, l, face }; });
93
+ }
94
+
79
95
  export function applyPending(list: Constraint[]): void {
80
96
  if (!settings.autoCons) return;
81
97
  for (const c of list) tryAddConstraint(c, { silent: true });
@@ -88,7 +104,8 @@ export function rememberHover(s: Snap | null): void {
88
104
  export function lengthPreview(c: DrawCtx, T: Xform, a: XY, b: XY, col?: string): void {
89
105
  const [x1, y1] = S(T, a.x, a.y), [x2, y2] = S(T, b.x, b.y);
90
106
  const L = hyp(b.x - a.x, b.y - a.y);
91
- if (L * T.s < 6) return;
107
+ // (not on a page without its scale: the length is only a placeholder)
108
+ if (L * T.s < 6 || noScaleYet()) return;
92
109
  chip(c, fmtLen(L), (x1 + x2) / 2, (y1 + y2) / 2 - 16, { bg: '#fff', fg: '#0066FF', border: [col || '#0066FF', 1.5] });
93
110
  }
94
111