@fieldnotes/core 0.31.0 → 0.32.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/README.md +17 -1
- package/dist/index.cjs +133 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +133 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -439,7 +439,7 @@ new Viewport(container, {
|
|
|
439
439
|
|
|
440
440
|
```typescript
|
|
441
441
|
new PencilTool({ color: '#ff0000', width: 3, smoothing: 1.5 });
|
|
442
|
-
new EraserTool({ radius: 30 }); // mode: 'partial' (default) splits strokes at the erased span; mode: 'stroke' deletes the whole stroke
|
|
442
|
+
new EraserTool({ radius: 30 }); // radius is screen pixels (converted to world units per zoom); mode: 'partial' (default) splits strokes at the erased span; mode: 'stroke' deletes the whole stroke
|
|
443
443
|
new ArrowTool({ color: '#333', width: 2 });
|
|
444
444
|
```
|
|
445
445
|
|
|
@@ -563,6 +563,22 @@ const unsub = viewport.onSelectionChange((ids) => {
|
|
|
563
563
|
// call unsub() to unsubscribe
|
|
564
564
|
```
|
|
565
565
|
|
|
566
|
+
## Aligning the Selection
|
|
567
|
+
|
|
568
|
+
Two methods on `Viewport` let you snap or space selected elements in one undo step.
|
|
569
|
+
|
|
570
|
+
- **`viewport.alignSelection(edge)`** — `edge`: `AlignEdge` = `'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom'`; aligns every selected element to the corresponding edge or center of the selection's bounding box. Needs 2+ selected elements. Locked elements anchor the bounding box without moving.
|
|
571
|
+
- **`viewport.distributeSelection(axis)`** — `axis`: `DistributeAxis` = `'horizontal' | 'vertical'`; evenly spaces selected elements' centers along the axis. Needs 3+ selected elements. Locked elements anchor the span without moving.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
viewport.alignSelection('left'); // flush left edges
|
|
575
|
+
viewport.alignSelection('center-x'); // center on vertical axis
|
|
576
|
+
viewport.alignSelection('middle'); // center on horizontal axis
|
|
577
|
+
viewport.distributeSelection('horizontal'); // equal horizontal spacing
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Grids are ignored by both operations.
|
|
581
|
+
|
|
566
582
|
## Built-in Interactions
|
|
567
583
|
|
|
568
584
|
| Input | Action |
|
package/dist/index.cjs
CHANGED
|
@@ -2351,6 +2351,23 @@ function distToBounds(point, bounds) {
|
|
|
2351
2351
|
function findBoundArrows(elementId, store) {
|
|
2352
2352
|
return store.getElementsByType("arrow").filter((a) => a.fromBinding?.elementId === elementId || a.toBinding?.elementId === elementId);
|
|
2353
2353
|
}
|
|
2354
|
+
function updateArrowsBoundToElements(movedIds, store) {
|
|
2355
|
+
const movedNonArrowIds = /* @__PURE__ */ new Set();
|
|
2356
|
+
for (const id of movedIds) {
|
|
2357
|
+
const el = store.getById(id);
|
|
2358
|
+
if (el && el.type !== "arrow") movedNonArrowIds.add(id);
|
|
2359
|
+
}
|
|
2360
|
+
if (movedNonArrowIds.size === 0) return;
|
|
2361
|
+
const updated = /* @__PURE__ */ new Set();
|
|
2362
|
+
for (const id of movedNonArrowIds) {
|
|
2363
|
+
for (const ba of findBoundArrows(id, store)) {
|
|
2364
|
+
if (updated.has(ba.id)) continue;
|
|
2365
|
+
updated.add(ba.id);
|
|
2366
|
+
const updates = updateBoundArrow(ba, store);
|
|
2367
|
+
if (updates) store.update(ba.id, updates);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2354
2371
|
function updateBoundArrow(arrow, store) {
|
|
2355
2372
|
if (!arrow.fromBinding && !arrow.toBinding) return null;
|
|
2356
2373
|
const updates = {};
|
|
@@ -3778,6 +3795,19 @@ var NoteEditor = class {
|
|
|
3778
3795
|
}
|
|
3779
3796
|
};
|
|
3780
3797
|
|
|
3798
|
+
// src/elements/translate.ts
|
|
3799
|
+
function translateElementPatch(el, dx, dy) {
|
|
3800
|
+
const position = { x: el.position.x + dx, y: el.position.y + dy };
|
|
3801
|
+
if (el.type === "arrow") {
|
|
3802
|
+
return {
|
|
3803
|
+
position,
|
|
3804
|
+
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
3805
|
+
to: { x: el.to.x + dx, y: el.to.y + dy }
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
return { position };
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3781
3811
|
// src/elements/arrow-label-editor.ts
|
|
3782
3812
|
var ArrowLabelEditor = class {
|
|
3783
3813
|
input = null;
|
|
@@ -5464,6 +5494,16 @@ function getElementStyle(element) {
|
|
|
5464
5494
|
}
|
|
5465
5495
|
|
|
5466
5496
|
// src/canvas/viewport.ts
|
|
5497
|
+
function unionBounds(list) {
|
|
5498
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
5499
|
+
for (const b of list) {
|
|
5500
|
+
minX = Math.min(minX, b.x);
|
|
5501
|
+
minY = Math.min(minY, b.y);
|
|
5502
|
+
maxX = Math.max(maxX, b.x + b.w);
|
|
5503
|
+
maxY = Math.max(maxY, b.y + b.h);
|
|
5504
|
+
}
|
|
5505
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
5506
|
+
}
|
|
5467
5507
|
var EMPTY_IDS = [];
|
|
5468
5508
|
var ARROW_HIT_THRESHOLD = 10;
|
|
5469
5509
|
function noop() {
|
|
@@ -5873,6 +5913,86 @@ var Viewport = class {
|
|
|
5873
5913
|
}
|
|
5874
5914
|
this.historyRecorder.commit();
|
|
5875
5915
|
}
|
|
5916
|
+
alignSelection(edge) {
|
|
5917
|
+
const bounded = this.boundedSelection();
|
|
5918
|
+
if (bounded.length < 2) return;
|
|
5919
|
+
const B = unionBounds(bounded.map((e) => e.bounds));
|
|
5920
|
+
this.historyRecorder.begin();
|
|
5921
|
+
const moved = [];
|
|
5922
|
+
for (const { id, el, bounds: b } of bounded) {
|
|
5923
|
+
if (!this.isMovable(el)) continue;
|
|
5924
|
+
let dx = 0;
|
|
5925
|
+
let dy = 0;
|
|
5926
|
+
switch (edge) {
|
|
5927
|
+
case "left":
|
|
5928
|
+
dx = B.x - b.x;
|
|
5929
|
+
break;
|
|
5930
|
+
case "right":
|
|
5931
|
+
dx = B.x + B.w - (b.x + b.w);
|
|
5932
|
+
break;
|
|
5933
|
+
case "center-x":
|
|
5934
|
+
dx = B.x + B.w / 2 - (b.x + b.w / 2);
|
|
5935
|
+
break;
|
|
5936
|
+
case "top":
|
|
5937
|
+
dy = B.y - b.y;
|
|
5938
|
+
break;
|
|
5939
|
+
case "bottom":
|
|
5940
|
+
dy = B.y + B.h - (b.y + b.h);
|
|
5941
|
+
break;
|
|
5942
|
+
case "middle":
|
|
5943
|
+
dy = B.y + B.h / 2 - (b.y + b.h / 2);
|
|
5944
|
+
break;
|
|
5945
|
+
}
|
|
5946
|
+
if (dx === 0 && dy === 0) continue;
|
|
5947
|
+
this.store.update(id, translateElementPatch(el, dx, dy));
|
|
5948
|
+
moved.push(id);
|
|
5949
|
+
}
|
|
5950
|
+
updateArrowsBoundToElements(moved, this.store);
|
|
5951
|
+
this.historyRecorder.commit();
|
|
5952
|
+
this.requestRender();
|
|
5953
|
+
}
|
|
5954
|
+
distributeSelection(axis) {
|
|
5955
|
+
const bounded = this.boundedSelection();
|
|
5956
|
+
if (bounded.length < 3) return;
|
|
5957
|
+
const center = (b) => axis === "horizontal" ? b.x + b.w / 2 : b.y + b.h / 2;
|
|
5958
|
+
const sorted = [...bounded].sort((p, q) => center(p.bounds) - center(q.bounds));
|
|
5959
|
+
const first = sorted[0];
|
|
5960
|
+
const last = sorted[sorted.length - 1];
|
|
5961
|
+
if (!first || !last) return;
|
|
5962
|
+
const c0 = center(first.bounds);
|
|
5963
|
+
const cN = center(last.bounds);
|
|
5964
|
+
const n = sorted.length;
|
|
5965
|
+
this.historyRecorder.begin();
|
|
5966
|
+
const moved = [];
|
|
5967
|
+
for (let i = 1; i < n - 1; i++) {
|
|
5968
|
+
const item = sorted[i];
|
|
5969
|
+
if (!item || !this.isMovable(item.el)) continue;
|
|
5970
|
+
const target = c0 + i * (cN - c0) / (n - 1);
|
|
5971
|
+
const delta = target - center(item.bounds);
|
|
5972
|
+
if (delta === 0) continue;
|
|
5973
|
+
const [dx, dy] = axis === "horizontal" ? [delta, 0] : [0, delta];
|
|
5974
|
+
this.store.update(item.id, translateElementPatch(item.el, dx, dy));
|
|
5975
|
+
moved.push(item.id);
|
|
5976
|
+
}
|
|
5977
|
+
updateArrowsBoundToElements(moved, this.store);
|
|
5978
|
+
this.historyRecorder.commit();
|
|
5979
|
+
this.requestRender();
|
|
5980
|
+
}
|
|
5981
|
+
boundedSelection() {
|
|
5982
|
+
const out = [];
|
|
5983
|
+
for (const id of this.getSelectedIds()) {
|
|
5984
|
+
const el = this.store.getById(id);
|
|
5985
|
+
if (!el) continue;
|
|
5986
|
+
const bounds = getElementBounds(el);
|
|
5987
|
+
if (bounds) out.push({ id, el, bounds });
|
|
5988
|
+
}
|
|
5989
|
+
return out;
|
|
5990
|
+
}
|
|
5991
|
+
isMovable(el) {
|
|
5992
|
+
if (el.locked) return false;
|
|
5993
|
+
if (el.type === "arrow" && (el.fromBinding ?? el.toBinding)) return false;
|
|
5994
|
+
return true;
|
|
5995
|
+
}
|
|
5876
5996
|
getRenderStats() {
|
|
5877
5997
|
return this.renderLoop.getStats();
|
|
5878
5998
|
}
|
|
@@ -6457,11 +6577,12 @@ var EraserTool = class {
|
|
|
6457
6577
|
}
|
|
6458
6578
|
eraseAt(state, ctx) {
|
|
6459
6579
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
6580
|
+
const worldRadius = this.radius / ctx.camera.zoom;
|
|
6460
6581
|
const queryBounds = {
|
|
6461
|
-
x: world.x -
|
|
6462
|
-
y: world.y -
|
|
6463
|
-
w:
|
|
6464
|
-
h:
|
|
6582
|
+
x: world.x - worldRadius,
|
|
6583
|
+
y: world.y - worldRadius,
|
|
6584
|
+
w: worldRadius * 2,
|
|
6585
|
+
h: worldRadius * 2
|
|
6465
6586
|
};
|
|
6466
6587
|
const candidates = ctx.store.queryRect(queryBounds);
|
|
6467
6588
|
let erased = false;
|
|
@@ -6469,14 +6590,14 @@ var EraserTool = class {
|
|
|
6469
6590
|
if (el.type !== "stroke") continue;
|
|
6470
6591
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
6471
6592
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
6472
|
-
if (!this.strokeIntersects(el, world)) continue;
|
|
6593
|
+
if (!this.strokeIntersects(el, world, worldRadius)) continue;
|
|
6473
6594
|
if (this.mode === "stroke") {
|
|
6474
6595
|
ctx.store.remove(el.id);
|
|
6475
6596
|
erased = true;
|
|
6476
6597
|
continue;
|
|
6477
6598
|
}
|
|
6478
6599
|
const localEraser = { x: world.x - el.position.x, y: world.y - el.position.y };
|
|
6479
|
-
const runs = erasePoints(el.points, localEraser,
|
|
6600
|
+
const runs = erasePoints(el.points, localEraser, worldRadius);
|
|
6480
6601
|
if (runs === null) continue;
|
|
6481
6602
|
ctx.store.remove(el.id);
|
|
6482
6603
|
for (const run of runs) {
|
|
@@ -6496,8 +6617,8 @@ var EraserTool = class {
|
|
|
6496
6617
|
}
|
|
6497
6618
|
if (erased) ctx.requestRender();
|
|
6498
6619
|
}
|
|
6499
|
-
strokeIntersects(stroke, point) {
|
|
6500
|
-
return hitTestStroke(stroke, point,
|
|
6620
|
+
strokeIntersects(stroke, point, worldRadius) {
|
|
6621
|
+
return hitTestStroke(stroke, point, worldRadius);
|
|
6501
6622
|
}
|
|
6502
6623
|
};
|
|
6503
6624
|
|
|
@@ -6879,40 +7000,15 @@ var SelectTool = class {
|
|
|
6879
7000
|
}
|
|
6880
7001
|
}
|
|
6881
7002
|
updateArrowsBoundTo(ids, ctx) {
|
|
6882
|
-
|
|
6883
|
-
for (const id of ids) {
|
|
6884
|
-
const el = ctx.store.getById(id);
|
|
6885
|
-
if (el && el.type !== "arrow") movedNonArrowIds.add(id);
|
|
6886
|
-
}
|
|
6887
|
-
if (movedNonArrowIds.size === 0) return;
|
|
6888
|
-
const updatedArrows = /* @__PURE__ */ new Set();
|
|
6889
|
-
for (const id of movedNonArrowIds) {
|
|
6890
|
-
const boundArrows = findBoundArrows(id, ctx.store);
|
|
6891
|
-
for (const ba of boundArrows) {
|
|
6892
|
-
if (updatedArrows.has(ba.id)) continue;
|
|
6893
|
-
updatedArrows.add(ba.id);
|
|
6894
|
-
const updates = updateBoundArrow(ba, ctx.store);
|
|
6895
|
-
if (updates) ctx.store.update(ba.id, updates);
|
|
6896
|
-
}
|
|
6897
|
-
}
|
|
7003
|
+
updateArrowsBoundToElements(ids, ctx.store);
|
|
6898
7004
|
}
|
|
6899
7005
|
nudgeSelection(dx, dy, ctx) {
|
|
6900
7006
|
let moved = false;
|
|
6901
7007
|
for (const id of this._selectedIds) {
|
|
6902
7008
|
const el = ctx.store.getById(id);
|
|
6903
7009
|
if (!el || el.locked) continue;
|
|
6904
|
-
if (el.type === "arrow")
|
|
6905
|
-
|
|
6906
|
-
ctx.store.update(id, {
|
|
6907
|
-
position: { x: el.position.x + dx, y: el.position.y + dy },
|
|
6908
|
-
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
6909
|
-
to: { x: el.to.x + dx, y: el.to.y + dy }
|
|
6910
|
-
});
|
|
6911
|
-
} else {
|
|
6912
|
-
ctx.store.update(id, {
|
|
6913
|
-
position: { x: el.position.x + dx, y: el.position.y + dy }
|
|
6914
|
-
});
|
|
6915
|
-
}
|
|
7010
|
+
if (el.type === "arrow" && (el.fromBinding || el.toBinding)) continue;
|
|
7011
|
+
ctx.store.update(id, translateElementPatch(el, dx, dy));
|
|
6916
7012
|
moved = true;
|
|
6917
7013
|
}
|
|
6918
7014
|
if (moved) {
|
|
@@ -8104,7 +8200,7 @@ var TemplateTool = class {
|
|
|
8104
8200
|
};
|
|
8105
8201
|
|
|
8106
8202
|
// src/index.ts
|
|
8107
|
-
var VERSION = "0.
|
|
8203
|
+
var VERSION = "0.32.0";
|
|
8108
8204
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8109
8205
|
0 && (module.exports = {
|
|
8110
8206
|
ArrowTool,
|