Pen tool: uniform-scale remap (fixes stroke distortion) + smaller handles

Two visual issues on top of the Phase D close-out commit:

1. **Stroke width varied along curves.** The pre-existing pipeline
   stored nodes in 0-100 × 0-100 viewBox space and let the SVG
   renderer pick `vbH = 100 * layer.h / layer.w` with
   `preserveAspectRatio="none"`. For any non-square layer, the
   0-100×0-100 node range got stretched non-uniformly into a 100×vbH
   viewBox, distorting the path: strokes appeared wider where the
   curve traveled more in one axis. Phase D's refit made this much
   more likely because dragging anchors out of bounds routinely
   produced non-square layers.

   Fix in two places:
   - finishPenPath: uniform-scale the bbox by `100/bbox.w` so y
     range lands at exactly `100 * h/w = vbH`. The viewBox aspect
     matches the layer aspect, no stretching.
   - refitPenLayerIfNeeded: same uniform-scale invariant. Also
     respects the layer's current vbH when deciding whether nodes
     are "out of bounds" — was previously hard-coded at 100, which
     wrongly fired refit on every drag in already-non-square layers.

2. **Anchor + handle glyphs were too prominent.** On a 1920px-wide
   slide preview the old `dotR = 0.013 * unit` rendered ~25px
   dots. Reduced to `0.0075 * unit` (~12px) and stroke from
   `0.003 * unit` to `0.0018 * unit`. The in-progress draft
   renderer matches.

Strokes now stay constant width all the way around a curve, and
the editing UI reads as overlay rather than as part of the artwork.
This commit is contained in:
thePR0M3TH3AN committed 2026-06-01 16:36:19 -04:00
1 parent b825181356
commit e5c06b7415
1 file changed
+42 -12
+42 -12
View File
@@ -9999,23 +9999,39 @@
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
// No-op unless we're outside [0, 100] on any side. A bit of
// tolerance to avoid micro-jiggle during normal in-bounds drags.
// Layer viewBox is 0-100 × 0-vbH where vbH = 100*layer.h/layer.w
// — the renderer's hard-coded formula. We want nodes to fit
// x:[0,100] and y:[0,vbH], matching the layer's actual aspect
// ratio after refit. The current viewBox-y bound is implied
// by what the renderer would compute given new layer.w/h.
//
// No-op unless we're outside the current viewBox on any side
// (with a small tolerance to avoid micro-jiggle during normal
// in-bounds drags). The current viewBox y-max is `vbH` computed
// from existing layer w/h.
const currentVbH = layer.w > 0 ? 100 * layer.h / layer.w : 100;
const EPS = 0.5;
if (minX > -EPS && minY > -EPS && maxX < 100 + EPS && maxY < 100 + EPS) return;
if (minX > -EPS && minY > -EPS && maxX < 100 + EPS && maxY < currentVbH + EPS) return;
const newW = Math.max(EPS, maxX - minX);
const newH = Math.max(EPS, maxY - minY);
// Layer scales: in inches, layer.w grows in proportion to
// newW vs current viewBox-x (always 100); layer.h grows in
// proportion to newH vs the existing vbH.
const sx = newW / 100;
const sy = newH / 100;
const sy = newH / currentVbH;
// Layer position shifts in inches by the bbox offset scaled
// to the existing layer w/h.
layer.x = layer.x + (minX / 100) * layer.w;
layer.y = layer.y + (minY / 100) * layer.h;
layer.y = layer.y + (minY / currentVbH) * layer.h;
layer.w = Math.max(0.01, layer.w * sx);
layer.h = Math.max(0.01, layer.h * sy);
// Uniform-scale remap so the new node range matches the new
// viewBox aspect exactly — no stretching when the SVG paints
// into the refitted layer rectangle.
const remapScale = 100 / newW;
const remap = (p) => ({
x: (p.x - minX) / newW * 100,
y: (p.y - minY) / newH * 100,
x: (p.x - minX) * remapScale,
y: (p.y - minY) * remapScale,
});
for (const n of nodes) {
const r = remap(n);
@@ -10113,8 +10129,13 @@
clearPenDraftRender();
const dims = currentCanvasDims();
const unit = Math.max(dims.w, dims.h);
const lineW = unit * 0.003;
const dotR = unit * 0.013;
// Smaller, less prominent anchors/handles — earlier sizes
// (0.003 stroke, 0.013 anchor) rendered ~25px dots on a
// 1920px-wide preview, which obscured short paths and made
// the canvas feel cluttered. ~50% smaller reads as deliberate
// editing UI without dominating.
const lineW = unit * 0.0018;
const dotR = unit * 0.0075;
const nodes = layer.shape.pen.nodes;
nodes.forEach((n, i) => {
const a = penNodeToInch(layer, n);
@@ -10469,7 +10490,14 @@
if (n.hOut) allPts.push(n.hOut);
}
const bbox = pointsBbox(allPts);
const remap = (pt) => ({ x: (pt.x - bbox.x) / bbox.w * 100, y: (pt.y - bbox.y) / bbox.h * 100 });
// 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.
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 => ({
x: remap(n).x, y: remap(n).y,
hIn: n.hIn ? remap(n.hIn) : null,
@@ -10515,8 +10543,10 @@
clearPenDraftRender();
const dims = currentCanvasDims();
const unit = Math.max(dims.w, dims.h);
const lineW = unit * 0.004;
const dotR = unit * 0.012;
// Sized to match renderPenEditHandles so the in-progress
// draw and the post-commit edit handles look consistent.
const lineW = unit * 0.0025;
const dotR = unit * 0.0075;
if (_penDraft.tool === 'pencil') {
const pts = _penDraft.points;