@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
package/src/editor/elevation.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { fmtLen } from '../units';
|
|
|
24
24
|
import { addCabinet, addHeightLine, CAB, cabinetSpot, deleteCabinets, dimensionFace, editCabinet, editCabinetDepth, snapObjectMove } from './face';
|
|
25
25
|
import type { RailItem } from './mode';
|
|
26
26
|
import { openKeypad, refreshModes, renderElevation, toast, updateCtx, updateHint, updateRail, updateTopBar } from './ports';
|
|
27
|
+
import { waitForScale } from './scaleGate';
|
|
27
28
|
|
|
28
29
|
/*
|
|
29
30
|
The wall view: one straight wall seen straight on, edited with the plan's own tools. Opening a wall makes the
|
|
@@ -50,8 +51,12 @@ export const elevOpen = (): boolean => elev.lid != null;
|
|
|
50
51
|
const wallBox = () => (elev.frame ? { x0: elev.frame.face[0], y0: 0, x1: elev.frame.face[1], y1: elev.frame.H } : null);
|
|
51
52
|
|
|
52
53
|
/** Open the wall view of a straight wall, seen from the room side. */
|
|
53
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Open a wall's view: from the side a room is on — or `side`, the face to the left (+1) or right (−1) of its A → B.
|
|
56
|
+
* Not before the page's first length: the wall's height is real, its length only a placeholder until then.
|
|
57
|
+
*/
|
|
54
58
|
export function openElevation(lid: string, side?: 1 | -1): void {
|
|
59
|
+
if (waitForScale()) return;
|
|
55
60
|
if (!elevFrame(planSK(), lid)) { toast('Elevations work on straight walls. Turn the line into a wall first.', { warn: true }); return; }
|
|
56
61
|
cancelTool();
|
|
57
62
|
elev.lid = lid; elev.flip = side != null && side !== (roomSideOf(planSK(), lid) || 1);
|
|
@@ -331,7 +336,7 @@ export const elevAct = (id: string): ElevAct => JSON.parse(id) as ElevAct;
|
|
|
331
336
|
/** A chip's name, as the bar shows it. */
|
|
332
337
|
export function elevChipName(a: ElevAct): string {
|
|
333
338
|
if (a.k === 'gap') return a.title;
|
|
334
|
-
if (a.k === 'len') return 'Wall length
|
|
339
|
+
if (a.k === 'len') return 'Wall length';
|
|
335
340
|
if (a.k === 'wallH') return 'Wall height';
|
|
336
341
|
if (a.k === 'pos') return a.title;
|
|
337
342
|
if (a.k === 'fdim') return 'Dimension';
|
|
@@ -698,7 +703,8 @@ export function wallBar(): { label?: string; items: WallBarItem[]; plus: boolean
|
|
|
698
703
|
if (pair) {
|
|
699
704
|
const name = (id: string) => (planSK().openings[id]?.type === 'door' ? 'Door' : 'Window');
|
|
700
705
|
const label = name(pair.op) + ' + ' + ('op' in pair.other ? name(pair.other.op).toLowerCase() : 'corner');
|
|
701
|
-
|
|
706
|
+
// (the distance between them: the yellow +)
|
|
707
|
+
return { label, items: [], plus: true };
|
|
702
708
|
}
|
|
703
709
|
if (ed.sel.length !== 1) return null;
|
|
704
710
|
const s = ed.sel[0], sk = planSK();
|
|
@@ -706,19 +712,10 @@ export function wallBar(): { label?: string; items: WallBarItem[]; plus: boolean
|
|
|
706
712
|
const a = elevAct(s.id);
|
|
707
713
|
return { label: elevChipName(a), items: a.k === 'len' ? [{ ic: 'back', label: 'Go to plan', act: 'wallGoPlan' }] : [], plus: a.k !== 'len' };
|
|
708
714
|
}
|
|
709
|
-
// a door, window or cabinet picked as a whole: its sizes to set
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return { label: openingLabel(o), items: OPENING_SIZES, plus: false };
|
|
714
|
-
}
|
|
715
|
-
if (s.type === 'cab') {
|
|
716
|
-
const face = SK() as WallFace, b = cabBox(face, s.id), cb = face.cabs[s.id];
|
|
717
|
-
return {
|
|
718
|
-
label: 'Cabinet' + (b ? ' ' + fmtLen(b.u1 - b.u0) + ' L × ' + fmtLen(b.z1 - b.z0) + ' H × ' + fmtLen(cb.depth) + ' D' : ''),
|
|
719
|
-
items: [{ ic: 'v', label: 'Height', act: 'cabH' }, { ic: 'h', label: 'Length', act: 'cabW' }, { ic: 'thick', label: 'Depth', act: 'cabD' }], plus: false,
|
|
720
|
-
};
|
|
721
|
-
}
|
|
715
|
+
// a door, window or cabinet picked as a whole: its sizes to set (no + — it isn't one dimension), with nothing saying
|
|
716
|
+
// them: the wall view shows them (the user's call, 2026-10-27: "get rid of extra dimensions text")
|
|
717
|
+
if (s.type === 'opening') return sk.openings[s.id] ? { items: OPENING_SIZES, plus: false } : null;
|
|
718
|
+
if (s.type === 'cab') return { items: [{ ic: 'v', label: 'Height', act: 'cabH' }, { ic: 'h', label: 'Length', act: 'cabW' }, { ic: 'thick', label: 'Depth', act: 'cabD' }], plus: false };
|
|
722
719
|
const face = SK() as WallFace;
|
|
723
720
|
if (s.type === 'line' && face.lines[s.id]?.anchor) return { label: anchorName(s.id), items: [], plus: false };
|
|
724
721
|
if (s.type === 'line' && face.lines[s.id]?.cab) return { label: 'Cabinet edge', items: [], plus: false };
|
package/src/editor/gestures.ts
CHANGED
|
@@ -17,7 +17,8 @@ import type { SelItem } from '../model/types';
|
|
|
17
17
|
/*
|
|
18
18
|
Pointer routing for the plan canvas, whatever delivers the pointers (DOM pointer events, React Native touches).
|
|
19
19
|
Pen draws, fingers navigate: once a pen has been seen, touch only pans and zooms. Without a pen, one finger drives
|
|
20
|
-
the tool and a second finger turns the action into a pan/pinch
|
|
20
|
+
the tool and a second finger turns the action into a pan/pinch — a finger's press waits a moment for one first
|
|
21
|
+
(pressTiming). Two-finger tap undoes, three-finger tap redoes.
|
|
21
22
|
Mouse: middle or right button, or Space held, drags to pan; the wheel zooms.
|
|
22
23
|
With a sheet up over the drawing (the keypad, a conflict, a question), the drawing only pans and zooms, by any
|
|
23
24
|
pointer, and a tap on it closes the sheet. View only, a tap on a link's tile asks to open what it's for.
|
|
@@ -52,22 +53,40 @@ export function setHostNavigation(on: boolean): void { hostNavigates = on; }
|
|
|
52
53
|
*/
|
|
53
54
|
const MOTION_PX = 14;
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* How long a finger's press waits before the tool hears of it (ms), unless it lifts or moves past a tap first: time
|
|
58
|
+
* for a second finger to land and make it a pan or pinch from the start. Then the first finger never pressed anything:
|
|
59
|
+
* nothing is picked, no bar or sheet shows for it, and nothing is drawn where it landed (the user's report, 2026-10-30:
|
|
60
|
+
* zooming, "there is a brief moment where my touch brings up a bottom sheet"). A pinch's fingers land a few tens of
|
|
61
|
+
* milliseconds apart; iOS holds a scroll view's touches 150 ms and Android a pressed look 100 ms, for the same reason.
|
|
62
|
+
* A second finger after the wait still undoes the press (cancelPrimary). A mouse and a Pencil press at once.
|
|
63
|
+
*/
|
|
64
|
+
export const pressTiming = {
|
|
65
|
+
/** tests pin it (0: a finger presses at once) */
|
|
66
|
+
holdMs: 120,
|
|
67
|
+
};
|
|
68
|
+
|
|
55
69
|
const ptrs = new Map<number, PointerIn>();
|
|
56
70
|
let penSeen = false, gesture: Gesture | null = null, primary: PointerIn | null = null;
|
|
57
71
|
/** close: a press under a sheet (see sheetUp) — a tap closes it; open: a view-only press — a tap opens a link's tile */
|
|
58
72
|
let panDrag: { x: number; y: number; id: number; moved: number; close?: boolean; open?: boolean } | null = null;
|
|
59
73
|
/** where the primary pointer went down, and whether a gesture on the drawing is in motion */
|
|
60
74
|
let primaryAt: { x: number; y: number } | null = null, inMotion = false;
|
|
75
|
+
/** the primary pointer has moved past a tap since it went down (see lensPointer) */
|
|
76
|
+
let primaryMoved = false;
|
|
61
77
|
/**
|
|
62
78
|
* What a press can change, as it was before the primary pointer went down: if a second finger turns the press into a
|
|
63
79
|
* pan or pinch, it's put back (see cancelPrimary). A tool's own state is kept a level deep — its presses set its fields,
|
|
64
80
|
* they don't change what's in them.
|
|
65
81
|
*/
|
|
66
82
|
let beforePress: { tool: string; st: unknown; sel: SelItem[]; preview: Preview | null; snap: Snap | null } | null = null;
|
|
83
|
+
/** A finger down whose press is waiting (pressTiming): the pointer, where it landed, and each move it's made since. */
|
|
84
|
+
let held: { p: PointerIn; at: PointerIn; moves: PointerIn[]; timer: ReturnType<typeof setTimeout> } | null = null;
|
|
67
85
|
let wheelT: ReturnType<typeof setTimeout> | undefined;
|
|
68
86
|
export const keys = { spaceDown: false };
|
|
69
87
|
onSessionReset(() => {
|
|
70
|
-
ptrs.clear(); penSeen = false; gesture = null; primary = null; panDrag = null; keys.spaceDown = false; primaryAt = null; beforePress = null;
|
|
88
|
+
ptrs.clear(); penSeen = false; gesture = null; primary = null; panDrag = null; keys.spaceDown = false; primaryAt = null; beforePress = null; primaryMoved = false;
|
|
89
|
+
dropHeld();
|
|
71
90
|
setMotion(false);
|
|
72
91
|
clearTimeout(wheelT);
|
|
73
92
|
});
|
|
@@ -164,6 +183,39 @@ function cancelPrimary(): void {
|
|
|
164
183
|
updateCtx(); updateHint(); requestDraw();
|
|
165
184
|
}
|
|
166
185
|
|
|
186
|
+
/** The primary pointer's press: the tool hears it land where `at` says (its pointer `p` may have moved on since). */
|
|
187
|
+
function press(p: PointerIn, at: PointerIn): void {
|
|
188
|
+
primary = p; primaryMoved = false;
|
|
189
|
+
primaryAt = { x: at.x, y: at.y };
|
|
190
|
+
// (what it lands on is picked from the drawing as it is now, not as the last frame painted it)
|
|
191
|
+
paintPending();
|
|
192
|
+
const t = curTool();
|
|
193
|
+
beforePress = { tool: ed.tool, st: t && t.st && typeof t.st === 'object' ? { ...t.st } : t?.st, sel: [...ed.sel], preview: ed.preview, snap: ed.snapState };
|
|
194
|
+
if (t && t.down) t.down(evOf(at));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The primary pointer moved: past a tap, a gesture is in motion (and the lens may show); the tool follows it. */
|
|
198
|
+
function movePrimary(e: PointerIn): void {
|
|
199
|
+
if (primaryAt && hyp(e.x - primaryAt.x, e.y - primaryAt.y) >= MOTION_PX) { setMotion(true); primaryMoved = true; }
|
|
200
|
+
const t = curTool();
|
|
201
|
+
if (t && t.move) t.move(evOf(e));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dropHeld(): void {
|
|
205
|
+
if (held) clearTimeout(held.timer);
|
|
206
|
+
held = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A finger's waiting press goes ahead: the tool hears it land where it did, then each move it's made since. */
|
|
210
|
+
function releaseHeld(): void {
|
|
211
|
+
const h = held;
|
|
212
|
+
dropHeld();
|
|
213
|
+
// (something else took the drawing meanwhile: a mouse or Pencil press, a sheet, view only)
|
|
214
|
+
if (!h || primary || navigationOnly()) return;
|
|
215
|
+
press(h.p, h.at);
|
|
216
|
+
for (const m of h.moves) movePrimary(m);
|
|
217
|
+
}
|
|
218
|
+
|
|
167
219
|
function endPtr(e: PointerIn, cancel: boolean): void {
|
|
168
220
|
const p = ptrs.get(e.id);
|
|
169
221
|
ptrs.delete(e.id);
|
|
@@ -175,6 +227,8 @@ function endPtr(e: PointerIn, cancel: boolean): void {
|
|
|
175
227
|
return;
|
|
176
228
|
}
|
|
177
229
|
if (gesture && p && p.kind === 'touch') { if (!touchList().length) endGesture(e.t); return; }
|
|
230
|
+
// (a finger let go while its press waited: a tap, pressed and let go now; lost, it did nothing)
|
|
231
|
+
if (held && e.id === held.at.id) { if (cancel) dropHeld(); else releaseHeld(); }
|
|
178
232
|
if (primary && e.id === primary.id) {
|
|
179
233
|
primary = null;
|
|
180
234
|
const t = curTool();
|
|
@@ -199,6 +253,8 @@ export function pointerDown(e: PointerIn): void {
|
|
|
199
253
|
if (e.kind === 'touch') {
|
|
200
254
|
const touches = touchList();
|
|
201
255
|
if (penSeen || touches.length >= 2 || navOnly) {
|
|
256
|
+
// (a second finger while the first one's press waited: that never pressed anything)
|
|
257
|
+
dropHeld();
|
|
202
258
|
if (primary && primary.kind === 'touch') cancelPrimary();
|
|
203
259
|
// (a finger joining one already moving the view: the pinch is measured from here, none of its first move lost)
|
|
204
260
|
if (!gesture) startGesture(e.t); else { gesture.max = Math.max(gesture.max, touches.length); gesture.last = gState(touches); }
|
|
@@ -206,13 +262,13 @@ export function pointerDown(e: PointerIn): void {
|
|
|
206
262
|
}
|
|
207
263
|
}
|
|
208
264
|
if (primary || navOnly) return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
265
|
+
// a finger's press waits a moment, in case another finger lands (pressTiming)
|
|
266
|
+
if (e.kind === 'touch' && pressTiming.holdMs > 0) {
|
|
267
|
+
dropHeld();
|
|
268
|
+
held = { p, at: { ...e }, moves: [], timer: setTimeout(releaseHeld, pressTiming.holdMs) };
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
press(p, e);
|
|
216
272
|
}
|
|
217
273
|
|
|
218
274
|
export function pointerMove(e: PointerIn): void {
|
|
@@ -225,15 +281,29 @@ export function pointerMove(e: PointerIn): void {
|
|
|
225
281
|
}
|
|
226
282
|
if (p) { p.x = e.x; p.y = e.y; }
|
|
227
283
|
if (gesture && p && p.kind === 'touch') { updateGesture(); return; }
|
|
284
|
+
// (a finger whose press is waiting: its moves wait with it, and one past a tap sends them all now — a drag)
|
|
285
|
+
if (held && e.id === held.at.id) {
|
|
286
|
+
held.moves.push({ ...e });
|
|
287
|
+
if (hyp(e.x - held.at.x, e.y - held.at.y) >= MOTION_PX) releaseHeld();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
228
290
|
const t = curTool();
|
|
229
291
|
if (!t) return;
|
|
230
|
-
if (primary && e.id === primary.id)
|
|
231
|
-
if (primaryAt && hyp(e.x - primaryAt.x, e.y - primaryAt.y) >= MOTION_PX) setMotion(true);
|
|
232
|
-
if (t.move) t.move(evOf(e));
|
|
233
|
-
}
|
|
292
|
+
if (primary && e.id === primary.id) movePrimary(e);
|
|
234
293
|
else if (!primary && e.kind !== 'touch' && !navigationOnly()) { ed.lastPointerTouch = false; if (t.hover) t.hover(evOf(e)); }
|
|
235
294
|
}
|
|
236
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The finger or Apple Pencil working the drawing, where it is on the canvas, once it's moved past a tap placing or
|
|
298
|
+
* dragging something (Tool.magnify): what a device's magnifier is centred on (render/lens.ts). Null for a mouse, a tap,
|
|
299
|
+
* a pan or a pinch, and a drawing that can't be edited or has a sheet up.
|
|
300
|
+
*/
|
|
301
|
+
export function lensPointer(): { x: number; y: number } | null {
|
|
302
|
+
if (!primary || primary.kind === 'mouse' || !primaryMoved || navigationOnly()) return null;
|
|
303
|
+
const t = curTool();
|
|
304
|
+
return t && (!t.magnify || t.magnify()) ? { x: primary.x, y: primary.y } : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
237
307
|
export function pointerUp(e: PointerIn): void { endPtr(e, false); settle(); }
|
|
238
308
|
export function pointerCancel(e: PointerIn): void { endPtr(e, true); settle(); }
|
|
239
309
|
|
package/src/editor/keys.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { saveSoon } from '../model/store';
|
|
|
5
5
|
import { fitView, requestDraw } from '../render/view';
|
|
6
6
|
import { cancelTool, setTool, tools } from '../tools/registry';
|
|
7
7
|
import { applyCtxAction, deleteSelection } from './actions';
|
|
8
|
-
import { addLinks, linkPlusVisible, openSelectedLink, selectedLink } from './linkActions';
|
|
8
|
+
import { addLinks, linkPlusVisible, openSelectedLink, selectedLink, selectedLinkedImage } from './linkActions';
|
|
9
9
|
import { keys } from './gestures';
|
|
10
10
|
import { keypadApply, keypadDeleteLast, keypadInsert, keypadOpen } from './keypad';
|
|
11
11
|
import { confirmCancel, confirmChoose, confirmOpen } from './confirm';
|
|
@@ -89,7 +89,7 @@ export function keyDown(e: KeyIn): boolean {
|
|
|
89
89
|
}
|
|
90
90
|
// Annotate: + adds files and measurements (the + by the rail), Enter opens the selected link
|
|
91
91
|
if ((e.key === '+' || e.key === '=') && linkPlusVisible()) { void addLinks(); return true; }
|
|
92
|
-
if (e.key === 'Enter' && ed.mode === 'annotate' && selectedLink()) { openSelectedLink(); return true; }
|
|
92
|
+
if (e.key === 'Enter' && ed.mode === 'annotate' && (selectedLink() || selectedLinkedImage())) { openSelectedLink(); return true; }
|
|
93
93
|
if (e.shift) {
|
|
94
94
|
const c = SHIFT_CONS[e.key.toUpperCase()];
|
|
95
95
|
if (c && ed.mode === 'draft') { applyCtxAction(c); return true; }
|
|
@@ -5,11 +5,14 @@ import { pushHistory } from '../model/history';
|
|
|
5
5
|
import { uid } from '../model/ids';
|
|
6
6
|
import { isLink, LINK_COLOR, LINK_MAX_IN, LINK_MIN_IN, LINK_TILE_PX, linkInfoOf, linkName, linkSide, linkSnapshot } from '../model/links';
|
|
7
7
|
import { dimMeasurement } from '../model/measurement';
|
|
8
|
-
import { linkAt } from '../render/hit';
|
|
8
|
+
import { imageLinkAt, linkAt } from '../render/hit';
|
|
9
9
|
import { askConfirm } from './confirm';
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
10
|
+
import { imageById } from '../model/images';
|
|
11
|
+
import { settings } from '../model/settings';
|
|
12
|
+
import { page, SK, store } from '../model/store';
|
|
13
|
+
import type { LinkTarget, Note, Underlay } from '../model/types';
|
|
14
|
+
import { tileTypeOf } from '../render/linkTile';
|
|
15
|
+
import { cvs, linkBox, linkThumb, mainT, requestDraw, s2w, underlayMid } from '../render/view';
|
|
13
16
|
import { setTool } from '../tools/registry';
|
|
14
17
|
import { elevOpen } from './elevation';
|
|
15
18
|
import { is3d } from './view3dState';
|
|
@@ -126,20 +129,94 @@ export function toggleLeader(id: string): void {
|
|
|
126
129
|
afterEdit();
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
/** Open what the selected tile is for. */
|
|
132
|
+
/** Open what the selected tile, or the selected expanded photo, is for. */
|
|
130
133
|
export function openSelectedLink(): void {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
if (!canOpenLink(n.link)) { toast(linkInfoOf(n.link)?.exists === false ? 'That’s been removed from the job, so it can’t be opened.' : 'That can’t be opened from here.', { warn: true }); return; }
|
|
134
|
-
openLink(n.link);
|
|
134
|
+
const t = selectedLink()?.link ?? selectedLinkedImage()?.link;
|
|
135
|
+
if (t) openTarget(t);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
function openTarget(t: LinkTarget): void {
|
|
139
|
+
if (!canOpenLink(t)) { toast(linkInfoOf(t)?.exists === false ? 'That’s been removed from the job, so it can’t be opened.' : 'That can’t be opened from here.', { warn: true }); return; }
|
|
140
|
+
openLink(t);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** An expanded photo's badge, tapped: what it's for opens. */
|
|
144
|
+
export function openImageLink(id: string): void {
|
|
145
|
+
const t = imageById(id)?.link;
|
|
146
|
+
if (t) openTarget(t);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
A photo's tile expands into the photo whole, and back (the user's call, 2026-10-26: "for image objects … you should
|
|
151
|
+
be able to expand and condense the tile … this is how skia works, it expands the image to full size + shows a link
|
|
152
|
+
icon in the corner"). Expanded, it's one of the page's images — moved, sized, stretched, turned, faded and locked as
|
|
153
|
+
any is — with the link's badge in its corner; Expand in the bar (on) folds it back into its tile where it is. An image
|
|
154
|
+
from the computer has no link, so none of this. Each way is one undo step, and what was selected stays selected.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/** The one selected image, if it's a photo expanded from its tile and all that's selected. */
|
|
158
|
+
export function selectedLinkedImage(): (Underlay & { link: LinkTarget }) | null {
|
|
159
|
+
if (ed.sel.length !== 1 || ed.sel[0].type !== 'underlay') return null;
|
|
160
|
+
const u = imageById(ed.sel[0].id);
|
|
161
|
+
return u?.link ? (u as Underlay & { link: LinkTarget }) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether a tile can expand: a photo's (not a sketch's, a document's or a measurement's), and the photo still there. */
|
|
165
|
+
export function canExpand(n: Note & { link: LinkTarget }): boolean {
|
|
166
|
+
const info = linkInfoOf(n.link);
|
|
167
|
+
return tileTypeOf(n, info) === 'image' && info?.exists !== false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A photo's tile, expanded: the photo whole where the tile was, as big as it fits in 60% of the screen across and 80%
|
|
172
|
+
* down (an image from the computer is placed 60% across), at full strength, free to move.
|
|
173
|
+
*/
|
|
174
|
+
export function expandLink(id: string): void {
|
|
175
|
+
const sk = SK(), at = sk.notes.findIndex(x => x.id === id), n = sk.notes[at];
|
|
176
|
+
if (!n || !isLink(n) || !canExpand(n) || ed.readOnly) return;
|
|
177
|
+
const url = linkInfoOf(n.link)?.thumbUrl, im = url ? linkThumb(url) : null;
|
|
178
|
+
if (!im?.complete || !im.naturalWidth) { toast('The photo is still loading. Try again in a moment.', { warn: true }); return; }
|
|
179
|
+
const T = mainT(), aspect = (im.naturalHeight || 1) / im.naturalWidth;
|
|
180
|
+
const w = Math.min(cvs.W * 0.6, (cvs.H * 0.8) / aspect) / T.s, h = w * aspect;
|
|
181
|
+
const { x = 0, y = 0, link, ...tile } = n;
|
|
182
|
+
pushHistory();
|
|
183
|
+
sk.notes.splice(at, 1);
|
|
184
|
+
const u: Underlay = { id: uid('img'), src: '', x: x - w / 2, y: y + h / 2, w, h, opacity: 1, locked: false, link, tile };
|
|
185
|
+
page().underlays.push(u);
|
|
186
|
+
settings.layers.underlay = true;
|
|
187
|
+
ed.sel = [{ type: 'underlay', id: u.id }];
|
|
188
|
+
afterEdit();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** An expanded photo, folded back into its tile: where the photo's middle is, the page's tiles' size, its leader back. */
|
|
192
|
+
export function collapseLink(id: string): void {
|
|
193
|
+
const u = imageById(id);
|
|
194
|
+
if (!u?.link || ed.readOnly) return;
|
|
195
|
+
if (u.locked) { toast('This photo is locked. Unlock it to fold it back into its tile.', { warn: true }); return; }
|
|
196
|
+
const sk = SK(), m = underlayMid(u), was = u.tile ? (JSON.parse(JSON.stringify(u.tile)) as Omit<Note, 'x' | 'y' | 'link'>) : null;
|
|
197
|
+
const ts = pageLinkSize(sk) ?? was?.ts ?? Math.round(clamp(LINK_TILE_PX / mainT().s, LINK_MIN_IN, LINK_MAX_IN) * 4) / 4;
|
|
198
|
+
const n: Note = { ...(was ?? { id: '', type: 'link', color: LINK_COLOR }), type: 'link', x: m.x, y: m.y, ts, link: u.link };
|
|
199
|
+
if (!n.id || sk.notes.some(x => x.id === n.id)) n.id = uid('lk');
|
|
200
|
+
pushHistory();
|
|
201
|
+
page().underlays.splice(page().underlays.indexOf(u), 1);
|
|
202
|
+
sk.notes.push(n);
|
|
203
|
+
ed.sel = [{ type: 'note', id: n.id }];
|
|
204
|
+
afterEdit();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Expand in the bar: the selected photo's tile expanded, or the selected expanded photo folded back. */
|
|
208
|
+
export function toggleExpand(): void {
|
|
209
|
+
const n = selectedLink(), u = selectedLinkedImage();
|
|
210
|
+
if (n) expandLink(n.id);
|
|
211
|
+
else if (u) collapseLink(u.id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A view-only tap on the plan: on a link's tile or an expanded photo's badge, the person is asked, then what it's for opens. */
|
|
138
215
|
export function readOnlyTap(sx: number, sy: number, touch: boolean): void {
|
|
139
216
|
if (!ed.readOnly || !canLink()) return;
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
const
|
|
217
|
+
const t = linkAt(sx, sy, touch)?.link ?? imageLinkAt(sx, sy, touch)?.link;
|
|
218
|
+
if (!t || !canOpenLink(t)) return;
|
|
219
|
+
const where = linkInfoOf(t)?.groupName ?? t.groupName;
|
|
143
220
|
askConfirm({
|
|
144
221
|
title: 'Open ' + linkName(t) + '?',
|
|
145
222
|
msg: where ? 'It’s in ' + where + '.' : undefined,
|
package/src/editor/mode.ts
CHANGED
|
@@ -58,12 +58,13 @@ export const railIcon = (id: string): string => WALL_ICON[id] ?? id;
|
|
|
58
58
|
/** The tool rail for each mode. 'toggle' items switch something on and off; 'act' items run an action. */
|
|
59
59
|
export const RAIL: Record<Mode, RailItem[]> = {
|
|
60
60
|
draft: [
|
|
61
|
-
// drawing; then the building (walls, what goes in them, spaces); then
|
|
61
|
+
// drawing; then the building (walls, what goes in them, spaces); then guides
|
|
62
62
|
['select', 'Select', 'V'], ['line', 'Line', 'L'], ['freehand', 'Draw to shape', 'S'], ['rect', 'Rectangle', 'R'], ['circle', 'Circle', 'C'], ['arc', 'Arc', 'A'], '|',
|
|
63
63
|
['door', 'Door', 'B'], ['window', 'Window', 'N'], ['room', 'Space', 'M'], '|',
|
|
64
|
-
// Trim (T)
|
|
64
|
+
// Trim (T), Offset (O) and Dimension (D) stay on the keyboard; Offset is also in the bar for a selected line, and
|
|
65
|
+
// measuring is the yellow + for what's selected (the user's call, 2026-10-24: no Dimension on the rail).
|
|
65
66
|
// The toggles together: what's drawn is walls, or guides (one or the other, never both)
|
|
66
|
-
['
|
|
67
|
+
['wall', 'Walls', 'W', 'toggle'], ['constr', 'Construction lines', 'X', 'toggle'], '|',
|
|
67
68
|
['undo', 'Undo', '⌘Z', 'act'], ['redo', 'Redo', '⇧⌘Z', 'act'],
|
|
68
69
|
],
|
|
69
70
|
annotate: [
|
package/src/editor/project.ts
CHANGED
|
@@ -83,11 +83,10 @@ export type LayerKey = keyof Layers | 'showCons';
|
|
|
83
83
|
export const LAYER_ROWS: [LayerKey, string, string][] = [
|
|
84
84
|
['construction', 'Construction lines', 'Dashed guides'],
|
|
85
85
|
['dims', 'Driving dimensions', 'Blue chips that set sizes'],
|
|
86
|
-
['print', 'Print dimensions', 'Labels for the crew; never move geometry'],
|
|
87
86
|
['rooms', 'Spaces', 'Fill, name and area'],
|
|
88
87
|
['cabinets', 'Cabinets', 'Their footprints, from each wall’s view'],
|
|
89
88
|
['annot', 'Markup', 'Pen, arrows, shapes, clouds and notes'],
|
|
90
|
-
['showCons', 'Constraint icons', 'H, V, ∥, ⊥ badges'],
|
|
89
|
+
['showCons', 'Constraint icons', 'H, V, ∥, ⊥ and lock badges'],
|
|
91
90
|
];
|
|
92
91
|
|
|
93
92
|
export const layerOn = (k: LayerKey): boolean => (k === 'showCons' ? settings.showCons : settings.layers[k]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isUnscaled } from '../model/scale';
|
|
2
|
+
import { planSK } from '../model/store';
|
|
3
|
+
import { fmtLen } from '../units';
|
|
4
|
+
import { toast } from './ports';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Before a new page's first length (model/scale.ts), its sizes are placeholders. So nothing shows one as if it were
|
|
8
|
+
real, and what needs real sizes waits: the 3D view, the wall view, doors, windows and cabinets (the user's call,
|
|
9
|
+
2026-10-29), and the tools whose typed size has nothing drawn to be compared with (offset, fillet, chamfer).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The page being drawn has no scale yet: its sizes are placeholders. */
|
|
13
|
+
export const noScaleYet = (): boolean => isUnscaled(planSK());
|
|
14
|
+
|
|
15
|
+
export const NEEDS_SCALE = 'Enter a length first: select a line and tap +. It sets the size of the whole drawing.';
|
|
16
|
+
|
|
17
|
+
/** What needs real sizes, asked for before the page has its scale: says what to do first, and true (wait). */
|
|
18
|
+
export function waitForScale(): boolean {
|
|
19
|
+
if (!noScaleYet()) return false;
|
|
20
|
+
toast(NEEDS_SCALE, { warn: true });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tools that place something with a real size, or take one typed: doors, windows and cabinets (the plan's and the wall view's), offsets. */
|
|
25
|
+
const SIZED_TOOLS = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet', 'height', 'offset']);
|
|
26
|
+
export const toolWaitsForScale = (t: string): boolean => SIZED_TOOLS.has(t);
|
|
27
|
+
|
|
28
|
+
/** A length being entered, as its label shows it: what's typed, else what it measures now — nothing while that's a placeholder. */
|
|
29
|
+
export const pendingLabel = (typed: number | null, now: number): string => (typed != null ? fmtLen(typed) : noScaleYet() ? '' : fmtLen(now));
|
|
30
|
+
|
|
31
|
+
/** A keypad's starting value: what it measures now, or nothing while that's only a placeholder. */
|
|
32
|
+
export const startingValue = (v: number): { initial: string; value?: number } => (noScaleYet() ? { initial: '' } : { initial: fmtLen(v), value: v });
|
package/src/editor/selection.ts
CHANGED
|
@@ -11,9 +11,13 @@ import { faceLength } from '../model/wallfaces';
|
|
|
11
11
|
import { planSK, SK } from '../model/store';
|
|
12
12
|
import type { Sketch } from '../model/types';
|
|
13
13
|
import { fmtArea, fmtLen } from '../units';
|
|
14
|
+
import { noScaleYet } from './scaleGate';
|
|
14
15
|
|
|
15
16
|
/* What is selected, grouped by kind, and the one-line hint describing it (bold parts in <b>). */
|
|
16
17
|
|
|
18
|
+
/** A size in a hint, after a space — none on a drawing without its scale yet (a placeholder isn't a size). */
|
|
19
|
+
const sized = (text: string): string => (noScaleYet() ? '' : ' ' + text);
|
|
20
|
+
|
|
17
21
|
export function selInfo() {
|
|
18
22
|
const g = (t: string) => ed.sel.filter(s => s.type === t).map(s => s.id);
|
|
19
23
|
const circles = g('circle'), arcs = g('arc');
|
|
@@ -54,13 +58,13 @@ export function selectionHint(): string {
|
|
|
54
58
|
if (s.n === 1 && s.lines.length && sk.lines[s.lines[0]]?.anchor) return '<b>Set by the plan</b> · pick a point or line too, to constrain or dimension to it';
|
|
55
59
|
if (s.n === 1 && s.lines.length) {
|
|
56
60
|
const l = sk.lines[s.lines[0]];
|
|
57
|
-
return '<b>' + (l.kind === 'wall' ? 'Wall' : l.constr ? 'Construction line' : 'Line') +
|
|
61
|
+
return '<b>' + (l.kind === 'wall' ? 'Wall' : l.constr ? 'Construction line' : 'Line') + sized(fmtLen(lineLen(sk, l))) + '</b> · ' + (lineDefined(ed.ana, l) ? 'fully defined' : lengthDimOf(sk, s.lines[0]) ? 'pick a constraint below or drag to adjust' : 'tap <b>+</b> below ' + forLength() + ', or pick a constraint');
|
|
58
62
|
}
|
|
59
63
|
if (s.n === 1 && s.wfaces.length) {
|
|
60
64
|
const [id, side] = edgeOf(s.wfaces[0]);
|
|
61
65
|
// (a curved wall's: its diameter across it)
|
|
62
|
-
if (!sk.lines[id]) return '<b>' + edgeLabel(sk, id, side) + '
|
|
63
|
-
return '<b>' + edgeLabel(sk, id, side) +
|
|
66
|
+
if (!sk.lines[id]) return '<b>' + edgeLabel(sk, id, side) + sized('Ø ' + fmtLen(measure(sk, { type: 'diam', c: id, face: side }))) + '</b> · tap <b>+</b> for its diameter';
|
|
67
|
+
return '<b>' + edgeLabel(sk, id, side) + sized(fmtLen(faceLength(sk, id, side))) + '</b> · tap <b>+</b> ' + forLength() + ', or pick another edge or a point to measure from it';
|
|
64
68
|
}
|
|
65
69
|
if (edgePair(s)) {
|
|
66
70
|
const one = s.wfaces.length === 2 && edgeOf(s.wfaces[0])[0] === edgeOf(s.wfaces[1])[0];
|
|
@@ -75,6 +79,7 @@ export function selectionHint(): string {
|
|
|
75
79
|
if (s.n === 1 && s.cons.length) return '<b>' + consLabel(sk.cons[s.cons[0]]) + '</b> constraint · delete it to free the shape';
|
|
76
80
|
if (s.n === 1 && s.rooms.length) {
|
|
77
81
|
const r = sk.rooms[s.rooms[0]], g = roomGeom(sk, r);
|
|
82
|
+
if (noScaleYet()) return '<b>' + r.name + '</b> · its area shows once the drawing has a length';
|
|
78
83
|
return '<b>' + r.name + '</b> · ' + fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perimeter';
|
|
79
84
|
}
|
|
80
85
|
if (s.n === 1 && s.openings.length) {
|
|
@@ -86,7 +91,6 @@ export function selectionHint(): string {
|
|
|
86
91
|
}
|
|
87
92
|
if (s.n === 1 && s.dims.length) {
|
|
88
93
|
const c = sk.cons[s.dims[0]];
|
|
89
|
-
if (c.layer === 'print') return '<b>Print dimension</b> · drag to move the label';
|
|
90
94
|
if (c.measurementRef) return '<b>' + consLabel(c) + '</b> · linked to <b>' + linkName(c.measurementRef) + '</b> · tap <b>+</b> to change it';
|
|
91
95
|
return '<b>' + consLabel(c) + '</b> · tap <b>+</b> to change it';
|
|
92
96
|
}
|
|
@@ -96,9 +100,11 @@ export function selectionHint(): string {
|
|
|
96
100
|
if (part === 'w') return '<b>' + (planSK().openings[s.odims[0].split('|')[0]]?.type === 'door' ? 'Door' : 'Window') + ' width</b> · tap <b>+</b> to change it';
|
|
97
101
|
return part.startsWith('d:') ? '<b>Door/window dimension</b> · tap <b>+</b> to change it' : '<b>Distance to the wall end</b> · tap <b>+</b> to lock it';
|
|
98
102
|
}
|
|
99
|
-
if (s.n === 1 && s.image)
|
|
100
|
-
|
|
101
|
-
: '<b>Image</b>
|
|
103
|
+
if (s.n === 1 && s.image) {
|
|
104
|
+
// (a photo of the job's, expanded from its tile: its link opens it, and Expand folds it back)
|
|
105
|
+
const u = imageById(ed.sel[0].id), what = u?.link ? '<b>Photo</b>' : '<b>Image</b>', link = u?.link ? ' · its link opens it' : '';
|
|
106
|
+
return u?.locked ? what + ' · locked — tap Unlock to move or resize it' + link : what + ' · drag to move · corners scale, side handles stretch' + link;
|
|
107
|
+
}
|
|
102
108
|
if (s.n === 1 && s.notes.length && SK().notes.find(n => n.id === s.notes[0])?.type === 'link') return '<b>Link</b> · drag to move · open it, or point it at the drawing with a leader, below · Size sizes every tile on the page';
|
|
103
109
|
if (s.notes.length) return 'Drag to move. Turn a pen stroke into lines below.';
|
|
104
110
|
return '<b>' + s.n + ' selected</b> · choose a constraint below';
|
package/src/editor/tooltips.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ed } from '../app/state';
|
|
2
2
|
import { settings } from '../model/settings';
|
|
3
|
+
import { noScaleYet, toolWaitsForScale } from './scaleGate';
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
What a tool does, said in a line when it's picked from the rail (a quick note under the Annotate / Draft tabs that
|
|
@@ -34,14 +35,21 @@ const TIPS: Record<string, string> = {
|
|
|
34
35
|
text: 'Tap where it goes, then type.',
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
/** The tools that draw shapes: on a page without its scale, what they draw is a sketch. */
|
|
39
|
+
const SKETCHING = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
|
|
40
|
+
|
|
37
41
|
/**
|
|
38
42
|
* The line for a rail item just picked (its name in bold), or null if it has none (Undo, a colour, an action) or the
|
|
39
43
|
* notes are turned off (••• → Tool tips).
|
|
40
44
|
*/
|
|
41
45
|
export function toolTip(id: string, label: string): string | null {
|
|
42
46
|
if (settings.toolTips === false) return null;
|
|
47
|
+
// (on a page without its scale a door, a window or an offset isn't picked: a toast says to enter a length first)
|
|
48
|
+
if (toolWaitsForScale(id) && noScaleYet()) return null;
|
|
43
49
|
if (id === 'constr') return ed.drawConstr ? '<b>Construction lines on</b> · new lines are dashed guides.' : '<b>Construction lines off</b> · new lines are solid.';
|
|
44
50
|
if (id === 'wall') return ed.drawWalls ? '<b>Walls on</b> · what you draw is walls.' : '<b>Walls off</b> · what you draw is lines.';
|
|
45
51
|
const t = TIPS[id];
|
|
52
|
+
// drawing on a page without its scale: sketched in proportion, sized by its first length (editor/scaleGate.ts)
|
|
53
|
+
if (t && SKETCHING.has(id) && noScaleYet()) return '<b>' + label + '</b> · ' + t + ' Your first length sets the size.';
|
|
46
54
|
return t ? '<b>' + label + '</b> · ' + t : null;
|
|
47
55
|
}
|
package/src/editor/view3d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { KeyIn } from './keys';
|
|
|
16
16
|
import { setMode } from './mode';
|
|
17
17
|
import { dismissConflict, dismissPopovers, refreshModes, updateCtx, updateHint, updateRail } from './ports';
|
|
18
18
|
import { command3d, pick3d, v3 } from './view3dState';
|
|
19
|
+
import { waitForScale } from './scaleGate';
|
|
19
20
|
|
|
20
21
|
/*
|
|
21
22
|
The 3D tab: opening and leaving it, its rail's views, what the bar shows for something tapped in it (its sizes, the
|
|
@@ -30,9 +31,12 @@ import { command3d, pick3d, v3 } from './view3dState';
|
|
|
30
31
|
export const view3dKey = (): string =>
|
|
31
32
|
[store.curPageId, ed.editSeq, settings.wallH, v3.cutaway, v3.cmd.seq, v3.pick ? v3.pick.kind + ':' + v3.pick.id : ''].join('|');
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Show the 3D tab: whatever tool was in hand is put down, and nothing is selected. Not before the page's first length:
|
|
36
|
+
* its walls' heights are real and their lengths only placeholders until then (editor/scaleGate.ts).
|
|
37
|
+
*/
|
|
34
38
|
export function open3d(): void {
|
|
35
|
-
if (v3.open) return;
|
|
39
|
+
if (v3.open || waitForScale()) return;
|
|
36
40
|
cancelTool();
|
|
37
41
|
dismissPopovers();
|
|
38
42
|
if (elevOpen()) closeElevation();
|
package/src/export/pdf.ts
CHANGED
|
@@ -20,10 +20,13 @@ const SCALES: [k: number, label: string][] = [
|
|
|
20
20
|
[3 / 16, '3/16"'], [1 / 8, '1/8"'], [3 / 32, '3/32"'], [1 / 16, '1/16"'], [1 / 32, '1/32"'],
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Pick a scale for a drawing w × h inches: points per world inch, and its label. A sketch without a scale yet (its
|
|
25
|
+
* sizes only placeholders: model/scale.ts) is fitted to the sheet, not to scale.
|
|
26
|
+
*/
|
|
27
|
+
export function pickScale(w: number, h: number, sketchOnly = false): { s: number; label: string } {
|
|
25
28
|
const aw = AREA.x1 - AREA.x0, ah = AREA.y1 - AREA.y0;
|
|
26
|
-
for (const [k, label] of SCALES) {
|
|
29
|
+
if (!sketchOnly) for (const [k, label] of SCALES) {
|
|
27
30
|
const s = (72 * k) / 12;
|
|
28
31
|
if (w * s <= aw && h * s <= ah) return { s, label: label + " = 1'-0\"" };
|
|
29
32
|
}
|
|
@@ -166,7 +169,7 @@ export function sketchToPdf(sk: Sketch, opts: PdfOptions = {}): string {
|
|
|
166
169
|
const rec = new Recorder();
|
|
167
170
|
emitDrawing(sk, rec, opts);
|
|
168
171
|
const bb = rec.bbox();
|
|
169
|
-
const { s, label } = bb ? pickScale(bb.x1 - bb.x0, bb.y1 - bb.y0) : { s: 1, label: 'Not to scale' };
|
|
172
|
+
const { s, label } = bb ? pickScale(bb.x1 - bb.x0, bb.y1 - bb.y0, !!sk.unscaled) : { s: 1, label: 'Not to scale' };
|
|
170
173
|
const cx = bb ? (bb.x0 + bb.x1) / 2 : 0, cy = bb ? (bb.y0 + bb.y1) / 2 : 0;
|
|
171
174
|
const ox = (AREA.x0 + AREA.x1) / 2, oy = (AREA.y0 + AREA.y1) / 2;
|
|
172
175
|
const T = (p: XY): XY => ({ x: ox + (p.x - cx) * s, y: oy + (p.y - cy) * s });
|