@reekon-tools/boldr-utils 1.17.3 → 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.
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +8 -2
- package/dist/annotation/canvas/AnnotationCanvasSkia.js +8 -9
- package/dist/annotation/canvas/Tool.d.ts +2 -0
- package/dist/annotation/canvas/backgroundLayers.d.ts +3 -1
- package/dist/annotation/canvas/backgroundLayers.js +23 -8
- package/dist/annotation/canvas/tools/selectTool.js +45 -26
- package/dist/annotation/data/hooks/useAnnotationCanvasDoc.js +9 -2
- package/dist/export/buildJobCsv.d.ts +1 -0
- package/dist/export/buildJobCsv.js +24 -10
- package/dist/exports.d.ts +1 -1
- package/dist/exports.js +1 -1
- package/dist/types/annotation.d.ts +1 -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
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
const layer =
|
|
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
|
|
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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
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,
|
|
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.
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// below
|
|
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
|
-
|
|
489
|
-
|
|
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 {
|
|
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
|
|
538
|
-
//
|
|
539
|
-
const layer = s ||
|
|
540
|
+
// A locked background cannot scale, so its corner offers no handle (the
|
|
541
|
+
// chrome draws none either — see 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
|
|
754
|
-
// pan, no drag): translating it takes a second gesture once
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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(
|
|
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 } },
|
|
@@ -15,6 +15,7 @@ export type BuildJobCsvInput = {
|
|
|
15
15
|
measurementsByGroup: Map<string, Measurement[]>;
|
|
16
16
|
objectsByGroup: Map<string, ExportObjectRow[]>;
|
|
17
17
|
formatMeasurementValue: (value: number, device?: string) => string;
|
|
18
|
+
applyGroupFormula?: (group: Group, measurement: Measurement) => number | null;
|
|
18
19
|
unitPrefs: {
|
|
19
20
|
defaultUnit?: Units;
|
|
20
21
|
fractionalTolerance?: FractionalTolerance;
|
|
@@ -39,7 +39,7 @@ const userCell = (user) => (user ? [user.firstName, user.lastName].filter(Boolea
|
|
|
39
39
|
// trailing "Objects" column (list groups) or extend the form row (form
|
|
40
40
|
// groups), one skipped column after the existing cells.
|
|
41
41
|
export function buildJobCsv(input) {
|
|
42
|
-
const { scope, activeGroupId, config, groups, sections, formulas, measurementsByGroup, objectsByGroup, formatMeasurementValue, unitPrefs, } = input;
|
|
42
|
+
const { scope, activeGroupId, config, groups, sections, formulas, measurementsByGroup, objectsByGroup, formatMeasurementValue, applyGroupFormula, unitPrefs, } = input;
|
|
43
43
|
const csvRows = [];
|
|
44
44
|
let listGroups;
|
|
45
45
|
let formGroups;
|
|
@@ -82,9 +82,13 @@ export function buildJobCsv(input) {
|
|
|
82
82
|
headerCells.push(escapeCSVField(''), escapeCSVField('Objects'));
|
|
83
83
|
}
|
|
84
84
|
csvRows.push(headerCells.join(','));
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
85
|
+
// File-scratch measurements (non-null fileId: created inside a CAD/PDF
|
|
86
|
+
// canvas, never saved into the group) live in the same subcollection
|
|
87
|
+
// but are internal — the group cards hide them, so exports must too.
|
|
88
|
+
// Then export rows in the card/table display order (incomplete first,
|
|
89
|
+
// then capture time), regardless of the order the caller fetched them
|
|
90
|
+
// in.
|
|
91
|
+
const measurements = sortMeasurementsByCompleted((measurementsByGroup.get(group.id) ?? []).filter((m) => m.fileId == null)).filter(measurementPassesFilter);
|
|
88
92
|
// Objects fill a parallel column, one per row from the top; extra rows
|
|
89
93
|
// are emitted when a group has more objects than measurements.
|
|
90
94
|
const rowCount = Math.max(measurements.length, objects.length);
|
|
@@ -94,12 +98,22 @@ export function buildJobCsv(input) {
|
|
|
94
98
|
if (includeField('measurement')) {
|
|
95
99
|
// Angles and unitless numbers store their value as entered, not as
|
|
96
100
|
// micrometers — running them through the length formatter would
|
|
97
|
-
// render 45° as ~0. Emit them as-is with their own suffix.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// render 45° as ~0. Emit them as-is with their own suffix. Group
|
|
102
|
+
// formulas apply to length measurements only, same as the tiles.
|
|
103
|
+
let cell = '';
|
|
104
|
+
if (measurement) {
|
|
105
|
+
if (isScalarMeasurement(measurement.type)) {
|
|
106
|
+
cell = `${measurement.value}${scalarValueSuffix(measurement.type)}`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const formulaValue = applyGroupFormula?.(group, measurement) ?? null;
|
|
110
|
+
cell =
|
|
111
|
+
formulaValue !== null
|
|
112
|
+
? formatMeasurementValue(formulaValue)
|
|
113
|
+
: formatMeasurementValue(measurement.value, measurement.device);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
row.push(escapeCSVField(cell));
|
|
103
117
|
}
|
|
104
118
|
if (includeField('label')) {
|
|
105
119
|
row.push(escapeCSVField(measurement?.label || ''));
|
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,
|
|
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,
|
|
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';
|
package/package.json
CHANGED
|
@@ -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;
|