stickyflow 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -17,6 +17,12 @@ Two things count as breaking here that are easy to overlook:
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [0.4.0] - 2026-10-17
21
+
22
+ ### Added
23
+
24
+ - `applyQuickDuplicateChange(source, event, { shapeId, linkId, gap? })` in `stickyflow/interaction` carries out a quick duplicate. It adds a copy of the object with its look, beyond the edge that was dragged, and a link to it that looks like the other links of the document. For an isometric object it adds a copy and a connection between the touching faces. The ids come from the caller, and `gap` is 200 for a shape and 40 for an isometric object by default.
25
+
20
26
  ## [0.3.0] - 2026-10-17
21
27
 
22
28
  ### Added
package/README.md CHANGED
@@ -155,7 +155,7 @@ npm run ci # lint, typecheck, tests, build, smoke
155
155
  npm run build # tsup
156
156
  ```
157
157
 
158
- Adding a new placeable object: [docs/ADDING_CANVAS_OBJECT.md](docs/ADDING_CANVAS_OBJECT.md). Changes: [CHANGELOG.md](CHANGELOG.md).
158
+ How the package and its neighbours fit together: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Adding a new placeable object: [docs/ADDING_CANVAS_OBJECT.md](docs/ADDING_CANVAS_OBJECT.md). Changes: [CHANGELOG.md](CHANGELOG.md).
159
159
 
160
160
  ## More
161
161
 
@@ -7288,6 +7288,257 @@ function shouldQuote(current, quote, indexed) {
7288
7288
  return indexed && trimmed === "none";
7289
7289
  }
7290
7290
 
7291
+ // src/runtime/interaction/sflPatch/connectionPatch/blockText.ts
7292
+ function buildConnectionBlock(options, baseIndent) {
7293
+ const bodyIndent = `${baseIndent} `;
7294
+ const block = [
7295
+ `${baseIndent}Connection ${options.id} {`,
7296
+ `${bodyIndent}from: ${options.fromId}`,
7297
+ `${bodyIndent}to: ${options.toId}`,
7298
+ `${bodyIndent}fromFace: "${options.fromFace}"`,
7299
+ `${bodyIndent}toFace: "${options.toFace}"`
7300
+ ];
7301
+ if (options.body) {
7302
+ block.push(...indentBlock(options.body, bodyIndent));
7303
+ }
7304
+ block.push(`${baseIndent}}`);
7305
+ return block;
7306
+ }
7307
+
7308
+ // src/runtime/interaction/sflPatch/connectionPatch/insertionTarget.ts
7309
+ function resolveConnectionInsertionTarget(sfl, lines, options) {
7310
+ if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7311
+ return null;
7312
+ }
7313
+ const canvasObjects = chunkCXL254BT_cjs.sflToCanvasState(sfl).canvasObjects;
7314
+ const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7315
+ const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7316
+ if (!hasSource || !hasTarget) {
7317
+ return null;
7318
+ }
7319
+ return findFirstBlockOfKind(lines, "Model");
7320
+ }
7321
+
7322
+ // src/runtime/interaction/sflPatch/connectionPatch/insert.ts
7323
+ function insertConnection(sfl, options) {
7324
+ const lines = sfl.split("\n");
7325
+ const modelRange = resolveConnectionInsertionTarget(sfl, lines, options);
7326
+ if (!modelRange) {
7327
+ return sfl;
7328
+ }
7329
+ insertBlockLines(lines, modelRange, (baseIndent) => buildConnectionBlock(options, baseIndent));
7330
+ return lines.join("\n");
7331
+ }
7332
+
7333
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/styleBlock.ts
7334
+ var QUOTED_KEYS = /* @__PURE__ */ new Set(["fill", "stroke", "textColor", "fontFamily", "shadow", "labelBackground"]);
7335
+ function styleLine(key, value) {
7336
+ return QUOTED_KEYS.has(key) ? ` ${key}: "${value.replace(/"/g, '\\"')}"` : ` ${key}: ${value}`;
7337
+ }
7338
+ function styleBlock(props, keys, leading = []) {
7339
+ const copied = keys.flatMap((key) => {
7340
+ const value = props?.[key];
7341
+ return value === void 0 ? [] : [styleLine(key, value)];
7342
+ });
7343
+ const lines = [...leading, ...copied];
7344
+ return lines.length > 0 ? `style {
7345
+ ${lines.join("\n")}
7346
+ }` : void 0;
7347
+ }
7348
+
7349
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/isometricDuplicate.ts
7350
+ var ISOMETRIC_GAP = 40;
7351
+ var ISOMETRIC_STYLE_KEYS = [
7352
+ "fill",
7353
+ "stroke",
7354
+ "strokeWidth",
7355
+ "cornerRadius",
7356
+ "shadow",
7357
+ "labelOffsetX",
7358
+ "labelOffsetY",
7359
+ "labelBackground"
7360
+ ];
7361
+ var TOUCHING_FACES = {
7362
+ "iso-width-pos": { fromFace: "right", toFace: "left" },
7363
+ "iso-width-neg": { fromFace: "left", toFace: "right" },
7364
+ "iso-depth-pos": { fromFace: "front", toFace: "back" },
7365
+ "iso-depth-neg": { fromFace: "back", toFace: "front" },
7366
+ "iso-height-pos": { fromFace: "bottom", toFace: "top" },
7367
+ "iso-height-neg": { fromFace: "top", toFace: "bottom" }
7368
+ };
7369
+ function isIsometricDirection2(direction) {
7370
+ return direction.startsWith("iso-");
7371
+ }
7372
+ function isometricOffset(direction, object, gap) {
7373
+ const { width, height } = object;
7374
+ const { depth } = object.isometric;
7375
+ switch (direction) {
7376
+ case "iso-width-pos":
7377
+ return { dx: (width + gap) * chunkCXL254BT_cjs.AXIS_COS, dy: (width + gap) * chunkCXL254BT_cjs.AXIS_SIN };
7378
+ case "iso-width-neg":
7379
+ return { dx: -(width + gap) * chunkCXL254BT_cjs.AXIS_COS, dy: -(width + gap) * chunkCXL254BT_cjs.AXIS_SIN };
7380
+ case "iso-depth-pos":
7381
+ return { dx: -(depth + gap) * chunkCXL254BT_cjs.AXIS_COS, dy: (depth + gap) * chunkCXL254BT_cjs.AXIS_SIN };
7382
+ case "iso-depth-neg":
7383
+ return { dx: (depth + gap) * chunkCXL254BT_cjs.AXIS_COS, dy: -(depth + gap) * chunkCXL254BT_cjs.AXIS_SIN };
7384
+ case "iso-height-pos":
7385
+ return { dx: 0, dy: height + gap };
7386
+ case "iso-height-neg":
7387
+ return { dx: 0, dy: -(height + gap) };
7388
+ }
7389
+ }
7390
+ function copyStyle(object, direction) {
7391
+ if (!direction.startsWith("iso-height-")) {
7392
+ return styleBlock(object.props, ISOMETRIC_STYLE_KEYS);
7393
+ }
7394
+ return styleBlock(
7395
+ object.props,
7396
+ ISOMETRIC_STYLE_KEYS.filter((key) => key !== "shadow"),
7397
+ [' shadow: "off"']
7398
+ );
7399
+ }
7400
+ function copyOf(object, direction, options) {
7401
+ const { dx, dy } = isometricOffset(direction, object, options.gap ?? ISOMETRIC_GAP);
7402
+ return {
7403
+ id: options.shapeId,
7404
+ kind: "isometric",
7405
+ text: object.text ?? "",
7406
+ x: object.x + dx,
7407
+ y: object.y + dy,
7408
+ width: object.width,
7409
+ height: object.height,
7410
+ depth: object.isometric.depth,
7411
+ parentId: object.parentFrameId,
7412
+ body: copyStyle(object, direction)
7413
+ };
7414
+ }
7415
+ function duplicateIsometricObject(sfl, object, direction, options) {
7416
+ const withCopy = insertCanvasObject(sfl, copyOf(object, direction, options));
7417
+ return insertConnection(withCopy, {
7418
+ id: options.linkId,
7419
+ fromId: object.id,
7420
+ toId: options.shapeId,
7421
+ ...TOUCHING_FACES[direction]
7422
+ });
7423
+ }
7424
+
7425
+ // src/runtime/interaction/sflPatch/linkPatch/blockText.ts
7426
+ function buildLinkBlock(options, baseIndent) {
7427
+ const bodyIndent = `${baseIndent} `;
7428
+ const from = formatLinkEndpoint({ id: options.fromId, side: options.fromSide, fraction: options.fromFraction });
7429
+ const to = formatLinkEndpoint({ id: options.toId, side: options.toSide, fraction: options.toFraction });
7430
+ const block = [`${baseIndent}Link ${options.id} {`, `${bodyIndent}from: ${from}`, `${bodyIndent}to: ${to}`];
7431
+ if (options.text !== void 0) {
7432
+ block.push(`${bodyIndent}text: "${escapeSflString(options.text)}"`);
7433
+ }
7434
+ if (options.body) {
7435
+ block.push(...indentBlock(options.body, bodyIndent));
7436
+ }
7437
+ block.push(`${baseIndent}}`);
7438
+ return block;
7439
+ }
7440
+
7441
+ // src/runtime/interaction/sflPatch/linkPatch/insertionTarget.ts
7442
+ function resolveLinkInsertionTarget(sfl, lines, options) {
7443
+ if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7444
+ return null;
7445
+ }
7446
+ const canvasObjects = chunkCXL254BT_cjs.sflToCanvasState(sfl).canvasObjects;
7447
+ const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7448
+ const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7449
+ if (!hasSource || !hasTarget) {
7450
+ return null;
7451
+ }
7452
+ return findFirstBlockOfKind(lines, "Model");
7453
+ }
7454
+
7455
+ // src/runtime/interaction/sflPatch/linkPatch/insert.ts
7456
+ function insertLink(sfl, options) {
7457
+ const lines = sfl.split("\n");
7458
+ const modelRange = resolveLinkInsertionTarget(sfl, lines, options);
7459
+ if (!modelRange) {
7460
+ return sfl;
7461
+ }
7462
+ insertBlockLines(lines, modelRange, (baseIndent) => buildLinkBlock(options, baseIndent));
7463
+ return lines.join("\n");
7464
+ }
7465
+
7466
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/shapeDuplicate.ts
7467
+ var SHAPE_GAP = 200;
7468
+ var OPPOSITE_SIDE = { top: "bottom", right: "left", bottom: "top", left: "right" };
7469
+ var SHAPE_STYLE_KEYS = [
7470
+ "fill",
7471
+ "stroke",
7472
+ "strokeWidth",
7473
+ "strokeStyle",
7474
+ "textColor",
7475
+ "fontSize",
7476
+ "fontFamily",
7477
+ "cornerRadius",
7478
+ "opacity",
7479
+ "shadow"
7480
+ ];
7481
+ var LINK_STYLE_KEYS = ["stroke", "strokeWidth", "strokeStyle", "markerStart", "markerEnd", "route"];
7482
+ function shapeOffset(side, object, gap) {
7483
+ switch (side) {
7484
+ case "top":
7485
+ return { dx: 0, dy: -(object.height + gap) };
7486
+ case "bottom":
7487
+ return { dx: 0, dy: object.height + gap };
7488
+ case "left":
7489
+ return { dx: -(object.width + gap), dy: 0 };
7490
+ case "right":
7491
+ return { dx: object.width + gap, dy: 0 };
7492
+ }
7493
+ }
7494
+ function copyOf2(object, side, options) {
7495
+ const { dx, dy } = shapeOffset(side, object, options.gap ?? SHAPE_GAP);
7496
+ const shape = object.shapeType ? [` shape: ${object.shapeType}`] : [];
7497
+ return {
7498
+ id: options.shapeId,
7499
+ kind: "shape",
7500
+ text: object.text ?? "",
7501
+ x: object.x + dx,
7502
+ y: object.y + dy,
7503
+ width: object.width,
7504
+ height: object.height,
7505
+ parentId: object.parentFrameId,
7506
+ body: styleBlock(object.props, SHAPE_STYLE_KEYS, shape)
7507
+ };
7508
+ }
7509
+ function linkStyleReference(links, objectId) {
7510
+ return links.find((link) => link.fromId === objectId) ?? links[0];
7511
+ }
7512
+ function linkToCopy(object, links, side, options) {
7513
+ return {
7514
+ id: options.linkId,
7515
+ fromId: object.id,
7516
+ toId: options.shapeId,
7517
+ fromSide: side,
7518
+ toSide: OPPOSITE_SIDE[side],
7519
+ body: styleBlock(linkStyleReference(links, object.id)?.props, LINK_STYLE_KEYS)
7520
+ };
7521
+ }
7522
+ function duplicateShape(sfl, input) {
7523
+ const { object, links, side, options } = input;
7524
+ const withCopy = insertCanvasObject(sfl, copyOf2(object, side, options));
7525
+ return insertLink(withCopy, linkToCopy(object, links, side, options));
7526
+ }
7527
+
7528
+ // src/runtime/interaction/sflPatch/changes/quickDuplicateChange.ts
7529
+ function applyQuickDuplicateChange(sfl, event, options) {
7530
+ const state = chunkCXL254BT_cjs.sflToCanvasState(sfl);
7531
+ const object = state.canvasObjects.find((candidate) => candidate.id === event.owner.id);
7532
+ if (!object) {
7533
+ return sfl;
7534
+ }
7535
+ const { direction } = event;
7536
+ if (isIsometricDirection2(direction)) {
7537
+ return chunkCXL254BT_cjs.isIsometricObject(object) ? duplicateIsometricObject(sfl, object, direction, options) : sfl;
7538
+ }
7539
+ return chunkCXL254BT_cjs.isIsometricObject(object) ? sfl : duplicateShape(sfl, { object, links: state.links, side: direction, options });
7540
+ }
7541
+
7291
7542
  // src/runtime/interaction/sflPatch/changes/resizeChange.ts
7292
7543
  function applyResizeChange(sfl, event) {
7293
7544
  const state = chunkCXL254BT_cjs.sflToCanvasState(sfl);
@@ -7412,89 +7663,6 @@ function applyTextChange(sfl, event) {
7412
7663
  return lines.join("\n");
7413
7664
  }
7414
7665
 
7415
- // src/runtime/interaction/sflPatch/connectionPatch/blockText.ts
7416
- function buildConnectionBlock(options, baseIndent) {
7417
- const bodyIndent = `${baseIndent} `;
7418
- const block = [
7419
- `${baseIndent}Connection ${options.id} {`,
7420
- `${bodyIndent}from: ${options.fromId}`,
7421
- `${bodyIndent}to: ${options.toId}`,
7422
- `${bodyIndent}fromFace: "${options.fromFace}"`,
7423
- `${bodyIndent}toFace: "${options.toFace}"`
7424
- ];
7425
- if (options.body) {
7426
- block.push(...indentBlock(options.body, bodyIndent));
7427
- }
7428
- block.push(`${baseIndent}}`);
7429
- return block;
7430
- }
7431
-
7432
- // src/runtime/interaction/sflPatch/connectionPatch/insertionTarget.ts
7433
- function resolveConnectionInsertionTarget(sfl, lines, options) {
7434
- if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7435
- return null;
7436
- }
7437
- const canvasObjects = chunkCXL254BT_cjs.sflToCanvasState(sfl).canvasObjects;
7438
- const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7439
- const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7440
- if (!hasSource || !hasTarget) {
7441
- return null;
7442
- }
7443
- return findFirstBlockOfKind(lines, "Model");
7444
- }
7445
-
7446
- // src/runtime/interaction/sflPatch/connectionPatch/insert.ts
7447
- function insertConnection(sfl, options) {
7448
- const lines = sfl.split("\n");
7449
- const modelRange = resolveConnectionInsertionTarget(sfl, lines, options);
7450
- if (!modelRange) {
7451
- return sfl;
7452
- }
7453
- insertBlockLines(lines, modelRange, (baseIndent) => buildConnectionBlock(options, baseIndent));
7454
- return lines.join("\n");
7455
- }
7456
-
7457
- // src/runtime/interaction/sflPatch/linkPatch/blockText.ts
7458
- function buildLinkBlock(options, baseIndent) {
7459
- const bodyIndent = `${baseIndent} `;
7460
- const from = formatLinkEndpoint({ id: options.fromId, side: options.fromSide, fraction: options.fromFraction });
7461
- const to = formatLinkEndpoint({ id: options.toId, side: options.toSide, fraction: options.toFraction });
7462
- const block = [`${baseIndent}Link ${options.id} {`, `${bodyIndent}from: ${from}`, `${bodyIndent}to: ${to}`];
7463
- if (options.text !== void 0) {
7464
- block.push(`${bodyIndent}text: "${escapeSflString(options.text)}"`);
7465
- }
7466
- if (options.body) {
7467
- block.push(...indentBlock(options.body, bodyIndent));
7468
- }
7469
- block.push(`${baseIndent}}`);
7470
- return block;
7471
- }
7472
-
7473
- // src/runtime/interaction/sflPatch/linkPatch/insertionTarget.ts
7474
- function resolveLinkInsertionTarget(sfl, lines, options) {
7475
- if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7476
- return null;
7477
- }
7478
- const canvasObjects = chunkCXL254BT_cjs.sflToCanvasState(sfl).canvasObjects;
7479
- const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7480
- const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7481
- if (!hasSource || !hasTarget) {
7482
- return null;
7483
- }
7484
- return findFirstBlockOfKind(lines, "Model");
7485
- }
7486
-
7487
- // src/runtime/interaction/sflPatch/linkPatch/insert.ts
7488
- function insertLink(sfl, options) {
7489
- const lines = sfl.split("\n");
7490
- const modelRange = resolveLinkInsertionTarget(sfl, lines, options);
7491
- if (!modelRange) {
7492
- return sfl;
7493
- }
7494
- insertBlockLines(lines, modelRange, (baseIndent) => buildLinkBlock(options, baseIndent));
7495
- return lines.join("\n");
7496
- }
7497
-
7498
7666
  // src/runtime/interaction/sflPatch/linkPatch/remove.ts
7499
7667
  function removeLink(sfl, id) {
7500
7668
  const lines = sfl.split("\n");
@@ -7565,6 +7733,7 @@ exports.applyLinkSegmentChange = applyLinkSegmentChange;
7565
7733
  exports.applyMoveChange = applyMoveChange;
7566
7734
  exports.applyPathPointChange = applyPathPointChange;
7567
7735
  exports.applyPropertyChange = applyPropertyChange;
7736
+ exports.applyQuickDuplicateChange = applyQuickDuplicateChange;
7568
7737
  exports.applyResizeChange = applyResizeChange;
7569
7738
  exports.applySankeyNodeMoveChange = applySankeyNodeMoveChange;
7570
7739
  exports.applySankeyNodeRenameChange = applySankeyNodeRenameChange;