@fieldnotes/core 0.58.0 → 0.60.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/index.cjs +206 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +206 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5272,6 +5272,7 @@ var ToolManager = class {
|
|
|
5272
5272
|
tools = /* @__PURE__ */ new Map();
|
|
5273
5273
|
current = null;
|
|
5274
5274
|
changeListeners = /* @__PURE__ */ new Set();
|
|
5275
|
+
registerListeners = /* @__PURE__ */ new Set();
|
|
5275
5276
|
get activeTool() {
|
|
5276
5277
|
return this.current;
|
|
5277
5278
|
}
|
|
@@ -5280,6 +5281,7 @@ var ToolManager = class {
|
|
|
5280
5281
|
}
|
|
5281
5282
|
register(tool) {
|
|
5282
5283
|
this.tools.set(tool.name, tool);
|
|
5284
|
+
this.registerListeners.forEach((fn) => fn(tool));
|
|
5283
5285
|
}
|
|
5284
5286
|
getTool(name) {
|
|
5285
5287
|
return this.tools.get(name);
|
|
@@ -5305,6 +5307,10 @@ var ToolManager = class {
|
|
|
5305
5307
|
this.changeListeners.add(listener);
|
|
5306
5308
|
return () => this.changeListeners.delete(listener);
|
|
5307
5309
|
}
|
|
5310
|
+
onRegister(listener) {
|
|
5311
|
+
this.registerListeners.add(listener);
|
|
5312
|
+
return () => this.registerListeners.delete(listener);
|
|
5313
|
+
}
|
|
5308
5314
|
};
|
|
5309
5315
|
|
|
5310
5316
|
// src/history/history-stack.ts
|
|
@@ -5496,6 +5502,7 @@ var HistoryRecorder = class {
|
|
|
5496
5502
|
generation = 0;
|
|
5497
5503
|
updateSnapshots = /* @__PURE__ */ new Map();
|
|
5498
5504
|
unsubscribers;
|
|
5505
|
+
transactionEndListeners = /* @__PURE__ */ new Set();
|
|
5499
5506
|
pause() {
|
|
5500
5507
|
this.recording = false;
|
|
5501
5508
|
}
|
|
@@ -5513,19 +5520,35 @@ var HistoryRecorder = class {
|
|
|
5513
5520
|
get currentTransactionId() {
|
|
5514
5521
|
return this.transaction !== null ? this.generation : null;
|
|
5515
5522
|
}
|
|
5523
|
+
onTransactionEnd(listener) {
|
|
5524
|
+
this.transactionEndListeners.add(listener);
|
|
5525
|
+
return () => this.transactionEndListeners.delete(listener);
|
|
5526
|
+
}
|
|
5527
|
+
notifyTransactionEnd() {
|
|
5528
|
+
for (const listener of this.transactionEndListeners) {
|
|
5529
|
+
try {
|
|
5530
|
+
listener();
|
|
5531
|
+
} catch {
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5516
5535
|
commit() {
|
|
5517
5536
|
if (!this.transaction) return;
|
|
5518
5537
|
const finalCommands = this.flushUpdateSnapshots();
|
|
5519
5538
|
const all = [...this.transaction, ...finalCommands];
|
|
5520
5539
|
this.transaction = null;
|
|
5521
5540
|
this.updateSnapshots.clear();
|
|
5522
|
-
if (all.length
|
|
5523
|
-
|
|
5524
|
-
|
|
5541
|
+
if (all.length > 0) {
|
|
5542
|
+
const first = all[0];
|
|
5543
|
+
this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
|
|
5544
|
+
}
|
|
5545
|
+
this.notifyTransactionEnd();
|
|
5525
5546
|
}
|
|
5526
5547
|
rollback() {
|
|
5548
|
+
const wasOpen = this.transaction !== null;
|
|
5527
5549
|
this.transaction = null;
|
|
5528
5550
|
this.updateSnapshots.clear();
|
|
5551
|
+
if (wasOpen) this.notifyTransactionEnd();
|
|
5529
5552
|
}
|
|
5530
5553
|
destroy() {
|
|
5531
5554
|
this.unsubscribers.forEach((fn) => fn());
|
|
@@ -5855,6 +5878,19 @@ function computeBounds(elements, padding) {
|
|
|
5855
5878
|
h: maxY - minY + padding * 2
|
|
5856
5879
|
};
|
|
5857
5880
|
}
|
|
5881
|
+
function resolveExportBounds(region, elements, padding) {
|
|
5882
|
+
if (!region) return computeBounds(elements, padding);
|
|
5883
|
+
const finite = Number.isFinite(region.x) && Number.isFinite(region.y) && Number.isFinite(region.w) && Number.isFinite(region.h);
|
|
5884
|
+
if (!finite || region.w <= 0 || region.h <= 0) {
|
|
5885
|
+
throw new RangeError("region must have finite coordinates and positive w/h");
|
|
5886
|
+
}
|
|
5887
|
+
return {
|
|
5888
|
+
x: region.x - padding,
|
|
5889
|
+
y: region.y - padding,
|
|
5890
|
+
w: region.w + padding * 2,
|
|
5891
|
+
h: region.h + padding * 2
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5858
5894
|
function renderGridForBounds(ctx, grid, bounds) {
|
|
5859
5895
|
const visibleBounds = {
|
|
5860
5896
|
minX: bounds.x,
|
|
@@ -5897,6 +5933,29 @@ function nonNegativeOption(value, fallback, name) {
|
|
|
5897
5933
|
}
|
|
5898
5934
|
return resolved;
|
|
5899
5935
|
}
|
|
5936
|
+
function fitExportScale(bounds, requestedScale, options) {
|
|
5937
|
+
const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
|
|
5938
|
+
const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
|
|
5939
|
+
const fits = (scale) => {
|
|
5940
|
+
const w = Math.ceil(bounds.w * scale);
|
|
5941
|
+
const h = Math.ceil(bounds.h * scale);
|
|
5942
|
+
return w <= maxDimension && h <= maxDimension && w * h <= maxPixels;
|
|
5943
|
+
};
|
|
5944
|
+
if (fits(requestedScale)) return requestedScale;
|
|
5945
|
+
const analytical = Math.min(
|
|
5946
|
+
maxDimension / Math.max(bounds.w, bounds.h),
|
|
5947
|
+
Math.sqrt(maxPixels) / Math.sqrt(bounds.w) / Math.sqrt(bounds.h)
|
|
5948
|
+
);
|
|
5949
|
+
let hi = Math.min(requestedScale, analytical);
|
|
5950
|
+
if (fits(hi)) return hi;
|
|
5951
|
+
let lo = 0;
|
|
5952
|
+
for (let i = 0; i < 40; i++) {
|
|
5953
|
+
const mid = (lo + hi) / 2;
|
|
5954
|
+
if (fits(mid)) lo = mid;
|
|
5955
|
+
else hi = mid;
|
|
5956
|
+
}
|
|
5957
|
+
return lo;
|
|
5958
|
+
}
|
|
5900
5959
|
function assertExportSize(width, height, options) {
|
|
5901
5960
|
const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
|
|
5902
5961
|
const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
|
|
@@ -5971,10 +6030,23 @@ function loadImages(elements, options = {}) {
|
|
|
5971
6030
|
});
|
|
5972
6031
|
}
|
|
5973
6032
|
async function exportImage(store, options = {}, layerManager) {
|
|
5974
|
-
const
|
|
6033
|
+
const requestedScale = positiveOption(options.scale, 2, "scale");
|
|
6034
|
+
const scaleMode = options.scaleMode ?? "exact";
|
|
6035
|
+
if (scaleMode !== "exact" && scaleMode !== "fit") {
|
|
6036
|
+
throw new RangeError(`scaleMode must be 'exact' or 'fit'`);
|
|
6037
|
+
}
|
|
5975
6038
|
const padding = nonNegativeOption(options.padding, 0, "padding");
|
|
5976
6039
|
validateExportResourceOptions(options);
|
|
5977
6040
|
validateHtmlExportOptions(options);
|
|
6041
|
+
const format = options.format ?? "png";
|
|
6042
|
+
if (format !== "png" && format !== "jpeg") {
|
|
6043
|
+
throw new RangeError(`format must be 'png' or 'jpeg'`);
|
|
6044
|
+
}
|
|
6045
|
+
if (options.quality !== void 0) {
|
|
6046
|
+
if (!Number.isFinite(options.quality) || options.quality <= 0 || options.quality > 1) {
|
|
6047
|
+
throw new RangeError("quality must be a finite number in (0, 1]");
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
5978
6050
|
const background = options.background ?? "#ffffff";
|
|
5979
6051
|
const filter = options.filter;
|
|
5980
6052
|
const allElements = store.getAll();
|
|
@@ -5982,8 +6054,9 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
5982
6054
|
if (filter) {
|
|
5983
6055
|
visibleElements = visibleElements.filter(filter);
|
|
5984
6056
|
}
|
|
5985
|
-
const bounds =
|
|
6057
|
+
const bounds = resolveExportBounds(options.region, visibleElements, padding);
|
|
5986
6058
|
if (!bounds) return null;
|
|
6059
|
+
const scale = scaleMode === "fit" ? fitExportScale(bounds, requestedScale, options) : requestedScale;
|
|
5987
6060
|
const width = Math.ceil(bounds.w * scale);
|
|
5988
6061
|
const height = Math.ceil(bounds.h * scale);
|
|
5989
6062
|
assertExportSize(width, height, options);
|
|
@@ -6085,8 +6158,9 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
6085
6158
|
renderGridForBounds(ctx, grid, bounds);
|
|
6086
6159
|
ctx.restore();
|
|
6087
6160
|
}
|
|
6161
|
+
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
6088
6162
|
return new Promise((resolve) => {
|
|
6089
|
-
canvas.toBlob((blob) => resolve(blob),
|
|
6163
|
+
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
6090
6164
|
});
|
|
6091
6165
|
}
|
|
6092
6166
|
|
|
@@ -7766,6 +7840,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
|
|
|
7766
7840
|
}
|
|
7767
7841
|
|
|
7768
7842
|
// src/canvas/selection-ops.ts
|
|
7843
|
+
var STYLE_FIELDS = [
|
|
7844
|
+
"color",
|
|
7845
|
+
"fillColor",
|
|
7846
|
+
"strokeWidth",
|
|
7847
|
+
"opacity",
|
|
7848
|
+
"fontSize",
|
|
7849
|
+
"strokeStyle"
|
|
7850
|
+
];
|
|
7769
7851
|
function sharedValue(values) {
|
|
7770
7852
|
const present = values.filter((v) => v !== void 0);
|
|
7771
7853
|
if (present.length === 0) return void 0;
|
|
@@ -7800,6 +7882,42 @@ var SelectionOps = class {
|
|
|
7800
7882
|
if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
|
|
7801
7883
|
return result;
|
|
7802
7884
|
}
|
|
7885
|
+
/**
|
|
7886
|
+
* Unlike `getStyle()` — which returns `{}` for a selection whose elements
|
|
7887
|
+
* have no applicable style fields (e.g. images) — this returns `null` when
|
|
7888
|
+
* no field is applicable, so callers can distinguish "nothing to show" from
|
|
7889
|
+
* "everything shared but empty".
|
|
7890
|
+
*/
|
|
7891
|
+
getStyleDetails() {
|
|
7892
|
+
const ids = this.deps.getSelectedIds();
|
|
7893
|
+
if (ids.length === 0) return null;
|
|
7894
|
+
const styles = [];
|
|
7895
|
+
for (const id of ids) {
|
|
7896
|
+
const el = this.deps.store.getById(id);
|
|
7897
|
+
if (el) styles.push(getElementStyle(el));
|
|
7898
|
+
}
|
|
7899
|
+
if (styles.length === 0) return null;
|
|
7900
|
+
const common = {};
|
|
7901
|
+
const applicable = [];
|
|
7902
|
+
const mixed = [];
|
|
7903
|
+
for (const field of STYLE_FIELDS) {
|
|
7904
|
+
const allValues = styles.map((s) => s[field]);
|
|
7905
|
+
const values = [];
|
|
7906
|
+
for (const v of allValues) {
|
|
7907
|
+
if (v !== void 0) values.push(v);
|
|
7908
|
+
}
|
|
7909
|
+
if (values.length === 0) continue;
|
|
7910
|
+
applicable.push(field);
|
|
7911
|
+
const distinct = new Set(values);
|
|
7912
|
+
if (distinct.size > 1) {
|
|
7913
|
+
mixed.push(field);
|
|
7914
|
+
} else {
|
|
7915
|
+
common[field] = values[0];
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
if (applicable.length === 0) return null;
|
|
7919
|
+
return { common, applicable, mixed };
|
|
7920
|
+
}
|
|
7803
7921
|
applyStyle(style) {
|
|
7804
7922
|
const ids = this.deps.getSelectedIds();
|
|
7805
7923
|
if (ids.length === 0) return;
|
|
@@ -8174,7 +8292,7 @@ var ViewportInteractions = class {
|
|
|
8174
8292
|
var EMPTY_IDS = [];
|
|
8175
8293
|
function noop() {
|
|
8176
8294
|
}
|
|
8177
|
-
var Viewport = class {
|
|
8295
|
+
var Viewport = class _Viewport {
|
|
8178
8296
|
constructor(container, options = {}) {
|
|
8179
8297
|
this.container = container;
|
|
8180
8298
|
this.camera = new Camera(options.camera);
|
|
@@ -8183,6 +8301,13 @@ var Viewport = class {
|
|
|
8183
8301
|
this.store = new ElementStore();
|
|
8184
8302
|
this.layerManager = new LayerManager(this.store);
|
|
8185
8303
|
this.toolManager = new ToolManager();
|
|
8304
|
+
this.unsubToolRegister = this.toolManager.onRegister((tool) => {
|
|
8305
|
+
if (_Viewport.isSelectionSource(tool)) this.attachSelectionSource(tool);
|
|
8306
|
+
});
|
|
8307
|
+
const existingSelect = this.getSelectTool();
|
|
8308
|
+
if (existingSelect && _Viewport.isSelectionSource(existingSelect)) {
|
|
8309
|
+
this.attachSelectionSource(existingSelect);
|
|
8310
|
+
}
|
|
8186
8311
|
this.renderer = new ElementRenderer();
|
|
8187
8312
|
this.renderer.setStore(this.store);
|
|
8188
8313
|
this.renderer.setCamera(this.camera);
|
|
@@ -8217,6 +8342,11 @@ var Viewport = class {
|
|
|
8217
8342
|
this.dropHandler = options.onDrop;
|
|
8218
8343
|
this.history = new HistoryStack();
|
|
8219
8344
|
this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
|
|
8345
|
+
this.unsubRecorderEnd = this.historyRecorder.onTransactionEnd(() => {
|
|
8346
|
+
if (!this.pendingSelectionPrune) return;
|
|
8347
|
+
this.pendingSelectionPrune = false;
|
|
8348
|
+
this.pruneSelection();
|
|
8349
|
+
});
|
|
8220
8350
|
this.selectionOps = new SelectionOps({
|
|
8221
8351
|
store: this.store,
|
|
8222
8352
|
recorder: this.historyRecorder,
|
|
@@ -8336,6 +8466,7 @@ var Viewport = class {
|
|
|
8336
8466
|
this.domNodeManager.removeDomNode(el.id);
|
|
8337
8467
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
8338
8468
|
this.requestRender();
|
|
8469
|
+
this.handleRemovedElement(el.id);
|
|
8339
8470
|
}),
|
|
8340
8471
|
this.store.on("update", ({ previous, current }) => {
|
|
8341
8472
|
if (current.type === "grid") this.gridController.syncContext();
|
|
@@ -8350,6 +8481,7 @@ var Viewport = class {
|
|
|
8350
8481
|
this.renderLoop.markAllLayersDirty();
|
|
8351
8482
|
this.gridController.syncContext();
|
|
8352
8483
|
this.requestRender();
|
|
8484
|
+
this.pruneSelection();
|
|
8353
8485
|
})
|
|
8354
8486
|
];
|
|
8355
8487
|
this.layerManager.on("change", () => {
|
|
@@ -8419,6 +8551,13 @@ var Viewport = class {
|
|
|
8419
8551
|
minimap = null;
|
|
8420
8552
|
htmlRenderers = /* @__PURE__ */ new Map();
|
|
8421
8553
|
resizeListeners = /* @__PURE__ */ new Set();
|
|
8554
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
8555
|
+
detachSelectionSource = null;
|
|
8556
|
+
unsubToolRegister = () => {
|
|
8557
|
+
};
|
|
8558
|
+
pendingSelectionPrune = false;
|
|
8559
|
+
unsubRecorderEnd = () => {
|
|
8560
|
+
};
|
|
8422
8561
|
get ctx() {
|
|
8423
8562
|
return this.canvasEl.getContext("2d");
|
|
8424
8563
|
}
|
|
@@ -8677,6 +8816,42 @@ var Viewport = class {
|
|
|
8677
8816
|
getSelectTool() {
|
|
8678
8817
|
return this.toolManager.getTool("select");
|
|
8679
8818
|
}
|
|
8819
|
+
pruneSelection() {
|
|
8820
|
+
const tool = this.getSelectTool();
|
|
8821
|
+
if (!tool) return;
|
|
8822
|
+
const ids = tool.selectedIds;
|
|
8823
|
+
const filtered = ids.filter((id) => this.store.getById(id) !== void 0);
|
|
8824
|
+
if (filtered.length !== ids.length) tool.setSelection(filtered);
|
|
8825
|
+
}
|
|
8826
|
+
handleRemovedElement(id) {
|
|
8827
|
+
if (!this.getSelectedIds().includes(id)) return;
|
|
8828
|
+
if (this.historyRecorder.currentTransactionId !== null) {
|
|
8829
|
+
this.pendingSelectionPrune = true;
|
|
8830
|
+
return;
|
|
8831
|
+
}
|
|
8832
|
+
this.pruneSelection();
|
|
8833
|
+
}
|
|
8834
|
+
static isSelectionSource(tool) {
|
|
8835
|
+
const candidate = tool;
|
|
8836
|
+
return tool.name === "select" && typeof candidate.onSelectionChange === "function" && typeof candidate.setSelection === "function";
|
|
8837
|
+
}
|
|
8838
|
+
emitSelectionChange() {
|
|
8839
|
+
for (const listener of this.selectionListeners) {
|
|
8840
|
+
try {
|
|
8841
|
+
listener();
|
|
8842
|
+
} catch {
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
attachSelectionSource(tool) {
|
|
8847
|
+
this.detachSelectionSource?.();
|
|
8848
|
+
this.detachSelectionSource = tool.onSelectionChange(() => this.emitSelectionChange());
|
|
8849
|
+
}
|
|
8850
|
+
/**
|
|
8851
|
+
* getSelectedIds() and the onSelectionChange emitter never surface stale ids:
|
|
8852
|
+
* once the enclosing history transaction completes, both reflect
|
|
8853
|
+
* the current selection.
|
|
8854
|
+
*/
|
|
8680
8855
|
getSelectedIds() {
|
|
8681
8856
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
8682
8857
|
}
|
|
@@ -8710,13 +8885,29 @@ var Viewport = class {
|
|
|
8710
8885
|
if (items.length === 0) return;
|
|
8711
8886
|
this.contextMenu.open(items, screenPos);
|
|
8712
8887
|
}
|
|
8888
|
+
/**
|
|
8889
|
+
* Persistent, viewport-owned selection-change emitter. Subscribing works
|
|
8890
|
+
* regardless of whether a select tool is registered yet; it forwards
|
|
8891
|
+
* events from whichever select tool is currently attached via
|
|
8892
|
+
* `toolManager.onRegister`. Never delivers stale ids once the enclosing
|
|
8893
|
+
* history transaction completes.
|
|
8894
|
+
*/
|
|
8713
8895
|
onSelectionChange(listener) {
|
|
8714
|
-
|
|
8715
|
-
return
|
|
8896
|
+
this.selectionListeners.add(listener);
|
|
8897
|
+
return () => {
|
|
8898
|
+
this.selectionListeners.delete(listener);
|
|
8899
|
+
};
|
|
8716
8900
|
}
|
|
8717
8901
|
getSelectionStyle() {
|
|
8718
8902
|
return this.selectionOps.getStyle();
|
|
8719
8903
|
}
|
|
8904
|
+
/**
|
|
8905
|
+
* Unlike `getSelectionStyle()` — which returns `{}` for a style-less
|
|
8906
|
+
* selection — this returns `null` when no style field is applicable.
|
|
8907
|
+
*/
|
|
8908
|
+
getSelectionStyleDetails() {
|
|
8909
|
+
return this.selectionOps.getStyleDetails();
|
|
8910
|
+
}
|
|
8720
8911
|
applyStyleToSelection(style) {
|
|
8721
8912
|
this.selectionOps.applyStyle(style);
|
|
8722
8913
|
}
|
|
@@ -8765,6 +8956,11 @@ var Viewport = class {
|
|
|
8765
8956
|
this.inputHandler.destroy();
|
|
8766
8957
|
this.unsubCamera();
|
|
8767
8958
|
this.unsubToolChange();
|
|
8959
|
+
this.unsubToolRegister();
|
|
8960
|
+
this.unsubRecorderEnd();
|
|
8961
|
+
this.detachSelectionSource?.();
|
|
8962
|
+
this.detachSelectionSource = null;
|
|
8963
|
+
this.selectionListeners.clear();
|
|
8768
8964
|
this.unsubStore.forEach((fn) => fn());
|
|
8769
8965
|
this.resizeObserver?.disconnect();
|
|
8770
8966
|
this.resizeObserver = null;
|
|
@@ -12366,7 +12562,7 @@ var PingTool = class {
|
|
|
12366
12562
|
};
|
|
12367
12563
|
|
|
12368
12564
|
// src/index.ts
|
|
12369
|
-
var VERSION = "0.
|
|
12565
|
+
var VERSION = "0.60.0";
|
|
12370
12566
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12371
12567
|
0 && (module.exports = {
|
|
12372
12568
|
ArrowTool,
|