task(B3): offset respects click side (fixes BUG-3)
This commit is contained in:
@@ -761,7 +761,23 @@ export class InteractionEngine {
|
|||||||
if (this.modifySelected.length === 1) {
|
if (this.modifySelected.length === 1) {
|
||||||
const first = this.state.points[0];
|
const first = this.state.points[0];
|
||||||
const dist = distance(first, pt);
|
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)
|
// Create as new element (copy)
|
||||||
this.onElementCreated?.({ ...offsetted, id: this.generateId() });
|
this.onElementCreated?.({ ...offsetted, id: this.generateId() });
|
||||||
this.resetModify();
|
this.resetModify();
|
||||||
|
|||||||
@@ -163,7 +163,14 @@ export function mirrorElement(el: CADElement, x1: number, y1: number, x2: number
|
|||||||
* Offset element by a distance (creates a parallel copy).
|
* Offset element by a distance (creates a parallel copy).
|
||||||
* For lines: offset perpendicular. For circles/arcs: adjust radius.
|
* 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 };
|
const props = { ...el.properties };
|
||||||
|
|
||||||
if (el.type === 'line' && props.x1 !== undefined && props.y1 !== undefined && props.x2 !== undefined && props.y2 !== undefined) {
|
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
|
// Perpendicular unit vector
|
||||||
const nx = -dy / len;
|
const nx = -dy / len;
|
||||||
const ny = dx / len;
|
const ny = dx / len;
|
||||||
const ox = nx * distance;
|
const ox = nx * signed;
|
||||||
const oy = ny * distance;
|
const oy = ny * signed;
|
||||||
props.x1 += ox;
|
props.x1 += ox;
|
||||||
props.y1 += oy;
|
props.y1 += oy;
|
||||||
props.x2 += ox;
|
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) {
|
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;
|
const d = props.radius * 2;
|
||||||
return { ...el, width: d, height: d, properties: props };
|
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;
|
if (len === 0) return p;
|
||||||
const nx = -dy / len;
|
const nx = -dy / len;
|
||||||
const ny = dx / 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 };
|
return { ...el, properties: props };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { SeatingService } from '../src/services/seatingService';
|
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';
|
import type { CADElement } from '../src/types/cad.types';
|
||||||
|
|
||||||
// ── Helfer (Muster aus tests/geometry.test.ts) ─────────────
|
// ── Helfer (Muster aus tests/geometry.test.ts) ─────────────
|
||||||
@@ -69,3 +69,34 @@ describe('BUG-2: trim line at circle', () => {
|
|||||||
expect(result).not.toBeNull();
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user