@reekon-tools/boldr-utils 1.17.4 → 1.18.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.
Files changed (71) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +8 -2
  2. package/dist/annotation/canvas/AnnotationCanvasSkia.js +8 -9
  3. package/dist/annotation/canvas/Tool.d.ts +2 -0
  4. package/dist/annotation/canvas/backgroundLayers.d.ts +3 -1
  5. package/dist/annotation/canvas/backgroundLayers.js +23 -8
  6. package/dist/annotation/canvas/tools/selectTool.js +45 -26
  7. package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +9 -2
  8. package/dist/exports.d.ts +1 -1
  9. package/dist/exports.js +1 -1
  10. package/dist/types/annotation.d.ts +1 -0
  11. package/package.json +1 -1
  12. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  13. package/dist/canvas/AnnotationCanvas.js +0 -10
  14. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  15. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  16. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  17. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  18. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  19. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  20. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  21. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  22. package/dist/canvas/Tool.d.ts +0 -38
  23. package/dist/canvas/Tool.js +0 -1
  24. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  25. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  26. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  27. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  28. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  29. package/dist/canvas/elements/ShapeElement.js +0 -62
  30. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  31. package/dist/canvas/elements/StrokeElement.js +0 -18
  32. package/dist/canvas/measurementPicker.d.ts +0 -10
  33. package/dist/canvas/measurementPicker.js +0 -1
  34. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  35. package/dist/canvas/measurementStampOverlay.js +0 -1
  36. package/dist/canvas/pointerAdapter.d.ts +0 -3
  37. package/dist/canvas/pointerAdapter.js +0 -19
  38. package/dist/canvas/stampLayout.d.ts +0 -5
  39. package/dist/canvas/stampLayout.js +0 -14
  40. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  41. package/dist/canvas/tools/measurementStampTool.js +0 -37
  42. package/dist/canvas/tools/panTool.d.ts +0 -5
  43. package/dist/canvas/tools/panTool.js +0 -25
  44. package/dist/canvas/tools/penTool.d.ts +0 -13
  45. package/dist/canvas/tools/penTool.js +0 -68
  46. package/dist/canvas/tools/selectTool.d.ts +0 -2
  47. package/dist/canvas/tools/selectTool.js +0 -182
  48. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  49. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  50. package/dist/canvas/viewport.d.ts +0 -16
  51. package/dist/canvas/viewport.js +0 -54
  52. package/dist/data/AnnotationDataContext.d.ts +0 -8
  53. package/dist/data/AnnotationDataContext.js +0 -11
  54. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  55. package/dist/data/AnnotationDataProvider.js +0 -4
  56. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  57. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  58. package/dist/data/canvasPersistence.d.ts +0 -3
  59. package/dist/data/canvasPersistence.js +0 -26
  60. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  61. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  62. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  63. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  64. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  65. package/dist/data/hooks/useAnnotationList.js +0 -26
  66. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  67. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  68. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  69. package/dist/hooks/useParseMeasurement.js +0 -14
  70. package/dist/utils/evaluateFormula.d.ts +0 -20
  71. package/dist/utils/evaluateFormula.js +0 -31
@@ -1469,10 +1469,16 @@ export const AnnotationCanvasInner = (props) => {
1469
1469
  // drag once every annotation element misses, and only when ALREADY
1470
1470
  // selected (tap-to-select flows through the tool's pointer
1471
1471
  // handlers). The hit rides the exact same dragX/dragY transform and
1472
- // buildTranslatePatch commit as any other element.
1472
+ // buildTranslatePatch commit as any other element. A LOCKED layer
1473
+ // never moves: the drag is inert, and the selection stays so the
1474
+ // chrome's lock toggle remains reachable.
1473
1475
  const bg = cfg.hitTestSelectedBackground?.(st.ctx.document, world, st.ctx.selection?.ids ?? []);
1474
1476
  if (bg) {
1475
- dragTargetRef.current = bg;
1477
+ if (bg.locked) {
1478
+ dragTargetRef.current = null;
1479
+ return;
1480
+ }
1481
+ dragTargetRef.current = { id: bg.id, kind: bg.kind };
1476
1482
  setDraggingId(bg.id);
1477
1483
  return;
1478
1484
  }
@@ -4,7 +4,7 @@ import { normalizeRect, placementOf } from './measurementGeometry.js';
4
4
  import { arrowheadTriangle, dashIntervals, toSkiaStrokeCap, } from './strokeGeometry.js';
5
5
  import { SELECTION_PAD, textResizeGeometry, textShapeBounds, } from './textGeometry.js';
6
6
  import { visualShapeBounds } from './shapeGeometry.js';
7
- import { backgroundLayerDocRect, backgroundLayersOf, canEditBackgroundLayers, } from './backgroundLayers.js';
7
+ import { backgroundLayerDocRect, backgroundLayersOf, isBackgroundLayerLocked, } from './backgroundLayers.js';
8
8
  import { BackgroundLayerElement } from './elements/BackgroundImageElement.js';
9
9
  import { PlaneElement, PLANE_CHROME_COLOR, PLANE_GRID_COLOR, PLANE_GRID_OPACITY, PLANE_GRID_WIDTH, PLANE_OUTLINE_WIDTH, } from './elements/PlaneElement.js';
10
10
  import { ShapeElement } from './elements/ShapeElement.js';
@@ -253,18 +253,17 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
253
253
  // bottom-right handle — backgrounds scale uniformly about their
254
254
  // top-left corner (the transform origin), so a single opposite-corner
255
255
  // grab is the whole gizmo. Chrome tracks the live drag/resize
256
- // transform the same way the layer itself does above. A lone
257
- // background is never editable (canEditBackgroundLayers), so it gets
258
- // no chrome even when a stale selection still names it e.g. right
259
- // after the other layer of a pair was removed.
260
- const layer = canEditBackgroundLayers(effectiveCanvas.viewport)
261
- ? backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId)
262
- : undefined;
256
+ // transform the same way the layer itself does above. A LOCKED layer
257
+ // (backgroundLayerIsLocked) keeps the outline it is selected, and
258
+ // the host's chrome offers the lock toggle but gets no handle: it
259
+ // cannot scale, so a grabbable corner would lie.
260
+ const layer = backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId);
263
261
  if (layer) {
262
+ const locked = isBackgroundLayerLocked(effectiveCanvas.viewport, layer.id);
264
263
  const r = backgroundLayerDocRect(layer);
265
264
  const isResizing = layer.id === resizingId;
266
265
  const liveTransform = isResizing ? resizeTransform : dragTransform;
267
- return (_jsxs(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: [_jsx(Rect, { x: r.x, y: r.y, width: r.width, height: r.height, color: SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }), handleRadius != null && (_jsx(Handle, { c: { x: r.x + r.width, y: r.y + r.height }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }))] }));
266
+ return (_jsxs(DraggableElement, { isDragging: isDragging || isResizing, transform: liveTransform, children: [_jsx(Rect, { x: r.x, y: r.y, width: r.width, height: r.height, color: SELECTION_COLOR, style: "stroke", strokeWidth: SELECTION_STROKE }), handleRadius != null && !locked && (_jsx(Handle, { c: { x: r.x + r.width, y: r.y + r.height }, r: handleRadius, ringWidth: handleRingWidth, color: SELECTION_COLOR }))] }));
268
267
  }
269
268
  return null;
270
269
  })(), penDrawingStroke && _jsx(StrokeElement, { stroke: penDrawingStroke }), shapePreview && (_jsxs(_Fragment, { children: [_jsx(Path, { path: shapePreview.path, color: shapePreview.color, style: "stroke", strokeWidth: shapePreview.width, strokeCap: toSkiaStrokeCap(shapePreview.cap), strokeJoin: "round", children: shapePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(shapePreview.width) })) }), _jsx(Path, { path: shapePreview.headPath, color: shapePreview.color, style: "fill" })] })), livePreview?.handoffPaths?.map((p, i) => (_jsx(Path, { path: p, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) }, i))), livePreview && (_jsx(Path, { path: livePreview.path, color: livePreview.color, style: "stroke", strokeWidth: livePreview.width, strokeCap: toSkiaStrokeCap(livePreview.cap), strokeJoin: "round", opacity: livePreview.opacity, children: livePreview.dash && (_jsx(DashPathEffect, { intervals: dashIntervals(livePreview.width) })) })), customPreview] }));
@@ -82,10 +82,12 @@ export interface DragSelectionConfig {
82
82
  isSelectedTileGrab?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number, viewportTileScale?: number, tileScaleFactor?: number, headerTileScaleFactor?: number, fileTileScaleFactor?: number): boolean;
83
83
  hitTestBackground?(doc: AnnotationCanvasState, world: Vec2): {
84
84
  id: AnnotationElementId;
85
+ locked: boolean;
85
86
  } | null;
86
87
  hitTestSelectedBackground?(doc: AnnotationCanvasState, world: Vec2, selectedIds: AnnotationElementId[]): {
87
88
  id: AnnotationElementId;
88
89
  kind: 'background';
90
+ locked: boolean;
89
91
  } | null;
90
92
  hitTestResizeHandle?(doc: AnnotationCanvasState, id: AnnotationElementId, world: Vec2, zoom: number): ResizeGeometry | null;
91
93
  buildResizePatch?(doc: AnnotationCanvasState, id: AnnotationElementId, delta: Vec2): AnnotationDocumentPatch | null;
@@ -2,7 +2,9 @@ import type { AnnotationBackgroundLayer, AnnotationViewport, BackgroundLayerTran
2
2
  export declare const LEGACY_BACKGROUND_LAYER_ID = "background-legacy";
3
3
  export declare const BACKGROUND_PLACEMENT_GAP_RATIO = 0.02;
4
4
  export declare const backgroundLayersOf: (viewport: AnnotationViewport) => AnnotationBackgroundLayer[];
5
- export declare const canEditBackgroundLayers: (viewport: AnnotationViewport) => boolean;
5
+ export declare const backgroundLayerIsLocked: (layer: AnnotationBackgroundLayer, index: number) => boolean;
6
+ export declare const isBackgroundLayerLocked: (viewport: AnnotationViewport, id: string) => boolean;
7
+ export declare const defaultBackgroundLayerLocked: (existing: AnnotationBackgroundLayer[]) => boolean;
6
8
  export declare const backgroundLayerDocRect: (layer: AnnotationBackgroundLayer) => {
7
9
  x: number;
8
10
  y: number;
@@ -46,14 +46,29 @@ export const backgroundLayersOf = (viewport) => {
46
46
  },
47
47
  ];
48
48
  };
49
- // Whether the background stack is user-editable at all selectable, draggable,
50
- // corner-scalable. A lone image IS the canvas: there is nothing to arrange it
51
- // against, so it exposes no selection chrome and never moves; taps and drags
52
- // on it fall through to the ordinary empty-canvas behavior (deselect / pan).
53
- // Only once a second image exists does arranging make sense, and then every
54
- // layer becomes editable. Shared by the select tool's hit-tests and the
55
- // selection chrome so both platforms agree on the rule.
56
- export const canEditBackgroundLayers = (viewport) => backgroundLayersOf(viewport).length > 1;
49
+ // Lock state of one layer in the stack. Every background is selectable; a
50
+ // LOCKED one additionally refuses to move, corner-scale, or delete it is
51
+ // the canvas the rest is arranged against. `locked` is explicit on layers
52
+ // written since the flag existed; absence falls back to the position rule the
53
+ // default follows (see defaultBackgroundLayerLocked): the bottom layer
54
+ // index 0, the first image placed or the legacy single slot is locked,
55
+ // every later one is free. So a pre-flag doc behaves the way a fresh one
56
+ // would: its base photo stays put, images added beside it arrange freely.
57
+ // Shared by the select tool's hit-tests and the selection chrome so both
58
+ // platforms agree on the rule.
59
+ export const backgroundLayerIsLocked = (layer, index) => layer.locked ?? index === 0;
60
+ // Lock state of the layer with `id` in this viewport's stack. False for an
61
+ // unknown id — a stale selection naming a removed layer locks nothing.
62
+ export const isBackgroundLayerLocked = (viewport, id) => {
63
+ const layers = backgroundLayersOf(viewport);
64
+ const index = layers.findIndex((l) => l.id === id);
65
+ return index >= 0 && backgroundLayerIsLocked(layers[index], index);
66
+ };
67
+ // Lock state a newly added image starts with: the first image on a canvas is
68
+ // locked (it IS the canvas), later ones are free so they can be arranged
69
+ // against it. Written explicitly onto the new layer so the state survives
70
+ // later removals — a second image stays free even once it is the only one.
71
+ export const defaultBackgroundLayerLocked = (existing) => existing.length === 0;
57
72
  // The doc-space rectangle a layer occupies: the intrinsic pixel grid under
58
73
  // the layer's translate+scale.
59
74
  export const backgroundLayerDocRect = (layer) => ({
@@ -1,4 +1,4 @@
1
- import { backgroundLayerDocRect, backgroundLayersOf, canEditBackgroundLayers, } from '../backgroundLayers.js';
1
+ import { backgroundLayerDocRect, backgroundLayerIsLocked, backgroundLayersOf, isBackgroundLayerLocked, } from '../backgroundLayers.js';
2
2
  import { stampTileDims } from '../stampLayout.js';
3
3
  import { placementOf, linePosOf, snapLinePos, lerp, recomputeAnchor, rectCenter, rectCornerPoint, oppositeRectCorner, hitPlacedMeasurement, } from '../measurementGeometry.js';
4
4
  import { hitShapeOutline, visualShapeBounds } from '../shapeGeometry.js';
@@ -114,9 +114,10 @@ const translatePatch = (elementKind, id, doc, delta) => {
114
114
  if (elementKind === 'background') {
115
115
  // Looked up through the normalized view so a legacy single-image doc's
116
116
  // synthesized layer is draggable too — the dedicated op materializes the
117
- // array on apply.
117
+ // array on apply. A locked layer never moves, whatever the caller thought
118
+ // it grabbed (belt and suspenders under the hit-tests below).
118
119
  const layer = backgroundLayerById(doc, id);
119
- if (!layer)
120
+ if (!layer || isBackgroundLayerLocked(doc.viewport, id))
120
121
  return null;
121
122
  return {
122
123
  op: 'updateBackgroundImage',
@@ -462,10 +463,11 @@ const shapeCornerPatch = (doc, id, corner, delta) => {
462
463
  // hit-test: a tap selects one only when every stroke/shape/measurement
463
464
  // misses, and a drag translates one only when it is ALREADY selected — the
464
465
  // layout tool's two-step model, which keeps a full-bleed background from
465
- // hijacking pan/marquee gestures. And none of it applies to a LONE image: with
466
- // a single background there is nothing to arrange, so it is not selectable,
467
- // draggable, or scalable at all (canEditBackgroundLayers) — every hit-test
468
- // below misses, and the gesture falls through to deselect / pan. ---
466
+ // hijacking pan/marquee gestures. A LOCKED layer (backgroundLayerIsLocked
467
+ // by default the first image, which IS the canvas) is selectable like any
468
+ // other, but never moves or scales:
469
+ // the drag/resize hit-tests below refuse it, and a gesture on it behaves like
470
+ // empty canvas (pan) while leaving the selection in place. ---
469
471
  // Doc-space clamp on a resized background's larger doc dimension. The floor
470
472
  // keeps a corner drag from collapsing a layer below grabbable size; the
471
473
  // ceiling keeps the doc rect (which feeds fit math and the web SVG raster
@@ -481,12 +483,11 @@ const pointInBackgroundLayer = (layer, p) => {
481
483
  // layers draw on top). Point-in-doc-rect, no padding: a background is a big
482
484
  // target already.
483
485
  const findBackgroundHit = (doc, world) => {
484
- if (!canEditBackgroundLayers(doc.viewport))
485
- return null;
486
486
  const layers = backgroundLayersOf(doc.viewport);
487
487
  for (let i = layers.length - 1; i >= 0; i--) {
488
- if (pointInBackgroundLayer(layers[i], world)) {
489
- return { id: layers[i].id };
488
+ const layer = layers[i];
489
+ if (pointInBackgroundLayer(layer, world)) {
490
+ return { id: layer.id, locked: backgroundLayerIsLocked(layer, i) };
490
491
  }
491
492
  }
492
493
  return null;
@@ -495,15 +496,17 @@ const findBackgroundHit = (doc, world) => {
495
496
  // paths call this only after the general hit-test misses, so annotation
496
497
  // elements always win a grab over the background beneath them.
497
498
  const findSelectedBackgroundHit = (doc, world, selectedIds) => {
498
- if (!canEditBackgroundLayers(doc.viewport))
499
- return null;
500
499
  const layers = backgroundLayersOf(doc.viewport);
501
500
  for (let i = layers.length - 1; i >= 0; i--) {
502
501
  const layer = layers[i];
503
502
  if (!selectedIds.includes(layer.id))
504
503
  continue;
505
504
  if (pointInBackgroundLayer(layer, world)) {
506
- return { id: layer.id, kind: 'background' };
505
+ return {
506
+ id: layer.id,
507
+ kind: 'background',
508
+ locked: backgroundLayerIsLocked(layer, i),
509
+ };
507
510
  }
508
511
  }
509
512
  return null;
@@ -534,9 +537,9 @@ const backgroundResizeGeometry = (layer) => {
534
537
  // endpoint handles either way.
535
538
  const findResizeHandleHit = (doc, id, world, zoom) => {
536
539
  const s = doc.shapes.find((x) => x.id === id);
537
- // A lone background is not editable at all (canEditBackgroundLayers), so
538
- // its corner offers no handle even to a stale selection naming it.
539
- const layer = s || !canEditBackgroundLayers(doc.viewport)
540
+ // A locked background cannot scale, so its corner offers no handle (the
541
+ // chrome draws none eithersee AnnotationCanvasSkia).
542
+ const layer = s || isBackgroundLayerLocked(doc.viewport, id)
540
543
  ? null
541
544
  : backgroundLayerById(doc, id);
542
545
  const geom = s
@@ -561,7 +564,7 @@ const resizePatch = (doc, id, delta) => {
561
564
  const s = doc.shapes.find((x) => x.id === id);
562
565
  if (!s) {
563
566
  const layer = backgroundLayerById(doc, id);
564
- if (!layer)
567
+ if (!layer || isBackgroundLayerLocked(doc.viewport, id))
565
568
  return null;
566
569
  const geom = backgroundResizeGeometry(layer);
567
570
  if (!geom)
@@ -737,10 +740,19 @@ export const createSelectTool = (options = {}) => ({
737
740
  // Background layers only get a look-in when every annotation element
738
741
  // misses — they can cover the whole document, so elements above always
739
742
  // win. An already-selected background grabs as a body drag (and arms
740
- // the tap-toggle: a release with no movement deselects it).
743
+ // the tap-toggle: a release with no movement deselects it). A LOCKED
744
+ // one never moves: it keeps the tap-toggle but the gesture otherwise
745
+ // behaves like empty canvas — panning (with panOnEmptyDrag) while the
746
+ // selection stays put, so the lock toggle in the host's chrome remains
747
+ // reachable.
741
748
  const selectedBg = findSelectedBackgroundHit(ctx.document, world, ctx.selection?.ids ?? []);
742
749
  if (selectedBg) {
743
750
  pendingBackgroundDeselectId = selectedBg.id;
751
+ if (selectedBg.locked) {
752
+ return options.panOnEmptyDrag
753
+ ? { kind: 'panning', lastScreen: event.screen }
754
+ : { kind: 'idle' };
755
+ }
744
756
  return {
745
757
  kind: 'dragging',
746
758
  id: selectedBg.id,
@@ -750,12 +762,16 @@ export const createSelectTool = (options = {}) => ({
750
762
  delta: { x: 0, y: 0 },
751
763
  };
752
764
  }
753
- // An unselected background selects only the gesture is consumed (no
754
- // pan, no drag): translating it takes a second gesture once selected,
755
- // the layout tool's two-step model.
765
+ // An unselected background selects. A free one consumes the gesture
766
+ // (no pan, no drag): translating it takes a second gesture once
767
+ // selected, the layout tool's two-step model. A locked one is the
768
+ // canvas: it selects AND the drag pans, as on empty canvas.
756
769
  const bg = findBackgroundHit(ctx.document, world);
757
770
  if (bg) {
758
771
  ctx.setSelection({ ids: [bg.id] });
772
+ if (bg.locked && options.panOnEmptyDrag) {
773
+ return { kind: 'panning', lastScreen: event.screen };
774
+ }
759
775
  return { kind: 'idle' };
760
776
  }
761
777
  ctx.setSelection(null);
@@ -792,10 +808,13 @@ export const createSelectTool = (options = {}) => ({
792
808
  onPointerMove(event, ctx, state) {
793
809
  const s = state;
794
810
  if (s?.kind === 'panning') {
795
- ctx.applyPan({
796
- x: event.screen.x - s.lastScreen.x,
797
- y: event.screen.y - s.lastScreen.y,
798
- });
811
+ const dx = event.screen.x - s.lastScreen.x;
812
+ const dy = event.screen.y - s.lastScreen.y;
813
+ // A pan that started on a selected locked background is a pan, not a
814
+ // re-tap — disarm its deselect toggle once the pointer actually moves.
815
+ if (dx !== 0 || dy !== 0)
816
+ pendingBackgroundDeselectId = null;
817
+ ctx.applyPan({ x: dx, y: dy });
799
818
  return { ...s, lastScreen: event.screen };
800
819
  }
801
820
  if (s?.kind !== 'dragging')
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { applyPatch, createEmptyCanvasState, } from '../../../types/annotation.js';
3
- import { backgroundLayersOf, placeNewBackgroundLayer, } from '../../canvas/backgroundLayers.js';
3
+ import { backgroundLayersOf, placeNewBackgroundLayer, defaultBackgroundLayerLocked, } from '../../canvas/backgroundLayers.js';
4
4
  import { imageDocRect } from '../../canvas/viewport.js';
5
5
  import { FileUploadType } from '../../../types/firestore.js';
6
6
  import { useAnnotationDoc } from './useAnnotationDoc.js';
@@ -373,6 +373,7 @@ export const useAnnotationCanvasDoc = (options) => {
373
373
  // still-hydrating existing doc can reach here with no working state —
374
374
  // fall back to the same empty canvas hydration would start from.
375
375
  const { viewport } = workingRef.current ?? createEmptyCanvasState(fallbackViewport);
376
+ const existing = backgroundLayersOf(viewport);
376
377
  onCommit({
377
378
  ops: [
378
379
  {
@@ -384,7 +385,10 @@ export const useAnnotationCanvasDoc = (options) => {
384
385
  widthPx: dims.width,
385
386
  heightPx: dims.height,
386
387
  format: backgroundFormatOf(blob),
387
- transform: placeNewBackgroundLayer(backgroundLayersOf(viewport), dims, viewport),
388
+ transform: placeNewBackgroundLayer(existing, dims, viewport),
389
+ // The first image is the canvas and starts pinned; later ones
390
+ // start free so they can be arranged against it.
391
+ locked: defaultBackgroundLayerLocked(existing),
388
392
  },
389
393
  },
390
394
  ],
@@ -449,6 +453,9 @@ export const useAnnotationCanvasDoc = (options) => {
449
453
  scaleX: dims.width > 0 ? rect.width / dims.width : 1,
450
454
  scaleY: dims.height > 0 ? rect.height / dims.height : 1,
451
455
  },
456
+ // The replacement is the sole image — the canvas — so it
457
+ // starts pinned like any first image.
458
+ locked: true,
452
459
  },
453
460
  },
454
461
  { op: 'setViewport', patch: { backgroundFit: fit } },
package/dist/exports.d.ts CHANGED
@@ -34,7 +34,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
34
34
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, type StampTileDims, } from './annotation/canvas/stampLayout.js';
35
35
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
36
36
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
37
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
37
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
38
38
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
39
39
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
40
40
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
package/dist/exports.js CHANGED
@@ -39,7 +39,7 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
39
39
  export { STAMP_TILE_SIZE, GROUP_HEADER_TILE_WIDTH, GROUP_HEADER_TILE_HEIGHT, GROUP_HEADER_COLLAPSED_WIDTH, GROUP_HEADER_COLLAPSED_HEIGHT, GROUP_HEADER_DESIGN, groupHeaderBaseDims, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, resolveTileScaleFactor, resolveHeaderTileScaleFactor, resolveFileTileScaleFactor, tileDocSizeForFactor, tileScaleFromDocSize, stampTileSize, stampTileDims, isUnassociatedStamp, isFileStamp, } from './annotation/canvas/stampLayout.js';
40
40
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
41
41
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
42
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
42
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerIsLocked, isBackgroundLayerLocked, defaultBackgroundLayerLocked, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
43
43
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
44
44
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
45
45
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
@@ -143,6 +143,7 @@ export interface AnnotationBackgroundLayer {
143
143
  heightPx: number;
144
144
  format?: 'raster' | 'svg';
145
145
  transform: BackgroundLayerTransform;
146
+ locked?: boolean;
146
147
  }
147
148
  export interface AnnotationViewport {
148
149
  width: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.17.4",
3
+ "version": "1.18.0",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,11 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
- export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export interface CanvasKitOpts {
5
- locateFile?: (file: string) => string;
6
- }
7
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
- fallback?: ReactNode;
9
- canvasKitOpts?: CanvasKitOpts;
10
- };
11
- export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
- // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
- // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
- // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
- // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
- // and skips WithSkiaWeb entirely (no wasm to load).
8
- export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
- default: m.AnnotationCanvasInner,
10
- })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -1,8 +0,0 @@
1
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
- export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
- export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
- fallback?: unknown;
6
- canvasKitOpts?: unknown;
7
- };
8
- export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
- export const AnnotationCanvas = (props) => {
4
- const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
- return _jsx(AnnotationCanvasInner, { ...rest });
6
- };
@@ -1,39 +0,0 @@
1
- import { type CSSProperties, type MutableRefObject } from 'react';
2
- import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
3
- import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
4
- import type { MeasurementRef } from './measurementPicker.js';
5
- import type { Tool } from './Tool.js';
6
- import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
7
- import { type ViewportState } from './viewport.js';
8
- import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
9
- export type { AnnotationCanvasHandle };
10
- export interface AnnotationCanvasInnerProps {
11
- canvas: AnnotationCanvasState;
12
- onCommit(patch: AnnotationDocumentPatch): void;
13
- tools: Tool[];
14
- activeToolId: string;
15
- selection: Selection | null;
16
- onSelectionChange(selection: Selection | null): void;
17
- measurements?: Measurement[];
18
- fallbackUnit?: Units;
19
- fractionalTolerance?: FractionalTolerance;
20
- decimalTolerance?: DecimalTolerance;
21
- resolveImageUrl?: (storagePath: string) => Promise<string>;
22
- pickMeasurement?: () => Promise<MeasurementRef | null>;
23
- renderMeasurementStamp?: RenderMeasurementStamp;
24
- stampFontSource?: unknown;
25
- stampValueFontSize?: number;
26
- stampLabelFontSize?: number;
27
- gestures?: GestureConfig;
28
- width: number;
29
- height: number;
30
- initialViewport?: ViewportState;
31
- style?: CSSProperties;
32
- imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
33
- }
34
- export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
35
- export interface GestureConfig {
36
- wheel?: 'zoom' | 'pan' | 'auto';
37
- panTriggers?: PanTrigger[];
38
- }
39
- export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;