diff --git a/frontend/src/interaction/index.ts b/frontend/src/interaction/index.ts index 0330d66..dd798e3 100644 --- a/frontend/src/interaction/index.ts +++ b/frontend/src/interaction/index.ts @@ -761,7 +761,23 @@ export class InteractionEngine { if (this.modifySelected.length === 1) { const first = this.state.points[0]; const dist = distance(first, pt); - const offsetted = offsetElement(this.modifySelected[0], dist); + const base = this.modifySelected[0]; + // Seitigkeit bestimmen: Kreuzprodukt der Linienrichtung mit Klickvektor. + // cross > 0 → Klick links der Laufrichtung (p1→p2) → side=+1, sonst -1. + let side: 1 | -1 = 1; + if (base.type === 'line' && base.properties.x1 !== undefined && base.properties.y1 !== undefined) { + const p1x = base.properties.x1 as number; + const p1y = base.properties.y1 as number; + const p2x = base.properties.x2 as number; + const p2y = base.properties.y2 as number; + const cross = (p2x - p1x) * (pt.y - p1y) - (p2y - p1y) * (pt.x - p1x); + side = cross >= 0 ? 1 : -1; + } else if ((base.type === 'circle' || base.type === 'arc') && base.properties.radius !== undefined) { + // Kreis/Bogen: Klick außerhalb → vergrößern (+1), innerhalb → verkleinern (-1) + const clickDist = Math.hypot(pt.x - base.x, pt.y - base.y); + side = clickDist >= base.properties.radius ? 1 : -1; + } + const offsetted = offsetElement(base, dist, side); // Create as new element (copy) this.onElementCreated?.({ ...offsetted, id: this.generateId() }); this.resetModify(); diff --git a/frontend/src/tools/modification/geometry.ts b/frontend/src/tools/modification/geometry.ts index f752d84..f5e119c 100644 --- a/frontend/src/tools/modification/geometry.ts +++ b/frontend/src/tools/modification/geometry.ts @@ -163,7 +163,14 @@ export function mirrorElement(el: CADElement, x1: number, y1: number, x2: number * Offset element by a distance (creates a parallel copy). * For lines: offset perpendicular. For circles/arcs: adjust radius. */ -export function offsetElement(el: CADElement, distance: number): CADElement { +/** + * Versetzt ein Element senkrecht zur seiner Richtung. + * @param distance positiver Betrag des Versatzes + * @param side 1 = links der Laufrichtung (Standard), -1 = rechts — + * bei circle/arc: 1 = Radius vergrößern, -1 = verkleinern + */ +export function offsetElement(el: CADElement, distance: number, side?: 1 | -1): CADElement { + const signed = distance * (side ?? 1); const props = { ...el.properties }; if (el.type === 'line' && props.x1 !== undefined && props.y1 !== undefined && props.x2 !== undefined && props.y2 !== undefined) { @@ -174,8 +181,8 @@ export function offsetElement(el: CADElement, distance: number): CADElement { // Perpendicular unit vector const nx = -dy / len; const ny = dx / len; - const ox = nx * distance; - const oy = ny * distance; + const ox = nx * signed; + const oy = ny * signed; props.x1 += ox; props.y1 += oy; props.x2 += ox; @@ -184,7 +191,7 @@ export function offsetElement(el: CADElement, distance: number): CADElement { } if ((el.type === 'circle' || el.type === 'arc') && props.radius !== undefined) { - props.radius = Math.abs(props.radius + distance); + props.radius = Math.abs(props.radius + signed); const d = props.radius * 2; return { ...el, width: d, height: d, properties: props }; } @@ -201,7 +208,7 @@ export function offsetElement(el: CADElement, distance: number): CADElement { if (len === 0) return p; const nx = -dy / len; const ny = dx / len; - return { x: p.x + nx * distance, y: p.y + ny * distance }; + return { x: p.x + nx * signed, y: p.y + ny * signed }; }); return { ...el, properties: props }; } diff --git a/frontend/tests/bugs.regress.test.ts b/frontend/tests/bugs.regress.test.ts index d42efaf..684eca3 100644 --- a/frontend/tests/bugs.regress.test.ts +++ b/frontend/tests/bugs.regress.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; import { SeatingService } from '../src/services/seatingService'; -import { trimElement } from '../src/tools/modification/geometry'; +import { trimElement, offsetElement } from '../src/tools/modification/geometry'; import type { CADElement } from '../src/types/cad.types'; // ── Helfer (Muster aus tests/geometry.test.ts) ───────────── @@ -69,3 +69,34 @@ describe('BUG-2: trim line at circle', () => { expect(result).not.toBeNull(); }); }); + +// ── BUG-3: Offset ignorierte die Klickseite ───────────────── +describe('BUG-3: offset respects click side', () => { + it('side=-1 versetzt Linie entgegen der Normalenrichtung', () => { + // Linie (100,100)→(300,100): Laufrichtung +x. + // Normale (-dy,dx)=(0,1) zeigt in Canvas-Koordinaten nach UNTEN (+y). + const line = makeLine('l1', 100, 100, 300, 100); + const result = offsetElement(line, 20, -1); + expect(result.properties.y1).toBeCloseTo(80); // nach OBEN versetzt + expect(result.properties.y2).toBeCloseTo(80); + }); + + it('side=+1 versetzt Linie in Normalenrichtung (Default wie vorher)', () => { + const line = makeLine('l1', 100, 100, 300, 100); + const result = offsetElement(line, 20, 1); + expect(result.properties.y1).toBeCloseTo(120); // nach UNTEN versetzt + expect(result.properties.y2).toBeCloseTo(120); + }); + + it('ohne side bleibt Verhalten identisch zur alten API (Rueckwaertskompatibilitaet)', () => { + const line = makeLine('l1', 100, 100, 300, 100); + const result = offsetElement(line, 20); + expect(result.properties.y1).toBeCloseTo(120); + }); + + it('Kreis: side=-1 verkleinert Radius, side=+1 vergroessert', () => { + const circle = makeCircle('c1', 0, 0, 50); + expect(offsetElement(circle, 10, -1).properties.radius).toBeCloseTo(40); + expect(offsetElement(circle, 10, 1).properties.radius).toBeCloseTo(60); + }); +});