@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/export/plan.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isDim, measure } from '../model/constraints';
|
|
2
2
|
import { hyp, norm2, pointInPoly } from '../model/geom';
|
|
3
|
-
import { edgeS, measureOpeningDim, openingSpan, refPoint, refS } from '../model/openings';
|
|
3
|
+
import { edgeS, hingeEdge, measureOpeningDim, openingSpan, refPoint, refS } from '../model/openings';
|
|
4
4
|
import { roomGeom } from '../model/rooms';
|
|
5
5
|
import { smoothPolyline } from '../model/smooth';
|
|
6
6
|
import { roomValid } from '../model/sketch';
|
|
7
|
-
import { dimEnds, dimRadius } from '../model/planGeom';
|
|
7
|
+
import { angleLabel, dimEnds, dimRadius } from '../model/planGeom';
|
|
8
8
|
import { wallPieces } from '../model/wallshape';
|
|
9
9
|
import { cabinetFootprints } from '../model/facesolve';
|
|
10
10
|
import { isSegment, noteLook, noteTextIn, shapeOutline } from '../model/markup';
|
|
@@ -41,7 +41,7 @@ export interface Plan {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export interface DrawOptions {
|
|
44
|
-
/** include dimensions (driving, reference,
|
|
44
|
+
/** include dimensions (driving, reference, door/window) — default true */
|
|
45
45
|
dims?: boolean;
|
|
46
46
|
/** include markup (pen, lines, arrows, shapes, clouds, notes) — default true */
|
|
47
47
|
markup?: boolean;
|
|
@@ -59,7 +59,7 @@ const defaultLinkLabel = (t: LinkTarget): { name: string; value?: string } => {
|
|
|
59
59
|
const info = linkInfoOf(t);
|
|
60
60
|
return { name: (info?.exists === false ? 'Removed: ' : '') + linkName(t), value: t.kind === 'measurement' ? info?.text ?? t.text : undefined };
|
|
61
61
|
};
|
|
62
|
-
const KIND_WORD: Record<string, string> = { photo: 'Photo', pdf: 'PDF', note: 'Note', calculator: 'Calculator', label: 'Label', drawing: '
|
|
62
|
+
const KIND_WORD: Record<string, string> = { photo: 'Photo', pdf: 'PDF', note: 'Note', calculator: 'Calculator', conversion: 'Conversion', label: 'Label', drawing: 'Sketch', cad: 'CAD' };
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* A name on paper, where nothing can be measured (a DXF has no fonts): about `perLine` characters a line, broken at a
|
|
@@ -201,7 +201,8 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
201
201
|
if (o.type === 'window') {
|
|
202
202
|
for (const k of [t / 6, -t / 6]) d.line(layer, Q(s0, k), Q(s1, k));
|
|
203
203
|
} else {
|
|
204
|
-
const f = o.flip ? -1 : 1,
|
|
204
|
+
const f = o.flip ? -1 : 1, [hs, ls] = hingeEdge(o) === 's1' ? [s1, s0] : [s0, s1];
|
|
205
|
+
const hinge = Q(hs, (f * t) / 2), leaf = Q(hs, f * (t / 2 + g.w)), shut = Q(ls, (f * t) / 2);
|
|
205
206
|
d.line(layer, hinge, leaf);
|
|
206
207
|
// swing: from the closed position to the open leaf, the short way round
|
|
207
208
|
const a0 = Math.atan2(shut.y - hinge.y, shut.x - hinge.x), a1 = Math.atan2(leaf.y - hinge.y, leaf.x - hinge.x);
|
|
@@ -219,14 +220,15 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
219
220
|
if (!roomValid(sk, r)) continue;
|
|
220
221
|
const g = roomGeom(sk, r);
|
|
221
222
|
d.text('SPACES', { x: g.c.x, y: g.c.y + 4 }, 6, r.name);
|
|
222
|
-
|
|
223
|
+
// (a sketch without its scale yet: its area is only a placeholder)
|
|
224
|
+
if (!sk.unscaled) d.text('SPACES', { x: g.c.x, y: g.c.y - 4 }, TEXT_H, fmtArea(g.area));
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
if (opts.dims !== false) {
|
|
226
228
|
for (const id in sk.cons) {
|
|
227
229
|
const c = sk.cons[id];
|
|
228
230
|
if (!isDim(c)) continue;
|
|
229
|
-
const ref = c.driving === false
|
|
231
|
+
const ref = c.driving === false;
|
|
230
232
|
const v = c.driving === false ? measure(sk, c) : c.value!;
|
|
231
233
|
const wrap = (s: string) => (ref ? '(' + s + ')' : s);
|
|
232
234
|
if (c.type === 'dist') {
|
|
@@ -241,11 +243,11 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
241
243
|
const off = c.off || (c.line ? WALL_DIM_OFF * outsideSide(sk, c.line) : 0);
|
|
242
244
|
linearDim(d, a, b, n, off, wrap(fmtLen(v)));
|
|
243
245
|
} else if (c.type === 'pldist') {
|
|
244
|
-
// (between walls' faces: face to face)
|
|
246
|
+
// (between walls' faces: face to face, where it was slid to along them)
|
|
245
247
|
const e = dimEnds(sk, c);
|
|
246
248
|
if (!e) continue;
|
|
247
249
|
const n = norm2(-(e.b.y - e.a.y), e.b.x - e.a.x);
|
|
248
|
-
linearDim(d, e.a, e.b, n, 0, wrap(fmtLen(v)));
|
|
250
|
+
linearDim(d, e.a, e.b, n, c.off || 0, wrap(fmtLen(v)));
|
|
249
251
|
} else if (c.type === 'diam') {
|
|
250
252
|
// (across a curved wall's face: on that face)
|
|
251
253
|
const g = dimRadius(sk, c);
|
|
@@ -261,7 +263,9 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
261
263
|
if (!sh) continue;
|
|
262
264
|
const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
|
|
263
265
|
const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y), bis = norm2(u1[0] + u2[0], u1[1] + u2[1]);
|
|
264
|
-
|
|
266
|
+
// (pulled off its corner: where it was left)
|
|
267
|
+
const moved = angleLabel(sk, c);
|
|
268
|
+
d.text('DIMENSIONS', moved ? { x: moved.vtx.x + Math.cos(moved.ang) * moved.r, y: moved.vtx.y + Math.sin(moved.ang) * moved.r } : { x: s.x + bis[0] * 18, y: s.y + bis[1] * 18 }, TEXT_H, wrap(fmtAng(v)));
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
// door/window dimensions: placed ones, and the width
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ 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 { laserReading, setLaserConnected, surveyCursor, surveyPoint } from './react/host';
|
|
13
|
+
export { laserReading, setLaserConnected, surveyClose, surveyCursor, surveyPoint } from './react/host';
|
|
14
14
|
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';
|
package/src/model/demo.ts
CHANGED
|
@@ -24,8 +24,9 @@ export function demoSketch(): Sketch {
|
|
|
24
24
|
return sk;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** A page: a copy's drawing (`sk`), or a new one's — without a scale until its first length (model/scale.ts). */
|
|
27
28
|
export function newPage(name: string, sk?: Sketch): Page {
|
|
28
|
-
return { id: uid('pg'), name, sk: sk || emptySketch(), underlays: [], view: null };
|
|
29
|
+
return { id: uid('pg'), name, sk: sk || { ...emptySketch(), unscaled: true }, underlays: [], view: null };
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function seedProject(): Project {
|
package/src/model/elevation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hyp, pointInPoly } from './geom';
|
|
2
|
-
import { edgeS, openingSpan } from './openings';
|
|
2
|
+
import { edgeS, hingeEdge, openingSpan } from './openings';
|
|
3
3
|
import { roomGeom } from './rooms';
|
|
4
4
|
import { settings } from './settings';
|
|
5
5
|
import { roomValid } from './sketch';
|
|
@@ -87,7 +87,7 @@ export function elevFrame(sk: Sketch, lid: string, flip = false): ElevFrame | nu
|
|
|
87
87
|
|
|
88
88
|
export interface ElevOpening {
|
|
89
89
|
id: string; type: Opening['type']; u0: number; u1: number; sill: number; head: number;
|
|
90
|
-
/** the door's hinge edge as seen (
|
|
90
|
+
/** the door's hinge edge as seen (Opening.hinge) */
|
|
91
91
|
hinge: 'l' | 'r';
|
|
92
92
|
/** the opening's edge (anchor) at u0 and at u1 */
|
|
93
93
|
e0: OpeningEdge; e1: OpeningEdge;
|
|
@@ -101,8 +101,8 @@ export function elevOpenings(sk: Sketch, f: ElevFrame): ElevOpening[] {
|
|
|
101
101
|
if (o.line !== f.lid) continue;
|
|
102
102
|
const g = openingSpan(sk, o);
|
|
103
103
|
if (!g) continue;
|
|
104
|
-
const a = toU(f, edgeS(o, g.w, 's0')), b = toU(f, edgeS(o, g.w, 's1'));
|
|
105
|
-
out.push({ id, type: o.type, u0: Math.min(a, b), u1: Math.max(a, b), ...openingHeights(o), hinge:
|
|
104
|
+
const a = toU(f, edgeS(o, g.w, 's0')), b = toU(f, edgeS(o, g.w, 's1')), left = (hingeEdge(o) === 's1' ? b <= a : a <= b);
|
|
105
|
+
out.push({ id, type: o.type, u0: Math.min(a, b), u1: Math.max(a, b), ...openingHeights(o), hinge: left ? 'l' : 'r', e0: a <= b ? 's0' : 's1', e1: a <= b ? 's1' : 's0' });
|
|
106
106
|
}
|
|
107
107
|
return out.sort((p, q) => p.u0 - q.u0);
|
|
108
108
|
}
|
package/src/model/history.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { page, store } from './store';
|
|
2
|
+
import { rezoomAcrossScale } from './scale';
|
|
2
3
|
import type { Underlay } from './types';
|
|
3
4
|
import { onSessionReset } from './session';
|
|
4
5
|
|
|
@@ -20,8 +21,10 @@ function take(): Snap {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function restore(s: Snap): void {
|
|
23
|
-
const pg = page();
|
|
24
|
+
const pg = page(), was = pg.sk;
|
|
24
25
|
pg.sk = JSON.parse(s.sk);
|
|
26
|
+
// the step that gave the drawing its scale zoomed the view to match (views aren't kept here): zoom back, or again
|
|
27
|
+
rezoomAcrossScale(pg, was, pg.sk);
|
|
25
28
|
// an image uploaded since keeps its stored address (not the local copy it had when this was taken)
|
|
26
29
|
const now = new Map(pg.underlays.map(u => [u.id, u.src]));
|
|
27
30
|
pg.underlays = s.imgs.map(u => ({ ...u, src: stored(u.src, now.get(u.id)) }));
|
package/src/model/links.ts
CHANGED
|
@@ -64,10 +64,16 @@ export function sketchLinks(sk: Sketch): LinkTarget[] {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Everything a project links to, each once: its pages' tiles, the photos expanded from tiles among their images, and
|
|
69
|
+
* the measurements its dimensions show.
|
|
70
|
+
*/
|
|
68
71
|
export function linkTargets(p: Project): LinkTarget[] {
|
|
69
72
|
const out = new Map<string, LinkTarget>();
|
|
70
|
-
for (const pg of p.pages)
|
|
73
|
+
for (const pg of p.pages) {
|
|
74
|
+
for (const t of sketchLinks(pg.sk)) if (!out.has(linkKey(t))) out.set(linkKey(t), t);
|
|
75
|
+
for (const u of pg.underlays ?? []) if (u.link && !out.has(linkKey(u.link))) out.set(linkKey(u.link), u.link);
|
|
76
|
+
}
|
|
71
77
|
return [...out.values()];
|
|
72
78
|
}
|
|
73
79
|
|
package/src/model/measurement.ts
CHANGED
|
@@ -9,11 +9,11 @@ import type { Constraint } from './types';
|
|
|
9
9
|
const LENGTHS = new Set<Constraint['type']>(['dist', 'pldist', 'diam']);
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Dimension `c`'s value (or `value`, one it's about to have) as a measurement. Null for what isn't one: reference
|
|
13
|
-
*
|
|
12
|
+
* Dimension `c`'s value (or `value`, one it's about to have) as a measurement. Null for what isn't one: reference
|
|
13
|
+
* dimensions, measured-only ones, and constraints without a value.
|
|
14
14
|
*/
|
|
15
15
|
export function dimMeasurement(c: Constraint, value = c.value): CadMeasurementValue | null {
|
|
16
|
-
if (c.driving === false ||
|
|
16
|
+
if (c.driving === false || value == null || c.auto) return null;
|
|
17
17
|
if (c.type === 'angle') return { kind: 'angle', value: Math.round(value * 100) / 100 };
|
|
18
18
|
// a fillet's radius is kept as its diameter; BOLDR keeps the radius
|
|
19
19
|
if (LENGTHS.has(c.type)) return { kind: 'length', value: Math.round((c.rad ? value / 2 : value) * 25400) };
|
package/src/model/openings.ts
CHANGED
|
@@ -28,6 +28,24 @@ export function openingSpan(sk: Sketch, o: Opening): Span | null {
|
|
|
28
28
|
/** Position of an edge, inches along the opening's line. */
|
|
29
29
|
export const edgeS = (o: Opening, w: number, edge: OpeningEdge): number => (edge === 's0' ? o.at - w / 2 : o.at + w / 2);
|
|
30
30
|
|
|
31
|
+
/** The jamb a door hangs on (Opening.hinge): the plan, the wall view, 3D and exports all draw its leaf from there. */
|
|
32
|
+
export const hingeEdge = (o: Opening): OpeningEdge => o.hinge ?? 's0';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A door turned to the next of its four ways round, by Swing in the bar (the user's call, 2026-10-27: "4 options of flippings instead of 2
|
|
36
|
+
* (inner, outer, inner left, outer left) pressing the one button should toggle between the different options"):
|
|
37
|
+
* swinging into the room, it swings out on the same jamb; swinging out, it swings back in, hung on its other jamb. So
|
|
38
|
+
* inner, outer, inner left, outer left, and inner again, from whichever it was. `inside`: the side of its wall the
|
|
39
|
+
* room is on (+1: the left of its line's start → end).
|
|
40
|
+
*/
|
|
41
|
+
export function flipDoor(o: Opening, inside: 1 | -1): void {
|
|
42
|
+
if ((o.flip ? -1 : 1) !== inside) {
|
|
43
|
+
if (hingeEdge(o) === 's0') o.hinge = 's1';
|
|
44
|
+
else delete o.hinge;
|
|
45
|
+
}
|
|
46
|
+
o.flip = !o.flip;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/** World position of an opening edge anchor. */
|
|
32
50
|
export function edgePoint(sk: Sketch, opId: string, edge: OpeningEdge): XY | null {
|
|
33
51
|
const o = sk.openings[opId], g = o && openingSpan(sk, o);
|
package/src/model/planGeom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clamp, hyp, norm2, normAng, pointInPoly } from './geom';
|
|
2
|
-
import { roomGeom } from './rooms';
|
|
3
|
-
import { frameMemo, roomValid } from './sketch';
|
|
2
|
+
import { roomGeom, roomOutline } from './rooms';
|
|
3
|
+
import { circleEdge, frameMemo, roomValid } from './sketch';
|
|
4
4
|
import type { Arc, Constraint, LineKind, Sketch, XY } from './types';
|
|
5
5
|
import { faceCorner } from './wallfaces';
|
|
6
6
|
import { getUnitPrefs, isMetric, type Unit } from '../units';
|
|
@@ -52,9 +52,16 @@ export function openingFrame(sk: Sketch, id: string): OpeningFrame | null {
|
|
|
52
52
|
return { A, ux, uy, nx: -uy, ny: ux, s0: at - w / 2, s1: at + w / 2, at, w, t: l.kind === 'wall' ? l.thick : 2 };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* An arc's centre, radius and angles: it runs counter-clockwise from t0 (its start, a) to t1 (its end, b). `full`: its
|
|
57
|
+
* ends have met — dragged all the way round onto its start — so it goes all the way round (t1 is t0 + 2π), as the
|
|
58
|
+
* circle it becomes when they join (select.ts; gc in model/sketch.ts), not nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function arcAngles(sk: Sketch, a: Arc): { o: XY; r: number; t0: number; t1: number; full: boolean } {
|
|
56
61
|
const o = sk.points[a.c], s = sk.points[a.a], e = sk.points[a.b];
|
|
57
|
-
|
|
62
|
+
const r = hyp(s.x - o.x, s.y - o.y), t0 = Math.atan2(s.y - o.y, s.x - o.x);
|
|
63
|
+
const full = a.a === a.b || hyp(e.x - s.x, e.y - s.y) <= r * 1e-7;
|
|
64
|
+
return { o, r, t0, t1: full ? t0 + 2 * Math.PI : Math.atan2(e.y - o.y, e.x - o.x), full };
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** +1 or -1: the side of a line facing away from any room (where opening dimensions go). */
|
|
@@ -65,7 +72,7 @@ export function outsideSide(sk: Sketch, lid: string): number {
|
|
|
65
72
|
// (asked for every door and window on every frame: the rooms' outlines, and the boxes around them, are worked out
|
|
66
73
|
// once a frame — a room the box rules out isn't looked at more closely)
|
|
67
74
|
const rooms = frameMemo(sk, 'roomOutlines', () => Object.values(sk.rooms).filter(r => roomValid(sk, r)).map(r => {
|
|
68
|
-
const poly =
|
|
75
|
+
const poly = roomOutline(sk, r), xs = poly.map(p => p.x), ys = poly.map(p => p.y);
|
|
69
76
|
return { poly, x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
|
|
70
77
|
}));
|
|
71
78
|
for (const r of rooms) if (m.x >= r.x0 && m.x <= r.x1 && m.y >= r.y0 && m.y <= r.y1 && pointInPoly(m, r.poly)) return -1;
|
|
@@ -88,13 +95,14 @@ export function curveFrame(sk: Sketch, id: string): { o: XY; r: number; arc?: {
|
|
|
88
95
|
const a = sk.arcs[id];
|
|
89
96
|
if (!a || !sk.points[a.c] || !sk.points[a.a] || !sk.points[a.b]) return null;
|
|
90
97
|
const g = arcAngles(sk, a);
|
|
91
|
-
|
|
98
|
+
// (its ends met: all the way round, framed as the circle it's becoming)
|
|
99
|
+
return g.full ? { o: g.o, r: g.r } : { o: g.o, r: g.r, arc: { t0: g.t0, t1: g.t1 } };
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
103
|
* The space against one side of a wall, if any (a point just past its face there is inside it): its id. A straight
|
|
96
|
-
* wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces —
|
|
97
|
-
*
|
|
104
|
+
* wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces — a
|
|
105
|
+
* circle's inside too, a round space (model/rooms.ts).
|
|
98
106
|
*/
|
|
99
107
|
export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
100
108
|
const l = sk.lines[id];
|
|
@@ -105,21 +113,25 @@ export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null
|
|
|
105
113
|
const m = { x: (a.x + b.x) / 2 + n[0] * k, y: (a.y + b.y) / 2 + n[1] * k };
|
|
106
114
|
for (const rid in sk.rooms) {
|
|
107
115
|
const r = sk.rooms[rid];
|
|
108
|
-
if (roomValid(sk, r) && pointInPoly(m,
|
|
116
|
+
if (roomValid(sk, r) && pointInPoly(m, roomOutline(sk, r))) return rid;
|
|
109
117
|
}
|
|
110
118
|
return null;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
function curveSpaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
114
122
|
const f = curveFrame(sk, id), w = sk.circles[id] ?? sk.arcs[id];
|
|
115
|
-
if (!f || !w
|
|
123
|
+
if (!f || !w) return null;
|
|
116
124
|
// (just past the face at the middle of the arc, or anywhere around a circle)
|
|
117
125
|
const at = f.arc ? f.arc.t0 + normAng(f.arc.t1 - f.arc.t0) / 2 : 0, rr = f.r + side * (wallThick(w) / 2 + 1);
|
|
118
126
|
if (rr <= 0) return null;
|
|
119
127
|
const m = { x: f.o.x + Math.cos(at) * rr, y: f.o.y + Math.sin(at) * rr };
|
|
128
|
+
// inside a circle: only a space it bounds — a round column's inside isn't the room it stands in (a space has no
|
|
129
|
+
// holes: the room's outline goes over it)
|
|
130
|
+
const own = !f.arc && side === -1;
|
|
120
131
|
// (a space's outline as it's drawn: its arcs as arcs, not chords)
|
|
121
132
|
for (const rid in sk.rooms) {
|
|
122
133
|
const r = sk.rooms[rid];
|
|
134
|
+
if (own && !(r.edges ?? []).some(e => circleEdge(e)?.cid === id)) continue;
|
|
123
135
|
if (roomValid(sk, r) && pointInPoly(m, roomGeom(sk, r).poly)) return rid;
|
|
124
136
|
}
|
|
125
137
|
return null;
|
|
@@ -188,6 +200,54 @@ export function dimEnds(sk: Sketch, c: Constraint): { a: XY; b: XY } | null {
|
|
|
188
200
|
return null;
|
|
189
201
|
}
|
|
190
202
|
|
|
203
|
+
/**
|
|
204
|
+
* An angle dimension's corner: where its two lines meet, with `u1`/`u2` along each away from it (`len1`/`len2`: as far
|
|
205
|
+
* as each goes), and the bisector. For two lines that don't meet: the middle of the first, its label straight up.
|
|
206
|
+
*/
|
|
207
|
+
export function angleCorner(sk: Sketch, c: Constraint): { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number]; len1?: number; len2?: number } | null {
|
|
208
|
+
const l1 = c.l1 ? sk.lines[c.l1] : undefined, l2 = c.l2 ? sk.lines[c.l2] : undefined;
|
|
209
|
+
if (!l1 || !l2) return null;
|
|
210
|
+
const P = (id: string) => sk.points[id];
|
|
211
|
+
const sh = [l1.a, l1.b].find(p => p === l2.a || p === l2.b);
|
|
212
|
+
if (sh) {
|
|
213
|
+
const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
|
|
214
|
+
const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y);
|
|
215
|
+
return { vtx: s, bis: norm2(u1[0] + u2[0], u1[1] + u2[1]), u1, u2, len1: hyp(o1.x - s.x, o1.y - s.y), len2: hyp(o2.x - s.x, o2.y - s.y) };
|
|
216
|
+
}
|
|
217
|
+
const a = P(l1.a), b = P(l1.b);
|
|
218
|
+
return { vtx: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, bis: [0, 1] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const wrapPi = (x: number) => { while (x > Math.PI) x -= 2 * Math.PI; while (x < -Math.PI) x += 2 * Math.PI; return x; };
|
|
222
|
+
|
|
223
|
+
/** The short way round from `u1` to `u2`: where it starts and how far it turns, world radians (negative: clockwise). */
|
|
224
|
+
export function angleSweep(u1: [number, number], u2: [number, number]): { t1: number; sweep: number } {
|
|
225
|
+
const t1 = Math.atan2(u1[1], u1[0]);
|
|
226
|
+
return { t1, sweep: wrapPi(Math.atan2(u2[1], u2[0]) - t1) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Direction `a` (world radians) kept within a sweep: past either end of it, the nearer end. In (-π, π]. */
|
|
230
|
+
export function inSweep(a: number, t1: number, sweep: number): number {
|
|
231
|
+
const rel = wrapPi(a - t1), lo = Math.min(0, sweep), hi = Math.max(0, sweep);
|
|
232
|
+
if (rel >= lo && rel <= hi) return wrapPi(t1 + rel);
|
|
233
|
+
return wrapPi(t1 + (Math.abs(wrapPi(rel - lo)) <= Math.abs(wrapPi(rel - hi)) ? lo : hi));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* An angle dimension pulled off its corner (the user's call, 2026-10-24: angles pull like lengths): its arc's radius
|
|
238
|
+
* (`off`), and its label's direction (`ang`), kept between its two lines, in the angle it measures. Null for one that
|
|
239
|
+
* hasn't been moved: it's drawn as it always was, a little arc at the corner and the label out along the bisector.
|
|
240
|
+
*/
|
|
241
|
+
export function angleLabel(sk: Sketch, c: Constraint): { vtx: XY; r: number; ang: number } | null {
|
|
242
|
+
if (c.type !== 'angle' || c.off == null) return null;
|
|
243
|
+
const g = angleCorner(sk, c);
|
|
244
|
+
if (!g) return null;
|
|
245
|
+
const want = c.ang ?? Math.atan2(g.bis[1], g.bis[0]);
|
|
246
|
+
if (!g.u1 || !g.u2) return { vtx: g.vtx, r: c.off, ang: want };
|
|
247
|
+
const { t1, sweep } = angleSweep(g.u1, g.u2);
|
|
248
|
+
return { vtx: g.vtx, r: c.off, ang: inSweep(want, t1, sweep) };
|
|
249
|
+
}
|
|
250
|
+
|
|
191
251
|
/**
|
|
192
252
|
* Where a diameter (or radius) dimension's arrows go: its curve's centre, and the radius it measures at — the
|
|
193
253
|
* centreline's, or on a curved wall's face, that face's.
|
package/src/model/project.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { normalizeImages } from './images';
|
|
2
2
|
import { newPage } from './demo';
|
|
3
|
+
import { settleScale } from './scale';
|
|
3
4
|
import type { Page, Project } from './types';
|
|
4
5
|
|
|
6
|
+
/** A page as it's loaded: its images in today's form, and its scale settled (model/scale.ts). */
|
|
7
|
+
const tidy = (pg: Page): Page => { const n = normalizeImages(pg); settleScale(n.sk); return n; };
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Older saves group pages into spaces/levels (Project → Space → Page). Flatten them, in order; when the same page
|
|
7
11
|
* name appears in more than one level, it's prefixed with the level's name ("Level 2 · Floor plan").
|
|
8
12
|
*/
|
|
9
13
|
export function migrateProject(p: { name: string; pages?: Page[]; spaces?: { name: string; pages: Page[] }[] }): Project {
|
|
10
|
-
if (p.pages) return { name: p.name, pages: p.pages.map(
|
|
14
|
+
if (p.pages) return { name: p.name, pages: p.pages.map(tidy) };
|
|
11
15
|
const spaces = p.spaces || [], count: Record<string, number> = {};
|
|
12
16
|
for (const s of spaces) for (const pg of s.pages) count[pg.name] = (count[pg.name] || 0) + 1;
|
|
13
|
-
const pages = spaces.flatMap(s => s.pages.map(pg =>
|
|
17
|
+
const pages = spaces.flatMap(s => s.pages.map(pg => tidy(count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
|
|
14
18
|
return { name: p.name, pages: pages.length ? pages : [newPage('Floor plan')] };
|
|
15
19
|
}
|