From 91a6ab5270a996f4849a551a7e525fbb8e1c6b2f Mon Sep 17 00:00:00 2001 From: u8array Date: Sun, 10 May 2026 21:57:51 +0200 Subject: [PATCH 1/2] fix(canvas): selected reverse line no longer hides its difference-blend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the selection overlay rendered as a solid line directly on top of the body. For reverse (^LRY) lines that meant the selection covered the inversion visualisation — text or shapes layered under the line stopped showing through as long as it was selected. Switch to an Illustrator-style outline pattern: two thin parallel selection-coloured strokes offset perpendicular to the body, so the body's difference-blend keeps painting the underlying content while the outline marks the selection alongside it. Same render path for reverse and non-reverse lines — no special case needed. Offset = strokeWidth / 2 + 1 (1 px clearance outside the body). Guard against zero-length lines (would div-by-zero in the perpendicular unit-vector calc). --- src/components/Canvas/LineObject.tsx | 39 +++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/Canvas/LineObject.tsx b/src/components/Canvas/LineObject.tsx index e39bb987..d985e8e1 100644 --- a/src/components/Canvas/LineObject.tsx +++ b/src/components/Canvas/LineObject.tsx @@ -216,15 +216,36 @@ export function LineObject({ listening={false} globalCompositeOperation={isReverse ? "difference" : "source-over"} /> - {isSelected && ( - - )} + {isSelected && (() => { + // Outline the line with two parallel selection-coloured strokes + // offset perpendicular to the body. Drawing alongside (not on + // top) keeps the body's difference-blend intact for reverse + // lines and matches the Illustrator-style stroke selection + // affordance. Guarded against zero-length (degenerate) lines. + const len = Math.hypot(dispX2 - dispX1, dispY2 - dispY1); + if (len === 0) return null; + const off = lineStrokeWidth / 2 + 1; + const px = (-(dispY2 - dispY1) / len) * off; + const py = ((dispX2 - dispX1) / len) * off; + return ( + <> + + + + ); + })()} {/* Wide transparent hit area — handles click-to-select and whole-line drag. id is here (not on the Group) so the Stage snap handler can find this node via e.target.id() and apply object-snap correctly. */} From 3a922453b6b4d572e14d8744ac88a21a9c935903 Mon Sep 17 00:00:00 2001 From: u8array Date: Sun, 10 May 2026 22:02:00 +0200 Subject: [PATCH 2/2] refactor(line-selection): extract dx/dy, fix actual 1 px gap Reviewer-spotted (gemini-code-assist on PR #51): - Hoist dispX2-dispX1 / dispY2-dispY1 into dx, dy locals; less repetition in the perpendicular-vector math. - Offset was lineStrokeWidth / 2 + 1, which puts the selection stroke *centre* 1 px past the body's edge. With a 1 px-wide selection stroke that leaves an actual visual gap of only 0.5 px between the edges. Bump to +1.5 so the gap matches the comment. --- src/components/Canvas/LineObject.tsx | 90 ++++++++++++++++++---------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/src/components/Canvas/LineObject.tsx b/src/components/Canvas/LineObject.tsx index d985e8e1..50f3b9f2 100644 --- a/src/components/Canvas/LineObject.tsx +++ b/src/components/Canvas/LineObject.tsx @@ -14,6 +14,56 @@ import { selectionHandlers, type KonvaObjectProps } from "./konvaObjectProps"; const HANDLE_VISIBLE_SIZE = 7; const HANDLE_HIT_SIZE = 14; +/** + * Selection outline for a line — two parallel selection-coloured strokes + * offset perpendicular to the body. Drawing alongside (not on top) keeps + * the body's difference-blend intact for reverse (^LRY) lines and matches + * the Illustrator-style stroke selection affordance. Returns null for + * zero-length lines (degenerate input). + * + * Offset breakdown for a 1 px visual gap between body and selection edges: + * bodyStrokeWidth / 2 — half of the body (centre → body edge) + * 0.5 — half of the 1 px selection stroke + * (centre → selection's body-side edge) + * 1 — actual gap requested between the two adjacent + * edges + */ +function LineSelectionOutline({ + x1, y1, x2, y2, + bodyStrokeWidth, + color, +}: { + x1: number; y1: number; x2: number; y2: number; + bodyStrokeWidth: number; + color: string; +}) { + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.hypot(dx, dy); + if (len === 0) return null; + const off = bodyStrokeWidth / 2 + 1.5; + const px = (-dy / len) * off; + const py = (dx / len) * off; + return ( + <> + + + + ); +} + type LineLabelObject = Extract; type Props = Omit & { obj: LineLabelObject }; @@ -216,36 +266,16 @@ export function LineObject({ listening={false} globalCompositeOperation={isReverse ? "difference" : "source-over"} /> - {isSelected && (() => { - // Outline the line with two parallel selection-coloured strokes - // offset perpendicular to the body. Drawing alongside (not on - // top) keeps the body's difference-blend intact for reverse - // lines and matches the Illustrator-style stroke selection - // affordance. Guarded against zero-length (degenerate) lines. - const len = Math.hypot(dispX2 - dispX1, dispY2 - dispY1); - if (len === 0) return null; - const off = lineStrokeWidth / 2 + 1; - const px = (-(dispY2 - dispY1) / len) * off; - const py = ((dispX2 - dispX1) / len) * off; - return ( - <> - - - - ); - })()} + {isSelected && ( + + )} {/* Wide transparent hit area — handles click-to-select and whole-line drag. id is here (not on the Group) so the Stage snap handler can find this node via e.target.id() and apply object-snap correctly. */}