@fieldnotes/core 0.53.0 → 0.54.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 +243 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -33
- package/dist/index.d.ts +148 -33
- package/dist/index.js +239 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6835,6 +6835,7 @@ var RenderLoop = class {
|
|
|
6835
6835
|
// set on recenter/viewport-change; consumed by the grid block
|
|
6836
6836
|
stats = new RenderStats();
|
|
6837
6837
|
layerGroups = /* @__PURE__ */ new Map();
|
|
6838
|
+
overlays = /* @__PURE__ */ new Set();
|
|
6838
6839
|
gridCacheCanvas = null;
|
|
6839
6840
|
gridCacheCtx = null;
|
|
6840
6841
|
lastGridRefs = [];
|
|
@@ -6893,6 +6894,29 @@ var RenderLoop = class {
|
|
|
6893
6894
|
getStats() {
|
|
6894
6895
|
return this.stats.getSnapshot();
|
|
6895
6896
|
}
|
|
6897
|
+
/**
|
|
6898
|
+
* Registers a viewport overlay drawn beneath the active tool's own overlay.
|
|
6899
|
+
* Returns an idempotent unsubscribe that also erases the overlay's last
|
|
6900
|
+
* frame.
|
|
6901
|
+
*/
|
|
6902
|
+
registerOverlay(draw) {
|
|
6903
|
+
this.overlays.add(draw);
|
|
6904
|
+
this.requestRender();
|
|
6905
|
+
return () => {
|
|
6906
|
+
if (this.overlays.delete(draw)) this.requestRender();
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6909
|
+
drawRegisteredOverlays(ctx) {
|
|
6910
|
+
for (const draw of this.overlays) {
|
|
6911
|
+
ctx.save();
|
|
6912
|
+
try {
|
|
6913
|
+
draw(ctx);
|
|
6914
|
+
} catch {
|
|
6915
|
+
} finally {
|
|
6916
|
+
ctx.restore();
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6896
6920
|
compositeLayerCache(ctx, layerId, opacity) {
|
|
6897
6921
|
const cached = this.layerCache.getCanvas(layerId);
|
|
6898
6922
|
const offset = this.marginViewport.compositeOffset(
|
|
@@ -7026,7 +7050,8 @@ var RenderLoop = class {
|
|
|
7026
7050
|
}
|
|
7027
7051
|
const activeTool = this.toolManager.activeTool;
|
|
7028
7052
|
const overlayOrder = visibleElements.length + 1;
|
|
7029
|
-
|
|
7053
|
+
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
7054
|
+
if (hybridActive && hasOverlay) hybridOrders.add(overlayOrder);
|
|
7030
7055
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
7031
7056
|
for (const [layerId, elements] of this.layerGroups) {
|
|
7032
7057
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -7136,7 +7161,7 @@ var RenderLoop = class {
|
|
|
7136
7161
|
hybridCtx.restore();
|
|
7137
7162
|
}
|
|
7138
7163
|
const overlayT0 = performance.now();
|
|
7139
|
-
if (hybridActive &&
|
|
7164
|
+
if (hybridActive && hasOverlay) {
|
|
7140
7165
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
7141
7166
|
if (overlayCtx) {
|
|
7142
7167
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -7144,11 +7169,13 @@ var RenderLoop = class {
|
|
|
7144
7169
|
overlayCtx.scale(dpr, dpr);
|
|
7145
7170
|
overlayCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
7146
7171
|
overlayCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
7147
|
-
|
|
7172
|
+
this.drawRegisteredOverlays(overlayCtx);
|
|
7173
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(overlayCtx);
|
|
7148
7174
|
overlayCtx.restore();
|
|
7149
7175
|
}
|
|
7150
|
-
} else if (
|
|
7151
|
-
|
|
7176
|
+
} else if (hasOverlay) {
|
|
7177
|
+
this.drawRegisteredOverlays(ctx);
|
|
7178
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(ctx);
|
|
7152
7179
|
}
|
|
7153
7180
|
const overlayMs = performance.now() - overlayT0;
|
|
7154
7181
|
ctx.restore();
|
|
@@ -8142,6 +8169,17 @@ var Viewport = class {
|
|
|
8142
8169
|
requestRender() {
|
|
8143
8170
|
this.renderLoop.requestRender();
|
|
8144
8171
|
}
|
|
8172
|
+
/**
|
|
8173
|
+
* Registers a world-space overlay drawn above elements on every frame,
|
|
8174
|
+
* regardless of the active tool — the surface for remote presence visuals
|
|
8175
|
+
* such as laser trails, cursors, pings, and shared rulers. Overlays draw
|
|
8176
|
+
* beneath the active tool's own `renderOverlay` and never touch elements,
|
|
8177
|
+
* history, or persisted state. Returns an idempotent unsubscribe that also
|
|
8178
|
+
* erases the overlay's last frame.
|
|
8179
|
+
*/
|
|
8180
|
+
registerOverlay(draw) {
|
|
8181
|
+
return this.renderLoop.registerOverlay(draw);
|
|
8182
|
+
}
|
|
8145
8183
|
exportState() {
|
|
8146
8184
|
return exportState(
|
|
8147
8185
|
this.store.snapshot(),
|
|
@@ -8489,6 +8527,154 @@ var Viewport = class {
|
|
|
8489
8527
|
}
|
|
8490
8528
|
};
|
|
8491
8529
|
|
|
8530
|
+
// src/canvas/remote-laser-overlay.ts
|
|
8531
|
+
var LASER_TRAIL_PRESENCE_KIND = "laser";
|
|
8532
|
+
function isFinitePoint(value) {
|
|
8533
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8534
|
+
const point = value;
|
|
8535
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
8536
|
+
}
|
|
8537
|
+
function isLaserTrailPresence(data) {
|
|
8538
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8539
|
+
const payload = data;
|
|
8540
|
+
if (payload.kind !== LASER_TRAIL_PRESENCE_KIND) return false;
|
|
8541
|
+
if (!Array.isArray(payload.points) || !payload.points.every(isFinitePoint)) return false;
|
|
8542
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8543
|
+
if (payload.width !== void 0 && !(typeof payload.width === "number" && payload.width > 0)) {
|
|
8544
|
+
return false;
|
|
8545
|
+
}
|
|
8546
|
+
if (payload.fadeMs !== void 0 && !(typeof payload.fadeMs === "number" && payload.fadeMs > 0)) {
|
|
8547
|
+
return false;
|
|
8548
|
+
}
|
|
8549
|
+
return true;
|
|
8550
|
+
}
|
|
8551
|
+
function toLaserTrailPresence(emission) {
|
|
8552
|
+
return {
|
|
8553
|
+
kind: LASER_TRAIL_PRESENCE_KIND,
|
|
8554
|
+
points: emission.points.map((point) => ({ x: point.x, y: point.y })),
|
|
8555
|
+
color: emission.color,
|
|
8556
|
+
width: emission.width,
|
|
8557
|
+
fadeMs: emission.fadeMs
|
|
8558
|
+
};
|
|
8559
|
+
}
|
|
8560
|
+
var DEFAULT_COLOR = "#ff3b30";
|
|
8561
|
+
var DEFAULT_WIDTH = 4;
|
|
8562
|
+
var DEFAULT_FADE_MS = 1200;
|
|
8563
|
+
var DEFAULT_MAX_POINTS = 512;
|
|
8564
|
+
var RemoteLaserOverlay = class {
|
|
8565
|
+
host;
|
|
8566
|
+
color;
|
|
8567
|
+
width;
|
|
8568
|
+
fadeMs;
|
|
8569
|
+
maxPointsPerSender;
|
|
8570
|
+
trails = /* @__PURE__ */ new Map();
|
|
8571
|
+
unregister;
|
|
8572
|
+
rafId = null;
|
|
8573
|
+
disposed = false;
|
|
8574
|
+
constructor(host, options = {}) {
|
|
8575
|
+
this.host = host;
|
|
8576
|
+
this.color = options.color ?? DEFAULT_COLOR;
|
|
8577
|
+
this.width = options.width ?? DEFAULT_WIDTH;
|
|
8578
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
|
|
8579
|
+
this.maxPointsPerSender = options.maxPointsPerSender ?? DEFAULT_MAX_POINTS;
|
|
8580
|
+
this.unregister = host.registerOverlay((ctx) => this.renderTrails(ctx));
|
|
8581
|
+
}
|
|
8582
|
+
now() {
|
|
8583
|
+
return performance.now();
|
|
8584
|
+
}
|
|
8585
|
+
/**
|
|
8586
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8587
|
+
* the envelope `from`). Non-laser or malformed payloads are ignored and
|
|
8588
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8589
|
+
*/
|
|
8590
|
+
apply(sender, data) {
|
|
8591
|
+
if (this.disposed || !isLaserTrailPresence(data)) return false;
|
|
8592
|
+
let trail = this.trails.get(sender);
|
|
8593
|
+
if (!trail) {
|
|
8594
|
+
trail = { points: [], color: this.color, width: this.width, fadeMs: this.fadeMs };
|
|
8595
|
+
this.trails.set(sender, trail);
|
|
8596
|
+
}
|
|
8597
|
+
trail.color = data.color ?? this.color;
|
|
8598
|
+
trail.width = data.width ?? this.width;
|
|
8599
|
+
trail.fadeMs = data.fadeMs ?? this.fadeMs;
|
|
8600
|
+
const t = this.now();
|
|
8601
|
+
for (const point of data.points) {
|
|
8602
|
+
trail.points.push({ x: point.x, y: point.y, t });
|
|
8603
|
+
}
|
|
8604
|
+
const excess = trail.points.length - this.maxPointsPerSender;
|
|
8605
|
+
if (excess > 0) trail.points.splice(0, excess);
|
|
8606
|
+
this.ensureAnimating();
|
|
8607
|
+
this.host.requestRender();
|
|
8608
|
+
return true;
|
|
8609
|
+
}
|
|
8610
|
+
/** Removes a sender's trail immediately (presence-leave/disconnect). */
|
|
8611
|
+
remove(sender) {
|
|
8612
|
+
if (this.trails.delete(sender)) this.host.requestRender();
|
|
8613
|
+
}
|
|
8614
|
+
/** Removes every trail immediately. */
|
|
8615
|
+
clear() {
|
|
8616
|
+
if (this.trails.size === 0) return;
|
|
8617
|
+
this.trails.clear();
|
|
8618
|
+
this.host.requestRender();
|
|
8619
|
+
}
|
|
8620
|
+
/** Number of senders with a live (unfaded) trail. */
|
|
8621
|
+
get activeSenderCount() {
|
|
8622
|
+
return this.trails.size;
|
|
8623
|
+
}
|
|
8624
|
+
/** Unregisters the overlay and stops the fade loop. Idempotent. */
|
|
8625
|
+
dispose() {
|
|
8626
|
+
if (this.disposed) return;
|
|
8627
|
+
this.disposed = true;
|
|
8628
|
+
if (this.rafId !== null) {
|
|
8629
|
+
cancelAnimationFrame(this.rafId);
|
|
8630
|
+
this.rafId = null;
|
|
8631
|
+
}
|
|
8632
|
+
this.trails.clear();
|
|
8633
|
+
this.unregister?.();
|
|
8634
|
+
this.unregister = null;
|
|
8635
|
+
}
|
|
8636
|
+
ensureAnimating() {
|
|
8637
|
+
if (this.rafId === null) {
|
|
8638
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
tick() {
|
|
8642
|
+
if (this.disposed) return;
|
|
8643
|
+
const now = this.now();
|
|
8644
|
+
for (const [sender, trail] of this.trails) {
|
|
8645
|
+
const cutoff = now - trail.fadeMs;
|
|
8646
|
+
trail.points = trail.points.filter((point) => point.t >= cutoff);
|
|
8647
|
+
if (trail.points.length === 0) this.trails.delete(sender);
|
|
8648
|
+
}
|
|
8649
|
+
this.host.requestRender();
|
|
8650
|
+
this.rafId = this.trails.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8651
|
+
}
|
|
8652
|
+
renderTrails(ctx) {
|
|
8653
|
+
if (this.trails.size === 0) return;
|
|
8654
|
+
const now = this.now();
|
|
8655
|
+
for (const trail of this.trails.values()) {
|
|
8656
|
+
if (trail.points.length < 2) continue;
|
|
8657
|
+
ctx.save();
|
|
8658
|
+
ctx.strokeStyle = trail.color;
|
|
8659
|
+
ctx.lineWidth = trail.width;
|
|
8660
|
+
ctx.lineCap = "round";
|
|
8661
|
+
ctx.lineJoin = "round";
|
|
8662
|
+
for (let i = 0; i < trail.points.length - 1; i++) {
|
|
8663
|
+
const a = trail.points[i];
|
|
8664
|
+
const b = trail.points[i + 1];
|
|
8665
|
+
if (!a || !b) continue;
|
|
8666
|
+
const age = now - b.t;
|
|
8667
|
+
ctx.globalAlpha = Math.max(0, 1 - age / trail.fadeMs);
|
|
8668
|
+
ctx.beginPath();
|
|
8669
|
+
ctx.moveTo(a.x, a.y);
|
|
8670
|
+
ctx.lineTo(b.x, b.y);
|
|
8671
|
+
ctx.stroke();
|
|
8672
|
+
}
|
|
8673
|
+
ctx.restore();
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
};
|
|
8677
|
+
|
|
8492
8678
|
// src/tools/hand-tool.ts
|
|
8493
8679
|
var HandTool = class {
|
|
8494
8680
|
name = "hand";
|
|
@@ -11045,9 +11231,9 @@ var TemplateTool = class {
|
|
|
11045
11231
|
};
|
|
11046
11232
|
|
|
11047
11233
|
// src/tools/laser-tool.ts
|
|
11048
|
-
var
|
|
11049
|
-
var
|
|
11050
|
-
var
|
|
11234
|
+
var DEFAULT_COLOR2 = "#ff3b30";
|
|
11235
|
+
var DEFAULT_WIDTH2 = 4;
|
|
11236
|
+
var DEFAULT_FADE_MS2 = 1200;
|
|
11051
11237
|
var LaserTool = class {
|
|
11052
11238
|
name;
|
|
11053
11239
|
color;
|
|
@@ -11057,11 +11243,13 @@ var LaserTool = class {
|
|
|
11057
11243
|
rafId = null;
|
|
11058
11244
|
drawing = false;
|
|
11059
11245
|
optionListeners = /* @__PURE__ */ new Set();
|
|
11246
|
+
trailListeners = /* @__PURE__ */ new Set();
|
|
11247
|
+
pendingEmission = [];
|
|
11060
11248
|
constructor(options = {}) {
|
|
11061
11249
|
this.name = options.name ?? "laser";
|
|
11062
|
-
this.color = options.color ??
|
|
11063
|
-
this.width = options.width ??
|
|
11064
|
-
this.fadeMs = options.fadeMs ??
|
|
11250
|
+
this.color = options.color ?? DEFAULT_COLOR2;
|
|
11251
|
+
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11252
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11065
11253
|
}
|
|
11066
11254
|
now() {
|
|
11067
11255
|
return performance.now();
|
|
@@ -11076,6 +11264,7 @@ var LaserTool = class {
|
|
|
11076
11264
|
}
|
|
11077
11265
|
this.trail = [];
|
|
11078
11266
|
this.drawing = false;
|
|
11267
|
+
this.pendingEmission = [];
|
|
11079
11268
|
ctx.setCursor?.("default");
|
|
11080
11269
|
ctx.requestRender();
|
|
11081
11270
|
}
|
|
@@ -11091,6 +11280,16 @@ var LaserTool = class {
|
|
|
11091
11280
|
this.optionListeners.add(listener);
|
|
11092
11281
|
return () => this.optionListeners.delete(listener);
|
|
11093
11282
|
}
|
|
11283
|
+
/**
|
|
11284
|
+
* Subscribes to coalesced trail emissions. Points recorded by pointer input
|
|
11285
|
+
* are batched and delivered at most once per animation frame, keeping
|
|
11286
|
+
* outgoing presence packets small. Listeners must not throw; a throwing
|
|
11287
|
+
* listener is isolated so it cannot stall the fade loop.
|
|
11288
|
+
*/
|
|
11289
|
+
onTrail(listener) {
|
|
11290
|
+
this.trailListeners.add(listener);
|
|
11291
|
+
return () => this.trailListeners.delete(listener);
|
|
11292
|
+
}
|
|
11094
11293
|
setOptions(options) {
|
|
11095
11294
|
if (options.color !== void 0) this.color = options.color;
|
|
11096
11295
|
if (options.width !== void 0) this.width = options.width;
|
|
@@ -11099,14 +11298,18 @@ var LaserTool = class {
|
|
|
11099
11298
|
}
|
|
11100
11299
|
onPointerDown(state, ctx) {
|
|
11101
11300
|
this.drawing = true;
|
|
11102
|
-
|
|
11103
|
-
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11104
|
-
this.ensureAnimating(ctx);
|
|
11301
|
+
this.recordPoint(state, ctx);
|
|
11105
11302
|
}
|
|
11106
11303
|
onPointerMove(state, ctx) {
|
|
11107
11304
|
if (!this.drawing) return;
|
|
11305
|
+
this.recordPoint(state, ctx);
|
|
11306
|
+
}
|
|
11307
|
+
recordPoint(state, ctx) {
|
|
11108
11308
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11109
11309
|
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11310
|
+
if (this.trailListeners.size > 0) {
|
|
11311
|
+
this.pendingEmission.push({ x: world.x, y: world.y });
|
|
11312
|
+
}
|
|
11110
11313
|
this.ensureAnimating(ctx);
|
|
11111
11314
|
}
|
|
11112
11315
|
onPointerUp(_state, _ctx) {
|
|
@@ -11138,7 +11341,24 @@ var LaserTool = class {
|
|
|
11138
11341
|
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11139
11342
|
}
|
|
11140
11343
|
}
|
|
11344
|
+
flushEmission() {
|
|
11345
|
+
if (this.pendingEmission.length === 0) return;
|
|
11346
|
+
const emission = {
|
|
11347
|
+
points: this.pendingEmission,
|
|
11348
|
+
color: this.color,
|
|
11349
|
+
width: this.width,
|
|
11350
|
+
fadeMs: this.fadeMs
|
|
11351
|
+
};
|
|
11352
|
+
this.pendingEmission = [];
|
|
11353
|
+
for (const listener of this.trailListeners) {
|
|
11354
|
+
try {
|
|
11355
|
+
listener(emission);
|
|
11356
|
+
} catch {
|
|
11357
|
+
}
|
|
11358
|
+
}
|
|
11359
|
+
}
|
|
11141
11360
|
tick(ctx) {
|
|
11361
|
+
this.flushEmission();
|
|
11142
11362
|
const cutoff = this.now() - this.fadeMs;
|
|
11143
11363
|
this.trail = this.trail.filter((p) => p.t >= cutoff);
|
|
11144
11364
|
if (this.trail.length > 0) {
|
|
@@ -11155,7 +11375,7 @@ var LaserTool = class {
|
|
|
11155
11375
|
};
|
|
11156
11376
|
|
|
11157
11377
|
// src/index.ts
|
|
11158
|
-
var VERSION = "0.
|
|
11378
|
+
var VERSION = "0.54.0";
|
|
11159
11379
|
export {
|
|
11160
11380
|
ArrowTool,
|
|
11161
11381
|
AutoSave,
|
|
@@ -11167,6 +11387,7 @@ export {
|
|
|
11167
11387
|
HistoryStack,
|
|
11168
11388
|
ImageTool,
|
|
11169
11389
|
IndexedDBAdapter,
|
|
11390
|
+
LASER_TRAIL_PRESENCE_KIND,
|
|
11170
11391
|
LaserTool,
|
|
11171
11392
|
LayerManager,
|
|
11172
11393
|
LocalStorageAdapter,
|
|
@@ -11174,6 +11395,7 @@ export {
|
|
|
11174
11395
|
MemoryAdapter,
|
|
11175
11396
|
NoteTool,
|
|
11176
11397
|
PencilTool,
|
|
11398
|
+
RemoteLaserOverlay,
|
|
11177
11399
|
SelectTool,
|
|
11178
11400
|
ShapeTool,
|
|
11179
11401
|
TemplateTool,
|
|
@@ -11209,12 +11431,14 @@ export {
|
|
|
11209
11431
|
getHexCellsInRectangle,
|
|
11210
11432
|
getHexCellsInSquare,
|
|
11211
11433
|
getHexDistance,
|
|
11434
|
+
isLaserTrailPresence,
|
|
11212
11435
|
isNearBezier,
|
|
11213
11436
|
setFontSize,
|
|
11214
11437
|
smartSnap,
|
|
11215
11438
|
snapPoint,
|
|
11216
11439
|
snapToHexCenter,
|
|
11217
11440
|
styleToPatch,
|
|
11441
|
+
toLaserTrailPresence,
|
|
11218
11442
|
toggleBold,
|
|
11219
11443
|
toggleItalic,
|
|
11220
11444
|
toggleStrikethrough,
|