@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.
- 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 +6 -8
- 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 +255 -181
- 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 +4 -3
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +4 -2
- package/dist/editor/laser.js +4 -2
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -12
- package/dist/editor/linkActions.js +164 -35
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/links.d.ts +13 -0
- package/dist/editor/links.js +7 -1
- package/dist/editor/links.js.map +1 -1
- package/dist/editor/mode.d.ts +2 -1
- package/dist/editor/mode.js +8 -5
- 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 +10 -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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.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/geom.d.ts +2 -0
- package/dist/model/geom.js +11 -0
- package/dist/model/geom.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 +10 -2
- package/dist/model/links.js +15 -3
- 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 +123 -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 +26 -2
- package/dist/model/sketch.js +77 -22
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/trim.d.ts +54 -0
- package/dist/model/trim.js +219 -0
- package/dist/model/trim.js.map +1 -0
- package/dist/model/types.d.ts +62 -9
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +144 -46
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/CadEditor.js +18 -9
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Drawer.js +6 -4
- 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/PlanCanvas.web.js +17 -5
- package/dist/react/PlanCanvas.web.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/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.js.map +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/ToolTip.js +3 -2
- package/dist/react/ToolTip.js.map +1 -1
- package/dist/react/TopBar.js +4 -3
- package/dist/react/TopBar.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/editorBox.d.ts +8 -0
- package/dist/react/editorBox.js +10 -0
- package/dist/react/editorBox.js.map +1 -0
- package/dist/react/host.d.ts +1 -1
- package/dist/react/host.js +1 -1
- package/dist/react/host.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/react/pageLock.native.d.ts +2 -0
- package/dist/react/pageLock.native.js +2 -0
- package/dist/react/pageLock.native.js.map +1 -1
- package/dist/react/pageLock.web.d.ts +6 -0
- package/dist/react/pageLock.web.js +12 -3
- package/dist/react/pageLock.web.js.map +1 -1
- package/dist/react/ports.js +8 -3
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +7 -1
- package/dist/react/saving.js +10 -2
- package/dist/react/saving.js.map +1 -1
- package/dist/react/store.d.ts +5 -2
- package/dist/react/store.js +4 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +203 -151
- 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 +10 -1
- package/dist/render/snap.js +87 -15
- 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 +38 -6
- 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 +53 -22
- 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.d.ts +32 -4
- package/dist/tools/modify.js +253 -98
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +14 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +30 -8
- package/dist/tools/select.js +165 -34
- 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 +6 -8
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +213 -122
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +3 -2
- package/src/editor/laser.ts +7 -4
- package/src/editor/linkActions.ts +169 -31
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +8 -5
- 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 +9 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/geom.ts +10 -0
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +14 -3
- 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 +107 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +62 -11
- package/src/model/stroke.ts +2 -2
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +57 -9
- package/src/react/BottomBar.tsx +177 -52
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +17 -12
- 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/PlanCanvas.web.tsx +17 -5
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/Sheet.tsx +3 -3
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +1 -1
- package/src/react/motion.tsx +6 -6
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +7 -2
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +169 -118
- 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 +87 -17
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +43 -7
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +52 -22
- 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 +210 -70
- package/src/tools/registry.ts +18 -1
- package/src/tools/select.ts +144 -36
- 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/dist/react/LaserTray.d.ts +0 -4
- package/dist/react/LaserTray.js +0 -26
- package/dist/react/LaserTray.js.map +0 -1
- package/src/react/LaserTray.tsx +0 -42
package/src/react/saving.ts
CHANGED
|
@@ -32,10 +32,18 @@ function setState(s: CadSaveState): void {
|
|
|
32
32
|
notify();
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Start manual saving for the drawing open in `sync`; `listener` hears each change of state, and the first, 'saved',
|
|
37
|
+
* once the editor has mounted (announceSaveState). This runs as the editor first renders, and a host keeps the state in
|
|
38
|
+
* its own: React won't have one component set another's state while it renders.
|
|
39
|
+
*/
|
|
36
40
|
export function startManualSave(sync: CadSync, listener: ((s: CadSaveState) => void) | null): void {
|
|
37
41
|
Object.assign(sv, { manual: true, state: 'saved', cleanSeq: ed.editSeq, sync, listener });
|
|
38
|
-
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tell the host the state as it is now (the editor, once it has mounted). */
|
|
45
|
+
export function announceSaveState(): void {
|
|
46
|
+
if (sv.manual) sv.listener?.(sv.state);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export function stopManualSave(): void {
|
package/src/react/store.ts
CHANGED
|
@@ -55,8 +55,11 @@ export const ui = sessionState(() => ({
|
|
|
55
55
|
palette: false,
|
|
56
56
|
/** what the tool just picked from the rail does (a note under the tabs that goes by itself); seq tells one pick from the next */
|
|
57
57
|
toolTip: null as { text: string; seq: number } | null,
|
|
58
|
-
/**
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* what's open above the bar: the selected image's opacity or turn, or the link tiles' size (sliders), or Open's
|
|
60
|
+
* question (here, or side by side)
|
|
61
|
+
*/
|
|
62
|
+
imgPop: null as null | 'opacity' | 'rotate' | 'tilesize' | 'open',
|
|
60
63
|
/** the tool each shared place on the rail draws with (the last picked; editor/mode.ts TOOL_GROUPS), and the menu of
|
|
61
64
|
a group's tools, open beside its place at x, y */
|
|
62
65
|
picked: { shapes: 'line', marks: 'arrow' } as Record<string, string>,
|
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,52 @@ 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
|
-
|
|
137
|
+
if (ready) {
|
|
138
|
+
// cropped: the picture whole, past the part that shows, clipped to it
|
|
139
|
+
const k = u.crop;
|
|
140
|
+
if (k) { c.beginPath(); c.rect(-w / 2, -hh / 2, w, hh); c.clip(); }
|
|
141
|
+
const fw = k ? w / (1 - k.l - k.r) : w, fh = k ? hh / (1 - k.t - k.b) : hh;
|
|
142
|
+
c.drawImage(im!, -w / 2 - (k ? k.l * fw : 0), -hh / 2 - (k ? k.t * fh : 0), fw, fh);
|
|
143
|
+
} else {
|
|
144
|
+
c.fillStyle = '#2B2C2E'; c.fillRect(-w / 2, -hh / 2, w, hh);
|
|
145
|
+
if (linkInfoOf(u.link!)?.exists === false) {
|
|
146
|
+
c.font = '500 13px ' + FONT; c.fillStyle = '#9FA0A2'; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
147
|
+
c.fillText('Removed', 0, 0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
c.restore();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Each expanded photo's badge: its link icon in a white disc in its top-right corner (imageLinkBadge), over the drawing
|
|
156
|
+
* — 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.
|
|
157
|
+
* `selected`: the selected photos' badges, drawn with their outlines and handles (paintOverlay), or the rest, with the
|
|
158
|
+
* drawing. A device keeps the drawing as a picture while a photo is moved, sized, cropped or turned, and draws only
|
|
159
|
+
* the photos and the moving parts again: a badge kept with the drawing stayed where the photo had been.
|
|
160
|
+
*/
|
|
161
|
+
function drawImageLinks(c: C2D, T: Xform, selected: boolean): void {
|
|
162
|
+
if (!settings.layers.underlay) return;
|
|
163
|
+
const { W, H } = cvs;
|
|
164
|
+
for (const u of page().underlays) {
|
|
165
|
+
if (isSel('underlay', u.id) !== selected) continue;
|
|
166
|
+
const b = imageLinkBadge(T, u);
|
|
167
|
+
if (!b || b.x < -b.r || b.y < -b.r || b.x > W + b.r || b.y > H + b.r) continue;
|
|
168
|
+
const r = b.r, disc = (cc: C2D, X: number, Y: number) => { cc.beginPath(); cc.arc(X + r, Y + r, r, 0, Math.PI * 2); };
|
|
169
|
+
c.save();
|
|
170
|
+
if (ed.mode === 'draft') c.globalAlpha = 0.75;
|
|
171
|
+
if (c.shape) c.shape('image link ' + r, b.x - r, b.y - r, disc); else disc(c, b.x - r, b.y - r);
|
|
172
|
+
c.fillStyle = '#FFFFFF'; c.fill();
|
|
173
|
+
c.lineWidth = 1; c.strokeStyle = 'rgba(24,26,28,.2)'; c.stroke();
|
|
174
|
+
linkIcon(c, b.x, b.y, r * 1.2, '#181A1C');
|
|
116
175
|
c.restore();
|
|
117
176
|
}
|
|
118
177
|
}
|
|
@@ -131,18 +190,40 @@ function drawRooms(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
|
|
|
131
190
|
c.fill();
|
|
132
191
|
if (thumb) continue;
|
|
133
192
|
const [cx, cy] = S(T, g.c.x, g.c.y);
|
|
134
|
-
|
|
193
|
+
// (its area and perimeter once the drawing has its scale: before, only placeholders — just its name)
|
|
194
|
+
const nm = r.name, info = sk.unscaled ? '' : fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perim.';
|
|
135
195
|
c.font = '600 14px ' + FONT;
|
|
136
|
-
const
|
|
137
|
-
c.
|
|
138
|
-
|
|
139
|
-
|
|
196
|
+
const nw = c.measureText(nm).width;
|
|
197
|
+
c.font = '500 12px ' + FONT;
|
|
198
|
+
const iw = info ? c.measureText(info).width : 0;
|
|
199
|
+
const show = spaceLabel(T, g.inner, nw, iw);
|
|
200
|
+
if (!show.name) continue;
|
|
201
|
+
c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillStyle = TH.label;
|
|
140
202
|
c.font = '600 14px ' + FONT;
|
|
141
|
-
|
|
203
|
+
c.fillText(nm, cx, show.info ? cy - 9 : cy);
|
|
204
|
+
if (show.info) { c.font = '500 12px ' + FONT; c.fillStyle = TH.ink2; c.fillText(info, cx, cy + 9); }
|
|
205
|
+
const w = Math.max(nw, 140);
|
|
142
206
|
addHit({ kind: 'room', id, x: cx - w / 2, y: cy - 20, w, h: 40 });
|
|
143
207
|
}
|
|
144
208
|
}
|
|
145
209
|
|
|
210
|
+
/** Room left round a space's label on the screen, each side, points. */
|
|
211
|
+
const SPACE_LABEL_PAD = 8;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Which of a space's label shows: only what fits inside the space on the screen, so zoomed out it goes rather than
|
|
215
|
+
* cluttering the drawing (the user's call, 2026-10-29, as a tile's name goes: LINK_FULL_PX) — its area and perimeter
|
|
216
|
+
* first, then its name. `nameW`, `infoW`: their widths (no area line: 0).
|
|
217
|
+
*/
|
|
218
|
+
export function spaceLabel(T: Xform, inner: XY[], nameW: number, infoW: number): { name: boolean; info: boolean } {
|
|
219
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
220
|
+
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); }
|
|
221
|
+
const w = (x1 - x0) * T.s, h = (y1 - y0) * T.s, pad = 2 * SPACE_LABEL_PAD;
|
|
222
|
+
// (one line of 14 pt; two, 18 apart, of 14 and 12)
|
|
223
|
+
const name = w >= nameW + pad && h >= 18 + pad;
|
|
224
|
+
return { name, info: name && infoW > 0 && w >= infoW + pad && h >= 36 + pad };
|
|
225
|
+
}
|
|
226
|
+
|
|
146
227
|
interface CurveWall { id: string; kind: 'circle' | 'arc'; thick: number; constr?: boolean }
|
|
147
228
|
|
|
148
229
|
function curveWalls(sk: Sketch): CurveWall[] {
|
|
@@ -333,11 +414,13 @@ function drawOpenings(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
333
414
|
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
415
|
c.stroke();
|
|
335
416
|
} else {
|
|
336
|
-
|
|
417
|
+
// the leaf, open square to the wall from the jamb it hangs on, and its swing back to the other
|
|
418
|
+
const [hs, ls] = hingeEdge(o) === 's1' ? [s1, s0] : [s0, s1];
|
|
419
|
+
const hinge = P(hs, (f * t) / 2), leaf = P(hs, f * (t / 2 + w));
|
|
337
420
|
c.beginPath(); c.moveTo(hinge[0], hinge[1]); c.lineTo(leaf[0], leaf[1]); c.stroke();
|
|
338
421
|
const [hx, hy] = hinge, r = w * T.s;
|
|
339
422
|
const a0 = Math.atan2(leaf[1] - hy, leaf[0] - hx);
|
|
340
|
-
const end = P(
|
|
423
|
+
const end = P(ls, (f * t) / 2);
|
|
341
424
|
const a1 = Math.atan2(end[1] - hy, end[0] - hx);
|
|
342
425
|
c.beginPath(); c.setLineDash([4, 3]); c.lineWidth = 1;
|
|
343
426
|
const ccw = normAng(a1 - a0) > Math.PI;
|
|
@@ -423,12 +506,25 @@ function drawLock(c: C2D, x: number, y: number): void {
|
|
|
423
506
|
c.restore();
|
|
424
507
|
}
|
|
425
508
|
|
|
509
|
+
/** The selected points and the selected lines' ends: what a constraint's icon still shows for with the icons off. */
|
|
510
|
+
function selPoints(sk: Sketch): Set<string> {
|
|
511
|
+
const out = new Set<string>();
|
|
512
|
+
for (const s of ed.sel) {
|
|
513
|
+
if (s.type === 'line') { const l = sk.lines[s.id]; if (l) { out.add(l.a); out.add(l.b); } }
|
|
514
|
+
if (s.type === 'point') out.add(s.id);
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
426
519
|
/** Points: filled = fully defined, hollow = still free (shape cue paired with color). */
|
|
427
520
|
function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
|
|
428
521
|
const { W, H } = cvs;
|
|
429
522
|
const touch = ed.lastPointerTouch;
|
|
430
523
|
// a cabinet's corners aren't grab points: it moves as one, like a door or window
|
|
431
524
|
const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cabPoints));
|
|
525
|
+
// a locked point's padlock is a constraint icon: with the icons off, it shows only as the badges do, on what's
|
|
526
|
+
// selected (the user's call, 2026-10-26)
|
|
527
|
+
const locks = settings.showCons ? null : selPoints(sk);
|
|
432
528
|
for (const id in sk.points) {
|
|
433
529
|
const p = sk.points[id];
|
|
434
530
|
const [x, y] = S(T, p.x, p.y);
|
|
@@ -447,47 +543,31 @@ function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
447
543
|
if (selected) { c.fillStyle = TH.yellow; c.fill(); c.lineWidth = 1.5; c.strokeStyle = TH.ink; c.stroke(); }
|
|
448
544
|
else if (det) { c.fillStyle = ed.highlight.has(id) ? TH.red : p.origin ? TH.ink : inkForPoint(sk, id); c.fill(); }
|
|
449
545
|
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);
|
|
546
|
+
if (p.fixed && !p.origin && !p.anchor && (!locks || locks.has(id) || ed.highlight.has(id))) drawLock(c, x + 9, y - 14);
|
|
451
547
|
}
|
|
452
548
|
}
|
|
453
549
|
|
|
454
550
|
type DimAnchor =
|
|
455
551
|
| { m: XY; a: XY; b: XY; nx?: number; ny?: number; L?: number }
|
|
456
552
|
| { m: XY; o: XY; r: number }
|
|
457
|
-
| { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number] };
|
|
553
|
+
| { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number]; len1?: number; len2?: number };
|
|
458
554
|
|
|
459
555
|
function dimAnchor(sk: Sketch, c: Constraint): DimAnchor | null {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
556
|
+
if (c.type === 'dist' || c.type === 'pldist') {
|
|
557
|
+
// (a length measured on a wall's face: from its corner to corner, the label off the face on that side. Between
|
|
558
|
+
// parallel lines or walls: from face to face, slid along them.)
|
|
463
559
|
const e = dimEnds(sk, c);
|
|
464
560
|
if (!e) return null;
|
|
465
561
|
const { a, b } = e;
|
|
466
562
|
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
563
|
return { m: { x: (a.x + b.x) / 2 + nx * off, y: (a.y + b.y) / 2 + ny * off }, a, b, nx, ny, L };
|
|
468
564
|
}
|
|
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
565
|
if (c.type === 'diam') {
|
|
476
566
|
// (across a curved wall's face: its arrows on that face)
|
|
477
567
|
const g = dimRadius(sk, c);
|
|
478
568
|
return g ? { m: { x: g.o.x, y: g.o.y }, o: g.o, r: g.r } : null;
|
|
479
569
|
}
|
|
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
|
-
}
|
|
570
|
+
if (c.type === 'angle') return angleCorner(sk, c);
|
|
491
571
|
return null;
|
|
492
572
|
}
|
|
493
573
|
|
|
@@ -509,35 +589,50 @@ function drawDims(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
509
589
|
if (!settings.layers.dims) return;
|
|
510
590
|
for (const id in sk.cons) {
|
|
511
591
|
const d = sk.cons[id];
|
|
512
|
-
if (!isDim(d) || d.
|
|
592
|
+
if (!isDim(d) || d.elev) continue; // the wall view's own dimensions are its chips
|
|
513
593
|
const A = dimAnchor(sk, d);
|
|
514
594
|
if (!A) continue;
|
|
515
595
|
const ref = d.driving === false, val = ref ? measure(sk, d) : d.value!;
|
|
516
596
|
const selected = isSel('dim', id) || isSel('cons', id);
|
|
517
597
|
const bad = ed.highlight.has(id) || ed.ana.bad.has(id);
|
|
518
598
|
if (ref && !bad && !dimLive(sk, id, d)) continue;
|
|
519
|
-
// a measurement of the job sets it:
|
|
599
|
+
// a measurement of the job sets it: the link badge before its value (chip()), on the chip's own colour — red once
|
|
600
|
+
// the job's measurement is gone
|
|
520
601
|
const linked = !!d.measurementRef && !ref && !bad, gone = linked && linkInfoOf(d.measurementRef!)?.exists === false;
|
|
521
602
|
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;
|
|
603
|
+
if (bad || gone) { bg = TH.red; fg = '#fff'; }
|
|
604
|
+
const border: [string, number] | null = selected ? [TH.yellow, 2.5] : null;
|
|
525
605
|
const mark = linked ? 'link' as const : undefined;
|
|
526
606
|
const wrap = (s: string) => (ref ? '(' : '') + s + (ref ? ')' : '');
|
|
527
607
|
if (d.type === 'angle' && 'vtx' in A) {
|
|
528
608
|
const [vx, vy] = S(T, A.vtx.x, A.vtx.y);
|
|
609
|
+
// pulled off its corner: its arc out where the label is, the label riding on it; else a little arc at the
|
|
610
|
+
// corner, the label out along the bisector
|
|
611
|
+
const moved = angleLabel(sk, d), R = moved ? moved.r * T.s : 22;
|
|
529
612
|
if (A.u1 && A.u2) {
|
|
530
613
|
// arc between the two lines, so the angle reads as belonging to that corner
|
|
531
614
|
const t1 = Math.atan2(-A.u1[1], A.u1[0]), t2 = Math.atan2(-A.u2[1], A.u2[0]);
|
|
532
615
|
let sweep = t2 - t1;
|
|
533
616
|
while (sweep > Math.PI) sweep -= 2 * Math.PI;
|
|
534
617
|
while (sweep < -Math.PI) sweep += 2 * Math.PI;
|
|
535
|
-
|
|
618
|
+
// (blue, as every other dimension is — the user's call, 2026-10-29; purple is the inference guides')
|
|
619
|
+
c.save(); c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue; c.lineWidth = 1.2;
|
|
536
620
|
if (ref) c.setLineDash([3, 3]);
|
|
537
|
-
c.beginPath(); c.arc(vx, vy,
|
|
621
|
+
c.beginPath(); c.arc(vx, vy, R, t1, t1 + sweep, sweep < 0); c.stroke();
|
|
622
|
+
// (pulled out past where a line ends: an extension line along it, out to the arc)
|
|
623
|
+
c.lineWidth = 1;
|
|
624
|
+
for (const [u, len] of [[A.u1, A.len1], [A.u2, A.len2]] as const) {
|
|
625
|
+
const from = (len ?? 0) * T.s;
|
|
626
|
+
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(); }
|
|
627
|
+
}
|
|
628
|
+
c.restore();
|
|
538
629
|
}
|
|
539
|
-
|
|
540
|
-
|
|
630
|
+
const text = wrap(fmtAng(val)), opts: ChipOpts = { bg, fg, border, kind: 'dim', id, mark };
|
|
631
|
+
// (left where it was drawn: 50 px out along the bisector, or as far as its corners stay clear of the corner — a
|
|
632
|
+
// linked chip is wider)
|
|
633
|
+
const sz = chipSize(c, text, opts), out = Math.max(50, Math.hypot(sz.w, sz.h) / 2 + 28);
|
|
634
|
+
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];
|
|
635
|
+
chip(c, text, lx, ly, opts);
|
|
541
636
|
continue;
|
|
542
637
|
}
|
|
543
638
|
if (d.type === 'diam' && 'o' in A) {
|
|
@@ -547,7 +642,7 @@ function drawDims(c: C2D, T: Xform, sk: Sketch): void {
|
|
|
547
642
|
if (!('a' in A)) continue;
|
|
548
643
|
const [mx, my] = S(T, A.m.x, A.m.y);
|
|
549
644
|
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) {
|
|
645
|
+
if ((d.type === 'dist' || d.type === 'pldist') && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
|
|
551
646
|
drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny!, d.off!, lineColor, ref);
|
|
552
647
|
} else if (d.type === 'pldist' || !d.line) {
|
|
553
648
|
// (dashed only when it's a reference: a dashed line means the number holds nothing, wherever it's drawn)
|
|
@@ -720,7 +815,8 @@ export function drawRubberBand(c: C2D, T: Xform, from: XY, to: XY, snapped: bool
|
|
|
720
815
|
if (snapped) endRing(c, x2, y2, TH.blue);
|
|
721
816
|
c.restore();
|
|
722
817
|
const L = hyp(to.x - from.x, to.y - from.y);
|
|
723
|
-
|
|
818
|
+
// (no distance on a drawing without its scale yet: it's only a placeholder)
|
|
819
|
+
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
820
|
}
|
|
725
821
|
|
|
726
822
|
/**
|
|
@@ -751,7 +847,8 @@ export function drawPendingDim(c: C2D, T: Xform, p: { from: XY; to: XY; a: XY; b
|
|
|
751
847
|
for (const q of [p.from, p.to]) { const [x, y] = S(T, q.x, q.y); endRing(c, x, y, TH.yellow); }
|
|
752
848
|
c.restore();
|
|
753
849
|
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
|
-
|
|
850
|
+
// (no label until one's typed, on a page whose sizes are still placeholders)
|
|
851
|
+
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
852
|
}
|
|
756
853
|
|
|
757
854
|
/**
|
|
@@ -800,54 +897,10 @@ function drawDiameter(c: C2D, T: Xform, o: XY, r: number, d: Constraint, text: s
|
|
|
800
897
|
arrowHead(c, p1, sx, sy);
|
|
801
898
|
if (!d.rad) arrowHead(c, p2, -sx, -sy);
|
|
802
899
|
c.restore();
|
|
803
|
-
|
|
804
|
-
const w = c.measureText(text).width + 14 + (chipOpts.mark ? MARK_W : 0);
|
|
900
|
+
const { w } = chipSize(c, text, chipOpts);
|
|
805
901
|
chip(c, text, end[0] + side * (w / 2 + 2), end[1], chipOpts);
|
|
806
902
|
}
|
|
807
903
|
|
|
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
904
|
/**
|
|
852
905
|
* A badge's symbol drawn as lines rather than a character: ∥ ⊥ ∘ ◎ aren't in every typeface — on a device the
|
|
853
906
|
* 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 +967,7 @@ function badgeSpots(T: Xform, sk: Sketch, c: Constraint): [number, number][] {
|
|
|
914
967
|
|
|
915
968
|
function drawBadges(c: C2D, T: Xform, sk: Sketch): void {
|
|
916
969
|
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
|
-
}
|
|
970
|
+
const selIds = new Set(ed.sel.map(s => s.id)), selPts = selPoints(sk);
|
|
922
971
|
for (const id in sk.cons) {
|
|
923
972
|
const k = sk.cons[id];
|
|
924
973
|
if (isDim(k) || k.elev) continue;
|
|
@@ -1068,7 +1117,7 @@ function drawSnap(c: C2D, T: Xform): void {
|
|
|
1068
1117
|
const [x, y] = S(T, s.x, s.y);
|
|
1069
1118
|
if (s.kind !== 'free' && s.kind !== 'grid') {
|
|
1070
1119
|
c.lineWidth = 2; c.beginPath();
|
|
1071
|
-
if (s.kind === 'point') c.arc(x, y, 8, 0, Math.PI * 2);
|
|
1120
|
+
if (s.kind === 'point' || s.kind === 'facecorner') c.arc(x, y, 8, 0, Math.PI * 2);
|
|
1072
1121
|
else if (s.kind === 'mid') { c.moveTo(x, y - 8); c.lineTo(x + 7, y + 5); c.lineTo(x - 7, y + 5); c.closePath(); }
|
|
1073
1122
|
else if (s.kind === 'inter') { c.moveTo(x - 6, y - 6); c.lineTo(x + 6, y + 6); c.moveTo(x + 6, y - 6); c.lineTo(x - 6, y + 6); }
|
|
1074
1123
|
else c.rect(x - 5, y - 5, 10, 10);
|
|
@@ -1162,7 +1211,6 @@ function paintPlanView(ctx: C2D, sk: Sketch, m: number, under = true): void {
|
|
|
1162
1211
|
if (settings.layers.rooms) drawRooms(ctx, T, sk);
|
|
1163
1212
|
if (settings.layers.cabinets) drawCabinetFootprints(ctx, T, sk);
|
|
1164
1213
|
drawWalls(ctx, T, sk); drawOpenings(ctx, T, sk); drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
|
|
1165
|
-
drawPrintDims(ctx, T, sk);
|
|
1166
1214
|
if (ed.mode === 'draft') drawBadges(ctx, T, sk);
|
|
1167
1215
|
// (the drawing's points only in Draft: Annotate can't move them, and shouldn't look as if it could — the user's
|
|
1168
1216
|
// call, 2026-10-21)
|
|
@@ -1170,6 +1218,8 @@ function paintPlanView(ctx: C2D, sk: Sketch, m: number, under = true): void {
|
|
|
1170
1218
|
drawOpeningAnchors(ctx, T, sk);
|
|
1171
1219
|
drawDims(ctx, T, sk);
|
|
1172
1220
|
drawOpeningDims(ctx, T, sk);
|
|
1221
|
+
// (an expanded photo's badge over the drawing, under the markup: as it's tapped. A selected one's: paintOverlay)
|
|
1222
|
+
drawImageLinks(ctx, T, false);
|
|
1173
1223
|
drawNotes(ctx, T, sk);
|
|
1174
1224
|
drawNoteSelection(ctx, T);
|
|
1175
1225
|
}
|
|
@@ -1190,7 +1240,6 @@ function paintWall(ctx: C2D, W: number, fixed: C2D): void {
|
|
|
1190
1240
|
drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
|
|
1191
1241
|
// (over the edges — its own and its neighbours' — so a selected cabinet's outline shows all the way round)
|
|
1192
1242
|
drawWallSelection(ctx, T, plan, f, sk);
|
|
1193
|
-
drawPrintDims(ctx, T, sk);
|
|
1194
1243
|
drawBadges(ctx, T, sk);
|
|
1195
1244
|
if (ed.mode === 'draft') drawPoints(ctx, T, sk);
|
|
1196
1245
|
drawDims(ctx, T, sk);
|
|
@@ -1218,7 +1267,9 @@ function drawSurveyCursor(ctx: C2D, T: Xform): void {
|
|
|
1218
1267
|
|
|
1219
1268
|
function paintOverlay(ctx: C2D, W: number, H: number, fixed: C2D = ctx): void {
|
|
1220
1269
|
const T = mainT();
|
|
1221
|
-
// a selected image's outline and handles: with the moving parts, as it's turned, moved or sized (over the
|
|
1270
|
+
// a selected image's badge, outline and handles: with the moving parts, as it's turned, moved or sized (over the
|
|
1271
|
+
// drawing; the badge under the handles, as it was under them kept with the drawing). The wall view has no photos.
|
|
1272
|
+
if (!(elevOpen() && elev.frame)) drawImageLinks(ctx, T, true);
|
|
1222
1273
|
drawUnderlaySelection(ctx, T);
|
|
1223
1274
|
if (ed.preview) ed.preview(ctx, T);
|
|
1224
1275
|
// (over the tool's preview — the templator's would-be edge runs into it — whatever tool is in hand)
|
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 };
|