@reekon-tools/boldr-utils 1.15.5 → 1.16.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 (76) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasSkia.js +8 -3
  2. package/dist/annotation/canvas/backgroundLayers.d.ts +1 -0
  3. package/dist/annotation/canvas/backgroundLayers.js +8 -0
  4. package/dist/annotation/canvas/tools/selectTool.js +14 -3
  5. package/dist/calculator/index.d.ts +1 -0
  6. package/dist/calculator/index.js +1 -0
  7. package/dist/calculator/preview.d.ts +22 -0
  8. package/dist/calculator/preview.js +43 -0
  9. package/dist/calculator/schema.d.ts +14 -0
  10. package/dist/calculator/schema.js +2 -0
  11. package/dist/calculator/validate.d.ts +1 -0
  12. package/dist/calculator/validate.js +17 -0
  13. package/dist/exports.d.ts +1 -1
  14. package/dist/exports.js +1 -1
  15. package/dist/types/firestore.d.ts +2 -0
  16. package/package.json +1 -1
  17. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  18. package/dist/canvas/AnnotationCanvas.js +0 -10
  19. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  20. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  21. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  22. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  23. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  24. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  25. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  26. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  27. package/dist/canvas/Tool.d.ts +0 -38
  28. package/dist/canvas/Tool.js +0 -1
  29. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  30. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  31. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  32. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  33. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  34. package/dist/canvas/elements/ShapeElement.js +0 -62
  35. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  36. package/dist/canvas/elements/StrokeElement.js +0 -18
  37. package/dist/canvas/measurementPicker.d.ts +0 -10
  38. package/dist/canvas/measurementPicker.js +0 -1
  39. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  40. package/dist/canvas/measurementStampOverlay.js +0 -1
  41. package/dist/canvas/pointerAdapter.d.ts +0 -3
  42. package/dist/canvas/pointerAdapter.js +0 -19
  43. package/dist/canvas/stampLayout.d.ts +0 -5
  44. package/dist/canvas/stampLayout.js +0 -14
  45. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  46. package/dist/canvas/tools/measurementStampTool.js +0 -37
  47. package/dist/canvas/tools/panTool.d.ts +0 -5
  48. package/dist/canvas/tools/panTool.js +0 -25
  49. package/dist/canvas/tools/penTool.d.ts +0 -13
  50. package/dist/canvas/tools/penTool.js +0 -68
  51. package/dist/canvas/tools/selectTool.d.ts +0 -2
  52. package/dist/canvas/tools/selectTool.js +0 -182
  53. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  54. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  55. package/dist/canvas/viewport.d.ts +0 -16
  56. package/dist/canvas/viewport.js +0 -54
  57. package/dist/data/AnnotationDataContext.d.ts +0 -8
  58. package/dist/data/AnnotationDataContext.js +0 -11
  59. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  60. package/dist/data/AnnotationDataProvider.js +0 -4
  61. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  62. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  63. package/dist/data/canvasPersistence.d.ts +0 -3
  64. package/dist/data/canvasPersistence.js +0 -26
  65. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  66. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  67. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  68. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  69. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  70. package/dist/data/hooks/useAnnotationList.js +0 -26
  71. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  72. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  73. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  74. package/dist/hooks/useParseMeasurement.js +0 -14
  75. package/dist/utils/evaluateFormula.d.ts +0 -20
  76. package/dist/utils/evaluateFormula.js +0 -31
@@ -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, } from './backgroundLayers.js';
7
+ import { backgroundLayerDocRect, backgroundLayersOf, canEditBackgroundLayers, } 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';
@@ -251,8 +251,13 @@ export const AnnotationCanvasSkia = ({ width, height, effectiveCanvas, worldTran
251
251
  // bottom-right handle — backgrounds scale uniformly about their
252
252
  // top-left corner (the transform origin), so a single opposite-corner
253
253
  // grab is the whole gizmo. Chrome tracks the live drag/resize
254
- // transform the same way the layer itself does above.
255
- const layer = backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId);
254
+ // transform the same way the layer itself does above. A lone
255
+ // background is never editable (canEditBackgroundLayers), so it gets
256
+ // no chrome even when a stale selection still names it — e.g. right
257
+ // after the other layer of a pair was removed.
258
+ const layer = canEditBackgroundLayers(effectiveCanvas.viewport)
259
+ ? backgroundLayersOf(effectiveCanvas.viewport).find((l) => l.id === selectedId)
260
+ : undefined;
256
261
  if (layer) {
257
262
  const r = backgroundLayerDocRect(layer);
258
263
  const isResizing = layer.id === resizingId;
@@ -2,6 +2,7 @@ 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
6
  export declare const backgroundLayerDocRect: (layer: AnnotationBackgroundLayer) => {
6
7
  x: number;
7
8
  y: number;
@@ -46,6 +46,14 @@ 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
57
  // The doc-space rectangle a layer occupies: the intrinsic pixel grid under
50
58
  // the layer's translate+scale.
51
59
  export const backgroundLayerDocRect = (layer) => ({
@@ -1,4 +1,4 @@
1
- import { backgroundLayerDocRect, backgroundLayersOf, } from '../backgroundLayers.js';
1
+ import { backgroundLayerDocRect, backgroundLayersOf, canEditBackgroundLayers, } 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';
@@ -462,7 +462,10 @@ const shapeCornerPatch = (doc, id, corner, delta) => {
462
462
  // hit-test: a tap selects one only when every stroke/shape/measurement
463
463
  // misses, and a drag translates one only when it is ALREADY selected — the
464
464
  // layout tool's two-step model, which keeps a full-bleed background from
465
- // hijacking pan/marquee gestures. ---
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
469
  // Doc-space clamp on a resized background's larger doc dimension. The floor
467
470
  // keeps a corner drag from collapsing a layer below grabbable size; the
468
471
  // ceiling keeps the doc rect (which feeds fit math and the web SVG raster
@@ -478,6 +481,8 @@ const pointInBackgroundLayer = (layer, p) => {
478
481
  // layers draw on top). Point-in-doc-rect, no padding: a background is a big
479
482
  // target already.
480
483
  const findBackgroundHit = (doc, world) => {
484
+ if (!canEditBackgroundLayers(doc.viewport))
485
+ return null;
481
486
  const layers = backgroundLayersOf(doc.viewport);
482
487
  for (let i = layers.length - 1; i >= 0; i--) {
483
488
  if (pointInBackgroundLayer(layers[i], world)) {
@@ -490,6 +495,8 @@ const findBackgroundHit = (doc, world) => {
490
495
  // paths call this only after the general hit-test misses, so annotation
491
496
  // elements always win a grab over the background beneath them.
492
497
  const findSelectedBackgroundHit = (doc, world, selectedIds) => {
498
+ if (!canEditBackgroundLayers(doc.viewport))
499
+ return null;
493
500
  const layers = backgroundLayersOf(doc.viewport);
494
501
  for (let i = layers.length - 1; i >= 0; i--) {
495
502
  const layer = layers[i];
@@ -527,7 +534,11 @@ const backgroundResizeGeometry = (layer) => {
527
534
  // endpoint handles either way.
528
535
  const findResizeHandleHit = (doc, id, world, zoom) => {
529
536
  const s = doc.shapes.find((x) => x.id === id);
530
- const layer = s ? null : backgroundLayerById(doc, 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
+ ? null
541
+ : backgroundLayerById(doc, id);
531
542
  const geom = s
532
543
  ? textResizeGeometry(s)
533
544
  : layer
@@ -9,4 +9,5 @@ export * from './instance.js';
9
9
  export * from './runtime.js';
10
10
  export * from './display.js';
11
11
  export * from './chips.js';
12
+ export * from './preview.js';
12
13
  export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, type IssueSeverity, type ValidationIssue, type ValidationResult, } from './validate.js';
@@ -15,4 +15,5 @@ export * from './instance.js';
15
15
  export * from './runtime.js';
16
16
  export * from './display.js';
17
17
  export * from './chips.js';
18
+ export * from './preview.js';
18
19
  export { calculatorDefinitionSchema, expressionSymbols, validateCalculatorDefinition, } from './validate.js';
@@ -0,0 +1,22 @@
1
+ import { type CalculatorDefinition, type CalculatorField } from './schema.js';
2
+ import type { CalculatorInstanceFileData } from './instance.js';
3
+ /**
4
+ * The field a thumbnail headlines, per CalculatorDefinition.previewFieldId's
5
+ * tri-state: an explicit id (when it names a numeric output), null for none,
6
+ * absent for auto — the sole numeric output, if there is exactly one.
7
+ */
8
+ export declare const previewFieldFor: (definition: Pick<CalculatorDefinition, "fields" | "previewFieldId">) => CalculatorField | null;
9
+ export interface PreviewValue {
10
+ field: CalculatorField;
11
+ /** Display string in `unit` — fractional inches etc., same as the runtime. */
12
+ value: string;
13
+ /** Unit label; empty for unit-less numbers. */
14
+ unit: string;
15
+ }
16
+ /**
17
+ * The headline value for an instance, or null when there is nothing to show:
18
+ * no preview field, the field is unresolved (inputs missing), or it is in
19
+ * conflict (the runtime withholds conflicted values behind a dash — a tile
20
+ * should show nothing rather than a dash).
21
+ */
22
+ export declare const previewValueFor: (fileData: Pick<CalculatorInstanceFileData, "definition" | "entries" | "displayUnitOverrides">) => PreviewValue | null;
@@ -0,0 +1,43 @@
1
+ // Thumbnail headline for a saved calculator instance (Asana
2
+ // 1217881404454870): which output a tile shows, and its formatted value.
3
+ // Shared by boldr-pro's FilePreview and rock-desktop so both platforms pick
4
+ // the same field and print the same string. Pure — runs the same
5
+ // deriveRuntime pass the runtime and the T1R drift check use, so it needs no
6
+ // stored result (computed values are never persisted; see instance.ts).
7
+ import { isEquationTargetKind, } from './schema.js';
8
+ import { deriveRuntime } from './runtime.js';
9
+ import { displayUnitFor, formatFieldValue } from './display.js';
10
+ /** Outputs eligible to headline a thumbnail: numeric, equation-target kinds. */
11
+ const numericOutputs = (definition) => definition.fields.filter((field) => field.role === 'output' && isEquationTargetKind(field.kind));
12
+ /**
13
+ * The field a thumbnail headlines, per CalculatorDefinition.previewFieldId's
14
+ * tri-state: an explicit id (when it names a numeric output), null for none,
15
+ * absent for auto — the sole numeric output, if there is exactly one.
16
+ */
17
+ export const previewFieldFor = (definition) => {
18
+ if (definition.previewFieldId === null)
19
+ return null;
20
+ const candidates = numericOutputs(definition);
21
+ if (typeof definition.previewFieldId === 'string') {
22
+ return (candidates.find((field) => field.id === definition.previewFieldId) ?? null);
23
+ }
24
+ return candidates.length === 1 ? candidates[0] : null;
25
+ };
26
+ /**
27
+ * The headline value for an instance, or null when there is nothing to show:
28
+ * no preview field, the field is unresolved (inputs missing), or it is in
29
+ * conflict (the runtime withholds conflicted values behind a dash — a tile
30
+ * should show nothing rather than a dash).
31
+ */
32
+ export const previewValueFor = (fileData) => {
33
+ const field = previewFieldFor(fileData.definition);
34
+ if (!field)
35
+ return null;
36
+ const resolved = deriveRuntime(fileData.definition, fileData.entries ?? {})
37
+ .resolved[field.id];
38
+ if (!resolved || resolved.conflict || typeof resolved.value !== 'number') {
39
+ return null;
40
+ }
41
+ const unit = displayUnitFor(field, fileData.displayUnitOverrides ?? {});
42
+ return { field, ...formatFieldValue(field, unit, resolved.value) };
43
+ };
@@ -190,6 +190,20 @@ export interface CalculatorDefinition {
190
190
  /** Inputs + outputs in one list, discriminated by `role`. */
191
191
  fields: CalculatorField[];
192
192
  equations: CalculatorEquation[];
193
+ /**
194
+ * The output whose resolved value a saved instance's THUMBNAIL headlines
195
+ * (Asana 1217881404454870 — Area/Volume and every T1R function calculator
196
+ * are impractical to open one by one just to read the result). Tri-state:
197
+ * - absent (undefined): AUTO — the sole numeric output when the
198
+ * calculator has exactly one, nothing when it has several. This is also
199
+ * what instances frozen before the field existed resolve to, so they
200
+ * gain a headline without being re-saved.
201
+ * - null: explicitly none — the author opted a single-output calc out.
202
+ * - id: that output. Must be role 'output' and an equation-target kind;
203
+ * anything else is validated as a warning and ignored at runtime.
204
+ * Always resolve through previewFieldFor() — never read this raw.
205
+ */
206
+ previewFieldId?: string | null;
193
207
  /** Diagram file (files subcollection doc carrying the Skia canvas). */
194
208
  diagramFileId?: string | null;
195
209
  /** Public calculators are readable by everyone; rules/query key. */
@@ -208,6 +208,8 @@ export const createEmptyCalculatorDefinition = (id) => ({
208
208
  category: null,
209
209
  fields: [],
210
210
  equations: [],
211
+ // previewFieldId is deliberately ABSENT, not null: absent means "auto"
212
+ // (headline the sole numeric output), null means the author opted out.
211
213
  diagramFileId: null,
212
214
  isPublic: false,
213
215
  });
@@ -174,6 +174,7 @@ export declare const calculatorDefinitionSchema: z.ZodObject<{
174
174
  variableUnits: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
175
175
  resultUnit: z.ZodOptional<z.ZodString>;
176
176
  }, z.core.$strip>>;
177
+ previewFieldId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
177
178
  diagramFileId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
179
  isPublic: z.ZodBoolean;
179
180
  sharedUserIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -127,6 +127,9 @@ export const calculatorDefinitionSchema = z.object({
127
127
  category: z.string().nullish(),
128
128
  fields: z.array(calculatorFieldSchema),
129
129
  equations: z.array(calculatorEquationSchema),
130
+ // Listed so zod's unknown-key stripping doesn't silently drop it (see the
131
+ // `category` note above). Referential checks live in the invariant pass.
132
+ previewFieldId: z.string().nullish(),
130
133
  diagramFileId: z.string().nullable().optional(),
131
134
  isPublic: z.boolean(),
132
135
  sharedUserIds: z.array(z.string()).optional(),
@@ -229,6 +232,20 @@ export const validateCalculatorDefinition = (input) => {
229
232
  }
230
233
  fieldIds.add(field.id);
231
234
  });
235
+ // --- Thumbnail headline field -------------------------------------------
236
+ // Warnings, not errors: a dangling or mis-typed previewFieldId must not brick
237
+ // a published calculator (or every instance that froze it) — the runtime
238
+ // helper (previewFieldFor) simply shows no headline for it.
239
+ if (typeof def.previewFieldId === 'string') {
240
+ const previewField = def.fields.find((f) => f.id === def.previewFieldId);
241
+ if (!previewField) {
242
+ push('preview-field-missing', 'previewFieldId', `Thumbnail result field "${def.previewFieldId}" does not exist`, 'warning');
243
+ }
244
+ else if (previewField.role !== 'output' ||
245
+ !isEquationTargetKind(previewField.kind)) {
246
+ push('preview-field-not-numeric-output', 'previewFieldId', `Thumbnail result field "${previewField.name}" must be a numeric output`, 'warning');
247
+ }
248
+ }
232
249
  const equationIds = new Set();
233
250
  def.equations.forEach((eq, i) => {
234
251
  if (equationIds.has(eq.id)) {
package/dist/exports.d.ts CHANGED
@@ -32,7 +32,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
32
32
  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';
33
33
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, type GroupHeaderBadge, type GroupHeaderDescriptor, } from './annotation/canvas/groupHeaderLayout.js';
34
34
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
35
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
35
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
36
36
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
37
37
  export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
38
38
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
package/dist/exports.js CHANGED
@@ -37,7 +37,7 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
37
37
  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';
38
38
  export { GROUP_HEADER_STYLE, describeGroupHeader, groupHeaderSignature, } from './annotation/canvas/groupHeaderLayout.js';
39
39
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
40
- export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
40
+ export { LEGACY_BACKGROUND_LAYER_ID, BACKGROUND_PLACEMENT_GAP_RATIO, backgroundLayersOf, canEditBackgroundLayers, backgroundLayerDocRect, backgroundLayersUnionRect, placeNewBackgroundLayer, } from './annotation/canvas/backgroundLayers.js';
41
41
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
42
42
  export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
43
43
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
@@ -67,6 +67,7 @@ export interface Project extends FirestoreDoc, Timestamps {
67
67
  state: string;
68
68
  collectionId: string | null;
69
69
  index: number | null;
70
+ bgImgUpdatedAt?: number;
70
71
  }
71
72
  export declare enum TemplateType {
72
73
  Layout = "layout",
@@ -228,6 +229,7 @@ export interface Job extends FirestoreDoc, Timestamps {
228
229
  description?: string;
229
230
  starred?: boolean;
230
231
  lastModified: Date;
232
+ bgImgUpdatedAt?: number;
231
233
  }
232
234
  export interface Section extends Timestamps {
233
235
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.15.5",
3
+ "version": "1.16.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;
@@ -1,219 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useFont } from '@shopify/react-native-skia';
3
- import { useCallback, useEffect, useRef, } from 'react';
4
- import { AnnotationCanvasSkia } from './AnnotationCanvasSkia.js';
5
- import { useAnnotationCanvasState, } from './useAnnotationCanvasState.js';
6
- import { STAMP_TILE_SIZE } from './stampLayout.js';
7
- const DEFAULT_PAN_TRIGGERS = ['middleMouse', 'space'];
8
- export const AnnotationCanvasInner = (props) => {
9
- const { fallbackUnit, fractionalTolerance, decimalTolerance, resolveImageUrl, stampFontSource, stampValueFontSize = 14, stampLabelFontSize = 11, gestures, width, height, style, activeToolId, tools, } = props;
10
- const wheelMode = gestures?.wheel ?? 'auto';
11
- const panTriggers = gestures?.panTriggers ?? DEFAULT_PAN_TRIGGERS;
12
- const allowSpacePan = panTriggers.includes('space');
13
- const allowMiddlePan = panTriggers.includes('middleMouse');
14
- const allowRightPan = panTriggers.includes('rightMouse');
15
- const valueFont = useFont(stampFontSource, stampValueFontSize);
16
- const labelFont = useFont(stampFontSource, stampLabelFontSize);
17
- const state = useAnnotationCanvasState(props);
18
- const containerRef = useRef(null);
19
- const panGestureRef = useRef(null);
20
- const spaceDownRef = useRef(false);
21
- const activeTool = tools.find((t) => t.id === activeToolId) ?? null;
22
- const toCanvasPointer = useCallback((event) => {
23
- const rect = containerRef.current?.getBoundingClientRect();
24
- const screen = {
25
- x: event.clientX - (rect?.left ?? 0),
26
- y: event.clientY - (rect?.top ?? 0),
27
- };
28
- return {
29
- pointerId: event.pointerId,
30
- screen,
31
- world: state.ctx.viewport.screenToWorld(screen),
32
- pressure: event.pressure || undefined,
33
- shiftKey: event.shiftKey,
34
- altKey: event.altKey,
35
- metaKey: event.metaKey,
36
- ctrlKey: event.ctrlKey,
37
- };
38
- }, [state.ctx.viewport]);
39
- const isPanTriggerDown = useCallback((event) => {
40
- if (allowMiddlePan && event.button === 1)
41
- return true;
42
- if (allowRightPan && event.button === 2)
43
- return true;
44
- if (allowSpacePan && event.button === 0 && spaceDownRef.current)
45
- return true;
46
- return false;
47
- }, [allowMiddlePan, allowRightPan, allowSpacePan]);
48
- const handlePointerDown = useCallback((event) => {
49
- const rect = containerRef.current?.getBoundingClientRect();
50
- const screen = {
51
- x: event.clientX - (rect?.left ?? 0),
52
- y: event.clientY - (rect?.top ?? 0),
53
- };
54
- if (isPanTriggerDown(event)) {
55
- panGestureRef.current = { pointerId: event.pointerId, lastScreen: screen };
56
- event.currentTarget.setPointerCapture(event.pointerId);
57
- event.preventDefault();
58
- return;
59
- }
60
- event.currentTarget.setPointerCapture(event.pointerId);
61
- state.dispatchPointerDown(toCanvasPointer(event));
62
- }, [state, toCanvasPointer, isPanTriggerDown]);
63
- const handlePointerMove = useCallback((event) => {
64
- const pan = panGestureRef.current;
65
- if (pan && event.pointerId === pan.pointerId) {
66
- const rect = containerRef.current?.getBoundingClientRect();
67
- const screen = {
68
- x: event.clientX - (rect?.left ?? 0),
69
- y: event.clientY - (rect?.top ?? 0),
70
- };
71
- state.pan({
72
- x: screen.x - pan.lastScreen.x,
73
- y: screen.y - pan.lastScreen.y,
74
- });
75
- panGestureRef.current = { pointerId: pan.pointerId, lastScreen: screen };
76
- return;
77
- }
78
- state.dispatchPointerMove(toCanvasPointer(event));
79
- }, [state, toCanvasPointer]);
80
- const handlePointerUp = useCallback((event) => {
81
- const pan = panGestureRef.current;
82
- if (pan && event.pointerId === pan.pointerId) {
83
- panGestureRef.current = null;
84
- return;
85
- }
86
- state.dispatchPointerUp(toCanvasPointer(event));
87
- }, [state, toCanvasPointer]);
88
- const handlePointerCancel = useCallback(() => {
89
- panGestureRef.current = null;
90
- state.dispatchPointerCancel();
91
- }, [state]);
92
- const handleWheel = useCallback((event) => {
93
- const rect = containerRef.current?.getBoundingClientRect();
94
- const focal = {
95
- x: event.clientX - (rect?.left ?? 0),
96
- y: event.clientY - (rect?.top ?? 0),
97
- };
98
- if (event.ctrlKey || event.metaKey) {
99
- const factor = Math.exp(-event.deltaY / 200);
100
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
101
- return;
102
- }
103
- if (wheelMode === 'zoom') {
104
- const factor = Math.exp(-event.deltaY / 300);
105
- state.zoom(focal, state.ctx.viewport.state.zoom * factor);
106
- return;
107
- }
108
- if (event.shiftKey) {
109
- state.pan({ x: event.deltaY, y: event.deltaX });
110
- return;
111
- }
112
- state.pan({ x: event.deltaX, y: event.deltaY });
113
- }, [state, wheelMode]);
114
- const handleContextMenu = useCallback((event) => {
115
- if (allowRightPan)
116
- event.preventDefault();
117
- }, [allowRightPan]);
118
- useEffect(() => {
119
- const el = containerRef.current;
120
- if (!el)
121
- return;
122
- const stop = (e) => {
123
- if (e.target === el || el.contains(e.target)) {
124
- e.preventDefault();
125
- }
126
- };
127
- el.addEventListener('wheel', stop, { passive: false });
128
- return () => el.removeEventListener('wheel', stop);
129
- }, []);
130
- useEffect(() => {
131
- if (!allowSpacePan)
132
- return;
133
- const onKeyDown = (e) => {
134
- if (e.code === 'Space' && !e.repeat) {
135
- spaceDownRef.current = true;
136
- if (containerRef.current)
137
- containerRef.current.style.cursor = 'grab';
138
- }
139
- };
140
- const onKeyUp = (e) => {
141
- if (e.code === 'Space') {
142
- spaceDownRef.current = false;
143
- if (containerRef.current)
144
- containerRef.current.style.cursor = '';
145
- }
146
- };
147
- window.addEventListener('keydown', onKeyDown);
148
- window.addEventListener('keyup', onKeyUp);
149
- return () => {
150
- window.removeEventListener('keydown', onKeyDown);
151
- window.removeEventListener('keyup', onKeyUp);
152
- };
153
- }, [allowSpacePan]);
154
- const containerStyle = {
155
- position: 'relative',
156
- width,
157
- height,
158
- overflow: 'hidden',
159
- touchAction: 'none',
160
- userSelect: 'none',
161
- cursor: activeTool?.cursor ?? 'default',
162
- ...style,
163
- };
164
- const customPreview = activeTool?.renderPreview?.(state.customPreviewState, state.ctx);
165
- const { renderMeasurementStamp, selection } = props;
166
- return (_jsxs("div", { ref: containerRef, style: containerStyle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onWheel: handleWheel, onContextMenu: handleContextMenu, children: [AnnotationCanvasSkia({
167
- width,
168
- height,
169
- effectiveCanvas: state.effectiveCanvas,
170
- worldTransform: state.worldTransform,
171
- measurementsById: state.measurementsById,
172
- fallbackUnit,
173
- fractionalTolerance,
174
- decimalTolerance,
175
- resolveImageUrl,
176
- valueFont,
177
- labelFont,
178
- hideMeasurementStamps: !!renderMeasurementStamp,
179
- penDrawingStroke: state.penDrawingStroke,
180
- customPreview,
181
- }), renderMeasurementStamp && (_jsx("div", { style: {
182
- position: 'absolute',
183
- inset: 0,
184
- pointerEvents: 'none',
185
- }, children: state.effectiveCanvas.placedMeasurements.map((placed) => {
186
- const size = STAMP_TILE_SIZE * (placed.scale ?? 1);
187
- const cx = (placed.anchor.x - state.viewport.pan.x) * state.viewport.zoom;
188
- const cy = (placed.anchor.y - state.viewport.pan.y) * state.viewport.zoom;
189
- const isSelected = selection?.ids.includes(placed.id) ?? false;
190
- return (_jsxs("div", { style: {
191
- position: 'absolute',
192
- left: 0,
193
- top: 0,
194
- width: size,
195
- height: size,
196
- transform: `translate(${cx - size / 2}px, ${cy - size / 2}px)`,
197
- }, children: [renderMeasurementStamp({
198
- placed,
199
- measurement: state.measurementsById.get(placed.measurementId) ?? null,
200
- selected: isSelected,
201
- size,
202
- zoom: state.viewport.zoom,
203
- }), isSelected && (_jsx("div", { role: "button", "aria-label": "Remove measurement", onPointerDown: (e) => {
204
- e.stopPropagation();
205
- state.ctx.commit({
206
- ops: [{ op: 'removeMeasurement', id: placed.id }],
207
- });
208
- state.ctx.setSelection(null);
209
- }, style: {
210
- position: 'absolute',
211
- top: -10,
212
- right: -10,
213
- width: 40,
214
- height: 40,
215
- cursor: 'pointer',
216
- pointerEvents: 'auto',
217
- } }))] }, placed.id));
218
- }) }))] }));
219
- };
@@ -1,35 +0,0 @@
1
- import { type MutableRefObject } from 'react';
2
- import { type ViewStyle } from 'react-native';
3
- import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
4
- import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
5
- import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
6
- import type { MeasurementRef } from './measurementPicker.js';
7
- import type { Tool } from './Tool.js';
8
- import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
9
- import type { ViewportState } from './viewport.js';
10
- export type { AnnotationCanvasHandle };
11
- export interface AnnotationCanvasInnerProps {
12
- canvas: AnnotationCanvasState;
13
- onCommit(patch: AnnotationDocumentPatch): void;
14
- tools: Tool[];
15
- activeToolId: string;
16
- selection: Selection | null;
17
- onSelectionChange(selection: Selection | null): void;
18
- measurements?: Measurement[];
19
- fallbackUnit?: Units;
20
- fractionalTolerance?: FractionalTolerance;
21
- decimalTolerance?: DecimalTolerance;
22
- resolveImageUrl?: (storagePath: string) => Promise<string>;
23
- pickMeasurement?: () => Promise<MeasurementRef | null>;
24
- renderMeasurementStamp?: RenderMeasurementStamp;
25
- stampFontSource?: unknown;
26
- stampValueFontSize?: number;
27
- stampLabelFontSize?: number;
28
- gestures?: unknown;
29
- width: number;
30
- height: number;
31
- initialViewport?: ViewportState;
32
- style?: ViewStyle;
33
- imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
34
- }
35
- export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;