@inditextech/weave-sdk 5.5.0 → 5.7.0-SNAPSHOT.595.1

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/sdk.js CHANGED
@@ -5937,6 +5937,7 @@ const MOVE_TOOL_STATE = {
5937
5937
  };
5938
5938
  //#endregion
5939
5939
  //#region src/nodes/node.ts
5940
+ const nodeInstanceListeners = /* @__PURE__ */ new WeakMap();
5940
5941
  const augmentKonvaNodeClass = (config) => {
5941
5942
  const { transform } = config ?? {};
5942
5943
  Konva.Node.prototype.getTransformerProperties = function() {
@@ -5952,6 +5953,8 @@ const augmentKonvaNodeClass = (config) => {
5952
5953
  Konva.Node.prototype.triggerCrop = function() {};
5953
5954
  Konva.Node.prototype.closeCrop = function() {};
5954
5955
  Konva.Node.prototype.resetCrop = function() {};
5956
+ Konva.Node.prototype.flipHorizontal = function() {};
5957
+ Konva.Node.prototype.flipVertical = function() {};
5955
5958
  Konva.Node.prototype.dblClick = function() {};
5956
5959
  Konva.Node.prototype.click = function() {};
5957
5960
  Konva.Node.prototype.allowedAnchors = function() {
@@ -6037,6 +6040,8 @@ var WeaveNode = class {
6037
6040
  };
6038
6041
  node.updatePosition = function() {};
6039
6042
  node.resetCrop = function() {};
6043
+ node.flipHorizontal = function() {};
6044
+ node.flipVertical = function() {};
6040
6045
  node.handleMouseover = function() {};
6041
6046
  node.handleMouseout = function() {};
6042
6047
  node.handleSelectNode = function() {};
@@ -6207,7 +6212,7 @@ var WeaveNode = class {
6207
6212
  node.draggable(false);
6208
6213
  };
6209
6214
  if (node.getAttrs().locked ?? false) {
6210
- this.instance.removeEventListener("onNodesChange", handleNodesChange);
6215
+ this.removeNodeInstanceListeners(node, false);
6211
6216
  node.off("transformstart");
6212
6217
  node.off("transform");
6213
6218
  node.off("transformend");
@@ -6217,7 +6222,7 @@ var WeaveNode = class {
6217
6222
  node.off("pointerover");
6218
6223
  node.off("pointerleave");
6219
6224
  } else {
6220
- this.instance.addEventListener("onNodesChange", handleNodesChange);
6225
+ this.setNodeInstanceListener(node, "onNodesChange", handleNodesChange);
6221
6226
  node.off("transformstart");
6222
6227
  node.on("transformstart", (e) => {
6223
6228
  if (e.target.getAttrs().strokeScaleEnabled !== false) {
@@ -6276,7 +6281,7 @@ var WeaveNode = class {
6276
6281
  });
6277
6282
  const stage = this.instance.getStage();
6278
6283
  let originalPosition = null;
6279
- this.instance.addEventListener("onSelectionState", (state) => {
6284
+ this.setNodeInstanceListener(node, "onSelectionState", (state) => {
6280
6285
  const selectedNodes = this.getSelectionPlugin()?.getSelectedNodes() ?? [];
6281
6286
  if (!state && selectedNodes?.some((n) => n.getAttrs().id === node.getAttrs().id)) originalPosition = node.getAbsolutePosition();
6282
6287
  });
@@ -6592,8 +6597,39 @@ var WeaveNode = class {
6592
6597
  async onRegister() {}
6593
6598
  onAdd(nodeInstance) {}
6594
6599
  onDestroy(nodeInstance) {
6600
+ this.removeNodeInstanceListeners(nodeInstance);
6595
6601
  nodeInstance.destroy();
6596
6602
  }
6603
+ /**
6604
+ * Registers a Weave instance listener on behalf of a node. Only one listener
6605
+ * is kept per node and event: registering the same event again for the same
6606
+ * node replaces the previous listener, including the ones registered by
6607
+ * setupDefaultNodeEvents (onNodesChange, onSelectionState).
6608
+ */
6609
+ setNodeInstanceListener(node, event, callback) {
6610
+ let listeners = nodeInstanceListeners.get(node);
6611
+ if (!listeners) {
6612
+ listeners = /* @__PURE__ */ new Map();
6613
+ nodeInstanceListeners.set(node, listeners);
6614
+ }
6615
+ const previous = listeners.get(event);
6616
+ if (previous) this.instance.removeEventListener(event, previous);
6617
+ listeners.set(event, callback);
6618
+ this.instance.addEventListener(event, callback);
6619
+ }
6620
+ /**
6621
+ * Removes the listeners registered with setNodeInstanceListener for the node
6622
+ * and, unless disabled, for its descendants.
6623
+ */
6624
+ removeNodeInstanceListeners(node, includeDescendants = true) {
6625
+ const nodes = includeDescendants && node instanceof Konva.Container ? [node, ...node.find(() => true)] : [node];
6626
+ for (const actNode of nodes) {
6627
+ const listeners = nodeInstanceListeners.get(actNode);
6628
+ if (!listeners) continue;
6629
+ for (const [event, callback] of listeners) this.instance.removeEventListener(event, callback);
6630
+ nodeInstanceListeners.delete(actNode);
6631
+ }
6632
+ }
6597
6633
  onDestroyInstance() {}
6598
6634
  serialize(instance) {
6599
6635
  const attrs = instance.getAttrs();
@@ -9466,7 +9502,7 @@ var WeaveRegisterManager = class {
9466
9502
  };
9467
9503
  //#endregion
9468
9504
  //#region package.json
9469
- var version = "5.5.0";
9505
+ var version = "5.7.0-SNAPSHOT.595.1";
9470
9506
  //#endregion
9471
9507
  //#region src/managers/setup.ts
9472
9508
  var WeaveSetupManager = class {
@@ -11187,6 +11223,9 @@ var Weave = class {
11187
11223
  getBoundingBox(nodes, config) {
11188
11224
  return getBoundingBox(nodes, config);
11189
11225
  }
11226
+ flipImage(node, options) {
11227
+ this.getNodeHandler("image")?.flipImage(node, options);
11228
+ }
11190
11229
  stateTransactional(callback, origin) {
11191
11230
  this.stateManager.stateTransactional(callback, origin);
11192
11231
  }
@@ -14261,99 +14300,2084 @@ var WeaveTextNode = class extends WeaveNode {
14261
14300
  };
14262
14301
  }
14263
14302
  static addNodeState(defaultNodeState, props) {
14264
- return mergeExceptArrays(defaultNodeState, { props: {
14265
- x: props.x,
14266
- y: props.y,
14267
- width: props.width,
14268
- ...props.height && { height: props.height },
14269
- FontFamily: props.fontFamily,
14270
- fontSize: props.fontSize,
14271
- fontStyle: props.fontStyle,
14272
- fontVariant: props.fontVariant,
14273
- textDecoration: props.textDecoration,
14274
- letterSpacing: props.letterSpacing,
14275
- lineHeight: props.lineHeight,
14276
- align: props.align,
14277
- verticalAlign: props.verticalAlign,
14278
- rotation: props.rotation,
14279
- fill: props.fill,
14280
- text: props.text,
14281
- layout: props.layout,
14282
- ...props.strokeEnabled && { strokeEnabled: props.strokeEnabled },
14283
- ...props.stroke && { stroke: props.stroke },
14284
- ...props.strokeWidth && { strokeWidth: props.strokeWidth },
14285
- ...props.fillAfterStrokeEnabled && { fillAfterStrokeEnabled: props.fillAfterStrokeEnabled },
14286
- ...props.link && { link: props.link }
14287
- } });
14303
+ return mergeExceptArrays(defaultNodeState, { props: {
14304
+ x: props.x,
14305
+ y: props.y,
14306
+ width: props.width,
14307
+ ...props.height && { height: props.height },
14308
+ FontFamily: props.fontFamily,
14309
+ fontSize: props.fontSize,
14310
+ fontStyle: props.fontStyle,
14311
+ fontVariant: props.fontVariant,
14312
+ textDecoration: props.textDecoration,
14313
+ letterSpacing: props.letterSpacing,
14314
+ lineHeight: props.lineHeight,
14315
+ align: props.align,
14316
+ verticalAlign: props.verticalAlign,
14317
+ rotation: props.rotation,
14318
+ fill: props.fill,
14319
+ text: props.text,
14320
+ layout: props.layout,
14321
+ ...props.strokeEnabled && { strokeEnabled: props.strokeEnabled },
14322
+ ...props.stroke && { stroke: props.stroke },
14323
+ ...props.strokeWidth && { strokeWidth: props.strokeWidth },
14324
+ ...props.fillAfterStrokeEnabled && { fillAfterStrokeEnabled: props.fillAfterStrokeEnabled },
14325
+ ...props.link && { link: props.link }
14326
+ } });
14327
+ }
14328
+ static updateNodeState(prevNodeState, nextProps) {
14329
+ return mergeExceptArrays(prevNodeState, { props: {
14330
+ x: nextProps.x,
14331
+ y: nextProps.y,
14332
+ width: nextProps.width,
14333
+ ...nextProps.height && { height: nextProps.height },
14334
+ FontFamily: nextProps.fontFamily,
14335
+ fontSize: nextProps.fontSize,
14336
+ fontStyle: nextProps.fontStyle,
14337
+ fontVariant: nextProps.fontVariant,
14338
+ textDecoration: nextProps.textDecoration,
14339
+ letterSpacing: nextProps.letterSpacing,
14340
+ lineHeight: nextProps.lineHeight,
14341
+ align: nextProps.align,
14342
+ verticalAlign: nextProps.verticalAlign,
14343
+ rotation: nextProps.rotation,
14344
+ fill: nextProps.fill,
14345
+ text: nextProps.text,
14346
+ layout: nextProps.layout,
14347
+ ...nextProps.strokeEnabled && { strokeEnabled: nextProps.strokeEnabled },
14348
+ ...nextProps.stroke && { stroke: nextProps.stroke },
14349
+ ...nextProps.strokeWidth && { strokeWidth: nextProps.strokeWidth },
14350
+ ...nextProps.fillAfterStrokeEnabled && { fillAfterStrokeEnabled: nextProps.fillAfterStrokeEnabled },
14351
+ ...nextProps.link && { link: nextProps.link }
14352
+ } });
14353
+ }
14354
+ static getSchema() {
14355
+ return super.getSchema().extend({
14356
+ type: z.literal(WEAVE_TEXT_NODE_TYPE).describe(`Type of the node, for a text node it will always be "${WEAVE_TEXT_NODE_TYPE}"`),
14357
+ props: z.object({
14358
+ nodeType: z.literal(WEAVE_TEXT_NODE_TYPE).describe(`Type of the node, for a text node it will always be "${WEAVE_TEXT_NODE_TYPE}"`),
14359
+ width: z.number().describe("Width of the text in pixels"),
14360
+ height: z.number().optional().describe("Height of the text in pixels. Optional if layout is auto-height or smart."),
14361
+ fontFamily: z.string().default("Arial").describe("Font family of the text, e.g. Arial, Helvetica, etc."),
14362
+ fontSize: z.number().default(16).describe("Font size of the text in pixels."),
14363
+ fontStyle: z.string().regex(/^(?:normal|bold|\d+)(?: italic)?$/).default("normal").describe("Font style of the text, can be \"normal\", \"bold\", \"400\", \"italic\" or a combination like \"bold italic\" or \"700 italic\"."),
14364
+ fontVariant: z.enum(["normal", "small-caps"]).describe("Font variant of the text, can be \"normal\" or \"small-caps\"."),
14365
+ textDecoration: z.enum([
14366
+ "line-through",
14367
+ "underline",
14368
+ ""
14369
+ ]).default("").describe("Text decoration can be \"line-through\", \"underline\" or empty string for none."),
14370
+ letterSpacing: z.number().default(0).describe("Spacing between letters in pixels."),
14371
+ lineHeight: z.number().default(1).describe("Line height of the text, as a multiplier of font size."),
14372
+ align: z.enum([
14373
+ "left",
14374
+ "center",
14375
+ "right",
14376
+ "justify"
14377
+ ]).default("left").describe("Text alignment, can be \"left\", \"center\", \"right\" or \"justify\"."),
14378
+ verticalAlign: z.enum([
14379
+ "top",
14380
+ "middle",
14381
+ "bottom"
14382
+ ]).default("top").describe("Vertical alignment of the text, can be 'top', 'middle' or 'bottom'."),
14383
+ fill: z.string().default("#000000ff").describe("Fill color of the text in hex format with alpha channel (e.g. #RRGGBBAA)."),
14384
+ text: z.string().default("text").describe("The actual text content of the node."),
14385
+ link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional().describe("External URL the whole text of the node links to. When set, the text is forced underlined and coloured with the configured link colours regardless of fill/textDecoration, is clickable, and opens the URL in a new tab. Only http(s) URLs are accepted. Omit/remove to make the node plain text again."),
14386
+ linkPreviousFill: z.string().optional().describe("Internal bookkeeping: the fill colour the node had right before a link was set, captured automatically so it can be restored when the link is removed. Managed automatically — do not set directly."),
14387
+ linkPreviousTextDecoration: z.string().optional().describe("Internal bookkeeping: the textDecoration the node had right before a link was set, captured automatically so it can be restored when the link is removed. Managed automatically — do not set directly."),
14388
+ strokeEnabled: z.boolean().default(false).describe("Whether the text outline is enabled."),
14389
+ stroke: z.string().optional().default("#d6d6d6").describe("Color of the text outline in hex format with alpha channel (e.g. #RRGGBBAA)."),
14390
+ strokeWidth: z.number().optional().default(2).describe("Width of the text outline in pixels."),
14391
+ strokeScaleEnabled: z.boolean().default(true).describe("Whether the stroke width should scale when the node is scaled."),
14392
+ fillAfterStrokeEnabled: z.boolean().default(true).describe("Whether the fill should be drawn after the stroke. If false, the stroke will be drawn on top of the fill."),
14393
+ layout: z.enum(TEXT_LAYOUT).default(TEXT_LAYOUT.SMART).describe("Layout mode of the text node. Can be:\n- 'fixed': the text node will have fixed width and height, and the text will be scaled to fit the node.\n- 'auto-height': the width of the text node will be fixed, but the height will adjust to fit the text content.\n- 'auto-all': both width and height of the text node will adjust to fit the text content.\n- 'smart': the text node will try to adjust its size based on the content and layout, but it will not exceed the initial width and height set on the node.")
14394
+ })
14395
+ });
14396
+ }
14397
+ };
14398
+ //#endregion
14399
+ //#region src/nodes/text-v2/model.ts
14400
+ function isEmptyMarks(m) {
14401
+ return !m || Object.keys(m).length === 0;
14402
+ }
14403
+ function sameMarks(a, b) {
14404
+ if (isEmptyMarks(a) && isEmptyMarks(b)) return true;
14405
+ if (isEmptyMarks(a) || isEmptyMarks(b)) return false;
14406
+ const ka = Object.keys(a);
14407
+ const kb = Object.keys(b);
14408
+ return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
14409
+ }
14410
+ /** Merge adjacent runs with equal marks and drop empty runs (keeps >= 0). */
14411
+ function normalizeRuns(runs) {
14412
+ const out = [];
14413
+ for (const run of runs) {
14414
+ if (run.t === "") continue;
14415
+ const last = out[out.length - 1];
14416
+ if (last && sameMarks(last.m, run.m)) last.t += run.t;
14417
+ else out.push({
14418
+ t: run.t,
14419
+ ...isEmptyMarks(run.m) ? {} : { m: run.m }
14420
+ });
14421
+ }
14422
+ return out;
14423
+ }
14424
+ /**
14425
+ * Synthesize blocks from legacy plain `text`. One run per paragraph; a
14426
+ * legacy whole-text `link` becomes a full-range `link` mark.
14427
+ */
14428
+ function synthesizeBlocks(text, link) {
14429
+ return (text ?? "").split("\n").map((paragraph) => ({ runs: paragraph === "" ? [] : [{
14430
+ t: paragraph,
14431
+ ...link ? { m: { link } } : {}
14432
+ }] }));
14433
+ }
14434
+ /** Resolve blocks of a node: stored `blocks` or lazily synthesized (no write). */
14435
+ function resolveBlocks(attrs) {
14436
+ if (Array.isArray(attrs.blocks) && attrs.blocks.length > 0) return attrs.blocks;
14437
+ return synthesizeBlocks(attrs.text ?? "", attrs.link);
14438
+ }
14439
+ function blockText(block) {
14440
+ return block.runs.map((r) => r.t).join("");
14441
+ }
14442
+ /** Derived mirror for the `text` attribute. */
14443
+ function blocksToText(blocks) {
14444
+ return blocks.map(blockText).join("\n");
14445
+ }
14446
+ /** Slice runs of a block between local offsets [from, to). */
14447
+ function sliceRuns(runs, from, to) {
14448
+ const out = [];
14449
+ let pos = 0;
14450
+ for (const run of runs) {
14451
+ const start = pos;
14452
+ const end = pos + run.t.length;
14453
+ pos = end;
14454
+ const s = Math.max(from, start);
14455
+ const e = Math.min(to, end);
14456
+ if (s < e) out.push({
14457
+ t: run.t.slice(s - start, e - start),
14458
+ m: run.m
14459
+ });
14460
+ }
14461
+ return out;
14462
+ }
14463
+ /** Marks of the run at (or immediately before) a local offset. */
14464
+ function marksAt(runs, offset) {
14465
+ let pos = 0;
14466
+ let found;
14467
+ for (const run of runs) {
14468
+ const end = pos + run.t.length;
14469
+ if (offset > pos && offset <= end) return run.m;
14470
+ if (offset === pos && !found) found = run.m;
14471
+ pos = end;
14472
+ }
14473
+ return found;
14474
+ }
14475
+ /**
14476
+ * Replace the global range [start, end) (newline counts as 1 char between
14477
+ * blocks) with plain `insert`. Marks on untouched text are preserved; the
14478
+ * inserted text inherits marks of the character before `start`.
14479
+ */
14480
+ function replaceRange(blocks, start, end, insert) {
14481
+ const total = blocksToText(blocks).length;
14482
+ start = Math.max(0, Math.min(start, total));
14483
+ end = Math.max(start, Math.min(end, total));
14484
+ let before = [];
14485
+ let after = [];
14486
+ const prefixBlocks = [];
14487
+ const suffixBlocks = [];
14488
+ let inheritedMarks;
14489
+ let pos = 0;
14490
+ blocks.forEach((block, i) => {
14491
+ const len = blockText(block).length;
14492
+ const bStart = pos;
14493
+ const bEnd = pos + len;
14494
+ pos = bEnd + 1;
14495
+ if (bEnd < start) prefixBlocks.push(block);
14496
+ else if (bStart > end) suffixBlocks.push(block);
14497
+ else {
14498
+ if (start >= bStart && start <= bEnd) {
14499
+ const local = start - bStart;
14500
+ before = sliceRuns(block.runs, 0, local);
14501
+ inheritedMarks = marksAt(block.runs, local);
14502
+ }
14503
+ if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
14504
+ }
14505
+ });
14506
+ const lines = insert.split("\n");
14507
+ const mk = (t) => t === "" ? [] : [{
14508
+ t,
14509
+ ...isEmptyMarks(inheritedMarks) ? {} : { m: inheritedMarks }
14510
+ }];
14511
+ const middle = [];
14512
+ lines.forEach((line, idx) => {
14513
+ let runs = mk(line);
14514
+ if (idx === 0) runs = [...before, ...runs];
14515
+ if (idx === lines.length - 1) runs = [...runs, ...after];
14516
+ middle.push({ runs: normalizeRuns(runs) });
14517
+ });
14518
+ return [
14519
+ ...prefixBlocks,
14520
+ ...middle,
14521
+ ...suffixBlocks
14522
+ ];
14523
+ }
14524
+ //#endregion
14525
+ //#region src/nodes/text-v2/layout/engine.ts
14526
+ /** Quote multi-word families like `Konva` does so the font shorthand parses. */
14527
+ function normalizeFontFamily(fontFamily) {
14528
+ return fontFamily.split(",").map((family) => {
14529
+ family = family.trim();
14530
+ const hasSpace = family.includes(" ");
14531
+ const hasQuotes = family.includes("\"") || family.includes("'");
14532
+ return hasSpace && !hasQuotes ? `"${family}"` : family;
14533
+ }).join(", ");
14534
+ }
14535
+ function fontString(s) {
14536
+ return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14537
+ }
14538
+ let sharedContext = null;
14539
+ /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
14540
+ const canvasMeasurer = (font, text) => {
14541
+ sharedContext ??= Konva.Util.createCanvasElement().getContext("2d");
14542
+ sharedContext.font = font;
14543
+ return sharedContext.measureText(text).width;
14544
+ };
14545
+ const graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
14546
+ function splitGraphemes(text) {
14547
+ if (text === "") return [];
14548
+ if (graphemeSegmenter) return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
14549
+ return Array.from(text);
14550
+ }
14551
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
14552
+ const WS = /\s/;
14553
+ const MAX_CACHE = 2e4;
14554
+ const MAX_CACHED_CHUNK = 256;
14555
+ var TextLayoutEngine = class {
14556
+ measure;
14557
+ cache = /* @__PURE__ */ new Map();
14558
+ paragraphs = [];
14559
+ /** Entries by signature, so paragraphs that shifted index are reused. */
14560
+ pool = /* @__PURE__ */ new Map();
14561
+ lastParagraphsRelaid = 0;
14562
+ constructor(measurer = canvasMeasurer) {
14563
+ this.measure = measurer;
14564
+ }
14565
+ /** Number of paragraphs recomputed by the last `layout()` call. */
14566
+ getLastRelaidCount() {
14567
+ return this.lastParagraphsRelaid;
14568
+ }
14569
+ clearCaches() {
14570
+ this.cache.clear();
14571
+ this.paragraphs = [];
14572
+ this.pool.clear();
14573
+ }
14574
+ paragraphSig(block, style, bounds) {
14575
+ const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14576
+ const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14577
+ return `${fontString(style)}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}|${JSON.stringify(block.runs)}`;
14578
+ }
14579
+ /**
14580
+ * Pre-compute paragraph layouts for `blocks` within a time budget so a
14581
+ * following `layout()` is cheap. Call repeatedly until it returns true.
14582
+ * Lets callers time-slice very large content (e.g. bulk paste).
14583
+ */
14584
+ warm(blocks, style, bounds, budgetMs) {
14585
+ const font = fontString(style);
14586
+ const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14587
+ const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14588
+ const deadline = now() + budgetMs;
14589
+ for (const block of blocks) {
14590
+ const sig = this.paragraphSig(block, style, bounds);
14591
+ if (this.pool.has(sig)) continue;
14592
+ this.pool.set(sig, this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth));
14593
+ if (now() >= deadline) return false;
14594
+ }
14595
+ return true;
14596
+ }
14597
+ measureChunk(font, text, spacing) {
14598
+ if (text === "") return 0;
14599
+ if (text.length > MAX_CACHED_CHUNK) return this.measure(font, text) + text.length * spacing;
14600
+ const key = `${font}\u0000${text}`;
14601
+ let w = this.cache.get(key);
14602
+ if (w === void 0) {
14603
+ w = this.measure(font, text);
14604
+ if (this.cache.size >= MAX_CACHE) this.cache.clear();
14605
+ this.cache.set(key, w);
14606
+ }
14607
+ return w + text.length * spacing;
14608
+ }
14609
+ layout(blocks, style, bounds) {
14610
+ const font = fontString(style);
14611
+ const padding = bounds.padding;
14612
+ const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14613
+ const fixedHeight = bounds.height !== void 0 && !Number.isNaN(bounds.height);
14614
+ const maxWidth = (bounds.width ?? 0) - padding * 2;
14615
+ const maxHeightPx = (bounds.height ?? 0) - padding * 2;
14616
+ const lineHeightPx = style.lineHeight * style.fontSize;
14617
+ const sigBase = `${font}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}`;
14618
+ const prev = this.paragraphs;
14619
+ const next = [];
14620
+ let relaid = 0;
14621
+ const lines = [];
14622
+ let textWidth = 0;
14623
+ let currentHeightPx = 0;
14624
+ let offset = 0;
14625
+ outer: for (let p = 0; p < blocks.length; p++) {
14626
+ const block = blocks[p];
14627
+ const sig = `${sigBase}|${JSON.stringify(block.runs)}`;
14628
+ let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
14629
+ if (!entry) {
14630
+ entry = this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth);
14631
+ relaid++;
14632
+ }
14633
+ next.push(entry);
14634
+ for (const pl of entry.lines) {
14635
+ const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
14636
+ const segments = this.buildSegments(entry, block, pl, font, style);
14637
+ const width = segments.reduce((a, s) => a + s.width, 0);
14638
+ const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
14639
+ lines.push({
14640
+ paragraph: p,
14641
+ start: offset + startChar,
14642
+ end: offset + startChar + text.length,
14643
+ text,
14644
+ width,
14645
+ measuredWidth: pl.measuredWidth,
14646
+ offsetX: 0,
14647
+ y: lines.length * lineHeightPx,
14648
+ segments,
14649
+ lastInParagraph: pl.lastInParagraph
14650
+ });
14651
+ textWidth = Math.max(textWidth, pl.measuredWidth);
14652
+ currentHeightPx += lineHeightPx;
14653
+ if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) break outer;
14654
+ }
14655
+ offset += blockText(block).length + 1;
14656
+ }
14657
+ this.paragraphs = next;
14658
+ this.pool = new Map(next.map((e) => [e.sig, e]));
14659
+ this.lastParagraphsRelaid = relaid;
14660
+ const width = fixedWidth ? bounds.width : textWidth + padding * 2;
14661
+ const height = fixedHeight ? bounds.height : lines.length * lineHeightPx + padding * 2;
14662
+ if (style.align !== "left") for (const line of lines) {
14663
+ const free = width - line.width - padding * 2;
14664
+ line.offsetX = style.align === "center" ? free / 2 : free;
14665
+ }
14666
+ return {
14667
+ lines,
14668
+ textWidth,
14669
+ textHeight: style.fontSize,
14670
+ lineHeightPx,
14671
+ width,
14672
+ height
14673
+ };
14674
+ }
14675
+ rangeWidth(entry, runs, from, to, font, spacing) {
14676
+ let width = 0;
14677
+ let i = from;
14678
+ while (i < to) {
14679
+ const run = entry.runOf[i];
14680
+ let j = i;
14681
+ while (j < to && entry.runOf[j] === run) j++;
14682
+ width += this.measureChunk(font, entry.graphemes.slice(i, j).join(""), spacing);
14683
+ i = j;
14684
+ }
14685
+ return width;
14686
+ }
14687
+ layoutParagraph(block, sig, font, style, fixedWidth, maxWidth) {
14688
+ const graphemes = [];
14689
+ const runOf = [];
14690
+ block.runs.forEach((run, ri) => {
14691
+ for (const g of splitGraphemes(run.t)) {
14692
+ graphemes.push(g);
14693
+ runOf.push(ri);
14694
+ }
14695
+ });
14696
+ const entry = {
14697
+ sig,
14698
+ graphemes,
14699
+ runOf,
14700
+ lines: []
14701
+ };
14702
+ const n = graphemes.length;
14703
+ const spacing = style.letterSpacing;
14704
+ const width = (a, b) => this.rangeWidth(entry, block.runs, a, b, font, spacing);
14705
+ const shouldWrap = style.wrap !== "none";
14706
+ const wrapAtWord = style.wrap !== "char" && shouldWrap;
14707
+ const whole = width(0, n);
14708
+ if (!fixedWidth || !shouldWrap || whole <= maxWidth || n === 0) {
14709
+ entry.lines.push({
14710
+ startG: 0,
14711
+ endG: n,
14712
+ measuredWidth: whole,
14713
+ lastInParagraph: true
14714
+ });
14715
+ return entry;
14716
+ }
14717
+ let start = 0;
14718
+ while (start < n) {
14719
+ let low = 0;
14720
+ let high = n - start;
14721
+ let probe = Math.min(high, 32);
14722
+ while (probe < high && width(start, start + probe) <= maxWidth) probe = Math.min(high, probe * 2);
14723
+ high = probe;
14724
+ let matchLen = 0;
14725
+ let matchWidth = 0;
14726
+ while (low < high) {
14727
+ const mid = low + high >>> 1;
14728
+ const w = width(start, start + mid + 1);
14729
+ if (w <= maxWidth) {
14730
+ low = mid + 1;
14731
+ matchLen = mid + 1;
14732
+ matchWidth = w;
14733
+ } else high = mid;
14734
+ }
14735
+ if (matchLen === 0) {
14736
+ matchLen = 1;
14737
+ matchWidth = width(start, start + 1);
14738
+ }
14739
+ if (start + matchLen >= n) {
14740
+ entry.lines.push({
14741
+ startG: start,
14742
+ endG: n,
14743
+ measuredWidth: matchWidth,
14744
+ lastInParagraph: true
14745
+ });
14746
+ return entry;
14747
+ }
14748
+ if (wrapAtWord) {
14749
+ const nextChar = graphemes[start + matchLen];
14750
+ const nextIsSpaceOrDash = nextChar === " " || nextChar === "-";
14751
+ let wrapIndex;
14752
+ if (nextIsSpaceOrDash && matchWidth <= maxWidth) wrapIndex = matchLen;
14753
+ else {
14754
+ const seg = graphemes.slice(start, start + matchLen);
14755
+ wrapIndex = Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
14756
+ }
14757
+ if (wrapIndex > 0) {
14758
+ matchLen = wrapIndex;
14759
+ matchWidth = width(start, start + matchLen);
14760
+ }
14761
+ }
14762
+ let end = start + matchLen;
14763
+ let trimmedEnd = end;
14764
+ while (trimmedEnd > start && WS.test(graphemes[trimmedEnd - 1])) trimmedEnd--;
14765
+ entry.lines.push({
14766
+ startG: start,
14767
+ endG: trimmedEnd,
14768
+ measuredWidth: matchWidth,
14769
+ lastInParagraph: false
14770
+ });
14771
+ while (end < n && WS.test(graphemes[end])) end++;
14772
+ start = end;
14773
+ }
14774
+ const last = entry.lines[entry.lines.length - 1];
14775
+ if (last) last.lastInParagraph = true;
14776
+ return entry;
14777
+ }
14778
+ buildSegments(entry, block, pl, font, style) {
14779
+ const segments = [];
14780
+ let x = 0;
14781
+ let i = pl.startG;
14782
+ while (i < pl.endG) {
14783
+ const run = entry.runOf[i];
14784
+ let j = i;
14785
+ while (j < pl.endG && entry.runOf[j] === run) j++;
14786
+ const t = entry.graphemes.slice(i, j).join("");
14787
+ const width = this.measureChunk(font, t, style.letterSpacing);
14788
+ segments.push({
14789
+ t,
14790
+ m: block.runs[run].m,
14791
+ x,
14792
+ width
14793
+ });
14794
+ x += width;
14795
+ i = j;
14796
+ }
14797
+ return segments;
14798
+ }
14799
+ };
14800
+ //#endregion
14801
+ //#region src/nodes/text-v2/shape.ts
14802
+ const LAYOUT_ATTRS = [
14803
+ "text",
14804
+ "blocks",
14805
+ "link",
14806
+ "fontSize",
14807
+ "fontFamily",
14808
+ "fontStyle",
14809
+ "fontVariant",
14810
+ "lineHeight",
14811
+ "letterSpacing",
14812
+ "wrap",
14813
+ "align",
14814
+ "padding",
14815
+ "width",
14816
+ "height"
14817
+ ];
14818
+ /**
14819
+ * Canvas shape that lays out and paints blocks/runs text with the
14820
+ * `TextLayoutEngine`. Exposes Konva.Text-compatible getters/setters so
14821
+ * code written against `Konva.Text` keeps working.
14822
+ */
14823
+ var WeaveTextV2Shape = class extends Konva.Shape {
14824
+ engine = new TextLayoutEngine();
14825
+ naturalEngine = new TextLayoutEngine();
14826
+ layoutCache = null;
14827
+ blocksKey = {};
14828
+ blocksCache = [];
14829
+ viewport = null;
14830
+ constructor(config) {
14831
+ super(config);
14832
+ this.className = "WeaveTextV2Shape";
14833
+ this.on(LAYOUT_ATTRS.map((a) => `${a}Change.konva`).join(" "), () => {
14834
+ this.layoutCache = null;
14835
+ });
14836
+ this.sceneFunc(this._paint);
14837
+ this.hitFunc(this._hit);
14838
+ }
14839
+ text(value) {
14840
+ if (value === void 0) return typeof this.attrs.text === "string" ? this.attrs.text : blocksToText(this.getBlocks());
14841
+ return this.setText(value);
14842
+ }
14843
+ setText(value) {
14844
+ this._setAttr("blocks", void 0);
14845
+ this._setAttr("text", value == null ? "" : String(value));
14846
+ return this;
14847
+ }
14848
+ /** Resolved blocks: stored or lazily synthesized (never written back). */
14849
+ getBlocks() {
14850
+ const k = this.blocksKey;
14851
+ const { blocks, text, link } = this.attrs;
14852
+ if (k.b !== blocks || k.t !== text || k.l !== link) {
14853
+ this.blocksCache = resolveBlocks({
14854
+ blocks,
14855
+ text,
14856
+ link
14857
+ });
14858
+ this.blocksKey = {
14859
+ b: blocks,
14860
+ t: text,
14861
+ l: link
14862
+ };
14863
+ }
14864
+ return this.blocksCache;
14865
+ }
14866
+ /** Replace content with blocks, keeping the `text` mirror in sync. */
14867
+ setBlocks(blocks) {
14868
+ this._setAttr("blocks", blocks);
14869
+ this._setAttr("text", blocksToText(blocks));
14870
+ return this;
14871
+ }
14872
+ fontSize(v) {
14873
+ return this.accessor("fontSize", 12, v);
14874
+ }
14875
+ fontFamily(v) {
14876
+ return this.accessor("fontFamily", "Arial", v);
14877
+ }
14878
+ fontStyle(v) {
14879
+ return this.accessor("fontStyle", "normal", v);
14880
+ }
14881
+ fontVariant(v) {
14882
+ return this.accessor("fontVariant", "normal", v);
14883
+ }
14884
+ lineHeight(v) {
14885
+ return this.accessor("lineHeight", 1, v);
14886
+ }
14887
+ letterSpacing(v) {
14888
+ return this.accessor("letterSpacing", 0, v);
14889
+ }
14890
+ padding(v) {
14891
+ return this.accessor("padding", 0, v);
14892
+ }
14893
+ align(v) {
14894
+ return this.accessor("align", "left", v);
14895
+ }
14896
+ verticalAlign(v) {
14897
+ return this.accessor("verticalAlign", "top", v);
14898
+ }
14899
+ wrap(v) {
14900
+ return this.accessor("wrap", "word", v);
14901
+ }
14902
+ textDecoration(v) {
14903
+ return this.accessor("textDecoration", "", v);
14904
+ }
14905
+ accessor(name, fallback, value) {
14906
+ if (value === void 0) return this.attrs[name] ?? fallback;
14907
+ this.setAttr(name, value);
14908
+ return this;
14909
+ }
14910
+ getTextWidth() {
14911
+ return this.ensureLayout().textWidth;
14912
+ }
14913
+ getTextHeight() {
14914
+ return this.fontSize();
14915
+ }
14916
+ measureSize(text) {
14917
+ return {
14918
+ width: canvasMeasurer(fontString(this.style()), text),
14919
+ height: this.fontSize()
14920
+ };
14921
+ }
14922
+ getWidth() {
14923
+ const w = this.attrs.width;
14924
+ return w === void 0 || w === "auto" || Number.isNaN(w) ? this.ensureLayout().width : w;
14925
+ }
14926
+ getHeight() {
14927
+ const h = this.attrs.height;
14928
+ return h === void 0 || h === "auto" || Number.isNaN(h) ? this.ensureLayout().height : h;
14929
+ }
14930
+ style() {
14931
+ return {
14932
+ fontSize: this.fontSize(),
14933
+ fontFamily: this.fontFamily(),
14934
+ fontStyle: this.fontStyle(),
14935
+ fontVariant: this.fontVariant(),
14936
+ lineHeight: this.lineHeight(),
14937
+ letterSpacing: this.letterSpacing(),
14938
+ wrap: this.wrap(),
14939
+ align: this.align()
14940
+ };
14941
+ }
14942
+ /** Lazy, cached layout. Size accessors and painting force it. */
14943
+ ensureLayout() {
14944
+ if (!this.layoutCache) {
14945
+ const w = this.attrs.width;
14946
+ const h = this.attrs.height;
14947
+ const num = (v) => typeof v === "number" && !Number.isNaN(v) ? v : void 0;
14948
+ this.layoutCache = this.engine.layout(this.getBlocks(), this.style(), {
14949
+ width: num(w),
14950
+ height: num(h),
14951
+ padding: this.padding()
14952
+ });
14953
+ }
14954
+ return this.layoutCache;
14955
+ }
14956
+ /** Size the text would take with no width constraint (for auto layouts). */
14957
+ measureNaturalSize() {
14958
+ const out = this.naturalEngine.layout(this.getBlocks(), this.style(), {
14959
+ width: void 0,
14960
+ height: void 0,
14961
+ padding: this.padding()
14962
+ });
14963
+ return {
14964
+ width: out.width,
14965
+ height: out.height
14966
+ };
14967
+ }
14968
+ /** Rendered width of a string with this shape's font and letter spacing. */
14969
+ measureText(text) {
14970
+ return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
14971
+ }
14972
+ /** Offset of the first line inside the box (padding + vertical align). */
14973
+ getContentOffset() {
14974
+ const padding = this.padding();
14975
+ const layout = this.ensureLayout();
14976
+ const used = layout.lines.length * layout.lineHeightPx + padding * 2;
14977
+ const va = this.verticalAlign();
14978
+ let alignY = 0;
14979
+ if (va === "middle") alignY = (this.getHeight() - used) / 2;
14980
+ else if (va === "bottom") alignY = this.getHeight() - used;
14981
+ return {
14982
+ x: padding,
14983
+ y: alignY + padding
14984
+ };
14985
+ }
14986
+ /** Pre-compute layout for candidate blocks within a time budget. */
14987
+ warmLayout(blocks, budgetMs) {
14988
+ const num = (v) => typeof v === "number" && !Number.isNaN(v) ? v : void 0;
14989
+ return this.engine.warm(blocks, this.style(), {
14990
+ width: num(this.attrs.width),
14991
+ height: num(this.attrs.height),
14992
+ padding: this.padding()
14993
+ }, budgetMs);
14994
+ }
14995
+ /** Optional visible y-range in local coordinates, used for line culling. */
14996
+ setViewport(range) {
14997
+ this.viewport = range;
14998
+ }
14999
+ visibleRange() {
15000
+ if (this.viewport) return this.viewport;
15001
+ const stage = this.getStage();
15002
+ if (!stage) return null;
15003
+ const inv = this.getAbsoluteTransform().copy().invert();
15004
+ const w = stage.width();
15005
+ const h = stage.height();
15006
+ const pts = [
15007
+ inv.point({
15008
+ x: 0,
15009
+ y: 0
15010
+ }),
15011
+ inv.point({
15012
+ x: w,
15013
+ y: 0
15014
+ }),
15015
+ inv.point({
15016
+ x: 0,
15017
+ y: h
15018
+ }),
15019
+ inv.point({
15020
+ x: w,
15021
+ y: h
15022
+ })
15023
+ ];
15024
+ return {
15025
+ y1: Math.min(...pts.map((p) => p.y)),
15026
+ y2: Math.max(...pts.map((p) => p.y))
15027
+ };
15028
+ }
15029
+ _paint = (ctx, shape) => {
15030
+ const self = shape;
15031
+ const layout = self.ensureLayout();
15032
+ if (layout.lines.length === 0) return;
15033
+ const fontSize = self.fontSize();
15034
+ const padding = self.padding();
15035
+ const lineHeightPx = layout.lineHeightPx;
15036
+ const decoration = self.textDecoration();
15037
+ const underline = decoration.includes("underline");
15038
+ const lineThrough = decoration.includes("line-through");
15039
+ const spacing = self.letterSpacing();
15040
+ const fill = self.fill();
15041
+ let baseline = "middle";
15042
+ let translateY = lineHeightPx / 2;
15043
+ if (!Konva.legacyTextRendering) {
15044
+ const m = self.measureMetrics();
15045
+ baseline = "alphabetic";
15046
+ translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15047
+ }
15048
+ ctx.setAttr("font", fontString(self.style()));
15049
+ ctx.setAttr("textBaseline", baseline);
15050
+ ctx.setAttr("textAlign", "left");
15051
+ let alignY = 0;
15052
+ const va = self.verticalAlign();
15053
+ const used = layout.lines.length * lineHeightPx + padding * 2;
15054
+ if (va === "middle") alignY = (self.getHeight() - used) / 2;
15055
+ else if (va === "bottom") alignY = self.getHeight() - used;
15056
+ ctx.translate(padding, alignY + padding);
15057
+ const range = self.visibleRange();
15058
+ const paintSelf = self;
15059
+ for (let n = 0; n < layout.lines.length; n++) {
15060
+ const line = layout.lines[n];
15061
+ const y = translateY + n * lineHeightPx;
15062
+ if (range && (alignY + padding + n * lineHeightPx + lineHeightPx < range.y1 - lineHeightPx || alignY + padding + n * lineHeightPx > range.y2 + lineHeightPx)) continue;
15063
+ ctx.save();
15064
+ const baseX = line.offsetX;
15065
+ if (underline) {
15066
+ ctx.save();
15067
+ ctx.beginPath();
15068
+ const uy = y + Math.round(fontSize / 4);
15069
+ ctx.moveTo(baseX, uy);
15070
+ ctx.lineTo(baseX + Math.round(line.width), uy);
15071
+ ctx.lineWidth = fontSize / 15;
15072
+ ctx.strokeStyle = fill;
15073
+ ctx.stroke();
15074
+ ctx.restore();
15075
+ }
15076
+ for (const seg of line.segments) {
15077
+ let x = baseX + seg.x;
15078
+ if (spacing !== 0) for (const ch of Array.from(seg.t)) {
15079
+ paintSelf._partialText = ch;
15080
+ paintSelf._partialTextX = x;
15081
+ paintSelf._partialTextY = y;
15082
+ ctx.fillStrokeShape(self);
15083
+ x += self.measureSize(ch).width + spacing;
15084
+ }
15085
+ else {
15086
+ paintSelf._partialText = seg.t;
15087
+ paintSelf._partialTextX = x;
15088
+ paintSelf._partialTextY = y;
15089
+ ctx.fillStrokeShape(self);
15090
+ }
15091
+ }
15092
+ if (lineThrough) {
15093
+ ctx.save();
15094
+ ctx.beginPath();
15095
+ const ly = y - Math.round(fontSize / 4);
15096
+ ctx.moveTo(baseX, ly);
15097
+ ctx.lineTo(baseX + Math.round(line.width), ly);
15098
+ ctx.lineWidth = fontSize / 15;
15099
+ ctx.strokeStyle = fill;
15100
+ ctx.stroke();
15101
+ ctx.restore();
15102
+ }
15103
+ ctx.restore();
15104
+ }
15105
+ };
15106
+ _hit = (ctx, shape) => {
15107
+ ctx.beginPath();
15108
+ const self = shape;
15109
+ ctx.rect(0, 0, self.getWidth(), self.getHeight());
15110
+ ctx.closePath();
15111
+ ctx.fillStrokeShape(shape);
15112
+ };
15113
+ metricsCache = null;
15114
+ /** Font ascent/descent used for baseline placement (parity with Konva.Text). */
15115
+ measureMetrics() {
15116
+ const font = fontString(this.style());
15117
+ if (this.metricsCache?.key === font) return this.metricsCache;
15118
+ const ctx = Konva.Util.createCanvasElement().getContext("2d");
15119
+ ctx.font = font;
15120
+ const m = ctx.measureText("M");
15121
+ const scale = this.fontSize() / 100;
15122
+ const ascent = m.fontBoundingBoxAscent ?? 91 * scale;
15123
+ const descent = m.fontBoundingBoxDescent ?? 21 * scale;
15124
+ this.metricsCache = {
15125
+ key: font,
15126
+ ascent,
15127
+ descent
15128
+ };
15129
+ return this.metricsCache;
15130
+ }
15131
+ };
15132
+ Object.assign(WeaveTextV2Shape.prototype, {
15133
+ _fillFunc(ctx) {
15134
+ ctx.fillText(this._partialText, this._partialTextX, this._partialTextY);
15135
+ },
15136
+ _strokeFunc(ctx) {
15137
+ ctx.setAttr("miterLimit", 2);
15138
+ ctx.strokeText(this._partialText, this._partialTextX, this._partialTextY);
15139
+ }
15140
+ });
15141
+ //#endregion
15142
+ //#region src/nodes/text-v2/edit/geometry.ts
15143
+ const graphemeSeg = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
15144
+ const wordSeg = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
15145
+ function boundaries(text, seg) {
15146
+ if (!seg) return Array.from({ length: text.length + 1 }, (_, i) => i);
15147
+ const out = [0];
15148
+ for (const s of seg.segment(text)) out.push(s.index + s.segment.length);
15149
+ return out;
15150
+ }
15151
+ function prevGrapheme(text, offset) {
15152
+ if (offset <= 0) return 0;
15153
+ const b = boundaries(text, graphemeSeg);
15154
+ for (let i = b.length - 1; i >= 0; i--) if (b[i] < offset) return b[i];
15155
+ return 0;
15156
+ }
15157
+ function nextGrapheme(text, offset) {
15158
+ if (offset >= text.length) return text.length;
15159
+ const b = boundaries(text, graphemeSeg);
15160
+ for (const v of b) if (v > offset) return v;
15161
+ return text.length;
15162
+ }
15163
+ /** Word segment [start, end) containing (or adjacent to) the offset. */
15164
+ function wordRangeAt(text, offset) {
15165
+ if (text === "") return [0, 0];
15166
+ offset = Math.max(0, Math.min(offset, text.length));
15167
+ if (!wordSeg) {
15168
+ let s = offset;
15169
+ let e = offset;
15170
+ while (s > 0 && /\S/.test(text[s - 1])) s--;
15171
+ while (e < text.length && /\S/.test(text[e])) e++;
15172
+ return [s, e];
15173
+ }
15174
+ let last = [offset, offset];
15175
+ for (const seg of wordSeg.segment(text)) {
15176
+ const s = seg.index;
15177
+ const e = s + seg.segment.length;
15178
+ last = [s, e];
15179
+ if (offset < e) return [s, e];
15180
+ }
15181
+ return last;
15182
+ }
15183
+ /** Paragraph range [start, end) (no trailing newline) containing offset. */
15184
+ function paragraphRangeAt(text, offset) {
15185
+ offset = Math.max(0, Math.min(offset, text.length));
15186
+ const s = text.lastIndexOf("\n", offset - 1) + 1;
15187
+ let e = text.indexOf("\n", offset);
15188
+ if (e === -1) e = text.length;
15189
+ return [s, e];
15190
+ }
15191
+ /** Previous word start (Ctrl+Left semantics). */
15192
+ function prevWordBoundary(text, offset) {
15193
+ let i = offset;
15194
+ while (i > 0 && /\s/.test(text[i - 1])) i--;
15195
+ if (i === 0) return 0;
15196
+ return wordRangeAt(text, i - 1)[0];
15197
+ }
15198
+ /** Next word end (Ctrl+Right semantics). */
15199
+ function nextWordBoundary(text, offset) {
15200
+ let i = offset;
15201
+ while (i < text.length && /\s/.test(text[i])) i++;
15202
+ if (i >= text.length) return text.length;
15203
+ return wordRangeAt(text, i)[1];
15204
+ }
15205
+ function lineIndexAt(layout, offset) {
15206
+ const lines = layout.lines;
15207
+ if (lines.length === 0) return 0;
15208
+ let fallback = 0;
15209
+ for (let i = 0; i < lines.length; i++) {
15210
+ const l = lines[i];
15211
+ if (l.start <= offset) fallback = i;
15212
+ if (offset >= l.start && offset <= l.end) {
15213
+ const next = lines[i + 1];
15214
+ if (offset === l.end && !l.lastInParagraph && next && next.start === offset) continue;
15215
+ return i;
15216
+ }
15217
+ }
15218
+ return fallback;
15219
+ }
15220
+ function lineX(line, offset, measure) {
15221
+ const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15222
+ return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15223
+ }
15224
+ /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15225
+ function caretRect(layout, offset, measure) {
15226
+ const i = lineIndexAt(layout, offset);
15227
+ const line = layout.lines[i];
15228
+ if (!line) return {
15229
+ x: 0,
15230
+ y: 0,
15231
+ height: layout.lineHeightPx,
15232
+ line: 0
15233
+ };
15234
+ return {
15235
+ x: lineX(line, offset, measure),
15236
+ y: i * layout.lineHeightPx,
15237
+ height: layout.lineHeightPx,
15238
+ line: i
15239
+ };
15240
+ }
15241
+ /** Offset closest to a point in text-local coordinates. */
15242
+ function offsetFromPoint(layout, point, measure) {
15243
+ const lines = layout.lines;
15244
+ if (lines.length === 0) return 0;
15245
+ return offsetInLine(lines[Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / layout.lineHeightPx)))], point.x, measure);
15246
+ }
15247
+ function offsetInLine(line, x, measure) {
15248
+ const rel = x - line.offsetX;
15249
+ if (rel <= 0 || line.text === "") return line.start;
15250
+ const b = boundaries(line.text, graphemeSeg);
15251
+ let prevW = 0;
15252
+ for (let k = 1; k < b.length; k++) {
15253
+ const w = measure(line.text.slice(0, b[k]));
15254
+ if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15255
+ prevW = w;
15256
+ }
15257
+ return line.end;
15258
+ }
15259
+ /** Move one visual line up/down keeping a desired x. */
15260
+ function moveVertical(layout, offset, dir, desiredX, measure) {
15261
+ const j = lineIndexAt(layout, offset) + dir;
15262
+ if (j < 0) return 0;
15263
+ if (j >= layout.lines.length) {
15264
+ const last = layout.lines[layout.lines.length - 1];
15265
+ return last ? last.end : 0;
15266
+ }
15267
+ return offsetInLine(layout.lines[j], desiredX, measure);
15268
+ }
15269
+ function lineHome(layout, offset) {
15270
+ return layout.lines[lineIndexAt(layout, offset)]?.start ?? 0;
15271
+ }
15272
+ function lineEnd(layout, offset) {
15273
+ return layout.lines[lineIndexAt(layout, offset)]?.end ?? 0;
15274
+ }
15275
+ /** Selection rectangles (one per touched line) in text-local coordinates. */
15276
+ function selectionRects(layout, from, to, measure) {
15277
+ if (from > to) [from, to] = [to, from];
15278
+ if (from === to) return [];
15279
+ const out = [];
15280
+ layout.lines.forEach((line, i) => {
15281
+ const hasNewline = line.lastInParagraph;
15282
+ const lineFrom = Math.max(from, line.start);
15283
+ const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
15284
+ if (lineTo <= lineFrom && !(line.start === line.end && from <= line.start && to > line.start)) return;
15285
+ const x1 = lineX(line, Math.min(lineFrom, line.end), measure);
15286
+ let x2 = lineX(line, Math.min(lineTo, line.end), measure);
15287
+ if (hasNewline && to > line.end) x2 += Math.max(4, layout.textHeight / 3);
15288
+ if (x2 <= x1) return;
15289
+ out.push({
15290
+ x: x1,
15291
+ y: i * layout.lineHeightPx,
15292
+ width: x2 - x1,
15293
+ height: layout.lineHeightPx
15294
+ });
15295
+ });
15296
+ return out;
15297
+ }
15298
+ //#endregion
15299
+ //#region src/nodes/text-v2/edit/model.ts
15300
+ /**
15301
+ * Session-local edit state: content + selection + undo/redo with
15302
+ * coalescing. Typing and deleting merge into one entry while they continue
15303
+ * in the same direction without a selection move; paste and IME composition
15304
+ * are always one entry each. Independent of Konva and the DOM.
15305
+ */
15306
+ var TextEditModel = class {
15307
+ blocks;
15308
+ anchor = 0;
15309
+ focus = 0;
15310
+ /** x kept while moving vertically; reset by any other move. */
15311
+ desiredX = null;
15312
+ undoStack = [];
15313
+ redoStack = [];
15314
+ lastKind = null;
15315
+ lastEditEnd = -1;
15316
+ composing = false;
15317
+ constructor(blocks) {
15318
+ this.blocks = blocks;
15319
+ const len = blocksToText(blocks).length;
15320
+ this.anchor = this.focus = len;
15321
+ }
15322
+ getBlocks() {
15323
+ return this.blocks;
15324
+ }
15325
+ getText() {
15326
+ return blocksToText(this.blocks);
15327
+ }
15328
+ getSelection() {
15329
+ return {
15330
+ start: Math.min(this.anchor, this.focus),
15331
+ end: Math.max(this.anchor, this.focus)
15332
+ };
15333
+ }
15334
+ getSelectedText() {
15335
+ const { start, end } = this.getSelection();
15336
+ return this.getText().slice(start, end);
15337
+ }
15338
+ canUndo() {
15339
+ return this.undoStack.length > 0;
15340
+ }
15341
+ canRedo() {
15342
+ return this.redoStack.length > 0;
15343
+ }
15344
+ setSelection(anchor, focus) {
15345
+ const len = this.getText().length;
15346
+ this.anchor = Math.max(0, Math.min(anchor, len));
15347
+ this.focus = Math.max(0, Math.min(focus, len));
15348
+ this.breakCoalescing();
15349
+ }
15350
+ moveTo(offset, extend = false, keepDesiredX = false) {
15351
+ const len = this.getText().length;
15352
+ const o = Math.max(0, Math.min(offset, len));
15353
+ this.focus = o;
15354
+ if (!extend) this.anchor = o;
15355
+ if (!keepDesiredX) this.desiredX = null;
15356
+ this.breakCoalescing();
15357
+ }
15358
+ selectAll() {
15359
+ this.anchor = 0;
15360
+ this.focus = this.getText().length;
15361
+ this.breakCoalescing();
15362
+ }
15363
+ breakCoalescing() {
15364
+ if (!this.composing) this.lastKind = null;
15365
+ }
15366
+ beginComposition() {
15367
+ this.composing = true;
15368
+ this.lastKind = null;
15369
+ }
15370
+ endComposition() {
15371
+ this.composing = false;
15372
+ this.lastKind = null;
15373
+ }
15374
+ snapshot() {
15375
+ return {
15376
+ blocks: this.blocks,
15377
+ anchor: this.anchor,
15378
+ focus: this.focus
15379
+ };
15380
+ }
15381
+ record(kind, editStart) {
15382
+ if (!(this.lastKind === kind && (kind === "type" || kind === "delete" || kind === "ime") && (kind === "ime" || this.lastEditEnd === editStart || kind === "delete"))) {
15383
+ this.undoStack.push(this.snapshot());
15384
+ if (this.undoStack.length > 500) this.undoStack.shift();
15385
+ }
15386
+ this.redoStack = [];
15387
+ this.lastKind = kind;
15388
+ }
15389
+ /** Replace [start,end) with plain text and put the caret after it. */
15390
+ replaceRange(start, end, text, kind) {
15391
+ const len = this.getText().length;
15392
+ start = Math.max(0, Math.min(start, len));
15393
+ end = Math.max(start, Math.min(end, len));
15394
+ if (start === end && text === "") return false;
15395
+ this.record(kind, start);
15396
+ this.blocks = replaceRange(this.blocks, start, end, text);
15397
+ this.anchor = this.focus = start + text.length;
15398
+ this.lastEditEnd = this.focus;
15399
+ this.desiredX = null;
15400
+ return true;
15401
+ }
15402
+ replaceSelection(text, kind) {
15403
+ const { start, end } = this.getSelection();
15404
+ return this.replaceRange(start, end, text, kind);
15405
+ }
15406
+ backspace(byWord = false) {
15407
+ const { start, end } = this.getSelection();
15408
+ if (start !== end) return this.replaceRange(start, end, "", "delete");
15409
+ if (start === 0) return false;
15410
+ const text = this.getText();
15411
+ const from = byWord ? prevWordBoundary(text, start) : prevGrapheme(text, start);
15412
+ return this.replaceRange(from, start, "", "delete");
15413
+ }
15414
+ del(byWord = false) {
15415
+ const { start, end } = this.getSelection();
15416
+ if (start !== end) return this.replaceRange(start, end, "", "delete");
15417
+ const text = this.getText();
15418
+ if (start >= text.length) return false;
15419
+ const to = byWord ? nextWordBoundary(text, start) : nextGrapheme(text, start);
15420
+ return this.replaceRange(start, to, "", "delete");
15421
+ }
15422
+ undo() {
15423
+ const prev = this.undoStack.pop();
15424
+ if (!prev) return false;
15425
+ this.redoStack.push(this.snapshot());
15426
+ this.restore(prev);
15427
+ return true;
15428
+ }
15429
+ redo() {
15430
+ const next = this.redoStack.pop();
15431
+ if (!next) return false;
15432
+ this.undoStack.push(this.snapshot());
15433
+ this.restore(next);
15434
+ return true;
15435
+ }
15436
+ restore(s) {
15437
+ this.blocks = s.blocks;
15438
+ this.anchor = s.anchor;
15439
+ this.focus = s.focus;
15440
+ this.lastKind = null;
15441
+ this.desiredX = null;
15442
+ }
15443
+ };
15444
+ const SLICE_BUDGET_MS = 8;
15445
+ const BLINK_MS = 530;
15446
+ var TextEditSession = class {
15447
+ model;
15448
+ target;
15449
+ host;
15450
+ id;
15451
+ abort = new AbortController();
15452
+ textarea = null;
15453
+ overlay = null;
15454
+ blinkTimer;
15455
+ caretOn = true;
15456
+ dragging = false;
15457
+ composing = false;
15458
+ pendingTimer;
15459
+ pendingFlush = false;
15460
+ destroyed = false;
15461
+ initialJson;
15462
+ constructor(target, host, id = "text-edit-session") {
15463
+ this.target = target;
15464
+ this.host = host;
15465
+ this.id = id;
15466
+ this.model = new TextEditModel(target.getBlocks());
15467
+ this.initialJson = JSON.stringify(this.model.getBlocks());
15468
+ }
15469
+ start() {
15470
+ const signal = this.abort.signal;
15471
+ host_signal(this.host.signal, this.abort);
15472
+ this.createTextarea();
15473
+ this.createOverlay();
15474
+ const ta = this.textarea;
15475
+ const opts = { signal };
15476
+ ta.addEventListener("keydown", this.onKeyDown, opts);
15477
+ ta.addEventListener("keyup", this.onKeyUp, opts);
15478
+ ta.addEventListener("beforeinput", this.onBeforeInput, opts);
15479
+ ta.addEventListener("input", this.onInput, opts);
15480
+ ta.addEventListener("paste", this.onPaste, opts);
15481
+ ta.addEventListener("compositionstart", this.onCompositionStart, opts);
15482
+ ta.addEventListener("compositionend", this.onCompositionEnd, opts);
15483
+ ta.addEventListener("select", this.syncFromTextarea, opts);
15484
+ ta.ownerDocument.addEventListener("selectionchange", this.syncFromTextarea, opts);
15485
+ window.addEventListener("pointerdown", this.onPointerDown, {
15486
+ signal,
15487
+ capture: true
15488
+ });
15489
+ window.addEventListener("pointermove", this.onPointerMove, { signal });
15490
+ window.addEventListener("pointerup", this.onPointerUp, { signal });
15491
+ window.addEventListener("mousedown", this.onMouseDown, {
15492
+ signal,
15493
+ capture: true
15494
+ });
15495
+ window.weaveTextEditing = {
15496
+ ...window.weaveTextEditing ?? {},
15497
+ [this.id]: this.id
15498
+ };
15499
+ this.syncTextarea();
15500
+ ta.focus();
15501
+ this.positionTextarea();
15502
+ this.startBlink();
15503
+ this.repaint();
15504
+ }
15505
+ /** Final content and whether it differs from when the session started. */
15506
+ getResult() {
15507
+ return {
15508
+ blocks: this.model.getBlocks(),
15509
+ changed: JSON.stringify(this.model.getBlocks()) !== this.initialJson
15510
+ };
15511
+ }
15512
+ destroy() {
15513
+ if (this.destroyed) return;
15514
+ this.destroyed = true;
15515
+ this.flushPending();
15516
+ this.abort.abort();
15517
+ clearInterval(this.blinkTimer);
15518
+ clearTimeout(this.pendingTimer);
15519
+ this.textarea?.remove();
15520
+ this.textarea = null;
15521
+ const layer = this.overlay?.getLayer();
15522
+ this.overlay?.destroy();
15523
+ this.overlay = null;
15524
+ layer?.batchDraw();
15525
+ const w = window;
15526
+ if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15527
+ }
15528
+ focus() {
15529
+ this.textarea?.focus();
15530
+ }
15531
+ createTextarea() {
15532
+ const ta = this.host.container.ownerDocument.createElement("textarea");
15533
+ ta.setAttribute("autocapitalize", "off");
15534
+ ta.setAttribute("autocomplete", "off");
15535
+ ta.setAttribute("autocorrect", "off");
15536
+ ta.setAttribute("spellcheck", "false");
15537
+ ta.setAttribute("wrap", "off");
15538
+ ta.setAttribute("aria-label", "Text editor");
15539
+ ta.tabIndex = 1;
15540
+ Object.assign(ta.style, {
15541
+ position: "absolute",
15542
+ left: "0px",
15543
+ top: "0px",
15544
+ width: "1px",
15545
+ height: "1em",
15546
+ margin: "0",
15547
+ padding: "0",
15548
+ border: "0",
15549
+ outline: "none",
15550
+ resize: "none",
15551
+ overflow: "hidden",
15552
+ whiteSpace: "pre",
15553
+ opacity: "0",
15554
+ fontSize: "16px",
15555
+ background: "transparent",
15556
+ color: "transparent",
15557
+ caretColor: "transparent",
15558
+ pointerEvents: "none",
15559
+ zIndex: "1"
15560
+ });
15561
+ this.host.container.appendChild(ta);
15562
+ this.textarea = ta;
15563
+ }
15564
+ syncTextarea() {
15565
+ const ta = this.textarea;
15566
+ if (!ta || this.composing) return;
15567
+ const text = this.model.getText();
15568
+ if (ta.value !== text) ta.value = text;
15569
+ const { start, end } = this.model.getSelection();
15570
+ const backward = this.model.focus < this.model.anchor;
15571
+ if (ta.selectionStart !== start || ta.selectionEnd !== end) ta.setSelectionRange(start, end, backward ? "backward" : "forward");
15572
+ }
15573
+ positionTextarea() {
15574
+ const ta = this.textarea;
15575
+ if (!ta) return;
15576
+ const c = this.caretStagePoint();
15577
+ const p = this.host.stageToContainer(c);
15578
+ ta.style.left = `${p.x}px`;
15579
+ ta.style.top = `${p.y}px`;
15580
+ }
15581
+ caretStagePoint() {
15582
+ const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
15583
+ const o = this.target.getContentOffset();
15584
+ return this.target.getAbsoluteTransform().point({
15585
+ x: r.x + o.x,
15586
+ y: r.y + o.y
15587
+ });
15588
+ }
15589
+ syncFromTextarea = () => {
15590
+ const ta = this.textarea;
15591
+ if (!ta || this.composing || this.destroyed) return;
15592
+ if (ta.getRootNode().activeElement !== ta) return;
15593
+ if (ta.value !== this.model.getText()) return;
15594
+ const backward = ta.selectionDirection === "backward";
15595
+ const a = backward ? ta.selectionEnd : ta.selectionStart;
15596
+ const f = backward ? ta.selectionStart : ta.selectionEnd;
15597
+ if (a !== this.model.anchor || f !== this.model.focus) {
15598
+ this.model.setSelection(a, f);
15599
+ this.afterSelectionChange();
15600
+ }
15601
+ };
15602
+ onKeyDown = (e) => {
15603
+ e.stopPropagation();
15604
+ const mod = e.ctrlKey || e.metaKey;
15605
+ const key = e.key.toLowerCase();
15606
+ if (e.key === "Escape") {
15607
+ e.preventDefault();
15608
+ this.host.onRequestExit("escape");
15609
+ return;
15610
+ }
15611
+ if (mod && key === "z" && !e.shiftKey) {
15612
+ e.preventDefault();
15613
+ this.undo();
15614
+ return;
15615
+ }
15616
+ if (mod && (key === "y" || key === "z" && e.shiftKey)) {
15617
+ e.preventDefault();
15618
+ this.redo();
15619
+ return;
15620
+ }
15621
+ if (e.key === "Tab") {
15622
+ e.preventDefault();
15623
+ return;
15624
+ }
15625
+ if (this.composing) return;
15626
+ if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !mod && !e.altKey) {
15627
+ e.preventDefault();
15628
+ this.moveVertically(e.key === "ArrowUp" ? -1 : 1, e.shiftKey);
15629
+ return;
15630
+ }
15631
+ if ((e.key === "Home" || e.key === "End") && !mod) {
15632
+ e.preventDefault();
15633
+ const layout = this.target.getLayout();
15634
+ const to = e.key === "Home" ? lineHome(layout, this.model.focus) : lineEnd(layout, this.model.focus);
15635
+ this.model.moveTo(to, e.shiftKey);
15636
+ this.afterSelectionChange();
15637
+ return;
15638
+ }
15639
+ setTimeout(this.syncFromTextarea, 0);
15640
+ };
15641
+ onKeyUp = (e) => {
15642
+ e.stopPropagation();
15643
+ this.syncFromTextarea();
15644
+ };
15645
+ onBeforeInput = (e) => {
15646
+ if (e.inputType === "historyUndo" || e.inputType === "historyRedo") e.preventDefault();
15647
+ };
15648
+ moveVertically(dir, extend) {
15649
+ const layout = this.target.getLayout();
15650
+ if (this.model.desiredX === null) this.model.desiredX = caretRect(layout, this.model.focus, this.target.measure).x;
15651
+ const to = moveVertical(layout, this.model.focus, dir, this.model.desiredX, this.target.measure);
15652
+ this.model.moveTo(to, extend, true);
15653
+ this.afterSelectionChange();
15654
+ }
15655
+ onInput = (e) => {
15656
+ const ta = this.textarea;
15657
+ if (!ta) return;
15658
+ const oldText = this.model.getText();
15659
+ const newText = ta.value;
15660
+ if (oldText === newText) return;
15661
+ let p = 0;
15662
+ const max = Math.min(oldText.length, newText.length);
15663
+ while (p < max && oldText[p] === newText[p]) p++;
15664
+ let s = 0;
15665
+ while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15666
+ const inserted = newText.slice(p, newText.length - s);
15667
+ const inputType = e.inputType ?? "";
15668
+ const kind = this.composing || inputType.includes("Composition") ? "ime" : inputType.startsWith("delete") ? "delete" : inputType === "insertFromPaste" ? "paste" : "type";
15669
+ this.model.replaceRange(p, oldText.length - s, inserted, kind);
15670
+ this.model.anchor = this.model.focus = Math.min(ta.selectionEnd, this.model.getText().length);
15671
+ this.afterContentChange(inserted.length);
15672
+ };
15673
+ onPaste = (e) => {
15674
+ e.stopPropagation();
15675
+ e.preventDefault();
15676
+ const raw = e.clipboardData?.getData("text/plain") ?? "";
15677
+ if (raw === "") return;
15678
+ this.insertText(raw.replace(/\r\n?/g, "\n"), "paste");
15679
+ };
15680
+ onCompositionStart = () => {
15681
+ this.composing = true;
15682
+ this.model.beginComposition();
15683
+ };
15684
+ onCompositionEnd = () => {
15685
+ this.composing = false;
15686
+ this.model.endComposition();
15687
+ this.syncFromTextarea();
15688
+ };
15689
+ /** Programmatic insert at the selection (paste, tests, host actions). */
15690
+ insertText(text, kind = "paste") {
15691
+ if (this.model.replaceSelection(text, kind)) {
15692
+ this.syncTextarea();
15693
+ this.afterContentChange(text.length);
15694
+ }
15695
+ }
15696
+ undo() {
15697
+ this.flushPending();
15698
+ if (this.model.undo()) {
15699
+ this.syncTextarea();
15700
+ this.afterContentChange(0, true);
15701
+ }
15702
+ }
15703
+ redo() {
15704
+ this.flushPending();
15705
+ if (this.model.redo()) {
15706
+ this.syncTextarea();
15707
+ this.afterContentChange(0, true);
15708
+ }
15709
+ }
15710
+ toLocal(e) {
15711
+ const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15712
+ const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
15713
+ const o = this.target.getContentOffset();
15714
+ return {
15715
+ x: p.x - o.x,
15716
+ y: p.y - o.y
15717
+ };
15718
+ }
15719
+ isInside(local) {
15720
+ const o = this.target.getContentOffset();
15721
+ const { width, height } = this.target.getSize();
15722
+ return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15723
+ }
15724
+ onMouseDown = (e) => {
15725
+ if (this.isInside(this.toLocal(e))) e.preventDefault();
15726
+ };
15727
+ onPointerDown = (e) => {
15728
+ if (e.button !== 0 && e.pointerType === "mouse") return;
15729
+ if (this.textarea && e.target === this.textarea) return;
15730
+ const local = this.toLocal(e);
15731
+ if (!this.isInside(local)) {
15732
+ e.stopPropagation();
15733
+ this.host.onRequestExit("outside");
15734
+ return;
15735
+ }
15736
+ e.stopPropagation();
15737
+ e.preventDefault();
15738
+ const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15739
+ const text = this.model.getText();
15740
+ if (e.detail >= 3) {
15741
+ const [s, en] = paragraphRangeAt(text, offset);
15742
+ this.model.setSelection(s, en);
15743
+ } else if (e.detail === 2) {
15744
+ const [s, en] = wordRangeAt(text, offset);
15745
+ this.model.setSelection(s, en);
15746
+ } else {
15747
+ this.model.moveTo(offset, e.shiftKey);
15748
+ this.dragging = true;
15749
+ }
15750
+ this.syncTextarea();
15751
+ this.afterSelectionChange();
15752
+ this.textarea?.focus();
15753
+ };
15754
+ onPointerMove = (e) => {
15755
+ if (!this.dragging) return;
15756
+ const local = this.toLocal(e);
15757
+ const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15758
+ this.model.moveTo(offset, true);
15759
+ this.syncTextarea();
15760
+ this.afterSelectionChange();
15761
+ };
15762
+ onPointerUp = () => {
15763
+ if (!this.dragging) return;
15764
+ this.dragging = false;
15765
+ this.textarea?.focus();
15766
+ };
15767
+ afterSelectionChange() {
15768
+ this.syncTextarea();
15769
+ this.positionTextarea();
15770
+ this.resetBlink();
15771
+ this.repaint();
15772
+ }
15773
+ afterContentChange(insertedLength, immediate = false) {
15774
+ this.flushPending();
15775
+ if (!immediate && insertedLength > 2e4 && typeof this.target.warmLayout === "function") {
15776
+ this.pendingFlush = true;
15777
+ this.sliceLayout();
15778
+ } else this.applyContent();
15779
+ this.resetBlink();
15780
+ }
15781
+ sliceLayout = () => {
15782
+ if (!this.pendingFlush || this.destroyed) return;
15783
+ if (this.target.warmLayout(this.model.getBlocks(), SLICE_BUDGET_MS)) this.flushPending();
15784
+ else this.pendingTimer = setTimeout(this.sliceLayout, 0);
15785
+ };
15786
+ flushPending() {
15787
+ if (!this.pendingFlush) return;
15788
+ this.pendingFlush = false;
15789
+ clearTimeout(this.pendingTimer);
15790
+ this.applyContent();
15791
+ }
15792
+ applyContent() {
15793
+ this.target.setBlocks(this.model.getBlocks());
15794
+ this.host.onChange?.(this.model.getBlocks());
15795
+ this.positionTextarea();
15796
+ this.target.requestRedraw();
15797
+ this.repaint();
15798
+ }
15799
+ createOverlay() {
15800
+ const layer = this.host.utilityLayer;
15801
+ if (!layer) return;
15802
+ const overlay = new Konva.Shape({
15803
+ name: "text-v2-edit-overlay",
15804
+ listening: false,
15805
+ sceneFunc: (ctx, shape) => this.paintOverlay(ctx, shape)
15806
+ });
15807
+ layer.add(overlay);
15808
+ overlay.moveToTop();
15809
+ this.overlay = overlay;
15810
+ }
15811
+ paintOverlay(ctx, shape) {
15812
+ if (this.destroyed || this.pendingFlush) return;
15813
+ const layout = this.target.getLayout();
15814
+ const abs = this.target.getAbsoluteTransform();
15815
+ const rel = shape.getAbsoluteTransform().copy().invert().multiply(abs);
15816
+ const o = this.target.getContentOffset();
15817
+ ctx.save();
15818
+ ctx.transform(rel.m[0], rel.m[1], rel.m[2], rel.m[3], rel.m[4], rel.m[5]);
15819
+ ctx.translate(o.x, o.y);
15820
+ const { start, end } = this.model.getSelection();
15821
+ if (start !== end) {
15822
+ ctx.setAttr("fillStyle", this.host.selectionColor);
15823
+ for (const r of selectionRects(layout, start, end, this.target.measure)) ctx.fillRect(r.x, r.y, r.width, r.height);
15824
+ } else if (this.caretOn) {
15825
+ const c = caretRect(layout, this.model.focus, this.target.measure);
15826
+ const scale = Math.hypot(abs.m[0], abs.m[1]) || 1;
15827
+ const w = Math.max(1, 1.5) / scale;
15828
+ ctx.setAttr("fillStyle", this.host.caretColor);
15829
+ ctx.fillRect(c.x - w / 2, c.y, w, c.height);
15830
+ }
15831
+ ctx.restore();
15832
+ }
15833
+ repaint() {
15834
+ this.overlay?.getLayer()?.batchDraw();
15835
+ }
15836
+ startBlink() {
15837
+ this.blinkTimer = setInterval(() => {
15838
+ this.caretOn = !this.caretOn;
15839
+ this.repaint();
15840
+ }, BLINK_MS);
15841
+ }
15842
+ resetBlink() {
15843
+ this.caretOn = true;
15844
+ if (this.blinkTimer) {
15845
+ clearInterval(this.blinkTimer);
15846
+ this.startBlink();
15847
+ }
15848
+ }
15849
+ };
15850
+ function host_signal(signal, abort) {
15851
+ if (!signal) return;
15852
+ if (signal.aborted) abort.abort();
15853
+ else signal.addEventListener("abort", () => abort.abort(), { once: true });
15854
+ }
15855
+ //#endregion
15856
+ //#region src/nodes/text-v2/constants.ts
15857
+ /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15858
+ const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
15859
+ const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15860
+ const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15861
+ ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15862
+ sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15863
+ };
15864
+ //#endregion
15865
+ //#region src/nodes/text-v2/text-v2.ts
15866
+ const CORNER_ANCHORS = [
15867
+ "top-left",
15868
+ "top-right",
15869
+ "bottom-left",
15870
+ "bottom-right"
15871
+ ];
15872
+ const SIDE_ANCHORS = ["middle-right", "middle-left"];
15873
+ /**
15874
+ * Drop-in replacement for `WeaveTextNode`. Same `nodeType` (`'text'`), so
15875
+ * existing documents load unchanged; register it INSTEAD of `WeaveTextNode`
15876
+ * (handlers are keyed by node type). Renders through `WeaveTextV2Shape`
15877
+ * (blocks/runs model + layout engine) instead of `Konva.Text`.
15878
+ */
15879
+ var WeaveTextV2Node = class extends WeaveNode {
15880
+ config;
15881
+ nodeType = WEAVE_TEXT_NODE_TYPE;
15882
+ createNode = false;
15883
+ editing = false;
15884
+ editingNodeId = null;
15885
+ session = null;
15886
+ sizeWarned = false;
15887
+ nodeRenderedAddedRegistered = false;
15888
+ keyPressHandler;
15889
+ constructor(params) {
15890
+ super();
15891
+ this.config = (0, import_merge.default)({}, WEAVE_TEXT_V2_DEFAULT_CONFIG, params?.config);
15892
+ }
15893
+ initialize() {
15894
+ this.keyPressHandler = void 0;
15895
+ this.editing = false;
15896
+ this.editingNodeId = null;
15897
+ this.session = null;
15898
+ this.nodeRenderedAddedRegistered = false;
15899
+ }
15900
+ /**
15901
+ * Resolve width/height for the layout mode. Auto modes size from the
15902
+ * engine's natural measure (independent of zoom, unlike the DOM-based
15903
+ * measure of the legacy node).
15904
+ */
15905
+ resolveSize(shape, props) {
15906
+ let width = props.width;
15907
+ let height = props.height;
15908
+ if (props.layout === TEXT_LAYOUT.AUTO_ALL) {
15909
+ const nat = shape.measureNaturalSize();
15910
+ width = nat.width + 2;
15911
+ height = nat.height + 2;
15912
+ }
15913
+ if (props.layout === TEXT_LAYOUT.SMART && !props.smartFixedWidth) {
15914
+ width = Math.max(1, shape.measureNaturalSize().width * 1.01);
15915
+ height = void 0;
15916
+ }
15917
+ if (props.layout === TEXT_LAYOUT.SMART && props.smartFixedWidth) height = void 0;
15918
+ if (props.layout === TEXT_LAYOUT.AUTO_HEIGHT) height = void 0;
15919
+ return {
15920
+ width,
15921
+ height
15922
+ };
15923
+ }
15924
+ measureMultilineText(shape) {
15925
+ return () => {
15926
+ const nat = shape.measureNaturalSize();
15927
+ return {
15928
+ width: Math.max(1, nat.width),
15929
+ height: Math.max(1, nat.height)
15930
+ };
15931
+ };
15932
+ }
15933
+ linkOverrides(props) {
15934
+ return {
15935
+ linkPreviousFill: props.link ? props.linkPreviousFill ?? props.fill : props.linkPreviousFill,
15936
+ linkPreviousTextDecoration: props.link ? props.linkPreviousTextDecoration ?? props.textDecoration : props.linkPreviousTextDecoration
15937
+ };
15938
+ }
15939
+ outlineProps() {
15940
+ return this.config.outline.enabled ? {
15941
+ strokeEnabled: true,
15942
+ stroke: this.config.outline.color,
15943
+ strokeWidth: this.config.outline.width,
15944
+ fillAfterStrokeEnabled: true
15945
+ } : { strokeEnabled: false };
15946
+ }
15947
+ onRender(props) {
15948
+ const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(props);
15949
+ const text = new WeaveTextV2Shape({
15950
+ ...props,
15951
+ name: "node",
15952
+ ...this.outlineProps(),
15953
+ ...props.link && {
15954
+ textDecoration: "underline",
15955
+ fill: this.config.link.defaultColor,
15956
+ linkPreviousFill,
15957
+ linkPreviousTextDecoration
15958
+ }
15959
+ });
15960
+ this.setupDefaultNodeAugmentation(text);
15961
+ const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
15962
+ text.getTransformerProperties = function() {
15963
+ const layout = this.getAttrs().layout;
15964
+ if (layout === TEXT_LAYOUT.SMART) return {
15965
+ ...defaultTransformerProperties,
15966
+ ignoreStroke: true,
15967
+ resizeEnabled: true,
15968
+ keepRatio: false,
15969
+ enabledAnchors: []
15970
+ };
15971
+ if (layout === TEXT_LAYOUT.AUTO_ALL) return {
15972
+ ...defaultTransformerProperties,
15973
+ ignoreStroke: true,
15974
+ resizeEnabled: false,
15975
+ enabledAnchors: []
15976
+ };
15977
+ if (layout === TEXT_LAYOUT.AUTO_HEIGHT) return {
15978
+ ...defaultTransformerProperties,
15979
+ ignoreStroke: true,
15980
+ resizeEnabled: true,
15981
+ enabledAnchors: SIDE_ANCHORS
15982
+ };
15983
+ return defaultTransformerProperties;
15984
+ };
15985
+ text.allowedAnchors = function() {
15986
+ const layout = this.getAttrs().layout;
15987
+ if (layout === TEXT_LAYOUT.SMART) return [...CORNER_ANCHORS, ...SIDE_ANCHORS];
15988
+ if (layout === TEXT_LAYOUT.AUTO_ALL) return [];
15989
+ if (layout === TEXT_LAYOUT.AUTO_HEIGHT) return SIDE_ANCHORS;
15990
+ return [
15991
+ ...CORNER_ANCHORS,
15992
+ "top-center",
15993
+ "bottom-center",
15994
+ ...SIDE_ANCHORS
15995
+ ];
15996
+ };
15997
+ text.setAttrs({ measureMultilineText: this.measureMultilineText(text) });
15998
+ this.setupDefaultNodeEvents(text, { performScaleReset: false });
15999
+ text.dblClick = () => {
16000
+ if (this.editing) return;
16001
+ if (!(this.isSelecting() && this.isNodeSelected(text))) return;
16002
+ this.triggerEditMode(text);
16003
+ };
16004
+ text.setAttr("triggerEditMode", this.triggerEditMode.bind(this));
16005
+ this.setupLinkBehavior(text);
16006
+ this.setupTransform(text);
16007
+ if (!this.nodeRenderedAddedRegistered) {
16008
+ this.instance.addEventListener("onNodeRenderedAdded", (node) => {
16009
+ if (this.editing && this.editingNodeId !== null && node.id() === this.editingNodeId && node.getAttr("cancelEditMode")) node.getAttr("cancelEditMode")?.();
16010
+ });
16011
+ this.nodeRenderedAddedRegistered = true;
16012
+ }
16013
+ this.registerKeyPress();
16014
+ return text;
16015
+ }
16016
+ handleKeyPress = (e) => {
16017
+ if (e.code !== "Enter" || this.instance.getActiveAction() !== "selectionTool" || this.editing) return;
16018
+ e.preventDefault();
16019
+ const selected = this.getNodesSelectionPlugin()?.getSelectedNodes() ?? [];
16020
+ if (this.isSelecting() && selected.length === 1 && selected[0].getAttrs().nodeType === "text") this.triggerEditMode(selected[0]);
16021
+ };
16022
+ registerKeyPress() {
16023
+ if (this.instance.isServerSide() || this.keyPressHandler) return;
16024
+ this.keyPressHandler = this.handleKeyPress.bind(this);
16025
+ window.addEventListener("keypress", this.keyPressHandler, { signal: this.instance.getEventsController().signal });
16026
+ }
16027
+ onAdd() {
16028
+ this.registerKeyPress();
16029
+ }
16030
+ onDestroyInstance() {
16031
+ super.onDestroyInstance();
16032
+ this.session?.destroy();
16033
+ this.session = null;
16034
+ if (!this.instance.isServerSide() && this.keyPressHandler) {
16035
+ window.removeEventListener("keypress", this.keyPressHandler);
16036
+ this.keyPressHandler = void 0;
16037
+ }
16038
+ }
16039
+ /** Re-apply layout-driven width/height after live content changes. */
16040
+ applySize(shape) {
16041
+ const { width, height } = this.resolveSize(shape, shape.getAttrs());
16042
+ shape.setAttrs({
16043
+ width,
16044
+ height
16045
+ });
16046
+ }
16047
+ editTarget(shape) {
16048
+ return {
16049
+ getBlocks: () => shape.getBlocks(),
16050
+ setBlocks: (blocks) => {
16051
+ shape.setBlocks(blocks);
16052
+ this.applySize(shape);
16053
+ },
16054
+ getLayout: () => shape.ensureLayout(),
16055
+ measure: (t) => shape.measureText(t),
16056
+ getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16057
+ getContentOffset: () => shape.getContentOffset(),
16058
+ getSize: () => ({
16059
+ width: shape.getWidth(),
16060
+ height: shape.getHeight()
16061
+ }),
16062
+ getFontSize: () => shape.fontSize(),
16063
+ requestRedraw: () => shape.getLayer()?.batchDraw(),
16064
+ warmLayout: (blocks, budget) => shape.warmLayout(blocks, budget)
16065
+ };
16066
+ }
16067
+ triggerEditMode(shape, create = false) {
16068
+ if (create) this.createNode = true;
16069
+ if (!this.instance.setMutexLock({
16070
+ nodeIds: [shape.id()],
16071
+ operation: "text-edit"
16072
+ })) return;
16073
+ this.editing = true;
16074
+ this.editingNodeId = shape.id();
16075
+ this.sizeWarned = false;
16076
+ const selectionPlugin = this.instance.getPlugin("nodesSelection");
16077
+ if (selectionPlugin) {
16078
+ const tr = selectionPlugin.getTransformer();
16079
+ this.instance.disablePlugin("nodesSelection");
16080
+ tr.hide();
16081
+ }
16082
+ const stage = this.instance.getStage();
16083
+ const container = stage.container();
16084
+ const rect = () => container.getBoundingClientRect();
16085
+ this.session = new TextEditSession(this.editTarget(shape), {
16086
+ utilityLayer: this.instance.getUtilityLayer(),
16087
+ container,
16088
+ clientToStage: (cx, cy) => {
16089
+ const r = rect();
16090
+ const sx = r.width ? stage.width() / r.width : 1;
16091
+ const sy = r.height ? stage.height() / r.height : 1;
16092
+ return {
16093
+ x: (cx - r.left) * sx,
16094
+ y: (cy - r.top) * sy
16095
+ };
16096
+ },
16097
+ stageToContainer: (p) => {
16098
+ const r = rect();
16099
+ const sx = r.width ? r.width / stage.width() : 1;
16100
+ const sy = r.height ? r.height / stage.height() : 1;
16101
+ return {
16102
+ x: p.x * sx,
16103
+ y: p.y * sy
16104
+ };
16105
+ },
16106
+ caretColor: this.config.cursor.color,
16107
+ selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16108
+ signal: this.instance.getEventsController().signal,
16109
+ onChange: (blocks) => this.checkSize(shape, blocks),
16110
+ onRequestExit: () => this.exitEditMode(shape)
16111
+ }, shape.id());
16112
+ shape.setAttr("cancelEditMode", () => this.exitEditMode(shape));
16113
+ stage.mode(WEAVE_STAGE_TEXT_EDITION_MODE);
16114
+ this.session.start();
16115
+ this.instance.emitEvent("onEnterTextNodeEditMode", { node: shape });
16116
+ }
16117
+ checkSize(shape, blocks) {
16118
+ const threshold = this.config.sizeWarning.threshold;
16119
+ if (blocksToText(blocks).length * 8 < threshold) {
16120
+ this.sizeWarned = false;
16121
+ return;
16122
+ }
16123
+ const size = JSON.stringify(blocks).length;
16124
+ if (size > threshold && !this.sizeWarned) {
16125
+ this.sizeWarned = true;
16126
+ this.instance.emitEvent("onTextSizeWarning", {
16127
+ node: shape,
16128
+ size,
16129
+ threshold
16130
+ });
16131
+ } else if (size <= threshold) this.sizeWarned = false;
16132
+ }
16133
+ exitEditMode(shape) {
16134
+ const session = this.session;
16135
+ if (!session || !this.editing) return;
16136
+ const { changed } = session.getResult();
16137
+ session.destroy();
16138
+ this.session = null;
16139
+ this.instance.releaseMutexLock();
16140
+ const stage = this.instance.getStage();
16141
+ stage.mode(WEAVE_STAGE_DEFAULT_MODE);
16142
+ this.editing = false;
16143
+ this.editingNodeId = null;
16144
+ shape.setAttr("cancelEditMode", void 0);
16145
+ this.commitNode(shape, changed);
16146
+ const liveNode = stage.findOne(`#${shape.id()}`) ?? shape;
16147
+ const selectionPlugin = this.instance.getPlugin("nodesSelection");
16148
+ if (selectionPlugin) {
16149
+ this.instance.enablePlugin("nodesSelection");
16150
+ selectionPlugin.setSelectedNodes([liveNode]);
16151
+ this.instance.triggerAction(SELECTION_TOOL_ACTION_NAME);
16152
+ }
16153
+ stage.container().tabIndex = 1;
16154
+ stage.container().click();
16155
+ stage.container().focus();
16156
+ this.instance.emitEvent("onExitTextNodeEditMode", { node: liveNode });
16157
+ }
16158
+ /** One `updateNode` per session; none when content is unchanged. */
16159
+ commitNode(shape, changed) {
16160
+ const live = this.instance.getStage().findOne(`#${shape.id()}`) ?? shape;
16161
+ if (this.createNode) {
16162
+ const serialized = this.serialize(live);
16163
+ if (live.text() !== "") {
16164
+ const container = live.getParent();
16165
+ live.destroy();
16166
+ this.instance.addNode(serialized, container?.getAttrs().id);
16167
+ } else live.destroy();
16168
+ } else if (changed) this.instance.updateNode(this.serialize(live));
16169
+ this.createNode = false;
16170
+ }
16171
+ setupTransform(text) {
16172
+ let actualAnchor = void 0;
16173
+ const isSide = () => SIDE_ANCHORS.includes(actualAnchor ?? "");
16174
+ const isCorner = () => CORNER_ANCHORS.includes(actualAnchor ?? "");
16175
+ const autoWidthLayout = () => [TEXT_LAYOUT.AUTO_HEIGHT, TEXT_LAYOUT.SMART].includes(text.getAttrs().layout);
16176
+ text.on("transformstart", (e) => {
16177
+ const ctrlOrMeta = e.evt.ctrlKey || e.evt.metaKey;
16178
+ this.instance.emitEvent("onTransform", e.target);
16179
+ actualAnchor = this.getNodesSelectionPlugin()?.getTransformer()?.getActiveAnchor();
16180
+ const layout = text.getAttrs().layout;
16181
+ this.getNodesSelectionPlugin()?.getTransformer()?.keepRatio(layout === TEXT_LAYOUT.SMART && isCorner() || layout === TEXT_LAYOUT.FIXED && ctrlOrMeta);
16182
+ if (autoWidthLayout() && isSide()) {
16183
+ text.wrap("word");
16184
+ text.scaleY(1);
16185
+ text.height(void 0);
16186
+ }
16187
+ e.cancelBubble = true;
16188
+ });
16189
+ text.on("transform", () => {
16190
+ if (autoWidthLayout() && isSide()) {
16191
+ text.width(text.width() * text.scaleX());
16192
+ text.scaleX(1);
16193
+ text.scaleY(1);
16194
+ text.height(void 0);
16195
+ text.getLayer()?.batchDraw();
16196
+ }
16197
+ const layout = text.getAttrs().layout;
16198
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16199
+ text.width(text.width() * text.scaleX());
16200
+ resetScale(text);
16201
+ text.fontSize(text.fontSize() * text.scaleY());
16202
+ }
16203
+ text.setAttr("shouldUpdateOnTransform", false);
16204
+ text.getLayer()?.batchDraw();
16205
+ });
16206
+ text.on("transformend", () => {
16207
+ this.instance.emitEvent("onTransform", null);
16208
+ let definedSmartWidth = false;
16209
+ let smartFixedWidth = text.getAttr("smartFixedWidth") ?? false;
16210
+ const layout = text.getAttrs().layout;
16211
+ if (layout !== TEXT_LAYOUT.SMART && !isCorner()) this.scaleReset(text);
16212
+ if (layout === TEXT_LAYOUT.SMART && isCorner()) text.setAttrs({
16213
+ width: Math.ceil(text.width() * text.scaleX()),
16214
+ height: Math.ceil(text.height() * text.scaleY()),
16215
+ fontSize: text.fontSize() * text.scaleY(),
16216
+ scaleX: 1,
16217
+ scaleY: 1
16218
+ });
16219
+ if (autoWidthLayout() && isSide() && !smartFixedWidth && !definedSmartWidth) {
16220
+ text.setAttr("smartFixedWidth", true);
16221
+ smartFixedWidth = true;
16222
+ definedSmartWidth = true;
16223
+ text.width(Math.ceil(text.width() * text.scaleX()));
16224
+ text.scaleX(1);
16225
+ text.scaleY(1);
16226
+ text.height(void 0);
16227
+ text.getLayer()?.batchDraw();
16228
+ text.height(Math.ceil(text.height()));
16229
+ }
16230
+ if (layout === TEXT_LAYOUT.SMART && isSide() && smartFixedWidth && !definedSmartWidth) {
16231
+ text.width(Math.ceil(text.width() * text.scaleX()));
16232
+ text.scaleX(1);
16233
+ }
16234
+ this.instance.updateNode(this.serialize(text));
16235
+ });
16236
+ }
16237
+ onUpdate(nodeInstance, nextProps) {
16238
+ const shape = nodeInstance;
16239
+ if (this.editing && this.editingNodeId === shape.id()) nextProps = {
16240
+ ...nextProps,
16241
+ text: shape.attrs.text,
16242
+ blocks: shape.attrs.blocks
16243
+ };
16244
+ const isLinked = Boolean(nextProps.link);
16245
+ const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
16246
+ shape.setAttrs({
16247
+ ...nextProps,
16248
+ ...this.outlineProps(),
16249
+ link: nextProps.link,
16250
+ linkPreviousFill: isLinked ? linkPreviousFill : void 0,
16251
+ linkPreviousTextDecoration: isLinked ? linkPreviousTextDecoration : void 0,
16252
+ ...isLinked && {
16253
+ textDecoration: "underline",
16254
+ fill: this.config.link.defaultColor
16255
+ }
16256
+ });
16257
+ if (nextProps.blocks === void 0 && shape.attrs.blocks !== void 0) shape.setAttr("blocks", void 0);
16258
+ const { width, height } = this.resolveSize(shape, nextProps);
16259
+ shape.setAttrs({
16260
+ width,
16261
+ height
16262
+ });
16263
+ if (!this.editing) {
16264
+ const selection = this.getNodesSelectionPlugin();
16265
+ if (selection) selection.setSelectedNodes(selection.getSelectedNodes());
16266
+ }
16267
+ }
16268
+ serialize(instance) {
16269
+ const attrs = instance.getAttrs();
16270
+ const cleanedAttrs = { ...attrs };
16271
+ delete cleanedAttrs.mutexLocked;
16272
+ delete cleanedAttrs.mutexUserId;
16273
+ delete cleanedAttrs.draggable;
16274
+ delete cleanedAttrs.triggerEditMode;
16275
+ delete cleanedAttrs.cancelEditMode;
16276
+ delete cleanedAttrs.measureMultilineText;
16277
+ delete cleanedAttrs.overridesMouseControl;
16278
+ delete cleanedAttrs.shouldUpdateOnTransform;
16279
+ delete cleanedAttrs.dragBoundFunc;
16280
+ if (cleanedAttrs.link) {
16281
+ if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16282
+ if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
16283
+ }
16284
+ return {
16285
+ key: attrs.id ?? "",
16286
+ type: attrs.nodeType,
16287
+ props: {
16288
+ ...cleanedAttrs,
16289
+ isCloned: void 0,
16290
+ isCloneOrigin: void 0,
16291
+ id: attrs.id ?? "",
16292
+ nodeType: attrs.nodeType,
16293
+ children: []
16294
+ }
16295
+ };
16296
+ }
16297
+ setupLinkBehavior(text) {
16298
+ text.handleMouseover = () => {
16299
+ if (!text.getAttrs().link) return;
16300
+ text.fill(this.config.link.hoverColor);
16301
+ text.getLayer()?.batchDraw();
16302
+ };
16303
+ text.handleMouseout = () => {
16304
+ if (!text.getAttrs().link) return;
16305
+ text.fill(this.config.link.defaultColor);
16306
+ text.getLayer()?.batchDraw();
16307
+ };
16308
+ text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
16309
+ let linkClickTimeout;
16310
+ const clearLinkClickTimeout = () => {
16311
+ if (linkClickTimeout) {
16312
+ clearTimeout(linkClickTimeout);
16313
+ linkClickTimeout = void 0;
16314
+ }
16315
+ };
16316
+ const previousDblClick = text.dblClick?.bind(text) ?? (() => {});
16317
+ text.dblClick = () => {
16318
+ clearLinkClickTimeout();
16319
+ previousDblClick();
16320
+ };
16321
+ text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16322
+ if (!text.getAttrs().link) return;
16323
+ if (ctrlOrMetaPressed) {
16324
+ clearLinkClickTimeout();
16325
+ this.openTextLink(text);
16326
+ return;
16327
+ }
16328
+ if (wasSelected) {
16329
+ clearLinkClickTimeout();
16330
+ linkClickTimeout = setTimeout(() => {
16331
+ linkClickTimeout = void 0;
16332
+ this.openTextLink(text);
16333
+ }, Konva.dblClickWindow);
16334
+ }
16335
+ };
16336
+ }
16337
+ openTextLink(textNode) {
16338
+ if (this.instance.isServerSide()) return;
16339
+ const link = textNode.getAttrs().link;
16340
+ if (typeof link !== "string" || !isValidTextLink(link)) return;
16341
+ window.open(link, "_blank", "noopener,noreferrer");
16342
+ }
16343
+ getLink(nodeInstance) {
16344
+ const link = nodeInstance.getAttrs().link;
16345
+ return typeof link === "string" ? link : void 0;
16346
+ }
16347
+ setLink(nodeInstance, url) {
16348
+ if (!isValidTextLink(url)) return;
16349
+ const serialized = this.serialize(nodeInstance);
16350
+ serialized.props.linkPreviousFill = serialized.props.fill;
16351
+ serialized.props.linkPreviousTextDecoration = serialized.props.textDecoration;
16352
+ serialized.props.link = url;
16353
+ this.instance.updateNode(serialized);
16354
+ }
16355
+ removeLink(nodeInstance) {
16356
+ const serialized = this.serialize(nodeInstance);
16357
+ delete serialized.props.link;
16358
+ delete serialized.props.linkPreviousFill;
16359
+ delete serialized.props.linkPreviousTextDecoration;
16360
+ this.instance.updateNode(serialized);
16361
+ }
16362
+ resetSmartLayout(textNode) {
16363
+ textNode.setAttr("smartFixedWidth", void 0);
16364
+ textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
16365
+ this.instance.updateNode(this.serialize(textNode));
16366
+ }
16367
+ static defaultState(nodeId, params) {
16368
+ return WeaveTextNode.defaultState(nodeId, params);
16369
+ }
16370
+ static addNodeState(defaultNodeState, props) {
16371
+ return mergeExceptArrays(WeaveTextNode.addNodeState(defaultNodeState, props), { props: { ...props.blocks && { blocks: props.blocks } } });
14288
16372
  }
14289
16373
  static updateNodeState(prevNodeState, nextProps) {
14290
- return mergeExceptArrays(prevNodeState, { props: {
14291
- x: nextProps.x,
14292
- y: nextProps.y,
14293
- width: nextProps.width,
14294
- ...nextProps.height && { height: nextProps.height },
14295
- FontFamily: nextProps.fontFamily,
14296
- fontSize: nextProps.fontSize,
14297
- fontStyle: nextProps.fontStyle,
14298
- fontVariant: nextProps.fontVariant,
14299
- textDecoration: nextProps.textDecoration,
14300
- letterSpacing: nextProps.letterSpacing,
14301
- lineHeight: nextProps.lineHeight,
14302
- align: nextProps.align,
14303
- verticalAlign: nextProps.verticalAlign,
14304
- rotation: nextProps.rotation,
14305
- fill: nextProps.fill,
14306
- text: nextProps.text,
14307
- layout: nextProps.layout,
14308
- ...nextProps.strokeEnabled && { strokeEnabled: nextProps.strokeEnabled },
14309
- ...nextProps.stroke && { stroke: nextProps.stroke },
14310
- ...nextProps.strokeWidth && { strokeWidth: nextProps.strokeWidth },
14311
- ...nextProps.fillAfterStrokeEnabled && { fillAfterStrokeEnabled: nextProps.fillAfterStrokeEnabled },
14312
- ...nextProps.link && { link: nextProps.link }
14313
- } });
16374
+ const next = WeaveTextNode.updateNodeState(prevNodeState, nextProps);
16375
+ if (nextProps.blocks) next.props.blocks = nextProps.blocks;
16376
+ else delete next.props.blocks;
16377
+ return next;
14314
16378
  }
14315
16379
  static getSchema() {
14316
- return super.getSchema().extend({
14317
- type: z.literal(WEAVE_TEXT_NODE_TYPE).describe(`Type of the node, for a text node it will always be "${WEAVE_TEXT_NODE_TYPE}"`),
14318
- props: z.object({
14319
- nodeType: z.literal(WEAVE_TEXT_NODE_TYPE).describe(`Type of the node, for a text node it will always be "${WEAVE_TEXT_NODE_TYPE}"`),
14320
- width: z.number().describe("Width of the text in pixels"),
14321
- height: z.number().optional().describe("Height of the text in pixels. Optional if layout is auto-height or smart."),
14322
- fontFamily: z.string().default("Arial").describe("Font family of the text, e.g. Arial, Helvetica, etc."),
14323
- fontSize: z.number().default(16).describe("Font size of the text in pixels."),
14324
- fontStyle: z.string().regex(/^(?:normal|bold|\d+)(?: italic)?$/).default("normal").describe("Font style of the text, can be \"normal\", \"bold\", \"400\", \"italic\" or a combination like \"bold italic\" or \"700 italic\"."),
14325
- fontVariant: z.enum(["normal", "small-caps"]).describe("Font variant of the text, can be \"normal\" or \"small-caps\"."),
14326
- textDecoration: z.enum([
14327
- "line-through",
14328
- "underline",
14329
- ""
14330
- ]).default("").describe("Text decoration can be \"line-through\", \"underline\" or empty string for none."),
14331
- letterSpacing: z.number().default(0).describe("Spacing between letters in pixels."),
14332
- lineHeight: z.number().default(1).describe("Line height of the text, as a multiplier of font size."),
14333
- align: z.enum([
14334
- "left",
14335
- "center",
14336
- "right",
14337
- "justify"
14338
- ]).default("left").describe("Text alignment, can be \"left\", \"center\", \"right\" or \"justify\"."),
14339
- verticalAlign: z.enum([
14340
- "top",
14341
- "middle",
14342
- "bottom"
14343
- ]).default("top").describe("Vertical alignment of the text, can be 'top', 'middle' or 'bottom'."),
14344
- fill: z.string().default("#000000ff").describe("Fill color of the text in hex format with alpha channel (e.g. #RRGGBBAA)."),
14345
- text: z.string().default("text").describe("The actual text content of the node."),
14346
- link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional().describe("External URL the whole text of the node links to. When set, the text is forced underlined and coloured with the configured link colours regardless of fill/textDecoration, is clickable, and opens the URL in a new tab. Only http(s) URLs are accepted. Omit/remove to make the node plain text again."),
14347
- linkPreviousFill: z.string().optional().describe("Internal bookkeeping: the fill colour the node had right before a link was set, captured automatically so it can be restored when the link is removed. Managed automatically — do not set directly."),
14348
- linkPreviousTextDecoration: z.string().optional().describe("Internal bookkeeping: the textDecoration the node had right before a link was set, captured automatically so it can be restored when the link is removed. Managed automatically — do not set directly."),
14349
- strokeEnabled: z.boolean().default(false).describe("Whether the text outline is enabled."),
14350
- stroke: z.string().optional().default("#d6d6d6").describe("Color of the text outline in hex format with alpha channel (e.g. #RRGGBBAA)."),
14351
- strokeWidth: z.number().optional().default(2).describe("Width of the text outline in pixels."),
14352
- strokeScaleEnabled: z.boolean().default(true).describe("Whether the stroke width should scale when the node is scaled."),
14353
- fillAfterStrokeEnabled: z.boolean().default(true).describe("Whether the fill should be drawn after the stroke. If false, the stroke will be drawn on top of the fill."),
14354
- layout: z.enum(TEXT_LAYOUT).default(TEXT_LAYOUT.SMART).describe("Layout mode of the text node. Can be:\n- 'fixed': the text node will have fixed width and height, and the text will be scaled to fit the node.\n- 'auto-height': the width of the text node will be fixed, but the height will adjust to fit the text content.\n- 'auto-all': both width and height of the text node will adjust to fit the text content.\n- 'smart': the text node will try to adjust its size based on the content and layout, but it will not exceed the initial width and height set on the node.")
14355
- })
14356
- });
16380
+ return WeaveTextNode.getSchema();
14357
16381
  }
14358
16382
  };
14359
16383
  //#endregion
@@ -14444,24 +16468,64 @@ var WeaveImageCrop = class WeaveImageCrop {
14444
16468
  this.onClose = () => {};
14445
16469
  this.handleHide = this.hide.bind(this);
14446
16470
  }
14447
- setupCropImage(imageAttrs, visible) {
14448
- this.cropGroup.destroyChildren();
16471
+ /**
16472
+ * Mirrors the start of a segment inside a container along one axis:
16473
+ * the segment [start, start + length] inside [0, size] becomes
16474
+ * [size - start - length, size - start]. Used to convert between visual
16475
+ * space (what the user sees on a flipped image) and source space (where
16476
+ * the crop information is stored). It is its own inverse.
16477
+ */
16478
+ static mirrorOffset(start, length, size) {
16479
+ return size - start - length;
16480
+ }
16481
+ /**
16482
+ * Returns the offset (in uncropped display units) of the visible crop
16483
+ * window inside the visually rendered uncropped image, along with the real
16484
+ * scale. When the image is flipped the stored crop (source space) is
16485
+ * mirrored to match what the user sees.
16486
+ */
16487
+ getVisualCropOffset(imageAttrs) {
14449
16488
  const actualScale = imageAttrs.uncroppedImage.width / imageAttrs.imageInfo.width;
14450
16489
  const realScale = actualScale / (imageAttrs.cropInfo ? imageAttrs.cropInfo.scaleX : actualScale);
16490
+ let x = 0;
16491
+ let y = 0;
16492
+ if (imageAttrs.cropInfo) {
16493
+ x = imageAttrs.cropInfo.x * realScale;
16494
+ y = imageAttrs.cropInfo.y * realScale;
16495
+ if (imageAttrs.flipX === true) x = WeaveImageCrop.mirrorOffset(x, imageAttrs.cropInfo.width * realScale, imageAttrs.uncroppedImage.width);
16496
+ if (imageAttrs.flipY === true) y = WeaveImageCrop.mirrorOffset(y, imageAttrs.cropInfo.height * realScale, imageAttrs.uncroppedImage.height);
16497
+ }
16498
+ return {
16499
+ x,
16500
+ y,
16501
+ realScale
16502
+ };
16503
+ }
16504
+ setupCropImage(imageAttrs, visible) {
16505
+ this.cropGroup.destroyChildren();
16506
+ const { x, y, realScale } = this.getVisualCropOffset(imageAttrs);
16507
+ const flipX = imageAttrs.flipX === true;
16508
+ const flipY = imageAttrs.flipY === true;
14451
16509
  this.cropImage = new Konva.Image({
14452
- x: imageAttrs.cropInfo ? -imageAttrs.cropInfo.x * realScale : 0,
14453
- y: imageAttrs.cropInfo ? -imageAttrs.cropInfo.y * realScale : 0,
16510
+ x: -x,
16511
+ y: -y,
14454
16512
  width: imageAttrs.uncroppedImage.width,
14455
16513
  height: imageAttrs.uncroppedImage.height,
14456
- scaleX: 1,
14457
- scaleY: 1,
16514
+ scaleX: flipX ? -1 : 1,
16515
+ scaleY: flipY ? -1 : 1,
16516
+ offsetX: flipX ? imageAttrs.uncroppedImage.width : 0,
16517
+ offsetY: flipY ? imageAttrs.uncroppedImage.height : 0,
14458
16518
  image: this.internalImage.image(),
14459
16519
  crop: void 0,
14460
16520
  visible,
14461
16521
  listening: false,
14462
16522
  draggable: false
14463
16523
  });
14464
- return { realScale };
16524
+ return {
16525
+ realScale,
16526
+ offsetX: x,
16527
+ offsetY: y
16528
+ };
14465
16529
  }
14466
16530
  show(onClose, options) {
14467
16531
  this.onClose = onClose;
@@ -14472,9 +16536,9 @@ var WeaveImageCrop = class WeaveImageCrop {
14472
16536
  this.image.listening(false);
14473
16537
  const imageAttrs = this.image.getAttrs();
14474
16538
  this.internalImage.hide();
14475
- const { realScale } = this.setupCropImage(imageAttrs, true);
14476
- this.imageOffsetX = imageAttrs.cropInfo ? imageAttrs.cropInfo.x * realScale : 0;
14477
- this.imageOffsetY = imageAttrs.cropInfo ? imageAttrs.cropInfo.y * realScale : 0;
16539
+ const { realScale, offsetX, offsetY } = this.setupCropImage(imageAttrs, true);
16540
+ this.imageOffsetX = offsetX;
16541
+ this.imageOffsetY = offsetY;
14478
16542
  const cropModeConfiguration = this.node.getConfiguration().cropMode;
14479
16543
  this.cropRect = new Konva.Rect({
14480
16544
  x: 0,
@@ -14522,7 +16586,10 @@ var WeaveImageCrop = class WeaveImageCrop {
14522
16586
  const center = this.cropImage.getAbsolutePosition();
14523
16587
  const width = this.cropImage.width() * stage.scaleX();
14524
16588
  const height = this.cropImage.height() * stage.scaleY();
14525
- const offset = this.cropImage.offset();
16589
+ const offset = {
16590
+ x: this.cropImage.scaleX() < 0 ? 0 : this.cropImage.offsetX(),
16591
+ y: this.cropImage.scaleY() < 0 ? 0 : this.cropImage.offsetY()
16592
+ };
14526
16593
  const corners = [
14527
16594
  {
14528
16595
  x: -offset.x,
@@ -14866,11 +16933,15 @@ var WeaveImageCrop = class WeaveImageCrop {
14866
16933
  if (!intersectionRect) return;
14867
16934
  const imageAttrs = this.image.getAttrs();
14868
16935
  const actualScale = imageAttrs.uncroppedImage.width / imageAttrs.imageInfo.width;
16936
+ let sourceX = clipRectGroup.x + this.imageOffsetX;
16937
+ let sourceY = clipRectGroup.y + this.imageOffsetY;
16938
+ if (imageAttrs.flipX === true) sourceX = WeaveImageCrop.mirrorOffset(sourceX, clipRectGroup.width, imageAttrs.uncroppedImage.width);
16939
+ if (imageAttrs.flipY === true) sourceY = WeaveImageCrop.mirrorOffset(sourceY, clipRectGroup.height, imageAttrs.uncroppedImage.height);
14869
16940
  const realClipRect = {
14870
16941
  scaleX: actualScale,
14871
16942
  scaleY: actualScale,
14872
- x: WeaveImageCrop.roundTo6Decimals(clipRectGroup.x + this.imageOffsetX),
14873
- y: WeaveImageCrop.roundTo6Decimals(clipRectGroup.y + this.imageOffsetY),
16943
+ x: WeaveImageCrop.roundTo6Decimals(sourceX),
16944
+ y: WeaveImageCrop.roundTo6Decimals(sourceY),
14874
16945
  width: WeaveImageCrop.roundTo6Decimals(clipRectGroup.width),
14875
16946
  height: WeaveImageCrop.roundTo6Decimals(clipRectGroup.height)
14876
16947
  };
@@ -14912,16 +16983,18 @@ var WeaveImageCrop = class WeaveImageCrop {
14912
16983
  if (!intersectionRect || !intersectionRectAbs) return;
14913
16984
  const imageAttrs = this.image.getAttrs();
14914
16985
  const actualScale = imageAttrs.uncroppedImage.width / imageAttrs.imageInfo.width;
14915
- const realScale = actualScale / (imageAttrs.cropInfo ? imageAttrs.cropInfo.scaleX : actualScale);
14916
- const imageOffsetX = imageAttrs.cropInfo ? imageAttrs.cropInfo.x * realScale : 0;
14917
- const imageOffsetY = imageAttrs.cropInfo ? imageAttrs.cropInfo.y * realScale : 0;
16986
+ const { x: imageOffsetX, y: imageOffsetY } = this.getVisualCropOffset(imageAttrs);
14918
16987
  const diffX = intersectionRect.x - imageRect.x;
14919
16988
  const diffY = intersectionRect.y - imageRect.y;
16989
+ let sourceX = diffX + imageOffsetX;
16990
+ let sourceY = diffY + imageOffsetY;
16991
+ if (imageAttrs.flipX === true) sourceX = WeaveImageCrop.mirrorOffset(sourceX, intersectionRect.width, imageAttrs.uncroppedImage.width);
16992
+ if (imageAttrs.flipY === true) sourceY = WeaveImageCrop.mirrorOffset(sourceY, intersectionRect.height, imageAttrs.uncroppedImage.height);
14920
16993
  const realClipRect = {
14921
16994
  scaleX: actualScale,
14922
16995
  scaleY: actualScale,
14923
- x: WeaveImageCrop.roundTo6Decimals(diffX + imageOffsetX),
14924
- y: WeaveImageCrop.roundTo6Decimals(diffY + imageOffsetY),
16996
+ x: WeaveImageCrop.roundTo6Decimals(sourceX),
16997
+ y: WeaveImageCrop.roundTo6Decimals(sourceY),
14925
16998
  width: WeaveImageCrop.roundTo6Decimals(intersectionRect.width),
14926
16999
  height: WeaveImageCrop.roundTo6Decimals(intersectionRect.height)
14927
17000
  };
@@ -15227,6 +17300,27 @@ var WeaveImageNode = class extends WeaveNode {
15227
17300
  if (!internalImage || !cropGroup) return;
15228
17301
  new WeaveImageCrop(this.instance, this, imageNode, internalImage, cropGroup).unCrop();
15229
17302
  };
17303
+ flipImage = (imageNode, options) => {
17304
+ if (imageNode.getAttrs().cropping) return;
17305
+ const prop = options.axis === "horizontal" ? "flipX" : "flipY";
17306
+ this.instance.stateTransactional(() => {
17307
+ imageNode.setAttrs({ [prop]: !(imageNode.getAttrs()[prop] ?? false) });
17308
+ this.applyFlip(imageNode);
17309
+ this.cacheNode(imageNode);
17310
+ this.instance.updateNode(this.serialize(imageNode));
17311
+ });
17312
+ };
17313
+ applyFlip(image) {
17314
+ const attrs = image.getAttrs();
17315
+ const internalImage = image.findOne(`#${attrs.id}-image`);
17316
+ if (!internalImage) return;
17317
+ const flipX = attrs.flipX === true;
17318
+ const flipY = attrs.flipY === true;
17319
+ internalImage.scaleX(flipX ? -1 : 1);
17320
+ internalImage.scaleY(flipY ? -1 : 1);
17321
+ internalImage.offsetX(flipX ? internalImage.width() : 0);
17322
+ internalImage.offsetY(flipY ? internalImage.height() : 0);
17323
+ }
15230
17324
  loadAsyncElement(nodeId) {
15231
17325
  this.instance.loadAsyncElement(nodeId, WEAVE_IMAGE_NODE_TYPE);
15232
17326
  }
@@ -15274,6 +17368,12 @@ var WeaveImageNode = class extends WeaveNode {
15274
17368
  new WeaveImageCrop(this.instance, this, image, internalImage, cropGroup).unCrop();
15275
17369
  };
15276
17370
  }
17371
+ image.flipHorizontal = () => {
17372
+ this.flipImage(image, { axis: "horizontal" });
17373
+ };
17374
+ image.flipVertical = () => {
17375
+ this.flipImage(image, { axis: "vertical" });
17376
+ };
15277
17377
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
15278
17378
  image.getTransformerProperties = function() {
15279
17379
  return defaultTransformerProperties;
@@ -15889,6 +17989,10 @@ var WeaveImageNode = class extends WeaveNode {
15889
17989
  if (!this.imageState[imageAttrs.id ?? ""]?.loaded) return;
15890
17990
  }
15891
17991
  updateImageCrop(image) {
17992
+ this.updateImageCropGeometry(image);
17993
+ this.applyFlip(image);
17994
+ }
17995
+ updateImageCropGeometry(image) {
15892
17996
  const imageAttrs = image.getAttrs();
15893
17997
  const internalImage = image?.findOne(`#${imageAttrs.id}-image`);
15894
17998
  if (!this.imageState[imageAttrs.id ?? ""]?.loaded) return;
@@ -15966,8 +18070,12 @@ var WeaveImageNode = class extends WeaveNode {
15966
18070
  });
15967
18071
  }
15968
18072
  if (internalImage) {
15969
- internalImage.width(Math.max(5, internalImage.width() * internalImage.scaleX()));
15970
- internalImage.height(Math.max(5, internalImage.height() * internalImage.scaleY()));
18073
+ internalImage.offset({
18074
+ x: 0,
18075
+ y: 0
18076
+ });
18077
+ internalImage.width(Math.max(5, internalImage.width() * Math.abs(internalImage.scaleX())));
18078
+ internalImage.height(Math.max(5, internalImage.height() * Math.abs(internalImage.scaleY())));
15971
18079
  internalImage.scale({
15972
18080
  x: 1,
15973
18081
  y: 1
@@ -16022,7 +18130,7 @@ var WeaveImageNode = class extends WeaveNode {
16022
18130
  clearTimeout(this.imageTryoutIds[nodeId]);
16023
18131
  delete this.imageTryoutIds[nodeId];
16024
18132
  }
16025
- nodeInstance.destroy();
18133
+ super.onDestroy(nodeInstance);
16026
18134
  }
16027
18135
  loadImageTryout(imageId) {
16028
18136
  this.imageTryoutIds[imageId] = setTimeout(() => {
@@ -16109,7 +18217,9 @@ var WeaveImageNode = class extends WeaveNode {
16109
18217
  width: props.uncroppedImage.width,
16110
18218
  height: props.uncroppedImage.height
16111
18219
  },
16112
- cropping: props.cropping
18220
+ cropping: props.cropping,
18221
+ ...props.flipX !== void 0 && { flipX: props.flipX },
18222
+ ...props.flipY !== void 0 && { flipY: props.flipY }
16113
18223
  } });
16114
18224
  }
16115
18225
  static updateNodeState(prevNodeState, nextProps) {
@@ -16132,7 +18242,9 @@ var WeaveImageNode = class extends WeaveNode {
16132
18242
  width: nextProps.uncroppedImage?.width,
16133
18243
  height: nextProps.uncroppedImage?.height
16134
18244
  } },
16135
- cropping: nextProps.cropping
18245
+ cropping: nextProps.cropping,
18246
+ ...nextProps.flipX !== void 0 && { flipX: nextProps.flipX },
18247
+ ...nextProps.flipY !== void 0 && { flipY: nextProps.flipY }
16136
18248
  } });
16137
18249
  }
16138
18250
  static getSchema() {
@@ -16156,7 +18268,9 @@ var WeaveImageNode = class extends WeaveNode {
16156
18268
  width: z.number().describe("The width of the image before cropping, used for cropping calculations"),
16157
18269
  height: z.number().describe("The height of the image before cropping, used for cropping calculations")
16158
18270
  }),
16159
- cropping: z.boolean().default(false).describe("Whether the image is currently being cropped")
18271
+ cropping: z.boolean().default(false).describe("Whether the image is currently being cropped"),
18272
+ flipX: z.boolean().optional().describe("Whether the image is flipped horizontally"),
18273
+ flipY: z.boolean().optional().describe("Whether the image is flipped vertically")
16160
18274
  })
16161
18275
  });
16162
18276
  }
@@ -19585,7 +21699,7 @@ var WeaveCommentNode = class extends WeaveNode {
19585
21699
  this.normalizeNodeSize(commentNode);
19586
21700
  }
19587
21701
  onDestroy(nodeInstance) {
19588
- nodeInstance.destroy();
21702
+ super.onDestroy(nodeInstance);
19589
21703
  }
19590
21704
  expandNode(commentNode) {
19591
21705
  const widthExpanded = this.config.style.expanded.width;
@@ -23239,6 +25353,70 @@ const RECTANGLE_TOOL_STATE = {
23239
25353
  ["ADDED"]: "added"
23240
25354
  };
23241
25355
  //#endregion
25356
+ //#region src/actions/shared/shape-geometry.ts
25357
+ /** Returns the normalized bounds described by a drag, including its modifiers. */
25358
+ function getShapeDragBounds(anchor, pointer, options = {}) {
25359
+ const { centered = false, constrained = false, aspectRatio = 1 } = options;
25360
+ const deltaX = Math.abs(pointer.x - anchor.x);
25361
+ const deltaY = Math.abs(pointer.y - anchor.y);
25362
+ let width = deltaX;
25363
+ let height = deltaY;
25364
+ if (constrained && aspectRatio > 0) {
25365
+ if (width / (height || Number.EPSILON) > aspectRatio) height = width / aspectRatio;
25366
+ else width = height * aspectRatio;
25367
+ }
25368
+ if (constrained && aspectRatio > 0 && width > 0 && height > 0) {
25369
+ const minimumSizeScale = Math.max(1, 1 / width, 1 / height);
25370
+ width *= minimumSizeScale;
25371
+ height *= minimumSizeScale;
25372
+ } else {
25373
+ width = Math.max(1, width);
25374
+ height = Math.max(1, height);
25375
+ }
25376
+ if (centered) {
25377
+ width *= 2;
25378
+ height *= 2;
25379
+ return {
25380
+ x: anchor.x - width / 2,
25381
+ y: anchor.y - height / 2,
25382
+ width,
25383
+ height
25384
+ };
25385
+ }
25386
+ const signedWidth = Math.sign(pointer.x - anchor.x || 1) * width;
25387
+ const signedHeight = Math.sign(pointer.y - anchor.y || 1) * height;
25388
+ return {
25389
+ x: Math.min(anchor.x, anchor.x + signedWidth),
25390
+ y: Math.min(anchor.y, anchor.y + signedHeight),
25391
+ width,
25392
+ height
25393
+ };
25394
+ }
25395
+ //#endregion
25396
+ //#region src/actions/shared/shape-keyboard.ts
25397
+ /** Binds the common keyboard behavior for tools that create shapes by dragging. */
25398
+ function bindShapeDragKeyboardHandlers(signal, isActionActive, isDragInProgress, canUpdateModifiers, onCancel, onModifiersChange) {
25399
+ const handleModifierKey = (event) => {
25400
+ if (event.key !== "Shift" && event.key !== "Alt") return;
25401
+ const actionActive = isActionActive();
25402
+ const dragInProgress = isDragInProgress();
25403
+ if (event.type === "keydown" && event.key === "Alt" && actionActive && dragInProgress) event.preventDefault();
25404
+ if (!actionActive || !canUpdateModifiers()) return;
25405
+ onModifiersChange({
25406
+ shiftKey: event.shiftKey,
25407
+ altKey: event.altKey
25408
+ });
25409
+ };
25410
+ window.addEventListener("keydown", (event) => {
25411
+ if (isActionActive() && (event.code === "Enter" || event.code === "Escape")) {
25412
+ onCancel();
25413
+ return;
25414
+ }
25415
+ handleModifierKey(event);
25416
+ }, { signal });
25417
+ window.addEventListener("keyup", handleModifierKey, { signal });
25418
+ }
25419
+ //#endregion
23242
25420
  //#region src/actions/rectangle-tool/rectangle-tool.ts
23243
25421
  var WeaveRectangleToolAction = class extends WeaveAction {
23244
25422
  initialized = false;
@@ -23284,16 +25462,7 @@ var WeaveRectangleToolAction = class extends WeaveAction {
23284
25462
  }
23285
25463
  setupEvents() {
23286
25464
  const stage = this.instance.getStage();
23287
- window.addEventListener("keydown", (e) => {
23288
- if (e.code === "Enter" && this.instance.getActiveAction() === "rectangleTool") {
23289
- this.cancelAction();
23290
- return;
23291
- }
23292
- if (e.code === "Escape" && this.instance.getActiveAction() === "rectangleTool") {
23293
- this.cancelAction();
23294
- return;
23295
- }
23296
- }, { signal: this.instance.getEventsController().signal });
25465
+ bindShapeDragKeyboardHandlers(this.instance.getEventsController().signal, () => this.instance.getActiveAction() === RECTANGLE_TOOL_ACTION_NAME, () => this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE, () => this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE && this.moved, () => this.cancelAction(), (modifiers) => this.handleMovement(modifiers));
23297
25466
  stage.on("pointermove", () => {
23298
25467
  if (this.state === RECTANGLE_TOOL_STATE.IDLE) return;
23299
25468
  this.setCursor();
@@ -23319,12 +25488,12 @@ var WeaveRectangleToolAction = class extends WeaveAction {
23319
25488
  this.state = RECTANGLE_TOOL_STATE.ADDING;
23320
25489
  return;
23321
25490
  }
23322
- if (this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE) this.handleMovement();
25491
+ if (this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE) this.handleMovement(e.evt);
23323
25492
  });
23324
25493
  stage.on("pointerup", (e) => {
23325
25494
  this.pointers.delete(e.evt.pointerId);
23326
25495
  if (this.isTap(e)) this.moved = false;
23327
- if (this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE) this.handleSettingSize();
25496
+ if (this.state === RECTANGLE_TOOL_STATE.DEFINING_SIZE) this.handleSettingSize(e.evt);
23328
25497
  });
23329
25498
  this.initialized = true;
23330
25499
  }
@@ -23358,7 +25527,7 @@ var WeaveRectangleToolAction = class extends WeaveAction {
23358
25527
  }
23359
25528
  this.setState(RECTANGLE_TOOL_STATE.DEFINING_SIZE);
23360
25529
  }
23361
- handleSettingSize() {
25530
+ handleSettingSize(modifiers = {}) {
23362
25531
  if (this.rectId && this.tempRectNode && this.clickPoint && this.container) {
23363
25532
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
23364
25533
  const rectPos = {
@@ -23368,10 +25537,14 @@ var WeaveRectangleToolAction = class extends WeaveAction {
23368
25537
  let rectWidth = this.props.width;
23369
25538
  let rectHeight = this.props.height;
23370
25539
  if (this.moved) {
23371
- rectPos.x = Math.min(this.clickPoint.x, mousePoint.x);
23372
- rectPos.y = Math.min(this.clickPoint.y, mousePoint.y);
23373
- rectWidth = Math.abs(this.clickPoint.x - mousePoint.x);
23374
- rectHeight = Math.abs(this.clickPoint.y - mousePoint.y);
25540
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
25541
+ centered: modifiers.altKey,
25542
+ constrained: modifiers.shiftKey
25543
+ });
25544
+ rectPos.x = bounds.x;
25545
+ rectPos.y = bounds.y;
25546
+ rectWidth = bounds.width;
25547
+ rectHeight = bounds.height;
23375
25548
  }
23376
25549
  this.tempRectNode.setAttrs({
23377
25550
  ...this.props,
@@ -23403,16 +25576,20 @@ var WeaveRectangleToolAction = class extends WeaveAction {
23403
25576
  }
23404
25577
  this.cancelAction();
23405
25578
  }
23406
- handleMovement() {
25579
+ handleMovement(modifiers = {}) {
23407
25580
  if (this.state !== RECTANGLE_TOOL_STATE.DEFINING_SIZE) return;
23408
25581
  if (this.rectId && this.tempRectNode && this.measureContainer && this.clickPoint) {
23409
25582
  this.moved = true;
23410
25583
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.measureContainer);
23411
- const deltaX = mousePoint.x - this.clickPoint?.x;
23412
- const deltaY = mousePoint.y - this.clickPoint?.y;
25584
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
25585
+ centered: modifiers.altKey,
25586
+ constrained: modifiers.shiftKey
25587
+ });
23413
25588
  this.tempRectNode.setAttrs({
23414
- width: deltaX,
23415
- height: deltaY
25589
+ x: bounds.x,
25590
+ y: bounds.y,
25591
+ width: bounds.width,
25592
+ height: bounds.height
23416
25593
  });
23417
25594
  }
23418
25595
  }
@@ -23511,16 +25688,7 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23511
25688
  }
23512
25689
  setupEvents() {
23513
25690
  const stage = this.instance.getStage();
23514
- window.addEventListener("keydown", (e) => {
23515
- if (e.code === "Enter" && this.instance.getActiveAction() === "ellipseTool") {
23516
- this.cancelAction();
23517
- return;
23518
- }
23519
- if (e.code === "Escape" && this.instance.getActiveAction() === "ellipseTool") {
23520
- this.cancelAction();
23521
- return;
23522
- }
23523
- }, { signal: this.instance.getEventsController().signal });
25691
+ bindShapeDragKeyboardHandlers(this.instance.getEventsController().signal, () => this.instance.getActiveAction() === ELLIPSE_TOOL_ACTION_NAME, () => this.state === ELLIPSE_TOOL_STATE.DEFINING_SIZE, () => this.state === ELLIPSE_TOOL_STATE.DEFINING_SIZE && this.moved, () => this.cancelAction(), (modifiers) => this.handleMovement(modifiers));
23524
25692
  stage.on("pointerdown", (e) => {
23525
25693
  this.setTapStart(e);
23526
25694
  this.pointers.set(e.evt.pointerId, {
@@ -23547,7 +25715,7 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23547
25715
  }
23548
25716
  if (this.state === ELLIPSE_TOOL_STATE.DEFINING_SIZE) {
23549
25717
  this.moved = true;
23550
- this.handleMovement();
25718
+ this.handleMovement(e.evt);
23551
25719
  }
23552
25720
  });
23553
25721
  stage.on("pointerup", (e) => {
@@ -23555,7 +25723,7 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23555
25723
  if (this.isTap(e)) this.moved = false;
23556
25724
  if (this.state === ELLIPSE_TOOL_STATE.DEFINING_SIZE) {
23557
25725
  this.creating = false;
23558
- this.handleSettingSize();
25726
+ this.handleSettingSize(e.evt);
23559
25727
  }
23560
25728
  });
23561
25729
  this.initialized = true;
@@ -23579,8 +25747,8 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23579
25747
  if (nodeHandler) {
23580
25748
  const node = nodeHandler.create(this.ellipseId, {
23581
25749
  ...this.props,
23582
- x: this.clickPoint?.x ?? 0 + this.props.radiusX,
23583
- y: this.clickPoint?.y ?? 0 + this.props.radiusY,
25750
+ x: this.clickPoint?.x ?? 0,
25751
+ y: this.clickPoint?.y ?? 0,
23584
25752
  radiusX: 1,
23585
25753
  radiusY: 1
23586
25754
  });
@@ -23588,22 +25756,28 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23588
25756
  }
23589
25757
  this.setState(ELLIPSE_TOOL_STATE.DEFINING_SIZE);
23590
25758
  }
23591
- handleSettingSize() {
25759
+ handleSettingSize(modifiers = {}) {
23592
25760
  const ellipse = this.instance.getStage().findOne(`#${this.ellipseId}`);
23593
25761
  if (this.ellipseId && this.clickPoint && this.container && ellipse) {
23594
25762
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
23595
25763
  const nodeHandler = this.instance.getNodeHandler("ellipse");
23596
- const ellipsePos = {
25764
+ let ellipsePos = {
23597
25765
  x: this.clickPoint.x,
23598
25766
  y: this.clickPoint.y
23599
25767
  };
23600
- let ellipseRadiusX = this.props.radiusY;
25768
+ let ellipseRadiusX = this.props.radiusX;
23601
25769
  let ellipseRadiusY = this.props.radiusY;
23602
25770
  if (this.moved) {
23603
- ellipsePos.x = Math.min(this.clickPoint.x, mousePoint.x);
23604
- ellipsePos.y = Math.min(this.clickPoint.y, mousePoint.y);
23605
- ellipseRadiusX = Math.abs(this.clickPoint.x - mousePoint.x);
23606
- ellipseRadiusY = Math.abs(this.clickPoint.y - mousePoint.y);
25771
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
25772
+ centered: modifiers.altKey,
25773
+ constrained: modifiers.shiftKey
25774
+ });
25775
+ ellipsePos = {
25776
+ x: bounds.x,
25777
+ y: bounds.y
25778
+ };
25779
+ ellipseRadiusX = bounds.width;
25780
+ ellipseRadiusY = bounds.height;
23607
25781
  }
23608
25782
  ellipse.setAttrs({
23609
25783
  ...this.props,
@@ -23617,27 +25791,23 @@ var WeaveEllipseToolAction = class extends WeaveAction {
23617
25791
  }
23618
25792
  this.cancelAction();
23619
25793
  }
23620
- handleMovement() {
25794
+ handleMovement(modifiers = {}) {
23621
25795
  if (this.state !== ELLIPSE_TOOL_STATE.DEFINING_SIZE) return;
23622
25796
  const ellipse = this.instance.getStage().findOne(`#${this.ellipseId}`);
23623
25797
  if (this.ellipseId && this.container && this.clickPoint && ellipse) {
23624
25798
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
23625
- const deltaX = Math.abs(mousePoint.x - this.clickPoint?.x);
23626
- const deltaY = Math.abs(mousePoint.y - this.clickPoint?.y);
23627
- const ellipsePos = {
23628
- x: this.clickPoint.x,
23629
- y: this.clickPoint.y
23630
- };
23631
- if (this.moved) {
23632
- ellipsePos.x = Math.min(this.clickPoint.x, mousePoint.x);
23633
- ellipsePos.y = Math.min(this.clickPoint.y, mousePoint.y);
23634
- }
25799
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
25800
+ centered: modifiers.altKey,
25801
+ constrained: modifiers.shiftKey
25802
+ });
23635
25803
  const nodeHandler = this.instance.getNodeHandler("ellipse");
23636
25804
  if (nodeHandler) nodeHandler.onUpdate(ellipse, {
23637
25805
  ...this.props,
23638
25806
  id: this.ellipseId,
23639
- radiusX: deltaX / 2,
23640
- radiusY: deltaY / 2
25807
+ x: bounds.x,
25808
+ y: bounds.y,
25809
+ radiusX: bounds.width / 2,
25810
+ radiusY: bounds.height / 2
23641
25811
  });
23642
25812
  }
23643
25813
  }
@@ -26608,16 +28778,7 @@ var WeaveRegularPolygonToolAction = class extends WeaveAction {
26608
28778
  }
26609
28779
  setupEvents() {
26610
28780
  const stage = this.instance.getStage();
26611
- window.addEventListener("keydown", (e) => {
26612
- if (e.code === "Enter" && this.instance.getActiveAction() === "regularPolygonTool") {
26613
- this.cancelAction();
26614
- return;
26615
- }
26616
- if (e.code === "Escape" && this.instance.getActiveAction() === "regularPolygonTool") {
26617
- this.cancelAction();
26618
- return;
26619
- }
26620
- }, { signal: this.instance.getEventsController().signal });
28781
+ bindShapeDragKeyboardHandlers(this.instance.getEventsController().signal, () => this.instance.getActiveAction() === REGULAR_POLYGON_TOOL_ACTION_NAME, () => this.state === REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE, () => this.state === REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE && this.moved, () => this.cancelAction(), (modifiers) => this.handleMovement(modifiers));
26621
28782
  stage.on("pointerdown", (e) => {
26622
28783
  this.setTapStart(e);
26623
28784
  this.pointers.set(e.evt.pointerId, {
@@ -26644,7 +28805,7 @@ var WeaveRegularPolygonToolAction = class extends WeaveAction {
26644
28805
  }
26645
28806
  if (this.state === REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE) {
26646
28807
  this.moved = true;
26647
- this.handleMovement();
28808
+ this.handleMovement(e.evt);
26648
28809
  }
26649
28810
  });
26650
28811
  stage.on("pointerup", (e) => {
@@ -26652,7 +28813,7 @@ var WeaveRegularPolygonToolAction = class extends WeaveAction {
26652
28813
  if (this.isTap(e)) this.moved = false;
26653
28814
  if (this.state === REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE) {
26654
28815
  this.creating = false;
26655
- this.handleSettingSize();
28816
+ this.handleSettingSize(e.evt);
26656
28817
  }
26657
28818
  });
26658
28819
  this.initialized = true;
@@ -26684,20 +28845,26 @@ var WeaveRegularPolygonToolAction = class extends WeaveAction {
26684
28845
  }
26685
28846
  this.setState(REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE);
26686
28847
  }
26687
- handleSettingSize() {
28848
+ handleSettingSize(modifiers = {}) {
26688
28849
  const regularPolygon = this.instance.getStage().findOne(`#${this.regularPolygonId}`);
26689
28850
  if (this.regularPolygonId && this.clickPoint && this.container && regularPolygon) {
26690
28851
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
26691
28852
  const nodeHandler = this.instance.getNodeHandler("regular-polygon");
26692
- const starPos = {
28853
+ let starPos = {
26693
28854
  x: this.clickPoint.x,
26694
28855
  y: this.clickPoint.y
26695
28856
  };
26696
28857
  let newRadius = this.props.radius;
26697
28858
  if (this.moved) {
26698
- starPos.x = Math.min(this.clickPoint.x, mousePoint.x);
26699
- starPos.y = Math.min(this.clickPoint.y, mousePoint.y);
26700
- newRadius = Math.abs(this.clickPoint.x - mousePoint.x);
28859
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
28860
+ centered: modifiers.altKey,
28861
+ constrained: true
28862
+ });
28863
+ starPos = {
28864
+ x: bounds.x,
28865
+ y: bounds.y
28866
+ };
28867
+ newRadius = bounds.width;
26701
28868
  }
26702
28869
  regularPolygon.setAttrs({
26703
28870
  ...this.props,
@@ -26710,25 +28877,22 @@ var WeaveRegularPolygonToolAction = class extends WeaveAction {
26710
28877
  }
26711
28878
  this.cancelAction();
26712
28879
  }
26713
- handleMovement() {
28880
+ handleMovement(modifiers = {}) {
26714
28881
  if (this.state !== REGULAR_POLYGON_TOOL_STATE.DEFINING_SIZE) return;
26715
28882
  const regularPolygon = this.instance.getStage().findOne(`#${this.regularPolygonId}`);
26716
28883
  if (this.regularPolygonId && this.container && this.clickPoint && regularPolygon) {
26717
28884
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
26718
- const deltaX = Math.abs(mousePoint.x - this.clickPoint?.x);
26719
- const starPos = {
26720
- x: this.clickPoint.x,
26721
- y: this.clickPoint.y
26722
- };
26723
- if (this.moved) {
26724
- starPos.x = Math.min(this.clickPoint.x, mousePoint.x);
26725
- starPos.y = Math.min(this.clickPoint.y, mousePoint.y);
26726
- }
28885
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
28886
+ centered: modifiers.altKey,
28887
+ constrained: true
28888
+ });
26727
28889
  const nodeHandler = this.instance.getNodeHandler("regular-polygon");
26728
28890
  if (nodeHandler) nodeHandler.onUpdate(regularPolygon, {
26729
28891
  ...this.props,
26730
28892
  id: this.regularPolygonId,
26731
- radius: deltaX / 2
28893
+ x: bounds.x,
28894
+ y: bounds.y,
28895
+ radius: bounds.width / 2
26732
28896
  });
26733
28897
  }
26734
28898
  }
@@ -26777,14 +28941,22 @@ const POLYGON_TOOL_ACTION_NAME = "polygonTool";
26777
28941
  const POLYGON_TOOL_STATE = {
26778
28942
  ["IDLE"]: "idle",
26779
28943
  ["ADDING"]: "adding",
28944
+ ["DEFINING_SIZE"]: "defining-size",
26780
28945
  ["ADDED"]: "added"
26781
28946
  };
26782
28947
  //#endregion
26783
28948
  //#region src/actions/polygon-tool/polygon-tool.ts
28949
+ const MIN_POLYGON_DRAG_DISTANCE = 3;
26784
28950
  var WeavePolygonToolAction = class extends WeaveAction {
26785
28951
  initialized = false;
26786
28952
  state;
26787
28953
  polygonId;
28954
+ pointers;
28955
+ clickPoint;
28956
+ container;
28957
+ moved;
28958
+ placementCommitted;
28959
+ polygonNode;
26788
28960
  cancelAction;
26789
28961
  preset;
26790
28962
  onPropsChange = void 0;
@@ -26798,6 +28970,12 @@ var WeavePolygonToolAction = class extends WeaveAction {
26798
28970
  this.initialized = false;
26799
28971
  this.state = POLYGON_TOOL_STATE.IDLE;
26800
28972
  this.polygonId = null;
28973
+ this.pointers = /* @__PURE__ */ new Map();
28974
+ this.clickPoint = null;
28975
+ this.container = void 0;
28976
+ this.moved = false;
28977
+ this.placementCommitted = false;
28978
+ this.polygonNode = null;
26801
28979
  this.props = this.initProps();
26802
28980
  }
26803
28981
  getName() {
@@ -26822,18 +29000,41 @@ var WeavePolygonToolAction = class extends WeaveAction {
26822
29000
  }
26823
29001
  setupEvents() {
26824
29002
  const stage = this.instance.getStage();
26825
- window.addEventListener("keydown", (e) => {
26826
- if ((e.code === "Enter" || e.code === "Escape") && this.instance.getActiveAction() === "polygonTool") this.cancelAction();
26827
- }, { signal: this.instance.getEventsController().signal });
29003
+ bindShapeDragKeyboardHandlers(this.instance.getEventsController().signal, () => this.instance.getActiveAction() === POLYGON_TOOL_ACTION_NAME, () => this.state === POLYGON_TOOL_STATE.DEFINING_SIZE, () => this.state === POLYGON_TOOL_STATE.DEFINING_SIZE && this.moved, () => this.cancelAction(), (modifiers) => this.handleMovement(modifiers));
26828
29004
  stage.on("pointermove", () => {
26829
29005
  if (this.state === POLYGON_TOOL_STATE.IDLE) return;
26830
29006
  this.setCursor();
26831
29007
  });
26832
29008
  stage.on("pointerdown", (e) => {
26833
29009
  this.setTapStart(e);
29010
+ this.pointers.set(e.evt.pointerId, {
29011
+ x: e.evt.clientX,
29012
+ y: e.evt.clientY
29013
+ });
26834
29014
  if (this.state !== POLYGON_TOOL_STATE.ADDING) return;
26835
29015
  this.handleAdding();
26836
29016
  });
29017
+ stage.on("pointermove", (e) => {
29018
+ if (this.state === POLYGON_TOOL_STATE.IDLE) return;
29019
+ this.setCursor();
29020
+ if (!this.isPressed(e) || !this.pointers.has(e.evt.pointerId)) return;
29021
+ if (this.state === POLYGON_TOOL_STATE.DEFINING_SIZE) {
29022
+ const pointerStart = this.pointers.get(e.evt.pointerId);
29023
+ if (!pointerStart || Math.hypot(e.evt.clientX - pointerStart.x, e.evt.clientY - pointerStart.y) < MIN_POLYGON_DRAG_DISTANCE) return;
29024
+ this.moved = true;
29025
+ this.handleMovement(e.evt);
29026
+ }
29027
+ });
29028
+ stage.on("pointerup", (e) => {
29029
+ this.pointers.delete(e.evt.pointerId);
29030
+ if (this.isTap(e)) this.moved = false;
29031
+ if (this.state === POLYGON_TOOL_STATE.DEFINING_SIZE) this.handleSettingSize(e.evt);
29032
+ });
29033
+ stage.on("pointercancel", (e) => {
29034
+ if (!this.pointers.has(e.evt.pointerId)) return;
29035
+ this.pointers.delete(e.evt.pointerId);
29036
+ if (this.state === POLYGON_TOOL_STATE.DEFINING_SIZE) this.cancelAction();
29037
+ });
26837
29038
  this.initialized = true;
26838
29039
  }
26839
29040
  setState(state) {
@@ -26847,7 +29048,10 @@ var WeavePolygonToolAction = class extends WeaveAction {
26847
29048
  }
26848
29049
  handleAdding() {
26849
29050
  const { mousePoint, container } = this.instance.getMousePointer();
29051
+ this.clickPoint = mousePoint;
29052
+ this.container = container;
26850
29053
  this.polygonId = v4();
29054
+ this.polygonNode = null;
26851
29055
  const presetDef = WEAVE_POLYGON_PRESETS[this.preset];
26852
29056
  const scaleFactor = this.props.scaleFactor ?? 1;
26853
29057
  const { points, innerRect, width, height } = instantiatePreset(presetDef, presetDef.defaultWidth * scaleFactor, presetDef.defaultHeight * scaleFactor);
@@ -26863,15 +29067,49 @@ var WeavePolygonToolAction = class extends WeaveAction {
26863
29067
  points,
26864
29068
  innerRect
26865
29069
  });
29070
+ this.polygonNode = node;
26866
29071
  this.instance.addNode(node, container?.getAttrs().id);
26867
29072
  }
26868
- this.instance.emitEvent("onAddedPolygon");
29073
+ this.setState(POLYGON_TOOL_STATE.DEFINING_SIZE);
29074
+ }
29075
+ handleMovement(modifiers = {}) {
29076
+ if (this.state !== POLYGON_TOOL_STATE.DEFINING_SIZE || !this.polygonId || !this.clickPoint || !this.container) return;
29077
+ const polygon = this.instance.getStage().findOne(`#${this.polygonId}`);
29078
+ const nodeHandler = this.instance.getNodeHandler(WEAVE_POLYGON_NODE_TYPE);
29079
+ const presetDef = WEAVE_POLYGON_PRESETS[this.preset];
29080
+ if (!polygon || !nodeHandler) return;
29081
+ const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.container);
29082
+ const defaultGeometry = instantiatePreset(presetDef, presetDef.defaultWidth, presetDef.defaultHeight);
29083
+ const bounds = getShapeDragBounds(this.clickPoint, mousePoint, {
29084
+ centered: modifiers.altKey,
29085
+ constrained: modifiers.shiftKey,
29086
+ aspectRatio: defaultGeometry.width / defaultGeometry.height
29087
+ });
29088
+ const geometry = instantiatePreset(presetDef, bounds.width * presetDef.defaultWidth / defaultGeometry.width, bounds.height * presetDef.defaultHeight / defaultGeometry.height);
29089
+ nodeHandler.onUpdate(polygon, {
29090
+ ...this.props,
29091
+ id: this.polygonId,
29092
+ x: bounds.x,
29093
+ y: bounds.y,
29094
+ ...geometry
29095
+ });
29096
+ }
29097
+ handleSettingSize(modifiers = {}) {
29098
+ if (this.moved) this.handleMovement(modifiers);
29099
+ const polygon = this.polygonId && this.instance.getStage().findOne(`#${this.polygonId}`);
29100
+ const nodeHandler = this.instance.getNodeHandler(WEAVE_POLYGON_NODE_TYPE);
29101
+ if (polygon && nodeHandler) {
29102
+ this.instance.updateNode(nodeHandler.serialize(polygon));
29103
+ this.placementCommitted = true;
29104
+ this.instance.emitEvent("onAddedPolygon");
29105
+ }
26869
29106
  this.cancelAction();
26870
29107
  }
26871
29108
  trigger(cancelAction, params) {
26872
29109
  if (!this.instance) throw new Error("Instance not defined");
26873
29110
  if (!this.initialized) this.setupEvents();
26874
29111
  this.preset = params?.presetId ?? "pentagon";
29112
+ this.placementCommitted = false;
26875
29113
  const stage = this.instance.getStage();
26876
29114
  stage.container().tabIndex = 1;
26877
29115
  stage.container().focus();
@@ -26884,13 +29122,21 @@ var WeavePolygonToolAction = class extends WeaveAction {
26884
29122
  cleanup() {
26885
29123
  const stage = this.instance.getStage();
26886
29124
  stage.container().style.cursor = "default";
29125
+ const isCancelledPlacement = this.state === POLYGON_TOOL_STATE.DEFINING_SIZE && this.polygonId !== null && !this.placementCommitted;
29126
+ if (isCancelledPlacement && this.polygonNode) this.instance.removeNode(this.polygonNode);
26887
29127
  const selectionPlugin = this.instance.getPlugin("nodesSelection");
26888
29128
  if (selectionPlugin) {
26889
- const node = stage.findOne(`#${this.polygonId}`);
29129
+ const node = isCancelledPlacement ? void 0 : stage.findOne(`#${this.polygonId}`);
26890
29130
  if (node) selectionPlugin.setSelectedNodes([node]);
26891
29131
  this.instance.triggerAction(SELECTION_TOOL_ACTION_NAME);
26892
29132
  }
26893
29133
  this.polygonId = null;
29134
+ this.pointers.clear();
29135
+ this.clickPoint = null;
29136
+ this.container = void 0;
29137
+ this.moved = false;
29138
+ this.placementCommitted = false;
29139
+ this.polygonNode = null;
26894
29140
  this.setState(POLYGON_TOOL_STATE.IDLE);
26895
29141
  }
26896
29142
  setCursor() {
@@ -33303,4 +35549,4 @@ function getJSONFromYjsBinary(actualState) {
33303
35549
  return JSON.parse(actualStateString);
33304
35550
  }
33305
35551
  //#endregion
33306
- export { ALIGN_NODES_ALIGN_TO, ALIGN_NODES_TOOL_ACTION_NAME, ALIGN_NODES_TOOL_STATE, BRUSH_TOOL_ACTION_NAME, BRUSH_TOOL_DEFAULT_CONFIG, BRUSH_TOOL_STATE, CONNECTOR_TOOL_ACTION_NAME, CONNECTOR_TOOL_DEFAULT_CONFIG, CONNECTOR_TOOL_STATE, COPY_PASTE_NODES_PLUGIN_STATE, DEFAULT_GUIDE_TOOL_ACTION_CONFIG, DEFAULT_SNAPPING_MANAGER_CONFIG, ELLIPSE_TOOL_ACTION_NAME, ELLIPSE_TOOL_STATE, ERASER_TOOL_ACTION_NAME, ERASER_TOOL_STATE, FRAME_TOOL_ACTION_NAME, FRAME_TOOL_STATE, GUIDE_DISTANCE_NAME, GUIDE_DISTANCE_ORIGIN, GUIDE_KIND, GUIDE_NAME, GUIDE_ORIENTATION, GUIDE_STATE, GUIDE_TOOL_ACTION_NAME, GUIDE_TOOL_STATE, LINE_TOOL_ACTION_NAME, LINE_TOOL_DEFAULT_CONFIG, LINE_TOOL_STATE, MEASURE_TOOL_ACTION_NAME, MEASURE_TOOL_STATE, MOVE_ORIENTATION, MOVE_TOOL_ACTION_NAME, MOVE_TOOL_STATE, PEN_TOOL_ACTION_NAME, PEN_TOOL_STATE, POLYGON_TOOL_ACTION_NAME, POLYGON_TOOL_STATE, PREVIEW_HANDOVER_MAX_FRAMES, PREVIEW_ID_SUFFIX, RECTANGLE_TOOL_ACTION_NAME, RECTANGLE_TOOL_STATE, REGULAR_POLYGON_TOOL_ACTION_NAME, REGULAR_POLYGON_TOOL_STATE, SELECTION_TOOL_ACTION_NAME, SELECTION_TOOL_STATE, STAGE_MINIMAP_DEFAULT_CONFIG, STAR_TOOL_ACTION_NAME, STAR_TOOL_STATE, TEXT_LAYOUT, TEXT_TOOL_ACTION_NAME, TEXT_TOOL_STATE, VIDEO_TOOL_ACTION_NAME, VIDEO_TOOL_STATE, WEAVE_ARROW_NODE_TYPE, WEAVE_ARROW_TOOL_ACTION_NAME, WEAVE_ARROW_TOOL_STATE, WEAVE_COMMENTS_RENDERER_KEY, WEAVE_COMMENTS_TOOL_LAYER_ID, WEAVE_COMMENT_CREATE_ACTION, WEAVE_COMMENT_NODE_ACTION, WEAVE_COMMENT_NODE_DEFAULTS, WEAVE_COMMENT_NODE_TYPE, WEAVE_COMMENT_STATUS, WEAVE_COMMENT_TOOL_ACTION_NAME, WEAVE_COMMENT_TOOL_DEFAULT_CONFIG, WEAVE_COMMENT_TOOL_STATE, WEAVE_COMMENT_VIEW_ACTION, WEAVE_CONNECTOR_NODE_ANCHOR_ORIGIN, WEAVE_CONNECTOR_NODE_DECORATOR_TYPE, WEAVE_CONNECTOR_NODE_DEFAULT_CONFIG, WEAVE_CONNECTOR_NODE_LINE_ORIGIN, WEAVE_CONNECTOR_NODE_LINE_TYPE, WEAVE_CONNECTOR_NODE_TYPE, WEAVE_COPY_PASTE_CONFIG_DEFAULT, WEAVE_COPY_PASTE_NODES_KEY, WEAVE_COPY_PASTE_PASTE_CATCHER_ID, WEAVE_COPY_PASTE_PASTE_MODES, WEAVE_DEFAULT_USER_INFO_FUNCTION, WEAVE_ELLIPSE_NODE_TYPE, WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, WEAVE_FRAME_NODE_DEFAULT_CONFIG, WEAVE_FRAME_NODE_DEFAULT_PROPS, WEAVE_FRAME_NODE_TYPE, WEAVE_GRID_DEFAULT_CONFIG, WEAVE_GRID_DOT_TYPES, WEAVE_GRID_LAYER_ID, WEAVE_GRID_TYPES, WEAVE_GROUP_NODE_TYPE, WEAVE_IMAGES_TOOL_ACTION_NAME, WEAVE_IMAGES_TOOL_DEFAULT_CONFIG, WEAVE_IMAGES_TOOL_STATE, WEAVE_IMAGES_TOOL_UPLOAD_TYPE, WEAVE_IMAGE_CROP_ANCHOR_POSITION, WEAVE_IMAGE_CROP_END_TYPE, WEAVE_IMAGE_DEFAULT_CONFIG, WEAVE_IMAGE_NODE_TYPE, WEAVE_IMAGE_TOOL_ACTION_NAME, WEAVE_IMAGE_TOOL_CONFIG_DEFAULT, WEAVE_IMAGE_TOOL_STATE, WEAVE_IMAGE_TOOL_UPLOAD_TYPE, WEAVE_LAYER_NODE_TYPE, WEAVE_LINE_NODE_DEFAULT_CONFIG, WEAVE_LINE_NODE_TYPE, WEAVE_MEASURE_NODE_DEFAULT_CONFIG, WEAVE_MEASURE_NODE_TYPE, WEAVE_MEASURE_TOOL_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_LAYER_ID, WEAVE_NODES_SELECTION_DEFAULT_CONFIG, WEAVE_NODES_SELECTION_KEY, WEAVE_NODES_SELECTION_LAYER_ID, WEAVE_NODES_SNAPPING_PLUGIN_KEY, WEAVE_POLYGON_NODE_TYPE, WEAVE_POLYGON_PRESETS, WEAVE_RECTANGLE_NODE_TYPE, WEAVE_REGULAR_POLYGON_NODE_TYPE, WEAVE_SHAPE_LABEL_DEFAULTS, WEAVE_STAGE_DEFAULT_MODE, WEAVE_STAGE_DROP_AREA_KEY, WEAVE_STAGE_GRID_PLUGIN_KEY, WEAVE_STAGE_IMAGE_CROPPING_MODE, WEAVE_STAGE_KEYBOARD_MOVE_DEFAULT_CONFIG, WEAVE_STAGE_KEYBOARD_MOVE_KEY, WEAVE_STAGE_KEYBOARD_MOVE_ORIENTATION, WEAVE_STAGE_MINIMAP_KEY, WEAVE_STAGE_NODE_TYPE, WEAVE_STAGE_PANNING_DEFAULT_CONFIG, WEAVE_STAGE_PANNING_KEY, WEAVE_STAGE_PANNING_THROTTLE_MS, WEAVE_STAGE_SHAPE_LABEL_EDITION_MODE, WEAVE_STAGE_TEXT_EDITION_MODE, WEAVE_STAGE_ZOOM_DEFAULT_CONFIG, WEAVE_STAGE_ZOOM_KEY, WEAVE_STAGE_ZOOM_TYPE, WEAVE_STAR_NODE_TYPE, WEAVE_STROKE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_NODE_TYPE, WEAVE_STROKE_SINGLE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_SINGLE_NODE_TIP_SIDE, WEAVE_STROKE_SINGLE_NODE_TIP_TYPE, WEAVE_STROKE_SINGLE_NODE_TYPE, WEAVE_STROKE_TOOL_ACTION_NAME, WEAVE_STROKE_TOOL_ACTION_NAME_ALIASES, WEAVE_STROKE_TOOL_DEFAULT_CONFIG, WEAVE_STROKE_TOOL_STATE, WEAVE_TEXT_LINK_ALLOWED_PROTOCOLS, WEAVE_TEXT_NODE_DEFAULT_CONFIG, WEAVE_TEXT_NODE_TYPE, WEAVE_USERS_POINTERS_CONFIG_DEFAULT_PROPS, WEAVE_USERS_POINTERS_KEY, WEAVE_USERS_PRESENCE_CONFIG_DEFAULT_PROPS, WEAVE_USERS_PRESENCE_PLUGIN_KEY, WEAVE_USERS_SELECTION_KEY, WEAVE_USER_POINTER_KEY, WEAVE_USER_PRESENCE_KEY, WEAVE_USER_SELECTION_KEY, WEAVE_VIDEO_DEFAULT_CONFIG, WEAVE_VIDEO_NODE_TYPE, Weave, WeaveAction, WeaveAlignNodesToolAction, WeaveArrowNode, WeaveArrowToolAction, WeaveBrushToolAction, WeaveCommentNode, WeaveCommentToolAction, WeaveCommentsRendererPlugin, WeaveConnectedUsersPlugin, WeaveConnectorNode, WeaveConnectorToolAction, WeaveContextMenuPlugin, WeaveCopyPasteNodesPlugin, WeaveEllipseNode, WeaveEllipseToolAction, WeaveEraserToolAction, WeaveExportNodesToolAction, WeaveExportStageToolAction, WeaveFitToScreenToolAction, WeaveFitToSelectionToolAction, WeaveFrameNode, WeaveFrameToolAction, WeaveGroupNode, WeaveGuideToolAction, WeaveImageNode, WeaveImageToolAction, WeaveImagesToolAction, WeaveLayerNode, WeaveLineNode, WeaveLineToolAction, WeaveMeasureNode, WeaveMeasureToolAction, WeaveMoveToolAction, WeaveNode, WeaveNodesMultiSelectionFeedbackPlugin, WeaveNodesSelectionPlugin, WeaveNodesSnappingPlugin, WeavePenToolAction, WeavePlugin, WeavePolygonNode, WeavePolygonToolAction, WeaveRectangleNode, WeaveRectangleToolAction, WeaveRegularPolygonNode, WeaveRegularPolygonToolAction, WeaveRenderer, WeaveSelectionToolAction, WeaveStageDropAreaPlugin, WeaveStageGridPlugin, WeaveStageKeyboardMovePlugin, WeaveStageMinimapPlugin, WeaveStageNode, WeaveStagePanningPlugin, WeaveStageResizePlugin, WeaveStageZoomPlugin, WeaveStarNode, WeaveStarToolAction, WeaveStateManipulation, WeaveStore, WeaveStrokeNode, WeaveStrokeSingleNode, WeaveStrokeToolAction, WeaveTextNode, WeaveTextToolAction, WeaveUsersPointersPlugin, WeaveUsersPresencePlugin, WeaveUsersSelectionPlugin, WeaveVideoNode, WeaveVideoToolAction, WeaveZoomInToolAction, WeaveZoomOutToolAction, buildAncestorGroupIds, canComposite, clearContainerTargets, computeEllipseLabelMinSize, computePolygonLabelMinSize, computeRectangleLabelMinSize, containerOverCursor, containsNodeDeep, defaultInitialState, downscaleImageFile, downscaleImageFromURL, getBoundingBox, getDownscaleRatio, getExportBoundingBox, getImageSizeFromFile, getJSONFromYjsBinary, getSelectedNodesMetadata, getShapeLabelSchemaFields, getStageClickPoint, getTargetAndSkipNodes, getTargetedNode, getTopmostShadowHost, getVisibleNodes, getVisibleNodesInViewport, hasFrames, hasImages, instantiatePreset, intersectArrays, isArray, isIOS, isInShadowDOM, isNodeInSelection, isNumber, isObject, isServer, labelId, loadImageSource, mapJsonToYjsArray, mapJsonToYjsElements, mapJsonToYjsMap, memoize, mergeExceptArrays, moveNodeToContainer, moveNodeToContainerNT, resetScale, sleep, spreadLabelProps, weavejsToYjsBinary };
35552
+ export { ALIGN_NODES_ALIGN_TO, ALIGN_NODES_TOOL_ACTION_NAME, ALIGN_NODES_TOOL_STATE, BRUSH_TOOL_ACTION_NAME, BRUSH_TOOL_DEFAULT_CONFIG, BRUSH_TOOL_STATE, CONNECTOR_TOOL_ACTION_NAME, CONNECTOR_TOOL_DEFAULT_CONFIG, CONNECTOR_TOOL_STATE, COPY_PASTE_NODES_PLUGIN_STATE, DEFAULT_GUIDE_TOOL_ACTION_CONFIG, DEFAULT_SNAPPING_MANAGER_CONFIG, ELLIPSE_TOOL_ACTION_NAME, ELLIPSE_TOOL_STATE, ERASER_TOOL_ACTION_NAME, ERASER_TOOL_STATE, FRAME_TOOL_ACTION_NAME, FRAME_TOOL_STATE, GUIDE_DISTANCE_NAME, GUIDE_DISTANCE_ORIGIN, GUIDE_KIND, GUIDE_NAME, GUIDE_ORIENTATION, GUIDE_STATE, GUIDE_TOOL_ACTION_NAME, GUIDE_TOOL_STATE, LINE_TOOL_ACTION_NAME, LINE_TOOL_DEFAULT_CONFIG, LINE_TOOL_STATE, MEASURE_TOOL_ACTION_NAME, MEASURE_TOOL_STATE, MOVE_ORIENTATION, MOVE_TOOL_ACTION_NAME, MOVE_TOOL_STATE, PEN_TOOL_ACTION_NAME, PEN_TOOL_STATE, POLYGON_TOOL_ACTION_NAME, POLYGON_TOOL_STATE, PREVIEW_HANDOVER_MAX_FRAMES, PREVIEW_ID_SUFFIX, RECTANGLE_TOOL_ACTION_NAME, RECTANGLE_TOOL_STATE, REGULAR_POLYGON_TOOL_ACTION_NAME, REGULAR_POLYGON_TOOL_STATE, SELECTION_TOOL_ACTION_NAME, SELECTION_TOOL_STATE, STAGE_MINIMAP_DEFAULT_CONFIG, STAR_TOOL_ACTION_NAME, STAR_TOOL_STATE, TEXT_LAYOUT, TEXT_TOOL_ACTION_NAME, TEXT_TOOL_STATE, TextLayoutEngine, VIDEO_TOOL_ACTION_NAME, VIDEO_TOOL_STATE, WEAVE_ARROW_NODE_TYPE, WEAVE_ARROW_TOOL_ACTION_NAME, WEAVE_ARROW_TOOL_STATE, WEAVE_COMMENTS_RENDERER_KEY, WEAVE_COMMENTS_TOOL_LAYER_ID, WEAVE_COMMENT_CREATE_ACTION, WEAVE_COMMENT_NODE_ACTION, WEAVE_COMMENT_NODE_DEFAULTS, WEAVE_COMMENT_NODE_TYPE, WEAVE_COMMENT_STATUS, WEAVE_COMMENT_TOOL_ACTION_NAME, WEAVE_COMMENT_TOOL_DEFAULT_CONFIG, WEAVE_COMMENT_TOOL_STATE, WEAVE_COMMENT_VIEW_ACTION, WEAVE_CONNECTOR_NODE_ANCHOR_ORIGIN, WEAVE_CONNECTOR_NODE_DECORATOR_TYPE, WEAVE_CONNECTOR_NODE_DEFAULT_CONFIG, WEAVE_CONNECTOR_NODE_LINE_ORIGIN, WEAVE_CONNECTOR_NODE_LINE_TYPE, WEAVE_CONNECTOR_NODE_TYPE, WEAVE_COPY_PASTE_CONFIG_DEFAULT, WEAVE_COPY_PASTE_NODES_KEY, WEAVE_COPY_PASTE_PASTE_CATCHER_ID, WEAVE_COPY_PASTE_PASTE_MODES, WEAVE_DEFAULT_USER_INFO_FUNCTION, WEAVE_ELLIPSE_NODE_TYPE, WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, WEAVE_FRAME_NODE_DEFAULT_CONFIG, WEAVE_FRAME_NODE_DEFAULT_PROPS, WEAVE_FRAME_NODE_TYPE, WEAVE_GRID_DEFAULT_CONFIG, WEAVE_GRID_DOT_TYPES, WEAVE_GRID_LAYER_ID, WEAVE_GRID_TYPES, WEAVE_GROUP_NODE_TYPE, WEAVE_IMAGES_TOOL_ACTION_NAME, WEAVE_IMAGES_TOOL_DEFAULT_CONFIG, WEAVE_IMAGES_TOOL_STATE, WEAVE_IMAGES_TOOL_UPLOAD_TYPE, WEAVE_IMAGE_CROP_ANCHOR_POSITION, WEAVE_IMAGE_CROP_END_TYPE, WEAVE_IMAGE_DEFAULT_CONFIG, WEAVE_IMAGE_NODE_TYPE, WEAVE_IMAGE_TOOL_ACTION_NAME, WEAVE_IMAGE_TOOL_CONFIG_DEFAULT, WEAVE_IMAGE_TOOL_STATE, WEAVE_IMAGE_TOOL_UPLOAD_TYPE, WEAVE_LAYER_NODE_TYPE, WEAVE_LINE_NODE_DEFAULT_CONFIG, WEAVE_LINE_NODE_TYPE, WEAVE_MEASURE_NODE_DEFAULT_CONFIG, WEAVE_MEASURE_NODE_TYPE, WEAVE_MEASURE_TOOL_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_LAYER_ID, WEAVE_NODES_SELECTION_DEFAULT_CONFIG, WEAVE_NODES_SELECTION_KEY, WEAVE_NODES_SELECTION_LAYER_ID, WEAVE_NODES_SNAPPING_PLUGIN_KEY, WEAVE_POLYGON_NODE_TYPE, WEAVE_POLYGON_PRESETS, WEAVE_RECTANGLE_NODE_TYPE, WEAVE_REGULAR_POLYGON_NODE_TYPE, WEAVE_SHAPE_LABEL_DEFAULTS, WEAVE_STAGE_DEFAULT_MODE, WEAVE_STAGE_DROP_AREA_KEY, WEAVE_STAGE_GRID_PLUGIN_KEY, WEAVE_STAGE_IMAGE_CROPPING_MODE, WEAVE_STAGE_KEYBOARD_MOVE_DEFAULT_CONFIG, WEAVE_STAGE_KEYBOARD_MOVE_KEY, WEAVE_STAGE_KEYBOARD_MOVE_ORIENTATION, WEAVE_STAGE_MINIMAP_KEY, WEAVE_STAGE_NODE_TYPE, WEAVE_STAGE_PANNING_DEFAULT_CONFIG, WEAVE_STAGE_PANNING_KEY, WEAVE_STAGE_PANNING_THROTTLE_MS, WEAVE_STAGE_SHAPE_LABEL_EDITION_MODE, WEAVE_STAGE_TEXT_EDITION_MODE, WEAVE_STAGE_ZOOM_DEFAULT_CONFIG, WEAVE_STAGE_ZOOM_KEY, WEAVE_STAGE_ZOOM_TYPE, WEAVE_STAR_NODE_TYPE, WEAVE_STROKE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_NODE_TYPE, WEAVE_STROKE_SINGLE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_SINGLE_NODE_TIP_SIDE, WEAVE_STROKE_SINGLE_NODE_TIP_TYPE, WEAVE_STROKE_SINGLE_NODE_TYPE, WEAVE_STROKE_TOOL_ACTION_NAME, WEAVE_STROKE_TOOL_ACTION_NAME_ALIASES, WEAVE_STROKE_TOOL_DEFAULT_CONFIG, WEAVE_STROKE_TOOL_STATE, WEAVE_TEXT_LINK_ALLOWED_PROTOCOLS, WEAVE_TEXT_NODE_DEFAULT_CONFIG, WEAVE_TEXT_NODE_TYPE, WEAVE_USERS_POINTERS_CONFIG_DEFAULT_PROPS, WEAVE_USERS_POINTERS_KEY, WEAVE_USERS_PRESENCE_CONFIG_DEFAULT_PROPS, WEAVE_USERS_PRESENCE_PLUGIN_KEY, WEAVE_USERS_SELECTION_KEY, WEAVE_USER_POINTER_KEY, WEAVE_USER_PRESENCE_KEY, WEAVE_USER_SELECTION_KEY, WEAVE_VIDEO_DEFAULT_CONFIG, WEAVE_VIDEO_NODE_TYPE, Weave, WeaveAction, WeaveAlignNodesToolAction, WeaveArrowNode, WeaveArrowToolAction, WeaveBrushToolAction, WeaveCommentNode, WeaveCommentToolAction, WeaveCommentsRendererPlugin, WeaveConnectedUsersPlugin, WeaveConnectorNode, WeaveConnectorToolAction, WeaveContextMenuPlugin, WeaveCopyPasteNodesPlugin, WeaveEllipseNode, WeaveEllipseToolAction, WeaveEraserToolAction, WeaveExportNodesToolAction, WeaveExportStageToolAction, WeaveFitToScreenToolAction, WeaveFitToSelectionToolAction, WeaveFrameNode, WeaveFrameToolAction, WeaveGroupNode, WeaveGuideToolAction, WeaveImageNode, WeaveImageToolAction, WeaveImagesToolAction, WeaveLayerNode, WeaveLineNode, WeaveLineToolAction, WeaveMeasureNode, WeaveMeasureToolAction, WeaveMoveToolAction, WeaveNode, WeaveNodesMultiSelectionFeedbackPlugin, WeaveNodesSelectionPlugin, WeaveNodesSnappingPlugin, WeavePenToolAction, WeavePlugin, WeavePolygonNode, WeavePolygonToolAction, WeaveRectangleNode, WeaveRectangleToolAction, WeaveRegularPolygonNode, WeaveRegularPolygonToolAction, WeaveRenderer, WeaveSelectionToolAction, WeaveStageDropAreaPlugin, WeaveStageGridPlugin, WeaveStageKeyboardMovePlugin, WeaveStageMinimapPlugin, WeaveStageNode, WeaveStagePanningPlugin, WeaveStageResizePlugin, WeaveStageZoomPlugin, WeaveStarNode, WeaveStarToolAction, WeaveStateManipulation, WeaveStore, WeaveStrokeNode, WeaveStrokeSingleNode, WeaveStrokeToolAction, WeaveTextNode, WeaveTextToolAction, WeaveTextV2Node, WeaveTextV2Shape, WeaveUsersPointersPlugin, WeaveUsersPresencePlugin, WeaveUsersSelectionPlugin, WeaveVideoNode, WeaveVideoToolAction, WeaveZoomInToolAction, WeaveZoomOutToolAction, buildAncestorGroupIds, canComposite, clearContainerTargets, computeEllipseLabelMinSize, computePolygonLabelMinSize, computeRectangleLabelMinSize, containerOverCursor, containsNodeDeep, defaultInitialState, downscaleImageFile, downscaleImageFromURL, getBoundingBox, getDownscaleRatio, getExportBoundingBox, getImageSizeFromFile, getJSONFromYjsBinary, getSelectedNodesMetadata, getShapeLabelSchemaFields, getStageClickPoint, getTargetAndSkipNodes, getTargetedNode, getTopmostShadowHost, getVisibleNodes, getVisibleNodesInViewport, hasFrames, hasImages, instantiatePreset, intersectArrays, isArray, isIOS, isInShadowDOM, isNodeInSelection, isNumber, isObject, isServer, labelId, loadImageSource, mapJsonToYjsArray, mapJsonToYjsElements, mapJsonToYjsMap, memoize, mergeExceptArrays, moveNodeToContainer, moveNodeToContainerNT, resetScale, sleep, spreadLabelProps, weavejsToYjsBinary };