Skip to content
Open
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
22 changes: 16 additions & 6 deletions packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1457,7 +1457,12 @@
* (renders solid) and `none` are painted. A zero-length dash paints only as a line cap: `0 4`
* is a dotted line under `stroke-linecap: round | square` and invisible under the default
* `butt` (the state a finished draw-off tween leaves behind: `0px, 999999px`). Caps that a
* non-zero dash would add at the window edges are ignored.
* non-zero dash would add at the window edges are ignored. The 10% rule applies uniformly: a
* window that spans a whole period without enclosing a whole dash (`2 97` on a 100-long path
* at offset 0.5 paints 3%) is hidden too. A `pathLength` attribute rescales every distance
* along the path, so the window is judged in the author's units: the CSS draw-on idiom
* `pathLength="1"` with `dasharray 1; dashoffset 1` is hidden. A `pathLength` that resolves
* to 0 (`"0"` or an unparseable value) zeroes the dash scale, so the stroke paints solid.
*/
function shaftDashHidden(path, total) {
const style = getComputedStyle(path);
Expand All @@ -1466,11 +1471,16 @@
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 authored = path.pathLength?.baseVal; // SVGAnimatedNumber; 0 when unset or unparseable
// A present `pathLength` that resolves to 0 zeroes the dash scale, so the stroke paints solid.
if (authored === 0 && path.hasAttribute("pathLength")) return false;
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 @@ -1482,7 +1492,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
40 changes: 37 additions & 3 deletions packages/cli/src/commands/layout-audit.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,14 @@ describe("layout-audit.browser coordinate-frame findings", () => {
// zero-length dash paints only as a round/square cap (`0 4` is dotted with round caps and
// invisible with the default butt cap). Path length is 100 (installConnectorGeometry);
// `50 100` at offset 40 leaves exactly 10% painted — the tolerance boundary — while offset 30
// shows 20% and fires.
// shows 20% and fires. `2 97` at offset 0.5 spans a whole period yet encloses no whole dash
// and paints 3% — hidden; there is no full-period shortcut. A negative offset wraps into the
// period (`100` at -150 shows half a dash; unwrapped it would read as hidden). Percentages
// resolve against the SVG viewport (1740×830 → `10%` ≈ 136), not as bare numbers.
// `pathLength` puts the dashes and the offset in the author's units, so `pathLength="1"` with
// `1 / 1` — the CSS draw-on idiom — is hidden, `1 / 0.9` sits on the 10% boundary (hidden,
// exact in fractional units too) while `1 / 0.5` and a `0.1 0.9` dot on the same path paint.
// A `pathLength` that resolves to 0 zeroes the dash scale and the stroke paints solid.
it.each([
{ dasharray: "0 4", offset: "0", count: 0 },
{ dasharray: "0 4", offset: "0", linecap: "round", count: 1 },
Expand All @@ -1097,13 +1104,24 @@ 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 },
])(
"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 @@ -3024,6 +3042,17 @@ interface CtmTranslate {
}

// happy-dom has no SVG geometry APIs; endpoints come from the path's `d`, the CTM is a pure translate.
/**
* `SVGGeometryElement.pathLength.baseVal` as the DOM computes it: a finite SVG number (one
* trailing comma allowed); anything else (unset, `1.`, `Infinity`, hex, garbage) reads as 0.
*/
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;
}

function installConnectorGeometry(translate: CtmTranslate, root: ParentNode = document): void {
const matrix = { a: 1, b: 0, c: 0, d: 1, e: translate.e, f: translate.f };
const prop = { configurable: true, writable: true };
Expand All @@ -3043,6 +3072,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