@fieldnotes/core 0.14.0 → 0.15.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
@@ -1084,6 +1084,14 @@ var InputHandler = class {
1084
1084
  e.preventDefault();
1085
1085
  this.handlePaste();
1086
1086
  }
1087
+ if (e.key === "]") {
1088
+ e.preventDefault();
1089
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
1090
+ }
1091
+ if (e.key === "[") {
1092
+ e.preventDefault();
1093
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
1094
+ }
1087
1095
  };
1088
1096
  onKeyUp = (e) => {
1089
1097
  if (e.key === " ") {
@@ -1260,6 +1268,33 @@ var InputHandler = class {
1260
1268
  selectTool.setSelection(newIds);
1261
1269
  this.toolContext.requestRender();
1262
1270
  }
1271
+ handleZOrder(operation) {
1272
+ if (!this.toolManager || !this.toolContext) return;
1273
+ const tool = this.toolManager.activeTool;
1274
+ if (tool?.name !== "select") return;
1275
+ const selectTool = tool;
1276
+ const ids = selectTool.selectedIds;
1277
+ if (ids.length === 0) return;
1278
+ this.historyRecorder?.begin();
1279
+ for (const id of ids) {
1280
+ switch (operation) {
1281
+ case "forward":
1282
+ this.toolContext.store.bringForward(id);
1283
+ break;
1284
+ case "backward":
1285
+ this.toolContext.store.sendBackward(id);
1286
+ break;
1287
+ case "front":
1288
+ this.toolContext.store.bringToFront(id);
1289
+ break;
1290
+ case "back":
1291
+ this.toolContext.store.sendToBack(id);
1292
+ break;
1293
+ }
1294
+ }
1295
+ this.historyRecorder?.commit();
1296
+ this.toolContext.requestRender();
1297
+ }
1263
1298
  cancelToolIfActive(e) {
1264
1299
  if (this.isToolActive) {
1265
1300
  this.dispatchToolUp(e);
@@ -1556,9 +1591,13 @@ var ElementStore = class {
1556
1591
  layerOrderMap = /* @__PURE__ */ new Map();
1557
1592
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1558
1593
  sortedCache = null;
1594
+ _versions = /* @__PURE__ */ new Map();
1559
1595
  get count() {
1560
1596
  return this.elements.size;
1561
1597
  }
1598
+ getVersion(id) {
1599
+ return this._versions.get(id) ?? -1;
1600
+ }
1562
1601
  setLayerOrder(order) {
1563
1602
  this.layerOrderMap = new Map(order);
1564
1603
  this.sortedCache = null;
@@ -1583,6 +1622,7 @@ var ElementStore = class {
1583
1622
  }
1584
1623
  add(element) {
1585
1624
  this.sortedCache = null;
1625
+ this._versions.set(element.id, 0);
1586
1626
  this.elements.set(element.id, element);
1587
1627
  const bounds = getElementBounds(element);
1588
1628
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1592,6 +1632,7 @@ var ElementStore = class {
1592
1632
  const existing = this.elements.get(id);
1593
1633
  if (!existing) return;
1594
1634
  this.sortedCache = null;
1635
+ this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
1595
1636
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1596
1637
  if (updated.type === "arrow") {
1597
1638
  const arrow = updated;
@@ -1611,12 +1652,14 @@ var ElementStore = class {
1611
1652
  const element = this.elements.get(id);
1612
1653
  if (!element) return;
1613
1654
  this.sortedCache = null;
1655
+ this._versions.delete(id);
1614
1656
  this.elements.delete(id);
1615
1657
  this.spatialIndex.remove(id);
1616
1658
  this.bus.emit("remove", element);
1617
1659
  }
1618
1660
  clear() {
1619
1661
  this.sortedCache = null;
1662
+ this._versions.clear();
1620
1663
  this.elements.clear();
1621
1664
  this.spatialIndex.clear();
1622
1665
  this.bus.emit("clear", null);
@@ -1626,10 +1669,12 @@ var ElementStore = class {
1626
1669
  }
1627
1670
  loadSnapshot(elements) {
1628
1671
  this.sortedCache = null;
1672
+ this._versions.clear();
1629
1673
  this.elements.clear();
1630
1674
  this.spatialIndex.clear();
1631
1675
  for (const el of elements) {
1632
1676
  this.elements.set(el.id, el);
1677
+ this._versions.set(el.id, 0);
1633
1678
  const bounds = getElementBounds(el);
1634
1679
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1635
1680
  }
@@ -1638,6 +1683,54 @@ var ElementStore = class {
1638
1683
  this.bus.emit("add", el);
1639
1684
  }
1640
1685
  }
1686
+ bringToFront(id) {
1687
+ const el = this.elements.get(id);
1688
+ if (!el) return;
1689
+ const siblings = [...this.elements.values()].filter(
1690
+ (e) => e.layerId === el.layerId && e.id !== id
1691
+ );
1692
+ if (siblings.length === 0) return;
1693
+ const maxZ = Math.max(...siblings.map((e) => e.zIndex));
1694
+ if (el.zIndex >= maxZ) return;
1695
+ this.update(id, { zIndex: maxZ + 1 });
1696
+ }
1697
+ sendToBack(id) {
1698
+ const el = this.elements.get(id);
1699
+ if (!el) return;
1700
+ const siblings = [...this.elements.values()].filter(
1701
+ (e) => e.layerId === el.layerId && e.id !== id
1702
+ );
1703
+ if (siblings.length === 0) return;
1704
+ const minZ = Math.min(...siblings.map((e) => e.zIndex));
1705
+ if (el.zIndex <= minZ) return;
1706
+ this.update(id, { zIndex: minZ - 1 });
1707
+ }
1708
+ bringForward(id) {
1709
+ const el = this.elements.get(id);
1710
+ if (!el) return;
1711
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1712
+ const idx = sorted.findIndex((e) => e.id === id);
1713
+ if (idx < 0 || idx >= sorted.length - 1) return;
1714
+ const next = sorted[idx + 1];
1715
+ if (!next) return;
1716
+ const myZ = el.zIndex;
1717
+ const nextZ = next.zIndex;
1718
+ this.update(id, { zIndex: nextZ });
1719
+ this.update(next.id, { zIndex: myZ });
1720
+ }
1721
+ sendBackward(id) {
1722
+ const el = this.elements.get(id);
1723
+ if (!el) return;
1724
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1725
+ const idx = sorted.findIndex((e) => e.id === id);
1726
+ if (idx <= 0) return;
1727
+ const prev = sorted[idx - 1];
1728
+ if (!prev) return;
1729
+ const myZ = el.zIndex;
1730
+ const prevZ = prev.zIndex;
1731
+ this.update(id, { zIndex: prevZ });
1732
+ this.update(prev.id, { zIndex: myZ });
1733
+ }
1641
1734
  queryRect(rect) {
1642
1735
  const ids = this.spatialIndex.query(rect);
1643
1736
  const elements = [];
@@ -3900,10 +3993,14 @@ var DomNodeManager = class {
3900
3993
  domLayer;
3901
3994
  onEditRequest;
3902
3995
  isEditingElement;
3996
+ getVersion;
3997
+ lastSyncedVersion = /* @__PURE__ */ new Map();
3998
+ lastSyncedZIndex = /* @__PURE__ */ new Map();
3903
3999
  constructor(deps) {
3904
4000
  this.domLayer = deps.domLayer;
3905
4001
  this.onEditRequest = deps.onEditRequest;
3906
4002
  this.isEditingElement = deps.isEditingElement;
4003
+ this.getVersion = deps.getVersion ?? null;
3907
4004
  }
3908
4005
  getNode(id) {
3909
4006
  return this.domNodes.get(id);
@@ -3922,6 +4019,17 @@ var DomNodeManager = class {
3922
4019
  });
3923
4020
  this.domLayer.appendChild(node);
3924
4021
  this.domNodes.set(element.id, node);
4022
+ } else if (this.getVersion) {
4023
+ const currentVersion = this.getVersion(element.id);
4024
+ const lastVersion = this.lastSyncedVersion.get(element.id);
4025
+ const lastZ = this.lastSyncedZIndex.get(element.id);
4026
+ if (lastVersion === currentVersion && lastZ === zIndex) {
4027
+ return;
4028
+ }
4029
+ }
4030
+ if (this.getVersion) {
4031
+ this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
4032
+ this.lastSyncedZIndex.set(element.id, zIndex);
3925
4033
  }
3926
4034
  const size = "size" in element ? element.size : null;
3927
4035
  Object.assign(node.style, {
@@ -3940,6 +4048,8 @@ var DomNodeManager = class {
3940
4048
  }
3941
4049
  removeDomNode(id) {
3942
4050
  this.htmlContent.delete(id);
4051
+ this.lastSyncedVersion.delete(id);
4052
+ this.lastSyncedZIndex.delete(id);
3943
4053
  const node = this.domNodes.get(id);
3944
4054
  if (node) {
3945
4055
  node.remove();
@@ -3950,6 +4060,8 @@ var DomNodeManager = class {
3950
4060
  this.domNodes.forEach((node) => node.remove());
3951
4061
  this.domNodes.clear();
3952
4062
  this.htmlContent.clear();
4063
+ this.lastSyncedVersion.clear();
4064
+ this.lastSyncedZIndex.clear();
3953
4065
  }
3954
4066
  reattachHtmlContent(store) {
3955
4067
  for (const el of store.getElementsByType("html")) {
@@ -4469,7 +4581,8 @@ var Viewport = class {
4469
4581
  this.domNodeManager = new DomNodeManager({
4470
4582
  domLayer: this.domLayer,
4471
4583
  onEditRequest: (id) => this.startEditingElement(id),
4472
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
4584
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
4585
+ getVersion: (id) => this.store.getVersion(id)
4473
4586
  });
4474
4587
  this.interactMode = new InteractMode({
4475
4588
  getNode: (id) => this.domNodeManager.getNode(id)
@@ -5292,6 +5405,7 @@ var SelectTool = class {
5292
5405
  ctx = null;
5293
5406
  pendingSingleSelectId = null;
5294
5407
  hasDragged = false;
5408
+ resizeAspectRatio = 0;
5295
5409
  get selectedIds() {
5296
5410
  return [...this._selectedIds];
5297
5411
  }
@@ -5337,7 +5451,8 @@ var SelectTool = class {
5337
5451
  const resizeHit = this.hitTestResizeHandle(world, ctx);
5338
5452
  if (resizeHit) {
5339
5453
  const el = ctx.store.getById(resizeHit.elementId);
5340
- if (el) {
5454
+ if (el && "size" in el) {
5455
+ this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
5341
5456
  this.mode = {
5342
5457
  type: "resizing",
5343
5458
  elementId: resizeHit.elementId,
@@ -5389,7 +5504,7 @@ var SelectTool = class {
5389
5504
  }
5390
5505
  if (this.mode.type === "resizing") {
5391
5506
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
5392
- this.handleResize(world, ctx);
5507
+ this.handleResize(world, ctx, state.shiftKey);
5393
5508
  return;
5394
5509
  }
5395
5510
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
@@ -5513,7 +5628,7 @@ var SelectTool = class {
5513
5628
  const hit = this.hitTest(world, ctx);
5514
5629
  ctx.setCursor?.(hit ? "move" : "default");
5515
5630
  }
5516
- handleResize(world, ctx) {
5631
+ handleResize(world, ctx, shiftKey = false) {
5517
5632
  if (this.mode.type !== "resizing") return;
5518
5633
  const el = ctx.store.getById(this.mode.elementId);
5519
5634
  if (!el || !("size" in el) || el.locked) return;
@@ -5544,6 +5659,21 @@ var SelectTool = class {
5544
5659
  h -= dy;
5545
5660
  break;
5546
5661
  }
5662
+ if (shiftKey && this.resizeAspectRatio > 0) {
5663
+ const absDw = Math.abs(w - el.size.w);
5664
+ const absDh = Math.abs(h - el.size.h);
5665
+ if (absDw >= absDh) {
5666
+ h = w / this.resizeAspectRatio;
5667
+ } else {
5668
+ w = h * this.resizeAspectRatio;
5669
+ }
5670
+ if (handle === "nw" || handle === "sw") {
5671
+ x = el.position.x + el.size.w - w;
5672
+ }
5673
+ if (handle === "nw" || handle === "ne") {
5674
+ y = el.position.y + el.size.h - h;
5675
+ }
5676
+ }
5547
5677
  if (w < MIN_ELEMENT_SIZE) {
5548
5678
  if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
5549
5679
  w = MIN_ELEMENT_SIZE;
@@ -6647,7 +6777,7 @@ var UpdateLayerCommand = class {
6647
6777
  };
6648
6778
 
6649
6779
  // src/index.ts
6650
- var VERSION = "0.14.0";
6780
+ var VERSION = "0.15.0";
6651
6781
  // Annotate the CommonJS export names for ESM import in node:
6652
6782
  0 && (module.exports = {
6653
6783
  AddElementCommand,
package/dist/index.d.cts CHANGED
@@ -204,7 +204,9 @@ declare class ElementStore {
204
204
  private layerOrderMap;
205
205
  private spatialIndex;
206
206
  private sortedCache;
207
+ private _versions;
207
208
  get count(): number;
209
+ getVersion(id: string): number;
208
210
  setLayerOrder(order: Map<string, number>): void;
209
211
  getAll(): CanvasElement[];
210
212
  getById(id: string): CanvasElement | undefined;
@@ -217,6 +219,10 @@ declare class ElementStore {
217
219
  clear(): void;
218
220
  snapshot(): CanvasElement[];
219
221
  loadSnapshot(elements: CanvasElement[]): void;
222
+ bringToFront(id: string): void;
223
+ sendToBack(id: string): void;
224
+ bringForward(id: string): void;
225
+ sendBackward(id: string): void;
220
226
  queryRect(rect: Bounds): CanvasElement[];
221
227
  queryPoint(point: Point): CanvasElement[];
222
228
  on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
@@ -462,6 +468,7 @@ declare class InputHandler {
462
468
  private handleRedo;
463
469
  private handleCopy;
464
470
  private handlePaste;
471
+ private handleZOrder;
465
472
  private cancelToolIfActive;
466
473
  }
467
474
 
@@ -935,6 +942,7 @@ declare class SelectTool implements Tool {
935
942
  private ctx;
936
943
  private pendingSingleSelectId;
937
944
  private hasDragged;
945
+ private resizeAspectRatio;
938
946
  get selectedIds(): string[];
939
947
  setSelection(ids: string[]): void;
940
948
  get isMarqueeActive(): boolean;
@@ -1181,6 +1189,6 @@ declare class UpdateLayerCommand implements Command {
1181
1189
  undo(_store: ElementStore): void;
1182
1190
  }
1183
1191
 
1184
- declare const VERSION = "0.14.0";
1192
+ declare const VERSION = "0.15.0";
1185
1193
 
1186
1194
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.d.ts CHANGED
@@ -204,7 +204,9 @@ declare class ElementStore {
204
204
  private layerOrderMap;
205
205
  private spatialIndex;
206
206
  private sortedCache;
207
+ private _versions;
207
208
  get count(): number;
209
+ getVersion(id: string): number;
208
210
  setLayerOrder(order: Map<string, number>): void;
209
211
  getAll(): CanvasElement[];
210
212
  getById(id: string): CanvasElement | undefined;
@@ -217,6 +219,10 @@ declare class ElementStore {
217
219
  clear(): void;
218
220
  snapshot(): CanvasElement[];
219
221
  loadSnapshot(elements: CanvasElement[]): void;
222
+ bringToFront(id: string): void;
223
+ sendToBack(id: string): void;
224
+ bringForward(id: string): void;
225
+ sendBackward(id: string): void;
220
226
  queryRect(rect: Bounds): CanvasElement[];
221
227
  queryPoint(point: Point): CanvasElement[];
222
228
  on<K extends keyof ElementStoreEvents>(event: K, listener: (data: ElementStoreEvents[K]) => void): () => void;
@@ -462,6 +468,7 @@ declare class InputHandler {
462
468
  private handleRedo;
463
469
  private handleCopy;
464
470
  private handlePaste;
471
+ private handleZOrder;
465
472
  private cancelToolIfActive;
466
473
  }
467
474
 
@@ -935,6 +942,7 @@ declare class SelectTool implements Tool {
935
942
  private ctx;
936
943
  private pendingSingleSelectId;
937
944
  private hasDragged;
945
+ private resizeAspectRatio;
938
946
  get selectedIds(): string[];
939
947
  setSelection(ids: string[]): void;
940
948
  get isMarqueeActive(): boolean;
@@ -1181,6 +1189,6 @@ declare class UpdateLayerCommand implements Command {
1181
1189
  undo(_store: ElementStore): void;
1182
1190
  }
1183
1191
 
1184
- declare const VERSION = "0.14.0";
1192
+ declare const VERSION = "0.15.0";
1185
1193
 
1186
1194
  export { type ActiveFormats, AddElementCommand, type ArrowElement, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, Background, type BackgroundOptions, type BackgroundPattern, BatchCommand, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, CreateLayerCommand, DEFAULT_FONT_SIZE_PRESETS, DEFAULT_NOTE_FONT_SIZE, DoubleTapDetector, type DoubleTapDetectorOptions, ElementRenderer, ElementStore, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, EventBus, type ExportImageOptions, type FilterAction, type FilteredEvent, type FilteredUpEvent, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryRecorder, HistoryStack, type HistoryStackOptions, type HtmlElement, type ImageElement, ImageTool, type ImageToolOptions, InputFilter, InputHandler, type Layer, LayerManager, MeasureTool, type MeasureToolOptions, type Measurement, NoteEditor, type NoteEditorOptions, type NoteElement, NoteTool, type NoteToolOptions, NoteToolbar, PencilTool, type PencilToolOptions, type Point, type PointerState, Quadtree, RemoveElementCommand, RemoveLayerCommand, type RenderStatsSnapshot, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type Size, type StrokeElement, type StrokePoint, type StyledRun, type TemplateElement, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, UpdateElementCommand, UpdateLayerCommand, VERSION, Viewport, type ViewportOptions, boundsIntersect, clearStaleBindings, createArrow, createGrid, createHtmlElement, createId, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportState, findBindTarget, findBoundArrows, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getEdgeIntersection, getElementBounds, getElementCenter, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInSquare, getHexDistance, isBindable, isNearBezier, parseState, sanitizeNoteHtml, setFontSize, smartSnap, snapPoint, snapToHexCenter, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline, unbindArrow, updateBoundArrow };
package/dist/index.js CHANGED
@@ -975,6 +975,14 @@ var InputHandler = class {
975
975
  e.preventDefault();
976
976
  this.handlePaste();
977
977
  }
978
+ if (e.key === "]") {
979
+ e.preventDefault();
980
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
981
+ }
982
+ if (e.key === "[") {
983
+ e.preventDefault();
984
+ this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
985
+ }
978
986
  };
979
987
  onKeyUp = (e) => {
980
988
  if (e.key === " ") {
@@ -1151,6 +1159,33 @@ var InputHandler = class {
1151
1159
  selectTool.setSelection(newIds);
1152
1160
  this.toolContext.requestRender();
1153
1161
  }
1162
+ handleZOrder(operation) {
1163
+ if (!this.toolManager || !this.toolContext) return;
1164
+ const tool = this.toolManager.activeTool;
1165
+ if (tool?.name !== "select") return;
1166
+ const selectTool = tool;
1167
+ const ids = selectTool.selectedIds;
1168
+ if (ids.length === 0) return;
1169
+ this.historyRecorder?.begin();
1170
+ for (const id of ids) {
1171
+ switch (operation) {
1172
+ case "forward":
1173
+ this.toolContext.store.bringForward(id);
1174
+ break;
1175
+ case "backward":
1176
+ this.toolContext.store.sendBackward(id);
1177
+ break;
1178
+ case "front":
1179
+ this.toolContext.store.bringToFront(id);
1180
+ break;
1181
+ case "back":
1182
+ this.toolContext.store.sendToBack(id);
1183
+ break;
1184
+ }
1185
+ }
1186
+ this.historyRecorder?.commit();
1187
+ this.toolContext.requestRender();
1188
+ }
1154
1189
  cancelToolIfActive(e) {
1155
1190
  if (this.isToolActive) {
1156
1191
  this.dispatchToolUp(e);
@@ -1447,9 +1482,13 @@ var ElementStore = class {
1447
1482
  layerOrderMap = /* @__PURE__ */ new Map();
1448
1483
  spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1449
1484
  sortedCache = null;
1485
+ _versions = /* @__PURE__ */ new Map();
1450
1486
  get count() {
1451
1487
  return this.elements.size;
1452
1488
  }
1489
+ getVersion(id) {
1490
+ return this._versions.get(id) ?? -1;
1491
+ }
1453
1492
  setLayerOrder(order) {
1454
1493
  this.layerOrderMap = new Map(order);
1455
1494
  this.sortedCache = null;
@@ -1474,6 +1513,7 @@ var ElementStore = class {
1474
1513
  }
1475
1514
  add(element) {
1476
1515
  this.sortedCache = null;
1516
+ this._versions.set(element.id, 0);
1477
1517
  this.elements.set(element.id, element);
1478
1518
  const bounds = getElementBounds(element);
1479
1519
  if (bounds) this.spatialIndex.insert(element.id, bounds);
@@ -1483,6 +1523,7 @@ var ElementStore = class {
1483
1523
  const existing = this.elements.get(id);
1484
1524
  if (!existing) return;
1485
1525
  this.sortedCache = null;
1526
+ this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
1486
1527
  const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1487
1528
  if (updated.type === "arrow") {
1488
1529
  const arrow = updated;
@@ -1502,12 +1543,14 @@ var ElementStore = class {
1502
1543
  const element = this.elements.get(id);
1503
1544
  if (!element) return;
1504
1545
  this.sortedCache = null;
1546
+ this._versions.delete(id);
1505
1547
  this.elements.delete(id);
1506
1548
  this.spatialIndex.remove(id);
1507
1549
  this.bus.emit("remove", element);
1508
1550
  }
1509
1551
  clear() {
1510
1552
  this.sortedCache = null;
1553
+ this._versions.clear();
1511
1554
  this.elements.clear();
1512
1555
  this.spatialIndex.clear();
1513
1556
  this.bus.emit("clear", null);
@@ -1517,10 +1560,12 @@ var ElementStore = class {
1517
1560
  }
1518
1561
  loadSnapshot(elements) {
1519
1562
  this.sortedCache = null;
1563
+ this._versions.clear();
1520
1564
  this.elements.clear();
1521
1565
  this.spatialIndex.clear();
1522
1566
  for (const el of elements) {
1523
1567
  this.elements.set(el.id, el);
1568
+ this._versions.set(el.id, 0);
1524
1569
  const bounds = getElementBounds(el);
1525
1570
  if (bounds) this.spatialIndex.insert(el.id, bounds);
1526
1571
  }
@@ -1529,6 +1574,54 @@ var ElementStore = class {
1529
1574
  this.bus.emit("add", el);
1530
1575
  }
1531
1576
  }
1577
+ bringToFront(id) {
1578
+ const el = this.elements.get(id);
1579
+ if (!el) return;
1580
+ const siblings = [...this.elements.values()].filter(
1581
+ (e) => e.layerId === el.layerId && e.id !== id
1582
+ );
1583
+ if (siblings.length === 0) return;
1584
+ const maxZ = Math.max(...siblings.map((e) => e.zIndex));
1585
+ if (el.zIndex >= maxZ) return;
1586
+ this.update(id, { zIndex: maxZ + 1 });
1587
+ }
1588
+ sendToBack(id) {
1589
+ const el = this.elements.get(id);
1590
+ if (!el) return;
1591
+ const siblings = [...this.elements.values()].filter(
1592
+ (e) => e.layerId === el.layerId && e.id !== id
1593
+ );
1594
+ if (siblings.length === 0) return;
1595
+ const minZ = Math.min(...siblings.map((e) => e.zIndex));
1596
+ if (el.zIndex <= minZ) return;
1597
+ this.update(id, { zIndex: minZ - 1 });
1598
+ }
1599
+ bringForward(id) {
1600
+ const el = this.elements.get(id);
1601
+ if (!el) return;
1602
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1603
+ const idx = sorted.findIndex((e) => e.id === id);
1604
+ if (idx < 0 || idx >= sorted.length - 1) return;
1605
+ const next = sorted[idx + 1];
1606
+ if (!next) return;
1607
+ const myZ = el.zIndex;
1608
+ const nextZ = next.zIndex;
1609
+ this.update(id, { zIndex: nextZ });
1610
+ this.update(next.id, { zIndex: myZ });
1611
+ }
1612
+ sendBackward(id) {
1613
+ const el = this.elements.get(id);
1614
+ if (!el) return;
1615
+ const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
1616
+ const idx = sorted.findIndex((e) => e.id === id);
1617
+ if (idx <= 0) return;
1618
+ const prev = sorted[idx - 1];
1619
+ if (!prev) return;
1620
+ const myZ = el.zIndex;
1621
+ const prevZ = prev.zIndex;
1622
+ this.update(id, { zIndex: prevZ });
1623
+ this.update(prev.id, { zIndex: myZ });
1624
+ }
1532
1625
  queryRect(rect) {
1533
1626
  const ids = this.spatialIndex.query(rect);
1534
1627
  const elements = [];
@@ -3791,10 +3884,14 @@ var DomNodeManager = class {
3791
3884
  domLayer;
3792
3885
  onEditRequest;
3793
3886
  isEditingElement;
3887
+ getVersion;
3888
+ lastSyncedVersion = /* @__PURE__ */ new Map();
3889
+ lastSyncedZIndex = /* @__PURE__ */ new Map();
3794
3890
  constructor(deps) {
3795
3891
  this.domLayer = deps.domLayer;
3796
3892
  this.onEditRequest = deps.onEditRequest;
3797
3893
  this.isEditingElement = deps.isEditingElement;
3894
+ this.getVersion = deps.getVersion ?? null;
3798
3895
  }
3799
3896
  getNode(id) {
3800
3897
  return this.domNodes.get(id);
@@ -3813,6 +3910,17 @@ var DomNodeManager = class {
3813
3910
  });
3814
3911
  this.domLayer.appendChild(node);
3815
3912
  this.domNodes.set(element.id, node);
3913
+ } else if (this.getVersion) {
3914
+ const currentVersion = this.getVersion(element.id);
3915
+ const lastVersion = this.lastSyncedVersion.get(element.id);
3916
+ const lastZ = this.lastSyncedZIndex.get(element.id);
3917
+ if (lastVersion === currentVersion && lastZ === zIndex) {
3918
+ return;
3919
+ }
3920
+ }
3921
+ if (this.getVersion) {
3922
+ this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
3923
+ this.lastSyncedZIndex.set(element.id, zIndex);
3816
3924
  }
3817
3925
  const size = "size" in element ? element.size : null;
3818
3926
  Object.assign(node.style, {
@@ -3831,6 +3939,8 @@ var DomNodeManager = class {
3831
3939
  }
3832
3940
  removeDomNode(id) {
3833
3941
  this.htmlContent.delete(id);
3942
+ this.lastSyncedVersion.delete(id);
3943
+ this.lastSyncedZIndex.delete(id);
3834
3944
  const node = this.domNodes.get(id);
3835
3945
  if (node) {
3836
3946
  node.remove();
@@ -3841,6 +3951,8 @@ var DomNodeManager = class {
3841
3951
  this.domNodes.forEach((node) => node.remove());
3842
3952
  this.domNodes.clear();
3843
3953
  this.htmlContent.clear();
3954
+ this.lastSyncedVersion.clear();
3955
+ this.lastSyncedZIndex.clear();
3844
3956
  }
3845
3957
  reattachHtmlContent(store) {
3846
3958
  for (const el of store.getElementsByType("html")) {
@@ -4360,7 +4472,8 @@ var Viewport = class {
4360
4472
  this.domNodeManager = new DomNodeManager({
4361
4473
  domLayer: this.domLayer,
4362
4474
  onEditRequest: (id) => this.startEditingElement(id),
4363
- isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
4475
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
4476
+ getVersion: (id) => this.store.getVersion(id)
4364
4477
  });
4365
4478
  this.interactMode = new InteractMode({
4366
4479
  getNode: (id) => this.domNodeManager.getNode(id)
@@ -5183,6 +5296,7 @@ var SelectTool = class {
5183
5296
  ctx = null;
5184
5297
  pendingSingleSelectId = null;
5185
5298
  hasDragged = false;
5299
+ resizeAspectRatio = 0;
5186
5300
  get selectedIds() {
5187
5301
  return [...this._selectedIds];
5188
5302
  }
@@ -5228,7 +5342,8 @@ var SelectTool = class {
5228
5342
  const resizeHit = this.hitTestResizeHandle(world, ctx);
5229
5343
  if (resizeHit) {
5230
5344
  const el = ctx.store.getById(resizeHit.elementId);
5231
- if (el) {
5345
+ if (el && "size" in el) {
5346
+ this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
5232
5347
  this.mode = {
5233
5348
  type: "resizing",
5234
5349
  elementId: resizeHit.elementId,
@@ -5280,7 +5395,7 @@ var SelectTool = class {
5280
5395
  }
5281
5396
  if (this.mode.type === "resizing") {
5282
5397
  ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
5283
- this.handleResize(world, ctx);
5398
+ this.handleResize(world, ctx, state.shiftKey);
5284
5399
  return;
5285
5400
  }
5286
5401
  if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
@@ -5404,7 +5519,7 @@ var SelectTool = class {
5404
5519
  const hit = this.hitTest(world, ctx);
5405
5520
  ctx.setCursor?.(hit ? "move" : "default");
5406
5521
  }
5407
- handleResize(world, ctx) {
5522
+ handleResize(world, ctx, shiftKey = false) {
5408
5523
  if (this.mode.type !== "resizing") return;
5409
5524
  const el = ctx.store.getById(this.mode.elementId);
5410
5525
  if (!el || !("size" in el) || el.locked) return;
@@ -5435,6 +5550,21 @@ var SelectTool = class {
5435
5550
  h -= dy;
5436
5551
  break;
5437
5552
  }
5553
+ if (shiftKey && this.resizeAspectRatio > 0) {
5554
+ const absDw = Math.abs(w - el.size.w);
5555
+ const absDh = Math.abs(h - el.size.h);
5556
+ if (absDw >= absDh) {
5557
+ h = w / this.resizeAspectRatio;
5558
+ } else {
5559
+ w = h * this.resizeAspectRatio;
5560
+ }
5561
+ if (handle === "nw" || handle === "sw") {
5562
+ x = el.position.x + el.size.w - w;
5563
+ }
5564
+ if (handle === "nw" || handle === "ne") {
5565
+ y = el.position.y + el.size.h - h;
5566
+ }
5567
+ }
5438
5568
  if (w < MIN_ELEMENT_SIZE) {
5439
5569
  if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
5440
5570
  w = MIN_ELEMENT_SIZE;
@@ -6538,7 +6668,7 @@ var UpdateLayerCommand = class {
6538
6668
  };
6539
6669
 
6540
6670
  // src/index.ts
6541
- var VERSION = "0.14.0";
6671
+ var VERSION = "0.15.0";
6542
6672
  export {
6543
6673
  AddElementCommand,
6544
6674
  ArrowTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Vanilla TypeScript infinite canvas engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",