@reekon-tools/boldr-cad 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +6 -8
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +255 -181
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +4 -3
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +4 -2
- package/dist/editor/laser.js +4 -2
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -12
- package/dist/editor/linkActions.js +164 -35
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/links.d.ts +13 -0
- package/dist/editor/links.js +7 -1
- package/dist/editor/links.js.map +1 -1
- package/dist/editor/mode.d.ts +2 -1
- package/dist/editor/mode.js +8 -5
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +10 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/geom.d.ts +2 -0
- package/dist/model/geom.js +11 -0
- package/dist/model/geom.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +10 -2
- package/dist/model/links.js +15 -3
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +123 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +26 -2
- package/dist/model/sketch.js +77 -22
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/trim.d.ts +54 -0
- package/dist/model/trim.js +219 -0
- package/dist/model/trim.js.map +1 -0
- package/dist/model/types.d.ts +62 -9
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +144 -46
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/CadEditor.js +18 -9
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Drawer.js +6 -4
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/PlanCanvas.web.js +17 -5
- package/dist/react/PlanCanvas.web.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.js.map +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/ToolTip.js +3 -2
- package/dist/react/ToolTip.js.map +1 -1
- package/dist/react/TopBar.js +4 -3
- package/dist/react/TopBar.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/editorBox.d.ts +8 -0
- package/dist/react/editorBox.js +10 -0
- package/dist/react/editorBox.js.map +1 -0
- package/dist/react/host.d.ts +1 -1
- package/dist/react/host.js +1 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/react/pageLock.native.d.ts +2 -0
- package/dist/react/pageLock.native.js +2 -0
- package/dist/react/pageLock.native.js.map +1 -1
- package/dist/react/pageLock.web.d.ts +6 -0
- package/dist/react/pageLock.web.js +12 -3
- package/dist/react/pageLock.web.js.map +1 -1
- package/dist/react/ports.js +8 -3
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +7 -1
- package/dist/react/saving.js +10 -2
- package/dist/react/saving.js.map +1 -1
- package/dist/react/store.d.ts +5 -2
- package/dist/react/store.js +4 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +203 -151
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +10 -1
- package/dist/render/snap.js +87 -15
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +38 -6
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +53 -22
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.d.ts +32 -4
- package/dist/tools/modify.js +253 -98
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +14 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +30 -8
- package/dist/tools/select.js +165 -34
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +6 -8
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +213 -122
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +3 -2
- package/src/editor/laser.ts +7 -4
- package/src/editor/linkActions.ts +169 -31
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +8 -5
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +9 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/geom.ts +10 -0
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +14 -3
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +107 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +62 -11
- package/src/model/stroke.ts +2 -2
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +57 -9
- package/src/react/BottomBar.tsx +177 -52
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +17 -12
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/PlanCanvas.web.tsx +17 -5
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/Sheet.tsx +3 -3
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +1 -1
- package/src/react/motion.tsx +6 -6
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +7 -2
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +169 -118
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +87 -17
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +43 -7
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +52 -22
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +210 -70
- package/src/tools/registry.ts +18 -1
- package/src/tools/select.ts +144 -36
- package/src/tools/shapes.ts +9 -4
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
- package/dist/react/LaserTray.d.ts +0 -4
- package/dist/react/LaserTray.js +0 -26
- package/dist/react/LaserTray.js.map +0 -1
- package/src/react/LaserTray.tsx +0 -42
package/src/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,8 @@ 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
|
-
|
|
92
|
+
// (⇧Enter: beside the drawing, where the host can show it there)
|
|
93
|
+
if (e.key === 'Enter' && ed.mode === 'annotate' && (selectedLink() || selectedLinkedImage())) { openSelectedLink(e.shift ? 'beside' : 'auto'); return true; }
|
|
93
94
|
if (e.shift) {
|
|
94
95
|
const c = SHIFT_CONS[e.key.toUpperCase()];
|
|
95
96
|
if (c && ed.mode === 'draft') { applyCtxAction(c); return true; }
|
package/src/editor/laser.ts
CHANGED
|
@@ -17,10 +17,11 @@ import { selInfo } from './selection';
|
|
|
17
17
|
|
|
18
18
|
/*
|
|
19
19
|
Measure-then-assign for laser and tape readings (inches), whatever device sends them: a reading goes into an open
|
|
20
|
-
keypad, onto the one selected line
|
|
20
|
+
keypad, or onto the one selected line; one with nowhere to go is kept for a keypad's Laser key (and, in the harness,
|
|
21
|
+
its tray, where it can be dragged onto a line later).
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
|
-
/** A reading, and the host's record of it (laserReading's `capture`), kept with it
|
|
24
|
+
/** A reading, and the host's record of it (laserReading's `capture`), kept with it until it's used. */
|
|
24
25
|
export interface Reading { id: string; v: number; capture?: unknown }
|
|
25
26
|
/** `external`: a real measure the host app is connected to (otherwise the harness's simulated one). */
|
|
26
27
|
export const laser = sessionState(() => ({ on: false, external: false, readings: [] as Reading[] }));
|
|
@@ -100,7 +101,9 @@ function appliedTo(id: string): string {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/**
|
|
103
|
-
* A reading arrived: into the keypad, onto the selected line or curve, or
|
|
104
|
+
* A reading arrived: into the keypad, onto the selected line or curve, or — with nowhere to go — kept for a keypad's
|
|
105
|
+
* Laser key (newest first, at most 8) and said so. The hosts show no tray of them (the user's call, 2026-11-03: "get rid
|
|
106
|
+
* of the long laser reading pop up … it is not needed"); the harness's own still lists them (ui/laser.ts).
|
|
104
107
|
* `capture` is the host's record of it (its device, its number), kept with the dimension it sets until that's saved
|
|
105
108
|
* to the group — or handed to the job with it, for one linked to a measurement there.
|
|
106
109
|
*/
|
|
@@ -117,7 +120,7 @@ export function applyReading(v: number, capture?: unknown): void {
|
|
|
117
120
|
laser.readings.unshift({ id: uid('m'), v, ...(capture !== undefined ? { capture } : {}) });
|
|
118
121
|
laser.readings = laser.readings.slice(0, 8);
|
|
119
122
|
refreshTray();
|
|
120
|
-
toast('
|
|
123
|
+
toast('Select a line, then take the reading.', { warn: true });
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
/**
|
|
@@ -3,17 +3,20 @@ import { ed } from '../app/state';
|
|
|
3
3
|
import { clamp } from '../model/geom';
|
|
4
4
|
import { pushHistory } from '../model/history';
|
|
5
5
|
import { uid } from '../model/ids';
|
|
6
|
-
import { isLink, LINK_COLOR, LINK_MAX_IN, LINK_MIN_IN, LINK_TILE_PX, linkInfoOf, linkName, linkSide, linkSnapshot } from '../model/links';
|
|
6
|
+
import { isLink, LINK_COLOR, LINK_HARD_MAX_IN, LINK_HARD_MIN_IN, LINK_MAX_IN, LINK_MIN_IN, LINK_SIDE_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, Sketch, 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';
|
|
16
|
-
import { canLink, linksHost, openLink, pickLinks } from './links';
|
|
19
|
+
import { besideOpen, canLink, canOpenBeside, linksHost, openLink, openLinkBeside, pickLinks } from './links';
|
|
17
20
|
import { toast } from './ports';
|
|
18
21
|
|
|
19
22
|
/*
|
|
@@ -65,6 +68,8 @@ export async function addLinks(): Promise<void> {
|
|
|
65
68
|
let k = 0;
|
|
66
69
|
while (k < 40 && spots.some(([x, y]) => taken(x + k * step, y - k * step))) k++;
|
|
67
70
|
pushHistory();
|
|
71
|
+
// (the page's first tiles: the size they're placed at is their 100%)
|
|
72
|
+
if (pageLinkSize(sk) == null) sk.tileRef = ts;
|
|
68
73
|
const ids = picked.map((t, i) => {
|
|
69
74
|
const n: Note = { id: uid('lk'), type: 'link', x: spots[i][0] + k * step, y: spots[i][1] - k * step, ts, color: LINK_COLOR, link: linkSnapshot(t) };
|
|
70
75
|
sk.notes.push(n);
|
|
@@ -80,7 +85,9 @@ const LINK_GAP_PX = 16;
|
|
|
80
85
|
|
|
81
86
|
/*
|
|
82
87
|
Every tile on a page is one size, on the drawing (so it zooms with the plan and prints at that size): the Tile size
|
|
83
|
-
slider above the bar, shown while a tile is selected, sizes them all together.
|
|
88
|
+
slider above the bar, shown while a tile is selected, sizes them all together. It says how big in percent of the size
|
|
89
|
+
they were first placed at (the user's call, 2026-11-03): a tile has no real size, and before the page's first length
|
|
90
|
+
its inches are only placeholders, so feet and metres said nothing true.
|
|
84
91
|
*/
|
|
85
92
|
|
|
86
93
|
/** The size of the page's tiles, inches on the drawing (null: it has none). */
|
|
@@ -96,21 +103,44 @@ export function linkSizeVisible(): boolean {
|
|
|
96
103
|
return ed.sel.some(x => x.type === 'note' && sk.notes.find(n => n.id === x.id)?.type === 'link');
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
/** What the page's tiles call 100%, inches on the drawing: the size they were first placed at (a page from before: theirs now). */
|
|
107
|
+
const tileRef = (sk: Sketch): number => sk.tileRef ?? pageLinkSize(sk) ?? LINK_SIDE_IN;
|
|
108
|
+
|
|
109
|
+
/** How big the page's tiles are, in percent of the size they were first placed at (null: it has none). */
|
|
110
|
+
export function pageTilePct(sk = SK()): number | null {
|
|
111
|
+
const s = pageLinkSize(sk);
|
|
112
|
+
return s == null ? null : Math.round((s / tileRef(sk)) * 100);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The slider's range, percent. */
|
|
116
|
+
export const TILE_PCT_MIN = 10, TILE_PCT_MAX = 1000;
|
|
117
|
+
|
|
99
118
|
/**
|
|
100
|
-
* Size every tile on the page
|
|
101
|
-
*
|
|
119
|
+
* Size every tile on the page to `pct` percent (TILE_PCT_MIN to TILE_PCT_MAX; and never past LINK_HARD_MIN_IN to
|
|
120
|
+
* LINK_HARD_MAX_IN on the drawing — placing's 6"–40' bounds would cut the range short), drawn at once — for the slider as
|
|
121
|
+
* it moves. The caller makes the undo step before, and finishes the edit after. (A page from before keeps its 100% from
|
|
122
|
+
* here on.)
|
|
102
123
|
*/
|
|
103
|
-
export function
|
|
104
|
-
const v =
|
|
105
|
-
|
|
124
|
+
export function setTilePct(pct: number): void {
|
|
125
|
+
const sk = SK(), ref = tileRef(sk), v = clamp((ref * clamp(pct, TILE_PCT_MIN, TILE_PCT_MAX)) / 100, LINK_HARD_MIN_IN, LINK_HARD_MAX_IN);
|
|
126
|
+
sk.tileRef = ref;
|
|
127
|
+
for (const n of sk.notes) if (isLink(n)) n.ts = v;
|
|
106
128
|
requestDraw();
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* The slider runs along a log scale (each step as noticeable as the last), 0 to SIZE_STEPS, 100% in the middle; it
|
|
133
|
+
* catches at 100%, so the size they came in at is easy to find again, and says round numbers: whole percents to 200%,
|
|
134
|
+
* then fives, then tens from 500%.
|
|
135
|
+
*/
|
|
110
136
|
export const SIZE_STEPS = 1000;
|
|
111
|
-
const LOG_MIN = Math.log(
|
|
112
|
-
export const
|
|
113
|
-
export
|
|
137
|
+
const LOG_MIN = Math.log(TILE_PCT_MIN), LOG_SPAN = Math.log(TILE_PCT_MAX) - LOG_MIN;
|
|
138
|
+
export const pctToStep = (pct: number): number => Math.round(((Math.log(clamp(pct, TILE_PCT_MIN, TILE_PCT_MAX)) - LOG_MIN) / LOG_SPAN) * SIZE_STEPS);
|
|
139
|
+
export function stepToPct(step: number): number {
|
|
140
|
+
const p = Math.exp(LOG_MIN + (clamp(step, 0, SIZE_STEPS) / SIZE_STEPS) * LOG_SPAN);
|
|
141
|
+
if (Math.abs(p - 100) < 4) return 100;
|
|
142
|
+
return p >= 500 ? Math.round(p / 10) * 10 : p >= 200 ? Math.round(p / 5) * 5 : Math.round(p);
|
|
143
|
+
}
|
|
114
144
|
|
|
115
145
|
/** A tile's leader on or off. On, it points out beside the tile (to the right, or the left near the screen's edge). */
|
|
116
146
|
export function toggleLeader(id: string): void {
|
|
@@ -126,24 +156,134 @@ export function toggleLeader(id: string): void {
|
|
|
126
156
|
afterEdit();
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
/*
|
|
160
|
+
Open, for what a link is for: the host's own way (another screen, a sheet over the drawing), or — the user's call,
|
|
161
|
+
2026-10-30 — in a pane beside the drawing, a split view, where the host has one (CadLinks.openBeside). The bar's Open
|
|
162
|
+
asks which while no pane is open (opensWithChoice); once one is, Open goes to it.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/** How Open opens: `here`, the host's own way; `beside`, in a pane beside the drawing; `auto`, beside if one's open. */
|
|
166
|
+
export type OpenHow = 'auto' | 'here' | 'beside';
|
|
167
|
+
|
|
168
|
+
/** Open what the selected tile, or the selected expanded photo, is for. */
|
|
169
|
+
export function openSelectedLink(how: OpenHow = 'auto'): void {
|
|
170
|
+
const t = selectedLink()?.link ?? selectedLinkedImage()?.link;
|
|
171
|
+
if (t) openLinkTarget(t, how);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** What the bar's Open is for: the selected tile's or expanded photo's link, or the selected linked dimension's. */
|
|
175
|
+
export function selectedOpenTarget(): LinkTarget | null {
|
|
176
|
+
const t = selectedLink()?.link ?? selectedLinkedImage()?.link;
|
|
177
|
+
if (t) return t;
|
|
178
|
+
const s = ed.sel;
|
|
179
|
+
return s.length === 1 && s[0].type === 'dim' ? SK().cons[s[0].id]?.measurementRef ?? null : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether Open asks how to open `t`: the host can show it beside the drawing, and no pane's open yet. */
|
|
183
|
+
export function opensWithChoice(t: LinkTarget): boolean {
|
|
184
|
+
return canOpenLink(t) && canOpenBeside(t) && !besideOpen();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Open what `t` is for, `how` it's asked to (see OpenHow); one that's gone, or can't be, says so instead. */
|
|
188
|
+
export function openLinkTarget(t: LinkTarget, how: OpenHow = 'auto'): void {
|
|
189
|
+
if (!canOpenLink(t)) {
|
|
190
|
+
const gone = linkInfoOf(t)?.exists === false;
|
|
191
|
+
toast(!gone ? 'That can’t be opened from here.'
|
|
192
|
+
: t.kind === 'measurement' ? 'That measurement has been removed from the job, so it can’t be opened.'
|
|
193
|
+
: 'That’s been removed from the job, so it can’t be opened.', { warn: true });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if ((how === 'beside' || (how === 'auto' && besideOpen())) && canOpenBeside(t)) openLinkBeside(t);
|
|
197
|
+
else openLink(t);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** An expanded photo's badge, tapped: what it's for opens (beside the drawing, if a pane's open there). */
|
|
201
|
+
export function openImageLink(id: string): void {
|
|
202
|
+
const t = imageById(id)?.link;
|
|
203
|
+
if (t) openLinkTarget(t);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
A photo's tile expands into the photo whole, and back (the user's call, 2026-10-26: "for image objects … you should
|
|
208
|
+
be able to expand and condense the tile … this is how skia works, it expands the image to full size + shows a link
|
|
209
|
+
icon in the corner"). Expanded, it's one of the page's images — moved, sized, cropped, turned, faded and locked as
|
|
210
|
+
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
|
|
211
|
+
from the computer has no link, so none of this. Each way is one undo step, and what was selected stays selected.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/** The one selected image, if it's a photo expanded from its tile and all that's selected. */
|
|
215
|
+
export function selectedLinkedImage(): (Underlay & { link: LinkTarget }) | null {
|
|
216
|
+
if (ed.sel.length !== 1 || ed.sel[0].type !== 'underlay') return null;
|
|
217
|
+
const u = imageById(ed.sel[0].id);
|
|
218
|
+
return u?.link ? (u as Underlay & { link: LinkTarget }) : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Whether a tile can expand: a photo's (not a sketch's, a document's or a measurement's), and the photo still there. */
|
|
222
|
+
export function canExpand(n: Note & { link: LinkTarget }): boolean {
|
|
223
|
+
const info = linkInfoOf(n.link);
|
|
224
|
+
return tileTypeOf(n, info) === 'image' && info?.exists !== false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 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%
|
|
229
|
+
* down (an image from the computer is placed 60% across), at full strength, free to move.
|
|
230
|
+
*/
|
|
231
|
+
export function expandLink(id: string): void {
|
|
232
|
+
const sk = SK(), at = sk.notes.findIndex(x => x.id === id), n = sk.notes[at];
|
|
233
|
+
if (!n || !isLink(n) || !canExpand(n) || ed.readOnly) return;
|
|
234
|
+
const url = linkInfoOf(n.link)?.thumbUrl, im = url ? linkThumb(url) : null;
|
|
235
|
+
if (!im?.complete || !im.naturalWidth) { toast('The photo is still loading. Try again in a moment.', { warn: true }); return; }
|
|
236
|
+
const T = mainT(), aspect = (im.naturalHeight || 1) / im.naturalWidth;
|
|
237
|
+
const w = Math.min(cvs.W * 0.6, (cvs.H * 0.8) / aspect) / T.s, h = w * aspect;
|
|
238
|
+
const { x = 0, y = 0, link, ...tile } = n;
|
|
239
|
+
pushHistory();
|
|
240
|
+
sk.notes.splice(at, 1);
|
|
241
|
+
const u: Underlay = { id: uid('img'), src: '', x: x - w / 2, y: y + h / 2, w, h, opacity: 1, locked: false, link, tile };
|
|
242
|
+
page().underlays.push(u);
|
|
243
|
+
settings.layers.underlay = true;
|
|
244
|
+
ed.sel = [{ type: 'underlay', id: u.id }];
|
|
245
|
+
afterEdit();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** An expanded photo, folded back into its tile: where the photo's middle is, the page's tiles' size, its leader back. */
|
|
249
|
+
export function collapseLink(id: string): void {
|
|
250
|
+
const u = imageById(id);
|
|
251
|
+
if (!u?.link || ed.readOnly) return;
|
|
252
|
+
if (u.locked) { toast('This photo is locked. Unlock it to fold it back into its tile.', { warn: true }); return; }
|
|
253
|
+
const sk = SK(), m = underlayMid(u), was = u.tile ? (JSON.parse(JSON.stringify(u.tile)) as Omit<Note, 'x' | 'y' | 'link'>) : null;
|
|
254
|
+
const ts = pageLinkSize(sk) ?? was?.ts ?? Math.round(clamp(LINK_TILE_PX / mainT().s, LINK_MIN_IN, LINK_MAX_IN) * 4) / 4;
|
|
255
|
+
const n: Note = { ...(was ?? { id: '', type: 'link', color: LINK_COLOR }), type: 'link', x: m.x, y: m.y, ts, link: u.link };
|
|
256
|
+
if (!n.id || sk.notes.some(x => x.id === n.id)) n.id = uid('lk');
|
|
257
|
+
pushHistory();
|
|
258
|
+
// (the page's only tile, back: its size is the 100%, unless the page already has one)
|
|
259
|
+
if (pageLinkSize(sk) == null && sk.tileRef == null) sk.tileRef = ts;
|
|
260
|
+
page().underlays.splice(page().underlays.indexOf(u), 1);
|
|
261
|
+
sk.notes.push(n);
|
|
262
|
+
ed.sel = [{ type: 'note', id: n.id }];
|
|
263
|
+
afterEdit();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Expand in the bar: the selected photo's tile expanded, or the selected expanded photo folded back. */
|
|
267
|
+
export function toggleExpand(): void {
|
|
268
|
+
const n = selectedLink(), u = selectedLinkedImage();
|
|
269
|
+
if (n) expandLink(n.id);
|
|
270
|
+
else if (u) collapseLink(u.id);
|
|
135
271
|
}
|
|
136
272
|
|
|
137
|
-
/** A view-only tap on the plan: on a link's tile, the person is asked, then what it's for opens. */
|
|
273
|
+
/** 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
274
|
export function readOnlyTap(sx: number, sy: number, touch: boolean): void {
|
|
139
275
|
if (!ed.readOnly || !canLink()) return;
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
const
|
|
276
|
+
const t = linkAt(sx, sy, touch)?.link ?? imageLinkAt(sx, sy, touch)?.link;
|
|
277
|
+
if (!t || !canOpenLink(t)) return;
|
|
278
|
+
const where = linkInfoOf(t)?.groupName ?? t.groupName;
|
|
143
279
|
askConfirm({
|
|
144
280
|
title: 'Open ' + linkName(t) + '?',
|
|
145
281
|
msg: where ? 'It’s in ' + where + '.' : undefined,
|
|
146
|
-
choices: [
|
|
282
|
+
choices: [
|
|
283
|
+
{ label: 'Open', run: () => openLinkTarget(t, 'here') },
|
|
284
|
+
...(canOpenBeside(t) ? [{ label: 'Open side by side', run: () => openLinkTarget(t, 'beside') }] : []),
|
|
285
|
+
{ label: 'Cancel', run: () => {} },
|
|
286
|
+
],
|
|
147
287
|
});
|
|
148
288
|
}
|
|
149
289
|
|
|
@@ -157,11 +297,9 @@ export function unlinkDim(id: string): void {
|
|
|
157
297
|
}
|
|
158
298
|
|
|
159
299
|
/** Open the measurement a dimension is linked to. */
|
|
160
|
-
export function openDimLink(id: string): void {
|
|
300
|
+
export function openDimLink(id: string, how: OpenHow = 'auto'): void {
|
|
161
301
|
const t = SK().cons[id]?.measurementRef;
|
|
162
|
-
if (
|
|
163
|
-
if (!canOpenLink(t)) { toast(linkInfoOf(t)?.exists === false ? 'That measurement has been removed from the job, so it can’t be opened.' : 'That can’t be opened from here.', { warn: true }); return; }
|
|
164
|
-
openLink(t);
|
|
302
|
+
if (t) openLinkTarget(t, how);
|
|
165
303
|
}
|
|
166
304
|
|
|
167
305
|
/**
|
package/src/editor/links.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { linkInfo, linkKey, linkTargets } from '../model/links';
|
|
1
|
+
import { linkInfo, linkInfoOf, linkKey, linkTargets } from '../model/links';
|
|
2
2
|
import { onSessionReset } from '../model/session';
|
|
3
3
|
import { store } from '../model/store';
|
|
4
4
|
import type { LinkInfo, LinkTarget } from '../model/types';
|
|
@@ -67,6 +67,13 @@ export interface CadLinks {
|
|
|
67
67
|
subscribe(targets: LinkTarget[], cb: (info: Record<string, LinkInfo>) => void): () => void;
|
|
68
68
|
/** Open what `target` is for (another screen, or a sheet over the drawing). */
|
|
69
69
|
open(target: LinkTarget): void;
|
|
70
|
+
/**
|
|
71
|
+
* Open what `target` is for in a pane beside the drawing (a split view), the drawing staying as it is. With this, the
|
|
72
|
+
* bar's Open asks how — Open, or Open side by side — until a pane is open (besideOpen), when Open goes to it.
|
|
73
|
+
*/
|
|
74
|
+
openBeside?(target: LinkTarget): void;
|
|
75
|
+
/** Whether a pane is open beside the drawing now (with openBeside). */
|
|
76
|
+
besideOpen?(): boolean;
|
|
70
77
|
/** Change a linked measurement (micrometres, or degrees), the person having said to; `capture` as saveMeasurement's. */
|
|
71
78
|
update?(target: LinkTarget, value: number, capture?: unknown): void;
|
|
72
79
|
/** Save a dimension as a new measurement in the drawing's own group; resolves with it (null: not saved). */
|
|
@@ -179,3 +186,12 @@ export async function pickJobMeasurement(kind: MeasurementKind): Promise<LinkTar
|
|
|
179
186
|
|
|
180
187
|
/** Open what a link is for. */
|
|
181
188
|
export function openLink(t: LinkTarget): void { host?.open(t); }
|
|
189
|
+
|
|
190
|
+
/** Open what a link is for in a pane beside the drawing (CadLinks.openBeside). */
|
|
191
|
+
export function openLinkBeside(t: LinkTarget): void { host?.openBeside?.(t); }
|
|
192
|
+
|
|
193
|
+
/** Whether the host can show `t` in a pane beside the drawing. */
|
|
194
|
+
export function canOpenBeside(t: LinkTarget): boolean { return !!host?.openBeside && linkInfoOf(t)?.canOpenBeside !== false; }
|
|
195
|
+
|
|
196
|
+
/** Whether a pane is open beside the drawing now. */
|
|
197
|
+
export function besideOpen(): boolean { return !!host?.besideOpen?.(); }
|
package/src/editor/mode.ts
CHANGED
|
@@ -58,12 +58,14 @@ 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);
|
|
62
|
-
|
|
61
|
+
// drawing, and Trim last of it (the user's call, 2026-11-03); then the building (walls, what goes in them, spaces);
|
|
62
|
+
// then guides
|
|
63
|
+
['select', 'Select', 'V'], ['line', 'Line', 'L'], ['freehand', 'Draw to shape', 'S'], ['rect', 'Rectangle', 'R'], ['circle', 'Circle', 'C'], ['arc', 'Arc', 'A'], ['trim', 'Trim', 'T'], '|',
|
|
63
64
|
['door', 'Door', 'B'], ['window', 'Window', 'N'], ['room', 'Space', 'M'], '|',
|
|
64
|
-
//
|
|
65
|
+
// Offset (O) and Dimension (D) stay on the keyboard; Offset is also in the bar for a selected line, and measuring is
|
|
66
|
+
// the yellow + for what's selected (the user's call, 2026-10-24: no Dimension on the rail).
|
|
65
67
|
// The toggles together: what's drawn is walls, or guides (one or the other, never both)
|
|
66
|
-
['
|
|
68
|
+
['wall', 'Walls', 'W', 'toggle'], ['constr', 'Construction lines', 'X', 'toggle'], '|',
|
|
67
69
|
['undo', 'Undo', '⌘Z', 'act'], ['redo', 'Redo', '⇧⌘Z', 'act'],
|
|
68
70
|
],
|
|
69
71
|
annotate: [
|
|
@@ -78,7 +80,8 @@ export const RAIL: Record<Mode, RailItem[]> = {
|
|
|
78
80
|
/**
|
|
79
81
|
* Tools that share one place on a phone's rail, which has no room for each: a tap draws with the one used last (the
|
|
80
82
|
* place shows it), and a long press opens a menu of them all (react/ShapeMenu.tsx) — Draft's Line, Rectangle, Circle
|
|
81
|
-
* and Arc, and Annotate's markup shapes. A tablet's or a browser's rail shows every one in its own place.
|
|
83
|
+
* and Arc, and Annotate's markup shapes. A tablet's or a browser's rail shows every one in its own place. (Trim isn't
|
|
84
|
+
* one of them: on a phone it has its own place, under Draw to shape — the user's call, 2026-11-03.)
|
|
82
85
|
*/
|
|
83
86
|
export const SHAPE_TOOLS = ['line', 'rect', 'circle', 'arc'];
|
|
84
87
|
export const SHAPES_GROUP = 'shapes';
|
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]);
|