@embedpdf/plugin-measurement 3.0.0-next.14 → 3.0.0-next.15

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/index.cjs CHANGED
@@ -1,471 +1,629 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-Dy7X9nSj.cjs");
2
+ const require_contract = require("./contract-BHj3w9WF.cjs");
3
3
  let _embedpdf_core = require("@embedpdf/core");
4
4
  let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
5
5
  let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
6
- let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
7
6
  let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
8
- //#region src/scale.ts
9
- const DEFAULT_PRESETS = [
10
- ...[
11
- 1,
12
- 10,
13
- 20,
14
- 50,
15
- 100,
16
- 200
17
- ].map((real) => ({
18
- id: `metric-${real}`,
19
- label: `1:${real}`,
20
- paper: 1,
21
- real,
22
- unit: "m"
23
- })),
24
- {
25
- id: "imperial-quarter",
26
- label: "1/4 in = 1 ft",
27
- paper: 1,
28
- real: 48,
29
- unit: "ft"
30
- },
31
- {
32
- id: "imperial-eighth",
33
- label: "1/8 in = 1 ft",
34
- paper: 1,
35
- real: 96,
36
- unit: "ft"
37
- }
7
+ //#region src/read/scale.ts
8
+ const UNITS = Object.keys(_embedpdf_engine_core_runtime.METRES);
9
+ const AREA_UNITS = [
10
+ ...UNITS.map(_embedpdf_engine_core_runtime.squareOf),
11
+ "ha",
12
+ "acre"
38
13
  ];
39
- const defaultMeasure = (config, userUnit = 1) => typeof config.defaultScale === "object" ? config.defaultScale : (0, _embedpdf_engine_core_runtime.measureFromRatio)(1, 1, config.defaultScale === "imperial" ? "ft" : "m", userUnit);
40
- function selectPageScale(viewports, crop, fallback, persistent) {
41
- const selected = viewports.find((v) => v.owned) ?? (0, _embedpdf_engine_core_runtime.viewportForPoint)(viewports, {
42
- x: (crop.left + crop.right) / 2,
43
- y: (crop.top + crop.bottom) / 2
14
+ function createScaleReads(ctx, { siblings }, { scaleOf }) {
15
+ const { annotation } = siblings;
16
+ const settings = ctx.settings();
17
+ const state = () => ctx.state.get();
18
+ const readouts = /* @__PURE__ */ new WeakMap();
19
+ const getReadout = (ref) => {
20
+ const raw = annotation.get(ref);
21
+ if (!raw) return require_contract.NO_READOUT;
22
+ let readout = readouts.get(raw);
23
+ if (!readout) {
24
+ readout = (0, _embedpdf_engine_core_runtime.measurementReadout)(raw);
25
+ readouts.set(raw, readout);
26
+ }
27
+ return readout;
28
+ };
29
+ const measureDistance = (page, from, to) => (0, _embedpdf_engine_core_runtime.measurementReadout)({
30
+ subtype: "line",
31
+ intent: "line-dimension",
32
+ measure: scaleOf(page).measure,
33
+ linePoints: {
34
+ start: from,
35
+ end: to
36
+ }
44
37
  });
38
+ const measureArea = (page, vertices) => (0, _embedpdf_engine_core_runtime.measurementReadout)({
39
+ subtype: "polygon",
40
+ intent: "polygon-dimension",
41
+ measure: scaleOf(page).measure,
42
+ vertices: [...vertices]
43
+ });
44
+ return { api: {
45
+ canMeasure: (page) => ctx.allows("annotations:create") && scaleOf(page).ready,
46
+ getPageScale: scaleOf,
47
+ isBusy: () => state().pending > 0,
48
+ listLastReports: () => state().reports,
49
+ listPresets: () => settings.get().presets,
50
+ listUnits: () => UNITS,
51
+ listAreaUnits: () => AREA_UNITS,
52
+ getReadout,
53
+ measureDistance,
54
+ measureArea,
55
+ getCalibrationRequest: () => state().calibration
56
+ } };
57
+ }
58
+ //#endregion
59
+ //#region src/services/events.ts
60
+ function createEvents(ctx) {
45
61
  return {
46
- measure: selected ? selected.measure ?? null : fallback,
47
- source: selected ? selected.owned ? "owned" : "foreign" : "default",
48
- ready: true,
49
- persistent
62
+ scaleChanged: ctx.events.source(),
63
+ calibrationRequested: ctx.events.source(),
64
+ calibrationCompleted: ctx.events.source(),
65
+ calibrationDismissed: ctx.events.source()
50
66
  };
51
67
  }
52
- function withUnit(m, unit, area = (0, _embedpdf_engine_core_runtime.squareOf)(unit)) {
53
- const basis = m.x[0]?.unit.trim();
54
- if (!(basis in _embedpdf_engine_core_runtime.METRES)) throw new RangeError("This scale has an unknown base unit; calibrate it first");
55
- const previous = m.distance[m.distance.length - 1];
56
- const precision = previous?.fraction === "fraction" ? 100 : previous?.precision ?? 100;
68
+ //#endregion
69
+ //#region src/services/siblings.ts
70
+ /** The sibling plugins: annotation (host lens) and the interaction hub. */
71
+ function resolveSiblings(ctx) {
57
72
  return {
58
- ...m,
59
- distance: [{
60
- unit,
61
- conversion: _embedpdf_engine_core_runtime.METRES[basis] / _embedpdf_engine_core_runtime.METRES[unit],
62
- precision,
63
- fraction: "decimal"
64
- }],
65
- area: [{
66
- unit: (0, _embedpdf_engine_core_runtime.areaUnitLabel)(area),
67
- conversion: _embedpdf_engine_core_runtime.METRES[basis] ** 2 / (0, _embedpdf_engine_core_runtime.squareMetres)(area),
68
- precision,
69
- fraction: "decimal"
70
- }]
73
+ annotation: ctx.get(_embedpdf_plugin_annotation_contract_host.AnnotationToken),
74
+ interaction: ctx.get(_embedpdf_plugin_interaction_contract.InteractionToken)
71
75
  };
72
76
  }
73
- function withPrecision(m, precision) {
74
- if (!Number.isInteger(Math.log10(precision)) || precision < 1 || precision > 1e9) throw new RangeError("Invalid decimal precision");
75
- const update = (formats) => formats.map((f, i) => i === formats.length - 1 ? {
76
- ...f,
77
- precision,
78
- fraction: "decimal"
79
- } : f);
77
+ //#endregion
78
+ //#region src/services/store.ts
79
+ /** Changing a page's scale rewrites its measurement annotations. */
80
+ const SCALE_PERMISSION = "doc.annotate.modify";
81
+ function createStore(ctx) {
82
+ /** A page argument of a verb, as a ref or an index: the page, or `not-found`. */
83
+ const requirePage = (page) => ctx.pageOf(page);
84
+ const canCalibrate = () => ctx.allows(SCALE_PERMISSION);
85
+ /** The pages a verb changes, each resolved: a page that isn't there refuses the whole call. */
86
+ const targets = (pages) => {
87
+ if (pages === "all") return (ctx.document()?.pages ?? []).map((layout) => layout.ref);
88
+ return (Array.isArray(pages) ? pages : [pages]).map((page) => ctx.pageOf(page).ref);
89
+ };
80
90
  return {
81
- ...m,
82
- distance: update(m.distance),
83
- area: update(m.area)
91
+ requirePage,
92
+ canCalibrate,
93
+ targets
84
94
  };
85
95
  }
86
- /** Change the area display unit while preserving imported distance formatting. */
87
- function withAreaUnit(measure, unit) {
88
- const basis = measure.x[0]?.unit.trim();
89
- if (!(basis in _embedpdf_engine_core_runtime.METRES)) throw new RangeError("This scale has an unknown base unit; calibrate it first");
90
- const previous = measure.area[measure.area.length - 1];
91
- const precision = previous?.fraction === "fraction" ? 100 : previous?.precision ?? 100;
96
+ //#endregion
97
+ //#region src/services/index.ts
98
+ function createServices(ctx) {
92
99
  return {
93
- ...measure,
94
- area: [{
95
- unit: (0, _embedpdf_engine_core_runtime.areaUnitLabel)(unit),
96
- conversion: _embedpdf_engine_core_runtime.METRES[basis] ** 2 / (0, _embedpdf_engine_core_runtime.squareMetres)(unit),
97
- precision,
98
- fraction: "decimal"
99
- }]
100
+ events: createEvents(ctx),
101
+ store: createStore(ctx),
102
+ siblings: resolveSiblings(ctx)
100
103
  };
101
104
  }
102
105
  //#endregion
103
- //#region src/capability.ts
104
- const LOADING = {
106
+ //#region src/model.ts
107
+ /**
108
+ * The measurement session: scale changes in flight, the pending calibration,
109
+ * the last change's reports, why a page's viewport read failed, and the
110
+ * session-only viewports of engines that cannot persist a scale. The pages'
111
+ * viewports themselves are engine data held by a page mirror. Every
112
+ * transition below is pure; the controller applies them with
113
+ * `ctx.state.update`. `selectPageScale` and `pageScaleOf` are the pure
114
+ * projections from viewports to the public {@link PageScale}.
115
+ */
116
+ /** No reports yet: the same array every time. */
117
+ const NO_REPORTS = Object.freeze([]);
118
+ const initialMeasurementState = () => ({
119
+ pending: 0,
120
+ calibration: null,
121
+ reports: NO_REPORTS,
122
+ loadErrors: {},
123
+ localViewports: {}
124
+ });
125
+ const beginScaleChange = (state) => ({
126
+ ...state,
127
+ pending: state.pending + 1
128
+ });
129
+ const endScaleChange = (state) => state.pending === 0 ? state : {
130
+ ...state,
131
+ pending: state.pending - 1
132
+ };
133
+ const setCalibration = (state, calibration) => state.calibration === calibration ? state : {
134
+ ...state,
135
+ calibration
136
+ };
137
+ const setReports = (state, reports) => state.reports === reports ? state : {
138
+ ...state,
139
+ reports
140
+ };
141
+ const recordLoadError = (state, pageObjectNumber, error) => ({
142
+ ...state,
143
+ loadErrors: {
144
+ ...state.loadErrors,
145
+ [pageObjectNumber]: error
146
+ }
147
+ });
148
+ const clearLoadError = (state, pageObjectNumber) => {
149
+ if (!(pageObjectNumber in state.loadErrors)) return state;
150
+ const { [pageObjectNumber]: _cleared, ...loadErrors } = state.loadErrors;
151
+ return {
152
+ ...state,
153
+ loadErrors
154
+ };
155
+ };
156
+ const setLocalViewports = (state, pageObjectNumber, viewports) => ({
157
+ ...state,
158
+ localViewports: {
159
+ ...state.localViewports,
160
+ [pageObjectNumber]: viewports
161
+ }
162
+ });
163
+ /**
164
+ * The scale a page's viewports resolve to: the owned viewport, else the one
165
+ * containing the page's center, else the fallback.
166
+ */
167
+ function selectPageScale(viewports, size, fallback, persistent) {
168
+ const selected = viewports.find((viewport) => viewport.owned) ?? (0, _embedpdf_engine_core_runtime.viewportForPoint)(viewports, {
169
+ x: size.width / 2,
170
+ y: size.height / 2
171
+ });
172
+ return {
173
+ measure: selected ? selected.measure ?? null : fallback,
174
+ source: selected ? selected.owned ? "owned" : "foreign" : "default",
175
+ ready: true,
176
+ persistent
177
+ };
178
+ }
179
+ /**
180
+ * The public page scale. A page is ready once its viewports were read; a
181
+ * failed re-read keeps the last confirmed viewports in effect and reports
182
+ * its error alongside them.
183
+ */
184
+ function pageScaleOf(inputs) {
185
+ const error = inputs.status === "error" && inputs.error ? { error: inputs.error } : {};
186
+ if (!inputs.viewports || !inputs.size) return {
187
+ measure: null,
188
+ source: "default",
189
+ ready: false,
190
+ persistent: inputs.persistent,
191
+ ...error
192
+ };
193
+ return {
194
+ ...selectPageScale(inputs.viewports, inputs.size, inputs.fallback, inputs.persistent),
195
+ ...error
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region src/sync/viewports.ts
200
+ /**
201
+ * The pages' viewports: a page mirror re-read whenever a confirmed
202
+ * `pages.scaleSet` names a loaded page, from this session or another
203
+ * (stream gaps and version moves re-read every loaded page). Its `changed`
204
+ * callback tells the annotation plugin a page's viewports, and a new
205
+ * `defaultScale` setting tells it again. `connect` loads every page in the
206
+ * registry, including pages inserted later.
207
+ */
208
+ /** The scale of a page index past the end: none, and never ready. */
209
+ const NO_PAGE = Object.freeze({
105
210
  measure: null,
106
211
  source: "default",
107
212
  ready: false,
108
213
  persistent: false
109
- };
110
- const UNITS = Object.keys(_embedpdf_engine_core_runtime.METRES);
111
- function createMeasurementCapability(ctx, config, effects) {
112
- const anno = ctx.get(_embedpdf_plugin_annotation_contract.AnnotationToken);
113
- const interaction = ctx.get(_embedpdf_plugin_interaction_contract.InteractionToken);
114
- const canCalibrate = () => ctx.doc?.security.allows("doc.annotate.modify") ?? false;
115
- const pons = (pon, opts) => pon === "all" || opts?.allPages ? (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber) : [pon];
116
- const scaleOf = (pon) => {
117
- const scale = ctx.getState().pages[pon]?.scale.measure;
118
- if (!scale || scale.subtype !== "RL") throw new RangeError("Calibrate this page before changing its units or precision");
119
- return scale;
120
- };
121
- const presets = config.presets ?? DEFAULT_PRESETS;
122
- return {
123
- canCalibrate,
124
- canMeasure: (pon) => anno.canCreate() && !!ctx.getState().pages[pon]?.scale.ready,
125
- pageScale: (pon) => ctx.getState().pages[pon]?.scale ?? LOADING,
126
- isBusy: () => ctx.getState().pending > 0,
127
- lastReports: () => ctx.getState().reports,
128
- prepare: effects.prepare,
129
- setPageScale: (pon, measure, opts = {}) => effects.change(pons(pon, opts), () => measure, opts),
130
- calibrate: (pon, from, to, real, opts = {}) => {
131
- const a = (0, _embedpdf_engine_core_runtime.measurementPoint)(from);
132
- const b = (0, _embedpdf_engine_core_runtime.measurementPoint)(to);
133
- const scale = (0, _embedpdf_engine_core_runtime.measureFromKnownLength)(Math.hypot(b.x - a.x, b.y - a.y), real);
134
- return effects.change(pons(pon, opts), () => scale, opts);
135
- },
136
- setUnit: (pon, unit, areaUnit, opts = {}) => effects.change(pons(pon, opts), (p) => withUnit(scaleOf(p), unit, areaUnit), {
137
- ...opts,
138
- allPages: pon === "all" || opts.allPages
139
- }),
140
- setPrecision: (pon, precision, opts = {}) => effects.change(pons(pon, opts), (p) => withPrecision(scaleOf(p), precision), {
141
- ...opts,
142
- allPages: pon === "all" || opts.allPages
143
- }),
144
- setPreset: (pon, id, opts = {}) => {
145
- const preset = presets.find((p) => p.id === id);
146
- if (!preset) return Promise.reject(/* @__PURE__ */ new RangeError("Unknown scale preset"));
147
- return effects.change(pons(pon, opts), (p) => (0, _embedpdf_engine_core_runtime.measureFromRatio)(preset.paper, preset.real, preset.unit, ctx.document()?.pages.find((page) => page.pageObjectNumber === p)?.userUnit ?? 1), opts);
148
- },
149
- presets: () => presets,
150
- units: () => UNITS,
151
- areaUnits: () => [
152
- ...UNITS.map(_embedpdf_engine_core_runtime.squareOf),
153
- "ha",
154
- "acre"
155
- ],
156
- setAreaUnit: (pon, unit, opts = {}) => effects.change(pons(pon, opts), (p) => withAreaUnit(scaleOf(p), unit), opts),
157
- readout: (ref) => {
158
- const dto = anno.get(ref);
159
- return dto ? (0, _embedpdf_engine_core_runtime.measurementReadout)(dto) : { unavailable: "not-dimension" };
160
- },
161
- startCalibration: () => {
162
- if (canCalibrate()) {
163
- ctx.dispatch({
164
- type: "CALIBRATION",
165
- request: null
166
- });
167
- interaction.activateTool("calibrate");
214
+ });
215
+ function createViewportSync(ctx, { siblings }) {
216
+ const { annotation } = siblings;
217
+ const settings = ctx.settings();
218
+ /** The scale of a page without one: the `defaultScale` setting, in the page's user unit. */
219
+ const fallbackOf = (layout) => require_contract.defaultMeasure(settings.get().defaultScale, layout?.userUnit ?? 1);
220
+ const viewports = ctx.pageMirror({
221
+ name: "viewports",
222
+ load: async (doc, page, signal) => {
223
+ ctx.assertPageRef(page);
224
+ const service = doc.page(page).measure;
225
+ if (!service) return ctx.state.get().localViewports[page.objectNumber] ?? [];
226
+ try {
227
+ return (await service.listViewports()).viewports;
228
+ } catch (error) {
229
+ if (!signal.aborted) ctx.state.update(recordLoadError, page.objectNumber, (0, _embedpdf_engine_core_runtime.serializeError)(error));
230
+ throw error;
168
231
  }
169
232
  },
170
- calibrationRequest: () => ctx.getState().calibration,
171
- dismissCalibration: () => ctx.dispatch({
172
- type: "CALIBRATION",
173
- request: null
174
- }),
175
- onCalibrationRequested: (cb) => {
176
- effects.calibrationListeners.add(cb);
177
- return () => {
178
- effects.calibrationListeners.delete(cb);
179
- };
180
- },
181
- onScaleChanged: (cb) => {
182
- effects.scaleListeners.add(cb);
183
- return () => {
184
- effects.scaleListeners.delete(cb);
185
- };
233
+ affected: (event) => event.type === "pages.scaleSet" ? [event.page] : null,
234
+ changed: ({ page, cause, next }) => {
235
+ if (cause === "drop" || next === void 0) return;
236
+ ctx.state.update(clearLoadError, page.objectNumber);
237
+ const layout = ctx.getPage(page);
238
+ if (layout) annotation.setPageViewports(page, [...next], fallbackOf(layout));
186
239
  }
240
+ });
241
+ /** The engine can persist a scale into the document. */
242
+ const isPersistent = (page) => ctx.doc.page(page).measure !== void 0;
243
+ /** The page's public scale, the same object until its viewports, read state or layout change. */
244
+ const scaleOfPage = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => {
245
+ const page = (0, _embedpdf_core.toPageRef)(pageObjectNumber);
246
+ const layout = ctx.getPage(page);
247
+ return [
248
+ viewports.get(page),
249
+ viewports.getStatus(page),
250
+ ctx.state.get().loadErrors[pageObjectNumber],
251
+ layout,
252
+ layout ? isPersistent(page) : false,
253
+ settings.get().defaultScale
254
+ ];
255
+ }, (_pageObjectNumber, pageViewports, status, error, layout, persistent) => pageScaleOf({
256
+ viewports: pageViewports,
257
+ status,
258
+ error,
259
+ size: layout?.size,
260
+ fallback: fallbackOf(layout),
261
+ persistent
262
+ }));
263
+ /** A page's scale, by ref or index; a page that isn't there has none, and isn't ready. */
264
+ const scaleOf = (page) => {
265
+ if (typeof page !== "number") return scaleOfPage(page.objectNumber);
266
+ const layout = ctx.getPage(page);
267
+ return layout ? scaleOfPage(layout.ref.objectNumber) : NO_PAGE;
268
+ };
269
+ const ensureLoaded = (page, options) => {
270
+ const layout = ctx.getPage(page);
271
+ if (!layout) return Promise.reject(new _embedpdf_core.PluginError("not-found", "measurement", "no such page"));
272
+ return ctx.cancellable(options?.signal, viewports.ensureLoaded(layout.ref));
273
+ };
274
+ const connect = () => {
275
+ const hydrate = (layouts) => {
276
+ for (const layout of layouts ?? []) {
277
+ if (viewports.getStatus(layout.ref) !== "idle") continue;
278
+ viewports.ensureLoaded(layout.ref).catch(() => {});
279
+ }
280
+ };
281
+ hydrate(ctx.document()?.pages);
282
+ ctx.watch(() => ctx.document()?.pages, hydrate);
283
+ ctx.listen(settings.api.onSettingsChanged, ({ changed }) => {
284
+ if (!changed.includes("defaultScale")) return;
285
+ for (const layout of ctx.document()?.pages ?? []) {
286
+ const known = viewports.get(layout.ref);
287
+ if (known) annotation.setPageViewports(layout.ref, [...known], fallbackOf(layout));
288
+ }
289
+ });
290
+ };
291
+ return {
292
+ ensureLoaded,
293
+ scaleOf,
294
+ /** Re-read one page; for session-only scales, which no engine event announces. */
295
+ refresh: (page) => viewports.refresh(page),
296
+ connect
187
297
  };
188
298
  }
189
299
  //#endregion
190
- //#region src/effects.ts
191
- /** All engine IO and cross-plugin lifecycle work lives here. Calls serialize
192
- * per page; refresh epochs prevent an old read from replacing a newer scale. */
193
- function createMeasurementEffects(ctx, config) {
194
- const anno = ctx.get(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
195
- const interaction = ctx.get(_embedpdf_plugin_interaction_contract.InteractionToken);
196
- let disposed = false;
197
- const epochs = /* @__PURE__ */ new Map();
198
- const reading = /* @__PURE__ */ new Map();
199
- const queues = /* @__PURE__ */ new Map();
200
- const calibrationListeners = /* @__PURE__ */ new Set();
201
- const scaleListeners = /* @__PURE__ */ new Set();
202
- const meta = (pon) => ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
203
- const live = () => {
204
- if (disposed || !ctx.doc) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen);
300
+ //#region src/write/calibration.ts
301
+ /** The calibrate flow: arm the tool, turn a captured distance draft into a
302
+ * request (page space), and dismiss it. */
303
+ function createCalibration(ctx, { events, store, siblings }) {
304
+ const { calibrationRequested, calibrationDismissed } = events;
305
+ const { canCalibrate } = store;
306
+ const { annotation, interaction } = siblings;
307
+ const startCalibration = () => {
308
+ ctx.assertAllowed(SCALE_PERMISSION, "measurement.startCalibration");
309
+ ctx.state.update(setCalibration, null);
310
+ interaction.activateTool("calibrate");
205
311
  };
206
- const assertAllowed = () => {
207
- live();
208
- if (!ctx.doc.security.allows("doc.annotate.modify")) throw new _embedpdf_engine_core_runtime.PermissionDenied("doc.annotate.modify", "measurement");
312
+ const dismissCalibration = () => {
313
+ if (!ctx.state.get().calibration) return;
314
+ ctx.state.update(setCalibration, null);
315
+ calibrationDismissed.emit({});
209
316
  };
210
- const publish = (pon, viewports) => {
211
- if (disposed) return;
212
- const page = meta(pon);
213
- if (!page) return;
214
- const fallback = defaultMeasure(config, page.userUnit);
215
- anno.setPageViewports(pon, viewports, fallback);
216
- ctx.dispatch({
217
- type: "PAGE_SCALE",
218
- pon,
219
- viewports,
220
- scale: selectPageScale(viewports, page.boxes.crop, fallback, !!ctx.doc?.page(pon).measure)
317
+ /** A distance draft from the calibrate tool becomes the pending request. */
318
+ const connect = () => {
319
+ ctx.listen(annotation.onDraftCaptured, (draft) => {
320
+ if (draft.tool !== "calibrate" || !canCalibrate()) return;
321
+ const from = (0, _embedpdf_engine_core_runtime.measurementPoint)(draft.from);
322
+ const to = (0, _embedpdf_engine_core_runtime.measurementPoint)(draft.to);
323
+ const userSpaceLength = Math.hypot(to.x - from.x, to.y - from.y);
324
+ if (!(userSpaceLength > 0)) return;
325
+ const request = {
326
+ page: draft.page,
327
+ from,
328
+ to,
329
+ userSpaceLength
330
+ };
331
+ interaction.activateTool("pointer");
332
+ ctx.state.update(setCalibration, request);
333
+ calibrationRequested.emit({ request });
221
334
  });
222
335
  };
223
- const refresh = (pon) => {
224
- live();
225
- const page = meta(pon);
226
- if (!page) return Promise.reject(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, "Page not found"));
227
- const epoch = (epochs.get(pon) ?? 0) + 1;
228
- epochs.set(pon, epoch);
229
- anno.setPageViewports(pon, void 0, defaultMeasure(config, page.userUnit));
230
- const previous = ctx.getState().pages[pon];
231
- ctx.dispatch({
232
- type: "PAGE_SCALE",
233
- pon,
234
- viewports: previous?.viewports ?? [],
235
- scale: {
236
- ...previous?.scale ?? {
237
- measure: null,
238
- source: "default",
239
- persistent: !!ctx.doc.page(pon).measure
240
- },
241
- ready: false
242
- }
243
- });
244
- const service = ctx.doc.page(pon).measure;
245
- const request = (async () => {
246
- try {
247
- const viewports = service ? await service.viewports() : ctx.getState().pages[pon]?.viewports ?? [];
248
- if (!disposed && epochs.get(pon) === epoch) publish(pon, viewports);
249
- } catch (error) {
250
- if (!disposed && epochs.get(pon) === epoch) ctx.dispatch({
251
- type: "PAGE_SCALE",
252
- pon,
253
- viewports: [],
254
- scale: {
255
- measure: null,
256
- source: "default",
257
- ready: false,
258
- persistent: !!service,
259
- error: (0, _embedpdf_engine_core_runtime.serializeError)(error)
260
- }
261
- });
262
- throw error;
263
- } finally {
264
- if (epochs.get(pon) === epoch) reading.delete(pon);
336
+ return {
337
+ connect,
338
+ api: {
339
+ startCalibration,
340
+ dismissCalibration
341
+ }
342
+ };
343
+ }
344
+ //#endregion
345
+ //#region src/write/create.ts
346
+ /** Measurement creation: a measurement annotation with the page's scale, through the annotation plugin. */
347
+ function createMeasuring(ctx, { siblings }, { scaleOf }) {
348
+ const { annotation } = siblings;
349
+ /**
350
+ * The measurement's engine draft: the tool's current defaults (style,
351
+ * caption and leader), the dimension intent, and the page's scale. The
352
+ * engine works out the label from the points and the scale.
353
+ */
354
+ const draftOf = (input, page, tool) => {
355
+ const style = annotation.tools.getDefaults(tool);
356
+ const shared = {
357
+ color: style.color,
358
+ strokeWidth: style.strokeWidth,
359
+ opacity: style.opacity,
360
+ measure: scaleOf(page).measure,
361
+ captionEnabled: style.captionEnabled ?? true
362
+ };
363
+ switch (input.kind) {
364
+ case "distance": {
365
+ const [from, to] = input.points;
366
+ if (!from || !to || input.points.length !== 2) return new _embedpdf_core.PluginError("invalid-input", "measurement", "a distance needs exactly two points");
367
+ return {
368
+ subtype: "line",
369
+ intent: "line-dimension",
370
+ linePoints: {
371
+ start: from,
372
+ end: to
373
+ },
374
+ lineEndings: style.lineEndings,
375
+ interiorColor: style.interiorColor ?? style.color,
376
+ ...shared,
377
+ captionPosition: style.captionPosition ?? "inline",
378
+ ...style.leader ? { leader: style.leader } : {}
379
+ };
265
380
  }
266
- })();
267
- reading.set(pon, request);
268
- return request;
381
+ case "perimeter": return {
382
+ subtype: "polyline",
383
+ intent: "polyline-dimension",
384
+ vertices: input.points,
385
+ lineEndings: style.lineEndings,
386
+ ...shared
387
+ };
388
+ case "area": return {
389
+ subtype: "polygon",
390
+ intent: "polygon-dimension",
391
+ vertices: input.points,
392
+ interiorColor: style.interiorColor,
393
+ ...shared
394
+ };
395
+ }
396
+ };
397
+ const createMeasurement = async (input, options) => {
398
+ ctx.assertAllowed("annotations:create", "measurement.createMeasurement");
399
+ const page = ctx.pageOf(input.page).ref;
400
+ if (!scaleOf(page).ready) throw new _embedpdf_core.PluginError("not-ready", "measurement", "the page scale is not known yet");
401
+ const draft = draftOf(input, page, input.tool ?? input.kind);
402
+ if (draft instanceof Error) throw draft;
403
+ return annotation.create(page, draft, void 0, { signal: options?.signal });
269
404
  };
270
- const prepare = (pon) => reading.get(pon) ?? (ctx.getState().pages[pon]?.scale.ready ? Promise.resolve() : refresh(pon));
271
- const enqueue = (pon, action) => {
272
- const result = (queues.get(pon) ?? Promise.resolve()).catch(() => {}).then(action);
273
- queues.set(pon, result);
274
- result.finally(() => {
275
- if (queues.get(pon) === result) queues.delete(pon);
276
- }).catch(() => {});
277
- return result;
405
+ return { api: { createMeasurement } };
406
+ }
407
+ //#endregion
408
+ //#region src/write/scale.ts
409
+ /**
410
+ * Scale writes. Every change serializes per page, loads the page first, and
411
+ * re-measures the page's measurement annotations unless told not to. A
412
+ * multi-page change reports per-page failures; a single-page change rejects.
413
+ */
414
+ function createScaleWrites(ctx, { events, store, siblings }, { ensureLoaded, refresh, scaleOf }) {
415
+ const { scaleChanged, calibrationCompleted } = events;
416
+ const { requirePage, targets } = store;
417
+ const { annotation } = siblings;
418
+ const settings = ctx.settings();
419
+ /** One write queue per page, so a page's scale changes land in order. */
420
+ const pageQueues = /* @__PURE__ */ new Map();
421
+ const queueOf = (page) => {
422
+ let queue = pageQueues.get(page.objectNumber);
423
+ if (!queue) {
424
+ queue = ctx.serialQueue(`scale:${page.objectNumber}`);
425
+ pageQueues.set(page.objectNumber, queue);
426
+ }
427
+ return queue;
278
428
  };
279
- const save = async (pon, scale, opts) => {
280
- assertAllowed();
281
- await prepare(pon);
282
- assertAllowed();
283
- const service = ctx.doc.page(pon).measure;
429
+ ctx.onSettle(async () => {
430
+ await Promise.all([...pageQueues.values()].map((queue) => queue.idle()));
431
+ });
432
+ /** Write one page's scale (`null` = back to the default) and re-measure. */
433
+ const save = async (page, scale, options) => {
434
+ const { signal } = options;
435
+ ctx.assertAllowed(SCALE_PERMISSION, "changing a scale");
436
+ await ensureLoaded(page, { signal });
437
+ ctx.assertAllowed(SCALE_PERMISSION, "changing a scale");
438
+ const service = ctx.doc.page(page).measure;
284
439
  if (service) {
285
- await service.setScale(scale);
286
- live();
287
- await refresh(pon);
440
+ await ctx.cancellable(signal, service.setScale(scale));
441
+ await ensureLoaded(page);
288
442
  } else {
289
- const page = meta(pon);
290
- const old = ctx.getState().pages[pon]?.viewports ?? [];
291
- publish(pon, [...old.filter((v) => !v.owned), {
292
- bbox: page.boxes.crop,
443
+ const layout = requirePage(page);
444
+ const kept = (ctx.state.get().localViewports[page.objectNumber] ?? []).filter((viewport) => !viewport.owned);
445
+ ctx.state.update(setLocalViewports, page.objectNumber, [...kept, ...scale ? [{
446
+ bbox: {
447
+ x: 0,
448
+ y: 0,
449
+ ...layout.size
450
+ },
293
451
  name: "EmbedPDF",
294
452
  owned: true,
295
453
  measure: scale
296
- }]);
454
+ }] : []]);
455
+ await refresh(page);
297
456
  }
298
- live();
299
- const report = opts.recalculate === false ? {
300
- pon,
301
- scale,
457
+ const effective = scale ?? require_contract.defaultMeasure(settings.get().defaultScale, requirePage(page).userUnit);
458
+ const report = options.recalculate === false ? {
459
+ page,
460
+ scale: effective,
302
461
  updated: [],
303
462
  skipped: [],
304
463
  failed: []
305
- } : await anno.remeasurePage(pon, scale);
306
- live();
307
- for (const cb of scaleListeners) try {
308
- cb({
309
- pon,
310
- report
311
- });
312
- } catch {}
464
+ } : await ctx.cancellable(signal, annotation.remeasurePage(page, effective));
465
+ scaleChanged.emit({
466
+ page,
467
+ report
468
+ });
313
469
  return report;
314
470
  };
315
- const change = async (pons, measure, opts = {}) => {
316
- assertAllowed();
317
- ctx.dispatch({
318
- type: "PENDING",
319
- delta: 1
320
- });
471
+ const change = async (pages, measure, options = {}) => {
472
+ ctx.assertAllowed(SCALE_PERMISSION, "changing a scale");
473
+ const target = targets(pages);
474
+ if (options.signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "measurement", "operation cancelled", { cause: options.signal.reason });
475
+ const tolerant = pages === "all" || target.length > 1;
476
+ ctx.state.update(beginScaleChange);
321
477
  try {
322
- const reports = await Promise.all(pons.map((pon) => enqueue(pon, async () => {
478
+ const reports = await Promise.all(target.map((page) => queueOf(page)(async () => {
323
479
  try {
324
- await prepare(pon);
325
- const scale = measure(pon);
326
- (0, _embedpdf_engine_core_runtime.assertWritableMeasure)(scale);
327
- return await save(pon, scale, opts);
480
+ await ensureLoaded(page, { signal: options.signal });
481
+ const scale = measure(page);
482
+ if (scale) (0, _embedpdf_engine_core_runtime.assertWritableMeasure)(scale);
483
+ return await save(page, scale, options);
328
484
  } catch (error) {
329
- if (!opts.allPages) throw error;
485
+ const refused = (0, _embedpdf_core.toPluginError)("measurement", error);
486
+ if (!tolerant || refused.code === "operation-cancelled") throw refused;
330
487
  return {
331
- pon,
488
+ page,
332
489
  updated: [],
333
490
  skipped: [],
334
491
  failed: [],
335
492
  scaleError: (0, _embedpdf_engine_core_runtime.serializeError)(error)
336
493
  };
337
494
  }
338
- })));
339
- live();
340
- ctx.dispatch({
341
- type: "REPORTS",
342
- reports
343
- });
495
+ }, options)));
496
+ ctx.state.update(setReports, reports);
344
497
  return reports;
345
498
  } finally {
346
- if (!disposed) ctx.dispatch({
347
- type: "PENDING",
348
- delta: -1
349
- });
499
+ ctx.state.update(endScaleChange);
350
500
  }
351
501
  };
352
- const start = () => {
353
- const doc = ctx.doc;
354
- if (!doc) return;
355
- ctx.cleanup(() => {
356
- disposed = true;
357
- epochs.clear();
358
- queues.clear();
359
- calibrationListeners.clear();
360
- scaleListeners.clear();
361
- });
362
- ctx.cleanup(anno.onDraftCaptured((draft) => {
363
- if (draft.tool !== "calibrate" || !doc.security.allows("doc.annotate.modify")) return;
364
- const a = (0, _embedpdf_engine_core_runtime.measurementPoint)(draft.from);
365
- const b = (0, _embedpdf_engine_core_runtime.measurementPoint)(draft.to);
366
- const userSpaceLength = Math.hypot(b.x - a.x, b.y - a.y);
367
- if (!(userSpaceLength > 0)) return;
368
- const request = {
369
- pon: draft.pon,
370
- from: a,
371
- to: b,
372
- userSpaceLength
373
- };
374
- interaction.activateTool("pointer");
375
- ctx.dispatch({
376
- type: "CALIBRATION",
377
- request
378
- });
379
- for (const cb of calibrationListeners) try {
380
- cb(request);
381
- } catch {}
382
- }));
383
- const hydrate = () => {
384
- for (const page of ctx.document()?.pages ?? []) refresh(page.pageObjectNumber).catch(() => {});
385
- };
386
- ctx.cleanup(doc.events.subscribe((event) => {
387
- if (event.type === "page.viewportsChanged") refresh(event.pageObjectNumber).catch(() => {});
388
- else if (event.type === "stream.desynced" || event.type === "document.versioned") hydrate();
389
- }));
390
- let pages = "";
391
- const reconcile = () => {
392
- const next = (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber).join(",");
393
- if (next === pages) return;
394
- pages = next;
395
- for (const page of ctx.document()?.pages ?? []) if (!ctx.getState().pages[page.pageObjectNumber]) prepare(page.pageObjectNumber).catch(() => {});
396
- };
397
- ctx.cleanup(ctx.subscribe(reconcile));
398
- reconcile();
502
+ /** The page's current rectilinear scale, for unit and precision edits. */
503
+ const rectilinearOf = (page) => {
504
+ const scale = scaleOf(page).measure;
505
+ if (!scale || scale.subtype !== "rectilinear") throw new _embedpdf_core.PluginError("not-ready", "measurement", "calibrate this page before changing its units or precision");
506
+ return scale;
399
507
  };
400
- return {
401
- start,
402
- prepare,
403
- change,
404
- calibrationListeners,
405
- scaleListeners
508
+ const calibrate = async (input, options = {}) => {
509
+ const start = (0, _embedpdf_engine_core_runtime.measurementPoint)(input.from);
510
+ const end = (0, _embedpdf_engine_core_runtime.measurementPoint)(input.to);
511
+ let scale;
512
+ try {
513
+ scale = (0, _embedpdf_engine_core_runtime.measureFromKnownLength)(Math.hypot(end.x - start.x, end.y - start.y), input.distance);
514
+ } catch (error) {
515
+ throw new _embedpdf_core.PluginError("invalid-input", "measurement", String(error.message ?? error));
516
+ }
517
+ const { applyTo, ...rest } = options;
518
+ const page = requirePage(input.page).ref;
519
+ const reports = await change(applyTo ?? page, () => scale, rest);
520
+ const request = ctx.state.get().calibration;
521
+ if (request && (0, _embedpdf_engine_core_runtime.pageRefsEqual)(request.page, page)) ctx.state.update(setCalibration, null);
522
+ calibrationCompleted.emit({ page });
523
+ return reports;
406
524
  };
525
+ return { api: {
526
+ setScale: (pages, measure, options) => change(pages, () => measure, options),
527
+ calibrate,
528
+ setUnit: (pages, unit, options) => change(pages, (page) => require_contract.withUnit(rectilinearOf(page), unit), options),
529
+ setAreaUnit: (pages, unit, options) => change(pages, (page) => require_contract.withAreaUnit(rectilinearOf(page), unit), options),
530
+ setPrecision: (pages, precision, options) => change(pages, (page) => require_contract.withPrecision(rectilinearOf(page), precision), options),
531
+ setPreset: (pages, presetId, options) => {
532
+ const preset = settings.get().presets.find((candidate) => candidate.id === presetId);
533
+ if (!preset) return Promise.reject(new _embedpdf_core.PluginError("not-found", "measurement", `unknown scale preset '${presetId}'`));
534
+ return change(pages, (page) => (0, _embedpdf_engine_core_runtime.measureFromRatio)(preset.paper, preset.real, preset.unit, { userUnit: ctx.getPage(page)?.userUnit ?? 1 }), options);
535
+ },
536
+ clearScale: (pages, options) => change(pages, () => null, options)
537
+ } };
407
538
  }
408
539
  //#endregion
409
- //#region src/reducer.ts
410
- const initialMeasurementState = {
411
- pages: {},
412
- pending: 0,
413
- calibration: null,
414
- reports: []
415
- };
416
- function measurementReducer(s, a) {
417
- switch (a.type) {
418
- case "PAGE_SCALE": return {
419
- ...s,
420
- pages: {
421
- ...s.pages,
422
- [a.pon]: {
423
- viewports: a.viewports,
424
- scale: a.scale
425
- }
540
+ //#region src/controller.ts
541
+ /**
542
+ * The measurement controller: the composition root. It builds the plugin's
543
+ * services once, wires each area with the services it declares, and
544
+ * assembles the capability from the areas' API slices. No behavior lives
545
+ * here: every verb and read has a home in `read/`, `write/` or `sync/`.
546
+ */
547
+ function createMeasurementController(ctx) {
548
+ const services = createServices(ctx);
549
+ const { events, store } = services;
550
+ const settings = ctx.settings();
551
+ const viewports = createViewportSync(ctx, services);
552
+ const reads = createScaleReads(ctx, services, viewports);
553
+ const scale = createScaleWrites(ctx, services, viewports);
554
+ const calibration = createCalibration(ctx, services);
555
+ const measuring = createMeasuring(ctx, services, viewports);
556
+ return {
557
+ api: (0, _embedpdf_core.composeApi)("measurement", [
558
+ settings.api,
559
+ reads.api,
560
+ scale.api,
561
+ calibration.api,
562
+ measuring.api,
563
+ {
564
+ canCalibrate: store.canCalibrate,
565
+ ensureLoaded: viewports.ensureLoaded,
566
+ onScaleChanged: events.scaleChanged.on,
567
+ onCalibrationRequested: events.calibrationRequested.on,
568
+ onCalibrationCompleted: events.calibrationCompleted.on,
569
+ onCalibrationDismissed: events.calibrationDismissed.on
426
570
  }
427
- };
428
- case "PENDING": return {
429
- ...s,
430
- pending: Math.max(0, s.pending + a.delta)
431
- };
432
- case "CALIBRATION": return {
433
- ...s,
434
- calibration: a.request
435
- };
436
- case "REPORTS": return {
437
- ...s,
438
- reports: a.reports
439
- };
440
- }
571
+ ]),
572
+ connect() {
573
+ calibration.connect();
574
+ viewports.connect();
575
+ }
576
+ };
441
577
  }
442
578
  //#endregion
443
579
  //#region src/measurement.plugin.ts
444
- const measurementPlugin = (config = {}) => {
445
- const drivers = /* @__PURE__ */ new Map();
446
- return (0, _embedpdf_core.definePlugin)({
447
- id: "measurement",
448
- scope: "document",
449
- token: require_contract.MeasurementToken,
450
- requires: [_embedpdf_plugin_annotation_contract_host.AnnotationToken, _embedpdf_plugin_interaction_contract.InteractionToken],
451
- initialState: initialMeasurementState,
452
- reduce: measurementReducer,
453
- capability: (ctx) => {
454
- const effects = createMeasurementEffects(ctx, config);
455
- drivers.set(ctx.documentId, effects);
456
- ctx.cleanup(() => {
457
- drivers.delete(ctx.documentId);
458
- });
459
- return createMeasurementCapability(ctx, config, effects);
460
- },
461
- effects: (ctx) => {
462
- ctx.get(require_contract.MeasurementToken);
463
- drivers.get(ctx.documentId).start();
464
- }
465
- });
466
- };
580
+ /**
581
+ * Page scale, calibration and measurement readouts, document-scoped. The
582
+ * annotation plugin owns the measurement annotations; this plugin owns the
583
+ * page's viewports (the scale), keeps the annotation plugin's measure in step,
584
+ * and turns the calibrate tool's drafts into scale requests. `config` is the
585
+ * settings the app registers, over {@link MEASUREMENT_DEFAULTS}.
586
+ */
587
+ const measurementPlugin = (config) => (0, _embedpdf_core.definePlugin)({
588
+ id: "measurement",
589
+ scope: "document",
590
+ token: require_contract.MeasurementToken,
591
+ requires: [_embedpdf_plugin_annotation_contract_host.AnnotationToken, _embedpdf_plugin_interaction_contract.InteractionToken],
592
+ state: initialMeasurementState,
593
+ settings: {
594
+ defaults: require_contract.MEASUREMENT_DEFAULTS,
595
+ registered: config,
596
+ whole: ["defaultScale"]
597
+ },
598
+ create: createMeasurementController
599
+ });
600
+ //#endregion
601
+ //#region src/state.ts
602
+ /**
603
+ * The measurements page's State table as code: what `useMeasurementState()`
604
+ * returns, and the same fields in every other framework. A page's scale and a
605
+ * measurement's readout are not here: `usePageScale(page)` and
606
+ * `useMeasurementReadout(ref)` read them, so a component re-renders only for
607
+ * its own page or measurement.
608
+ */
609
+ const measurementState = (0, _embedpdf_core.defineState)(require_contract.MeasurementToken, {
610
+ read: (measurement) => ({
611
+ busy: measurement.isBusy(),
612
+ calibrationRequest: measurement.getCalibrationRequest(),
613
+ lastReports: measurement.listLastReports()
614
+ }),
615
+ empty: {
616
+ busy: false,
617
+ calibrationRequest: null,
618
+ lastReports: NO_REPORTS
619
+ }
620
+ });
467
621
  //#endregion
622
+ exports.MEASUREMENT_DEFAULTS = require_contract.MEASUREMENT_DEFAULTS;
468
623
  exports.MeasurementToken = require_contract.MeasurementToken;
624
+ exports.NO_PAGE_SCALE = require_contract.NO_PAGE_SCALE;
625
+ exports.NO_READOUT = require_contract.NO_READOUT;
469
626
  exports.measurementPlugin = measurementPlugin;
627
+ exports.measurementState = measurementState;
470
628
 
471
629
  //# sourceMappingURL=index.cjs.map