@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/render/snap.ts
CHANGED
|
@@ -2,18 +2,57 @@ import { ed, type Guide, type Snap } from '../app/state';
|
|
|
2
2
|
import { distSeg, hyp, norm2, projT, segInter } from '../model/geom';
|
|
3
3
|
import { settings } from '../model/settings';
|
|
4
4
|
import { SK } from '../model/store';
|
|
5
|
-
import { gridSteps } from '../model/planGeom';
|
|
5
|
+
import { edgeId, gridSteps } from '../model/planGeom';
|
|
6
|
+
import { faceCorner, wallFace } from '../model/wallfaces';
|
|
7
|
+
import type { Sketch, XY } from '../model/types';
|
|
6
8
|
import { mainT, S, s2w } from './view';
|
|
7
9
|
import { intoWall } from '../editor/elevation';
|
|
10
|
+
import { drawsWalls } from '../editor/mode';
|
|
8
11
|
|
|
9
12
|
export interface SnapOpts {
|
|
10
13
|
touch?: boolean;
|
|
11
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* point ids to ignore (e.g. the point being dragged) — and what moves with them: the lines they're an end of (their
|
|
16
|
+
* middles, where they cross others, a point on them) and the circles they're the centre of. Snapping to those would
|
|
17
|
+
* only hold a dragged point to its own line.
|
|
18
|
+
*/
|
|
12
19
|
exclude?: Set<string>;
|
|
13
20
|
excludeLines?: Set<string>;
|
|
14
21
|
/** start of the segment being drawn (enables H/V/perpendicular inference) */
|
|
15
22
|
from?: string | null;
|
|
16
23
|
prevLine?: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Onto walls' faces beside the pointer, and the corners where two meet. On unless walls are being drawn: walls join
|
|
26
|
+
* along their centrelines (and a wall's own corner, dragged, says so).
|
|
27
|
+
*/
|
|
28
|
+
faces?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Where two of `walls`' faces meet — an inside corner, or an outside one a face wraps — and the two edges there. */
|
|
32
|
+
function faceCorners(sk: Sketch, walls: Set<string>): { x: number; y: number; refs: [string, string] }[] {
|
|
33
|
+
const out: { x: number; y: number; refs: [string, string] }[] = [];
|
|
34
|
+
for (const id of walls) {
|
|
35
|
+
for (const side of [1, -1] as const) {
|
|
36
|
+
for (const end of ['a', 'b'] as const) {
|
|
37
|
+
const p = faceCorner(sk, id, side, end);
|
|
38
|
+
if (!p) continue;
|
|
39
|
+
let other: string | null = null;
|
|
40
|
+
for (const w of walls) if (w !== id && (other = faceThrough(sk, w, p))) break;
|
|
41
|
+
if (other) out.push({ x: p.x, y: p.y, refs: [edgeId(id, side), other] });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The face of straight wall `id` that point `p` is on, between that face's corners (its edge id), if it's on one. */
|
|
49
|
+
function faceThrough(sk: Sketch, id: string, p: XY): string | null {
|
|
50
|
+
const l = sk.lines[id], A = sk.points[l.a], B = sk.points[l.b], L = hyp(B.x - A.x, B.y - A.y);
|
|
51
|
+
if (L < 1e-9) return null;
|
|
52
|
+
const k = ((B.x - A.x) * (p.y - A.y) - (B.y - A.y) * (p.x - A.x)) / L, side = k > 0 ? 1 : -1;
|
|
53
|
+
if (Math.abs(Math.abs(k) - l.thick / 2) > 1e-6) return null;
|
|
54
|
+
const f = wallFace(sk, id, side), s = ((p.x - A.x) * (B.x - A.x) + (p.y - A.y) * (B.y - A.y)) / L;
|
|
55
|
+
return f && s >= f.s0 - 1e-6 && s <= f.s1 + 1e-6 ? edgeId(id, side) : null;
|
|
17
56
|
}
|
|
18
57
|
|
|
19
58
|
/** Snap a screen point: endpoints > midpoints/intersections > on line > H/V/perp guides > on circle > grid. In the wall view, never past the wall. */
|
|
@@ -93,20 +132,36 @@ function snapRaw(sx: number, sy: number, o: SnapOpts): Snap {
|
|
|
93
132
|
const p = sk.points[ci.c];
|
|
94
133
|
cand(p.x, p.y, 'point', ci.c, 0, 'Center');
|
|
95
134
|
}
|
|
135
|
+
// straight walls near the pointer, for their faces: a face corner is a corner as an endpoint is (the nearer wins).
|
|
136
|
+
// Beside one (not over it), what's inside it — its centreline's middle, where something crosses its centreline — isn't
|
|
137
|
+
// snapped to: that's what landed lines half a wall's thickness inside it
|
|
138
|
+
const walls = new Set<string>(), beside = new Set<string>();
|
|
139
|
+
if (o.faces ?? !drawsWalls()) {
|
|
140
|
+
for (const id in sk.lines) {
|
|
141
|
+
const l = sk.lines[id];
|
|
142
|
+
if (l.kind !== 'wall' || l.constr || !(l.thick > 0) || ex.has(l.a) || ex.has(l.b) || o.excludeLines?.has(id)) continue;
|
|
143
|
+
const A = sk.points[l.a], B = sk.points[l.b], [ax, ay] = S(T, A.x, A.y), [bx, by] = S(T, B.x, B.y);
|
|
144
|
+
if (distSeg({ x: sx, y: sy }, { x: ax, y: ay }, { x: bx, y: by }) >= tol * 3 + (l.thick / 2) * T.s) continue;
|
|
145
|
+
walls.add(id);
|
|
146
|
+
const L = hyp(B.x - A.x, B.y - A.y);
|
|
147
|
+
if (L > 1e-9 && Math.abs((B.x - A.x) * (wy - A.y) - (B.y - A.y) * (wx - A.x)) / L > l.thick / 2) beside.add(id);
|
|
148
|
+
}
|
|
149
|
+
for (const c of faceCorners(sk, walls)) cand(c.x, c.y, 'facecorner', c.refs, 0, 'Face corner');
|
|
150
|
+
}
|
|
96
151
|
const near: string[] = [];
|
|
97
152
|
for (const id in sk.lines) {
|
|
98
153
|
if (o.excludeLines && o.excludeLines.has(id)) continue;
|
|
99
154
|
const l = sk.lines[id];
|
|
100
|
-
if (ex.has(l.a)
|
|
155
|
+
if (ex.has(l.a) || ex.has(l.b)) continue;
|
|
101
156
|
const A = sk.points[l.a], B = sk.points[l.b];
|
|
102
|
-
cand((A.x + B.x) / 2, (A.y + B.y) / 2, 'mid', id, 1, 'Midpoint');
|
|
157
|
+
if (!beside.has(id)) cand((A.x + B.x) / 2, (A.y + B.y) / 2, 'mid', id, 1, 'Midpoint');
|
|
103
158
|
const [ax, ay] = S(T, A.x, A.y), [bx, by] = S(T, B.x, B.y);
|
|
104
|
-
if (distSeg({ x: sx, y: sy }, { x: ax, y: ay }, { x: bx, y: by }) < tol * 3) near.push(id);
|
|
159
|
+
if (walls.has(id) || distSeg({ x: sx, y: sy }, { x: ax, y: ay }, { x: bx, y: by }) < tol * 3) near.push(id);
|
|
105
160
|
}
|
|
106
161
|
for (let i = 0; i < near.length; i++) {
|
|
107
162
|
for (let j = i + 1; j < near.length; j++) {
|
|
108
163
|
const l1 = sk.lines[near[i]], l2 = sk.lines[near[j]];
|
|
109
|
-
if (l1.a === l2.a || l1.a === l2.b || l1.b === l2.a || l1.b === l2.b) continue;
|
|
164
|
+
if (l1.a === l2.a || l1.a === l2.b || l1.b === l2.a || l1.b === l2.b || beside.has(near[i]) || beside.has(near[j])) continue;
|
|
110
165
|
const r = segInter(sk.points[l1.a], sk.points[l1.b], sk.points[l2.a], sk.points[l2.b]);
|
|
111
166
|
if (r) cand(r.x, r.y, 'inter', [near[i], near[j]], 1, 'Intersection');
|
|
112
167
|
}
|
|
@@ -117,15 +172,27 @@ function snapRaw(sx: number, sy: number, o: SnapOpts): Snap {
|
|
|
117
172
|
}
|
|
118
173
|
// on a line, near the pointer (checked before the alignment guides: a line under the pointer wins over lining up with
|
|
119
174
|
// a point elsewhere — where both apply, the snap is where the guide crosses the line)
|
|
120
|
-
|
|
175
|
+
// (a wall with the pointer beside it, not over it: its face on that side, between the face's corners — t0 to t1 of
|
|
176
|
+
// the face's own A → B, which is the centreline's moved over)
|
|
177
|
+
let bestL: { d: number; x: number; y: number; id: string; A: XY; B: XY; t0: number; t1: number; face?: 1 | -1 } | null = null;
|
|
121
178
|
for (const id of near) {
|
|
122
|
-
const l = sk.lines[id]
|
|
179
|
+
const l = sk.lines[id];
|
|
180
|
+
let A: XY = sk.points[l.a], B: XY = sk.points[l.b], t0 = 0, t1 = 1, face: 1 | -1 | undefined;
|
|
181
|
+
if (walls.has(id)) {
|
|
182
|
+
const L = hyp(B.x - A.x, B.y - A.y), k = ((B.x - A.x) * (wy - A.y) - (B.y - A.y) * (wx - A.x)) / L, side = k > 0 ? 1 : -1;
|
|
183
|
+
const f = Math.abs(k) > l.thick / 2 ? wallFace(sk, id, side) : null;
|
|
184
|
+
if (f) {
|
|
185
|
+
const nx = (-(B.y - A.y) / L) * side * (l.thick / 2), ny = ((B.x - A.x) / L) * side * (l.thick / 2);
|
|
186
|
+
A = { x: A.x + nx, y: A.y + ny }; B = { x: B.x + nx, y: B.y + ny };
|
|
187
|
+
t0 = f.s0 / L; t1 = f.s1 / L; face = side;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
123
190
|
const t = projT(A, B, { x: wx, y: wy });
|
|
124
|
-
if (t <
|
|
191
|
+
if (t < t0 || t > t1) continue;
|
|
125
192
|
const fx = A.x + (B.x - A.x) * t, fy = A.y + (B.y - A.y) * t;
|
|
126
193
|
const [qx, qy] = S(T, fx, fy);
|
|
127
194
|
const d = hyp(qx - sx, qy - sy);
|
|
128
|
-
if (d < tol && (!bestL || d < bestL.d)) bestL = { d, x: fx, y: fy, id };
|
|
195
|
+
if (d < tol && (!bestL || d < bestL.d)) bestL = { d, x: fx, y: fy, id, A, B, t0, t1, face };
|
|
129
196
|
}
|
|
130
197
|
let x = wx, y = wy, kind: Snap['kind'] = 'free';
|
|
131
198
|
const guides: Guide[] = [];
|
|
@@ -142,31 +209,33 @@ function snapRaw(sx: number, sy: number, o: SnapOpts): Snap {
|
|
|
142
209
|
}
|
|
143
210
|
if (bestL) {
|
|
144
211
|
// a line under the pointer: on it — where a guide crosses it, if one does (level with the start, and on the line)
|
|
145
|
-
const
|
|
212
|
+
const on = bestL, { A, B, t0, t1 } = on, dx = B.x - A.x, dy = B.y - A.y;
|
|
213
|
+
const onIt = (x: number, y: number, guides: Guide[]): Snap =>
|
|
214
|
+
finish(on.face ? { x, y, kind: 'onface', ref: edgeId(on.id, on.face), label: 'On face', guides } : { x, y, kind: 'online', ref: on.id, label: 'On line', guides });
|
|
146
215
|
const g = gy && Math.abs(dy) > 1e-9 ? { t: (gy.y - A.y) / dy, h: gy } : gx && Math.abs(dx) > 1e-9 ? { t: (gx.x - A.x) / dx, v: gx } : null;
|
|
147
|
-
if (g && g.t >=
|
|
216
|
+
if (g && g.t >= t0 && g.t <= t1) {
|
|
148
217
|
const ix = A.x + dx * g.t, iy = A.y + dy * g.t, [qx, qy] = S(T, ix, iy);
|
|
149
218
|
if (hyp(qx - sx, qy - sy) < tol * 1.5) {
|
|
150
219
|
const gd = 'h' in g ? g.h! : g.v!, H = 'h' in g;
|
|
151
|
-
return
|
|
220
|
+
return onIt(ix, iy, [H ? { x1: ix - 1, y1: iy, x2: ix + 1, y2: iy, kind: 'H', from: gd.from, label: gd.own ? 'Horizontal' : 'Aligned' } : { x1: ix, y1: iy - 1, x2: ix, y2: iy + 1, kind: 'V', from: gd.from, label: gd.own ? 'Vertical' : 'Aligned' }]);
|
|
152
221
|
}
|
|
153
222
|
}
|
|
154
223
|
// on the grid, as far as the line allows: where it crosses the grid line nearest (along a level or plumb line,
|
|
155
224
|
// the grid point on it)
|
|
156
225
|
if (st) {
|
|
157
|
-
const
|
|
226
|
+
const near2 = (c: { x: number; y: number }) => hyp(c.x - on.x, c.y - on.y);
|
|
158
227
|
const opts: { x: number; y: number; t: number; axis: 'x' | 'y' }[] = [];
|
|
159
228
|
if (Math.abs(dx) > 1e-9) { const gv = onGrid(on.x, st, was?.x), t = (gv - A.x) / dx; opts.push({ x: gv, y: A.y + dy * t, t, axis: 'x' }); }
|
|
160
229
|
if (Math.abs(dy) > 1e-9) { const gv = onGrid(on.y, st, was?.y), t = (gv - A.y) / dy; opts.push({ x: A.x + dx * t, y: gv, t, axis: 'y' }); }
|
|
161
230
|
const pick = opts
|
|
162
|
-
.filter(c => { const [qx, qy] = S(T, c.x, c.y); return c.t >=
|
|
231
|
+
.filter(c => { const [qx, qy] = S(T, c.x, c.y); return c.t >= t0 && c.t <= t1 && hyp(qx - sx, qy - sy) < tol * 1.5; })
|
|
163
232
|
.sort((a, b) => near2(a) - near2(b))[0];
|
|
164
233
|
if (pick) {
|
|
165
234
|
if (pick.axis === 'x') usedX = pick.x; else usedY = pick.y;
|
|
166
|
-
return
|
|
235
|
+
return onIt(pick.x, pick.y, []);
|
|
167
236
|
}
|
|
168
237
|
}
|
|
169
|
-
return
|
|
238
|
+
return onIt(on.x, on.y, []);
|
|
170
239
|
}
|
|
171
240
|
if (gy) { y = gy.y; guides.push({ x1: wx - 1, y1: gy.y, x2: wx + 1, y2: gy.y, kind: 'H', from: gy.from, label: gy.own ? 'Horizontal' : 'Aligned' }); }
|
|
172
241
|
if (gx) { x = gx.x; guides.push({ x1: gx.x, y1: wy - 1, x2: gx.x, y2: wy + 1, kind: 'V', from: gx.from, label: gx.own ? 'Vertical' : 'Aligned' }); }
|
|
@@ -186,6 +255,7 @@ function snapRaw(sx: number, sy: number, o: SnapOpts): Snap {
|
|
|
186
255
|
if (!guides.length) {
|
|
187
256
|
for (const id in sk.circles) {
|
|
188
257
|
const ci = sk.circles[id], o2 = sk.points[ci.c];
|
|
258
|
+
if (ex.has(ci.c)) continue;
|
|
189
259
|
const d = hyp(wx - o2.x, wy - o2.y);
|
|
190
260
|
if (Math.abs(d - ci.r) * T.s < tol && d > 1e-6) {
|
|
191
261
|
return finish({ x: o2.x + ((wx - o2.x) / d) * ci.r, y: o2.y + ((wy - o2.y) / d) * ci.r, kind: 'oncircle', ref: id, label: 'On circle', guides: [] });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { DrawCtx } from './ctx';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
An icon kept exactly as Figma exports it (render/figmaIcons.ts, render/tileIcons.ts), drawn on the canvas, where there's
|
|
5
|
+
no SVG: its paths' `d` replayed as canvas calls, each filled in its own colour, and stroked over its fill where it has a
|
|
6
|
+
stroke too (the CAD badge). Figma writes absolute M, L, H, V, C and Z, and that's all this reads: any other command, or
|
|
7
|
+
a path with no fill, throws, so an icon exported some other way fails rather than drawing wrong — when the renderer
|
|
8
|
+
loads (it reads them then), and in tests/unit/linked-chip.test.ts and links-badges.test.ts. Not read: a <rect> (an
|
|
9
|
+
icon's tile behind its glyph: the caller draws that) and a fill rule (none of these icons has one).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type PathOp = ['M' | 'L', number, number] | ['C', number, number, number, number, number, number] | ['Z'];
|
|
13
|
+
|
|
14
|
+
export interface SvgIcon {
|
|
15
|
+
/** the viewBox: its origin and size */
|
|
16
|
+
x: number; y: number; w: number; h: number;
|
|
17
|
+
paths: { ops: PathOp[]; fill: string; stroke?: { color: string; width: number } }[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TOKEN = /[A-Za-z]|-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
|
|
21
|
+
const COMMANDS = new Set(['M', 'L', 'H', 'V', 'C', 'Z']);
|
|
22
|
+
|
|
23
|
+
/** A path's `d` as moves, lines and cubic curves (H and V become lines). */
|
|
24
|
+
export function parsePath(d: string): PathOp[] {
|
|
25
|
+
const tk = d.match(TOKEN) ?? [], out: PathOp[] = [];
|
|
26
|
+
let i = 0, x = 0, y = 0, sx = 0, sy = 0;
|
|
27
|
+
const num = (): number => {
|
|
28
|
+
const n = Number(tk[i++]);
|
|
29
|
+
if (!Number.isFinite(n)) throw new Error('svgPath: a number expected in "' + d + '"');
|
|
30
|
+
return n;
|
|
31
|
+
};
|
|
32
|
+
while (i < tk.length) {
|
|
33
|
+
const cmd = tk[i++];
|
|
34
|
+
if (!COMMANDS.has(cmd)) throw new Error('svgPath: unsupported command ' + cmd);
|
|
35
|
+
if (cmd === 'Z') { out.push(['Z']); x = sx; y = sy; continue; }
|
|
36
|
+
// (a command's numbers can repeat it: pairs after M's first are lines)
|
|
37
|
+
let first = true;
|
|
38
|
+
do {
|
|
39
|
+
if (cmd === 'M' || cmd === 'L') {
|
|
40
|
+
x = num(); y = num();
|
|
41
|
+
if (cmd === 'M' && first) { out.push(['M', x, y]); sx = x; sy = y; } else out.push(['L', x, y]);
|
|
42
|
+
} else if (cmd === 'H') { x = num(); out.push(['L', x, y]); }
|
|
43
|
+
else if (cmd === 'V') { y = num(); out.push(['L', x, y]); }
|
|
44
|
+
else {
|
|
45
|
+
const a = num(), b = num(), c = num(), e = num();
|
|
46
|
+
x = num(); y = num();
|
|
47
|
+
out.push(['C', a, b, c, e, x, y]);
|
|
48
|
+
}
|
|
49
|
+
first = false;
|
|
50
|
+
} while (i < tk.length && !/^[A-Za-z]$/.test(tk[i]));
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Trace parsed path ops on `c` (the caller begins the path, then fills it). */
|
|
56
|
+
export function tracePath(c: DrawCtx, ops: PathOp[]): void {
|
|
57
|
+
for (const o of ops) {
|
|
58
|
+
if (o[0] === 'M') c.moveTo(o[1], o[2]);
|
|
59
|
+
else if (o[0] === 'L') c.lineTo(o[1], o[2]);
|
|
60
|
+
else if (o[0] === 'C') c.bezierCurveTo(o[1], o[2], o[3], o[4], o[5], o[6]);
|
|
61
|
+
else c.closePath();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** An exported SVG, read once: its viewBox and its filled paths (and their strokes), in order. */
|
|
66
|
+
export function svgIcon(svg: string): SvgIcon {
|
|
67
|
+
const vb = /<svg\b[^>]*\sviewBox="([^"]*)"/.exec(svg)?.[1].trim().split(/[\s,]+/).map(Number);
|
|
68
|
+
if (!vb || vb.length !== 4 || vb.some(n => !Number.isFinite(n))) throw new Error('svgPath: an SVG without a viewBox');
|
|
69
|
+
const paths = [...svg.matchAll(/<path\b([^>]*)>/g)].map(([, attrs]) => {
|
|
70
|
+
const d = /\sd="([^"]*)"/.exec(attrs)?.[1], fill = /\sfill="([^"]*)"/.exec(attrs)?.[1];
|
|
71
|
+
if (!d || !fill || fill === 'none') throw new Error('svgPath: only filled paths are drawn');
|
|
72
|
+
const stroke = /\sstroke="([^"]*)"/.exec(attrs)?.[1], width = Number(/\sstroke-width="([^"]*)"/.exec(attrs)?.[1] ?? 1);
|
|
73
|
+
return { ops: parsePath(d), fill, ...(stroke && stroke !== 'none' ? { stroke: { color: stroke, width } } : {}) };
|
|
74
|
+
});
|
|
75
|
+
return { x: vb[0], y: vb[1], w: vb[2], h: vb[3], paths };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* An icon `size` across (its longer side), its top left at x, y: each path filled (and stroked, where it is) in its own
|
|
80
|
+
* colour, or all in `color`. `key` names it where the engine keeps shapes (each path is described once, then moved into
|
|
81
|
+
* place): one key to an icon.
|
|
82
|
+
*/
|
|
83
|
+
export function drawSvgIcon(c: DrawCtx, icon: SvgIcon, key: string, x: number, y: number, size: number, color?: string): void {
|
|
84
|
+
const u = size / Math.max(icon.w, icon.h);
|
|
85
|
+
c.save();
|
|
86
|
+
c.translate(x, y); c.scale(u, u); c.translate(-icon.x, -icon.y);
|
|
87
|
+
icon.paths.forEach(({ ops, fill, stroke }, i) => {
|
|
88
|
+
if (c.shape) c.shape('svg ' + key + ' ' + i, 0, 0, cc => tracePath(cc, ops));
|
|
89
|
+
else { c.beginPath(); tracePath(c, ops); }
|
|
90
|
+
c.fillStyle = color ?? fill; c.fill();
|
|
91
|
+
// (its width in the icon's own units: the context is scaled to it; SVG's own joins and caps)
|
|
92
|
+
if (stroke) { c.lineWidth = stroke.width; c.lineJoin = 'miter'; c.lineCap = 'butt'; c.strokeStyle = color ?? stroke.color; c.stroke(); }
|
|
93
|
+
});
|
|
94
|
+
c.restore();
|
|
95
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The object tiles' icons, exactly as Figma exports them (render/svgPath.ts draws them from their own paths): the
|
|
3
|
+
badge glyphs of "Object Tile Icons" (Figma 704:23628, used by the tiles 657:8732) and, for kinds with no badge there,
|
|
4
|
+
the add-object sheet's own (1059:79267). Each badge is a coloured square with one of these in white; the hues are the
|
|
5
|
+
sheet's (the app's ActionBottomSheet ACTION map), so an object type's colour is the same everywhere.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** File (a PDF or any other document): 2803:81886. */
|
|
9
|
+
export const BADGE_FILE_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="10" height="12.7932" viewBox="0 0 10 12.7932" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
10
|
+
<path id="Vector" d="M9.58955 2.56397L8.15032 1.12473C7.6178 0.676972 7.3049 0 6.43923 0H1.40192C0.628998 0 0 0.628998 0 1.40192V11.3913C0 12.1642 0.628998 12.7932 1.40192 12.7932H8.59808C9.371 12.7932 10 12.1642 10 11.3913V3.56077C10 3.18763 9.85075 2.82516 9.58955 2.56397ZM4.06183 3.37953H5.93817C6.23134 3.37953 6.47122 3.6194 6.47122 3.91258C6.47122 4.21109 6.23134 4.44563 5.93817 4.44563H4.06183C3.76866 4.44563 3.52878 4.21109 3.52878 3.91258C3.52878 3.6194 3.76866 3.37953 4.06183 3.37953ZM7.60128 9.41365H2.39872C2.10554 9.41365 1.86567 9.17377 1.86567 8.8806C1.86567 8.58209 2.10554 8.34755 2.39872 8.34755H7.60128C7.89446 8.34755 8.13433 8.58209 8.13433 8.8806C8.13433 9.17377 7.89446 9.41365 7.60128 9.41365ZM7.60128 6.92964H2.39872C2.10554 6.92964 1.86567 6.68977 1.86567 6.39659C1.86567 6.10341 2.10554 5.86354 2.39872 5.86354H7.60128C7.89446 5.86354 8.13433 6.10341 8.13433 6.39659C8.13433 6.68977 7.89446 6.92964 7.60128 6.92964Z" fill="white"/>
|
|
11
|
+
</svg>`;
|
|
12
|
+
|
|
13
|
+
/** Construction calculator: 2803:81320. */
|
|
14
|
+
export const BADGE_CON_CALC_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14.1429" height="11" viewBox="0 0 14.1429 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
15
|
+
<path id="Vector" d="M0 8.80008C0.529231 8.80008 1.05846 8.80008 1.58769 8.80008C1.98819 8.80008 2.39346 8.80008 2.79396 8.80008C2.97991 8.80008 2.97991 8.80008 2.97991 8.62145C2.97991 7.74242 2.97991 6.8681 2.97991 5.98907C2.97991 5.89505 3.00851 5.88095 3.09433 5.88095C4.01453 5.88095 4.92996 5.88095 5.85015 5.88095C5.9312 5.88095 5.95981 5.85275 5.95981 5.77754C5.95981 5.68352 5.95981 5.58951 5.95981 5.49549C5.95981 4.66817 5.95981 3.84555 5.95981 3.01823C5.95981 2.94772 5.97888 2.93362 6.0504 2.93362C6.97536 2.93362 7.90033 2.93362 8.82529 2.93362C8.90634 2.93362 8.93018 2.91012 8.93018 2.83021C8.93018 1.91827 8.93018 1.01104 8.93018 0.0991062C8.93018 0.0238952 8.94448 0.000391724 9.02554 0.000391724C10.6895 0.000391724 12.3535 0.000391724 14.0175 0.000391724C14.1653 0.000391724 14.1414 -0.0137103 14.1414 0.12731C14.1414 0.790107 14.1414 1.4576 14.1414 2.1204C14.1414 2.19091 14.1128 2.20031 14.0556 2.20031C13.1497 2.20031 12.2486 2.20031 11.3427 2.20031C11.2251 2.20031 11.1663 2.25986 11.1663 2.37894C11.1663 3.25797 11.1663 4.1417 11.1663 5.02073C11.1663 5.10534 11.1425 5.11944 11.0614 5.11944C10.1412 5.11944 9.21625 5.11944 8.29606 5.11944C8.20547 5.11944 8.1864 5.14764 8.1864 5.22756C8.1864 6.05958 8.1864 6.8963 8.1864 7.72832C8.1864 7.78003 8.1864 7.83174 8.1864 7.88344C8.18957 7.99939 8.13395 8.05737 8.01952 8.05737C7.1184 8.05737 6.21727 8.05737 5.31615 8.05737C5.2351 8.05737 5.21126 8.08087 5.21126 8.16078C5.21126 9.04921 5.21126 9.93764 5.21126 10.8261C5.21126 10.8825 5.22079 10.9436 5.19695 11C3.49007 11 1.78318 11 0.0810534 11C0.0238392 11 0 10.9953 0 10.9248C0 10.215 0 9.50518 0 8.79068L0 8.80008Z" fill="white"/>
|
|
16
|
+
</svg>`;
|
|
17
|
+
|
|
18
|
+
/** Conversion: 704:23616. */
|
|
19
|
+
export const BADGE_CONVERSION_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12" height="10.5869" viewBox="0 0 12 10.5869" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
20
|
+
<g id="Group">
|
|
21
|
+
<path id="Vector" d="M11.1895 4.50438H0.336525C0.151436 4.50438 0 4.34136 0 4.14212V2.78363C0 2.58439 0.151436 2.42137 0.336525 2.42137H7.3497L7.34299 0.363716C7.34299 0.0413019 7.70307 -0.121716 7.91509 0.10651L11.425 3.88491C11.6371 4.11314 11.4856 4.50076 11.1895 4.50076V4.50438Z" fill="white"/>
|
|
22
|
+
<path id="Vector_2" d="M0.810528 6.08256H11.6635C11.8486 6.08256 12 6.24558 12 6.44483V7.80331C12 8.00256 11.8486 8.16557 11.6635 8.16557H4.65029L4.65701 10.2232C4.65701 10.5456 4.29695 10.7087 4.08493 10.4804L0.574976 6.70203C0.362965 6.47381 0.514385 6.08619 0.810528 6.08619V6.08256Z" fill="white"/>
|
|
23
|
+
</g>
|
|
24
|
+
</svg>`;
|
|
25
|
+
|
|
26
|
+
/** Note: 2803:80267. */
|
|
27
|
+
export const BADGE_NOTE_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
28
|
+
<g id="Group">
|
|
29
|
+
<path id="Vector" d="M9.28791 4.67696C8.86519 4.67696 8.52701 4.31881 8.52701 3.8806V0H5.18546C4.64196 0 4.20312 0.459245 4.20312 1.02807V2.65871L7.7661 6.38763L7.9835 6.61515L7.99961 6.9396L8.0761 8.46488C8.0922 8.73876 7.99558 9.01264 7.81441 9.21489C7.63324 9.41714 7.37558 9.5309 7.1139 9.5309H7.08571H7.06558L5.60818 9.45085L5.29819 9.43398L5.08078 9.20646L4.20312 8.28792V10.9719C4.20312 11.5407 4.64196 12 5.18546 12H12.0175C12.561 12 12.9998 11.5407 12.9998 10.9719V4.68118H9.29194L9.28791 4.67696Z" fill="white"/>
|
|
30
|
+
<path id="Vector_2" d="M10.0246 3.59367H12.8468L9.55762 0.151263V3.10492C9.55762 3.37458 9.76697 3.59367 10.0246 3.59367Z" fill="white"/>
|
|
31
|
+
<path id="Vector_3" d="M1.36816 4.12827L4.19841 7.09034V3.84595L2.91816 2.50607L1.3883 4.10721L1.36816 4.12827Z" fill="white"/>
|
|
32
|
+
<path id="Vector_4" d="M5.64405 8.60391L7.10145 8.68396C7.10145 8.68396 7.10548 8.68396 7.1095 8.68396C7.2021 8.68396 7.27457 8.60389 7.27054 8.50698L7.19405 6.9817L4.19873 3.85108V7.09547L5.64405 8.60813V8.60391Z" fill="white"/>
|
|
33
|
+
<path id="Vector_5" d="M1.48558 1.04862C1.44934 1.04862 1.41311 1.06547 1.3809 1.09918L0.0442827 2.47698C-2.8256e-06 2.52332 0 2.56547 0 2.59075C0 2.61603 -2.8256e-06 2.65817 0.0442827 2.70452L0.797139 3.49244L2.327 1.89132L1.57415 1.1034C1.55402 1.06969 1.52181 1.05284 1.48558 1.05284V1.04862Z" fill="white"/>
|
|
34
|
+
<path id="Vector_6" d="M0.796875 3.48782L0.82103 3.5131L2.3509 1.91198L2.32674 1.8867L0.796875 3.48782Z" fill="white"/>
|
|
35
|
+
</g>
|
|
36
|
+
</svg>`;
|
|
37
|
+
|
|
38
|
+
/** CAD drawing: 2803:81241 (filled, and stroked over its fill). */
|
|
39
|
+
export const BADGE_CAD_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="13.4887" height="14.4887" viewBox="0 0 13.4887 14.4887" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
+
<path id="Vector" d="M11.6357 2.49703L8.21928 0.62548C7.76732 0.376265 7.25704 0.244328 6.7419 0.244328C6.22676 0.244328 5.71648 0.376265 5.26452 0.62548L1.85293 2.49703C0.856667 3.03944 0.244328 4.08517 0.244328 5.22373V9.26981C0.244328 10.4035 0.856667 11.4492 1.85293 11.9916L5.26452 13.8681C5.71648 14.1124 6.22676 14.2443 6.7419 14.2443C7.25704 14.2443 7.76732 14.1124 8.21928 13.8681L11.6357 11.9916C12.6271 11.4492 13.2443 10.4035 13.2443 9.26981V5.22373C13.2443 4.08517 12.6271 3.03944 11.6357 2.49703ZM6.2948 13.4136L2.28059 11.2049C1.57106 10.8188 1.13367 10.0761 1.13367 9.26981V4.28551L6.2948 7.48621V13.4136ZM1.71685 3.59162L5.69218 1.41222C6.01293 1.2363 6.37741 1.14346 6.7419 1.14346C7.10639 1.14346 7.46601 1.2363 7.78676 1.41222L11.7621 3.59162L6.7419 6.70925L1.71685 3.59162ZM12.3501 9.26981C12.3501 10.0761 11.9127 10.8188 11.2081 11.2049L7.189 13.4136V7.48621L12.3501 4.28551V9.26981Z" fill="white" stroke-width="0.488656" stroke-miterlimit="10" stroke="white"/>
|
|
41
|
+
</svg>`;
|
|
42
|
+
|
|
43
|
+
/** Label printer: 2803:81633. */
|
|
44
|
+
export const BADGE_LABEL_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="13" height="12.5" viewBox="0 0 13 12.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
45
|
+
<g id="Group">
|
|
46
|
+
<path id="Vector" d="M10.9488 0H2.06055V2.70494H10.9488V0Z" fill="white"/>
|
|
47
|
+
<path id="Vector_2" d="M12.1295 3.67403H0.870457C0.389299 3.67403 0 4.06232 0 4.54222V9.3326C0 9.81251 0.489906 10.2008 0.971064 10.2008H2.05585V12.5H10.9441V10.2008H12.1295C12.6063 10.2008 13 9.81251 13 9.3326V4.54222C13 4.06668 12.6107 3.67403 12.1295 3.67403ZM9.58378 11.1344H3.42497V7.9234H9.58378V11.1344ZM11.4166 5.74636C11.141 5.74636 10.9179 5.52385 10.9179 5.249C10.9179 4.97414 11.1454 4.75166 11.4166 4.75166C11.6878 4.75166 11.9152 4.97414 11.9152 5.249C11.9152 5.52385 11.6921 5.74636 11.4166 5.74636Z" fill="white"/>
|
|
48
|
+
</g>
|
|
49
|
+
</svg>`;
|
|
50
|
+
|
|
51
|
+
/** The sheet's Sketch, on its 70 tile (the tile's rect is drawn apart). */
|
|
52
|
+
export const KEY_SKETCH_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53
|
+
<g id="Drawer Tiles">
|
|
54
|
+
<rect width="70" height="70" rx="10" fill="#EFB107"/>
|
|
55
|
+
<path id="Vector" d="M45.6741 49.9898L44.8257 46.0647L35.3709 49.3151C34.4203 49.6422 33.3777 49.356 32.7235 48.5894C32.0693 47.8228 31.9569 46.7393 32.4271 45.8603L35.1869 40.6576L27.0302 45.155C24.8019 46.3918 21.991 45.7581 20.4884 43.7036C18.9756 41.6286 19.3334 38.7053 21.2959 37.0494L41.5651 20L44.7031 23.7308L24.4339 40.7802L40.0114 32.4293C40.962 31.908 42.1273 32.0715 42.9041 32.8279C43.6707 33.5843 43.8547 34.7598 43.3436 35.7104L39.6332 42.6916L45.8377 40.5553C46.4918 40.3203 47.2176 40.3918 47.8308 40.7393C48.4441 41.0869 48.8734 41.6695 49.0165 42.3543L50.4475 48.9676L45.6741 50V49.9898Z" fill="white"/>
|
|
56
|
+
</g>
|
|
57
|
+
</svg>`;
|
|
58
|
+
|
|
59
|
+
/** The sheet's Camera: black lens over a white body. */
|
|
60
|
+
export const KEY_CAMERA_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="36" height="26.8881" viewBox="0 0 36 26.8881" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
61
|
+
<g id="Group">
|
|
62
|
+
<path id="Vector" d="M22.2038 14.9415C22.2038 17.2728 20.3321 19.1445 18.0008 19.1445C15.6696 19.1445 13.7866 17.2616 13.7978 14.9303C13.7978 12.6215 15.692 10.7497 18.0008 10.7497C20.3097 10.7497 22.215 12.6439 22.2038 14.9415Z" fill="black"/>
|
|
63
|
+
<path id="Vector_2" d="M33.6912 3.46344H29.3313C28.4682 3.46344 27.6949 2.98152 27.2914 2.21937L26.7758 1.24425C26.3836 0.48211 25.599 0.000183947 24.736 0.000183947H11.2304C10.3786 0.000183947 9.59402 0.482133 9.19054 1.23307L8.67497 2.23056C8.27148 2.98149 7.49813 3.45223 6.64633 3.46344H2.30884C1.03113 3.46344 0 4.50579 0 5.7835L0.0224159 24.5456C0.0224159 25.8345 1.07596 26.8881 2.36488 26.8881H33.6687C34.9577 26.8881 36 25.8345 36 24.5456V5.77228C36 4.49457 34.9689 3.46344 33.6912 3.46344ZM18 22.4385C13.8643 22.4385 10.5131 19.0874 10.5131 14.9516C10.5131 10.8159 13.8643 7.45348 18 7.45348C22.1357 7.45348 25.4869 10.8159 25.4869 14.9516C25.4869 19.0874 22.1357 22.4385 18 22.4385Z" fill="white"/>
|
|
64
|
+
<path id="Vector_3" d="M22.2038 14.9415C22.2038 17.2728 20.3321 19.1445 18.0008 19.1445C15.6696 19.1445 13.7866 17.2616 13.7978 14.9303C13.7978 12.6215 15.692 10.7497 18.0008 10.7497C20.3097 10.7497 22.215 12.6439 22.2038 14.9415Z" fill="black"/>
|
|
65
|
+
<path id="Vector_4" d="M22.2038 14.9415C22.2038 17.2728 20.3321 19.1445 18.0008 19.1445C15.6696 19.1445 13.7866 17.2616 13.7978 14.9303C13.7978 12.6215 15.692 10.7497 18.0008 10.7497C20.3097 10.7497 22.215 12.6439 22.2038 14.9415Z" fill="black"/>
|
|
66
|
+
</g>
|
|
67
|
+
</svg>`;
|
|
68
|
+
|
|
69
|
+
/** The sheet's Measurement, on its 70 tile (the tile's rect is drawn apart). */
|
|
70
|
+
export const KEY_MEASUREMENT_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
71
|
+
<g id="Drawer Tiles">
|
|
72
|
+
<rect width="70" height="70" rx="10" fill="#0066FF"/>
|
|
73
|
+
<path id="Vector" d="M51.109 29.5365L29.5248 51.1207C28.3524 52.2931 26.4414 52.2931 25.2572 51.1207L18.8793 44.7428C17.7069 43.5703 17.7069 41.6593 18.8793 40.4752L19.7938 39.5607L25.1166 44.8834L26.6407 43.3593L21.3179 38.0365L24.3545 35L26.6407 37.2862L28.1648 35.7621L25.8786 33.4759L28.9152 30.4393L34.2379 35.7621L35.7621 34.2379L30.4393 28.9152L33.4759 25.8786L35.7621 28.1648L37.2862 26.6407L35 24.3545L38.0366 21.3179L43.3593 26.6407L44.8834 25.1165L39.5607 19.7938L40.4752 18.8793C41.6476 17.7069 43.5586 17.7069 44.7428 18.8793L51.1207 25.2572C52.2931 26.4297 52.2931 28.3407 51.1207 29.5248L51.109 29.5365Z" fill="white"/>
|
|
74
|
+
</g>
|
|
75
|
+
</svg>`;
|
package/src/render/view.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { faceEdit } from '../model/faces';
|
|
|
3
3
|
import { page, SK } from '../model/store';
|
|
4
4
|
import type { Note, Sketch, Underlay, View } from '../model/types';
|
|
5
5
|
import { CLOSED_SHAPES, isSegment, NOTE_TEXT_SIZE, noteTextIn } from '../model/markup';
|
|
6
|
-
import { LINK_FULL_PX, LINK_PIN_PX, LINK_TILE_PX, linkSide } from '../model/links';
|
|
6
|
+
import { LINK_FULL_PX, LINK_PIN_PX, LINK_TILE_PX, linkInfoOf, linkSide } from '../model/links';
|
|
7
7
|
import { draw } from './draw';
|
|
8
8
|
import { onSessionReset, sessionState } from '../model/session';
|
|
9
9
|
import { refreshDrawerSoon } from '../editor/ports';
|
|
@@ -117,10 +117,13 @@ export function fitView(): void {
|
|
|
117
117
|
/** Size of the wall view's key plan (top right), pixels. */
|
|
118
118
|
export const keyPlanSize = (W: number): number => Math.min(150, Math.max(96, W * 0.16));
|
|
119
119
|
|
|
120
|
+
/** Where an image's picture is: its own, or a photo of the job's expanded from its tile, the one the host shows it as. */
|
|
121
|
+
export const underlaySrc = (u: Underlay): string => (u.link ? linkInfoOf(u.link)?.thumbUrl ?? '' : u.src);
|
|
122
|
+
|
|
120
123
|
/** Height of an image underlay in inches: its own, or from the picture's aspect ratio. */
|
|
121
124
|
export function underlayH(u: Underlay): number {
|
|
122
125
|
if (u.h) return u.h;
|
|
123
|
-
const img = imgCache(u
|
|
126
|
+
const img = imgCache(underlaySrc(u));
|
|
124
127
|
return img && img.naturalWidth ? (u.w * img.naturalHeight) / img.naturalWidth : u.w * 0.7;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -162,6 +165,20 @@ export function underlayHandles(T: Xform, u: Underlay): { part: HandlePart; x: n
|
|
|
162
165
|
return HANDLES.map(([part, i, j]) => { const p = underlayAt(u, i * hw, j * hh), [x, y] = S(T, p.x, p.y); return { part, x, y, ang }; });
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
/**
|
|
169
|
+
* An expanded photo's badge (its link icon, in a white disc, 2r in from its top-right corner, as Skia's): its middle
|
|
170
|
+
* and radius on the screen. One size at any zoom, as the editor's chrome is (the user's call, 2026-10-26), smaller on a
|
|
171
|
+
* photo too small for it, and none on one smaller still. Whole pixels, so a device keeps a few shapes, not one a step.
|
|
172
|
+
*/
|
|
173
|
+
export function imageLinkBadge(T: Xform, u: Underlay): { x: number; y: number; r: number } | null {
|
|
174
|
+
if (!u.link) return null;
|
|
175
|
+
const h = underlayH(u), r = Math.min(IMAGE_BADGE_R, Math.floor(Math.min(u.w, h) * T.s * 0.1));
|
|
176
|
+
if (r < IMAGE_BADGE_MIN) return null;
|
|
177
|
+
const p = underlayAt(u, u.w / 2 - (2 * r) / T.s, h / 2 - (2 * r) / T.s), [x, y] = S(T, p.x, p.y);
|
|
178
|
+
return { x, y, r };
|
|
179
|
+
}
|
|
180
|
+
const IMAGE_BADGE_R = 15, IMAGE_BADGE_MIN = 7;
|
|
181
|
+
|
|
165
182
|
/** A world-space box (y up: top > bottom). */
|
|
166
183
|
export interface Box { left: number; right: number; top: number; bottom: number }
|
|
167
184
|
|
|
@@ -218,16 +235,28 @@ export function noteTextBox(T: Xform, n: Note, c: TextMeasure = textMeasure()) {
|
|
|
218
235
|
return { x, y: mid - 14 * k, w, h: 28 * k, mid, k, size };
|
|
219
236
|
}
|
|
220
237
|
|
|
221
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* How much a link's tile shows on the screen: all of it — or, once its name won't fit (under LINK_FULL_PX), a file's
|
|
240
|
+
* goes straight to its icon alone, a pin (the user's call, 2026-10-29: "this middle stage is not needed for object
|
|
241
|
+
* tiles - it can just jump straight to icon only"); a photo's or a sketch's is its picture alone down to the pin's
|
|
242
|
+
* size (it has only its size to lose), then a pin.
|
|
243
|
+
*/
|
|
222
244
|
export type LinkLod = 'full' | 'compact' | 'pin';
|
|
223
245
|
|
|
224
246
|
/** A tile on the screen: its square (top left, side), its middle, how much it shows, and its scale from the layout. */
|
|
225
247
|
export interface LinkBox { x: number; y: number; s: number; cx: number; cy: number; lod: LinkLod; k: number }
|
|
226
248
|
|
|
249
|
+
/** A photo's or a sketch's tile: its picture is the tile (tileTypeOf's 'image' and 'sketch' in render/linkTile.ts). */
|
|
250
|
+
const picturesItself = (n: Note): boolean => {
|
|
251
|
+
const t = n.link, ft = t && (linkInfoOf(t)?.fileType ?? t.fileType);
|
|
252
|
+
return t?.kind === 'file' && (ft === 'photo' || ft === 'drawing');
|
|
253
|
+
};
|
|
254
|
+
|
|
227
255
|
export function linkBox(T: Xform, n: Note): LinkBox {
|
|
228
256
|
const [cx, cy] = S(T, n.x ?? 0, n.y ?? 0), side = linkSide(n) * T.s;
|
|
229
|
-
const lod: LinkLod = side >= LINK_FULL_PX ? 'full' : side >= LINK_PIN_PX ? 'compact' : 'pin';
|
|
230
|
-
|
|
257
|
+
const lod: LinkLod = side >= LINK_FULL_PX ? 'full' : side >= LINK_PIN_PX && picturesItself(n) ? 'compact' : 'pin';
|
|
258
|
+
// (a pin is its own size, however big the tile is on the drawing: what's tapped and where a leader starts are it)
|
|
259
|
+
const s = lod === 'pin' ? LINK_PIN_PX : side;
|
|
231
260
|
return { x: cx - s / 2, y: cy - s / 2, s, cx, cy, lod, k: s / LINK_TILE_PX };
|
|
232
261
|
}
|
|
233
262
|
|
|
@@ -332,6 +361,9 @@ export function linkThumb(src: string): ImageLike {
|
|
|
332
361
|
export function imgCache(src: string | undefined): ImageLike | null {
|
|
333
362
|
if (!src) return null;
|
|
334
363
|
let im = imgs.get(src);
|
|
364
|
+
// (a picture a tile has already loaded, as its photo's expanded: that one, not a flash of nothing while it loads again)
|
|
365
|
+
const thumb = !im ? thumbs.get(src)?.im : undefined;
|
|
366
|
+
if (thumb?.complete && thumb.naturalWidth) imgs.set(src, (im = thumb));
|
|
335
367
|
if (!im) {
|
|
336
368
|
const loaded = () => { requestDraw(); refreshDrawerSoon(); };
|
|
337
369
|
if (isStoredRef(src)) {
|
|
@@ -434,11 +466,15 @@ export function setViewSize(W: number, H: number): void {
|
|
|
434
466
|
}
|
|
435
467
|
|
|
436
468
|
export function resize(): void {
|
|
437
|
-
const r = cvs.cv.getBoundingClientRect();
|
|
469
|
+
const r = cvs.cv.getBoundingClientRect(), dpr = Math.min(window.devicePixelRatio || 1, 2.5);
|
|
470
|
+
// (sized again to the same size, a canvas is cleared all the same: it showed blank until the next frame)
|
|
471
|
+
if (r.width === cvs.W && r.height === cvs.H && dpr === cvs.dpr) return;
|
|
438
472
|
cvs.W = r.width;
|
|
439
473
|
cvs.H = r.height;
|
|
440
|
-
cvs.dpr =
|
|
474
|
+
cvs.dpr = dpr;
|
|
441
475
|
cvs.cv.width = Math.max(1, Math.round(cvs.W * cvs.dpr));
|
|
442
476
|
cvs.cv.height = Math.max(1, Math.round(cvs.H * cvs.dpr));
|
|
477
|
+
// drawn now: left for the next frame, the cleared canvas flickered as a split view's divider was dragged
|
|
443
478
|
requestDraw();
|
|
479
|
+
paintPending();
|
|
444
480
|
}
|
package/src/skia/SkiaDrawCtx.ts
CHANGED
|
@@ -254,7 +254,14 @@ export class SkiaDrawCtx implements DrawCtx {
|
|
|
254
254
|
fill(): void { this.drawCurrent(this.paint(false)); }
|
|
255
255
|
stroke(): void { this.drawCurrent(this.paint(true)); }
|
|
256
256
|
clip(): void {
|
|
257
|
-
|
|
257
|
+
const pl = this.placed;
|
|
258
|
+
if (pl) {
|
|
259
|
+
// a kept shape: clipped to where it's placed, as it is (made into path commands again, it'd be a new path a frame)
|
|
260
|
+
if (pl.x || pl.y) this.canvas.translate(pl.x, pl.y);
|
|
261
|
+
this.canvas.clipPath(pl.path, INTERSECT, true);
|
|
262
|
+
if (pl.x || pl.y) this.canvas.translate(-pl.x, -pl.y);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
258
265
|
this.canvas.clipPath(this.built ??= this.path.build(), INTERSECT, true);
|
|
259
266
|
}
|
|
260
267
|
private drawCurrent(paint: SkPaint): void {
|
|
@@ -353,10 +360,10 @@ export class SkiaDrawCtx implements DrawCtx {
|
|
|
353
360
|
const { weight, size } = parseFont(key);
|
|
354
361
|
const ws = Object.keys(this.typefaces.weights).map(Number);
|
|
355
362
|
const near = ws.sort((a, b) => Math.abs(a - weight) - Math.abs(b - weight))[0];
|
|
356
|
-
const main = this.
|
|
363
|
+
const main = this.makeFont(this.typefaces.weights[near]!, size), m = main.getMetrics();
|
|
357
364
|
f = {
|
|
358
365
|
main,
|
|
359
|
-
fb: this.typefaces.fallback ? this.
|
|
366
|
+
fb: this.typefaces.fallback ? this.makeFont(this.typefaces.fallback, size) : null,
|
|
360
367
|
metrics: { ascent: m.ascent, descent: m.descent, size: main.getSize() },
|
|
361
368
|
};
|
|
362
369
|
remember(this.fonts, key, f);
|
|
@@ -364,6 +371,19 @@ export class SkiaDrawCtx implements DrawCtx {
|
|
|
364
371
|
return f;
|
|
365
372
|
}
|
|
366
373
|
|
|
374
|
+
/**
|
|
375
|
+
* A font that lays text out as the browser does: each character as wide as the typeface says at any size (linear
|
|
376
|
+
* metrics). Skia's own rounds every width to a whole pixel at the font's size. On Android (FreeType) that was a
|
|
377
|
+
* tile's text, set at 4 to 9 px and scaled up with the card, with a space gone or one put inside a word ("ToDoList",
|
|
378
|
+
* "Lis t"), and the scale made each gap as much bigger. (Nothing else is set: the app's React Native Skia, 2.8, reads
|
|
379
|
+
* setSubpixel's flag as a number and throws on true — and a later one throws on a number.)
|
|
380
|
+
*/
|
|
381
|
+
private makeFont(face: SkTypeface, size: number): SkFont {
|
|
382
|
+
const f = this.Skia.Font(face, size);
|
|
383
|
+
f.setLinearMetrics(true);
|
|
384
|
+
return f;
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
/** Split text into runs the main font can draw and runs that need the fallback, with their widths (cached). */
|
|
368
388
|
private runs(text: string, f: FontPair): Run[] {
|
|
369
389
|
const css = this.st.font;
|
|
@@ -380,7 +400,9 @@ export class SkiaDrawCtx implements DrawCtx {
|
|
|
380
400
|
if (last && last.font === font) last.text += ch; else out.push({ text: ch, font, width: 0 });
|
|
381
401
|
});
|
|
382
402
|
}
|
|
383
|
-
|
|
403
|
+
// (each character's width, added up here: a device's getTextWidth adds them as whole numbers, each one's fraction
|
|
404
|
+
// dropped, so text was measured short of what's drawn)
|
|
405
|
+
for (const r of out) r.width = r.font.getGlyphWidths(r.font.getGlyphIDs(r.text, Array.from(r.text).length)).reduce((w, g) => w + g, 0);
|
|
384
406
|
// (kept: measuring and drawing a label again — every frame — asks native code nothing)
|
|
385
407
|
return remember(byText, text, out);
|
|
386
408
|
}
|