@reekon-tools/boldr-cad 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +3 -5
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +149 -169
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +2 -2
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/linkActions.d.ts +22 -3
- package/dist/editor/linkActions.js +95 -13
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/mode.js +4 -3
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +9 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +4 -1
- package/dist/model/links.js +9 -2
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +120 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +21 -1
- package/dist/model/sketch.js +58 -4
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/types.d.ts +38 -7
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +94 -25
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/Drawer.js +3 -2
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +184 -149
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +5 -1
- package/dist/render/snap.js +3 -1
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +31 -4
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +23 -6
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.js +6 -1
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +6 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +19 -0
- package/dist/tools/select.js +116 -17
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +3 -5
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +126 -112
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +2 -2
- package/src/editor/linkActions.ts +90 -13
- package/src/editor/mode.ts +4 -3
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +8 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +8 -2
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +105 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +48 -4
- package/src/model/stroke.ts +2 -2
- package/src/model/types.ts +38 -7
- package/src/react/BottomBar.tsx +114 -29
- package/src/react/Drawer.tsx +14 -10
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/Sheet.tsx +3 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/motion.tsx +6 -6
- package/src/render/draw.ts +156 -116
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +7 -2
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +37 -5
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +22 -6
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +5 -1
- package/src/tools/registry.ts +10 -1
- package/src/tools/select.ts +99 -18
- package/src/tools/shapes.ts +9 -4
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
package/src/render/draw.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ed, isSel, type HitChip } from '../app/state';
|
|
2
2
|
import { consPoints, curveDefined, isDim, lineDefined, measure } from '../model/constraints';
|
|
3
3
|
import { hyp, norm2, normAng, projT } from '../model/geom';
|
|
4
|
-
import { drivingDim, edgePoint, measureOpeningDim, refPoint, refS, widthEntered } from '../model/openings';
|
|
5
|
-
import { arcAngles, curveFrame, dimEnds, dimRadius, edgeOf, gridSteps, OPENING_TICK_EXT, openingFrame, outsideSide, wallThick } from '../model/planGeom';
|
|
4
|
+
import { drivingDim, edgePoint, hingeEdge, measureOpeningDim, refPoint, refS, widthEntered } from '../model/openings';
|
|
5
|
+
import { angleCorner, angleLabel, arcAngles, curveFrame, dimEnds, dimRadius, edgeOf, gridSteps, OPENING_TICK_EXT, openingFrame, outsideSide, wallThick } from '../model/planGeom';
|
|
6
6
|
import { faceCorner } from '../model/wallfaces';
|
|
7
7
|
import { roomGeom } from '../model/rooms';
|
|
8
8
|
import { wallPieces } from '../model/wallshape';
|
|
@@ -16,13 +16,15 @@ import { drawKeyPlan, drawWallBackdrop, drawWallChips, drawWallSelection, type E
|
|
|
16
16
|
import type { Constraint, Page, Sketch, WallFace, XY } from '../model/types';
|
|
17
17
|
import { fmtAng, fmtArea, fmtLen } from '../units';
|
|
18
18
|
import { FONT, TH } from './theme';
|
|
19
|
-
import { cvs, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid, type Xform } from './view';
|
|
19
|
+
import { cvs, imageLinkBadge, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid, underlaySrc, type Xform } from './view';
|
|
20
20
|
import type { DrawCtx } from './ctx';
|
|
21
21
|
import { dashFor, isSegment, NOTE_TEXT_SIZE, noteLook, shapeOutline } from '../model/markup';
|
|
22
22
|
import { strokeMarkup, strokeOutline } from './markup';
|
|
23
23
|
import { drawLink } from './linkTile';
|
|
24
24
|
import { isLink, linkInfoOf } from '../model/links';
|
|
25
25
|
import { smoothRectPath } from './squircle';
|
|
26
|
+
import { LINK_SVG } from './figmaIcons';
|
|
27
|
+
import { drawSvgIcon, svgIcon } from './svgPath';
|
|
26
28
|
|
|
27
29
|
type C2D = DrawCtx;
|
|
28
30
|
|
|
@@ -41,7 +43,10 @@ export function rr(c: C2D, x: number, y: number, w: number, h: number, r: number
|
|
|
41
43
|
export interface ChipOpts {
|
|
42
44
|
bg: string; fg: string; border?: [string, number?] | null; font?: string; pad?: number; h?: number; minW?: number;
|
|
43
45
|
kind?: HitChip['kind']; id?: string; rot?: number;
|
|
44
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* 'link': a measurement of the job sets it. The chip is then Figma's linked measurement (2795:77149): a white badge
|
|
48
|
+
* with the link icon before the value (LINKED), at the chip's own height and font, so it's the size of a plain chip.
|
|
49
|
+
*/
|
|
45
50
|
mark?: 'link';
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -52,30 +57,46 @@ export function linkBlue(): string {
|
|
|
52
57
|
return dark ? TH.lblue : TH.blue;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
c.restore();
|
|
60
|
+
/** Figma's link icon (render/figmaIcons.ts), read once. */
|
|
61
|
+
const LINK_ICON = svgIcon(LINK_SVG);
|
|
62
|
+
|
|
63
|
+
/** The link icon, `size` across, centred on (x, y): in its own dark grey, or `color`. */
|
|
64
|
+
export function linkIcon(c: C2D, x: number, y: number, size: number, color?: string): void {
|
|
65
|
+
drawSvgIcon(c, LINK_ICON, 'link', x - size / 2, y - size / 2, size, color);
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
68
|
+
/**
|
|
69
|
+
* A linked measurement's badge, as the frame draws it on its 30 px chip (Figma 2795:77149): 5 in from the chip's edges,
|
|
70
|
+
* 20 across with the 12 px link icon in its middle, 10 before the value, and the value 5 from the chip's end. A linked
|
|
71
|
+
* chip is the size of a plain one (the user's call, 2026-10-25: "the same size as the regular measurement tiles"), so
|
|
72
|
+
* the badge is drawn at the chip's own height: all of it scaled by h / 30.
|
|
73
|
+
*/
|
|
74
|
+
const LINKED = { h: 30, pad: 5, badge: 20, badgeR: 3.826, icon: 12, gap: 10 } as const;
|
|
66
75
|
|
|
67
|
-
/**
|
|
68
|
-
export function
|
|
76
|
+
/** The width and height chip() draws `text` at (and its font, set on `c`). */
|
|
77
|
+
export function chipSize(c: C2D, text: string, { font = '600 12px', pad = 7, h = 22, minW = 0, mark }: ChipOpts): { w: number; h: number } {
|
|
69
78
|
c.font = font + ' ' + FONT;
|
|
70
|
-
const
|
|
71
|
-
|
|
79
|
+
const room = mark ? (LINKED.pad * 2 + LINKED.badge + LINKED.gap) * (h / LINKED.h) : pad * 2;
|
|
80
|
+
return { w: Math.max(minW, c.measureText(text).width + room), h };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Rounded label on the canvas; registers a hit region when `kind` is given. */
|
|
84
|
+
export function chip(c: C2D, text: string, cx: number, cy: number, opts: ChipOpts): number {
|
|
85
|
+
const { bg, fg, border, kind, id, rot = 0, mark } = opts;
|
|
86
|
+
const { w, h } = chipSize(c, text, opts), x = cx - w / 2, y = cy - h / 2;
|
|
72
87
|
c.save();
|
|
73
88
|
if (rot) { c.translate(cx, cy); c.rotate(rot); c.translate(-cx, -cy); }
|
|
74
89
|
rr(c, x, y, w, h, 5);
|
|
75
90
|
c.fillStyle = bg; c.fill();
|
|
76
91
|
if (border) { c.lineWidth = border[1] || 1; c.strokeStyle = border[0]; c.stroke(); }
|
|
77
|
-
c.fillStyle = fg; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
78
|
-
if (mark)
|
|
92
|
+
c.fillStyle = fg; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
93
|
+
if (mark) {
|
|
94
|
+
const k = h / LINKED.h, inset = LINKED.pad * k, bw = LINKED.badge * k, bx = x + inset, tx = bx + bw + LINKED.gap * k;
|
|
95
|
+
c.fillText(text, (tx + x + w - inset) / 2, cy + 0.5);
|
|
96
|
+
rr(c, bx, cy - bw / 2, bw, bw, LINKED.badgeR * k);
|
|
97
|
+
c.fillStyle = '#FFFFFF'; c.fill();
|
|
98
|
+
linkIcon(c, bx + bw / 2, cy, LINKED.icon * k);
|
|
99
|
+
} else c.fillText(text, cx, cy + 0.5);
|
|
79
100
|
c.restore();
|
|
80
101
|
if (kind) {
|
|
81
102
|
const ext = rot ? Math.max(w, h) / 2 : 0;
|
|
@@ -105,14 +126,43 @@ function drawGrid(c: C2D, T: Xform, m = 0): void {
|
|
|
105
126
|
function drawUnderlay(c: C2D, T: Xform): void {
|
|
106
127
|
if (!settings.layers.underlay) return;
|
|
107
128
|
for (const u of page().underlays) {
|
|
108
|
-
const im = imgCache(u.
|
|
109
|
-
|
|
129
|
+
const im = imgCache(underlaySrc(u)), ready = !!im && im.complete && !!im.naturalWidth;
|
|
130
|
+
// (an expanded photo whose picture hasn't come yet, or is gone: where it is, as its tile's card)
|
|
131
|
+
if (!ready && !u.link) continue;
|
|
110
132
|
// drawn about its middle, turned as it's set (clockwise as seen: the canvas turns that way)
|
|
111
133
|
const h = underlayH(u), m = underlayMid(u), [x, y] = S(T, m.x, m.y), w = u.w * T.s, hh = h * T.s;
|
|
112
134
|
c.save(); c.globalAlpha = u.opacity ?? 0.6;
|
|
113
135
|
c.translate(x, y);
|
|
114
136
|
if (u.rot) c.rotate((u.rot * Math.PI) / 180);
|
|
115
|
-
c.drawImage(im
|
|
137
|
+
if (ready) c.drawImage(im!, -w / 2, -hh / 2, w, hh);
|
|
138
|
+
else {
|
|
139
|
+
c.fillStyle = '#2B2C2E'; c.fillRect(-w / 2, -hh / 2, w, hh);
|
|
140
|
+
if (linkInfoOf(u.link!)?.exists === false) {
|
|
141
|
+
c.font = '500 13px ' + FONT; c.fillStyle = '#9FA0A2'; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
142
|
+
c.fillText('Removed', 0, 0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
c.restore();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Each expanded photo's badge: its link icon in a white disc in its top-right corner (imageLinkBadge), over the drawing
|
|
151
|
+
* — always, as Skia's, selected or not, locked or not: it says the photo is the job's, and a tap on it opens it.
|
|
152
|
+
*/
|
|
153
|
+
function drawImageLinks(c: C2D, T: Xform): void {
|
|
154
|
+
if (!settings.layers.underlay) return;
|
|
155
|
+
const { W, H } = cvs;
|
|
156
|
+
for (const u of page().underlays) {
|
|
157
|
+
const b = imageLinkBadge(T, u);
|
|
158
|
+
if (!b || b.x < -b.r || b.y < -b.r || b.x > W + b.r || b.y > H + b.r) continue;
|
|
159
|
+
const r = b.r, disc = (cc: C2D, X: number, Y: number) => { cc.beginPath(); cc.arc(X + r, Y + r, r, 0, Math.PI * 2); };
|
|
160
|
+
c.save();
|
|
161
|
+
if (ed.mode === 'draft') c.globalAlpha = 0.75;
|
|
162
|
+
if (c.shape) c.shape('image link ' + r, b.x - r, b.y - r, disc); else disc(c, b.x - r, b.y - r);
|
|
163
|
+
c.fillStyle = '#FFFFFF'; c.fill();
|
|
164
|
+
c.lineWidth = 1; c.strokeStyle = 'rgba(24,26,28,.2)'; c.stroke();
|
|
165
|
+
linkIcon(c, b.x, b.y, r * 1.2, '#181A1C');
|
|
116
166
|
c.restore();
|
|
117
167
|
}
|
|
118
168
|
}
|
|
@@ -131,18 +181,40 @@ function drawRooms(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
|
|
|
131
181
|
c.fill();
|
|
132
182
|
if (thumb) continue;
|
|
133
183
|
const [cx, cy] = S(T, g.c.x, g.c.y);
|
|
134
|
-
|
|
184
|
+
// (its area and perimeter once the drawing has its scale: before, only placeholders — just its name)
|
|
185
|
+
const nm = r.name, info = sk.unscaled ? '' : fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perim.';
|
|
135
186
|
c.font = '600 14px ' + FONT;
|
|
136
|
-
const
|
|
137
|
-
c.
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
const nw = c.measureText(nm).width;
|
|
188
|
+
c.font = '500 12px ' + FONT;
|
|
189
|
+
const iw = info ? c.measureText(info).width : 0;
|
|
190
|
+
const show = spaceLabel(T, g.inner, nw, iw);
|
|
191
|
+
if (!show.name) continue;
|
|
192
|
+
c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillStyle = TH.label;
|
|
140
193
|
c.font = '600 14px ' + FONT;
|
|
141
|
-
|
|
194
|
+
c.fillText(nm, cx, show.info ? cy - 9 : cy);
|
|
195
|
+
if (show.info) { c.font = '500 12px ' + FONT; c.fillStyle = TH.ink2; c.fillText(info, cx, cy + 9); }
|
|
196
|
+
const w = Math.max(nw, 140);
|
|
142
197
|
addHit({ kind: 'room', id, x: cx - w / 2, y: cy - 20, w, h: 40 });
|
|
143
198
|
}
|
|
144
199
|
}
|
|
145
200
|
|
|
201
|
+
/** Room left round a space's label on the screen, each side, points. */
|
|
202
|
+
const SPACE_LABEL_PAD = 8;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Which of a space's label shows: only what fits inside the space on the screen, so zoomed out it goes rather than
|
|
206
|
+
* cluttering the drawing (the user's call, 2026-10-29, as a tile's name goes: LINK_FULL_PX) — its area and perimeter
|
|
207
|
+
* first, then its name. `nameW`, `infoW`: their widths (no area line: 0).
|
|
208
|
+
*/
|
|
209
|
+
export function spaceLabel(T: Xform, inner: XY[], nameW: number, infoW: number): { name: boolean; info: boolean } {
|
|
210
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
211
|
+
for (const p of inner) { x0 = Math.min(x0, p.x); x1 = Math.max(x1, p.x); y0 = Math.min(y0, p.y); y1 = Math.max(y1, p.y); }
|
|
212
|
+
const w = (x1 - x0) * T.s, h = (y1 - y0) * T.s, pad = 2 * SPACE_LABEL_PAD;
|
|
213
|
+
// (one line of 14 pt; two, 18 apart, of 14 and 12)
|
|
214
|
+
const name = w >= nameW + pad && h >= 18 + pad;
|
|
215
|
+
return { name, info: name && infoW > 0 && w >= infoW + pad && h >= 36 + pad };
|
|
216
|
+
}
|
|
217
|
+
|
|
146
218
|
interface CurveWall { id: string; kind: 'circle' | 'arc'; thick: number; constr?: boolean }
|
|
147
219
|
|
|
148
220
|
function curveWalls(sk: Sketch): CurveWall[] {
|
|
@@ -333,11 +405,13 @@ function drawOpenings(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
333
405
|
for (const k of [t / 6, -t / 6]) { const a = P(s0, k), b = P(s1, k); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); }
|
|
334
406
|
c.stroke();
|
|
335
407
|
} else {
|
|
336
|
-
|
|
408
|
+
// the leaf, open square to the wall from the jamb it hangs on, and its swing back to the other
|
|
409
|
+
const [hs, ls] = hingeEdge(o) === 's1' ? [s1, s0] : [s0, s1];
|
|
410
|
+
const hinge = P(hs, (f * t) / 2), leaf = P(hs, f * (t / 2 + w));
|
|
337
411
|
c.beginPath(); c.moveTo(hinge[0], hinge[1]); c.lineTo(leaf[0], leaf[1]); c.stroke();
|
|
338
412
|
const [hx, hy] = hinge, r = w * T.s;
|
|
339
413
|
const a0 = Math.atan2(leaf[1] - hy, leaf[0] - hx);
|
|
340
|
-
const end = P(
|
|
414
|
+
const end = P(ls, (f * t) / 2);
|
|
341
415
|
const a1 = Math.atan2(end[1] - hy, end[0] - hx);
|
|
342
416
|
c.beginPath(); c.setLineDash([4, 3]); c.lineWidth = 1;
|
|
343
417
|
const ccw = normAng(a1 - a0) > Math.PI;
|
|
@@ -423,12 +497,25 @@ function drawLock(c: C2D, x: number, y: number): void {
|
|
|
423
497
|
c.restore();
|
|
424
498
|
}
|
|
425
499
|
|
|
500
|
+
/** The selected points and the selected lines' ends: what a constraint's icon still shows for with the icons off. */
|
|
501
|
+
function selPoints(sk: Sketch): Set<string> {
|
|
502
|
+
const out = new Set<string>();
|
|
503
|
+
for (const s of ed.sel) {
|
|
504
|
+
if (s.type === 'line') { const l = sk.lines[s.id]; if (l) { out.add(l.a); out.add(l.b); } }
|
|
505
|
+
if (s.type === 'point') out.add(s.id);
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
426
510
|
/** Points: filled = fully defined, hollow = still free (shape cue paired with color). */
|
|
427
511
|
function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
|
|
428
512
|
const { W, H } = cvs;
|
|
429
513
|
const touch = ed.lastPointerTouch;
|
|
430
514
|
// a cabinet's corners aren't grab points: it moves as one, like a door or window
|
|
431
515
|
const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cabPoints));
|
|
516
|
+
// a locked point's padlock is a constraint icon: with the icons off, it shows only as the badges do, on what's
|
|
517
|
+
// selected (the user's call, 2026-10-26)
|
|
518
|
+
const locks = settings.showCons ? null : selPoints(sk);
|
|
432
519
|
for (const id in sk.points) {
|
|
433
520
|
const p = sk.points[id];
|
|
434
521
|
const [x, y] = S(T, p.x, p.y);
|
|
@@ -447,47 +534,31 @@ function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
447
534
|
if (selected) { c.fillStyle = TH.yellow; c.fill(); c.lineWidth = 1.5; c.strokeStyle = TH.ink; c.stroke(); }
|
|
448
535
|
else if (det) { c.fillStyle = ed.highlight.has(id) ? TH.red : p.origin ? TH.ink : inkForPoint(sk, id); c.fill(); }
|
|
449
536
|
else { c.fillStyle = TH.bg; c.fill(); c.lineWidth = 1.8; c.strokeStyle = TH.ink; c.stroke(); }
|
|
450
|
-
if (p.fixed && !p.origin && !p.anchor) drawLock(c, x + 9, y - 14);
|
|
537
|
+
if (p.fixed && !p.origin && !p.anchor && (!locks || locks.has(id) || ed.highlight.has(id))) drawLock(c, x + 9, y - 14);
|
|
451
538
|
}
|
|
452
539
|
}
|
|
453
540
|
|
|
454
541
|
type DimAnchor =
|
|
455
542
|
| { m: XY; a: XY; b: XY; nx?: number; ny?: number; L?: number }
|
|
456
543
|
| { m: XY; o: XY; r: number }
|
|
457
|
-
| { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number] };
|
|
544
|
+
| { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number]; len1?: number; len2?: number };
|
|
458
545
|
|
|
459
546
|
function dimAnchor(sk: Sketch, c: Constraint): DimAnchor | null {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
547
|
+
if (c.type === 'dist' || c.type === 'pldist') {
|
|
548
|
+
// (a length measured on a wall's face: from its corner to corner, the label off the face on that side. Between
|
|
549
|
+
// parallel lines or walls: from face to face, slid along them.)
|
|
463
550
|
const e = dimEnds(sk, c);
|
|
464
551
|
if (!e) return null;
|
|
465
552
|
const { a, b } = e;
|
|
466
553
|
const dx = b.x - a.x, dy = b.y - a.y, L = hyp(dx, dy) || 1e-9, nx = -dy / L, ny = dx / L, off = c.off || 0;
|
|
467
554
|
return { m: { x: (a.x + b.x) / 2 + nx * off, y: (a.y + b.y) / 2 + ny * off }, a, b, nx, ny, L };
|
|
468
555
|
}
|
|
469
|
-
if (c.type === 'pldist') {
|
|
470
|
-
// (between walls' faces: from face to face)
|
|
471
|
-
const e = dimEnds(sk, c);
|
|
472
|
-
if (!e) return null;
|
|
473
|
-
return { m: { x: (e.a.x + e.b.x) / 2, y: (e.a.y + e.b.y) / 2 }, a: e.a, b: e.b };
|
|
474
|
-
}
|
|
475
556
|
if (c.type === 'diam') {
|
|
476
557
|
// (across a curved wall's face: its arrows on that face)
|
|
477
558
|
const g = dimRadius(sk, c);
|
|
478
559
|
return g ? { m: { x: g.o.x, y: g.o.y }, o: g.o, r: g.r } : null;
|
|
479
560
|
}
|
|
480
|
-
if (c.type === 'angle')
|
|
481
|
-
const l1 = sk.lines[c.l1!], l2 = sk.lines[c.l2!];
|
|
482
|
-
const sh = [l1.a, l1.b].find(p => p === l2.a || p === l2.b);
|
|
483
|
-
if (sh) {
|
|
484
|
-
const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
|
|
485
|
-
const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y);
|
|
486
|
-
return { vtx: s, bis: norm2(u1[0] + u2[0], u1[1] + u2[1]), u1, u2 };
|
|
487
|
-
}
|
|
488
|
-
const a = P(l1.a), b = P(l1.b);
|
|
489
|
-
return { vtx: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, bis: [0, 1] };
|
|
490
|
-
}
|
|
561
|
+
if (c.type === 'angle') return angleCorner(sk, c);
|
|
491
562
|
return null;
|
|
492
563
|
}
|
|
493
564
|
|
|
@@ -509,35 +580,50 @@ function drawDims(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
509
580
|
if (!settings.layers.dims) return;
|
|
510
581
|
for (const id in sk.cons) {
|
|
511
582
|
const d = sk.cons[id];
|
|
512
|
-
if (!isDim(d) || d.
|
|
583
|
+
if (!isDim(d) || d.elev) continue; // the wall view's own dimensions are its chips
|
|
513
584
|
const A = dimAnchor(sk, d);
|
|
514
585
|
if (!A) continue;
|
|
515
586
|
const ref = d.driving === false, val = ref ? measure(sk, d) : d.value!;
|
|
516
587
|
const selected = isSel('dim', id) || isSel('cons', id);
|
|
517
588
|
const bad = ed.highlight.has(id) || ed.ana.bad.has(id);
|
|
518
589
|
if (ref && !bad && !dimLive(sk, id, d)) continue;
|
|
519
|
-
// a measurement of the job sets it:
|
|
590
|
+
// a measurement of the job sets it: the link badge before its value (chip()), on the chip's own colour — red once
|
|
591
|
+
// the job's measurement is gone
|
|
520
592
|
const linked = !!d.measurementRef && !ref && !bad, gone = linked && linkInfoOf(d.measurementRef!)?.exists === false;
|
|
521
593
|
let bg = ref ? TH.ref : TH.blue, fg = ref ? TH.reftext : '#fff';
|
|
522
|
-
if (bad) { bg = TH.red; fg = '#fff'; }
|
|
523
|
-
|
|
524
|
-
const border: [string, number] | null = selected ? [TH.yellow, 2.5] : linked ? [fg, 1.5] : null;
|
|
594
|
+
if (bad || gone) { bg = TH.red; fg = '#fff'; }
|
|
595
|
+
const border: [string, number] | null = selected ? [TH.yellow, 2.5] : null;
|
|
525
596
|
const mark = linked ? 'link' as const : undefined;
|
|
526
597
|
const wrap = (s: string) => (ref ? '(' : '') + s + (ref ? ')' : '');
|
|
527
598
|
if (d.type === 'angle' && 'vtx' in A) {
|
|
528
599
|
const [vx, vy] = S(T, A.vtx.x, A.vtx.y);
|
|
600
|
+
// pulled off its corner: its arc out where the label is, the label riding on it; else a little arc at the
|
|
601
|
+
// corner, the label out along the bisector
|
|
602
|
+
const moved = angleLabel(sk, d), R = moved ? moved.r * T.s : 22;
|
|
529
603
|
if (A.u1 && A.u2) {
|
|
530
604
|
// arc between the two lines, so the angle reads as belonging to that corner
|
|
531
605
|
const t1 = Math.atan2(-A.u1[1], A.u1[0]), t2 = Math.atan2(-A.u2[1], A.u2[0]);
|
|
532
606
|
let sweep = t2 - t1;
|
|
533
607
|
while (sweep > Math.PI) sweep -= 2 * Math.PI;
|
|
534
608
|
while (sweep < -Math.PI) sweep += 2 * Math.PI;
|
|
535
|
-
|
|
609
|
+
// (blue, as every other dimension is — the user's call, 2026-10-29; purple is the inference guides')
|
|
610
|
+
c.save(); c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue; c.lineWidth = 1.2;
|
|
536
611
|
if (ref) c.setLineDash([3, 3]);
|
|
537
|
-
c.beginPath(); c.arc(vx, vy,
|
|
612
|
+
c.beginPath(); c.arc(vx, vy, R, t1, t1 + sweep, sweep < 0); c.stroke();
|
|
613
|
+
// (pulled out past where a line ends: an extension line along it, out to the arc)
|
|
614
|
+
c.lineWidth = 1;
|
|
615
|
+
for (const [u, len] of [[A.u1, A.len1], [A.u2, A.len2]] as const) {
|
|
616
|
+
const from = (len ?? 0) * T.s;
|
|
617
|
+
if (moved && R > from + 2) { c.beginPath(); c.moveTo(vx + u[0] * from, vy - u[1] * from); c.lineTo(vx + u[0] * (R + 4), vy - u[1] * (R + 4)); c.stroke(); }
|
|
618
|
+
}
|
|
619
|
+
c.restore();
|
|
538
620
|
}
|
|
539
|
-
|
|
540
|
-
|
|
621
|
+
const text = wrap(fmtAng(val)), opts: ChipOpts = { bg, fg, border, kind: 'dim', id, mark };
|
|
622
|
+
// (left where it was drawn: 50 px out along the bisector, or as far as its corners stay clear of the corner — a
|
|
623
|
+
// linked chip is wider)
|
|
624
|
+
const sz = chipSize(c, text, opts), out = Math.max(50, Math.hypot(sz.w, sz.h) / 2 + 28);
|
|
625
|
+
const [lx, ly] = moved ? [vx + Math.cos(moved.ang) * R, vy - Math.sin(moved.ang) * R] : [vx + A.bis[0] * out, vy - A.bis[1] * out];
|
|
626
|
+
chip(c, text, lx, ly, opts);
|
|
541
627
|
continue;
|
|
542
628
|
}
|
|
543
629
|
if (d.type === 'diam' && 'o' in A) {
|
|
@@ -547,7 +633,7 @@ function drawDims(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
547
633
|
if (!('a' in A)) continue;
|
|
548
634
|
const [mx, my] = S(T, A.m.x, A.m.y);
|
|
549
635
|
const lineColor = bad ? TH.red : ref ? TH.chipline : TH.blue;
|
|
550
|
-
if (d.type === 'dist' && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
|
|
636
|
+
if ((d.type === 'dist' || d.type === 'pldist') && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
|
|
551
637
|
drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny!, d.off!, lineColor, ref);
|
|
552
638
|
} else if (d.type === 'pldist' || !d.line) {
|
|
553
639
|
// (dashed only when it's a reference: a dashed line means the number holds nothing, wherever it's drawn)
|
|
@@ -720,7 +806,8 @@ export function drawRubberBand(c: C2D, T: Xform, from: XY, to: XY, snapped: bool
|
|
|
720
806
|
if (snapped) endRing(c, x2, y2, TH.blue);
|
|
721
807
|
c.restore();
|
|
722
808
|
const L = hyp(to.x - from.x, to.y - from.y);
|
|
723
|
-
|
|
809
|
+
// (no distance on a drawing without its scale yet: it's only a placeholder)
|
|
810
|
+
if (L * T.s > 24 && !planSK().unscaled) chip(c, fmtLen(L), (x1 + x2) / 2, (y1 + y2) / 2 - 16, { bg: '#fff', fg: TH.blue, border: [TH.blue, 1.5] });
|
|
724
811
|
}
|
|
725
812
|
|
|
726
813
|
/**
|
|
@@ -751,7 +838,8 @@ export function drawPendingDim(c: C2D, T: Xform, p: { from: XY; to: XY; a: XY; b
|
|
|
751
838
|
for (const q of [p.from, p.to]) { const [x, y] = S(T, q.x, q.y); endRing(c, x, y, TH.yellow); }
|
|
752
839
|
c.restore();
|
|
753
840
|
const [mx, my] = S(T, (p.a.x + p.b.x) / 2 + p.nx * p.off, (p.a.y + p.b.y) / 2 + p.ny * p.off);
|
|
754
|
-
|
|
841
|
+
// (no label until one's typed, on a page whose sizes are still placeholders)
|
|
842
|
+
if (p.label) chip(c, p.label, mx, my - (Math.abs(p.off * T.s) > 1 ? 0 : 16), { bg: TH.blue, fg: '#fff', border: [TH.yellow, 2] });
|
|
755
843
|
}
|
|
756
844
|
|
|
757
845
|
/**
|
|
@@ -800,54 +888,10 @@ function drawDiameter(c: C2D, T: Xform, o: XY, r: number, d: Constraint, text: s
|
|
|
800
888
|
arrowHead(c, p1, sx, sy);
|
|
801
889
|
if (!d.rad) arrowHead(c, p2, -sx, -sy);
|
|
802
890
|
c.restore();
|
|
803
|
-
|
|
804
|
-
const w = c.measureText(text).width + 14 + (chipOpts.mark ? MARK_W : 0);
|
|
891
|
+
const { w } = chipSize(c, text, chipOpts);
|
|
805
892
|
chip(c, text, end[0] + side * (w / 2 + 2), end[1], chipOpts);
|
|
806
893
|
}
|
|
807
894
|
|
|
808
|
-
/** Architectural dimension strings on the Print dimensions layer (reference-only). */
|
|
809
|
-
function drawPrintDims(c: C2D, T: Xform, sk: Sketch): void {
|
|
810
|
-
if (!settings.layers.print) return;
|
|
811
|
-
for (const id in sk.cons) {
|
|
812
|
-
const d = sk.cons[id];
|
|
813
|
-
if (d.layer !== 'print' || d.type !== 'dist') continue;
|
|
814
|
-
const a = sk.points[d.a!], b = sk.points[d.b!];
|
|
815
|
-
if (!a || !b) continue;
|
|
816
|
-
const dx = b.x - a.x, dy = b.y - a.y, L = hyp(dx, dy);
|
|
817
|
-
if (L < 1e-6) continue;
|
|
818
|
-
const nx = -dy / L, ny = dx / L, off = d.off || 24;
|
|
819
|
-
const pa = { x: a.x + nx * off, y: a.y + ny * off }, pb = { x: b.x + nx * off, y: b.y + ny * off };
|
|
820
|
-
const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y), [qax, qay] = S(T, pa.x, pa.y), [qbx, qby] = S(T, pb.x, pb.y);
|
|
821
|
-
const sgn = Math.sign(off) || 1, ex = nx * sgn * 6, ey = -ny * sgn * 6;
|
|
822
|
-
const selected = isSel('dim', id);
|
|
823
|
-
c.strokeStyle = selected ? TH.yellow : TH.ink; c.lineWidth = 1;
|
|
824
|
-
c.beginPath();
|
|
825
|
-
const gap = ax - qax || ay - qay ? 4 : 0, ga = norm2(qax - ax, qay - ay);
|
|
826
|
-
c.moveTo(ax + ga[0] * gap, ay + ga[1] * gap); c.lineTo(qax + ex, qay + ey);
|
|
827
|
-
c.moveTo(bx + ga[0] * gap, by + ga[1] * gap); c.lineTo(qbx + ex, qby + ey);
|
|
828
|
-
c.moveTo(qax, qay); c.lineTo(qbx, qby);
|
|
829
|
-
c.stroke();
|
|
830
|
-
const u = norm2(qbx - qax, qby - qay);
|
|
831
|
-
c.lineWidth = 1.8; c.beginPath();
|
|
832
|
-
for (const [x, y] of [[qax, qay], [qbx, qby]]) {
|
|
833
|
-
const tx = ((u[0] - u[1]) * 5) / Math.SQRT2, ty = ((u[1] + u[0]) * 5) / Math.SQRT2;
|
|
834
|
-
c.moveTo(x - tx, y - ty); c.lineTo(x + tx, y + ty);
|
|
835
|
-
}
|
|
836
|
-
c.stroke();
|
|
837
|
-
let ang = Math.atan2(qby - qay, qbx - qax);
|
|
838
|
-
if (ang > Math.PI / 2) ang -= Math.PI;
|
|
839
|
-
if (ang < -Math.PI / 2) ang += Math.PI;
|
|
840
|
-
const mx = (qax + qbx) / 2, my = (qay + qby) / 2, txt = fmtLen(L);
|
|
841
|
-
c.save(); c.translate(mx, my); c.rotate(ang);
|
|
842
|
-
c.font = '600 12px ' + FONT;
|
|
843
|
-
const w = c.measureText(txt).width + 8;
|
|
844
|
-
c.fillStyle = TH.bg; c.fillRect(-w / 2, -17, w, 14);
|
|
845
|
-
c.fillStyle = selected ? TH.yellow : TH.ink; c.textAlign = 'center'; c.textBaseline = 'alphabetic'; c.fillText(txt, 0, -6);
|
|
846
|
-
c.restore();
|
|
847
|
-
addHit({ kind: 'pdim', id, x: mx - w / 2 - 4, y: my - 18, w: w + 8, h: 24 });
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
895
|
/**
|
|
852
896
|
* A badge's symbol drawn as lines rather than a character: ∥ ⊥ ∘ ◎ aren't in every typeface — on a device the
|
|
853
897
|
* app's own (Inter) lacks them and Skia has nothing to fall back on, so they came out as the font's "missing glyph"
|
|
@@ -914,11 +958,7 @@ function badgeSpots(T: Xform, sk: Sketch, c: Constraint): [number, number][] {
|
|
|
914
958
|
|
|
915
959
|
function drawBadges(c: C2D, T: Xform, sk: Sketch): void {
|
|
916
960
|
const { W, H } = cvs;
|
|
917
|
-
const selIds = new Set(ed.sel.map(s => s.id)), selPts =
|
|
918
|
-
for (const s of ed.sel) {
|
|
919
|
-
if (s.type === 'line') { const l = sk.lines[s.id]; if (l) { selPts.add(l.a); selPts.add(l.b); } }
|
|
920
|
-
if (s.type === 'point') selPts.add(s.id);
|
|
921
|
-
}
|
|
961
|
+
const selIds = new Set(ed.sel.map(s => s.id)), selPts = selPoints(sk);
|
|
922
962
|
for (const id in sk.cons) {
|
|
923
963
|
const k = sk.cons[id];
|
|
924
964
|
if (isDim(k) || k.elev) continue;
|
|
@@ -1162,7 +1202,6 @@ function paintPlanView(ctx: C2D, sk: Sketch, m: number, under = true): void {
|
|
|
1162
1202
|
if (settings.layers.rooms) drawRooms(ctx, T, sk);
|
|
1163
1203
|
if (settings.layers.cabinets) drawCabinetFootprints(ctx, T, sk);
|
|
1164
1204
|
drawWalls(ctx, T, sk); drawOpenings(ctx, T, sk); drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
|
|
1165
|
-
drawPrintDims(ctx, T, sk);
|
|
1166
1205
|
if (ed.mode === 'draft') drawBadges(ctx, T, sk);
|
|
1167
1206
|
// (the drawing's points only in Draft: Annotate can't move them, and shouldn't look as if it could — the user's
|
|
1168
1207
|
// call, 2026-10-21)
|
|
@@ -1170,6 +1209,8 @@ function paintPlanView(ctx: C2D, sk: Sketch, m: number, under = true): void {
|
|
|
1170
1209
|
drawOpeningAnchors(ctx, T, sk);
|
|
1171
1210
|
drawDims(ctx, T, sk);
|
|
1172
1211
|
drawOpeningDims(ctx, T, sk);
|
|
1212
|
+
// (an expanded photo's badge over the drawing, under the markup: as it's tapped)
|
|
1213
|
+
drawImageLinks(ctx, T);
|
|
1173
1214
|
drawNotes(ctx, T, sk);
|
|
1174
1215
|
drawNoteSelection(ctx, T);
|
|
1175
1216
|
}
|
|
@@ -1190,7 +1231,6 @@ function paintWall(ctx: C2D, W: number, fixed: C2D): void {
|
|
|
1190
1231
|
drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
|
|
1191
1232
|
// (over the edges — its own and its neighbours' — so a selected cabinet's outline shows all the way round)
|
|
1192
1233
|
drawWallSelection(ctx, T, plan, f, sk);
|
|
1193
|
-
drawPrintDims(ctx, T, sk);
|
|
1194
1234
|
drawBadges(ctx, T, sk);
|
|
1195
1235
|
if (ed.mode === 'draft') drawPoints(ctx, T, sk);
|
|
1196
1236
|
drawDims(ctx, T, sk);
|
package/src/render/elevation.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { cabPoints, cornerAnchor, emptyFace, openingAnchor } from '../model/face
|
|
|
5
5
|
import { edgeS, openingSpan, pickBetween, refS, sameRef, widthEntered } from '../model/openings';
|
|
6
6
|
import type { Constraint, OpeningDim, OpeningEdge, OpeningRef, Sketch, WallFace } from '../model/types';
|
|
7
7
|
import { fmtLen } from '../units';
|
|
8
|
-
import { chip, linkBlue,
|
|
8
|
+
import { chip, linkBlue, linkIcon } from './draw';
|
|
9
9
|
import { FONT, TH } from './theme';
|
|
10
10
|
import { S, type Xform } from './view';
|
|
11
11
|
import type { DrawCtx } from './ctx';
|
|
@@ -35,7 +35,8 @@ export interface ElevHit { x: number; y: number; w: number; h: number; act: Elev
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Chip look: locked blue, reference grey in parentheses, unlocked (just measured) neutral grey, red when it can't be met,
|
|
38
|
-
* and 'linked': locked, but set in another view (the floor plan) — an outlined blue chip with
|
|
38
|
+
* and 'linked': locked, but set in another view (the floor plan) — an outlined blue chip with the link icon, read-only
|
|
39
|
+
* here. (Not a linked measurement's chip: that's one of the job's, set from outside the drawing — chip()'s `mark`.)
|
|
39
40
|
*/
|
|
40
41
|
type Look = 'drive' | 'ref' | 'free' | 'bad' | 'linked';
|
|
41
42
|
const LOOK = (look: Look) => look === 'drive' ? { bg: TH.blue, fg: '#fff', line: TH.blue } : look === 'bad' ? { bg: TH.red, fg: '#fff', line: TH.red }
|
|
@@ -75,7 +76,7 @@ function dim(c: C2D, p: [number, number], q: [number, number], nx: number, ny: n
|
|
|
75
76
|
// a number that holds nothing (dashed) reads in parentheses, as in the plan
|
|
76
77
|
const linked = look === 'linked', text = dashed ? '(' + label + ')' : linked ? ' ' + label : label; // (figure spaces: room for the link mark)
|
|
77
78
|
const w = chip(c, text, cx, cy, { bg: st.bg, fg: st.fg, font: '600 11.5px', h, pad: 6, border: selected ? [TH.yellow, 2] : linked ? [st.line, 1.5] : null });
|
|
78
|
-
if (linked)
|
|
79
|
+
if (linked) linkIcon(c, cx - w / 2 + 11, cy, 10, st.fg);
|
|
79
80
|
return { x: cx - w / 2 - 4, y: cy - h / 2 - 4, w: w + 8, h: h + 8 };
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -433,7 +434,7 @@ export function drawWallChips(c: C2D, T: Xform, sk: Sketch, f: ElevFrame, face:
|
|
|
433
434
|
}
|
|
434
435
|
next += lostRows.length;
|
|
435
436
|
// the wall's length comes from the floor plan: linked (outlined blue) when a dimension locks it there, grey when free
|
|
436
|
-
const planLocked = Object.values(sk.cons).some(cn => cn.type === 'dist' && cn.line === f.lid && cn.driving !== false
|
|
437
|
+
const planLocked = Object.values(sk.cons).some(cn => cn.type === 'dist' && cn.line === f.lid && cn.driving !== false);
|
|
437
438
|
const anyRow = rows.some(r => r.length) || next > 1;
|
|
438
439
|
tag(S(T, f.face[0], 0), S(T, f.face[1], 0), 0, anyRow ? ROW0 + ROW * next : ROW0, fmtLen(f.face[1] - f.face[0]), planLocked ? 'linked' : 'free', { k: 'len' });
|
|
439
440
|
// the wall's height: blue once entered, grey while it's the default
|
package/src/render/figmaIcons.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Icons drawn in a Figma frame, kept exactly as Figma exports them (their own size, viewBox and colours) rather than
|
|
3
|
-
redrawn on the rail's 24 grid: the Pages panel's Delete and + Page (Figma 2548:101989)
|
|
3
|
+
redrawn on the rail's 24 grid: the Pages panel's Delete and + Page (Figma 2548:101989), and the link icon on a
|
|
4
|
+
linked measurement's chip (Figma 2795:77149), which the canvas draws from its paths (render/svgPath.ts).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
/** Delete page: a trash can, white, for the red pill. */
|
|
@@ -20,3 +21,18 @@ export const PLUS_SVG = `<svg preserveAspectRatio="none" overflow="visible" styl
|
|
|
20
21
|
<path id="Vector" d="M9.25 0.125C9.58152 0.125 9.89937 0.256791 10.1338 0.491211C10.3682 0.725631 10.5 1.04348 10.5 1.375V8H17.125C17.4565 8 17.7744 8.13179 18.0088 8.36621C18.2432 8.60063 18.375 8.91848 18.375 9.25C18.375 9.58152 18.2432 9.89937 18.0088 10.1338C17.7744 10.3682 17.4565 10.5 17.125 10.5H10.5V17.125C10.5 17.4565 10.3682 17.7744 10.1338 18.0088C9.89937 18.2432 9.58152 18.375 9.25 18.375C8.91848 18.375 8.60063 18.2432 8.36621 18.0088C8.13179 17.7744 8 17.4565 8 17.125V10.5H1.375C1.04348 10.5 0.725631 10.3682 0.491211 10.1338C0.256791 9.89937 0.125 9.58152 0.125 9.25C0.125 8.91848 0.256791 8.60063 0.491211 8.36621C0.725631 8.13179 1.04348 8 1.375 8H8V1.375C8 1.04348 8.13179 0.725631 8.36621 0.491211C8.60063 0.256791 8.91848 0.125 9.25 0.125Z" fill="#181A1C" stroke="#181A1C" stroke-width="0.25"/>
|
|
21
22
|
</svg>`;
|
|
22
23
|
export const PLUS_SIZE = { width: 18.5, height: 18.5 };
|
|
24
|
+
|
|
25
|
+
/** Link: a chain link, dark grey, for the white badge on a linked measurement's chip. */
|
|
26
|
+
export const LINK_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<g id="link-svgrepo-com (2) 1" clip-path="url(#clip0_0_4)">
|
|
28
|
+
<path id="Vector" d="M5.28769 1.15165C6.02508 0.414261 7.02519 0 8.06805 0C10.2396 0 12 1.76041 12 3.93198C12 4.97481 11.5858 5.97492 10.8484 6.71231L9.31065 8.25L8.25 7.18934L9.78772 5.65165C10.2438 5.19557 10.5 4.57698 10.5 3.93198C10.5 2.58884 9.41115 1.5 8.06805 1.5C7.42302 1.5 6.80444 1.75622 6.34835 2.21231L4.81066 3.75L3.75 2.68934L5.28769 1.15165Z" fill="#181A1C"/>
|
|
29
|
+
<path id="Vector_2" d="M5.65165 9.78772L7.18934 8.25L8.25 9.31065L6.71231 10.8484C5.97492 11.5858 4.97481 12 3.93198 12C1.76041 12 0 10.2396 0 8.06805C0 7.0252 0.41426 6.02508 1.15165 5.28769L2.68934 3.75L3.75 4.81066L2.21231 6.34835C1.75622 6.80444 1.5 7.42302 1.5 8.06805C1.5 9.41115 2.58884 10.5 3.93198 10.5C4.57698 10.5 5.19557 10.2438 5.65165 9.78772Z" fill="#181A1C"/>
|
|
30
|
+
<path id="Vector_3" d="M4.28039 8.78136L8.78038 4.28137L7.71973 3.2207L3.21973 7.72071L4.28039 8.78136Z" fill="#181A1C"/>
|
|
31
|
+
</g>
|
|
32
|
+
<defs>
|
|
33
|
+
<clipPath id="clip0_0_4">
|
|
34
|
+
<rect width="12" height="12" fill="white"/>
|
|
35
|
+
</clipPath>
|
|
36
|
+
</defs>
|
|
37
|
+
</svg>`;
|
|
38
|
+
export const LINK_SIZE = { width: 12, height: 12 };
|
package/src/render/hit.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { roomValid } from '../model/sketch';
|
|
|
6
6
|
import { page, SK } from '../model/store';
|
|
7
7
|
import { edgePoint } from '../model/openings';
|
|
8
8
|
import { elevOpen, wallBodyAt } from '../editor/elevation';
|
|
9
|
-
import type { Note, OpeningEdge } from '../model/types';
|
|
9
|
+
import type { LinkTarget, Note, OpeningEdge, Underlay } from '../model/types';
|
|
10
10
|
import { cabinetFootprints } from '../model/facesolve';
|
|
11
11
|
import { arcAngles, curveFrame, edgeId, OPENING_TICK_EXT, openingFrame, wallThick } from '../model/planGeom';
|
|
12
12
|
import { noteLook, shapeOutline } from '../model/markup';
|
|
13
13
|
import { smoothPolyline } from '../model/smooth';
|
|
14
|
-
import { leaderPath, linkBox, mainT, noteHandles, noteTextBox, S, s2w, underlayH, underlayHandles, underlayLocal, type Xform } from './view';
|
|
14
|
+
import { imageLinkBadge, leaderPath, linkBox, mainT, noteHandles, noteTextBox, S, s2w, underlayH, underlayHandles, underlayLocal, type Xform } from './view';
|
|
15
15
|
|
|
16
16
|
/** The handle closest to (sx, sy) within `tol` px (small boxes put handles close together; take the nearest, not the first). */
|
|
17
17
|
function nearestHandle<H extends { x: number; y: number }>(hs: H[], sx: number, sy: number, tol: number): H | null {
|
|
@@ -42,6 +42,20 @@ export function linkAt(sx: number, sy: number, touch = false): (Note & { link: N
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The expanded photo whose badge is under a screen point (its link icon, in its corner: a tap opens what it's for —
|
|
47
|
+
* a view-only one too), the top one. The badges are drawn over every image, so they're picked before any.
|
|
48
|
+
*/
|
|
49
|
+
export function imageLinkAt(sx: number, sy: number, touch = false): (Underlay & { link: LinkTarget }) | null {
|
|
50
|
+
if (!settings.layers.underlay || elevOpen()) return null;
|
|
51
|
+
const T = mainT(), ims = page().underlays;
|
|
52
|
+
for (let i = ims.length - 1; i >= 0; i--) {
|
|
53
|
+
const b = imageLinkBadge(T, ims[i]);
|
|
54
|
+
if (b && hyp(sx - b.x, sy - b.y) <= b.r + (touch ? 6 : 2)) return ims[i] as Underlay & { link: LinkTarget };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
function notePointHit(n: Note, sx: number, sy: number, T: Xform, tol: number): boolean {
|
|
46
60
|
if (n.type === 'link') return !!linkHit(T, n, sx, sy, tol);
|
|
47
61
|
if (n.type === 'text') {
|
|
@@ -129,6 +143,9 @@ export function hitAny(sx: number, sy: number, { touch = false, only = null as s
|
|
|
129
143
|
if (notePointHit(n, sx, sy, T, tol)) return { type: 'note', id: n.id };
|
|
130
144
|
}
|
|
131
145
|
if (!only) {
|
|
146
|
+
// an expanded photo's badge: a tap opens what it's for, a drag moves the photo
|
|
147
|
+
const lu = imageLinkAt(sx, sy, touch);
|
|
148
|
+
if (lu) return { type: 'underlay', id: lu.id, part: 'link' };
|
|
132
149
|
// the topmost image under the point
|
|
133
150
|
const [wx, wy] = s2w(sx, sy);
|
|
134
151
|
for (let i = ims.length - 1; i >= 0; i--) {
|
|
@@ -145,7 +162,7 @@ export function hitAny(sx: number, sy: number, { touch = false, only = null as s
|
|
|
145
162
|
const h = chips[i];
|
|
146
163
|
if (h.weak) continue;
|
|
147
164
|
if (sx >= h.x && sx <= h.x + h.w && sy >= h.y && sy <= h.y + h.h) {
|
|
148
|
-
if (h.kind === 'dim'
|
|
165
|
+
if (h.kind === 'dim') return { type: 'dim', id: h.id };
|
|
149
166
|
if (h.kind === 'cons') return { type: 'cons', id: h.id };
|
|
150
167
|
if (h.kind === 'odim') return { type: 'odim', id: h.id, part: h.part };
|
|
151
168
|
if (h.kind === 'room') return { type: 'room', id: h.id };
|
|
@@ -219,7 +236,7 @@ export function hitAny(sx: number, sy: number, { touch = false, only = null as s
|
|
|
219
236
|
const [x, y] = S(T, g.o.x, g.o.y);
|
|
220
237
|
const [wx, wy] = s2w(sx, sy);
|
|
221
238
|
const th = Math.atan2(wy - g.o.y, wx - g.o.x);
|
|
222
|
-
const sweep = normAng(g.t1 - g.t0), at = normAng(th - g.t0);
|
|
239
|
+
const sweep = g.full ? 2 * Math.PI : normAng(g.t1 - g.t0), at = normAng(th - g.t0);
|
|
223
240
|
const tw = sk.arcs[id].kind === 'wall' ? Math.max(0, ((sk.arcs[id].thick || 4.5) * T.s) / 2 - tol / 2) : 0;
|
|
224
241
|
if (at <= sweep) consider('arc', id, Math.abs(hyp(x - sx, y - sy) - g.r * T.s) - tw);
|
|
225
242
|
}
|