@reekon-tools/boldr-cad 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools/dim.ts CHANGED
@@ -24,7 +24,7 @@ import { addOpeningDim } from './openings';
24
24
  import type { PEv } from './registry';
25
25
  import { keypadFresh, keypadOpen, keypadSetReading } from '../editor/keypad';
26
26
  import { advanceAfterReading } from '../editor/advance';
27
- import { cornerLines } from '../editor/selection';
27
+ import { cornerLines, edgePair, selInfo } from '../editor/selection';
28
28
  import { closeKeypad, kpTyped, openKeypad, showConflict, toast, updateCtx, updateHint, type KeypadSpec } from '../editor/ports';
29
29
  import { noScaleYet, pendingLabel, startingValue } from '../editor/scaleGate';
30
30
 
@@ -165,7 +165,7 @@ export function dimLine(lid: string, side?: 1 | -1): void {
165
165
  const wall = faced(sk, lid);
166
166
  const ex = lengthDimOf(sk, lid);
167
167
  if (ex) { editDim(ex, undefined, false, wall && side ? { face: side } : undefined); return; }
168
- let face: 0 | 1 | -1 = wall ? side ?? defaultFace(sk, lid, ed.wallTap?.id === lid ? ed.wallTap.side : undefined) ?? 0 : 0;
168
+ let face: 0 | 1 | -1 = wall ? firstFace(sk, lid, side) : 0;
169
169
  const len = () => (face ? faceLength(sk, lid, face) : lineLen(sk, l));
170
170
  // (while it's being entered, what it measures is what's highlighted: the edge, or the wall)
171
171
  const was = ed.sel;
@@ -196,7 +196,7 @@ export function dimLine(lid: string, side?: 1 | -1): void {
196
196
  /** The line's length dimension, driving, measured where the keypad says (`extra`: what it's from). Whether it took. */
197
197
  function addLength(v: number, extra: Partial<Constraint>): boolean {
198
198
  pushHistory();
199
- const r = tryAddConstraint({ type: 'dist', a: l.a, b: l.b, kind: 'al', value: v, line: lid, driving: true, off: face ? (face * 30) / V().s : 0, ...(face ? { face } : {}), ...extra });
199
+ const r = tryAddConstraint({ ...lengthDim(sk, lid, face), value: v, ...extra });
200
200
  // a measurement of the job drives a line, or nothing: not one whose length the drawing already sets
201
201
  let took = r.ok;
202
202
  if (r.ok && r.ref && extra.measurementRef) {
@@ -383,9 +383,102 @@ export function dimEdgePoint(lid: string, side: 1 | -1, p: string): void {
383
383
  const sk = SK(), l = sk.lines[lid];
384
384
  // (one of the wall's own ends is no distance from it)
385
385
  if (!l || p === l.a || p === l.b) { toast('Pick a point off the wall to measure from its edge.', { warn: true }); return; }
386
- dimPointLine(p, lid, { faces: [faceToward(sk, lid, sk.points[p], side), 0], title: 'Distance from the edge' });
386
+ dimPointLine(p, lid, { faces: edgePointFaces(sk, lid, side, p), title: 'Distance from the edge' });
387
387
  }
388
388
 
389
+ /** From a wall's edge to a point: measured from that face of the wall. */
390
+ const edgePointFaces = (sk: Sketch, lid: string, side: 1 | -1, p: string): [number, number] => [faceToward(sk, lid, sk.points[p], side), 0];
391
+
392
+ /* What the + measures, as a dimension not made yet: the builders above make these, and the grey number shows them. */
393
+
394
+ /**
395
+ * The face a wall's length (a curved wall's diameter) is first measured on: `side`, the edge picked; picked whole, the
396
+ * side a space is on (the side it was tapped on, between two); its centreline with no space beside it.
397
+ */
398
+ const firstFace = (sk: Sketch, id: string, side?: 1 | -1): 0 | 1 | -1 =>
399
+ side ?? defaultFace(sk, id, ed.wallTap?.id === id ? ed.wallTap.side : undefined) ?? 0;
400
+
401
+ /** Line `lid`'s length, measured on `face` (a wall's; 0 along the line). Its label sits off the side it's measured on. */
402
+ function lengthDim(sk: Sketch, lid: string, face: 0 | 1 | -1): Constraint {
403
+ const l = sk.lines[lid];
404
+ return { type: 'dist', a: l.a, b: l.b, kind: 'al', line: lid, driving: true, off: face ? (face * 30) / V().s : 0, ...(face ? { face } : {}) };
405
+ }
406
+
407
+ /** A circle's or arc's diameter, across `face` (a curved wall's; 0 its centreline). */
408
+ const diamDim = (id: string, face: 0 | 1 | -1): Constraint => ({ type: 'diam', c: id, driving: true, ...(face ? { face } : {}) });
409
+
410
+ /** A circle's or arc's diameter dimension, if it has one. */
411
+ const diamDimOf = (sk: Sketch, id: string): string | undefined => Object.keys(sk.cons).find(k => sk.cons[k].type === 'diam' && sk.cons[k].c === id);
412
+
413
+ /** The angle between two lines. */
414
+ const angleDim = (l1: string, l2: string): Constraint => ({ type: 'angle', l1, l2, value: 0, driving: true });
415
+
416
+ /** The straight distance between two points. */
417
+ const pointsDim = (a: string, b: string): Constraint => ({ type: 'dist', a, b, kind: 'al', driving: true, off: 0 });
418
+
419
+ /**
420
+ * Point `p`'s distance from line `lid`, square to it, on its side of it. On a wall, from `faces[0]` (see dimPointLine) —
421
+ * by default the face toward the point where a space is against it; `l2`, the wall the point is the end of.
422
+ */
423
+ function pointLineDim(sk: Sketch, p: string, lid: string, opts: { faces?: [number, number]; l2?: string } = {}): Constraint {
424
+ const l = sk.lines[lid], P = sk.points[p], A = sk.points[l.a], B = sk.points[l.b];
425
+ const sd = ((B.x - A.x) * (P.y - A.y) - (B.y - A.y) * (P.x - A.x)) / (hyp(B.x - A.x, B.y - A.y) || 1);
426
+ const c: Constraint = { type: 'pldist', p, l: lid, value: 0, sign: sd < 0 ? -1 : 1, driving: true, ...(opts.l2 ? { l2: opts.l2 } : {}) };
427
+ applyFace(c, { faces: opts.faces ?? [faceToward(sk, lid, P), 0] });
428
+ return c;
429
+ }
430
+
431
+ /**
432
+ * What the yellow + measures for the selection, as the dimension it would make (`c`, its value unset), with the one
433
+ * already there for it (`existing`), which the + edits instead. Null where it makes no dimension of the plan: nothing it
434
+ * measures, a door's or window's sizes (their own dimensions), a wall's thickness, the wall view (wallPlus).
435
+ */
436
+ export function selectionDim(): { c: Constraint; existing?: string } | null {
437
+ const sk = SK(), s = selInfo();
438
+ if (s.dims.length || s.odims.length || s.openings.length || s.oanchors.length) return null;
439
+ if (s.wfaces.length === 1 && s.n === 1) {
440
+ const [id, side] = edgeOf(s.wfaces[0]);
441
+ if (sk.lines[id]) return { c: lengthDim(sk, id, side), existing: lengthDimOf(sk, id) ?? undefined };
442
+ return { c: diamDim(id, side), existing: diamDimOf(sk, id) };
443
+ }
444
+ if (edgePair(s)) {
445
+ const edges = s.wfaces.map(edgeOf), other = ed.sel.find(x => x.type !== 'wface');
446
+ if (edges.length === 2) {
447
+ const [[a, sa], [b, sb]] = edges;
448
+ return a !== b && sk.lines[a] && sk.lines[b] ? twoLinesDim(sk, a, b, { [a]: sa, [b]: sb }) : null;
449
+ }
450
+ const [lid, side] = edges[0], l = sk.lines[lid];
451
+ if (other?.type === 'line') return twoLinesDim(sk, lid, other.id, { [lid]: side });
452
+ if (other?.type !== 'point' || !l || other.id === l.a || other.id === l.b) return null;
453
+ return { c: pointLineDim(sk, other.id, lid, { faces: edgePointFaces(sk, lid, side, other.id) }), existing: pointLineDimOf(sk, other.id, lid) };
454
+ }
455
+ if (s.lines.length === 1 && s.n === 1) {
456
+ const lid = s.lines[0];
457
+ return { c: lengthDim(sk, lid, faced(sk, lid) ? firstFace(sk, lid) : 0), existing: lengthDimOf(sk, lid) ?? undefined };
458
+ }
459
+ if (s.curves.length === 1 && s.n === 1) {
460
+ const id = s.curves[0];
461
+ return { c: diamDim(id, wallOf(sk, id) ? firstFace(sk, id) : 0), existing: diamDimOf(sk, id) };
462
+ }
463
+ if (s.points.length === 1 && s.n === 1) return cornerDim(sk, s.points[0]);
464
+ if (s.n === 2 && s.lines.length === 2) return twoLinesDim(sk, s.lines[0], s.lines[1]);
465
+ if (s.n === 2 && s.points.length === 2) {
466
+ const [a, b] = s.points;
467
+ const existing = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'dist' && !c.line && ((c.a === a && c.b === b) || (c.a === b && c.b === a)); });
468
+ return { c: pointsDim(a, b), existing };
469
+ }
470
+ if (s.n === 2 && s.points.length === 1 && s.lines.length === 1) {
471
+ const [p] = s.points, [lid] = s.lines, l = sk.lines[lid];
472
+ // (one of the line's own ends is no distance from it)
473
+ if (!l || p === l.a || p === l.b) return null;
474
+ return { c: pointLineDim(sk, p, lid), existing: pointLineDimOf(sk, p, lid) };
475
+ }
476
+ return null;
477
+ }
478
+
479
+ /** A distance from point `p` to line `lid` already in the drawing. */
480
+ const pointLineDimOf = (sk: Sketch, p: string, lid: string): string | undefined => Object.keys(sk.cons).find(id => sk.cons[id].type === 'pldist' && sk.cons[id].p === p && sk.cons[id].l === lid);
481
+
389
482
  /**
390
483
  * Two things picked, at least one a wall's edge: the distance between them — two edges of one wall, its thickness
391
484
  * (`thickness` opens it); of two walls, face to face; an edge and a line, from that face; an edge and a point, from it.
@@ -417,7 +510,7 @@ export function dimPoints(a: string, b: string): void {
417
510
  title: 'Distance', ...startingValue(L), selectAll: true, laser: true,
418
511
  onDone: (v, capture) => {
419
512
  pushHistory();
420
- const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0, ...(capture !== undefined ? { capture } : {}) });
513
+ const r = tryAddConstraint({ ...pointsDim(a, b), value: v, ...(capture !== undefined ? { capture } : {}) });
421
514
  if (!r.ok) dropHistory();
422
515
  ed.sel = [];
423
516
  afterEdit();
@@ -432,8 +525,28 @@ export function dimPoints(a: string, b: string): void {
432
525
  * or, picked whole, each wall's room side (see faceToward).
433
526
  */
434
527
  export function dimTwoLines(l1: string, l2: string, sides: Record<string, 0 | 1 | -1> = {}): void {
435
- const sk = SK(), L1 = sk.lines[l1], L2 = sk.lines[l2];
436
- if (!L1 || !L2) return;
528
+ const sk = SK(), d = twoLinesDim(sk, l1, l2, sides);
529
+ if (!d) return;
530
+ if (d.existing) {
531
+ const c = sk.cons[d.existing];
532
+ // (the edges picked: it's measured between them now)
533
+ editDim(d.existing, undefined, false, c.type === 'pldist' && Object.keys(sides).length ? d.facesFrom(c.l!, c.p!) : undefined);
534
+ return;
535
+ }
536
+ if (d.c.type === 'angle') { dimAngle(l1, l2); return; }
537
+ const f = d.facesFrom(l1, d.c.p!);
538
+ dimPointLine(d.c.p!, l1, { keepParallel: l2, title: 'Distance between lines', faces: f.faces, l2: f.l2 });
539
+ }
540
+
541
+ /**
542
+ * What the + measures between two lines: their distance if they're parallel — square to the first, from the end of the
543
+ * second that sits most squarely across from it, between the faces `sides` says (see dimTwoLines) — otherwise the angle
544
+ * between them. `existing`: a dimension already between them; `facesFrom`: the faces between line `l` and the other,
545
+ * from point `p` on the other.
546
+ */
547
+ function twoLinesDim(sk: Sketch, l1: string, l2: string, sides: Record<string, 0 | 1 | -1> = {}): { c: Constraint; existing?: string; facesFrom: (l: string, p: string) => FaceChoice } | null {
548
+ const L1 = sk.lines[l1], L2 = sk.lines[l2];
549
+ if (!L1 || !L2) return null;
437
550
  const [ax, ay] = norm2(sk.points[L1.b].x - sk.points[L1.a].x, sk.points[L1.b].y - sk.points[L1.a].y);
438
551
  const [bx, by] = norm2(sk.points[L2.b].x - sk.points[L2.a].x, sk.points[L2.b].y - sk.points[L2.a].y);
439
552
  const parallel = Math.abs(ax * by - ay * bx) < 0.02; // within about a degree
@@ -444,24 +557,16 @@ export function dimTwoLines(l1: string, l2: string, sides: Record<string, 0 | 1
444
557
  if (kind === 'angle') return (c.l1 === l1 && c.l2 === l2) || (c.l1 === l2 && c.l2 === l1);
445
558
  return (c.l === l1 && (c.p === L2.a || c.p === L2.b)) || (c.l === l2 && (c.p === L1.a || c.p === L1.b));
446
559
  });
447
- /** The faces between line `l` (the one measured square to) and the other, from point `p` on the other. */
448
560
  const facesFrom = (l: string, p: string): FaceChoice => {
449
561
  const o = l === l1 ? l2 : l1, ol = sk.lines[o], on = sk.lines[l];
450
562
  return { faces: [faceToward(sk, l, sk.points[p], sides[l], true), faceToward(sk, o, sk.points[on.a], sides[o], true)], ...(faced(sk, o) && ol ? { l2: o } : {}) };
451
563
  };
452
- if (existing) {
453
- const c = sk.cons[existing];
454
- // (the edges picked: it's measured between them now)
455
- editDim(existing, undefined, false, c.type === 'pldist' && Object.keys(sides).length ? facesFrom(c.l!, c.p!) : undefined);
456
- return;
457
- }
458
- if (!parallel) { dimAngle(l1, l2); return; }
459
- // measure from the end of the second line that sits most squarely across from the first
564
+ if (!parallel) return { c: angleDim(l1, l2), existing, facesFrom };
460
565
  const A = sk.points[L1.a], B = sk.points[L1.b], len2 = (B.x - A.x) ** 2 + (B.y - A.y) ** 2 || 1;
461
566
  const t = (pid: string) => { const P = sk.points[pid]; return ((P.x - A.x) * (B.x - A.x) + (P.y - A.y) * (B.y - A.y)) / len2; };
462
567
  const p = Math.abs(t(L2.a) - 0.5) <= Math.abs(t(L2.b) - 0.5) ? L2.a : L2.b;
463
568
  const f = facesFrom(l1, p);
464
- dimPointLine(p, l1, { keepParallel: l2, title: 'Distance between lines', faces: f.faces, l2: f.l2 });
569
+ return { c: pointLineDim(sk, p, l1, { faces: f.faces, l2: f.l2 }), existing, facesFrom };
465
570
  }
466
571
 
467
572
  /**
@@ -471,11 +576,7 @@ export function dimTwoLines(l1: string, l2: string, sides: Record<string, 0 | 1
471
576
  * switch them.
472
577
  */
473
578
  export function dimPointLine(p: string, lid: string, opts: { keepParallel?: string; title?: string; faces?: [number, number]; l2?: string } = {}): void {
474
- const sk = SK(), l = sk.lines[lid], P = sk.points[p], A = sk.points[l.a], B = sk.points[l.b];
475
- const sd = ((B.x - A.x) * (P.y - A.y) - (B.y - A.y) * (P.x - A.x)) / (hyp(B.x - A.x, B.y - A.y) || 1);
476
- const sign = sd < 0 ? -1 : 1;
477
- const c: Constraint = { type: 'pldist', p, l: lid, value: 0, sign, driving: true, ...(opts.l2 ? { l2: opts.l2 } : {}) };
478
- applyFace(c, { faces: opts.faces ?? [faceToward(sk, lid, P), 0] });
579
+ const sk = SK(), c = pointLineDim(sk, p, lid, opts);
479
580
  const choices = faceChoices(sk, c);
480
581
  ed.sel = [];
481
582
  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)) }); };
@@ -508,9 +609,10 @@ export function dimPointLine(p: string, lid: string, opts: { keepParallel?: stri
508
609
  */
509
610
  export function dimCurve(id: string, side?: 1 | -1): void {
510
611
  const sk = SK(), wall = !!wallOf(sk, id);
511
- for (const k in sk.cons) { const c = sk.cons[k]; if (c.type === 'diam' && c.c === id) { editDim(k, undefined, false, wall && side ? { face: side } : undefined); return; } }
512
- let face: 0 | 1 | -1 = wall ? side ?? defaultFace(sk, id, ed.wallTap?.id === id ? ed.wallTap.side : undefined) ?? 0 : 0;
513
- const now = () => measure(sk, { type: 'diam', c: id, ...(face ? { face } : {}) });
612
+ const ex = diamDimOf(sk, id);
613
+ if (ex) { editDim(ex, undefined, false, wall && side ? { face: side } : undefined); return; }
614
+ let face: 0 | 1 | -1 = wall ? firstFace(sk, id, side) : 0;
615
+ const now = () => measure(sk, diamDim(id, face));
514
616
  // (while it's being entered, what it measures across is what's highlighted: the edge, or the wall)
515
617
  const was = ed.sel, type = sk.circles[id] ? 'circle' as const : 'arc' as const;
516
618
  const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(id, face) }] : [{ type, id }]; requestDraw(); };
@@ -525,7 +627,7 @@ export function dimCurve(id: string, side?: 1 | -1): void {
525
627
  onDone: v => {
526
628
  if (v <= 0) return;
527
629
  pushHistory();
528
- const r = tryAddConstraint({ type: 'diam', c: id, value: v, driving: true, ...(face ? { face } : {}) });
630
+ const r = tryAddConstraint({ ...diamDim(id, face), value: v });
529
631
  if (!r.ok) dropHistory();
530
632
  // (as a length: the dimension made, nothing left highlighted)
531
633
  ed.sel = [];
@@ -537,16 +639,23 @@ export function dimCurve(id: string, side?: 1 | -1): void {
537
639
 
538
640
  /** A corner's angle (two lines meeting at `pid`): its angle dimension's keypad, or a new one's. False if it isn't a corner. */
539
641
  export function cornerAngle(pid: string, done?: () => void, modeless = false): boolean {
540
- const sk = SK(), pair = cornerLines(sk, pid);
541
- if (!pair) return false;
642
+ const sk = SK(), d = cornerDim(sk, pid);
643
+ if (!d) return false;
542
644
  // edit the corner's existing angle dimension rather than stacking a second one
543
- 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])); });
544
- if (ex) editDim(ex, done, modeless); else dimAngle(pair[0], pair[1], done, modeless);
645
+ if (d.existing) editDim(d.existing, done, modeless); else dimAngle(d.c.l1!, d.c.l2!, done, modeless);
545
646
  return true;
546
647
  }
547
648
 
649
+ /** A corner's angle (two lines meeting at `pid`), as the + measures it; `existing`, its angle dimension. Null if it isn't a corner. */
650
+ function cornerDim(sk: Sketch, pid: string): { c: Constraint; existing?: string } | null {
651
+ const pair = cornerLines(sk, pid);
652
+ if (!pair) return null;
653
+ const existing = 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])); });
654
+ return { c: angleDim(pair[0], pair[1]), existing };
655
+ }
656
+
548
657
  export function dimAngle(l1: string, l2: string, done?: () => void, modeless = false): void {
549
- const c: Constraint = { type: 'angle', l1, l2, value: 0, driving: true };
658
+ const c = angleDim(l1, l2);
550
659
  const cur = measure(SK(), c);
551
660
  /** The angle dimension, driving, at `v` degrees (`extra`: what it's from). */
552
661
  const addAngle = (v: number, extra: Partial<Constraint>) => {
package/src/tools/line.ts CHANGED
@@ -128,6 +128,10 @@ export function makeLineTool(): LineTool {
128
128
  splitWallsAtEnds(SK(), lid);
129
129
  if (settings.autoCons) {
130
130
  for (const g of s.guides || []) {
131
+ // level or plumb from the line's own start: the line itself is. Lined up with any other point ("Aligned"),
132
+ // only placed there — never tied to it (the user's call, 2026-11-05: a V between two unconnected points
133
+ // "just auto made" sat in space between them, and nothing said what it tied)
134
+ if ((g.kind === 'H' || g.kind === 'V') && g.from !== st.start) continue;
131
135
  if (g.kind === 'H') tryAddConstraint({ type: 'h', a: g.from, b: b.id }, { silent: true });
132
136
  else if (g.kind === 'V') tryAddConstraint({ type: 'v', a: g.from, b: b.id }, { silent: true });
133
137
  else if (g.kind === 'perp' && st.prev) tryAddConstraint({ type: 'perp', l1: st.prev, l2: lid }, { silent: true });