hyperframes 0.8.31 → 0.8.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.
@@ -245,10 +245,8 @@
245
245
  return rects;
246
246
  }
247
247
 
248
- function textRectFor(element, directOnly) {
249
- const rects = textClientRects(element, directOnly);
248
+ function unionRects(rects) {
250
249
  if (rects.length === 0) return null;
251
-
252
250
  const union = rects.reduce(
253
251
  (acc, rect) => ({
254
252
  left: Math.min(acc.left, rect.left),
@@ -263,7 +261,6 @@
263
261
  bottom: Number.NEGATIVE_INFINITY,
264
262
  },
265
263
  );
266
-
267
264
  return toRect({
268
265
  ...union,
269
266
  width: union.right - union.left,
@@ -271,6 +268,38 @@
271
268
  });
272
269
  }
273
270
 
271
+ function visibleTextClientRects(element, directOnly) {
272
+ // Range rects stay geometrically present outside an overflow clip. Reduce
273
+ // them in viewport coordinates so overlap measures only paintable text.
274
+ let rects = textClientRects(element, directOnly).map(toRect);
275
+ for (
276
+ let ancestor = element.parentElement;
277
+ ancestor && rects.length > 0;
278
+ ancestor = ancestor.parentElement
279
+ ) {
280
+ const style = getComputedStyle(ancestor);
281
+ const clipX = clipsOverflowValue(style.overflowX || style.overflow);
282
+ const clipY = clipsOverflowValue(style.overflowY || style.overflow);
283
+ if (!clipX && !clipY) continue;
284
+ const clip = toRect(ancestor.getBoundingClientRect());
285
+ rects = rects
286
+ .map((rect) => {
287
+ const left = clipX ? Math.max(rect.left, clip.left) : rect.left;
288
+ const right = clipX ? Math.min(rect.right, clip.right) : rect.right;
289
+ const top = clipY ? Math.max(rect.top, clip.top) : rect.top;
290
+ const bottom = clipY ? Math.min(rect.bottom, clip.bottom) : rect.bottom;
291
+ if (right - left <= 0.5 || bottom - top <= 0.5) return null;
292
+ return toRect({ left, right, top, bottom, width: right - left, height: bottom - top });
293
+ })
294
+ .filter(Boolean);
295
+ }
296
+ return rects;
297
+ }
298
+
299
+ function textRectFor(element, directOnly) {
300
+ return unionRects(textClientRects(element, directOnly));
301
+ }
302
+
274
303
  function parsePx(value) {
275
304
  const parsed = Number.parseFloat(value);
276
305
  return Number.isFinite(parsed) ? parsed : 0;
@@ -317,9 +346,13 @@
317
346
  return hasBackground || hasImage || hasBorder || hasRadius;
318
347
  }
319
348
 
349
+ function clipsOverflowValue(value) {
350
+ return value && value !== "visible" && value !== "clip visible";
351
+ }
352
+
320
353
  function clipsOverflow(style) {
321
- return [style.overflowX, style.overflowY, style.overflow].some(
322
- (value) => value && value !== "visible" && value !== "clip visible",
354
+ return [style.overflowX, style.overflowY, style.overflow].some((value) =>
355
+ clipsOverflowValue(value),
323
356
  );
324
357
  }
325
358
 
@@ -594,8 +627,8 @@
594
627
  const blocks = [];
595
628
  for (const element of Array.from(root.querySelectorAll("*"))) {
596
629
  if (!isSolidTextBlock(element)) continue;
597
- const rects = textClientRects(element, true);
598
- const rect = textRectFor(element, true);
630
+ const rects = visibleTextClientRects(element, true);
631
+ const rect = unionRects(rects);
599
632
  if (rect) blocks.push({ element, rect, rects });
600
633
  }
601
634
  return blocks;
@@ -1300,7 +1333,7 @@
1300
1333
  const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
1301
1334
  const MIN_CONNECTOR_CHORD_PX = 8;
1302
1335
  for (const svg of Array.from(root.querySelectorAll("svg"))) {
1303
- if (!isVisibleElement(svg) || hasAllowOverflowFlag(svg)) continue;
1336
+ if (!isVisibleElement(svg)) continue;
1304
1337
  for (const path of Array.from(svg.querySelectorAll("path"))) {
1305
1338
  if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
1306
1339
  if (!isConnectorPath(svg, path)) continue;
@@ -1333,7 +1366,16 @@
1333
1366
  // Paste-into-`d` bug: both raw endpoints land on distinct anchors as screen pixels.
1334
1367
  const userStartKey = attachmentKey(user.start);
1335
1368
  const userEndKey = attachmentKey(user.end);
1336
- if (!userStartKey || !userEndKey || userStartKey === userEndKey) continue;
1369
+ const pasteBug = Boolean(userStartKey && userEndKey && userStartKey !== userEndKey);
1370
+ // Guessed marked shaft: both frames miss. Same-anchor grazes attach in user-space
1371
+ // and must stay skipped. Name-only decorative flow/arrow paths stay skipped.
1372
+ // 80px keeps short marker glyphs (chevrons, tips) out.
1373
+ const markedMiss =
1374
+ renderedChord >= 80 &&
1375
+ !userStartKey &&
1376
+ !userEndKey &&
1377
+ (path.hasAttribute("marker-start") || path.hasAttribute("marker-end"));
1378
+ if (!pasteBug && !markedMiss) continue;
1337
1379
  const gap = Math.round(
1338
1380
  Math.min(
1339
1381
  Math.min(...anchors.compact.map((a) => distanceToRect(rendered.start, a.rect))),
@@ -1346,7 +1388,113 @@
1346
1388
  time,
1347
1389
  selector: selectorFor(path),
1348
1390
  containerSelector: selectorFor(svg),
1349
- message: `Connector path endpoints render ${gap}px from the nearest anchorable element, but the path's user-space coordinates would attach if read as screen pixels — screen/viewport numbers were likely written into SVG \`d\` without inverting the CTM.`,
1391
+ message: pasteBug
1392
+ ? `Connector path endpoints render ${gap}px from the nearest anchorable element, but the path's user-space coordinates would attach if read as screen pixels — screen/viewport numbers were likely written into SVG \`d\` without inverting the CTM.`
1393
+ : `Connector path endpoints render ${gap}px from the nearest anchorable element — a marked shaft that meets no node.`,
1394
+ rect: toRect({
1395
+ left: Math.min(rendered.start.x, rendered.end.x),
1396
+ top: Math.min(rendered.start.y, rendered.end.y),
1397
+ right: Math.max(rendered.start.x, rendered.end.x),
1398
+ bottom: Math.max(rendered.start.y, rendered.end.y),
1399
+ width: Math.abs(rendered.end.x - rendered.start.x),
1400
+ height: Math.abs(rendered.end.y - rendered.start.y),
1401
+ }),
1402
+ fixHint: pasteBug
1403
+ ? "Convert measured screen coordinates into the SVG's user space (subtract the SVG rect / invert getScreenCTM) before writing path `d`, and keep the SVG a direct child of the stage."
1404
+ : "Measure the settled node boxes and write `d` in the SVG's user space (invert getScreenCTM), or grow a layout-owned shaft from the source node.",
1405
+ });
1406
+ }
1407
+ }
1408
+ return issues;
1409
+ }
1410
+
1411
+ function shaftIsPainted(path) {
1412
+ if (IGNORE_TAGS.has(path.tagName) || hasIgnoreFlag(path)) return false;
1413
+ const style = getComputedStyle(path);
1414
+ if (
1415
+ style.display === "none" ||
1416
+ style.visibility === "hidden" ||
1417
+ style.visibility === "collapse"
1418
+ ) {
1419
+ return false;
1420
+ }
1421
+ return opacityChain(path) >= 0.2;
1422
+ }
1423
+
1424
+ function shaftDashHidden(path) {
1425
+ if (typeof path.getTotalLength !== "function") return false;
1426
+ let total;
1427
+ try {
1428
+ total = path.getTotalLength();
1429
+ } catch {
1430
+ return false;
1431
+ }
1432
+ if (!Number.isFinite(total) || total <= 0) return false;
1433
+ const style = getComputedStyle(path);
1434
+ const offset = Number.parseFloat(style.strokeDashoffset || "0");
1435
+ const dash = Number.parseFloat(String(style.strokeDasharray || "").split(/[\s,]+/)[0] || "0");
1436
+ return offset >= total * 0.9 && dash >= total * 0.9;
1437
+ }
1438
+
1439
+ function connectorEndpointCandidates(root, rootRect) {
1440
+ const candidates = [];
1441
+ const rootArea = rectArea(rootRect);
1442
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1443
+ if (element.closest("svg") || IGNORE_TAGS.has(element.tagName) || hasIgnoreFlag(element))
1444
+ continue;
1445
+ const style = getComputedStyle(element);
1446
+ const opaque = RASTER_TAGS.has(element.tagName) || hasOpaqueBackground(style);
1447
+ if (!opaque && !textContentFor(element)) continue;
1448
+ const rect = toRect(element.getBoundingClientRect());
1449
+ const area = rectArea(rect);
1450
+ if (area < 400 || area > rootArea * 0.15) continue;
1451
+ candidates.push({ rect, element });
1452
+ }
1453
+ return candidates;
1454
+ }
1455
+
1456
+ function connectorOrphanIssues(root, rootRect, time) {
1457
+ const issues = [];
1458
+ let candidates = null;
1459
+ const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
1460
+ for (const svg of Array.from(root.querySelectorAll("svg"))) {
1461
+ if (!isVisibleElement(svg)) continue;
1462
+ for (const path of Array.from(svg.querySelectorAll("path"))) {
1463
+ if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
1464
+ if (!isConnectorPath(svg, path)) continue;
1465
+ if (!shaftIsPainted(path) || shaftDashHidden(path)) continue;
1466
+ const user = pathUserEndpoints(path);
1467
+ const rendered = pathScreenEndpoints(svg, path, user);
1468
+ if (!user || !rendered) continue;
1469
+ const renderedChord = Math.hypot(
1470
+ rendered.end.x - rendered.start.x,
1471
+ rendered.end.y - rendered.start.y,
1472
+ );
1473
+ if (renderedChord < 80) continue;
1474
+ if (candidates === null) candidates = connectorEndpointCandidates(root, rootRect);
1475
+ const dark = [];
1476
+ for (const point of [rendered.start, rendered.end]) {
1477
+ let best = null;
1478
+ let attached = false;
1479
+ for (const candidate of candidates) {
1480
+ const gap = distanceToRect(point, candidate.rect);
1481
+ if (gap > threshold) continue;
1482
+ if (isVisibleElement(candidate.element)) {
1483
+ attached = true;
1484
+ break;
1485
+ }
1486
+ if (best === null || gap < best.gap) best = { gap, candidate };
1487
+ }
1488
+ if (!attached && best !== null) dark.push(best.candidate);
1489
+ }
1490
+ if (dark.length === 0) continue;
1491
+ issues.push({
1492
+ code: "connector_orphan",
1493
+ severity: "warning",
1494
+ time,
1495
+ selector: selectorFor(path),
1496
+ containerSelector: selectorFor(svg),
1497
+ message: `Connector shaft is visible while ${dark.length === 2 ? "both endpoints are" : `its endpoint ${selectorFor(dark[0].element)} is`} not on stage.`,
1350
1498
  rect: toRect({
1351
1499
  left: Math.min(rendered.start.x, rendered.end.x),
1352
1500
  top: Math.min(rendered.start.y, rendered.end.y),
@@ -1356,7 +1504,7 @@
1356
1504
  height: Math.abs(rendered.end.y - rendered.start.y),
1357
1505
  }),
1358
1506
  fixHint:
1359
- "Convert measured screen coordinates into the SVG's user space (subtract the SVG rect / invert getScreenCTM) before writing path `d`, and keep the SVG a direct child of the stage.",
1507
+ "Show the shaft only after both ends are on, and hide it with the earlier exit. Do not give the line its own clock.",
1360
1508
  });
1361
1509
  }
1362
1510
  }
@@ -1467,6 +1615,7 @@
1467
1615
  issues.push(...escaped.issues);
1468
1616
  issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
1469
1617
  issues.push(...connectorDetachmentIssues(root, rootRect, time));
1618
+ issues.push(...connectorOrphanIssues(root, rootRect, time));
1470
1619
  return issues;
1471
1620
  };
1472
1621