@canvas-harness/core 0.1.9 → 0.1.11

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.d.cts CHANGED
@@ -2529,15 +2529,36 @@ type SerializedClipboard = {
2529
2529
  */
2530
2530
  declare const serializeSelection: (store: CanvasStore) => SerializedClipboard;
2531
2531
  type DeserializeOptions = {
2532
- /** World-space offset applied to all pasted nodes. Default (20, 20). */
2532
+ /**
2533
+ * Relative world-space offset added to every pasted node's `x/y`
2534
+ * (and to free-floating edge endpoints). Takes precedence over
2535
+ * `at` when both are passed. Default `(20, 20)` when neither is
2536
+ * given.
2537
+ */
2533
2538
  offset?: Vec2;
2539
+ /**
2540
+ * Absolute world-space target — the *center* of the pasted bbox
2541
+ * lands here. Used by `paste()` to place the paste at the cursor;
2542
+ * pass directly for programmatic absolute placement. Ignored if
2543
+ * `offset` is also set.
2544
+ */
2545
+ at?: Vec2;
2534
2546
  /** Override the selection on the store after applying. Default true. */
2535
2547
  select?: boolean;
2536
2548
  };
2537
2549
  /**
2538
2550
  * Applies a clipboard payload to the store. New ids are minted; edge
2539
- * endpoints are rewired; offset defaults to `(20, 20)` world units;
2540
- * the resulting nodes + edges become the new selection by default.
2551
+ * endpoints are rewired; the resulting nodes + edges become the new
2552
+ * selection by default. Positioning precedence:
2553
+ *
2554
+ * 1. `opts.offset` (relative) — used as-is.
2555
+ * 2. `opts.at` (absolute) — offset computed so the clip's bbox
2556
+ * center lands on this point.
2557
+ * 3. Default — relative `(20, 20)` offset.
2558
+ *
2559
+ * Free-floating edge endpoints (`{ worldPoint }`) also receive the
2560
+ * offset so an edge with an unattached end stays connected to the
2561
+ * surrounding nodes after the paste.
2541
2562
  *
2542
2563
  * One `store.batch` — one undo step.
2543
2564
  *
@@ -2577,8 +2598,17 @@ declare const cut: (store: CanvasStore) => Promise<SerializedClipboard>;
2577
2598
  /**
2578
2599
  * Paste from the system clipboard (or a supplied payload). Every node
2579
2600
  * + edge gets a fresh id; edge endpoints rewire to the new ids; the
2580
- * paste is offset by `(+20, +20)` world units so it doesn't overlay
2581
- * the original. Wrapped in one undoable batch.
2601
+ * resulting nodes + edges become the new selection. Wrapped in one
2602
+ * undoable batch.
2603
+ *
2604
+ * Positioning, in precedence order:
2605
+ * 1. `opts.offset` — relative offset, used as-is.
2606
+ * 2. `opts.at` — absolute target; the paste's bbox center lands here.
2607
+ * 3. The store's current cursor (`interactionState.pointer`) — the
2608
+ * paste lands centered under the cursor. This is the default
2609
+ * `paste(store)` behavior on a Cmd+V keybind.
2610
+ * 4. Fallback `(20, 20)` relative offset when nothing else is known
2611
+ * (e.g. fresh session with no pointermove yet).
2582
2612
  *
2583
2613
  * Returns the new node ids on success, or `null` if the clipboard
2584
2614
  * didn't contain a canvas-harness payload.
@@ -2587,8 +2617,8 @@ declare const cut: (store: CanvasStore) => Promise<SerializedClipboard>;
2587
2617
  * <button onClick={() => paste(store)}>Paste</button>
2588
2618
  *
2589
2619
  * @example
2590
- * // Programmatic paste from a saved JSON snippet:
2591
- * paste(store, savedClip, { offset: { x: 0, y: 0 }, select: false })
2620
+ * // Programmatic paste at a specific world point:
2621
+ * paste(store, savedClip, { at: { x: 300, y: 200 }, select: false })
2592
2622
  */
2593
2623
  declare const paste: (store: CanvasStore, payload?: SerializedClipboard, opts?: DeserializeOptions) => Promise<(NodeId | EdgeId)[] | null>;
2594
2624
 
package/dist/index.d.ts CHANGED
@@ -2529,15 +2529,36 @@ type SerializedClipboard = {
2529
2529
  */
2530
2530
  declare const serializeSelection: (store: CanvasStore) => SerializedClipboard;
2531
2531
  type DeserializeOptions = {
2532
- /** World-space offset applied to all pasted nodes. Default (20, 20). */
2532
+ /**
2533
+ * Relative world-space offset added to every pasted node's `x/y`
2534
+ * (and to free-floating edge endpoints). Takes precedence over
2535
+ * `at` when both are passed. Default `(20, 20)` when neither is
2536
+ * given.
2537
+ */
2533
2538
  offset?: Vec2;
2539
+ /**
2540
+ * Absolute world-space target — the *center* of the pasted bbox
2541
+ * lands here. Used by `paste()` to place the paste at the cursor;
2542
+ * pass directly for programmatic absolute placement. Ignored if
2543
+ * `offset` is also set.
2544
+ */
2545
+ at?: Vec2;
2534
2546
  /** Override the selection on the store after applying. Default true. */
2535
2547
  select?: boolean;
2536
2548
  };
2537
2549
  /**
2538
2550
  * Applies a clipboard payload to the store. New ids are minted; edge
2539
- * endpoints are rewired; offset defaults to `(20, 20)` world units;
2540
- * the resulting nodes + edges become the new selection by default.
2551
+ * endpoints are rewired; the resulting nodes + edges become the new
2552
+ * selection by default. Positioning precedence:
2553
+ *
2554
+ * 1. `opts.offset` (relative) — used as-is.
2555
+ * 2. `opts.at` (absolute) — offset computed so the clip's bbox
2556
+ * center lands on this point.
2557
+ * 3. Default — relative `(20, 20)` offset.
2558
+ *
2559
+ * Free-floating edge endpoints (`{ worldPoint }`) also receive the
2560
+ * offset so an edge with an unattached end stays connected to the
2561
+ * surrounding nodes after the paste.
2541
2562
  *
2542
2563
  * One `store.batch` — one undo step.
2543
2564
  *
@@ -2577,8 +2598,17 @@ declare const cut: (store: CanvasStore) => Promise<SerializedClipboard>;
2577
2598
  /**
2578
2599
  * Paste from the system clipboard (or a supplied payload). Every node
2579
2600
  * + edge gets a fresh id; edge endpoints rewire to the new ids; the
2580
- * paste is offset by `(+20, +20)` world units so it doesn't overlay
2581
- * the original. Wrapped in one undoable batch.
2601
+ * resulting nodes + edges become the new selection. Wrapped in one
2602
+ * undoable batch.
2603
+ *
2604
+ * Positioning, in precedence order:
2605
+ * 1. `opts.offset` — relative offset, used as-is.
2606
+ * 2. `opts.at` — absolute target; the paste's bbox center lands here.
2607
+ * 3. The store's current cursor (`interactionState.pointer`) — the
2608
+ * paste lands centered under the cursor. This is the default
2609
+ * `paste(store)` behavior on a Cmd+V keybind.
2610
+ * 4. Fallback `(20, 20)` relative offset when nothing else is known
2611
+ * (e.g. fresh session with no pointermove yet).
2582
2612
  *
2583
2613
  * Returns the new node ids on success, or `null` if the clipboard
2584
2614
  * didn't contain a canvas-harness payload.
@@ -2587,8 +2617,8 @@ declare const cut: (store: CanvasStore) => Promise<SerializedClipboard>;
2587
2617
  * <button onClick={() => paste(store)}>Paste</button>
2588
2618
  *
2589
2619
  * @example
2590
- * // Programmatic paste from a saved JSON snippet:
2591
- * paste(store, savedClip, { offset: { x: 0, y: 0 }, select: false })
2620
+ * // Programmatic paste at a specific world point:
2621
+ * paste(store, savedClip, { at: { x: 300, y: 200 }, select: false })
2592
2622
  */
2593
2623
  declare const paste: (store: CanvasStore, payload?: SerializedClipboard, opts?: DeserializeOptions) => Promise<(NodeId | EdgeId)[] | null>;
2594
2624
 
package/dist/index.js CHANGED
@@ -2336,6 +2336,7 @@ var createDefaultTextareaEditor = ({
2336
2336
  wrap.style.borderRadius = "4px";
2337
2337
  wrap.style.background = style.backgroundColor ?? "#ffffff";
2338
2338
  wrap.style.zIndex = "20";
2339
+ wrap.style.pointerEvents = "auto";
2339
2340
  const ta = document.createElement("textarea");
2340
2341
  ta.value = node.content ?? "";
2341
2342
  ta.spellcheck = false;
@@ -4784,7 +4785,13 @@ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
4784
4785
  strokeWidth,
4785
4786
  roughness,
4786
4787
  seed,
4787
- strokeLineDash: dash.length > 0 ? dash : void 0,
4788
+ // Always pass an explicit array (empty = solid) so rough.js calls
4789
+ // ctx.setLineDash() to a known state. Passing `undefined` makes
4790
+ // rough skip that call, and the canvas inherits whatever the
4791
+ // previous draw left behind — a transparent-stroke node's
4792
+ // fill-derived outline would pick up the dash from an earlier
4793
+ // dashed node in the same paint pass.
4794
+ strokeLineDash: dash,
4788
4795
  curveStepCount: detail.curveStepCount,
4789
4796
  maxRandomnessOffset: detail.maxRandomnessOffset
4790
4797
  });
@@ -5957,8 +5964,30 @@ var endInside = (end, ids) => {
5957
5964
  if (!isAttached(end)) return true;
5958
5965
  return ids.has(end.nodeId);
5959
5966
  };
5967
+ var clipBboxCenter = (nodes) => {
5968
+ if (nodes.length === 0) return { x: 0, y: 0 };
5969
+ let minX = Number.POSITIVE_INFINITY;
5970
+ let minY = Number.POSITIVE_INFINITY;
5971
+ let maxX = Number.NEGATIVE_INFINITY;
5972
+ let maxY = Number.NEGATIVE_INFINITY;
5973
+ for (const n of nodes) {
5974
+ if (n.x < minX) minX = n.x;
5975
+ if (n.y < minY) minY = n.y;
5976
+ if (n.x + n.w > maxX) maxX = n.x + n.w;
5977
+ if (n.y + n.h > maxY) maxY = n.y + n.h;
5978
+ }
5979
+ return { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
5980
+ };
5960
5981
  var deserializeClipboard = (store, clip, opts = {}) => {
5961
- const offset = opts.offset ?? { x: 20, y: 20 };
5982
+ let offset;
5983
+ if (opts.offset) {
5984
+ offset = opts.offset;
5985
+ } else if (opts.at && clip.nodes.length > 0) {
5986
+ const center = clipBboxCenter(clip.nodes);
5987
+ offset = { x: opts.at.x - center.x, y: opts.at.y - center.y };
5988
+ } else {
5989
+ offset = { x: 20, y: 20 };
5990
+ }
5962
5991
  const select = opts.select ?? true;
5963
5992
  const nodeMap = /* @__PURE__ */ new Map();
5964
5993
  const edgeMap = /* @__PURE__ */ new Map();
@@ -5971,7 +6000,9 @@ var deserializeClipboard = (store, clip, opts = {}) => {
5971
6000
  y: n.y + offset.y
5972
6001
  }));
5973
6002
  const remapEnd = (end) => {
5974
- if (!isAttached(end)) return end;
6003
+ if (!isAttached(end)) {
6004
+ return { worldPoint: { x: end.worldPoint.x + offset.x, y: end.worldPoint.y + offset.y } };
6005
+ }
5975
6006
  const newId = nodeMap.get(end.nodeId);
5976
6007
  return newId ? { nodeId: newId, localOffset: end.localOffset } : end;
5977
6008
  };
@@ -6014,7 +6045,14 @@ var cut = async (store) => {
6014
6045
  var paste = async (store, payload, opts) => {
6015
6046
  const clip = payload ?? await readClipboard();
6016
6047
  if (!clip) return null;
6017
- const ids = deserializeClipboard(store, clip, opts);
6048
+ let effective = opts;
6049
+ if (!opts?.offset && !opts?.at) {
6050
+ const pointer = store.getInteractionState().pointer;
6051
+ if (pointer) {
6052
+ effective = { ...opts, at: { x: pointer.worldX, y: pointer.worldY } };
6053
+ }
6054
+ }
6055
+ const ids = deserializeClipboard(store, clip, effective);
6018
6056
  return ids;
6019
6057
  };
6020
6058
  var writeClipboard = async (clip) => {