Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1612,18 +1612,23 @@

// True when the visible dash window [offset, offset + length] sits in one gap (an unstarted
// draw-on), allowing 10% overlap. Zero-length dashes paint only with a round or square linecap.
// Distances are in `pathLength` units when set; a `pathLength` resolving to 0 paints solid.
function shaftDashHidden(path, total) {
const style = getComputedStyle(path);
const dashes = dashArrayLengths(style.strokeDasharray, path);
if (dashes === null) return false;
const authored = path.pathLength?.baseVal; // SVGAnimatedNumber; 0 when unset or unparseable
if (authored === 0 && path.hasAttribute("pathLength")) return false;
const dotsPaint = (style.strokeLinecap || "butt") !== "butt";
const dashPaints = (index) => index % 2 === 0 && (dashes[index] > 0 || dotsPaint);
if (!dashes.some((_, index) => dashPaints(index))) return true; // only butt-capped dots
const period = dashes.reduce((sum, length) => sum + length, 0);
if (total > period) return false; // a full period of dash paints — call it visible
const period = dashes.reduce((sum, dash) => sum + dash, 0);
const length = authored > 0 ? authored : total;
const offset = dashLength(style.strokeDashoffset, path); // unparseable reads as 0
const start = Number.isFinite(offset) ? ((offset % period) + period) % period : 0;
const end = start + total;
// Wrap into [0, period); adding the period only to negatives keeps `0.9 % 2` exact.
const wrapped = Number.isFinite(offset) ? offset % period : 0;
const start = wrapped < 0 ? wrapped + period : wrapped;
const end = start + length;
let painted = 0;
let segmentStart = 0;
// Two periods cover any window that starts inside the first.
Expand All @@ -1635,7 +1640,7 @@
}
segmentStart = segmentEnd;
}
return painted <= total * 0.1;
return painted <= length * 0.1;
}

// Computed `stroke-dasharray` as an even-length list of user-unit lengths, or null when the
Expand Down
29 changes: 27 additions & 2 deletions packages/cli/src/commands/layout-audit.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,13 +1528,25 @@ describe("layout-audit.browser coordinate-frame findings", () => {
{ dasharray: "none", offset: "0", count: 1 },
{ dasharray: "100px", offset: "100px", count: 0 },
{ dasharray: "100", offset: "-100", count: 0 },
{ dasharray: "100", offset: "-150", count: 1 },
{ dasharray: "10%", offset: "10%", count: 0 },
{ dasharray: "50 100", offset: "50", count: 0 },
{ dasharray: "50 100", offset: "40", count: 0 },
{ dasharray: "50 100", offset: "30", count: 1 },
{ dasharray: "2 97", offset: "0.5", count: 0 },
{ dasharray: "1", offset: "1", pathLength: 1, count: 0 },
{ dasharray: "1", offset: "0.9", pathLength: 1, count: 0 },
{ dasharray: "1", offset: "0.5", pathLength: 1, count: 1 },
{ dasharray: "0.1 0.9", offset: "0", pathLength: 1, linecap: "round", count: 1 },
{ dasharray: "100", offset: "100", pathLength: 0, count: 1 },
{ dasharray: "0 4", offset: "0", pathLength: 0, count: 1 },
])(
"stroke-dasharray $dasharray, dashoffset $offset, linecap $linecap → $count connector_detached",
({ dasharray, offset, linecap, count }) => {
"stroke-dasharray $dasharray, dashoffset $offset, linecap $linecap, pathLength $pathLength → $count connector_detached",
({ dasharray, offset, linecap, pathLength, count }) => {
document.body.innerHTML = foreignFrameDom;
if (pathLength !== undefined) {
document.getElementById("detached")?.setAttribute("pathLength", String(pathLength));
}
installGeometry(foreignFrameRects, {
...foreignFrameStyles,
detached: {
Expand Down Expand Up @@ -3473,6 +3485,14 @@ interface CtmTranslate {
f: number;
}

// `pathLength.baseVal` as the DOM computes it: a finite SVG number, else 0 (unset, `1.`, `Infinity`, hex).
function domPathLength(attr: string | null): number {
if (attr === null) return 0;
const svgNumber = /^\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?(?:\s*,)?\s*$/.test(attr);
const parsed = svgNumber ? Number(attr.replace(",", "")) : 0;
return Number.isFinite(parsed) ? parsed : 0;
}

// happy-dom has no SVG geometry APIs; endpoints come from the path's `d`, the CTM is a pure translate.
function installConnectorGeometry(translate: CtmTranslate, root: ParentNode = document): void {
const matrix = { a: 1, b: 0, c: 0, d: 1, e: translate.e, f: translate.f };
Expand All @@ -3493,6 +3513,11 @@ function installConnectorGeometry(translate: CtmTranslate, root: ParentNode = do
const start = { x: numbers[0] ?? 0, y: numbers[1] ?? 0 };
const end = { x: numbers[numbers.length - 2] ?? 0, y: numbers[numbers.length - 1] ?? 0 };
Object.defineProperty(path, "getTotalLength", { ...prop, value: () => 100 });
// happy-dom has no SVGGeometryElement; mirror the DOM's `pathLength`.
Object.defineProperty(path, "pathLength", {
...prop,
value: { baseVal: domPathLength(path.getAttribute("pathLength")) },
});
Object.defineProperty(path, "getPointAtLength", {
...prop,
value: (length: number) => (length === 0 ? start : end),
Expand Down
Loading