From 3831b39b2e72f9cccf506f075fb7e6dc2f106a99 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:46:58 -0400 Subject: [PATCH] Pen tool: write viewBox.h to match layer aspect for uniform strokes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous "uniform-scale remap" commit fixed node coordinates to land in 0-100 × 0-(100*h/w), but the renderer (buildShapeSvgInner path branch) reads shape.viewBox and computes sx = 100/vb.w, sy = vbH/vb.h. With shape.viewBox.h hardcoded to 100, sx ≠ sy for non-square layers and the path got stretched again — strokes distorted around curves after the path was committed, even though the in-progress draft (which renders inside the pen overlay, not the layer SVG) looked fine. Fix is the matching half: also write shape.viewBox.h = 100*lh/lw wherever we touch the pen data, so sx == sy and the renderer applies a uniform scale. - finishPenPath now sets viewBox.h = vbH from the bbox aspect. - regenPenLayerD (called on every edit drag + insert/delete/toggle) recomputes vbH from current layer.w/h before writing the new d string. This catches the rare case where layer.w/h change between refit calls without my refit running. - The "Make editable path" conversion remaps primitiveToPenNodes / parsePathDToNodes results (which always come in 0-100×0-100) into the layer's actual aspect space, and writes viewBox.h to match. Result: converting a rect → editable path on a non-square layer keeps its proportions, and the resulting pen path edits cleanly with uniform strokes. Strokes now stay a constant width all the way around closed shapes, even after the path is fully committed and re-rendered through the standard shape-layer pipeline. --- src/index.html | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/index.html b/src/index.html index 6808551..1fc28c1 100644 --- a/src/index.html +++ b/src/index.html @@ -9925,8 +9925,15 @@ } function regenPenLayerD(layer) { + // The renderer applies sx=100/vb.w, sy=vbH/vb.h; we want sx==sy. + // So whenever we regenerate the d-string we also update the + // stored viewBox.h to match the layer's current aspect, keeping + // strokes uniform. Nodes are kept in 0-100 × 0-(100*h/w) space + // by the uniform-scale remap in finishPenPath + refit. + const vbH = layer.w > 0 ? 100 * layer.h / layer.w : 100; layer.shape = { ...layer.shape, + viewBox: { x: 0, y: 0, w: 100, h: vbH }, d: penNodesToPathD(layer.shape.pen.nodes, layer.shape.pen.closed) }; } @@ -10439,11 +10446,28 @@ width: Math.max(0.5, Number(sh.strokeWidth) || 6), dash: 'solid' }; layer.fill = { type: 'none' }; } + // primitiveToPenNodes / parsePathDToNodes return nodes in + // 0-100 × 0-100. The renderer paints pen paths with sx=100/vb.w, + // sy=vbH/vb.h, where vbH = 100*layer.h/layer.w — so for any + // non-square layer this would stretch the path. Remap node y + // coordinates to 0..vbH so the resulting viewBox is the layer's + // actual aspect, giving uniform sx==sy at render time. + const lw = Number(layer.w) > 0 ? Number(layer.w) : 1; + const lh = Number(layer.h) > 0 ? Number(layer.h) : 1; + const vbH = 100 * lh / lw; + const yScale = vbH / 100; + const scaledNodes = result.nodes.map(n => ({ + x: n.x, + y: n.y * yScale, + hIn: n.hIn ? { x: n.hIn.x, y: n.hIn.y * yScale } : null, + hOut: n.hOut ? { x: n.hOut.x, y: n.hOut.y * yScale } : null, + kind: n.kind || 'corner', + })); layer.shape = { kind: 'path', - viewBox: { x: 0, y: 0, w: 100, h: 100 }, - pen: { closed: result.closed, tool: 'pen', nodes: result.nodes }, - d: penNodesToPathD(result.nodes, result.closed) + viewBox: { x: 0, y: 0, w: 100, h: vbH }, + pen: { closed: result.closed, tool: 'pen', nodes: scaledNodes }, + d: penNodesToPathD(scaledNodes, result.closed) }; render(); return { ok: true }; @@ -10491,11 +10515,13 @@ } const bbox = pointsBbox(allPts); // Uniform scaling: divide both x and y by bbox.w so nodes land - // in 0-100 × 0-(100*h/w) — matching the renderer's vbH formula - // (= 100 * layer.h / layer.w). Independent x/y scaling here - // forces a 0-100×0-100 node range into a non-square viewBox - // later, stretching curves and varying stroke widths around - // the path. Uniform scaling keeps strokes a constant width. + // in 0-100 × 0-(100*h/w). Then store shape.viewBox.h to match, + // so the renderer (buildShapeSvgInner path branch, sx = 100/vb.w, + // sy = H/vb.h) ends up with sx == sy = uniform scaling. The + // earlier hardcoded viewBox.h = 100 with bbox-rescaled nodes + // forced the renderer to stretch the path vertically into a + // non-square layer rectangle, varying stroke widths around + // curves. const scale = bbox.w > 0 ? 100 / bbox.w : 1; const remap = (pt) => ({ x: (pt.x - bbox.x) * scale, y: (pt.y - bbox.y) * scale }); const normNodes = nodes.map(n => ({ @@ -10503,6 +10529,7 @@ hIn: n.hIn ? remap(n.hIn) : null, hOut: n.hOut ? remap(n.hOut) : null })); + const vbH = bbox.w > 0 ? 100 * bbox.h / bbox.w : 100; const d = penNodesToPathD(normNodes, closed); pushHistory('draw pen path'); const fill = (closed && _penToolOpts.penFill) @@ -10516,7 +10543,7 @@ x: bbox.x, y: bbox.y, w: bbox.w, h: bbox.h, rotate: 0, opacity: 1, z: nextLayerZ(), shape: { - kind: 'path', d, viewBox: { x: 0, y: 0, w: 100, h: 100 }, + kind: 'path', d, viewBox: { x: 0, y: 0, w: 100, h: vbH }, pen: { closed: !!closed, tool: 'pen', nodes: normNodes } }, fill,