@fieldnotes/core 0.59.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 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 === 0) return;
5523
- const first = all[0];
5524
- this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
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());
@@ -7817,6 +7840,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
7817
7840
  }
7818
7841
 
7819
7842
  // src/canvas/selection-ops.ts
7843
+ var STYLE_FIELDS = [
7844
+ "color",
7845
+ "fillColor",
7846
+ "strokeWidth",
7847
+ "opacity",
7848
+ "fontSize",
7849
+ "strokeStyle"
7850
+ ];
7820
7851
  function sharedValue(values) {
7821
7852
  const present = values.filter((v) => v !== void 0);
7822
7853
  if (present.length === 0) return void 0;
@@ -7851,6 +7882,42 @@ var SelectionOps = class {
7851
7882
  if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
7852
7883
  return result;
7853
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
+ }
7854
7921
  applyStyle(style) {
7855
7922
  const ids = this.deps.getSelectedIds();
7856
7923
  if (ids.length === 0) return;
@@ -8225,7 +8292,7 @@ var ViewportInteractions = class {
8225
8292
  var EMPTY_IDS = [];
8226
8293
  function noop() {
8227
8294
  }
8228
- var Viewport = class {
8295
+ var Viewport = class _Viewport {
8229
8296
  constructor(container, options = {}) {
8230
8297
  this.container = container;
8231
8298
  this.camera = new Camera(options.camera);
@@ -8234,6 +8301,13 @@ var Viewport = class {
8234
8301
  this.store = new ElementStore();
8235
8302
  this.layerManager = new LayerManager(this.store);
8236
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
+ }
8237
8311
  this.renderer = new ElementRenderer();
8238
8312
  this.renderer.setStore(this.store);
8239
8313
  this.renderer.setCamera(this.camera);
@@ -8268,6 +8342,11 @@ var Viewport = class {
8268
8342
  this.dropHandler = options.onDrop;
8269
8343
  this.history = new HistoryStack();
8270
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
+ });
8271
8350
  this.selectionOps = new SelectionOps({
8272
8351
  store: this.store,
8273
8352
  recorder: this.historyRecorder,
@@ -8387,6 +8466,7 @@ var Viewport = class {
8387
8466
  this.domNodeManager.removeDomNode(el.id);
8388
8467
  this.renderLoop.markLayerDirty(el.layerId);
8389
8468
  this.requestRender();
8469
+ this.handleRemovedElement(el.id);
8390
8470
  }),
8391
8471
  this.store.on("update", ({ previous, current }) => {
8392
8472
  if (current.type === "grid") this.gridController.syncContext();
@@ -8401,6 +8481,7 @@ var Viewport = class {
8401
8481
  this.renderLoop.markAllLayersDirty();
8402
8482
  this.gridController.syncContext();
8403
8483
  this.requestRender();
8484
+ this.pruneSelection();
8404
8485
  })
8405
8486
  ];
8406
8487
  this.layerManager.on("change", () => {
@@ -8470,6 +8551,13 @@ var Viewport = class {
8470
8551
  minimap = null;
8471
8552
  htmlRenderers = /* @__PURE__ */ new Map();
8472
8553
  resizeListeners = /* @__PURE__ */ new Set();
8554
+ selectionListeners = /* @__PURE__ */ new Set();
8555
+ detachSelectionSource = null;
8556
+ unsubToolRegister = () => {
8557
+ };
8558
+ pendingSelectionPrune = false;
8559
+ unsubRecorderEnd = () => {
8560
+ };
8473
8561
  get ctx() {
8474
8562
  return this.canvasEl.getContext("2d");
8475
8563
  }
@@ -8728,6 +8816,42 @@ var Viewport = class {
8728
8816
  getSelectTool() {
8729
8817
  return this.toolManager.getTool("select");
8730
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
+ */
8731
8855
  getSelectedIds() {
8732
8856
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
8733
8857
  }
@@ -8761,13 +8885,29 @@ var Viewport = class {
8761
8885
  if (items.length === 0) return;
8762
8886
  this.contextMenu.open(items, screenPos);
8763
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
+ */
8764
8895
  onSelectionChange(listener) {
8765
- const tool = this.getSelectTool();
8766
- return tool ? tool.onSelectionChange(listener) : noop;
8896
+ this.selectionListeners.add(listener);
8897
+ return () => {
8898
+ this.selectionListeners.delete(listener);
8899
+ };
8767
8900
  }
8768
8901
  getSelectionStyle() {
8769
8902
  return this.selectionOps.getStyle();
8770
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
+ }
8771
8911
  applyStyleToSelection(style) {
8772
8912
  this.selectionOps.applyStyle(style);
8773
8913
  }
@@ -8816,6 +8956,11 @@ var Viewport = class {
8816
8956
  this.inputHandler.destroy();
8817
8957
  this.unsubCamera();
8818
8958
  this.unsubToolChange();
8959
+ this.unsubToolRegister();
8960
+ this.unsubRecorderEnd();
8961
+ this.detachSelectionSource?.();
8962
+ this.detachSelectionSource = null;
8963
+ this.selectionListeners.clear();
8819
8964
  this.unsubStore.forEach((fn) => fn());
8820
8965
  this.resizeObserver?.disconnect();
8821
8966
  this.resizeObserver = null;
@@ -12417,7 +12562,7 @@ var PingTool = class {
12417
12562
  };
12418
12563
 
12419
12564
  // src/index.ts
12420
- var VERSION = "0.59.0";
12565
+ var VERSION = "0.60.0";
12421
12566
  // Annotate the CommonJS export names for ESM import in node:
12422
12567
  0 && (module.exports = {
12423
12568
  ArrowTool,