@reekon-tools/boldr-cad 1.3.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 +21 -4
- 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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.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/host.d.ts +7 -0
- package/dist/react/host.js +14 -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/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/tools/survey.d.ts +6 -0
- package/dist/tools/survey.js +28 -0
- package/dist/tools/survey.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/index.ts +1 -1
- 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/host.ts +14 -1
- 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/tools/survey.ts +21 -0
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { C } from './theme';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A bottom bar too long for the screen scrolls, and has to look like it does (the user's call, 2026-10-27: "on mobile
|
|
5
|
+
it is hard to tell that the bottom bar is scrollable"). Three things say so, in react/BottomBar.tsx:
|
|
6
|
+
- a fade at each end that has more of the bar past it, there as it's scrolled;
|
|
7
|
+
- its end always cuts through a button, never at a gap: there, what shows looks like all there is (barCut);
|
|
8
|
+
- the first bar of a session that doesn't fit slides a little way along and back, unless Reduce Motion is on.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A button's place along the bar's buttons: from their start, and its width. */
|
|
12
|
+
export type Span = readonly [x: number, w: number];
|
|
13
|
+
|
|
14
|
+
/** How much of a button the end leaves showing: what it's cut to, and the shares that already read as cut. */
|
|
15
|
+
export const CUT = 0.6, CUT_MIN = 0.3, CUT_MAX = 0.8;
|
|
16
|
+
/** A fade's width; how far the bar scrolls as one comes or goes. */
|
|
17
|
+
export const FADE = 20, FADE_RAMP = 24;
|
|
18
|
+
/** The nudge: how far along, how long after the bar shows, and how long before it goes back (ms). */
|
|
19
|
+
export const NUDGE = 40, NUDGE_AFTER = 400, NUDGE_HOLD = 450;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The buttons' places along the bar, from the widths of everything in it in order (a divider's too) and the gap
|
|
23
|
+
* between each: null while any is still unmeasured.
|
|
24
|
+
*/
|
|
25
|
+
export function barSpans(items: readonly { w: number | undefined; button: boolean }[], gap: number): Span[] | null {
|
|
26
|
+
const out: Span[] = [];
|
|
27
|
+
let x = 0;
|
|
28
|
+
for (const { w, button } of items) {
|
|
29
|
+
if (w == null) return null;
|
|
30
|
+
if (button) out.push([x, w]);
|
|
31
|
+
x += w + gap;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How much of a scrolling bar shows, given `room` for it: all of that, if it already ends partway through a button
|
|
38
|
+
* (between CUT_MIN and CUT_MAX of it showing); otherwise less, ending at CUT of the last button that leaves showing.
|
|
39
|
+
*/
|
|
40
|
+
export function barCut(spans: readonly Span[], room: number): number {
|
|
41
|
+
let best = 0;
|
|
42
|
+
for (const [x, w] of spans) {
|
|
43
|
+
const f = (room - x) / w;
|
|
44
|
+
if (f >= CUT_MIN && f <= CUT_MAX) return room;
|
|
45
|
+
const c = x + CUT * w;
|
|
46
|
+
if (c <= room && c > best) best = c;
|
|
47
|
+
}
|
|
48
|
+
return best || room;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A fade, as an SVG: the bar's grey, clear at one side, whole at the other (`to`: the side that's whole). */
|
|
52
|
+
export const fadeSvg = (to: 'left' | 'right'): string => {
|
|
53
|
+
const id = 'barFade' + (to === 'left' ? 'L' : 'R'), [a, b] = to === 'right' ? [0, 1] : [1, 0];
|
|
54
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${FADE}" height="54" viewBox="0 0 1 1" preserveAspectRatio="none">` +
|
|
55
|
+
`<defs><linearGradient id="${id}" x1="${a}" y1="0" x2="${b}" y2="0"><stop offset="0" stop-color="${C.g2}" stop-opacity="0"/>` +
|
|
56
|
+
`<stop offset="1" stop-color="${C.g2}" stop-opacity="1"/></linearGradient></defs><rect width="1" height="1" fill="url(#${id})"/></svg>`;
|
|
57
|
+
};
|
package/src/react/corners.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
1
|
+
import { Platform, StyleSheet, type ImageStyle, type TextStyle, type ViewStyle } from 'react-native';
|
|
2
2
|
import { CORNER_SMOOTHING } from '../render/squircle';
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -44,9 +44,12 @@ export function smoothCorners<T extends Style>(st: T): T {
|
|
|
44
44
|
return out as T;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** A sheet's styles by name. (React Native 0.88's generated types have no `StyleSheet.NamedStyles`.) */
|
|
48
|
+
type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };
|
|
49
|
+
|
|
47
50
|
/** StyleSheet.create, every style's corners smoothed (smoothCorners). */
|
|
48
|
-
export function createSheet<T extends
|
|
51
|
+
export function createSheet<T extends NamedStyles<T>>(styles: T): T {
|
|
49
52
|
const out = {} as Record<string, unknown>;
|
|
50
53
|
for (const k of Object.keys(styles)) out[k] = smoothCorners((styles as Record<string, Style>)[k]);
|
|
51
|
-
return StyleSheet.create(out as T
|
|
54
|
+
return StyleSheet.create(out as Record<string, ViewStyle | TextStyle | ImageStyle>) as unknown as T;
|
|
52
55
|
}
|
package/src/react/host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyReading, laser } from '../editor/laser';
|
|
2
2
|
import { store } from '../model/store';
|
|
3
|
-
import { setSurveyCursor, takeSurveyPoint } from '../tools/survey';
|
|
3
|
+
import { closeSurveyRun, setSurveyCursor, takeSurveyPoint } from '../tools/survey';
|
|
4
4
|
import { notify } from './store';
|
|
5
5
|
|
|
6
6
|
/*
|
|
@@ -49,6 +49,19 @@ export function surveyPoint(x: number, y: number, capture?: unknown): boolean {
|
|
|
49
49
|
return ok;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Close the templating device's open run, from a button that means "close" (BOLDR's T1R's D) — the bar's Close shape:
|
|
54
|
+
* the last edge back to the first point, the corners it makes, and a space. It takes no point; a host whose device
|
|
55
|
+
* also measured one on that press drops it. False when no drawing is open, there's no run of 3 points or more, or it
|
|
56
|
+
* can't now (a wall view or keypad open); the editor says why.
|
|
57
|
+
*/
|
|
58
|
+
export function surveyClose(): boolean {
|
|
59
|
+
if (!editorHost.open || !store.project) return false;
|
|
60
|
+
const ok = closeSurveyRun();
|
|
61
|
+
notify();
|
|
62
|
+
return ok;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
/**
|
|
53
66
|
* The templating device's live point — where its next surveyPoint would land — on the same axes as surveyPoint, or
|
|
54
67
|
* null to hide it (the device stopped, its reading went stale, the host left the screen). Drawn as a ring, with the
|
package/src/react/motion.tsx
CHANGED
|
@@ -51,7 +51,7 @@ export const reducedMotion = (): boolean => reduceMotion;
|
|
|
51
51
|
* device, the wait before it moved). `mounted` says whether to draw it at all; the style hides it when it's away.
|
|
52
52
|
*/
|
|
53
53
|
export function usePopup(open: boolean, from: { x?: number; y?: number }, origin: string, keep = false): {
|
|
54
|
-
present: boolean; mounted: boolean; style: Animated.
|
|
54
|
+
present: boolean; mounted: boolean; style: Animated.WithAnimatedValue<ViewStyle>;
|
|
55
55
|
} {
|
|
56
56
|
const v = useRef(new Animated.Value(open ? 1 : 0)).current, [present, setPresent] = useState(open), been = useRef(open);
|
|
57
57
|
if (open) been.current = true;
|
|
@@ -77,9 +77,9 @@ export function usePopup(open: boolean, from: { x?: number; y?: number }, origin
|
|
|
77
77
|
{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [fy, 0] }) },
|
|
78
78
|
{ scale: v.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
|
|
79
79
|
],
|
|
80
|
-
}) as Animated.
|
|
80
|
+
}) as Animated.WithAnimatedValue<ViewStyle>, [v, fx, fy, origin]);
|
|
81
81
|
const shown = open || present;
|
|
82
|
-
const hidden = useMemo(() => [style, { display: 'none' as const }] as unknown as Animated.
|
|
82
|
+
const hidden = useMemo(() => [style, { display: 'none' as const }] as unknown as Animated.WithAnimatedValue<ViewStyle>, [style]);
|
|
83
83
|
return { present: shown, mounted: shown || (keep && been.current), style: shown ? style : hidden };
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -185,13 +185,13 @@ function SegOption({ label, on, onPress, onLayout, style, textStyle, onText, off
|
|
|
185
185
|
onLayout={e => onLayout(e.nativeEvent.layout)} style={[segCenter, style, { backgroundColor: 'transparent' }]}>
|
|
186
186
|
{NATIVE ? (
|
|
187
187
|
<>
|
|
188
|
-
<Animated.Text style={[textStyle, { color: offText, opacity: ink }]}>{label}</Animated.Text>
|
|
188
|
+
<Animated.Text numberOfLines={1} style={[textStyle, { color: offText, opacity: ink }]}>{label}</Animated.Text>
|
|
189
189
|
{/* (the chosen colour's label, exactly over the other) */}
|
|
190
190
|
<View pointerEvents="none" style={segOver}>
|
|
191
|
-
<Animated.Text accessible={false} style={[textStyle, { color: onText, opacity: t }]}>{label}</Animated.Text>
|
|
191
|
+
<Animated.Text numberOfLines={1} accessible={false} style={[textStyle, { color: onText, opacity: t }]}>{label}</Animated.Text>
|
|
192
192
|
</View>
|
|
193
193
|
</>
|
|
194
|
-
) : <Animated.Text style={[textStyle, { color: ink }]}>{label}</Animated.Text>}
|
|
194
|
+
) : <Animated.Text numberOfLines={1} style={[textStyle, { color: ink }]}>{label}</Animated.Text>}
|
|
195
195
|
</Tap>
|
|
196
196
|
);
|
|
197
197
|
}
|
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);
|