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