@reekon-tools/boldr-utils 1.8.4 → 1.9.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/dist/annotation/canvas/AnnotationCanvasInner.native.js +41 -10
- package/dist/annotation/canvas/measurementGeometry.js +8 -8
- package/dist/annotation/canvas/stampLayout.js +59 -54
- package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -1
- package/dist/annotation/canvas/useAnnotationCanvasState.js +88 -38
- package/dist/calculator/evaluate.js +38 -4
- package/dist/calculator/expressionUnits.d.ts +84 -0
- package/dist/calculator/expressionUnits.js +439 -0
- package/dist/calculator/index.d.ts +1 -0
- package/dist/calculator/index.js +1 -0
- package/dist/calculator/schema.d.ts +18 -0
- package/dist/calculator/schema.js +26 -5
- package/dist/calculator/validate.d.ts +12 -2
- package/dist/calculator/validate.js +116 -43
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +3 -0
- package/dist/theme/preset.d.ts +120 -0
- package/dist/theme/preset.js +25 -0
- package/dist/theme/semantic.d.ts +136 -0
- package/dist/theme/semantic.js +116 -0
- package/package.json +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +0 -11
- package/dist/canvas/AnnotationCanvas.js +0 -10
- package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
- package/dist/canvas/AnnotationCanvas.native.js +0 -6
- package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
- package/dist/canvas/AnnotationCanvasInner.js +0 -219
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
- package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
- package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
- package/dist/canvas/AnnotationCanvasSkia.js +0 -20
- package/dist/canvas/Tool.d.ts +0 -38
- package/dist/canvas/Tool.js +0 -1
- package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
- package/dist/canvas/elements/BackgroundImageElement.js +0 -37
- package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
- package/dist/canvas/elements/MeasurementStampElement.js +0 -30
- package/dist/canvas/elements/ShapeElement.d.ts +0 -7
- package/dist/canvas/elements/ShapeElement.js +0 -62
- package/dist/canvas/elements/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/measurementPicker.d.ts +0 -10
- package/dist/canvas/measurementPicker.js +0 -1
- package/dist/canvas/measurementStampOverlay.d.ts +0 -11
- package/dist/canvas/measurementStampOverlay.js +0 -1
- package/dist/canvas/pointerAdapter.d.ts +0 -3
- package/dist/canvas/pointerAdapter.js +0 -19
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
- package/dist/canvas/tools/measurementStampTool.js +0 -37
- package/dist/canvas/tools/panTool.d.ts +0 -5
- package/dist/canvas/tools/panTool.js +0 -25
- package/dist/canvas/tools/penTool.d.ts +0 -13
- package/dist/canvas/tools/penTool.js +0 -68
- package/dist/canvas/tools/selectTool.d.ts +0 -2
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
- package/dist/canvas/useAnnotationCanvasState.js +0 -210
- package/dist/canvas/viewport.d.ts +0 -16
- package/dist/canvas/viewport.js +0 -54
- package/dist/data/AnnotationDataContext.d.ts +0 -8
- package/dist/data/AnnotationDataContext.js +0 -11
- package/dist/data/AnnotationDataProvider.d.ts +0 -65
- package/dist/data/AnnotationDataProvider.js +0 -4
- package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
- package/dist/data/InMemoryAnnotationProvider.js +0 -197
- package/dist/data/canvasPersistence.d.ts +0 -3
- package/dist/data/canvasPersistence.js +0 -26
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
- package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
- package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
- package/dist/data/hooks/useAnnotationDoc.js +0 -33
- package/dist/data/hooks/useAnnotationList.d.ts +0 -7
- package/dist/data/hooks/useAnnotationList.js +0 -26
- package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
- package/dist/data/hooks/useAnnotationMutations.js +0 -11
- package/dist/hooks/useParseMeasurement.d.ts +0 -4
- package/dist/hooks/useParseMeasurement.js +0 -14
- package/dist/utils/evaluateFormula.d.ts +0 -20
- package/dist/utils/evaluateFormula.js +0 -31
|
@@ -628,8 +628,9 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
628
628
|
// effect so an in-flight gesture's shared values are never clobbered by a
|
|
629
629
|
// late-flushing setViewport from a previous gesture.
|
|
630
630
|
const activeViewportGestures = useRef(0);
|
|
631
|
-
// >0 while an animated viewport transition (
|
|
632
|
-
//
|
|
631
|
+
// >0 while an animated viewport transition (a gliding handle move —
|
|
632
|
+
// zoomToFit, fitContentToRect, ensureMeasurementVisible) is in flight.
|
|
633
|
+
// Guards the JS→UI sync effect the same way the gesture counter
|
|
633
634
|
// does: mid-glide re-renders must not snap the shared values back to the
|
|
634
635
|
// (still-old) JS snapshot.
|
|
635
636
|
const viewportAnimations = useRef(0);
|
|
@@ -646,7 +647,7 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
646
647
|
panX.value = state.viewport.pan.x;
|
|
647
648
|
panY.value = state.viewport.pan.y;
|
|
648
649
|
}, [state.viewport, zoom, panX, panY]);
|
|
649
|
-
// Animated viewport transitions (
|
|
650
|
+
// Animated viewport transitions (the handle's gliding moves): a JS
|
|
650
651
|
// rAF driver eases the same shared values gestures write — DIRECT writes,
|
|
651
652
|
// three per frame, no React involvement until the single end-of-motion
|
|
652
653
|
// commit. Direct `.value =` writes are the proven repaint channel for the
|
|
@@ -670,8 +671,22 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
670
671
|
animateViewportImplRef.current = (target, durationMs) => {
|
|
671
672
|
cancelViewportDrive();
|
|
672
673
|
const from = { zoom: zoom.value, panX: panX.value, panY: panY.value };
|
|
674
|
+
// Interpolate the world-space view CENTER plus geometric zoom, deriving
|
|
675
|
+
// pan per frame. Pan is the world point at the screen origin (screen =
|
|
676
|
+
// (world − pan)·zoom), so lerping it while zoom sweeps makes the content
|
|
677
|
+
// veer off-line mid-glide; a straight center path with geometric zoom is
|
|
678
|
+
// the standard camera move. Pan-only glides (zoomRatio 1) reduce to a
|
|
679
|
+
// plain world-space lerp.
|
|
680
|
+
const fromCx = from.panX + width / (2 * from.zoom);
|
|
681
|
+
const fromCy = from.panY + height / (2 * from.zoom);
|
|
682
|
+
const toCx = target.pan.x + width / (2 * target.zoom);
|
|
683
|
+
const toCy = target.pan.y + height / (2 * target.zoom);
|
|
684
|
+
const zoomRatio = target.zoom / from.zoom;
|
|
673
685
|
const start = Date.now();
|
|
674
|
-
|
|
686
|
+
// Smootherstep: velocity AND acceleration are zero at both ends, so the
|
|
687
|
+
// glide has no kick at launch and no jolt on landing — easeInOutCubic's
|
|
688
|
+
// abrupt acceleration endpoints read as rigid/mechanical.
|
|
689
|
+
const ease = (t) => t * t * t * (t * (t * 6 - 15) + 10);
|
|
675
690
|
viewportAnimations.current = 1;
|
|
676
691
|
const step = () => {
|
|
677
692
|
if (activeViewportGestures.current > 0) {
|
|
@@ -680,10 +695,11 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
680
695
|
return;
|
|
681
696
|
}
|
|
682
697
|
const t = Math.min(1, (Date.now() - start) / durationMs);
|
|
683
|
-
const eased =
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
698
|
+
const eased = ease(t);
|
|
699
|
+
const z = from.zoom * zoomRatio ** eased;
|
|
700
|
+
zoom.value = z;
|
|
701
|
+
panX.value = fromCx + (toCx - fromCx) * eased - width / (2 * z);
|
|
702
|
+
panY.value = fromCy + (toCy - fromCy) * eased - height / (2 * z);
|
|
687
703
|
if (t < 1) {
|
|
688
704
|
viewportDriveRef.current = requestAnimationFrame(step);
|
|
689
705
|
}
|
|
@@ -1529,11 +1545,20 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
|
|
|
1529
1545
|
const anchorX = placed.anchor.x;
|
|
1530
1546
|
const anchorY = placed.anchor.y;
|
|
1531
1547
|
// doc → screen each frame, on the UI thread. Position is a translate
|
|
1532
|
-
// transform (cheap, no layout) so the
|
|
1548
|
+
// transform (cheap, no layout) so the tile stays glued to its anchor. While
|
|
1533
1549
|
// dragging, the world-space drag offset is folded in; while sliding, the
|
|
1534
1550
|
// position is mapped onto the line. `dragging`/`sliding` are captured props,
|
|
1535
1551
|
// so the moment they flip false (on commit) the worklet drops the live offset
|
|
1536
1552
|
// in the same render the new anchor arrives — no flicker.
|
|
1553
|
+
//
|
|
1554
|
+
// Size: the LAID-OUT box is the tile's screen size at `zoomSnapshot` (the
|
|
1555
|
+
// JS viewport this render was built from). Tiles scale with zoom, so while
|
|
1556
|
+
// a pinch/glide has the live zoom ahead of that snapshot the worklet
|
|
1557
|
+
// bridges the gap with a scale transform (about the centered anchor);
|
|
1558
|
+
// when the gesture ends and the JS snapshot syncs, the item re-renders at
|
|
1559
|
+
// the final size and the scale settles back to exactly 1 — the same
|
|
1560
|
+
// captured-prop handoff the drag offsets use, so there is no jump.
|
|
1561
|
+
const zoomBase = zoomSnapshot > 0 ? zoomSnapshot : 1;
|
|
1537
1562
|
const animatedStyle = useAnimatedStyle(() => {
|
|
1538
1563
|
'worklet';
|
|
1539
1564
|
let worldX = anchorX;
|
|
@@ -1581,7 +1606,13 @@ const MeasurementStampOverlayItem = ({ placed, measurement, selected, dragging,
|
|
|
1581
1606
|
const cx = (worldX - panX.value) * zoom.value;
|
|
1582
1607
|
const cy = (worldY - panY.value) * zoom.value;
|
|
1583
1608
|
return {
|
|
1584
|
-
transform: [
|
|
1609
|
+
transform: [
|
|
1610
|
+
{ translateX: cx - halfW },
|
|
1611
|
+
{ translateY: cy - halfH },
|
|
1612
|
+
// Scales about the box center, which the translate just pinned to
|
|
1613
|
+
// the anchor — so the tile grows/shrinks in place.
|
|
1614
|
+
{ scale: zoom.value / zoomBase },
|
|
1615
|
+
],
|
|
1585
1616
|
};
|
|
1586
1617
|
});
|
|
1587
1618
|
return (_jsxs(Animated.View, { pointerEvents: "box-none", style: [
|
|
@@ -106,17 +106,17 @@ const segmentDistSq = (p, a, b) => {
|
|
|
106
106
|
const dy = p.y - (a.y + t * aby);
|
|
107
107
|
return dx * dx + dy * dy;
|
|
108
108
|
};
|
|
109
|
-
// Whether a world-space point hits a placed measurement: the
|
|
110
|
-
//
|
|
109
|
+
// Whether a world-space point hits a placed measurement: the stamp tile
|
|
110
|
+
// around the anchor, the line body of a line annotation, or the
|
|
111
111
|
// border ring of a rectangle annotation (interiors stay transparent to hits so
|
|
112
112
|
// elements inside remain reachable).
|
|
113
113
|
export const hitPlacedMeasurement = (m, p, zoom = 1, tileScaleFactor = 1, viewportScale = 1) => {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// the square footprint; group-header chips are a wide pill —
|
|
119
|
-
// stampTileDims in stampLayout).
|
|
114
|
+
// stampTileDims yields the tile's CURRENT screen-space box (the
|
|
115
|
+
// zoom-relative multiplier arrives via `viewportScale`); converting the
|
|
116
|
+
// half-extents (+ padding) back to doc space via the zoom keeps the hit box
|
|
117
|
+
// matching what's drawn — a doc-constant footprint (blank and populated
|
|
118
|
+
// stamps share the square footprint; group-header chips are a wide pill —
|
|
119
|
+
// see stampTileDims in stampLayout).
|
|
120
120
|
const dims = stampTileDims(m, tileScaleFactor, viewportScale);
|
|
121
121
|
const halfW = (dims.width / 2 + STAMP_HIT_PADDING) / zoom;
|
|
122
122
|
const halfH = (dims.height / 2 + STAMP_HIT_PADDING) / zoom;
|
|
@@ -3,26 +3,31 @@
|
|
|
3
3
|
// overlay — see measurementStampOverlay.ts) and the hit-test (selectTool,
|
|
4
4
|
// measurementGeometry) can import it without dragging
|
|
5
5
|
// @shopify/react-native-skia into the consumer's static import graph.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `placed.scale
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
6
|
+
// Base edge length of a placed measurement, rendered as a square tile: the
|
|
7
|
+
// tile's on-screen size at the document's content-fit zoom, times
|
|
8
|
+
// `placed.scale`. Tiles scale WITH the zoom from there (the zoom-relative
|
|
9
|
+
// `viewportScale` multiplier below — Asana 1216878199784924, matching the
|
|
10
|
+
// Nutrient viewer), so a tile's DOC-space footprint is constant like any
|
|
11
|
+
// other element on the drawing. Also drives the select-tool hit box, which
|
|
12
|
+
// converts the same footprint back to doc space via the zoom. Blank "+"
|
|
13
|
+
// input placeholders take the SAME footprint as populated tiles: earlier
|
|
14
|
+
// builds gave them a compact 44px size, but then a user who set the
|
|
15
|
+
// tile-scale slider against blank tiles saw their tiles balloon once
|
|
16
|
+
// populated (Asana 1216233394800417).
|
|
14
17
|
export const STAMP_TILE_SIZE = 96;
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// header stamp (150×40).
|
|
18
|
-
//
|
|
18
|
+
// Base footprint of a group-header chip (`isGroupHeader`) — a wide pill
|
|
19
|
+
// rather than the square tile, same 3.75:1 ratio as the legacy Nutrient
|
|
20
|
+
// header stamp (150×40). Sized exactly like STAMP_TILE_SIZE: `placed.scale`
|
|
21
|
+
// × the document-wide tile-scale factor, scaling with zoom from the
|
|
22
|
+
// content-fit reference.
|
|
19
23
|
export const GROUP_HEADER_TILE_WIDTH = 240;
|
|
20
24
|
export const GROUP_HEADER_TILE_HEIGHT = 64;
|
|
21
25
|
// Document-wide tile scale factor: one knob that shrinks/grows EVERY
|
|
22
26
|
// measurement tile on the canvas at once, on top of each tile's own `scale`.
|
|
23
27
|
// Lets a user pull tiles down on a dense drawing where lines crowd together,
|
|
24
|
-
// or bump them up on a sparse one. Like `scale` it is
|
|
25
|
-
//
|
|
28
|
+
// or bump them up on a sparse one. Like `scale` it is a plain multiplier on
|
|
29
|
+
// the tile footprint, orthogonal to the zoom-relative `viewportScale`.
|
|
30
|
+
// Absent === DEFAULT_TILE_SCALE
|
|
26
31
|
// (visually identical to documents written before the knob existed).
|
|
27
32
|
// Web and mobile persist separate knobs (AnnotationCanvasState.tileScaleFactor
|
|
28
33
|
// vs .tileScaleFactorMobile) — read them through resolveTileScaleFactor below.
|
|
@@ -44,41 +49,40 @@ export const resolveTileScaleFactor = (canvas, platform) => (platform === 'mobil
|
|
|
44
49
|
// for the value before it lands in the document (slider input, restored docs).
|
|
45
50
|
export const clampTileScale = (v) => v < TILE_SCALE_MIN ? TILE_SCALE_MIN : v > TILE_SCALE_MAX ? TILE_SCALE_MAX : v;
|
|
46
51
|
// --- Cross-platform initial-size calibration ---------------------------------
|
|
47
|
-
// The platform knobs are
|
|
48
|
-
// very different share of the DRAWING on a phone than on a desktop pane. To
|
|
52
|
+
// The platform knobs are fit-referenced multipliers, so the same value covers
|
|
53
|
+
// a very different share of the DRAWING on a phone than on a desktop pane. To
|
|
49
54
|
// hand mobile a sensible starting size, web persists the doc-space edge length
|
|
50
|
-
// a scale-1 tile
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
55
|
+
// a scale-1 tile covers (`tileDocSize`, stamped on tile placement and web
|
|
56
|
+
// slider commits); a mobile canvas with no own knob yet inverts that at its
|
|
57
|
+
// own content-fit zoom so its tiles cover the SAME area of the image. Two
|
|
58
|
+
// inverses of one identity: fitScreenSize = docSize × fitZoom. Both sides pass
|
|
59
|
+
// the content-fit reference zoom (tiles are doc-constant, so the CURRENT zoom
|
|
60
|
+
// has no bearing on how much of the drawing a tile covers).
|
|
61
|
+
// Doc-space edge length a scale-1 tile covers under `factor`, given the
|
|
62
|
+
// canvas's content-fit reference `zoom` — the web write side of the
|
|
63
|
+
// calibration. Non-positive zoom yields 0, which the setTileDocSize op
|
|
64
|
+
// ignores.
|
|
57
65
|
export const tileDocSizeForFactor = (factor, zoom) => zoom > 0 ? (STAMP_TILE_SIZE * factor) / zoom : 0;
|
|
58
|
-
// Tile-scale factor that makes a scale-1 tile cover `docSize` doc units
|
|
59
|
-
// `zoom` — the mobile read side of
|
|
60
|
-
// to the slider range: faithfully
|
|
61
|
-
// TILE_SCALE_MIN on a phone, and
|
|
62
|
-
// value. The clamp applies once the
|
|
66
|
+
// Tile-scale factor that makes a scale-1 tile cover `docSize` doc units,
|
|
67
|
+
// given the canvas's content-fit reference `zoom` — the mobile read side of
|
|
68
|
+
// the calibration. Deliberately NOT clamped to the slider range: faithfully
|
|
69
|
+
// matching the web look can land below TILE_SCALE_MIN on a phone, and
|
|
70
|
+
// rendering/hit-testing handle any positive value. The clamp applies once the
|
|
71
|
+
// user drives the slider themselves.
|
|
63
72
|
export const tileScaleFromDocSize = (docSize, zoom) => (docSize * zoom) / STAMP_TILE_SIZE;
|
|
64
|
-
// ---
|
|
65
|
-
// A measurement tile's footprint is base × per-tile `scale` × the
|
|
66
|
-
// `tileScaleFactor`
|
|
67
|
-
// the same document shows the same tiles at any canvas/window size.
|
|
73
|
+
// --- How the footprint composes ----------------------------------------------
|
|
74
|
+
// A measurement tile's screen footprint is base × per-tile `scale` × the
|
|
75
|
+
// document-wide `tileScaleFactor` × the zoom-relative `viewportScale`.
|
|
68
76
|
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// `stampTileSize` keeps its `viewportScale` parameter (default 1) only so the
|
|
80
|
-
// render overlay, hit-test, and tools that thread it stay call-compatible; it is
|
|
81
|
-
// now always 1.
|
|
77
|
+
// `viewportScale` is the canvas's CURRENT zoom over its content-fit reference
|
|
78
|
+
// zoom (see tileViewportScale in useAnnotationCanvasState): 1 at the fit
|
|
79
|
+
// view, growing/shrinking with pinch/wheel zoom so tiles track the drawing
|
|
80
|
+
// like every other element (Asana 1216878199784924 — the Nutrient behavior).
|
|
81
|
+
// Earlier builds pinned tiles to a constant screen size regardless of zoom;
|
|
82
|
+
// users kept pinch-zooming to read a tile and it refused to get bigger.
|
|
83
|
+
// (An even earlier per-canvas auto-multiplier — tile size derived from the
|
|
84
|
+
// canvas's pixel size — was retired for the user-driven "Tile size" slider,
|
|
85
|
+
// which remains: the slider and the zoom multiplier compose.)
|
|
82
86
|
// A placed measurement is an unassociated input until a measurement reference
|
|
83
87
|
// is attached (id or path) OR it is bound to a form column (`columnId`). Such
|
|
84
88
|
// stamps render as the blank "+" placeholder (same footprint as a populated
|
|
@@ -86,15 +90,16 @@ export const tileScaleFromDocSize = (docSize, zoom) => (docSize * zoom) / STAMP_
|
|
|
86
90
|
// NOT inputs — never treat them as blank "+" placeholders.
|
|
87
91
|
export const isUnassociatedStamp = (m) => !m.isGroupHeader && !m.measurementId && !m.measurementPath && !m.columnId;
|
|
88
92
|
// Screen-space edge length for a placed stamp: the base size scaled by the
|
|
89
|
-
// per-stamp `scale
|
|
90
|
-
// populated stamps share the
|
|
91
|
-
//
|
|
92
|
-
// hit-test, and slide-grab
|
|
93
|
-
//
|
|
94
|
-
// (default 1), driven by
|
|
95
|
-
// caller's platform via
|
|
96
|
-
//
|
|
97
|
-
//
|
|
93
|
+
// per-stamp `scale`, the document-wide `tileScaleFactor`, and the
|
|
94
|
+
// zoom-relative `viewportScale`. Blank and populated stamps share the
|
|
95
|
+
// footprint so a tile never resizes when it gains a value. The ONE source of
|
|
96
|
+
// truth for tile footprint — render overlay, hit-test, and slide-grab
|
|
97
|
+
// classification all call this so the drawn tile and its touch box always
|
|
98
|
+
// agree. `tileScaleFactor` is the document-wide knob (default 1), driven by
|
|
99
|
+
// the "Tile size" slider and already resolved for the caller's platform via
|
|
100
|
+
// resolveTileScaleFactor above. `viewportScale` is the current zoom over the
|
|
101
|
+
// canvas's content-fit reference zoom (see the note above), so tiles scale
|
|
102
|
+
// with the drawing as the user zooms.
|
|
98
103
|
export const stampTileSize = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => STAMP_TILE_SIZE * (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
99
104
|
export const stampTileDims = (m, tileScaleFactor = DEFAULT_TILE_SCALE, viewportScale = 1) => {
|
|
100
105
|
const k = (m.scale ?? 1) * tileScaleFactor * viewportScale;
|
|
@@ -14,7 +14,10 @@ export interface AnnotationCanvasHandle {
|
|
|
14
14
|
redo(): void;
|
|
15
15
|
canUndo(): boolean;
|
|
16
16
|
canRedo(): boolean;
|
|
17
|
-
zoomToFit(
|
|
17
|
+
zoomToFit(opts?: {
|
|
18
|
+
animated?: boolean;
|
|
19
|
+
durationMs?: number;
|
|
20
|
+
}): void;
|
|
18
21
|
resetView(): void;
|
|
19
22
|
fitContentToRect(rect: CanvasScreenRect, opts?: {
|
|
20
23
|
animated?: boolean;
|
|
@@ -11,10 +11,11 @@ import { buildRemoveMeasurementOps, recomputeAnchor, rectCenter, DEFAULT_LINE_PO
|
|
|
11
11
|
// too: authors routinely drop them AROUND the image (dimension labels sit
|
|
12
12
|
// outside the object they measure), and a fit framed on the image alone opens
|
|
13
13
|
// with those tiles off-screen — so the rect is widened to the union of the
|
|
14
|
-
// base rect and every placed tile's doc geometry. Tiles are
|
|
15
|
-
//
|
|
16
|
-
// rect
|
|
17
|
-
// tiles all sit inside the image keeps the exact image fit it
|
|
14
|
+
// base rect and every placed tile's doc geometry. Tiles are boxes centered on
|
|
15
|
+
// that geometry (≈ base size on screen at the fit view), so when the union
|
|
16
|
+
// extends past the base rect a half-tile of screen-space padding is reserved;
|
|
17
|
+
// a canvas whose tiles all sit inside the image keeps the exact image fit it
|
|
18
|
+
// had before.
|
|
18
19
|
// Shared by the load-time auto-fit and the zoomToFit/resetView/
|
|
19
20
|
// fitContentToRect handle methods so they stay in lockstep.
|
|
20
21
|
const computeContentFitRect = (canvas, rect) => {
|
|
@@ -63,13 +64,30 @@ const computeContentFitRect = (canvas, rect) => {
|
|
|
63
64
|
};
|
|
64
65
|
};
|
|
65
66
|
const computeContentFit = (canvas, width, height) => computeContentFitRect(canvas, { x: 0, y: 0, width, height });
|
|
67
|
+
// Content-fit zoom of the DOCUMENT alone — the background image's rendered
|
|
68
|
+
// rect when present, the document rect otherwise, deliberately ignoring
|
|
69
|
+
// placed tiles — into the full canvas box. This is the stable reference zoom
|
|
70
|
+
// for tile sizing (see tileViewportScale below): at this zoom a tile renders
|
|
71
|
+
// at exactly its slider size. computeContentFitRect's tile-extended union
|
|
72
|
+
// would make the reference move whenever a tile is dragged outside the image
|
|
73
|
+
// (every tile on the canvas would resize); the document rect can't.
|
|
74
|
+
const computeBaseFitZoom = (canvas, width, height) => {
|
|
75
|
+
if (!(width > 0) || !(height > 0))
|
|
76
|
+
return 1;
|
|
77
|
+
const { viewport } = canvas;
|
|
78
|
+
const bg = viewport.backgroundImage;
|
|
79
|
+
const baseRect = bg
|
|
80
|
+
? imageDocRect(bg.widthPx, bg.heightPx, viewport.width, viewport.height, viewport.backgroundFit ?? 'contain')
|
|
81
|
+
: { x: 0, y: 0, width: viewport.width, height: viewport.height };
|
|
82
|
+
return fitRectToScreen(baseRect, width, height).zoom;
|
|
83
|
+
};
|
|
66
84
|
// Default leader length (doc units) for a placed measurement when no explicit
|
|
67
85
|
// length is given. A generous fraction of the document width, clamped, so the
|
|
68
|
-
// leader — and its draggable endcaps — stay clear of the
|
|
69
|
-
//
|
|
70
|
-
// under the tile).
|
|
71
|
-
//
|
|
72
|
-
//
|
|
86
|
+
// leader — and its draggable endcaps — stay clear of the value tile at
|
|
87
|
+
// typical full-fit zoom (Asana 1216212322376723: leaders were collapsing
|
|
88
|
+
// under the tile). Tiles are doc-constant boxes like the line itself, so the
|
|
89
|
+
// clearance holds at any zoom, but the clamp can still cap the length short
|
|
90
|
+
// of it on very large drawings.
|
|
73
91
|
const defaultLeaderLenDoc = (docWidth) => Math.min(800, Math.max(240, docWidth * 0.45));
|
|
74
92
|
// Platform-agnostic state machine for the annotation canvas. Web and native
|
|
75
93
|
// inners share this hook; each wraps it with platform-specific event
|
|
@@ -109,13 +127,16 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
109
127
|
didInitialFitRef.current = true;
|
|
110
128
|
setViewport(computeContentFit(canvas, width, height));
|
|
111
129
|
}, [canvas, width, height]);
|
|
112
|
-
// Tiles
|
|
113
|
-
// per-tile scale ×
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
130
|
+
// Tiles scale WITH the zoom (Asana 1216878199784924 — match the Nutrient
|
|
131
|
+
// viewer): a tile renders at base × per-tile scale × `tileScaleFactor` at
|
|
132
|
+
// the document's content-fit zoom, and grows/shrinks proportionally as the
|
|
133
|
+
// user zooms — its DOC-space footprint is constant, like any other element
|
|
134
|
+
// on the drawing. `tileViewportScale` carries the zoom-relative multiplier
|
|
135
|
+
// through the existing plumbing: overlays multiply the drawn size by it and
|
|
136
|
+
// hit-tests divide the same footprint by the current zoom, so the drawn
|
|
137
|
+
// tile and its touch box agree by construction.
|
|
138
|
+
const baseFitZoom = computeBaseFitZoom(effectiveCanvas, width, height);
|
|
139
|
+
const tileViewportScale = baseFitZoom > 0 ? viewport.zoom / baseFitZoom : 1;
|
|
119
140
|
// The platform's tile-scale knob, resolved once from the effective canvas so
|
|
120
141
|
// tools (hit boxes) and overlays (drawn size) share one value. Uses the
|
|
121
142
|
// effective (preview-applied) canvas for parity with what is on screen; the
|
|
@@ -133,7 +154,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
133
154
|
effectiveCanvas.tileDocSize > 0 &&
|
|
134
155
|
width > 0 &&
|
|
135
156
|
height > 0) {
|
|
136
|
-
tileScaleFactor = tileScaleFromDocSize(effectiveCanvas.tileDocSize,
|
|
157
|
+
tileScaleFactor = tileScaleFromDocSize(effectiveCanvas.tileDocSize, baseFitZoom);
|
|
137
158
|
}
|
|
138
159
|
const ctx = useMemo(() => ({
|
|
139
160
|
document: canvas,
|
|
@@ -146,15 +167,17 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
146
167
|
},
|
|
147
168
|
commit(patch) {
|
|
148
169
|
// Placing a tile on WEB stamps the doc-space calibration: the area of
|
|
149
|
-
// the drawing a scale-1 tile covers
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
170
|
+
// the drawing a scale-1 tile covers. Tiles are doc-constant boxes
|
|
171
|
+
// (base size at the content-fit zoom, scaling with zoom from there),
|
|
172
|
+
// so the calibration derives from the fit reference — the current
|
|
173
|
+
// zoom no longer changes how much of the drawing a tile covers.
|
|
174
|
+
// Mobile derives its initial factor from it (see tileScaleFactor
|
|
175
|
+
// above). Appended into the same patch so placement + calibration are
|
|
176
|
+
// one undo step.
|
|
154
177
|
let effective = patch;
|
|
155
178
|
if (tileScalePlatformResolved !== 'mobile' &&
|
|
156
179
|
patch.ops.some((o) => o.op === 'addMeasurement')) {
|
|
157
|
-
const docSize = tileDocSizeForFactor(tileScaleFactor,
|
|
180
|
+
const docSize = tileDocSizeForFactor(tileScaleFactor, baseFitZoom);
|
|
158
181
|
if (docSize > 0) {
|
|
159
182
|
effective = {
|
|
160
183
|
ops: [...patch.ops, { op: 'setTileDocSize', value: docSize }],
|
|
@@ -191,6 +214,7 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
191
214
|
tileViewportScale,
|
|
192
215
|
tileScaleFactor,
|
|
193
216
|
tileScalePlatformResolved,
|
|
217
|
+
baseFitZoom,
|
|
194
218
|
onCommit,
|
|
195
219
|
onSelectionChange,
|
|
196
220
|
pickMeasurement,
|
|
@@ -200,6 +224,9 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
200
224
|
// would otherwise capture a stale ctx/viewport).
|
|
201
225
|
const ctxRef = useRef(ctx);
|
|
202
226
|
ctxRef.current = ctx;
|
|
227
|
+
// Ditto for the tile-sizing fit reference (getTileDocSize).
|
|
228
|
+
const baseFitZoomRef = useRef(baseFitZoom);
|
|
229
|
+
baseFitZoomRef.current = baseFitZoom;
|
|
203
230
|
// Tool hand-over. When the active tool's identity changes — a real tool
|
|
204
231
|
// switch OR the consumer rebuilding the tools array (e.g. a style change
|
|
205
232
|
// recreates every factory) — give the outgoing instance a chance to wind
|
|
@@ -314,8 +341,8 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
314
341
|
// down-right in fixed screen-px steps from center until a slot with no
|
|
315
342
|
// existing anchor is found. Occupancy is derived from the live placed
|
|
316
343
|
// measurements (not a drop counter), so moving or deleting a tile frees
|
|
317
|
-
// its slot automatically. Screen space is the right frame —
|
|
318
|
-
//
|
|
344
|
+
// its slot automatically. Screen space is the right frame — the drop
|
|
345
|
+
// lands where the user is looking — and mirrors the Nutrient cascade.
|
|
319
346
|
const cascadeCenterWorld = () => {
|
|
320
347
|
const c = ctxRef.current;
|
|
321
348
|
const CASCADE_STEP = 30;
|
|
@@ -356,21 +383,31 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
356
383
|
canRedo() {
|
|
357
384
|
return redoStackRef.current.length > 0;
|
|
358
385
|
},
|
|
359
|
-
zoomToFit() {
|
|
360
|
-
|
|
386
|
+
zoomToFit(opts) {
|
|
387
|
+
// The document is read through ctxRef, not the effect closure: this
|
|
388
|
+
// effect's deps deliberately exclude `canvas`, so the closed-over
|
|
389
|
+
// binding can predate tiles that landed or moved since — and the fit
|
|
390
|
+
// unions placed-tile geometry.
|
|
391
|
+
const target = computeContentFit(ctxRef.current.document, width, height);
|
|
392
|
+
if (opts?.animated && animateViewport) {
|
|
393
|
+
animateViewport(target, opts.durationMs ?? 650);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
setViewport(target);
|
|
397
|
+
}
|
|
361
398
|
},
|
|
362
399
|
resetView() {
|
|
363
400
|
// "Reset view" frames the whole document/image again — the same fit the
|
|
364
401
|
// canvas opens with — rather than snapping to a 1:1 top-left view, which
|
|
365
402
|
// for a high-res background is the very crop this is meant to escape.
|
|
366
|
-
setViewport(computeContentFit(
|
|
403
|
+
setViewport(computeContentFit(ctxRef.current.document, width, height));
|
|
367
404
|
},
|
|
368
405
|
fitContentToRect(rect, opts) {
|
|
369
406
|
if (!(rect.width > 0) || !(rect.height > 0))
|
|
370
407
|
return;
|
|
371
|
-
const target = computeContentFitRect(
|
|
408
|
+
const target = computeContentFitRect(ctxRef.current.document, rect);
|
|
372
409
|
if (opts?.animated && animateViewport) {
|
|
373
|
-
animateViewport(target, opts.durationMs ??
|
|
410
|
+
animateViewport(target, opts.durationMs ?? 450);
|
|
374
411
|
}
|
|
375
412
|
else {
|
|
376
413
|
setViewport(target);
|
|
@@ -380,15 +417,17 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
380
417
|
return ctxRef.current.tileScaleFactor;
|
|
381
418
|
},
|
|
382
419
|
getTileDocSize(factor) {
|
|
383
|
-
|
|
420
|
+
// Tiles are doc-constant, so the doc size a factor yields derives
|
|
421
|
+
// from the content-fit reference, not the current zoom.
|
|
422
|
+
return tileDocSizeForFactor(factor, baseFitZoomRef.current);
|
|
384
423
|
},
|
|
385
424
|
ensureMeasurementVisible(id) {
|
|
386
425
|
const c = ctxRef.current;
|
|
387
426
|
const m = c.document.placedMeasurements.find((x) => x.id === id);
|
|
388
427
|
if (!m)
|
|
389
428
|
return;
|
|
390
|
-
// The tile
|
|
391
|
-
//
|
|
429
|
+
// The tile's CURRENT on-screen half-extents: stampTileDims with the
|
|
430
|
+
// zoom-relative tileViewportScale folded in.
|
|
392
431
|
const dims = stampTileDims(m, c.tileScaleFactor, c.tileViewportScale);
|
|
393
432
|
const halfW = dims.width / 2;
|
|
394
433
|
const halfH = dims.height / 2;
|
|
@@ -399,13 +438,24 @@ export const useAnnotationCanvasState = (props) => {
|
|
|
399
438
|
s.y + halfH <= height) {
|
|
400
439
|
return;
|
|
401
440
|
}
|
|
402
|
-
|
|
403
|
-
|
|
441
|
+
// Glide, don't snap: this fires mid-flow (a value just landed and the
|
|
442
|
+
// selection auto-advanced), where an instant recenter reads as the
|
|
443
|
+
// canvas hopping around. Same fallback as fitContentToRect when the
|
|
444
|
+
// renderer has no animation hook (web).
|
|
445
|
+
const zoomNow = c.viewport.state.zoom;
|
|
446
|
+
const target = {
|
|
447
|
+
zoom: zoomNow,
|
|
404
448
|
pan: {
|
|
405
|
-
x: m.anchor.x - width / (2 *
|
|
406
|
-
y: m.anchor.y - height / (2 *
|
|
449
|
+
x: m.anchor.x - width / (2 * zoomNow),
|
|
450
|
+
y: m.anchor.y - height / (2 * zoomNow),
|
|
407
451
|
},
|
|
408
|
-
}
|
|
452
|
+
};
|
|
453
|
+
if (animateViewport) {
|
|
454
|
+
animateViewport(target, 450);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
setViewport(target);
|
|
458
|
+
}
|
|
409
459
|
},
|
|
410
460
|
placeMeasurementAtCenter(ref) {
|
|
411
461
|
const c = ctxRef.current;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { create, all } from 'mathjs';
|
|
2
2
|
import { ColumnType } from '../types/firestore.js';
|
|
3
3
|
import { equationForField, findField } from './schema.js';
|
|
4
|
+
import { fromCanonical, toCanonical } from './units.js';
|
|
4
5
|
// Trig in calculator equations works in DEGREES: angle fields are stored in
|
|
5
6
|
// degrees canonically, and construction authors write `H = W * tan(A)`
|
|
6
7
|
// expecting A in degrees. mathjs has no `angle` config option (evaluateFormula
|
|
@@ -125,7 +126,20 @@ const evaluateEquationInContext = (equation, ctx) => {
|
|
|
125
126
|
}
|
|
126
127
|
return resolved;
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
+
// Field values resolve canonically; a variable unit re-expresses the
|
|
130
|
+
// value in the author's chosen display unit before it enters the scope.
|
|
131
|
+
const unit = equation.variableUnits?.[variable];
|
|
132
|
+
try {
|
|
133
|
+
scope[variable] =
|
|
134
|
+
unit != null ? fromCanonical(resolved.value, unit) : resolved.value;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reason: 'error',
|
|
140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
129
143
|
}
|
|
130
144
|
if (missing.length > 0) {
|
|
131
145
|
const result = {
|
|
@@ -137,9 +151,29 @@ const evaluateEquationInContext = (equation, ctx) => {
|
|
|
137
151
|
return result;
|
|
138
152
|
}
|
|
139
153
|
const evaluated = evaluateExpression(equation.expression, scope);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
let result;
|
|
155
|
+
if (!evaluated.ok) {
|
|
156
|
+
result = { ok: false, reason: 'error', error: evaluated.error };
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// A result unit means the expression produced a display-unit value;
|
|
160
|
+
// convert back so the returned value is canonical like everything else.
|
|
161
|
+
try {
|
|
162
|
+
result = {
|
|
163
|
+
ok: true,
|
|
164
|
+
value: equation.resultUnit != null
|
|
165
|
+
? toCanonical(evaluated.value, equation.resultUnit)
|
|
166
|
+
: evaluated.value,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
result = {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: 'error',
|
|
173
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
143
177
|
ctx.cache.set(equation.id, result);
|
|
144
178
|
return result;
|
|
145
179
|
}
|