@fieldnotes/core 0.27.0 → 0.29.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 +9 -1
- package/dist/index.cjs +242 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +242 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -439,8 +439,16 @@ new Viewport(container, {
|
|
|
439
439
|
|
|
440
440
|
```typescript
|
|
441
441
|
new PencilTool({ color: '#ff0000', width: 3, smoothing: 1.5 });
|
|
442
|
-
new EraserTool({ radius: 30 });
|
|
442
|
+
new EraserTool({ radius: 30 }); // mode: 'partial' (default) splits strokes at the erased span; mode: 'stroke' deletes the whole stroke
|
|
443
443
|
new ArrowTool({ color: '#333', width: 2 });
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Arrow Labels
|
|
447
|
+
|
|
448
|
+
Arrows support an optional `label` string, rendered as a pill at the curve midpoint. Pass it at creation or double-click an arrow on the canvas to add or edit the label inline.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
createArrow({ from: { x: 0, y: 0 }, to: { x: 200, y: 0 }, label: 'depends on' });
|
|
444
452
|
new NoteTool({ backgroundColor: '#fff9c4', size: { w: 200, h: 150 } });
|
|
445
453
|
new ImageTool({ size: { w: 400, h: 300 } });
|
|
446
454
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -2739,6 +2739,7 @@ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
|
2739
2739
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
2740
2740
|
var ARROWHEAD_LENGTH = 12;
|
|
2741
2741
|
var ARROWHEAD_ANGLE = Math.PI / 6;
|
|
2742
|
+
var ARROW_LABEL_FONT_SIZE = 14;
|
|
2742
2743
|
var ElementRenderer = class {
|
|
2743
2744
|
store = null;
|
|
2744
2745
|
imageCache = /* @__PURE__ */ new Map();
|
|
@@ -2749,6 +2750,7 @@ var ElementRenderer = class {
|
|
|
2749
2750
|
hexTileCache = null;
|
|
2750
2751
|
hexTileCacheKey = "";
|
|
2751
2752
|
gridBoundsOverride = null;
|
|
2753
|
+
labelEditingId = null;
|
|
2752
2754
|
setStore(store) {
|
|
2753
2755
|
this.store = store;
|
|
2754
2756
|
}
|
|
@@ -2767,6 +2769,9 @@ var ElementRenderer = class {
|
|
|
2767
2769
|
setGridBoundsOverride(bounds) {
|
|
2768
2770
|
this.gridBoundsOverride = bounds;
|
|
2769
2771
|
}
|
|
2772
|
+
setLabelEditingId(id) {
|
|
2773
|
+
this.labelEditingId = id;
|
|
2774
|
+
}
|
|
2770
2775
|
isDomElement(element) {
|
|
2771
2776
|
return DOM_ELEMENT_TYPES.has(element.type);
|
|
2772
2777
|
}
|
|
@@ -2843,6 +2848,28 @@ var ElementRenderer = class {
|
|
|
2843
2848
|
ctx.stroke();
|
|
2844
2849
|
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2845
2850
|
ctx.restore();
|
|
2851
|
+
this.renderArrowLabel(ctx, arrow);
|
|
2852
|
+
}
|
|
2853
|
+
renderArrowLabel(ctx, arrow) {
|
|
2854
|
+
if (!arrow.label || arrow.label.length === 0) return;
|
|
2855
|
+
if (arrow.id === this.labelEditingId) return;
|
|
2856
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
2857
|
+
ctx.save();
|
|
2858
|
+
ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
|
|
2859
|
+
const metrics = ctx.measureText(arrow.label);
|
|
2860
|
+
const padX = 6;
|
|
2861
|
+
const padY = 4;
|
|
2862
|
+
const w = metrics.width + padX * 2;
|
|
2863
|
+
const h = ARROW_LABEL_FONT_SIZE + padY * 2;
|
|
2864
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
|
|
2865
|
+
ctx.beginPath();
|
|
2866
|
+
ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
|
|
2867
|
+
ctx.fill();
|
|
2868
|
+
ctx.fillStyle = "#1a1a1a";
|
|
2869
|
+
ctx.textAlign = "center";
|
|
2870
|
+
ctx.textBaseline = "middle";
|
|
2871
|
+
ctx.fillText(arrow.label, mid.x, mid.y);
|
|
2872
|
+
ctx.restore();
|
|
2846
2873
|
}
|
|
2847
2874
|
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2848
2875
|
ctx.beginPath();
|
|
@@ -3253,6 +3280,7 @@ function createArrow(input) {
|
|
|
3253
3280
|
};
|
|
3254
3281
|
if (input.fromBinding) result.fromBinding = input.fromBinding;
|
|
3255
3282
|
if (input.toBinding) result.toBinding = input.toBinding;
|
|
3283
|
+
if (input.label !== void 0) result.label = input.label;
|
|
3256
3284
|
return result;
|
|
3257
3285
|
}
|
|
3258
3286
|
function createImage(input) {
|
|
@@ -3716,6 +3744,84 @@ var NoteEditor = class {
|
|
|
3716
3744
|
}
|
|
3717
3745
|
};
|
|
3718
3746
|
|
|
3747
|
+
// src/elements/arrow-label-editor.ts
|
|
3748
|
+
var ArrowLabelEditor = class {
|
|
3749
|
+
input = null;
|
|
3750
|
+
done = false;
|
|
3751
|
+
get isEditing() {
|
|
3752
|
+
return this.input !== null;
|
|
3753
|
+
}
|
|
3754
|
+
startEditing(start) {
|
|
3755
|
+
if (this.input) this.cleanup();
|
|
3756
|
+
const { arrow, layer, store, recorder, onDone } = start;
|
|
3757
|
+
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
3758
|
+
const input = document.createElement("input");
|
|
3759
|
+
input.type = "text";
|
|
3760
|
+
input.value = arrow.label ?? "";
|
|
3761
|
+
Object.assign(input.style, {
|
|
3762
|
+
position: "absolute",
|
|
3763
|
+
left: `${mid.x}px`,
|
|
3764
|
+
top: `${mid.y}px`,
|
|
3765
|
+
transform: "translate(-50%, -50%)",
|
|
3766
|
+
// domLayer is pointer-events:none; the input must opt back in to receive taps/clicks.
|
|
3767
|
+
pointerEvents: "auto",
|
|
3768
|
+
font: "14px system-ui, sans-serif",
|
|
3769
|
+
padding: "2px 6px",
|
|
3770
|
+
border: "1px solid #2196F3",
|
|
3771
|
+
borderRadius: "4px",
|
|
3772
|
+
background: "#ffffff",
|
|
3773
|
+
color: "#1a1a1a",
|
|
3774
|
+
outline: "none",
|
|
3775
|
+
minWidth: "40px"
|
|
3776
|
+
});
|
|
3777
|
+
this.done = false;
|
|
3778
|
+
const commit = () => {
|
|
3779
|
+
if (this.done) return;
|
|
3780
|
+
this.done = true;
|
|
3781
|
+
const next = input.value.trim() || void 0;
|
|
3782
|
+
if (next !== arrow.label) {
|
|
3783
|
+
recorder.begin();
|
|
3784
|
+
store.update(arrow.id, { label: next });
|
|
3785
|
+
recorder.commit();
|
|
3786
|
+
}
|
|
3787
|
+
this.cleanup();
|
|
3788
|
+
onDone();
|
|
3789
|
+
};
|
|
3790
|
+
const cancel = () => {
|
|
3791
|
+
if (this.done) return;
|
|
3792
|
+
this.done = true;
|
|
3793
|
+
this.cleanup();
|
|
3794
|
+
onDone();
|
|
3795
|
+
};
|
|
3796
|
+
input.addEventListener("keydown", (e) => {
|
|
3797
|
+
if (e.key === "Enter") {
|
|
3798
|
+
e.preventDefault();
|
|
3799
|
+
commit();
|
|
3800
|
+
} else if (e.key === "Escape") {
|
|
3801
|
+
e.preventDefault();
|
|
3802
|
+
cancel();
|
|
3803
|
+
}
|
|
3804
|
+
e.stopPropagation();
|
|
3805
|
+
});
|
|
3806
|
+
input.addEventListener("blur", commit);
|
|
3807
|
+
layer.appendChild(input);
|
|
3808
|
+
this.input = input;
|
|
3809
|
+
input.focus();
|
|
3810
|
+
input.select();
|
|
3811
|
+
}
|
|
3812
|
+
/** Abort any in-progress edit without committing (e.g. on viewport teardown). */
|
|
3813
|
+
cancel() {
|
|
3814
|
+
this.done = true;
|
|
3815
|
+
this.cleanup();
|
|
3816
|
+
}
|
|
3817
|
+
cleanup() {
|
|
3818
|
+
if (this.input) {
|
|
3819
|
+
this.input.remove();
|
|
3820
|
+
this.input = null;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
};
|
|
3824
|
+
|
|
3719
3825
|
// src/tools/tool-manager.ts
|
|
3720
3826
|
var ToolManager = class {
|
|
3721
3827
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -5325,6 +5431,7 @@ function getElementStyle(element) {
|
|
|
5325
5431
|
|
|
5326
5432
|
// src/canvas/viewport.ts
|
|
5327
5433
|
var EMPTY_IDS = [];
|
|
5434
|
+
var ARROW_HIT_THRESHOLD = 10;
|
|
5328
5435
|
function noop() {
|
|
5329
5436
|
}
|
|
5330
5437
|
function sharedValue(values) {
|
|
@@ -5366,6 +5473,7 @@ var Viewport = class {
|
|
|
5366
5473
|
placeholder: options.placeholder
|
|
5367
5474
|
});
|
|
5368
5475
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
5476
|
+
this.arrowLabelEditor = new ArrowLabelEditor();
|
|
5369
5477
|
this.noteEditor.setHistoryHooks(
|
|
5370
5478
|
() => this.historyRecorder.begin(),
|
|
5371
5479
|
() => this.historyRecorder.commit()
|
|
@@ -5492,6 +5600,7 @@ var Viewport = class {
|
|
|
5492
5600
|
background;
|
|
5493
5601
|
renderer;
|
|
5494
5602
|
noteEditor;
|
|
5603
|
+
arrowLabelEditor;
|
|
5495
5604
|
historyRecorder;
|
|
5496
5605
|
toolContext;
|
|
5497
5606
|
marginViewport;
|
|
@@ -5746,6 +5855,7 @@ var Viewport = class {
|
|
|
5746
5855
|
this.renderLoop.stop();
|
|
5747
5856
|
this.interactMode.destroy();
|
|
5748
5857
|
this.noteEditor.destroy(this.store);
|
|
5858
|
+
this.arrowLabelEditor.cancel();
|
|
5749
5859
|
this.historyRecorder.destroy();
|
|
5750
5860
|
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
5751
5861
|
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
@@ -5831,8 +5941,35 @@ var Viewport = class {
|
|
|
5831
5941
|
const hit = this.hitTestWorld(world);
|
|
5832
5942
|
if (hit?.type === "html") {
|
|
5833
5943
|
this.interactMode.startInteracting(hit.id);
|
|
5944
|
+
return;
|
|
5945
|
+
}
|
|
5946
|
+
const arrow = this.findArrowAt(world);
|
|
5947
|
+
if (arrow) {
|
|
5948
|
+
this.startArrowLabelEdit(arrow);
|
|
5834
5949
|
}
|
|
5835
5950
|
};
|
|
5951
|
+
findArrowAt(world) {
|
|
5952
|
+
const candidates = this.store.queryPoint(world).reverse();
|
|
5953
|
+
for (const el of candidates) {
|
|
5954
|
+
if (el.type === "arrow" && isNearBezier(world, el.from, el.to, el.bend, ARROW_HIT_THRESHOLD)) {
|
|
5955
|
+
return el;
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
return void 0;
|
|
5959
|
+
}
|
|
5960
|
+
startArrowLabelEdit(arrow) {
|
|
5961
|
+
this.arrowLabelEditor.startEditing({
|
|
5962
|
+
arrow,
|
|
5963
|
+
layer: this.domLayer,
|
|
5964
|
+
store: this.store,
|
|
5965
|
+
recorder: this.historyRecorder,
|
|
5966
|
+
onDone: () => {
|
|
5967
|
+
this.renderer.setLabelEditingId(null);
|
|
5968
|
+
this.requestRender();
|
|
5969
|
+
}
|
|
5970
|
+
});
|
|
5971
|
+
this.renderer.setLabelEditingId(arrow.id);
|
|
5972
|
+
}
|
|
5836
5973
|
hitTestWorld(world) {
|
|
5837
5974
|
const candidates = this.store.queryPoint(world).reverse();
|
|
5838
5975
|
for (const el of candidates) {
|
|
@@ -6154,6 +6291,79 @@ function hitTestStroke(stroke, point, radius) {
|
|
|
6154
6291
|
return false;
|
|
6155
6292
|
}
|
|
6156
6293
|
|
|
6294
|
+
// src/elements/stroke-erase.ts
|
|
6295
|
+
function lerp(a, b, t) {
|
|
6296
|
+
return {
|
|
6297
|
+
x: a.x + (b.x - a.x) * t,
|
|
6298
|
+
y: a.y + (b.y - a.y) * t,
|
|
6299
|
+
pressure: a.pressure + (b.pressure - a.pressure) * t
|
|
6300
|
+
};
|
|
6301
|
+
}
|
|
6302
|
+
function erasePoints(points, eraser, radius) {
|
|
6303
|
+
const r2 = radius * radius;
|
|
6304
|
+
if (points.length < 2) {
|
|
6305
|
+
const p = points[0];
|
|
6306
|
+
if (p && (p.x - eraser.x) ** 2 + (p.y - eraser.y) ** 2 <= r2) return [];
|
|
6307
|
+
return null;
|
|
6308
|
+
}
|
|
6309
|
+
const runs = [];
|
|
6310
|
+
let current = [];
|
|
6311
|
+
let erased = false;
|
|
6312
|
+
const flush = () => {
|
|
6313
|
+
if (current.length >= 2) runs.push(current);
|
|
6314
|
+
current = [];
|
|
6315
|
+
};
|
|
6316
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
6317
|
+
const a = points[i];
|
|
6318
|
+
const b = points[i + 1];
|
|
6319
|
+
if (!a || !b) continue;
|
|
6320
|
+
const dx = b.x - a.x;
|
|
6321
|
+
const dy = b.y - a.y;
|
|
6322
|
+
const fx = a.x - eraser.x;
|
|
6323
|
+
const fy = a.y - eraser.y;
|
|
6324
|
+
const A = dx * dx + dy * dy;
|
|
6325
|
+
const B = 2 * (fx * dx + fy * dy);
|
|
6326
|
+
const C = fx * fx + fy * fy - r2;
|
|
6327
|
+
let tLo = 1;
|
|
6328
|
+
let tHi = 0;
|
|
6329
|
+
if (A === 0) {
|
|
6330
|
+
if (C <= 0) {
|
|
6331
|
+
tLo = 0;
|
|
6332
|
+
tHi = 1;
|
|
6333
|
+
}
|
|
6334
|
+
} else {
|
|
6335
|
+
const disc = B * B - 4 * A * C;
|
|
6336
|
+
if (disc >= 0) {
|
|
6337
|
+
const sq = Math.sqrt(disc);
|
|
6338
|
+
const lo = Math.max(0, (-B - sq) / (2 * A));
|
|
6339
|
+
const hi = Math.min(1, (-B + sq) / (2 * A));
|
|
6340
|
+
if (lo < hi) {
|
|
6341
|
+
tLo = lo;
|
|
6342
|
+
tHi = hi;
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
if (tLo > tHi) {
|
|
6347
|
+
if (current.length === 0) current.push(a);
|
|
6348
|
+
current.push(b);
|
|
6349
|
+
continue;
|
|
6350
|
+
}
|
|
6351
|
+
erased = true;
|
|
6352
|
+
if (tLo > 0) {
|
|
6353
|
+
if (current.length === 0) current.push(a);
|
|
6354
|
+
current.push(lerp(a, b, tLo));
|
|
6355
|
+
flush();
|
|
6356
|
+
} else {
|
|
6357
|
+
flush();
|
|
6358
|
+
}
|
|
6359
|
+
if (tHi < 1) {
|
|
6360
|
+
current = [lerp(a, b, tHi), b];
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
flush();
|
|
6364
|
+
return erased ? runs : null;
|
|
6365
|
+
}
|
|
6366
|
+
|
|
6157
6367
|
// src/tools/eraser-tool.ts
|
|
6158
6368
|
var DEFAULT_RADIUS = 20;
|
|
6159
6369
|
function makeEraserCursor(radius) {
|
|
@@ -6166,12 +6376,21 @@ var EraserTool = class {
|
|
|
6166
6376
|
erasing = false;
|
|
6167
6377
|
radius;
|
|
6168
6378
|
cursor;
|
|
6379
|
+
mode;
|
|
6169
6380
|
constructor(options = {}) {
|
|
6170
6381
|
this.radius = options.radius ?? DEFAULT_RADIUS;
|
|
6171
6382
|
this.cursor = makeEraserCursor(this.radius);
|
|
6383
|
+
this.mode = options.mode ?? "partial";
|
|
6172
6384
|
}
|
|
6173
6385
|
getOptions() {
|
|
6174
|
-
return { radius: this.radius };
|
|
6386
|
+
return { radius: this.radius, mode: this.mode };
|
|
6387
|
+
}
|
|
6388
|
+
setOptions(options) {
|
|
6389
|
+
if (options.mode !== void 0) this.mode = options.mode;
|
|
6390
|
+
if (options.radius !== void 0) {
|
|
6391
|
+
this.radius = options.radius;
|
|
6392
|
+
this.cursor = makeEraserCursor(this.radius);
|
|
6393
|
+
}
|
|
6175
6394
|
}
|
|
6176
6395
|
onActivate(ctx) {
|
|
6177
6396
|
ctx.setCursor?.(this.cursor);
|
|
@@ -6204,10 +6423,30 @@ var EraserTool = class {
|
|
|
6204
6423
|
if (el.type !== "stroke") continue;
|
|
6205
6424
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
6206
6425
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
6207
|
-
if (this.strokeIntersects(el, world))
|
|
6426
|
+
if (!this.strokeIntersects(el, world)) continue;
|
|
6427
|
+
if (this.mode === "stroke") {
|
|
6208
6428
|
ctx.store.remove(el.id);
|
|
6209
6429
|
erased = true;
|
|
6430
|
+
continue;
|
|
6431
|
+
}
|
|
6432
|
+
const localEraser = { x: world.x - el.position.x, y: world.y - el.position.y };
|
|
6433
|
+
const runs = erasePoints(el.points, localEraser, this.radius);
|
|
6434
|
+
if (runs === null) continue;
|
|
6435
|
+
ctx.store.remove(el.id);
|
|
6436
|
+
for (const run of runs) {
|
|
6437
|
+
ctx.store.add(
|
|
6438
|
+
createStroke({
|
|
6439
|
+
points: run,
|
|
6440
|
+
color: el.color,
|
|
6441
|
+
width: el.width,
|
|
6442
|
+
opacity: el.opacity,
|
|
6443
|
+
layerId: el.layerId,
|
|
6444
|
+
zIndex: el.zIndex,
|
|
6445
|
+
position: el.position
|
|
6446
|
+
})
|
|
6447
|
+
);
|
|
6210
6448
|
}
|
|
6449
|
+
erased = true;
|
|
6211
6450
|
}
|
|
6212
6451
|
if (erased) ctx.requestRender();
|
|
6213
6452
|
}
|
|
@@ -7746,7 +7985,7 @@ var TemplateTool = class {
|
|
|
7746
7985
|
};
|
|
7747
7986
|
|
|
7748
7987
|
// src/index.ts
|
|
7749
|
-
var VERSION = "0.
|
|
7988
|
+
var VERSION = "0.29.0";
|
|
7750
7989
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7751
7990
|
0 && (module.exports = {
|
|
7752
7991
|
ArrowTool,
|