@reekon-tools/boldr-cad 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +3 -5
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +149 -169
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +2 -2
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/linkActions.d.ts +22 -3
- package/dist/editor/linkActions.js +95 -13
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/mode.js +4 -3
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +9 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +4 -1
- package/dist/model/links.js +9 -2
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +120 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +21 -1
- package/dist/model/sketch.js +58 -4
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/types.d.ts +38 -7
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +94 -25
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/Drawer.js +3 -2
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/host.d.ts +7 -0
- package/dist/react/host.js +14 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +184 -149
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +5 -1
- package/dist/render/snap.js +3 -1
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +31 -4
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +23 -6
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.js +6 -1
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +6 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +19 -0
- package/dist/tools/select.js +116 -17
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/tools/survey.d.ts +6 -0
- package/dist/tools/survey.js +28 -0
- package/dist/tools/survey.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +3 -5
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +126 -112
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +2 -2
- package/src/editor/linkActions.ts +90 -13
- package/src/editor/mode.ts +4 -3
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +8 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -1
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +8 -2
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +105 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +48 -4
- package/src/model/stroke.ts +2 -2
- package/src/model/types.ts +38 -7
- package/src/react/BottomBar.tsx +114 -29
- package/src/react/Drawer.tsx +14 -10
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/Sheet.tsx +3 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/host.ts +14 -1
- package/src/react/motion.tsx +6 -6
- package/src/render/draw.ts +156 -116
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +7 -2
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +37 -5
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +22 -6
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +5 -1
- package/src/tools/registry.ts +10 -1
- package/src/tools/select.ts +99 -18
- package/src/tools/shapes.ts +9 -4
- package/src/tools/survey.ts +21 -0
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
package/src/render/linkTile.ts
CHANGED
|
@@ -4,16 +4,24 @@ import type { LinkInfo, Note } from '../model/types';
|
|
|
4
4
|
import type { DrawCtx } from './ctx';
|
|
5
5
|
import { strokeMarkup } from './markup';
|
|
6
6
|
import { smoothRectPath } from './squircle';
|
|
7
|
+
import { drawSvgIcon, svgIcon, type SvgIcon } from './svgPath';
|
|
7
8
|
import { FONT, TH } from './theme';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BADGE_CAD_SVG, BADGE_CON_CALC_SVG, BADGE_CONVERSION_SVG, BADGE_FILE_SVG, BADGE_LABEL_SVG, BADGE_NOTE_SVG, KEY_CAMERA_SVG,
|
|
11
|
+
KEY_MEASUREMENT_SVG, KEY_SKETCH_SVG,
|
|
12
|
+
} from './tileIcons';
|
|
13
|
+
import { cvs, leaderPath, linkBox, linkThumb, requestDraw, type ImageLike, type LinkBox, type Xform } from './view';
|
|
9
14
|
|
|
10
15
|
/*
|
|
11
|
-
A link's tile on the plan
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
A link's tile on the plan, as Figma draws an object tile (657:8732): a square card filled with what the object looks
|
|
17
|
+
like (its picture, a note's text, a conversion's screen), a grey bar along its foot with the object's badge — its
|
|
18
|
+
type's colour and icon, as the add-object sheet has them — and its name. A photo or a sketch with its picture is that
|
|
19
|
+
picture alone, edge to edge. Selected, a yellow ring round its edge; otherwise a grey hairline inside it. The card is
|
|
20
|
+
laid out once at LINK_TILE_PX (Figma's 120 tile, scaled) and scaled to its size on the drawing; its bar, the bar's
|
|
21
|
+
badge and name, and the badge in the middle of a card with nothing to show stay one size on the screen (BAR_K), and a
|
|
22
|
+
wider card shows more of its name. What it's made of is kept, on a device: a pinch makes a few new shapes, not one a
|
|
23
|
+
step (screenW, cornerOf). Small on the screen it shows less: its picture or its badge alone, then a pin of a fixed
|
|
24
|
+
size. Where it is on the screen (and its handles) is view.ts's, and picking it hit.ts's.
|
|
17
25
|
*/
|
|
18
26
|
|
|
19
27
|
export interface LinkDrawOpts {
|
|
@@ -25,6 +33,7 @@ export interface LinkDrawOpts {
|
|
|
25
33
|
|
|
26
34
|
/** Draw tile `n` and its leader. */
|
|
27
35
|
export function drawLink(c: DrawCtx, T: Xform, n: Note, o: LinkDrawOpts): void {
|
|
36
|
+
if (!watchingFonts) watchFonts();
|
|
28
37
|
const b = linkBox(T, n), lp = leaderPath(T, n, b);
|
|
29
38
|
c.save();
|
|
30
39
|
c.globalAlpha = o.alpha;
|
|
@@ -41,8 +50,44 @@ export function drawLink(c: DrawCtx, T: Xform, n: Note, o: LinkDrawOpts): void {
|
|
|
41
50
|
*/
|
|
42
51
|
const screenW = (px: number, k: number): number => Math.pow(2, Math.round(Math.log2(px / k) * 4) / 4);
|
|
43
52
|
|
|
44
|
-
/**
|
|
45
|
-
const L = LINK_TILE_PX,
|
|
53
|
+
/** Figma's 120 tile in the layout's units: every measure of the frame × F. */
|
|
54
|
+
const L = LINK_TILE_PX, F = L / 120;
|
|
55
|
+
/** The card's corners (10, smoothed 60% as every corner in the editor); how wide a selected tile's ring is (pt). */
|
|
56
|
+
const R = 10 * F, SELECT_W = 3;
|
|
57
|
+
/** The bar along the card's foot: 28 tall. */
|
|
58
|
+
const FOOT_H = 28 * F;
|
|
59
|
+
/** The badge in it (20, corners 6), 4 in from the card's side and the bar's top; the name 5.37 past it, 4 short of the far side. */
|
|
60
|
+
const BADGE = 20 * F, BADGE_IN = 4 * F, NAME_X = 29.366 * F, NAME_END = 4 * F;
|
|
61
|
+
/** The name: Inter Medium 10 in the frame, its baseline 18 below the bar's top. */
|
|
62
|
+
const NAME_PX = 10 * F, NAME_BASE = 18 * F;
|
|
63
|
+
/**
|
|
64
|
+
* The bar, its badge and its name are one size on the screen at any zoom (the user's call, 2026-10-26: "on zoom
|
|
65
|
+
* nothing scales but the text — the bottom banner stays the same height, and so does the icon"): the frame's, ×
|
|
66
|
+
* BAR_SCALE. Its name is 12 (the user's call, 2026-10-26: "make it 12 px font … adjust the banner and icon size if
|
|
67
|
+
* needed to scale properly"), so the bar is 33.6 and its badge 24, as the frame has them to its 10. The card and what
|
|
68
|
+
* it shows grow with the drawing; a name shows more of itself as the card widens. BAR_K: the card's scale at which its
|
|
69
|
+
* layout's bar is that size.
|
|
70
|
+
*/
|
|
71
|
+
const BAR_SCALE = 1.2, BAR_K = BAR_SCALE / F;
|
|
72
|
+
/**
|
|
73
|
+
* The tile's type: Figma's Inter. BOLDR Web's pages load it; on a device the host's font is drawn whatever is named
|
|
74
|
+
* here (BOLDR passes Inter). Anywhere without it, the editor's own.
|
|
75
|
+
*/
|
|
76
|
+
const TILE_FONT = '"Inter",' + FONT;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The card's corners at scale `k`: R, up to the card at BAR_K (144 across); bigger than that, about as big on the
|
|
80
|
+
* screen as they are there (12: the frame's 10, as the bar is the frame's × BAR_SCALE), so they never reach into its
|
|
81
|
+
* badge or its name's ends — in steps, as screenW's, so a pinch makes a few shapes and not one a step: 11 to 13.1.
|
|
82
|
+
*/
|
|
83
|
+
const cornerOf = (k: number): number => (k <= BAR_K ? R : R * Math.pow(2, Math.round(Math.log2(BAR_K / k) * 4) / 4));
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The tile's own colours (Figma's, the same in both themes: a tile is a dark card on any canvas): the card under its
|
|
87
|
+
* picture (General/Grey - 1), the bar (Grey - 2), its hairlines (Grey - 3), quiet text (Grey - 4), and the light grey a
|
|
88
|
+
* calculator's diagram sits on (Light Grey - 2).
|
|
89
|
+
*/
|
|
90
|
+
const TILE = { card: '#2B2C2E', bar: '#424345', line: '#626364', quiet: '#9FA0A2', name: '#FFFFFF', paper: '#E8EAED' } as const;
|
|
46
91
|
|
|
47
92
|
/** A rounded rectangle's path, one shape for each size (as draw.ts's rr: kept by an engine that can). */
|
|
48
93
|
function rr(c: DrawCtx, x: number, y: number, w: number, h: number, r: number): void {
|
|
@@ -50,91 +95,398 @@ function rr(c: DrawCtx, x: number, y: number, w: number, h: number, r: number):
|
|
|
50
95
|
else smoothRectPath(c, x, y, w, h, r);
|
|
51
96
|
}
|
|
52
97
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Clip to the card above its bar, or (`bar`) to the bar — the bar drawn at its scale `u` within the card's. Described in
|
|
100
|
+
* the bar's own scale, where it's one size whatever the zoom, so it's one shape kept, never one a step of a pinch.
|
|
101
|
+
*/
|
|
102
|
+
function clipToBar(c: DrawCtx, u: number, bar: boolean): void {
|
|
103
|
+
// (far past the card every way but the one edge that counts, the bar's top: put where the shape starts, so it's
|
|
104
|
+
// exactly there however the rest rounds. The biggest card, 480" at the closest zoom (400 px an inch), is 128,000 of
|
|
105
|
+
// the bar's units across: LINK_MAX_IN × ZOOM_MAX / BAR_SCALE.)
|
|
106
|
+
const far = 1e6, d = bar ? far : -far;
|
|
107
|
+
const draw = (cc: DrawCtx, X: number, Y: number) => {
|
|
108
|
+
cc.beginPath(); cc.moveTo(X - far, Y); cc.lineTo(X + far, Y); cc.lineTo(X + far, Y + d); cc.lineTo(X - far, Y + d); cc.closePath();
|
|
109
|
+
};
|
|
110
|
+
c.translate(0, L); c.scale(u, u); c.translate(0, -L);
|
|
111
|
+
if (c.shape) c.shape('bar clip ' + bar, 0, L - FOOT_H, draw);
|
|
112
|
+
else draw(c, 0, L - FOOT_H);
|
|
113
|
+
c.clip();
|
|
114
|
+
c.translate(0, L); c.scale(1 / u, 1 / u); c.translate(0, -L);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What a tile is, by the object it's for: Figma's tile types (and a sketch, a measurement: no tile there). */
|
|
118
|
+
export type TileType = 'image' | 'sketch' | 'file' | 'concalc' | 'conversion' | 'note' | 'cad' | 'label' | 'measurement';
|
|
119
|
+
|
|
120
|
+
/** The host's kind of file (LinkInfo.fileType) as a tile type: a PDF or anything unknown is a file. */
|
|
121
|
+
export function tileTypeOf(n: Note, info?: LinkInfo): TileType {
|
|
122
|
+
if (n.link!.kind === 'measurement') return 'measurement';
|
|
123
|
+
switch (info?.fileType ?? n.link!.fileType) {
|
|
124
|
+
case 'photo': return 'image';
|
|
125
|
+
case 'drawing': return 'sketch';
|
|
126
|
+
case 'calculator': return 'concalc';
|
|
127
|
+
case 'conversion': return 'conversion';
|
|
128
|
+
case 'note': return 'note';
|
|
129
|
+
case 'label': return 'label';
|
|
130
|
+
case 'cad': return 'cad';
|
|
131
|
+
default: return 'file';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A badge: its hue, its icon (render/tileIcons.ts) and where the icon sits in a badge 20 across. Figma's for six types
|
|
137
|
+
* ("Object Tile Icons" 704:23628, glyph boxes as drawn there); the rest from the add-object sheet, their glyphs as big
|
|
138
|
+
* in the badge as those are (about 13 of its 20).
|
|
139
|
+
*/
|
|
140
|
+
interface Badge { hue: string; icon: SvgIcon; key: string; x: number; y: number; size: number }
|
|
141
|
+
const badge = (hue: string, svg: string, key: string, x: number, y: number, size: number): Badge => ({ hue, icon: svgIcon(svg), key, x, y, size });
|
|
142
|
+
export const BADGES: Record<TileType, Badge> = {
|
|
143
|
+
file: badge('#E93C3E', BADGE_FILE_SVG, 'tile file', 5, 3.6034, 12.7932),
|
|
144
|
+
concalc: badge('#BD30A8', BADGE_CON_CALC_SVG, 'tile concalc', 2.9285, 4.5, 14.1429),
|
|
145
|
+
conversion: badge('#505DC1', BADGE_CONVERSION_SVG, 'tile conversion', 4.0002, 4.7068, 12),
|
|
146
|
+
note: badge('#0DBF4A', BADGE_NOTE_SVG, 'tile note', 3.5, 4, 13),
|
|
147
|
+
cad: badge('#23C7C7', BADGE_CAD_SVG, 'tile cad', 3.2557, 2.7557, 14.4887),
|
|
148
|
+
label: badge('#F7781E', BADGE_LABEL_SVG, 'tile label', 3.5, 3.7491, 13),
|
|
149
|
+
// (the sheet's icons are drawn on their 70 tile, their glyph in its middle: the tile scaled so the glyph is 13)
|
|
150
|
+
sketch: badge('#EFB107', KEY_SKETCH_SVG, 'tile sketch', 10 - 29.4 / 2, 10 - 29.4 / 2, 29.4),
|
|
151
|
+
measurement: badge('#0066FF', KEY_MEASUREMENT_SVG, 'tile measurement', 10 - 26.8 / 2, 10 - 26.8 / 2, 26.8),
|
|
152
|
+
image: badge('#000000', KEY_CAMERA_SVG, 'tile camera', 3.5, 10 - (26.8881 * 13) / 36 / 2, 13),
|
|
60
153
|
};
|
|
61
154
|
|
|
155
|
+
/** Badge `t` `size` across, its top left at x, y: its hue (or `hue`), its icon in white in its middle. */
|
|
156
|
+
function drawBadge(c: DrawCtx, t: TileType, x: number, y: number, size: number, hue?: string): void {
|
|
157
|
+
const bd = BADGES[t], s = size / 20;
|
|
158
|
+
rr(c, x, y, size, size, 6 * s);
|
|
159
|
+
c.fillStyle = hue ?? bd.hue; c.fill();
|
|
160
|
+
drawSvgIcon(c, bd.icon, bd.key, x + bd.x * s, y + bd.y * s, bd.size * s);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Its picture, once loaded (asked for only near the screen: a drawing can link more than are kept). */
|
|
164
|
+
function pictureOf(b: LinkBox, n: Note, info: LinkInfo | undefined): ImageLike | null {
|
|
165
|
+
const gone = info?.exists === false, near = b.x < cvs.W * 2 && b.y < cvs.H * 2 && b.x + b.s > -cvs.W && b.y + b.s > -cvs.H;
|
|
166
|
+
const pic = near && !gone && n.link!.kind === 'file' && info?.thumbUrl ? linkThumb(info.thumbUrl) : null;
|
|
167
|
+
return pic && pic.complete && pic.naturalWidth > 0 ? pic : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What the card is filled with under its preview: a calculator's light grey, a label's bar grey, a conversion's page. */
|
|
171
|
+
function cardFill(type: TileType, pic: ImageLike | null, info: LinkInfo | undefined): string {
|
|
172
|
+
if (pic) return type === 'concalc' ? TILE.paper : type === 'label' ? TILE.bar : TILE.card;
|
|
173
|
+
return type === 'conversion' && info?.text ? CONV.page : TILE.card;
|
|
174
|
+
}
|
|
175
|
+
|
|
62
176
|
function drawCard(c: DrawCtx, b: LinkBox, n: Note, o: LinkDrawOpts): void {
|
|
63
|
-
const k = b.k, t = n.link!, info = o.info, gone = info?.exists === false,
|
|
177
|
+
const k = b.k, t = n.link!, info = o.info, gone = info?.exists === false, type = tileTypeOf(n, info);
|
|
178
|
+
const pic = pictureOf(b, n, info), picture = !!pic && (type === 'image' || type === 'sketch');
|
|
179
|
+
const withBar = !picture && b.lod !== 'compact';
|
|
180
|
+
// the bar's scale within the card's: one size on the screen (BAR_K)
|
|
181
|
+
const u = BAR_K / k, r = cornerOf(k);
|
|
64
182
|
c.save();
|
|
65
183
|
c.translate(b.x, b.y);
|
|
66
184
|
c.scale(k, k);
|
|
67
|
-
rr(c, 0, 0, L, L,
|
|
185
|
+
rr(c, 0, 0, L, L, r);
|
|
68
186
|
// selected: a yellow ring round the card's edge, the same 3 pt at any size — stroked twice as wide on the card's own
|
|
69
187
|
// outline before it's filled, so the card covers the inside half (and no new shape is made as it zooms)
|
|
70
188
|
if (o.selected) { c.lineWidth = screenW(2 * SELECT_W, k); c.strokeStyle = TH.yellow; c.stroke(); }
|
|
71
|
-
c.fillStyle = gone ?
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// the picture or the icon alone
|
|
81
|
-
if (shown) cover(c, pic, 6, 6, L - 12, L - 12, R - 4);
|
|
82
|
-
else drawGlyph(c, glyph, L / 2, L / 2, 46, TH.ink2);
|
|
83
|
-
} else if (t.kind === 'measurement' && !gone) {
|
|
84
|
-
// its value, and its name
|
|
85
|
-
drawGlyph(c, 'tape', 18, 17, 18, TH.ink2);
|
|
86
|
-
const value = info?.text ?? t.text ?? '—';
|
|
87
|
-
c.font = '700 18px ' + FONT; c.fillStyle = TH.blue; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
88
|
-
c.fillText(fit(c, value, L - 12), L / 2, 46);
|
|
89
|
-
drawName(c, k, info?.name ?? t.name ?? 'Measurement', 75, L - 14, TH.ink2, false);
|
|
90
|
-
} else if (gone) {
|
|
91
|
-
// the removed mark, and what it was
|
|
92
|
-
drawGlyph(c, glyph, L / 2, 30, 28, TH.ink2);
|
|
93
|
-
c.font = '700 11px ' + FONT; c.fillStyle = TH.ink2; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
94
|
-
c.fillText('Removed', L / 2, 58);
|
|
95
|
-
drawName(c, k, t.name ?? (t.kind === 'file' ? 'File' : 'Measurement'), 79, L - 12, TH.ink2, false);
|
|
189
|
+
c.fillStyle = cardFill(type, pic, gone ? undefined : info); c.fill();
|
|
190
|
+
// everything else within the card's edge: one clip, to the outline just filled (kept as it is, on a device)
|
|
191
|
+
c.clip();
|
|
192
|
+
|
|
193
|
+
if (picture) cover(c, pic!, 0, 0, L, L);
|
|
194
|
+
else if (!withBar) {
|
|
195
|
+
// small: the picture or the badge alone
|
|
196
|
+
if (pic) cover(c, pic, 0, 0, L, L);
|
|
197
|
+
else drawBadge(c, type, L / 4, L / 4, L / 2, gone ? TILE.line : undefined);
|
|
96
198
|
} else {
|
|
97
|
-
|
|
98
|
-
|
|
199
|
+
drawPreview(c, type, pic, u, gone, gone ? undefined : info, t.text);
|
|
200
|
+
drawBar(c, u, k, type, gone, info?.name ?? t.name ?? (t.kind === 'file' ? 'File' : 'Measurement'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// a grey hairline inside its edge, 1 px on the screen (the clip keeps the inside half of a line twice that); selected,
|
|
204
|
+
// the bar keeps its own inside the ring
|
|
205
|
+
if (!o.selected || withBar) {
|
|
206
|
+
if (o.selected) { c.save(); clipToBar(c, u, true); }
|
|
207
|
+
rr(c, 0, 0, L, L, r);
|
|
208
|
+
c.lineWidth = screenW(2, k); c.strokeStyle = TILE.line; c.stroke();
|
|
209
|
+
if (o.selected) c.restore();
|
|
210
|
+
}
|
|
211
|
+
c.restore();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Where a picture goes that isn't simply the whole card (Figma's frame, at its 120): a label as tall as the card, its
|
|
216
|
+
* foot under the bar, on the bar's grey, as the label printer shows it — kept whole within 8 of the card's sides; a
|
|
217
|
+
* calculator's diagram nine tenths of the card across, on light grey, its top cut off where the frame cuts it and its
|
|
218
|
+
* foot just above the bar. Each is a picture placed whole in that box, so the frame's own (a portrait label, a
|
|
219
|
+
* near-square diagram) land exactly there.
|
|
220
|
+
*/
|
|
221
|
+
const LABEL_BOX = [8 * F, 3.45 * F, L - 16 * F, 112 * F] as const;
|
|
222
|
+
const CALC_BOX = [6.244 * F, -8.956 * F, 107.984 * F, 106.86 * F] as const;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A label's box with the bar at `u`: the frame's, and on a card past the 144 tile, where the bar is less of it than the
|
|
226
|
+
* frame's, as much further down as keeps its foot as far under the bar as the frame's (23.45 of its 28).
|
|
227
|
+
*/
|
|
228
|
+
const labelBox = (u: number): readonly [number, number, number, number] =>
|
|
229
|
+
u >= 1 ? LABEL_BOX : [LABEL_BOX[0], LABEL_BOX[1], LABEL_BOX[2], L - 4.55 * F * u - LABEL_BOX[1]];
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* What the object looks like, above its bar (drawn at scale `u` within the card's): its picture over the whole card (a
|
|
233
|
+
* label's and a calculator's placed whole, as LABEL_BOX and CALC_BOX), a note's text, a conversion's screen, a
|
|
234
|
+
* measurement's value (`was`: as it was when linked, till the host says) — or, with none of those, its badge, larger,
|
|
235
|
+
* in the middle: one size on the screen, as the bar's, but never more than three quarters of what's above the bar (so
|
|
236
|
+
* smaller on a card under about 98 across). Removed: says so.
|
|
237
|
+
*
|
|
238
|
+
* A label, a calculator's diagram and a conversion's screen are laid out as the frame has them over its bar. On a card
|
|
239
|
+
* under the 144 tile (BAR_K) the bar is more of the card than the frame's, so that layout is drawn smaller, to fit
|
|
240
|
+
* what's above this bar as the frame's fits what's above its own: the diagram's foot and the screen's second unit stay
|
|
241
|
+
* in sight, and a label's foot as far under the bar.
|
|
242
|
+
*/
|
|
243
|
+
function drawPreview(c: DrawCtx, type: TileType, pic: ImageLike | null, u: number, gone: boolean, info: LinkInfo | undefined, was?: string): void {
|
|
244
|
+
const foot = L - FOOT_H * u, mid = foot / 2;
|
|
245
|
+
if (gone) {
|
|
246
|
+
c.font = '500 ' + 11 * F + 'px ' + TILE_FONT; c.fillStyle = TILE.quiet; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
247
|
+
c.fillText('Removed', L / 2, mid);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (pic || (type === 'conversion' && info?.text)) {
|
|
251
|
+
// above the bar only: in Draft a tile is drawn see-through, and what's under the bar would show through it
|
|
252
|
+
c.save();
|
|
253
|
+
clipToBar(c, u, false);
|
|
254
|
+
if (pic && type !== 'label' && type !== 'concalc') cover(c, pic, 0, 0, L, L);
|
|
99
255
|
else {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
256
|
+
// (the frame's layout above its bar, in what's above this one: from its top middle)
|
|
257
|
+
const s = Math.min(1, foot / (L - FOOT_H));
|
|
258
|
+
if (s < 1) { c.translate(L / 2, 0); c.scale(s, s); c.translate(-L / 2, 0); }
|
|
259
|
+
if (!pic) drawConversion(c, info!.text!);
|
|
260
|
+
else contain(c, pic, ...(type === 'label' ? labelBox(u) : CALC_BOX));
|
|
104
261
|
}
|
|
105
|
-
|
|
262
|
+
c.restore();
|
|
263
|
+
return;
|
|
106
264
|
}
|
|
265
|
+
if (type === 'note' && info?.text) { drawNoteText(c, info.text, foot); return; }
|
|
266
|
+
if (type === 'measurement') {
|
|
267
|
+
c.font = '700 ' + 22 * F + 'px ' + TILE_FONT; c.fillStyle = TILE.name; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
268
|
+
c.fillText(fit(c, info?.text ?? was ?? '—', L - 16 * F), L / 2, mid);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// (the frame's 40, one size on the screen as the bar's badge is, and as much bigger than the frame's: no bigger than
|
|
272
|
+
// three quarters of what's above the bar — drawn at 40 and scaled, so its shapes are the same at every step of a pinch)
|
|
273
|
+
const s = 40 * F, q = Math.min(u, (0.75 * foot) / s);
|
|
274
|
+
c.save();
|
|
275
|
+
c.translate(L / 2, mid); c.scale(q, q);
|
|
276
|
+
drawBadge(c, type, -s / 2, -s / 2, s);
|
|
277
|
+
c.restore();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The bar along the card's foot (Figma: 28 tall, General/Grey - 2, a Grey - 3 hairline along its top), drawn at its own
|
|
282
|
+
* scale `u` within the card's (one size on the screen: BAR_K), anchored at the card's foot: its badge, then the name —
|
|
283
|
+
* white, Medium, on one line, cut short with an ellipsis. Once the object's gone, a grey badge.
|
|
284
|
+
*/
|
|
285
|
+
function drawBar(c: DrawCtx, u: number, k: number, type: TileType, gone: boolean, name: string): void {
|
|
286
|
+
c.save();
|
|
287
|
+
c.translate(0, L); c.scale(u, u); c.translate(0, -L);
|
|
288
|
+
const W = L / u, top = L - FOOT_H;
|
|
289
|
+
c.fillStyle = TILE.bar; c.fillRect(0, top, W, FOOT_H);
|
|
290
|
+
c.fillStyle = TILE.line; c.fillRect(0, top, W, screenW(1, k * u));
|
|
291
|
+
drawBadge(c, type, BADGE_IN, top + BADGE_IN, BADGE, gone ? TILE.line : undefined);
|
|
292
|
+
c.font = '500 ' + NAME_PX + 'px ' + TILE_FONT; c.fillStyle = TILE.name; c.textAlign = 'left'; c.textBaseline = 'alphabetic';
|
|
293
|
+
c.fillText(fitName(c, name, W - NAME_X - NAME_END), NAME_X, top + NAME_BASE);
|
|
107
294
|
c.restore();
|
|
108
295
|
}
|
|
109
296
|
|
|
110
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* A note's text over the card, as its editor shows it (Figma's preview: white on Grey - 1, about 9 to the 120 tile): a
|
|
299
|
+
* line every 13.5, and 4.25 more where a new block starts (the editor's half-em paragraph margin; the host marks one
|
|
300
|
+
* with a blank line). Each of the host's lines wraps to the card's width at its spaces. A line the host starts with
|
|
301
|
+
* "• " is a list item, a dot before it, and one it starts with a number ("1. ") a numbered item, the number before it
|
|
302
|
+
* (as the editor numbers them, and the job's own tile): its text further in, and so are its lines after the first. As
|
|
303
|
+
* many lines as fit above the bar.
|
|
304
|
+
*/
|
|
305
|
+
const NOTE_PX = 9 * F, NOTE_LINE = 13.5 * F, NOTE_GAP = 4.25 * F, NOTE_X = 8.75 * F, NOTE_FIRST = 17.25 * F;
|
|
306
|
+
const NOTE_DOT_X = 14 * F, NOTE_DOT_R = 1.7 * F, NOTE_ITEM_X = 22.25 * F;
|
|
307
|
+
/** where a number ends: a third of the type's size before its item's text */
|
|
308
|
+
const NOTE_NUM_END = NOTE_ITEM_X - 3 * F;
|
|
309
|
+
function drawNoteText(c: DrawCtx, text: string, foot: number): void {
|
|
310
|
+
c.font = '400 ' + NOTE_PX + 'px ' + TILE_FONT; c.fillStyle = TILE.name; c.textBaseline = 'alphabetic';
|
|
311
|
+
let y = NOTE_FIRST - NOTE_LINE;
|
|
312
|
+
for (const { text: line, x, mark, gap } of noteRows(c, text)) {
|
|
313
|
+
y += NOTE_LINE + (gap ? NOTE_GAP : 0);
|
|
314
|
+
if (y > foot - 4 * F) break;
|
|
315
|
+
if (mark === '•') {
|
|
316
|
+
// (its dot on the line's x-height middle, kept as the badges are)
|
|
317
|
+
dot(c, NOTE_DOT_X, y - NOTE_PX * 0.33, NOTE_DOT_R);
|
|
318
|
+
c.fill();
|
|
319
|
+
} else if (mark) { c.textAlign = 'right'; c.fillText(mark, NOTE_NUM_END, y); }
|
|
320
|
+
c.textAlign = 'left';
|
|
321
|
+
c.fillText(line, x, y);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** A dot's path (kept, one a size). */
|
|
325
|
+
function dot(c: DrawCtx, x: number, y: number, r: number): void {
|
|
326
|
+
if (c.shape) c.shape('dot ' + r, x - r, y - r, (cc, X, Y) => { cc.beginPath(); cc.arc(X + r, Y + r, r, 0, Math.PI * 2); });
|
|
327
|
+
else { c.beginPath(); c.arc(x, y, r, 0, Math.PI * 2); }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A note's text as the host's lines: each with its mark ("•" for a list item, its number for a numbered one) and
|
|
332
|
+
* whether a new block starts there (a blank line before it, left out itself) — at most NOTE_LINES, each no longer than
|
|
333
|
+
* NOTE_CHARS (more than a card shows of one: it's only wrapped as far as that). Split once for each text.
|
|
334
|
+
*/
|
|
335
|
+
const NOTE_LINES = 12, NOTE_CHARS = 400;
|
|
336
|
+
const NUMBERED = /^(\d{1,3}\.)\s+/;
|
|
337
|
+
interface NoteLine { text: string; mark?: string; gap: boolean }
|
|
338
|
+
const lineCache = new Map<string, NoteLine[]>();
|
|
339
|
+
function noteLines(text: string): NoteLine[] {
|
|
340
|
+
let lines = lineCache.get(text);
|
|
341
|
+
if (!lines) {
|
|
342
|
+
lines = [];
|
|
343
|
+
let gap = false;
|
|
344
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
345
|
+
const l = raw.trim();
|
|
346
|
+
if (!l) { gap = lines.length > 0; continue; }
|
|
347
|
+
if (lines.length >= NOTE_LINES) break;
|
|
348
|
+
const num = NUMBERED.exec(l), mark = l.startsWith('• ') ? '•' : num?.[1];
|
|
349
|
+
const s = mark === '•' ? l.slice(2).trim() : num ? l.slice(num[0].length) : l;
|
|
350
|
+
lines.push({ text: s.length > NOTE_CHARS ? Array.from(s).slice(0, NOTE_CHARS).join('') : s, mark, gap });
|
|
351
|
+
gap = false;
|
|
352
|
+
}
|
|
353
|
+
if (lineCache.size > 500) lineCache.clear();
|
|
354
|
+
lineCache.set(text, lines);
|
|
355
|
+
}
|
|
356
|
+
return lines;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The lines a note's card shows: the host's, each wrapped to the card's width (at most NOTE_LINES in all), an item's
|
|
361
|
+
* mark on its first. A note's lines are the same width at every zoom: wrapped once for each text, at each font (one
|
|
362
|
+
* that arrives later wraps it again: watchFonts).
|
|
363
|
+
*/
|
|
364
|
+
interface NoteRow { text: string; x: number; mark?: string; gap: boolean }
|
|
365
|
+
const rowCache = new Map<string, NoteRow[]>();
|
|
366
|
+
function noteRows(c: DrawCtx, text: string): NoteRow[] {
|
|
367
|
+
const key = c.font + '|' + text;
|
|
368
|
+
let rows = rowCache.get(key);
|
|
369
|
+
if (rows) return rows;
|
|
370
|
+
rows = [];
|
|
371
|
+
for (const { text: line, mark, gap } of noteLines(text)) {
|
|
372
|
+
const x = mark ? NOTE_ITEM_X : NOTE_X;
|
|
373
|
+
wrap(c, line, L - NOTE_X - x, NOTE_LINES - rows.length).forEach((t, i) => rows!.push({ text: t, x, mark: i ? undefined : mark, gap: !i && gap }));
|
|
374
|
+
if (rows.length >= NOTE_LINES) break;
|
|
375
|
+
}
|
|
376
|
+
if (rowCache.size > 500) rowCache.clear();
|
|
377
|
+
rowCache.set(key, rows);
|
|
378
|
+
return rows;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** `text` as at most `room` lines no wider than `maxW` at the current font, broken at its spaces (a word wider than a line, cut short). */
|
|
382
|
+
function wrap(c: DrawCtx, text: string, maxW: number, room: number): string[] {
|
|
383
|
+
const out: string[] = [];
|
|
384
|
+
let line = '';
|
|
385
|
+
for (const word of text.split(/\s+/)) {
|
|
386
|
+
if (!word || out.length >= room) continue;
|
|
387
|
+
const next = line ? line + ' ' + word : word;
|
|
388
|
+
if (c.measureText(next).width <= maxW) { line = next; continue; }
|
|
389
|
+
if (line) out.push(line);
|
|
390
|
+
line = '';
|
|
391
|
+
if (c.measureText(word).width <= maxW) line = word;
|
|
392
|
+
else if (out.length < room) out.push(fit(c, word, maxW));
|
|
393
|
+
}
|
|
394
|
+
if (line && out.length < room) out.push(line);
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* A conversion as its screen shows it — Figma's preview is that screen, its top cut to the card (657:8990): the unit
|
|
400
|
+
* it's from on a grey header, what was typed under it (the field's INPUT, with nothing), the swap button, and the unit
|
|
401
|
+
* it's to on a blue header, the result's grey below. `text` is the host's: the two units' names, a line each, then the
|
|
402
|
+
* value typed, if any. Measured at the 120 tile from the frame's screenshot; the colours are BOLDR's palette, as the
|
|
403
|
+
* screen's are (General/Dark Grey, Grey - 1, Grey - 2, Grey - 4; Secondary/Accent Blue - 2 and - 3).
|
|
404
|
+
*/
|
|
405
|
+
const CONV = { page: '#181A1C', result: '#2B2C2E', head: '#424345', to: '#4B6074', chipLine: '#2E4C6A', hint: '#9FA0A2' } as const;
|
|
406
|
+
/** its column (the screen's sides, 4.27 in), the headers' tops and height, the field's foot, the corners (8 pt) */
|
|
407
|
+
const CV_X = 4.27 * F, CV_W = 111.46 * F, CV_FROM = 4.48 * F, CV_HEAD = 13.13 * F, CV_FIELD = 66.16 * F;
|
|
408
|
+
const CV_TO = 69.21 * F, CV_RESULT = 82.04 * F, CV_R = 2.44 * F;
|
|
409
|
+
/**
|
|
410
|
+
* the units' names: Semibold 16 pt, 21 pt in from the header's side; the value: Regular 32 pt, its ink 14 pt in from the
|
|
411
|
+
* field's (the screenshot's, both: the screen's code says 24); the swap button: 52 pt, its line 2 pt
|
|
412
|
+
*/
|
|
413
|
+
const CV_NAME_PX = 4.885 * F, CV_NAME_X = 10.74 * F, CV_VALUE_PX = 9.77 * F, CV_VALUE_X = 7.55 * F, CV_VALUE_Y = 45.1 * F;
|
|
414
|
+
const CV_CHIP = [52.11 * F, 59.75 * F, 15.88 * F] as const, CV_CHIP_LINE = 0.61 * F;
|
|
415
|
+
/** the chevrons (16 pt, 21 pt in from the header's right, as the screenshot's) and the swap arrows (20 pt), Lucide's, drawn with their 2-in-24 line */
|
|
416
|
+
const CV_CHEV = 4.885 * F, CV_CHEV_X = 104.45 * F, CV_ARROWS = 6.107 * F;
|
|
417
|
+
|
|
418
|
+
function drawConversion(c: DrawCtx, text: string): void {
|
|
419
|
+
const [from = '', to = '', value = ''] = text.split('\n').map(s => s.trim());
|
|
420
|
+
const headMid = (top: number) => top + CV_HEAD / 2;
|
|
421
|
+
// each header, its top rounded, its foot under what's below it: the result's grey, the field
|
|
422
|
+
rr(c, CV_X, CV_TO, CV_W, CV_RESULT - CV_TO + CV_R, CV_R); c.fillStyle = CONV.to; c.fill();
|
|
423
|
+
c.fillStyle = CONV.result; c.fillRect(CV_X, CV_RESULT, CV_W, L - CV_RESULT);
|
|
424
|
+
rr(c, CV_X, CV_FROM, CV_W, CV_HEAD + CV_R, CV_R); c.fillStyle = CONV.head; c.fill();
|
|
425
|
+
// the field: the page's dark, a Grey - 2 line round it (1 pt)
|
|
426
|
+
const fieldTop = CV_FROM + CV_HEAD, line = 0.305 * F;
|
|
427
|
+
c.fillStyle = CONV.page; c.fillRect(CV_X, fieldTop, CV_W, CV_FIELD - fieldTop);
|
|
428
|
+
c.lineWidth = line; c.strokeStyle = CONV.head; c.strokeRect(CV_X + line / 2, fieldTop, CV_W - line, CV_FIELD - fieldTop - line / 2);
|
|
429
|
+
// the units, white, and what was typed (or the field's hint)
|
|
430
|
+
c.textAlign = 'left'; c.textBaseline = 'middle'; c.fillStyle = TILE.name;
|
|
431
|
+
c.font = '600 ' + CV_NAME_PX + 'px ' + TILE_FONT;
|
|
432
|
+
c.fillText(fit(c, from, CV_CHEV_X - CV_NAME_X - 2 * F), CV_NAME_X, headMid(CV_FROM));
|
|
433
|
+
c.fillText(fit(c, to, CV_CHEV_X - CV_NAME_X - 2 * F), CV_NAME_X, headMid(CV_TO));
|
|
434
|
+
c.font = '400 ' + CV_VALUE_PX + 'px ' + TILE_FONT; c.textBaseline = 'alphabetic';
|
|
435
|
+
c.fillStyle = value ? TILE.name : CONV.hint;
|
|
436
|
+
c.fillText(fit(c, value || 'INPUT', CV_X + CV_W - CV_VALUE_X - 2 * F), CV_VALUE_X, CV_VALUE_Y);
|
|
437
|
+
// each header's chevron, and the swap button over where the two meet
|
|
438
|
+
c.lineCap = 'round'; c.lineJoin = 'round'; c.strokeStyle = TILE.name; c.lineWidth = (2 * CV_CHEV) / 24;
|
|
439
|
+
for (const top of [CV_FROM, CV_TO]) { icon(c, 'conv chevron', CHEVRON, CV_CHEV_X, headMid(top) - CV_CHEV / 2, CV_CHEV); c.stroke(); }
|
|
440
|
+
const [cx, cy, cs] = CV_CHIP;
|
|
441
|
+
rr(c, cx, cy, cs, cs, CV_R); c.fillStyle = CONV.chipLine; c.fill();
|
|
442
|
+
rr(c, cx + CV_CHIP_LINE, cy + CV_CHIP_LINE, cs - 2 * CV_CHIP_LINE, cs - 2 * CV_CHIP_LINE, CV_R - CV_CHIP_LINE); c.fillStyle = CONV.to; c.fill();
|
|
443
|
+
c.lineWidth = (2 * CV_ARROWS) / 24;
|
|
444
|
+
icon(c, 'conv arrows', ARROWS, cx + (cs - CV_ARROWS) / 2, cy + (cs - CV_ARROWS) / 2, CV_ARROWS); c.stroke();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Lucide's chevron-down and arrow-up-down, as polylines on their 24 grid. */
|
|
448
|
+
const CHEVRON: number[][] = [[6, 9, 12, 15, 18, 9]];
|
|
449
|
+
const ARROWS: number[][] = [[21, 16, 17, 20, 13, 16], [17, 20, 17, 4], [3, 8, 7, 4, 11, 8], [7, 4, 7, 20]];
|
|
450
|
+
/** An icon's path (polylines on a 24 grid), `size` across with its top left at x, y: kept, one a size. */
|
|
451
|
+
function icon(c: DrawCtx, key: string, lines: number[][], x: number, y: number, size: number): void {
|
|
452
|
+
const s = size / 24;
|
|
453
|
+
const draw = (cc: DrawCtx, X: number, Y: number) => {
|
|
454
|
+
cc.beginPath();
|
|
455
|
+
for (const p of lines) {
|
|
456
|
+
cc.moveTo(X + p[0] * s, Y + p[1] * s);
|
|
457
|
+
for (let i = 2; i < p.length; i += 2) cc.lineTo(X + p[i] * s, Y + p[i + 1] * s);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
if (c.shape) c.shape(key + ' ' + size, x, y, draw);
|
|
461
|
+
else draw(c, x, y);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Far out, a tile is a pin: its badge, a fixed size, ringed when it's selected (a picture's: the camera's or sketch's). */
|
|
111
465
|
function drawPin(c: DrawCtx, b: LinkBox, n: Note, o: LinkDrawOpts): void {
|
|
112
|
-
const s = LINK_PIN_PX;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
drawGlyph(c, glyphOf(n, o.info), b.cx, b.cy, 14, TH.ink2);
|
|
466
|
+
const s = LINK_PIN_PX, x = b.cx - s / 2, y = b.cy - s / 2, gone = o.info?.exists === false;
|
|
467
|
+
if (o.selected) {
|
|
468
|
+
rr(c, x, y, s, s, (6 * s) / 20);
|
|
469
|
+
c.lineWidth = 2 * SELECT_W; c.strokeStyle = TH.yellow; c.stroke();
|
|
470
|
+
}
|
|
471
|
+
drawBadge(c, tileTypeOf(n, o.info), x, y, s, gone ? TILE.line : undefined);
|
|
119
472
|
}
|
|
120
473
|
|
|
121
|
-
/** A picture filling
|
|
122
|
-
function cover(c: DrawCtx, im: ImageLike, x: number, y: number, w: number, h: number
|
|
474
|
+
/** A picture filling x, y, w, h (its middle shown, its sides cut off: by the card's edge, which the card has clipped to). */
|
|
475
|
+
function cover(c: DrawCtx, im: ImageLike, x: number, y: number, w: number, h: number): void {
|
|
123
476
|
const iw = im.naturalWidth, ih = im.naturalHeight || 1, k = Math.max(w / iw, h / ih), dw = iw * k, dh = ih * k;
|
|
124
|
-
c.save();
|
|
125
|
-
rr(c, x, y, w, h, r);
|
|
126
|
-
c.clip();
|
|
127
477
|
c.drawImage(im, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
|
|
128
|
-
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** A picture whole within x, y, w, h, in its middle. */
|
|
481
|
+
function contain(c: DrawCtx, im: ImageLike, x: number, y: number, w: number, h: number): void {
|
|
482
|
+
const iw = im.naturalWidth, ih = im.naturalHeight || 1, k = Math.min(w / iw, h / ih), dw = iw * k, dh = ih * k;
|
|
483
|
+
c.drawImage(im, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
|
|
129
484
|
}
|
|
130
485
|
|
|
131
486
|
/*
|
|
132
|
-
A name
|
|
133
|
-
|
|
134
|
-
(the strip under a picture always has the room by then), and on one once one does. Each name is measured once, at
|
|
135
|
-
each of its two sizes: laying it out again at another width, every step of a pinch, measures nothing.
|
|
487
|
+
A name is one size on the screen, as the bar it's on is; the card, wider on the screen as it's zoomed in, shows more
|
|
488
|
+
of it. Each name is measured once: fitting it again at another width, every step of a pinch, measures nothing.
|
|
136
489
|
*/
|
|
137
|
-
const NAME_PX = 11, NAME_MAX_PX = 14, NAME_LINE = 1.25;
|
|
138
490
|
|
|
139
491
|
/** A name's prefixes' widths at a font (w[i]: its first i characters), and an ellipsis's. */
|
|
140
492
|
const prefixes = new Map<string, { w: number[]; ell: number }>();
|
|
@@ -150,83 +502,19 @@ function widthsOf(c: DrawCtx, text: string): { w: number[]; ell: number } {
|
|
|
150
502
|
return p;
|
|
151
503
|
}
|
|
152
504
|
|
|
505
|
+
/** Where to cut `text` at `i` without parting a character in two (an emoji's surrogate pair): there, or one before. */
|
|
506
|
+
const cutAt = (text: string, i: number): number => (i > 0 && (text.charCodeAt(i - 1) & 0xfc00) === 0xd800 ? i - 1 : i);
|
|
507
|
+
|
|
153
508
|
/** The most of `text` that fits `maxW` at the current font: all of it, or its start and an ellipsis. */
|
|
154
509
|
export function fitName(c: DrawCtx, text: string, maxW: number): string {
|
|
155
510
|
const { w, ell } = widthsOf(c, text), n = text.length;
|
|
156
511
|
if (w[n] <= maxW) return text;
|
|
157
512
|
let lo = 0, hi = n;
|
|
158
513
|
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (w[mid] + ell <= maxW) lo = mid; else hi = mid - 1; }
|
|
159
|
-
return text.slice(0, lo).trimEnd() + '…';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Where a name breaks onto two lines: at the space nearest half its width (with none, after the _ - . or / nearest it;
|
|
164
|
-
* with none of those, halfway across), so the two are even — and the same however wide its tile, so zooming never
|
|
165
|
-
* moves the break. `width`: the wider of the two lines.
|
|
166
|
-
*/
|
|
167
|
-
const splits = new Map<string, { cut: number; next: number; width: number }>();
|
|
168
|
-
function splitOf(c: DrawCtx, text: string): { cut: number; next: number; width: number } {
|
|
169
|
-
const key = c.font + '|' + text;
|
|
170
|
-
let sp = splits.get(key);
|
|
171
|
-
if (sp) return sp;
|
|
172
|
-
const { w } = widthsOf(c, text), n = text.length, total = w[n];
|
|
173
|
-
sp = { cut: 0, next: 0, width: Infinity };
|
|
174
|
-
for (let i = 1; i < n - 1; i++) {
|
|
175
|
-
if (text[i] !== ' ') continue;
|
|
176
|
-
const width = Math.max(w[i], total - w[i + 1]);
|
|
177
|
-
if (width < sp.width) sp = { cut: i, next: i + 1, width };
|
|
178
|
-
}
|
|
179
|
-
// (no space, as a file's name often has none: after a _ - . or /, which stays on the first line)
|
|
180
|
-
if (!sp.cut) for (let i = 2; i < n - 1; i++) {
|
|
181
|
-
if (!'_-./'.includes(text[i - 1])) continue;
|
|
182
|
-
const width = Math.max(w[i], total - w[i]);
|
|
183
|
-
if (width < sp.width) sp = { cut: i, next: i, width };
|
|
184
|
-
}
|
|
185
|
-
if (!sp.cut) {
|
|
186
|
-
let i = 1;
|
|
187
|
-
while (i < n - 1 && w[i] < total / 2) i++;
|
|
188
|
-
sp = { cut: i, next: i, width: Math.max(w[i], total - w[i]) };
|
|
189
|
-
}
|
|
190
|
-
if (splits.size > 1000) splits.clear();
|
|
191
|
-
splits.set(key, sp);
|
|
192
|
-
return sp;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** All of `text` on two lines `maxW` wide, evenly (see splitOf); null if one line takes it, or two can't. */
|
|
196
|
-
export function twoLines(c: DrawCtx, text: string, maxW: number): [string, string] | null {
|
|
197
|
-
const n = text.length;
|
|
198
|
-
if (n < 2 || widthsOf(c, text).w[n] <= maxW) return null;
|
|
199
|
-
const sp = splitOf(c, text);
|
|
200
|
-
return sp.width <= maxW ? [text.slice(0, sp.cut).trimEnd(), text.slice(sp.next).trimStart()] : null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A name on card `k` (its scale from the layout), centred on `y` and at most `maxW` wide in the layout: the layout's
|
|
205
|
-
* size until that's NAME_MAX_PX on the screen; past it, that size on the screen (drawn at the screen's own scale, so one
|
|
206
|
-
* font does), showing more of the name as the card grows — and with `wrap`, on two lines once two show all of it.
|
|
207
|
-
*/
|
|
208
|
-
function drawName(c: DrawCtx, k: number, text: string, y: number, maxW: number, color: string, wrap: boolean): void {
|
|
209
|
-
c.fillStyle = color; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
210
|
-
if (NAME_PX * k <= NAME_MAX_PX) {
|
|
211
|
-
c.font = '600 ' + NAME_PX + 'px ' + FONT;
|
|
212
|
-
c.fillText(fitName(c, text, maxW), L / 2, y);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
c.save();
|
|
216
|
-
c.scale(1 / k, 1 / k);
|
|
217
|
-
c.font = '600 ' + NAME_MAX_PX + 'px ' + FONT;
|
|
218
|
-
const W = maxW * k, x = (L / 2) * k, one = fitName(c, text, W);
|
|
219
|
-
const two = wrap && one !== text ? twoLines(c, text, W) : null;
|
|
220
|
-
if (two) {
|
|
221
|
-
// (centred in the strip under the picture: from 68 to the card's foot)
|
|
222
|
-
const h = NAME_MAX_PX * NAME_LINE, mid = 82 * k;
|
|
223
|
-
c.fillText(two[0], x, mid - h / 2);
|
|
224
|
-
c.fillText(two[1], x, mid + h / 2);
|
|
225
|
-
} else c.fillText(one, x, y * k);
|
|
226
|
-
c.restore();
|
|
514
|
+
return text.slice(0, cutAt(text, lo)).trimEnd() + '…';
|
|
227
515
|
}
|
|
228
516
|
|
|
229
|
-
/**
|
|
517
|
+
/** Text cut to fit, by font, width and text: measured once, not every frame. */
|
|
230
518
|
const fitted = new Map<string, string>();
|
|
231
519
|
function fit(c: DrawCtx, text: string, maxW: number): string {
|
|
232
520
|
const key = c.font + '|' + maxW + '|' + text;
|
|
@@ -236,62 +524,20 @@ function fit(c: DrawCtx, text: string, maxW: number): string {
|
|
|
236
524
|
if (c.measureText(text).width > maxW) {
|
|
237
525
|
let lo = 0, hi = text.length;
|
|
238
526
|
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (c.measureText(text.slice(0, mid) + '…').width <= maxW) lo = mid; else hi = mid - 1; }
|
|
239
|
-
out = text.slice(0, lo).trimEnd() + '…';
|
|
527
|
+
out = text.slice(0, cutAt(text, lo)).trimEnd() + '…';
|
|
240
528
|
}
|
|
241
529
|
if (fitted.size > 1000) fitted.clear();
|
|
242
530
|
fitted.set(key, out);
|
|
243
531
|
return out;
|
|
244
532
|
}
|
|
245
533
|
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
};
|
|
255
|
-
const rules = (c: DrawCtx, ys: number[]) => { for (const y of ys) { c.moveTo(9, y); c.lineTo(15, y); } };
|
|
256
|
-
const dots = (c: DrawCtx, at: [number, number][], r: number) => { for (const [x, y] of at) { c.moveTo(x + r, y); c.arc(x, y, r, 0, Math.PI * 2); } };
|
|
257
|
-
|
|
258
|
-
const GLYPHS: Record<Glyph, GlyphDraw> = {
|
|
259
|
-
photo: {
|
|
260
|
-
stroke: c => { c.roundRect(3, 5, 18, 14, 2); c.moveTo(5, 17); c.lineTo(10, 11); c.lineTo(13, 14); c.lineTo(15, 12); c.lineTo(19, 17); },
|
|
261
|
-
fill: c => dots(c, [[16, 9]], 1.6),
|
|
262
|
-
},
|
|
263
|
-
pdf: { stroke: c => page(c, true), text: 'PDF' },
|
|
264
|
-
note: { stroke: c => { page(c, false); rules(c, [8, 12, 16]); } },
|
|
265
|
-
calculator: {
|
|
266
|
-
stroke: c => { c.roundRect(5, 3, 14, 18, 2); c.moveTo(8, 7); c.lineTo(16, 7); },
|
|
267
|
-
fill: c => dots(c, [11.5, 15, 18.5].flatMap(y => [8.5, 12, 15.5].map(x => [x, y] as [number, number])), 0.9),
|
|
268
|
-
},
|
|
269
|
-
label: {
|
|
270
|
-
stroke: c => { c.moveTo(4, 8); c.lineTo(14, 8); c.lineTo(20, 12); c.lineTo(14, 16); c.lineTo(4, 16); c.closePath(); },
|
|
271
|
-
fill: c => dots(c, [[14, 12]], 1.2),
|
|
272
|
-
},
|
|
273
|
-
drawing: { stroke: c => { page(c, false); c.moveTo(9, 17); c.lineTo(15.5, 8.5); } },
|
|
274
|
-
tape: { stroke: c => { c.roundRect(2.5, 8, 19, 8, 1.5); for (const x of [6, 9, 12, 15, 18]) { c.moveTo(x, 8); c.lineTo(x, x === 12 ? 12.5 : 11); } } },
|
|
275
|
-
removed: { dashed: c => page(c, false), stroke: c => { c.moveTo(4, 20); c.lineTo(20, 4); } },
|
|
276
|
-
document: { stroke: c => { page(c, true); rules(c, [12, 16]); } },
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
/** A glyph part's path: kept as a shape where the engine can (a tile's icon is drawn every frame, the same each time). */
|
|
280
|
-
function part(c: DrawCtx, key: string, draw: (c: DrawCtx) => void): void {
|
|
281
|
-
if (c.shape) c.shape(key, 0, 0, cc => draw(cc));
|
|
282
|
-
else { c.beginPath(); draw(c); }
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** An icon for kind `g`, `size` across, centred on cx, cy (paths: the canvas can't draw the chrome's SVG icons). */
|
|
286
|
-
function drawGlyph(c: DrawCtx, g: Glyph, cx: number, cy: number, size: number, color: string): void {
|
|
287
|
-
const u = size / 24, d = GLYPHS[g];
|
|
288
|
-
c.save();
|
|
289
|
-
c.translate(cx - size / 2, cy - size / 2);
|
|
290
|
-
c.scale(u, u);
|
|
291
|
-
c.strokeStyle = color; c.fillStyle = color; c.lineWidth = 1.8; c.lineCap = 'round'; c.lineJoin = 'round';
|
|
292
|
-
if (d.dashed) { c.setLineDash([2.2, 2]); part(c, 'glyph- ' + g, d.dashed); c.stroke(); c.setLineDash([]); }
|
|
293
|
-
if (d.stroke) { part(c, 'glyph| ' + g, d.stroke); c.stroke(); }
|
|
294
|
-
if (d.fill) { part(c, 'glyph. ' + g, d.fill); c.fill(); }
|
|
295
|
-
if (d.text) { c.font = '700 6px ' + FONT; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(d.text, 12, 15); }
|
|
296
|
-
c.restore();
|
|
534
|
+
/**
|
|
535
|
+
* In a browser, a font can arrive after a tile's text was measured in another (Inter, still loading, the first time a
|
|
536
|
+
* page draws): measured again once it has. (A device draws in the fonts it's given.)
|
|
537
|
+
*/
|
|
538
|
+
let watchingFonts = false;
|
|
539
|
+
function watchFonts(): void {
|
|
540
|
+
watchingFonts = true;
|
|
541
|
+
const fonts = typeof document !== 'undefined' ? document.fonts : undefined;
|
|
542
|
+
fonts?.addEventListener?.('loadingdone', () => { prefixes.clear(); fitted.clear(); rowCache.clear(); requestDraw(); });
|
|
297
543
|
}
|