canvu-react 0.4.30 → 0.4.32

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/react.cjs CHANGED
@@ -56,11 +56,170 @@ var init_custom_shape = __esm({
56
56
  }
57
57
  });
58
58
 
59
+ // src/scene/link-item.ts
60
+ function buildLinkCardSvg(width, _height, link) {
61
+ const cardWidth = Math.max(1, width);
62
+ const scale = cardWidth / DEFAULT_LINK_CARD_WIDTH;
63
+ const contentWidth = DEFAULT_LINK_CARD_WIDTH;
64
+ const contentHeight = DEFAULT_LINK_CARD_HEIGHT;
65
+ const padding = 14;
66
+ const badgeSize = 42;
67
+ const gap = 13;
68
+ const buttonSize = 34;
69
+ const textX = padding + badgeSize + gap;
70
+ const textWidth = Math.max(1, contentWidth - textX - buttonSize - gap - padding);
71
+ const hostname = getLinkHostname(link.href);
72
+ const title = link.title?.trim() || hostname || "Link";
73
+ const protocol = getLinkProtocol(link.href);
74
+ const subtitle = hostname || link.href;
75
+ const favicon = link.favicon?.trim() || buildGoogleFaviconUrl(hostname);
76
+ const idSuffix = getStableLinkIdSuffix(`${hostname}:${link.href}`);
77
+ const clipId = `canvu-link-favicon-${idSuffix}`;
78
+ const gradientId = `canvu-link-favicon-gradient-${idSuffix}`;
79
+ const buttonX = contentWidth - padding - buttonSize;
80
+ const buttonY = (contentHeight - buttonSize) / 2;
81
+ const isSecure = protocol === "https:";
82
+ const subtitleX = isSecure ? textX + 13 : textX;
83
+ const subtitleWidth = isSecure ? textWidth - 13 : textWidth;
84
+ const faviconImage = favicon ? `<image class="canvu-link-favicon-img" href="${escapeXmlAttribute(favicon)}" x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${clipId})" />` : "";
85
+ return `
86
+ <style>
87
+ .canvu-link-card-root .canvu-link-card { transition: transform .18s ease, filter .18s ease, stroke .18s ease; }
88
+ .canvu-link-card-root .canvu-link-open { opacity: 0; transform: translateX(-4px); transition: opacity .18s ease, transform .18s ease; }
89
+ .canvu-link-card-root:hover .canvu-link-card { transform: translateY(-2px); filter: drop-shadow(0 4px 14px oklch(0.4 0.05 265 / .08)) drop-shadow(0 1px 3px oklch(0.4 0.05 265 / .06)); stroke: ${LINK_CARD_BORDER_STRONG}; }
90
+ .canvu-link-card-root:hover .canvu-link-open { opacity: 1; transform: translateX(0); }
91
+ </style>
92
+ <g class="canvu-link-card-root" transform="scale(${formatNumber(scale)})">
93
+ <rect class="canvu-link-card" width="${formatNumber(contentWidth)}" height="${formatNumber(contentHeight)}" rx="16" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1" filter="drop-shadow(0 1px 2px oklch(0.4 0.03 265 / .05)) drop-shadow(0 1px 1px oklch(0.4 0.03 265 / .04))" />
94
+ <defs>
95
+ <linearGradient id="${gradientId}" x1="8" y1="48" x2="48" y2="8" gradientUnits="userSpaceOnUse">
96
+ <stop stop-color="${LINK_CARD_ACCENT}" />
97
+ <stop offset="1" stop-color="${LINK_CARD_ACCENT_DEEP}" />
98
+ </linearGradient>
99
+ <clipPath id="${clipId}">
100
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" />
101
+ </clipPath>
102
+ </defs>
103
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" fill="url(#${gradientId})" />
104
+ <text x="${formatNumber(padding + badgeSize / 2)}" y="${formatNumber(padding + badgeSize / 2 + 5)}" text-anchor="middle" font-family="system-ui,sans-serif" font-size="17" font-weight="700" fill="#ffffff">${escapeHtmlText(getLinkInitial(hostname))}</text>
105
+ ${faviconImage}
106
+ ${buildLinkTextBand({ x: textX, y: 16, width: textWidth, height: 19, text: title, fontSize: 14.5, color: LINK_CARD_TITLE_COLOR, fontWeight: 700 })}
107
+ ${isSecure ? `<g transform="translate(${formatNumber(textX)},40)" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" fill="none"><rect x="1.5" y="4.5" width="7" height="6" rx="1" /><path d="M3 4.5 V3 a2 2 0 0 1 4 0 v1.5" /></g>` : ""}
108
+ ${buildLinkTextBand({ x: subtitleX, y: 36, width: subtitleWidth, height: 17, text: subtitle, fontSize: 12.5, color: LINK_CARD_TEXT_COLOR })}
109
+ <g class="canvu-link-open" transform="translate(${formatNumber(buttonX)},${formatNumber(buttonY)})">
110
+ <rect width="${formatNumber(buttonSize)}" height="${formatNumber(buttonSize)}" rx="10" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1" />
111
+ <g transform="translate(10,10)" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none">
112
+ <path d="M8 2 H14 V8" />
113
+ <path d="M14 2 L7 9" />
114
+ <path d="M12 10 V13 a1 1 0 0 1 -1 1 H3 a1 1 0 0 1 -1 -1 V5 a1 1 0 0 1 1 -1 H6" />
115
+ </g>
116
+ </g>
117
+ </g>
118
+ `;
119
+ }
120
+ function getLinkData(item) {
121
+ const entry = item.pluginData?.[LINK_PLUGIN_KEY];
122
+ return isCanvuLinkData(entry) ? entry : null;
123
+ }
124
+ function rebuildLinkItemSvg(item) {
125
+ const link = getLinkData(item);
126
+ if (!link) return item;
127
+ const scale = clamp(
128
+ item.bounds.width / DEFAULT_LINK_CARD_WIDTH,
129
+ LINK_CARD_MIN_SCALE,
130
+ LINK_CARD_MAX_SCALE
131
+ );
132
+ const width = DEFAULT_LINK_CARD_WIDTH * scale;
133
+ const height = DEFAULT_LINK_CARD_HEIGHT * scale;
134
+ const bounds = {
135
+ ...item.bounds,
136
+ width,
137
+ height
138
+ };
139
+ const customInnerSvg = buildLinkCardSvg(
140
+ DEFAULT_LINK_CARD_WIDTH,
141
+ DEFAULT_LINK_CARD_HEIGHT,
142
+ link
143
+ );
144
+ return {
145
+ ...item,
146
+ x: bounds.x,
147
+ y: bounds.y,
148
+ bounds,
149
+ customIntrinsicSize: {
150
+ width: DEFAULT_LINK_CARD_WIDTH,
151
+ height: DEFAULT_LINK_CARD_HEIGHT
152
+ },
153
+ customInnerSvg,
154
+ childrenSvg: buildLinkCardSvg(width, height, link)
155
+ };
156
+ }
157
+ var LINK_PLUGIN_KEY, DEFAULT_LINK_CARD_WIDTH, DEFAULT_LINK_CARD_HEIGHT, LINK_CARD_MIN_SCALE, LINK_CARD_MAX_SCALE, LINK_CARD_ASPECT, LINK_CARD_BORDER, LINK_CARD_BORDER_STRONG, LINK_CARD_ACCENT, LINK_CARD_ACCENT_DEEP, LINK_CARD_TITLE_COLOR, LINK_CARD_TEXT_COLOR, clamp, formatNumber, escapeXmlAttribute, escapeHtmlText, getLinkHostname, buildLinkTextBand, getLinkProtocol, getLinkInitial, buildGoogleFaviconUrl, getStableLinkIdSuffix, isCanvuLinkData;
158
+ var init_link_item = __esm({
159
+ "src/scene/link-item.ts"() {
160
+ LINK_PLUGIN_KEY = "canvuLink";
161
+ DEFAULT_LINK_CARD_WIDTH = 320;
162
+ DEFAULT_LINK_CARD_HEIGHT = 70;
163
+ LINK_CARD_MIN_SCALE = 0.6;
164
+ LINK_CARD_MAX_SCALE = 2.5;
165
+ LINK_CARD_ASPECT = DEFAULT_LINK_CARD_WIDTH / DEFAULT_LINK_CARD_HEIGHT;
166
+ LINK_CARD_BORDER = "oklch(0.918 0.008 255)";
167
+ LINK_CARD_BORDER_STRONG = "oklch(0.86 0.012 255)";
168
+ LINK_CARD_ACCENT = "oklch(0.55 0.19 264)";
169
+ LINK_CARD_ACCENT_DEEP = "oklch(0.46 0.18 264)";
170
+ LINK_CARD_TITLE_COLOR = "oklch(0.26 0.022 265)";
171
+ LINK_CARD_TEXT_COLOR = "oklch(0.55 0.022 265)";
172
+ clamp = (value, min, max) => Math.min(max, Math.max(min, value));
173
+ formatNumber = (value) => {
174
+ const rounded = Math.round(value * 100) / 100;
175
+ return Object.is(rounded, -0) ? "0" : String(rounded);
176
+ };
177
+ escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
178
+ escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
179
+ getLinkHostname = (href) => {
180
+ try {
181
+ return new URL(href).hostname.replace(/^www\./, "");
182
+ } catch {
183
+ return href;
184
+ }
185
+ };
186
+ buildLinkTextBand = (band) => {
187
+ const lineHeight = band.height;
188
+ const weight = band.fontWeight != null ? `font-weight:${band.fontWeight};` : "";
189
+ return `<foreignObject x="${formatNumber(band.x)}" y="${formatNumber(band.y)}" width="${formatNumber(Math.max(1, band.width))}" height="${formatNumber(Math.max(1, band.height))}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;font-family:system-ui,sans-serif;font-size:${formatNumber(band.fontSize)}px;line-height:${formatNumber(lineHeight)}px;letter-spacing:-0.01em;color:${band.color};overflow:hidden;white-space:nowrap;text-overflow:ellipsis;${weight}">${escapeHtmlText(band.text)}</div></foreignObject>`;
190
+ };
191
+ getLinkProtocol = (href) => {
192
+ try {
193
+ return new URL(href).protocol;
194
+ } catch {
195
+ return "";
196
+ }
197
+ };
198
+ getLinkInitial = (hostname) => {
199
+ const first = hostname.trim().charAt(0).toUpperCase();
200
+ return first || "L";
201
+ };
202
+ buildGoogleFaviconUrl = (hostname) => hostname ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=64` : null;
203
+ getStableLinkIdSuffix = (value) => {
204
+ let hash = 0;
205
+ for (const char of value) {
206
+ hash = hash * 31 + char.charCodeAt(0) >>> 0;
207
+ }
208
+ return hash.toString(36);
209
+ };
210
+ isCanvuLinkData = (value) => {
211
+ if (!value || typeof value !== "object") return false;
212
+ const candidate = value;
213
+ return typeof candidate.href === "string" && candidate.href.length > 0;
214
+ };
215
+ }
216
+ });
217
+
59
218
  // src/scene/text-svg.ts
60
219
  function escapeSvgTextContent(s) {
61
220
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
62
221
  }
63
- function escapeHtmlText(s) {
222
+ function escapeHtmlText2(s) {
64
223
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
65
224
  }
66
225
  function getSharedMeasureContext() {
@@ -161,9 +320,9 @@ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#1d1d1d",
161
320
  const trimmed = content.trim();
162
321
  const padTop = EDIT_TOP_PAD_RATIO * fontSize;
163
322
  if (trimmed.length === 0) {
164
- return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
323
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText2(PLACEHOLDER)}</div></foreignObject>`;
165
324
  }
166
- const body = escapeHtmlText(content);
325
+ const body = escapeHtmlText2(content);
167
326
  return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
168
327
  }
169
328
  var DEFAULT_TEXT_FONT_SIZE, DEFAULT_TEXT_TOOL_FONT_SIZE, TEXT_FONT_FAMILY, LINE_HEIGHT_RATIO, FIRST_LINE_BASELINE_RATIO, EDIT_TOP_PAD_RATIO, BOTTOM_PAD_RATIO, PLACEHOLDER, MIN_TEXT_BOX_W, MIN_TEXT_BOX_H, TEXT_PAD_X, MAX_TEXT_MEASURE_CACHE_ENTRIES, sharedMeasureContext, textMeasureCache;
@@ -820,6 +979,9 @@ function rebuildItemSvg(item) {
820
979
  )
821
980
  };
822
981
  }
982
+ if (getLinkData(item)) {
983
+ return rebuildLinkItemSvg(item);
984
+ }
823
985
  if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
824
986
  const b = normalizeRect(item.bounds);
825
987
  return {
@@ -1045,6 +1207,7 @@ var init_shape_builders = __esm({
1045
1207
  "src/scene/shape-builders.ts"() {
1046
1208
  init_rect();
1047
1209
  init_custom_shape();
1210
+ init_link_item();
1048
1211
  init_text_svg();
1049
1212
  DEFAULT_STROKE_STYLE = {
1050
1213
  stroke: "#1d1d1d",
@@ -2205,7 +2368,7 @@ var defaultLabels = {
2205
2368
  title: "Images",
2206
2369
  dragHandle: "Drag to reorder",
2207
2370
  focus: "Focus on canvas",
2208
- copy: "Copy",
2371
+ duplicate: "Duplicate",
2209
2372
  rotate: "Rotate",
2210
2373
  delete: "Delete",
2211
2374
  collapse: "Collapse images menu",
@@ -2231,7 +2394,11 @@ function ImagesMenu({
2231
2394
  if (managed.length === 0) {
2232
2395
  return null;
2233
2396
  }
2234
- const resolvedLabels = { ...defaultLabels, ...labels };
2397
+ const resolvedLabels = {
2398
+ ...defaultLabels,
2399
+ ...labels,
2400
+ duplicate: labels?.duplicate ?? labels?.copy ?? defaultLabels.duplicate
2401
+ };
2235
2402
  if (collapsed) {
2236
2403
  return /* @__PURE__ */ jsxRuntime.jsx(
2237
2404
  "button",
@@ -2345,7 +2512,7 @@ function ImagesMenuRow({
2345
2512
  ),
2346
2513
  /* @__PURE__ */ jsxRuntime.jsx(FocusTarget, { label: labels.focus, onFocus, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: thumbBoxStyle, children: src ? /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: "", style: thumbImgStyle, draggable: false }) : null }) }),
2347
2514
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: actionsColumnStyle, children: [
2348
- /* @__PURE__ */ jsxRuntime.jsx(ImagesMenuAction, { label: labels.copy, onClick: onCopy, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { size: 18 }) }),
2515
+ /* @__PURE__ */ jsxRuntime.jsx(ImagesMenuAction, { label: labels.duplicate, onClick: onCopy, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CopyPlus, { size: 18 }) }),
2349
2516
  /* @__PURE__ */ jsxRuntime.jsx(ImagesMenuAction, { label: labels.rotate, onClick: onRotate, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCw, { size: 18 }) }),
2350
2517
  /* @__PURE__ */ jsxRuntime.jsx(ImagesMenuAction, { label: labels.delete, onClick: onDelete, danger: true, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { size: 18 }) })
2351
2518
  ] })
@@ -6358,10 +6525,80 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
6358
6525
 
6359
6526
  // src/interaction/mutations.ts
6360
6527
  init_rect();
6528
+ init_link_item();
6361
6529
  init_shape_builders();
6362
6530
  init_text_svg();
6531
+ var LINK_CORNER_HANDLES = /* @__PURE__ */ new Set(["nw", "ne", "se", "sw"]);
6532
+ var clamp2 = (value, min, max) => Math.min(max, Math.max(min, value));
6533
+ var clampLinkResizeBounds = (startBounds, nextBounds, handle, intrinsicWidth) => {
6534
+ const next = normalizeRect(nextBounds);
6535
+ const scale = clamp2(
6536
+ next.width / Math.max(1e-9, intrinsicWidth),
6537
+ LINK_CARD_MIN_SCALE,
6538
+ LINK_CARD_MAX_SCALE
6539
+ );
6540
+ const width = intrinsicWidth * scale;
6541
+ const height = width / LINK_CARD_ASPECT;
6542
+ const start = normalizeRect(startBounds);
6543
+ const x0 = start.x;
6544
+ const y0 = start.y;
6545
+ const x1 = start.x + start.width;
6546
+ const y1 = start.y + start.height;
6547
+ switch (handle) {
6548
+ case "nw":
6549
+ return { x: x1 - width, y: y1 - height, width, height };
6550
+ case "ne":
6551
+ return { x: x0, y: y1 - height, width, height };
6552
+ case "sw":
6553
+ return { x: x1 - width, y: y0, width, height };
6554
+ default:
6555
+ return { x: x0, y: y0, width, height };
6556
+ }
6557
+ };
6363
6558
  function computeNewBoundsForResize(item, sb, handle, currentWorld) {
6364
6559
  const rot = getItemRotationRad(item);
6560
+ const link = getLinkData(item);
6561
+ if (link && item.customIntrinsicSize) {
6562
+ if (!LINK_CORNER_HANDLES.has(handle)) return sb;
6563
+ const intrinsicWidth = Math.max(1e-9, item.customIntrinsicSize.width);
6564
+ if (Math.abs(rot) < 1e-12) {
6565
+ const next = computeResizeBoundsFixedAspect(
6566
+ sb,
6567
+ handle,
6568
+ currentWorld,
6569
+ LINK_CARD_ASPECT
6570
+ );
6571
+ return clampLinkResizeBounds(sb, next, handle, intrinsicWidth);
6572
+ }
6573
+ const local2 = worldToItemLocal(
6574
+ currentWorld.x,
6575
+ currentWorld.y,
6576
+ sb.x,
6577
+ sb.y,
6578
+ sb.width,
6579
+ sb.height,
6580
+ rot
6581
+ );
6582
+ const localBounds2 = { x: 0, y: 0, width: sb.width, height: sb.height };
6583
+ const nextLocal = computeResizeBoundsFixedAspect(
6584
+ localBounds2,
6585
+ handle,
6586
+ local2,
6587
+ LINK_CARD_ASPECT
6588
+ );
6589
+ const clamped = clampLinkResizeBounds(
6590
+ localBounds2,
6591
+ nextLocal,
6592
+ handle,
6593
+ intrinsicWidth
6594
+ );
6595
+ return {
6596
+ x: sb.x + clamped.x,
6597
+ y: sb.y + clamped.y,
6598
+ width: clamped.width,
6599
+ height: clamped.height
6600
+ };
6601
+ }
6365
6602
  if (Math.abs(rot) < 1e-12) {
6366
6603
  if (item.toolKind === "image") {
6367
6604
  let aspect;
@@ -6834,17 +7071,8 @@ var SvgVectorRenderer = class {
6834
7071
  }
6835
7072
  };
6836
7073
 
6837
- // src/scene/link-item.ts
6838
- var LINK_PLUGIN_KEY = "canvuLink";
6839
- var isCanvuLinkData = (value) => {
6840
- if (!value || typeof value !== "object") return false;
6841
- const candidate = value;
6842
- return typeof candidate.href === "string" && candidate.href.length > 0;
6843
- };
6844
- function getLinkData(item) {
6845
- const entry = item.pluginData?.[LINK_PLUGIN_KEY];
6846
- return isCanvuLinkData(entry) ? entry : null;
6847
- }
7074
+ // src/react/VectorViewport.tsx
7075
+ init_link_item();
6848
7076
 
6849
7077
  // src/scene/scene.ts
6850
7078
  var VectorScene = class {
@@ -6938,8 +7166,10 @@ function smoothFreehandPointsToPathD(points) {
6938
7166
  }
6939
7167
 
6940
7168
  // src/react/InteractionOverlay.tsx
7169
+ init_link_item();
6941
7170
  init_shape_builders();
6942
7171
  var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
7172
+ var LINK_HANDLE_ORDER = ["nw", "ne", "se", "sw"];
6943
7173
  var ERASER_TINT = "#cbd5e1";
6944
7174
  var ERASER_TINT_OPACITY = 0.95;
6945
7175
  var ERASER_PREVIEW_OPACITY = 0.3;
@@ -6992,7 +7222,7 @@ function InteractionOverlay({
6992
7222
  ) }, it.id);
6993
7223
  }),
6994
7224
  showResizeHandles && bSingle && single && rotHandlePos && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6995
- HANDLE_ORDER.map((hid) => {
7225
+ (getLinkData(single) ? LINK_HANDLE_ORDER : HANDLE_ORDER).map((hid) => {
6996
7226
  const p = getHandleWorldPositionRotated(bSingle, hid, rotSingle);
6997
7227
  return /* @__PURE__ */ jsxRuntime.jsx(
6998
7228
  "circle",
@@ -7806,6 +8036,7 @@ function defaultPlacementWorld(tool, center) {
7806
8036
  lineWorld: [a, b]
7807
8037
  };
7808
8038
  }
8039
+ var LINK_CORNER_HANDLES2 = /* @__PURE__ */ new Set(["nw", "ne", "se", "sw"]);
7809
8040
  function pointInSelectedItemBounds(item, worldX, worldY) {
7810
8041
  const bounds = normalizeRect(item.bounds);
7811
8042
  const local = worldToItemLocal(
@@ -8283,7 +8514,11 @@ var VectorViewport = react.forwardRef(
8283
8514
  setEffectiveSelectedIdsRef.current = setEffectiveSelectedIds;
8284
8515
  toolIdRef.current = toolId;
8285
8516
  interactiveRef.current = interactive;
8286
- itemsRef.current = items;
8517
+ const normalizedItems = react.useMemo(
8518
+ () => items.map((item) => getLinkData(item) ? rebuildLinkItemSvg(item) : item),
8519
+ [items]
8520
+ );
8521
+ itemsRef.current = normalizedItems;
8287
8522
  onWorldPointerDownRef.current = onWorldPointerDown;
8288
8523
  const originalOnItemsChangeRef = react.useRef(onItemsChange);
8289
8524
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8304,7 +8539,10 @@ var VectorViewport = react.forwardRef(
8304
8539
  autoResetToolToRef.current = autoResetToolTo;
8305
8540
  const onToolChangeRequestRef = react.useRef(onToolChangeRequest);
8306
8541
  onToolChangeRequestRef.current = onToolChangeRequest;
8307
- const resolvedItems = react.useMemo(() => resolveArrowBindingsInScene(items), [items]);
8542
+ const resolvedItems = react.useMemo(
8543
+ () => resolveArrowBindingsInScene(normalizedItems),
8544
+ [normalizedItems]
8545
+ );
8308
8546
  const resolvedItemsRef = react.useRef(resolvedItems);
8309
8547
  resolvedItemsRef.current = resolvedItems;
8310
8548
  const liveId = react.useId();
@@ -9725,7 +9963,8 @@ var VectorViewport = react.forwardRef(
9725
9963
  handleRadiusWorld,
9726
9964
  rot
9727
9965
  );
9728
- if (hb) {
9966
+ const isLinkResizeHandle = hb && getLinkData(selected) ? LINK_CORNER_HANDLES2.has(hb) : Boolean(hb);
9967
+ if (hb && isLinkResizeHandle) {
9729
9968
  const snapRs = bakedSnapshot(selected.id);
9730
9969
  if (!snapRs) return;
9731
9970
  dragStateRef.current = {