@reekon-tools/boldr-cad 1.5.0 → 1.7.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 +12 -1
- package/dist/app/state.d.ts +3 -3
- package/dist/data/provider.d.ts +2 -1
- package/dist/data/sync.js +4 -1
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.js +122 -19
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/advance.d.ts +2 -0
- package/dist/editor/advance.js +23 -0
- package/dist/editor/advance.js.map +1 -0
- package/dist/editor/dimSize.d.ts +13 -0
- package/dist/editor/dimSize.js +50 -0
- package/dist/editor/dimSize.js.map +1 -0
- package/dist/editor/gestures.js +30 -7
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keypad.d.ts +8 -0
- package/dist/editor/keypad.js +15 -1
- package/dist/editor/keypad.js.map +1 -1
- package/dist/editor/keys.js +2 -1
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +13 -2
- package/dist/editor/laser.js +42 -14
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +29 -11
- package/dist/editor/linkActions.js +76 -29
- 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 +7 -5
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/ports.d.ts +9 -0
- package/dist/editor/ports.js +2 -0
- package/dist/editor/ports.js.map +1 -1
- package/dist/editor/selection.js +1 -1
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +1 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/model/clockwise.d.ts +11 -0
- package/dist/model/clockwise.js +123 -0
- package/dist/model/clockwise.js.map +1 -0
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.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/links.d.ts +6 -1
- package/dist/model/links.js +6 -1
- package/dist/model/links.js.map +1 -1
- package/dist/model/scale.js +3 -0
- package/dist/model/scale.js.map +1 -1
- package/dist/model/settings.d.ts +2 -0
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +5 -1
- package/dist/model/sketch.js +19 -18
- package/dist/model/sketch.js.map +1 -1
- 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 +24 -2
- package/dist/react/BottomBar.js +94 -25
- 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 +3 -2
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/PlanCanvas.native.js +15 -9
- 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/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.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/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 +7 -1
- package/dist/react/host.js +12 -2
- package/dist/react/host.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 +10 -4
- 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 +7 -0
- package/dist/render/draw.js +41 -15
- package/dist/render/draw.js.map +1 -1
- package/dist/render/linkTile.js +2 -2
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/navigate.d.ts +11 -6
- package/dist/render/navigate.js +34 -20
- package/dist/render/navigate.js.map +1 -1
- package/dist/render/snap.d.ts +5 -0
- package/dist/render/snap.js +84 -14
- package/dist/render/snap.js.map +1 -1
- package/dist/render/view.js +7 -2
- package/dist/render/view.js.map +1 -1
- package/dist/solver/equations.js +30 -16
- package/dist/solver/equations.js.map +1 -1
- package/dist/tools/dim.d.ts +6 -0
- package/dist/tools/dim.js +28 -4
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/modify.d.ts +32 -4
- package/dist/tools/modify.js +247 -97
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +8 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +11 -8
- package/dist/tools/select.js +51 -19
- package/dist/tools/select.js.map +1 -1
- package/package.json +1 -1
- package/src/app/state.ts +3 -3
- package/src/data/provider.ts +2 -2
- package/src/data/sync.ts +4 -1
- package/src/editor/actions.ts +98 -16
- package/src/editor/advance.ts +23 -0
- package/src/editor/dimSize.ts +53 -0
- package/src/editor/gestures.ts +31 -9
- package/src/editor/keypad.ts +17 -1
- package/src/editor/keys.ts +2 -1
- package/src/editor/laser.ts +45 -14
- package/src/editor/linkActions.ts +87 -26
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +7 -5
- package/src/editor/ports.ts +10 -0
- package/src/editor/selection.ts +1 -1
- package/src/editor/tooltips.ts +1 -0
- package/src/index.ts +2 -2
- package/src/model/clockwise.ts +121 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/geom.ts +10 -0
- package/src/model/links.ts +6 -1
- package/src/model/scale.ts +2 -0
- package/src/model/settings.ts +3 -1
- package/src/model/sketch.ts +14 -7
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +19 -2
- package/src/react/BottomBar.tsx +109 -25
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +3 -2
- package/src/react/PlanCanvas.native.tsx +25 -12
- package/src/react/PlanCanvas.web.tsx +17 -5
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +12 -2
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +10 -4
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +38 -16
- package/src/render/linkTile.ts +2 -2
- package/src/render/navigate.ts +31 -20
- package/src/render/snap.ts +80 -15
- package/src/render/view.ts +6 -2
- package/src/solver/equations.ts +30 -16
- package/src/tools/dim.ts +28 -4
- package/src/tools/modify.ts +205 -69
- package/src/tools/registry.ts +8 -0
- package/src/tools/select.ts +47 -20
- package/src/ui/domPorts.ts +2 -0
- 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
|
@@ -3,20 +3,20 @@ import { ed } from '../app/state';
|
|
|
3
3
|
import { clamp } from '../model/geom';
|
|
4
4
|
import { pushHistory } from '../model/history';
|
|
5
5
|
import { uid } from '../model/ids';
|
|
6
|
-
import { isLink, LINK_COLOR, LINK_MAX_IN, LINK_MIN_IN, LINK_TILE_PX, linkInfoOf, linkName, linkSide, linkSnapshot } from '../model/links';
|
|
6
|
+
import { isLink, LINK_COLOR, LINK_HARD_MAX_IN, LINK_HARD_MIN_IN, LINK_MAX_IN, LINK_MIN_IN, LINK_SIDE_IN, LINK_TILE_PX, linkInfoOf, linkName, linkSide, linkSnapshot } from '../model/links';
|
|
7
7
|
import { dimMeasurement } from '../model/measurement';
|
|
8
8
|
import { imageLinkAt, linkAt } from '../render/hit';
|
|
9
9
|
import { askConfirm } from './confirm';
|
|
10
10
|
import { imageById } from '../model/images';
|
|
11
11
|
import { settings } from '../model/settings';
|
|
12
12
|
import { page, SK, store } from '../model/store';
|
|
13
|
-
import type { LinkTarget, Note, Underlay } from '../model/types';
|
|
13
|
+
import type { LinkTarget, Note, Sketch, Underlay } from '../model/types';
|
|
14
14
|
import { tileTypeOf } from '../render/linkTile';
|
|
15
15
|
import { cvs, linkBox, linkThumb, mainT, requestDraw, s2w, underlayMid } from '../render/view';
|
|
16
16
|
import { setTool } from '../tools/registry';
|
|
17
17
|
import { elevOpen } from './elevation';
|
|
18
18
|
import { is3d } from './view3dState';
|
|
19
|
-
import { canLink, linksHost, openLink, pickLinks } from './links';
|
|
19
|
+
import { besideOpen, canLink, canOpenBeside, linksHost, openLink, openLinkBeside, pickLinks } from './links';
|
|
20
20
|
import { toast } from './ports';
|
|
21
21
|
|
|
22
22
|
/*
|
|
@@ -68,6 +68,8 @@ export async function addLinks(): Promise<void> {
|
|
|
68
68
|
let k = 0;
|
|
69
69
|
while (k < 40 && spots.some(([x, y]) => taken(x + k * step, y - k * step))) k++;
|
|
70
70
|
pushHistory();
|
|
71
|
+
// (the page's first tiles: the size they're placed at is their 100%)
|
|
72
|
+
if (pageLinkSize(sk) == null) sk.tileRef = ts;
|
|
71
73
|
const ids = picked.map((t, i) => {
|
|
72
74
|
const n: Note = { id: uid('lk'), type: 'link', x: spots[i][0] + k * step, y: spots[i][1] - k * step, ts, color: LINK_COLOR, link: linkSnapshot(t) };
|
|
73
75
|
sk.notes.push(n);
|
|
@@ -83,7 +85,9 @@ const LINK_GAP_PX = 16;
|
|
|
83
85
|
|
|
84
86
|
/*
|
|
85
87
|
Every tile on a page is one size, on the drawing (so it zooms with the plan and prints at that size): the Tile size
|
|
86
|
-
slider above the bar, shown while a tile is selected, sizes them all together.
|
|
88
|
+
slider above the bar, shown while a tile is selected, sizes them all together. It says how big in percent of the size
|
|
89
|
+
they were first placed at (the user's call, 2026-11-03): a tile has no real size, and before the page's first length
|
|
90
|
+
its inches are only placeholders, so feet and metres said nothing true.
|
|
87
91
|
*/
|
|
88
92
|
|
|
89
93
|
/** The size of the page's tiles, inches on the drawing (null: it has none). */
|
|
@@ -99,21 +103,44 @@ export function linkSizeVisible(): boolean {
|
|
|
99
103
|
return ed.sel.some(x => x.type === 'note' && sk.notes.find(n => n.id === x.id)?.type === 'link');
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/** What the page's tiles call 100%, inches on the drawing: the size they were first placed at (a page from before: theirs now). */
|
|
107
|
+
const tileRef = (sk: Sketch): number => sk.tileRef ?? pageLinkSize(sk) ?? LINK_SIDE_IN;
|
|
108
|
+
|
|
109
|
+
/** How big the page's tiles are, in percent of the size they were first placed at (null: it has none). */
|
|
110
|
+
export function pageTilePct(sk = SK()): number | null {
|
|
111
|
+
const s = pageLinkSize(sk);
|
|
112
|
+
return s == null ? null : Math.round((s / tileRef(sk)) * 100);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The slider's range, percent. */
|
|
116
|
+
export const TILE_PCT_MIN = 10, TILE_PCT_MAX = 1000;
|
|
117
|
+
|
|
102
118
|
/**
|
|
103
|
-
* Size every tile on the page
|
|
104
|
-
*
|
|
119
|
+
* Size every tile on the page to `pct` percent (TILE_PCT_MIN to TILE_PCT_MAX; and never past LINK_HARD_MIN_IN to
|
|
120
|
+
* LINK_HARD_MAX_IN on the drawing — placing's 6"–40' bounds would cut the range short), drawn at once — for the slider as
|
|
121
|
+
* it moves. The caller makes the undo step before, and finishes the edit after. (A page from before keeps its 100% from
|
|
122
|
+
* here on.)
|
|
105
123
|
*/
|
|
106
|
-
export function
|
|
107
|
-
const v =
|
|
108
|
-
|
|
124
|
+
export function setTilePct(pct: number): void {
|
|
125
|
+
const sk = SK(), ref = tileRef(sk), v = clamp((ref * clamp(pct, TILE_PCT_MIN, TILE_PCT_MAX)) / 100, LINK_HARD_MIN_IN, LINK_HARD_MAX_IN);
|
|
126
|
+
sk.tileRef = ref;
|
|
127
|
+
for (const n of sk.notes) if (isLink(n)) n.ts = v;
|
|
109
128
|
requestDraw();
|
|
110
129
|
}
|
|
111
130
|
|
|
112
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* The slider runs along a log scale (each step as noticeable as the last), 0 to SIZE_STEPS, 100% in the middle; it
|
|
133
|
+
* catches at 100%, so the size they came in at is easy to find again, and says round numbers: whole percents to 200%,
|
|
134
|
+
* then fives, then tens from 500%.
|
|
135
|
+
*/
|
|
113
136
|
export const SIZE_STEPS = 1000;
|
|
114
|
-
const LOG_MIN = Math.log(
|
|
115
|
-
export const
|
|
116
|
-
export
|
|
137
|
+
const LOG_MIN = Math.log(TILE_PCT_MIN), LOG_SPAN = Math.log(TILE_PCT_MAX) - LOG_MIN;
|
|
138
|
+
export const pctToStep = (pct: number): number => Math.round(((Math.log(clamp(pct, TILE_PCT_MIN, TILE_PCT_MAX)) - LOG_MIN) / LOG_SPAN) * SIZE_STEPS);
|
|
139
|
+
export function stepToPct(step: number): number {
|
|
140
|
+
const p = Math.exp(LOG_MIN + (clamp(step, 0, SIZE_STEPS) / SIZE_STEPS) * LOG_SPAN);
|
|
141
|
+
if (Math.abs(p - 100) < 4) return 100;
|
|
142
|
+
return p >= 500 ? Math.round(p / 10) * 10 : p >= 200 ? Math.round(p / 5) * 5 : Math.round(p);
|
|
143
|
+
}
|
|
117
144
|
|
|
118
145
|
/** A tile's leader on or off. On, it points out beside the tile (to the right, or the left near the screen's edge). */
|
|
119
146
|
export function toggleLeader(id: string): void {
|
|
@@ -129,27 +156,57 @@ export function toggleLeader(id: string): void {
|
|
|
129
156
|
afterEdit();
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
/*
|
|
160
|
+
Open, for what a link is for: the host's own way (another screen, a sheet over the drawing), or — the user's call,
|
|
161
|
+
2026-10-30 — in a pane beside the drawing, a split view, where the host has one (CadLinks.openBeside). The bar's Open
|
|
162
|
+
asks which while no pane is open (opensWithChoice); once one is, Open goes to it.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/** How Open opens: `here`, the host's own way; `beside`, in a pane beside the drawing; `auto`, beside if one's open. */
|
|
166
|
+
export type OpenHow = 'auto' | 'here' | 'beside';
|
|
167
|
+
|
|
132
168
|
/** Open what the selected tile, or the selected expanded photo, is for. */
|
|
133
|
-
export function openSelectedLink(): void {
|
|
169
|
+
export function openSelectedLink(how: OpenHow = 'auto'): void {
|
|
170
|
+
const t = selectedLink()?.link ?? selectedLinkedImage()?.link;
|
|
171
|
+
if (t) openLinkTarget(t, how);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** What the bar's Open is for: the selected tile's or expanded photo's link, or the selected linked dimension's. */
|
|
175
|
+
export function selectedOpenTarget(): LinkTarget | null {
|
|
134
176
|
const t = selectedLink()?.link ?? selectedLinkedImage()?.link;
|
|
135
|
-
if (t)
|
|
177
|
+
if (t) return t;
|
|
178
|
+
const s = ed.sel;
|
|
179
|
+
return s.length === 1 && s[0].type === 'dim' ? SK().cons[s[0].id]?.measurementRef ?? null : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether Open asks how to open `t`: the host can show it beside the drawing, and no pane's open yet. */
|
|
183
|
+
export function opensWithChoice(t: LinkTarget): boolean {
|
|
184
|
+
return canOpenLink(t) && canOpenBeside(t) && !besideOpen();
|
|
136
185
|
}
|
|
137
186
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
/** Open what `t` is for, `how` it's asked to (see OpenHow); one that's gone, or can't be, says so instead. */
|
|
188
|
+
export function openLinkTarget(t: LinkTarget, how: OpenHow = 'auto'): void {
|
|
189
|
+
if (!canOpenLink(t)) {
|
|
190
|
+
const gone = linkInfoOf(t)?.exists === false;
|
|
191
|
+
toast(!gone ? 'That can’t be opened from here.'
|
|
192
|
+
: t.kind === 'measurement' ? 'That measurement has been removed from the job, so it can’t be opened.'
|
|
193
|
+
: 'That’s been removed from the job, so it can’t be opened.', { warn: true });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if ((how === 'beside' || (how === 'auto' && besideOpen())) && canOpenBeside(t)) openLinkBeside(t);
|
|
197
|
+
else openLink(t);
|
|
141
198
|
}
|
|
142
199
|
|
|
143
|
-
/** An expanded photo's badge, tapped: what it's for opens. */
|
|
200
|
+
/** An expanded photo's badge, tapped: what it's for opens (beside the drawing, if a pane's open there). */
|
|
144
201
|
export function openImageLink(id: string): void {
|
|
145
202
|
const t = imageById(id)?.link;
|
|
146
|
-
if (t)
|
|
203
|
+
if (t) openLinkTarget(t);
|
|
147
204
|
}
|
|
148
205
|
|
|
149
206
|
/*
|
|
150
207
|
A photo's tile expands into the photo whole, and back (the user's call, 2026-10-26: "for image objects … you should
|
|
151
208
|
be able to expand and condense the tile … this is how skia works, it expands the image to full size + shows a link
|
|
152
|
-
icon in the corner"). Expanded, it's one of the page's images — moved, sized,
|
|
209
|
+
icon in the corner"). Expanded, it's one of the page's images — moved, sized, cropped, turned, faded and locked as
|
|
153
210
|
any is — with the link's badge in its corner; Expand in the bar (on) folds it back into its tile where it is. An image
|
|
154
211
|
from the computer has no link, so none of this. Each way is one undo step, and what was selected stays selected.
|
|
155
212
|
*/
|
|
@@ -198,6 +255,8 @@ export function collapseLink(id: string): void {
|
|
|
198
255
|
const n: Note = { ...(was ?? { id: '', type: 'link', color: LINK_COLOR }), type: 'link', x: m.x, y: m.y, ts, link: u.link };
|
|
199
256
|
if (!n.id || sk.notes.some(x => x.id === n.id)) n.id = uid('lk');
|
|
200
257
|
pushHistory();
|
|
258
|
+
// (the page's only tile, back: its size is the 100%, unless the page already has one)
|
|
259
|
+
if (pageLinkSize(sk) == null && sk.tileRef == null) sk.tileRef = ts;
|
|
201
260
|
page().underlays.splice(page().underlays.indexOf(u), 1);
|
|
202
261
|
sk.notes.push(n);
|
|
203
262
|
ed.sel = [{ type: 'note', id: n.id }];
|
|
@@ -220,7 +279,11 @@ export function readOnlyTap(sx: number, sy: number, touch: boolean): void {
|
|
|
220
279
|
askConfirm({
|
|
221
280
|
title: 'Open ' + linkName(t) + '?',
|
|
222
281
|
msg: where ? 'It’s in ' + where + '.' : undefined,
|
|
223
|
-
choices: [
|
|
282
|
+
choices: [
|
|
283
|
+
{ label: 'Open', run: () => openLinkTarget(t, 'here') },
|
|
284
|
+
...(canOpenBeside(t) ? [{ label: 'Open side by side', run: () => openLinkTarget(t, 'beside') }] : []),
|
|
285
|
+
{ label: 'Cancel', run: () => {} },
|
|
286
|
+
],
|
|
224
287
|
});
|
|
225
288
|
}
|
|
226
289
|
|
|
@@ -234,11 +297,9 @@ export function unlinkDim(id: string): void {
|
|
|
234
297
|
}
|
|
235
298
|
|
|
236
299
|
/** Open the measurement a dimension is linked to. */
|
|
237
|
-
export function openDimLink(id: string): void {
|
|
300
|
+
export function openDimLink(id: string, how: OpenHow = 'auto'): void {
|
|
238
301
|
const t = SK().cons[id]?.measurementRef;
|
|
239
|
-
if (
|
|
240
|
-
if (!canOpenLink(t)) { toast(linkInfoOf(t)?.exists === false ? 'That measurement has been removed from the job, so it can’t be opened.' : 'That can’t be opened from here.', { warn: true }); return; }
|
|
241
|
-
openLink(t);
|
|
302
|
+
if (t) openLinkTarget(t, how);
|
|
242
303
|
}
|
|
243
304
|
|
|
244
305
|
/**
|
package/src/editor/links.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { linkInfo, linkKey, linkTargets } from '../model/links';
|
|
1
|
+
import { linkInfo, linkInfoOf, linkKey, linkTargets } from '../model/links';
|
|
2
2
|
import { onSessionReset } from '../model/session';
|
|
3
3
|
import { store } from '../model/store';
|
|
4
4
|
import type { LinkInfo, LinkTarget } from '../model/types';
|
|
@@ -67,6 +67,13 @@ export interface CadLinks {
|
|
|
67
67
|
subscribe(targets: LinkTarget[], cb: (info: Record<string, LinkInfo>) => void): () => void;
|
|
68
68
|
/** Open what `target` is for (another screen, or a sheet over the drawing). */
|
|
69
69
|
open(target: LinkTarget): void;
|
|
70
|
+
/**
|
|
71
|
+
* Open what `target` is for in a pane beside the drawing (a split view), the drawing staying as it is. With this, the
|
|
72
|
+
* bar's Open asks how — Open, or Open side by side — until a pane is open (besideOpen), when Open goes to it.
|
|
73
|
+
*/
|
|
74
|
+
openBeside?(target: LinkTarget): void;
|
|
75
|
+
/** Whether a pane is open beside the drawing now (with openBeside). */
|
|
76
|
+
besideOpen?(): boolean;
|
|
70
77
|
/** Change a linked measurement (micrometres, or degrees), the person having said to; `capture` as saveMeasurement's. */
|
|
71
78
|
update?(target: LinkTarget, value: number, capture?: unknown): void;
|
|
72
79
|
/** Save a dimension as a new measurement in the drawing's own group; resolves with it (null: not saved). */
|
|
@@ -179,3 +186,12 @@ export async function pickJobMeasurement(kind: MeasurementKind): Promise<LinkTar
|
|
|
179
186
|
|
|
180
187
|
/** Open what a link is for. */
|
|
181
188
|
export function openLink(t: LinkTarget): void { host?.open(t); }
|
|
189
|
+
|
|
190
|
+
/** Open what a link is for in a pane beside the drawing (CadLinks.openBeside). */
|
|
191
|
+
export function openLinkBeside(t: LinkTarget): void { host?.openBeside?.(t); }
|
|
192
|
+
|
|
193
|
+
/** Whether the host can show `t` in a pane beside the drawing. */
|
|
194
|
+
export function canOpenBeside(t: LinkTarget): boolean { return !!host?.openBeside && linkInfoOf(t)?.canOpenBeside !== false; }
|
|
195
|
+
|
|
196
|
+
/** Whether a pane is open beside the drawing now. */
|
|
197
|
+
export function besideOpen(): boolean { return !!host?.besideOpen?.(); }
|
package/src/editor/mode.ts
CHANGED
|
@@ -58,11 +58,12 @@ export const railIcon = (id: string): string => WALL_ICON[id] ?? id;
|
|
|
58
58
|
/** The tool rail for each mode. 'toggle' items switch something on and off; 'act' items run an action. */
|
|
59
59
|
export const RAIL: Record<Mode, RailItem[]> = {
|
|
60
60
|
draft: [
|
|
61
|
-
// drawing; then the building (walls, what goes in them, spaces);
|
|
62
|
-
|
|
61
|
+
// drawing, and Trim last of it (the user's call, 2026-11-03); then the building (walls, what goes in them, spaces);
|
|
62
|
+
// then guides
|
|
63
|
+
['select', 'Select', 'V'], ['line', 'Line', 'L'], ['freehand', 'Draw to shape', 'S'], ['rect', 'Rectangle', 'R'], ['circle', 'Circle', 'C'], ['arc', 'Arc', 'A'], ['trim', 'Trim', 'T'], '|',
|
|
63
64
|
['door', 'Door', 'B'], ['window', 'Window', 'N'], ['room', 'Space', 'M'], '|',
|
|
64
|
-
//
|
|
65
|
-
//
|
|
65
|
+
// Offset (O) and Dimension (D) stay on the keyboard; Offset is also in the bar for a selected line, and measuring is
|
|
66
|
+
// the yellow + for what's selected (the user's call, 2026-10-24: no Dimension on the rail).
|
|
66
67
|
// The toggles together: what's drawn is walls, or guides (one or the other, never both)
|
|
67
68
|
['wall', 'Walls', 'W', 'toggle'], ['constr', 'Construction lines', 'X', 'toggle'], '|',
|
|
68
69
|
['undo', 'Undo', '⌘Z', 'act'], ['redo', 'Redo', '⇧⌘Z', 'act'],
|
|
@@ -79,7 +80,8 @@ export const RAIL: Record<Mode, RailItem[]> = {
|
|
|
79
80
|
/**
|
|
80
81
|
* Tools that share one place on a phone's rail, which has no room for each: a tap draws with the one used last (the
|
|
81
82
|
* place shows it), and a long press opens a menu of them all (react/ShapeMenu.tsx) — Draft's Line, Rectangle, Circle
|
|
82
|
-
* and Arc, and Annotate's markup shapes. A tablet's or a browser's rail shows every one in its own place.
|
|
83
|
+
* and Arc, and Annotate's markup shapes. A tablet's or a browser's rail shows every one in its own place. (Trim isn't
|
|
84
|
+
* one of them: on a phone it has its own place, under Draw to shape — the user's call, 2026-11-03.)
|
|
83
85
|
*/
|
|
84
86
|
export const SHAPE_TOOLS = ['line', 'rect', 'circle', 'arc'];
|
|
85
87
|
export const SHAPES_GROUP = 'shapes';
|
package/src/editor/ports.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface KeypadSpec {
|
|
|
38
38
|
extras?: () => KeypadExtra[];
|
|
39
39
|
/** the value; `capture`, a laser reading's record (laserReading's), when it's that reading applied as it came in */
|
|
40
40
|
onDone?: (v: number, capture?: unknown) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Whether `v` (as onDone takes it) would replace a value measured there: set by a keypad opened on a dimension, so a
|
|
43
|
+
* host that warns before a reading overwrites can ask, where the host's keypad applies a reading at once
|
|
44
|
+
* (laserReadingOverwrites).
|
|
45
|
+
*/
|
|
46
|
+
overwrites?: (v: number) => boolean;
|
|
41
47
|
/**
|
|
42
48
|
* A line's length, or a dimension's angle: the job's measurements are offered with the keys (a page swiped to on a
|
|
43
49
|
* touch screen, Select From List on a desktop), those of `kind` pickable — the one picked handed to `pick` (the
|
|
@@ -91,6 +97,8 @@ export interface EditorPorts {
|
|
|
91
97
|
toggleImageRotation(anchor?: unknown): void;
|
|
92
98
|
/** the link tiles' size slider, above the bar (its button again closes it) */
|
|
93
99
|
toggleTileSize(anchor?: unknown): void;
|
|
100
|
+
/** the measurement tiles' size slider, above the bar, as the tiles' (editor/dimSize.ts) */
|
|
101
|
+
toggleDimSize(anchor?: unknown): void;
|
|
94
102
|
/** let the user type a new name for a space */
|
|
95
103
|
editRoomName(roomId: string): void;
|
|
96
104
|
/** the mouse cursor over the plan (or the wall elevation), as a CSS cursor name ('' = the default); ignored without a mouse */
|
|
@@ -125,6 +133,7 @@ export const basePorts = (): EditorPorts => ({
|
|
|
125
133
|
toggleImageOpacity() {},
|
|
126
134
|
toggleImageRotation() {},
|
|
127
135
|
toggleTileSize() {},
|
|
136
|
+
toggleDimSize() {},
|
|
128
137
|
editRoomName() {},
|
|
129
138
|
setCursor() {},
|
|
130
139
|
dismissPopovers: () => false,
|
|
@@ -154,6 +163,7 @@ export const openPalette = (anchor?: unknown): void => ports.openPalette(anchor)
|
|
|
154
163
|
export const toggleImageOpacity = (anchor?: unknown): void => ports.toggleImageOpacity(anchor);
|
|
155
164
|
export const toggleImageRotation = (anchor?: unknown): void => ports.toggleImageRotation(anchor);
|
|
156
165
|
export const toggleTileSize = (anchor?: unknown): void => ports.toggleTileSize(anchor);
|
|
166
|
+
export const toggleDimSize = (anchor?: unknown): void => ports.toggleDimSize(anchor);
|
|
157
167
|
export const editRoomName = (roomId: string): void => ports.editRoomName(roomId);
|
|
158
168
|
export const setCursor = (cursor: string, where: 'plan' | 'elevation' = 'plan'): void => ports.setCursor(cursor, where);
|
|
159
169
|
export const dismissPopovers = (): boolean => ports.dismissPopovers();
|
package/src/editor/selection.ts
CHANGED
|
@@ -103,7 +103,7 @@ export function selectionHint(): string {
|
|
|
103
103
|
if (s.n === 1 && s.image) {
|
|
104
104
|
// (a photo of the job's, expanded from its tile: its link opens it, and Expand folds it back)
|
|
105
105
|
const u = imageById(ed.sel[0].id), what = u?.link ? '<b>Photo</b>' : '<b>Image</b>', link = u?.link ? ' · its link opens it' : '';
|
|
106
|
-
return u?.locked ? what + ' · locked — tap Unlock to move or resize it' + link : what + ' · drag to move · corners scale, side handles
|
|
106
|
+
return u?.locked ? what + ' · locked — tap Unlock to move or resize it' + link : what + ' · drag to move · corners scale, side handles crop' + link;
|
|
107
107
|
}
|
|
108
108
|
if (s.n === 1 && s.notes.length && SK().notes.find(n => n.id === s.notes[0])?.type === 'link') return '<b>Link</b> · drag to move · open it, or point it at the drawing with a leader, below · Size sizes every tile on the page';
|
|
109
109
|
if (s.notes.length) return 'Drag to move. Turn a pen stroke into lines below.';
|
package/src/editor/tooltips.ts
CHANGED
|
@@ -15,6 +15,7 @@ const TIPS: Record<string, string> = {
|
|
|
15
15
|
rect: 'Tap or drag corner to corner.',
|
|
16
16
|
circle: 'Tap the center, then the edge.',
|
|
17
17
|
arc: 'Tap both ends, then drag to bend.',
|
|
18
|
+
trim: 'Tap or drag across the pieces to cut away.',
|
|
18
19
|
door: 'Tap a wall to add a 3\'-0" door.',
|
|
19
20
|
window: 'Tap a wall to add a 3\'-0" window.',
|
|
20
21
|
room: 'Tap inside a closed shape.',
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type { CadDataProvider, CadFileMeta, CadMeasurements, CadMeasurementValue
|
|
|
10
10
|
export type { Unit } from './units';
|
|
11
11
|
export { memoryProvider } from './data/memory';
|
|
12
12
|
export { setFileSaver } from './react/ports';
|
|
13
|
-
export {
|
|
14
|
-
export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
13
|
+
export { BESIDE as CAD_BESIDE } from './react/pageLock';
|
|
14
|
+
export { laserReading, laserReadingOverwrites, setLaserConnected, surveyClose, surveyCursor, surveyPoint } from './react/host';export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
15
15
|
export type { CadLinkPick, CadLinks, CadSaveMeasurement, MeasurementKind } from './editor/links';
|
|
16
16
|
export type { LinkInfo, LinkTarget } from './model/types';
|
|
17
17
|
export { linkKey } from './model/links';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Sketch } from './types';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The next line clockwise round the shape a line is part of: where a laser reading moves on to once it has set a
|
|
5
|
+
line's length (editor/advance.ts; the user's call, 2026-11-05). Clockwise as seen: the plan's y runs up, so as it is
|
|
6
|
+
in the model too, and turning the view doesn't change it.
|
|
7
|
+
|
|
8
|
+
The shape is the face the line bounds, walked with it on the right: at each corner the walk takes the edge that turns
|
|
9
|
+
most to the right, so it keeps tracing the same face past a T (an inner wall) and comes round to where it started. A
|
|
10
|
+
line can bound two faces (a wall between two rooms) or only the outside: the walk that closes clockwise is the room
|
|
11
|
+
(its signed area negative), and between two rooms, the smaller one, or for a wall edge, the room that edge faces. A
|
|
12
|
+
run that never closes goes clockwise round its own middle, and stops at its end. Arcs are walked through (a fillet in
|
|
13
|
+
a room's corner) but never moved onto: the next straight line is. Guides aren't part of any shape.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** An edge walked one way: `id` (a line or an arc) from point `from` to point `to`. */
|
|
17
|
+
interface Step { id: string; from: string; to: string }
|
|
18
|
+
|
|
19
|
+
/** Every line and arc, by the points they end at (guides left out). */
|
|
20
|
+
function ends(sk: Sketch): Map<string, { id: string; other: string }[]> {
|
|
21
|
+
const m = new Map<string, { id: string; other: string }[]>();
|
|
22
|
+
const add = (id: string, a: string, b: string) => {
|
|
23
|
+
if (a === b || !sk.points[a] || !sk.points[b]) return;
|
|
24
|
+
(m.get(a) ?? m.set(a, []).get(a)!).push({ id, other: b });
|
|
25
|
+
(m.get(b) ?? m.set(b, []).get(b)!).push({ id, other: a });
|
|
26
|
+
};
|
|
27
|
+
for (const [id, l] of Object.entries(sk.lines)) if (!l.constr) add(id, l.a, l.b);
|
|
28
|
+
for (const [id, c] of Object.entries(sk.arcs)) if (!c.constr) add(id, c.a, c.b);
|
|
29
|
+
return m;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TURN = 2 * Math.PI;
|
|
33
|
+
|
|
34
|
+
/** The step after `s`, keeping the face on the right: the edge at its end turning most to the right. Null at a dead end. */
|
|
35
|
+
function after(sk: Sketch, at: Map<string, { id: string; other: string }[]>, s: Step): Step | null {
|
|
36
|
+
const v = sk.points[s.to], u = sk.points[s.from];
|
|
37
|
+
const back = Math.atan2(u.y - v.y, u.x - v.x);
|
|
38
|
+
let best: Step | null = null, least = Infinity;
|
|
39
|
+
for (const e of at.get(s.to) ?? []) {
|
|
40
|
+
if (e.id === s.id) continue;
|
|
41
|
+
const w = sk.points[e.other];
|
|
42
|
+
// (counter-clockwise from the way back: the least is the sharpest turn right)
|
|
43
|
+
let d = Math.atan2(w.y - v.y, w.x - v.x) - back;
|
|
44
|
+
while (d <= 1e-9) d += TURN;
|
|
45
|
+
while (d > TURN) d -= TURN;
|
|
46
|
+
if (d < least) { least = d; best = { id: e.id, from: s.to, to: e.other }; }
|
|
47
|
+
}
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The walk from `s` on round: every step after it, and whether it came back to `s` (a closed shape). */
|
|
52
|
+
function walk(sk: Sketch, at: Map<string, { id: string; other: string }[]>, s: Step): { steps: Step[]; closed: boolean } {
|
|
53
|
+
const steps: Step[] = [];
|
|
54
|
+
let cur: Step | null = s;
|
|
55
|
+
const most = 2 * (Object.keys(sk.lines).length + Object.keys(sk.arcs).length) + 2;
|
|
56
|
+
while (steps.length < most) {
|
|
57
|
+
cur = after(sk, at, cur!);
|
|
58
|
+
if (!cur) return { steps, closed: false };
|
|
59
|
+
if (cur.id === s.id && cur.from === s.from) return { steps, closed: true };
|
|
60
|
+
steps.push(cur);
|
|
61
|
+
}
|
|
62
|
+
return { steps, closed: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Twice the signed area round `s` and the steps after it (negative: clockwise, y up). */
|
|
66
|
+
function area(sk: Sketch, s: Step, steps: Step[]): number {
|
|
67
|
+
let a = 0;
|
|
68
|
+
for (const t of [s, ...steps]) {
|
|
69
|
+
const p = sk.points[t.from], q = sk.points[t.to];
|
|
70
|
+
a += p.x * q.y - q.x * p.y;
|
|
71
|
+
}
|
|
72
|
+
return a;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The middle of everything joined to `pid` (the points it can reach along lines and arcs). */
|
|
76
|
+
function middle(sk: Sketch, at: Map<string, { id: string; other: string }[]>, pid: string): { x: number; y: number } {
|
|
77
|
+
const seen = new Set([pid]), todo = [pid];
|
|
78
|
+
while (todo.length) for (const e of at.get(todo.pop()!) ?? []) if (!seen.has(e.other)) { seen.add(e.other); todo.push(e.other); }
|
|
79
|
+
let x = 0, y = 0;
|
|
80
|
+
for (const id of seen) { x += sk.points[id].x; y += sk.points[id].y; }
|
|
81
|
+
return { x: x / seen.size, y: y / seen.size };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The side of a straight line on the right of walking it as `s` does (+1 the left of its A → B, −1 the right). */
|
|
85
|
+
const rightOf = (sk: Sketch, s: Step): 1 | -1 => (s.from === sk.lines[s.id]?.a ? -1 : 1);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The next line clockwise from line or arc `id`, round the shape it's part of; null when there's none (a lone line, a
|
|
89
|
+
* run's end, a circle). `side`: a wall's edge it was measured on (+1 the left of its A → B): the edge of the next wall on
|
|
90
|
+
* the same side of the shape (the inside stays the inside) comes back with it. A line between two rooms goes round the
|
|
91
|
+
* one that edge faces, or the smaller one.
|
|
92
|
+
*/
|
|
93
|
+
export function nextClockwise(sk: Sketch, id: string, side?: 1 | -1): { id: string; side?: 1 | -1 } | null {
|
|
94
|
+
const e = sk.lines[id] ?? sk.arcs[id];
|
|
95
|
+
if (!e || e.constr || e.a === e.b) return null;
|
|
96
|
+
const at = ends(sk);
|
|
97
|
+
const fwd: Step = { id, from: e.a, to: e.b }, rev: Step = { id, from: e.b, to: e.a };
|
|
98
|
+
const wf = walk(sk, at, fwd), wr = walk(sk, at, rev);
|
|
99
|
+
const room = (s: Step, w: { steps: Step[]; closed: boolean }) => w.closed && area(sk, s, w.steps) < 0;
|
|
100
|
+
const inF = room(fwd, wf), inR = room(rev, wr);
|
|
101
|
+
let start: Step, w: { steps: Step[]; closed: boolean };
|
|
102
|
+
if (inF && inR) {
|
|
103
|
+
// between two rooms: the one the edge faces (on the walk's right), or the smaller
|
|
104
|
+
const pickF = side && sk.lines[id] ? rightOf(sk, fwd) === side : Math.abs(area(sk, fwd, wf.steps)) <= Math.abs(area(sk, rev, wr.steps));
|
|
105
|
+
[start, w] = pickF ? [fwd, wf] : [rev, wr];
|
|
106
|
+
} else if (inF || inR) {
|
|
107
|
+
[start, w] = inF ? [fwd, wf] : [rev, wr];
|
|
108
|
+
} else {
|
|
109
|
+
// a run that doesn't close: clockwise round its middle (the middle on the right of the way it's walked)
|
|
110
|
+
const m = middle(sk, at, e.a), a = sk.points[e.a], b = sk.points[e.b];
|
|
111
|
+
const leftOfAB = (b.x - a.x) * (m.y - a.y) - (b.y - a.y) * (m.x - a.x) >= 0;
|
|
112
|
+
[start, w] = leftOfAB ? [rev, wr] : [fwd, wf];
|
|
113
|
+
}
|
|
114
|
+
const next = w.steps.find(s => s.id !== id && sk.lines[s.id]);
|
|
115
|
+
if (!next) return null;
|
|
116
|
+
if (!side || !sk.lines[id]) return { id: next.id };
|
|
117
|
+
// the measured edge's side of the shape (on the walk's right, or its left), kept on the next line
|
|
118
|
+
const sameRight = rightOf(sk, start) === side;
|
|
119
|
+
const r = rightOf(sk, next);
|
|
120
|
+
return { id: next.id, side: sameRight ? r : (-r as 1 | -1) };
|
|
121
|
+
}
|
package/src/model/constraints.ts
CHANGED
|
@@ -16,6 +16,7 @@ export function consLabel(c: Constraint): string {
|
|
|
16
16
|
if (c.type === 'dist' || c.type === 'pldist') return (c.type === 'dist' ? (c.line ? 'Length ' : 'Distance ') : 'Offset ') + fmtLen(c.value!);
|
|
17
17
|
if (c.type === 'diam') return c.rad ? 'Radius ' + fmtLen(c.value! / 2) : 'Diameter ' + fmtLen(c.value!);
|
|
18
18
|
if (c.type === 'angle') return 'Angle ' + fmtAng(c.value!);
|
|
19
|
+
if (c.type === 'online' && c.face) return 'Point on face';
|
|
19
20
|
return CONS_LABEL[c.type] || c.type;
|
|
20
21
|
}
|
|
21
22
|
|
package/src/model/geom.ts
CHANGED
|
@@ -34,6 +34,16 @@ export function lineCircleT(A: XY, B: XY, c: XY, r: number): number[] {
|
|
|
34
34
|
return [(-b - s) / (2 * a), (-b + s) / (2 * a)].filter(t => t > 1e-6 && t < 1 - 1e-6);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Where circle (c1, r1) crosses circle (c2, r2): none, one where they touch, or two. */
|
|
38
|
+
export function circleCircle(c1: XY, r1: number, c2: XY, r2: number): XY[] {
|
|
39
|
+
const dx = c2.x - c1.x, dy = c2.y - c1.y, d = hyp(dx, dy);
|
|
40
|
+
if (d < 1e-12 || d > r1 + r2 + 1e-9 || d < Math.abs(r1 - r2) - 1e-9) return [];
|
|
41
|
+
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d), h2 = r1 * r1 - a * a, h = h2 > 0 ? Math.sqrt(h2) : 0;
|
|
42
|
+
const mx = c1.x + (dx * a) / d, my = c1.y + (dy * a) / d;
|
|
43
|
+
if (h < 1e-12) return [{ x: mx, y: my }];
|
|
44
|
+
return [{ x: mx - (dy * h) / d, y: my + (dx * h) / d }, { x: mx + (dy * h) / d, y: my - (dx * h) / d }];
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
/** Circumcenter of three points, or null if they are collinear. */
|
|
38
48
|
export function circum(a: XY, b: XY, c: XY): XY | null {
|
|
39
49
|
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
package/src/model/links.ts
CHANGED
|
@@ -22,8 +22,13 @@ export const isLink = (n: Note): n is Note & { link: LinkTarget } => n.type ===
|
|
|
22
22
|
|
|
23
23
|
/** How big a tile is on the screen as it's placed (points): its side on the drawing is what that is at the zoom then. */
|
|
24
24
|
export const LINK_TILE_PX = 96;
|
|
25
|
-
/** How big a tile can be on the drawing, inches; unset (never, once placed), this. */
|
|
25
|
+
/** How big a tile can be on the drawing as it's placed, inches; unset (never, once placed), this. */
|
|
26
26
|
export const LINK_MIN_IN = 6, LINK_MAX_IN = 480, LINK_SIDE_IN = 24;
|
|
27
|
+
/**
|
|
28
|
+
* How big the Tile size slider can make one, inches: wide enough that its 10%–1000% of the size tiles came in at is
|
|
29
|
+
* there to reach, and inside what the tile's drawing clips to (render/linkTile.ts: clipToBar).
|
|
30
|
+
*/
|
|
31
|
+
export const LINK_HARD_MIN_IN = 1, LINK_HARD_MAX_IN = 2400;
|
|
27
32
|
/**
|
|
28
33
|
* On the screen, points: from LINK_FULL_PX across a tile shows its picture or icon and its name; under LINK_PIN_PX it's
|
|
29
34
|
* a pin of that size, however far out the drawing is.
|
package/src/model/scale.ts
CHANGED
|
@@ -47,6 +47,8 @@ export function scaleSketch(sk: Sketch, k: number): void {
|
|
|
47
47
|
// (a note's letters and a tile's side are inches on the drawing: they zoom with it, so they scale with it)
|
|
48
48
|
if (n.ts != null) n.ts *= k;
|
|
49
49
|
}
|
|
50
|
+
// (and what the tiles' 100% is, so their percent holds)
|
|
51
|
+
if (sk.tileRef != null) sk.tileRef *= k;
|
|
50
52
|
for (const c of Object.values(sk.cons)) {
|
|
51
53
|
if (c.off != null) c.off *= k;
|
|
52
54
|
// (none should be here before the first length; one that is keeps measuring what it measures, met as it was)
|
package/src/model/settings.ts
CHANGED
|
@@ -24,13 +24,15 @@ export interface Settings extends UnitPrefs {
|
|
|
24
24
|
layers: Layers;
|
|
25
25
|
/** default wall height (floor to ceiling), inches */
|
|
26
26
|
wallH: number;
|
|
27
|
+
/** the size of the drawing's measurement tiles (its dimensions' chips), percent: the file's, everyone's (editor/dimSize.ts) */
|
|
28
|
+
chipPct: number;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, rooms: true, annot: true, cabinets: true, objSnap: true });
|
|
30
32
|
|
|
31
33
|
export const settings = sessionState((): Settings => ({
|
|
32
34
|
unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, toolTips: true, showCons: true, theme: 'system', welcomed: false,
|
|
33
|
-
layers: defaultLayers(), wallH: 96,
|
|
35
|
+
layers: defaultLayers(), wallH: 96, chipPct: 100,
|
|
34
36
|
}));
|
|
35
37
|
|
|
36
38
|
useUnitPrefs(settings);
|
package/src/model/sketch.ts
CHANGED
|
@@ -321,23 +321,30 @@ export function restorePos(sk: Sketch, s: PosSnapshot): void {
|
|
|
321
321
|
for (const id in s.r) if (sk.circles[id]) sk.circles[id].r = s.r[id];
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
/**
|
|
324
|
+
/**
|
|
325
|
+
* Every line and arc joined end to end with the given ones (through shared points), including them. Guides join only
|
|
326
|
+
* guides, and walls and lines only walls and lines: a guide drawn off a shape's corner isn't part of the shape, nor the
|
|
327
|
+
* shape part of the guides (the user's call, 2026-11-02).
|
|
328
|
+
*/
|
|
325
329
|
export function connectedGeometry(sk: Sketch, lineIds: string[], arcIds: string[] = []): { lines: string[]; arcs: string[] } {
|
|
330
|
+
// the points reached by walls and lines [0], and by guides [1]
|
|
331
|
+
const pts = [new Set<string>(), new Set<string>()], lines = new Set(lineIds), arcs = new Set(arcIds);
|
|
332
|
+
type End = { a: string; b: string; constr?: boolean };
|
|
326
333
|
// on a wall face, the plan's corners and edges aren't part of a shape: nothing joins through them
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
lineIds.forEach(id => { const l = sk.lines[id]; if (l)
|
|
330
|
-
arcIds.forEach(id => { const a = sk.arcs[id]; if (a)
|
|
334
|
+
const join = (o: End) => { for (const p of [o.a, o.b]) if (!sk.points[p]?.anchor) pts[+!!o.constr].add(p); };
|
|
335
|
+
const meets = (o: End) => pts[+!!o.constr].has(o.a) || pts[+!!o.constr].has(o.b);
|
|
336
|
+
lineIds.forEach(id => { const l = sk.lines[id]; if (l) join(l); });
|
|
337
|
+
arcIds.forEach(id => { const a = sk.arcs[id]; if (a) join(a); });
|
|
331
338
|
let grow = true;
|
|
332
339
|
while (grow) {
|
|
333
340
|
grow = false;
|
|
334
341
|
for (const id in sk.lines) {
|
|
335
342
|
const l = sk.lines[id];
|
|
336
|
-
if (!lines.has(id) && !l.anchor && !l.cab && (
|
|
343
|
+
if (!lines.has(id) && !l.anchor && !l.cab && meets(l)) { lines.add(id); join(l); grow = true; }
|
|
337
344
|
}
|
|
338
345
|
for (const id in sk.arcs) {
|
|
339
346
|
const a = sk.arcs[id];
|
|
340
|
-
if (!arcs.has(id) && (
|
|
347
|
+
if (!arcs.has(id) && meets(a)) { arcs.add(id); join(a); grow = true; }
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
return { lines: [...lines], arcs: [...arcs] };
|