Files
DOSSIER-STANDALONE/src/ui/lineSegments.ts
T
karim 25cebbd98c Linien: modulares Segment-System (Strich/Punkt/Luecke) + Loop-Vorschau
Der Linien-Editor ist modular: eine Linie ist eine geordnete, loopende Folge
aus Segmenten Strich (Laenge), Punkt (Dot) und Luecke (Laenge) — beliebige
Sequenzen (Volllinie/Strichlinie/Punktlinie/Strich-Punkt als Presets, plus
frei), die Schluss-Luecke ist die letzte Luecke. Datenbasis bleibt
LineStyle.dash (mm, alternierend); ein Punkt ist ein 0-Laengen-AN-Segment.
Enthaelt dash eine 0, wird die Linie mit runder Kappe gezeichnet, damit
Punkte als Dots erscheinen (Live + Print; GL/DXF Folgearbeit). Neue reine
Segment-Logik in ui/lineSegments.ts. LineSwatch zeigt den ersten Loop dunkel
und 2 weitere grau (Loop-Kontext). 14 neue Tests, 127 gruen.
2026-07-04 01:21:04 +02:00

108 lines
4.3 KiB
TypeScript

// Modulares Linien-Segment-System — die Brücke zwischen dem Segment-Editor
// (ResourceManager) und der Datenbasis `LineStyle.dash` (alternierend AN/AUS in
// mm, loopend). Eine musterbasierte Linie ist eine geordnete, loopende Folge aus
// drei Segment-Arten:
// • "dash" — ein sichtbarer Strich (Länge in mm > 0),
// • "dot" — ein Punkt (AN-Segment der Länge 0; wird mit runder Strichkappe zum
// Dot, siehe dashHasDot + `stroke-linecap: round` in den Renderern),
// • "gap" — eine Lücke (AUS-Länge in mm).
// Die letzte Lücke der Folge ist die „Schluss-Lücke" (Ende des Loops bis zum
// Start des nächsten). Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
/** Eine Segment-Art des Linien-Musters. */
export type LineSegmentType = "dash" | "dot" | "gap";
/** Ein Segment der Muster-Folge (Länge in mm; bei „dot" ignoriert/0). */
export interface LineSegment {
type: LineSegmentType;
/** Länge in mm (Papier). Bei „dot" stets 0. */
length: number;
}
/**
* Bildet eine geordnete Segment-Folge auf ein `dash`-Array ab
* (`[on, off, on, off, …]` in mm; ein `on`-Wert von 0 ⇒ Punkt). Da SVG-Dash
* strikt AN/AUS alterniert, werden 0-Längen eingefügt bzw. gleichartige
* Nachbarn zusammengefasst, um die Alternation zu wahren:
* • zwei AN-Segmente in Folge (z. B. Strich·Punkt) ⇒ 0-Lücke dazwischen,
* • zwei Lücken in Folge ⇒ zur vorigen Lücke addiert.
* Das Muster endet stets auf einer AUS-Länge (gerade Array-Länge), damit es
* sauber loopt. Leere Folge ⇒ `null` (Volllinie).
*/
export function segmentsToDash(segments: LineSegment[]): number[] | null {
const dash: number[] = [];
for (const seg of segments) {
const on = seg.type !== "gap";
const value = seg.type === "dot" ? 0 : Math.max(0, seg.length);
const nextSlotIsOn = dash.length % 2 === 0;
if (on) {
if (!nextSlotIsOn) dash.push(0); // 0-Lücke, um die AN/AUS-Alternation zu wahren
dash.push(value);
} else {
if (nextSlotIsOn) {
if (dash.length > 0) {
// zwei Lücken in Folge → in die vorherige Lücke einrechnen
dash[dash.length - 1] += value;
continue;
}
dash.push(0); // führende Lücke: 0-Strich davor
}
dash.push(value);
}
}
// Muster muss mit einer AUS-Länge enden (gerade Länge), damit es sauber loopt.
if (dash.length % 2 === 1) dash.push(0);
return dash.length ? dash : null;
}
/**
* Kehrt `segmentsToDash` um: ein `dash`-Array (`[on, off, …]`) wird zur
* Segment-Folge. Gerade Indizes sind AN (0 ⇒ Punkt, sonst Strich), ungerade
* Indizes sind Lücken. `null`/leer ⇒ leere Folge (Volllinie).
*/
export function dashToSegments(dash: number[] | null | undefined): LineSegment[] {
if (!dash) return [];
return dash.map((v, i) =>
i % 2 === 0
? { type: v === 0 ? "dot" : "dash", length: v }
: { type: "gap", length: v },
);
}
/**
* Enthält das Strichmuster einen Punkt (ein AN-Segment der Länge 0)? Solche
* Linien müssen mit runder Strichkappe (`stroke-linecap: round`) gezeichnet
* werden, damit die 0-Längen-Segmente als Dots sichtbar werden.
*/
export function dashHasDot(dash: number[] | null | undefined): boolean {
return !!dash && dash.some((v) => v === 0);
}
/** Schnell-Presets für die drei Grundtypen (+ Strich-Punkt), Längen in mm. */
export const LINE_PRESETS = {
/** Volllinie — durchgezogen. */
solid: null as number[] | null,
/** Strichlinie. */
dash: [3, 2] as number[] | null,
/** Punktlinie (Dots). */
dot: [0, 2] as number[] | null,
/** Strich-Punkt-Linie. */
dashDot: [4, 2, 0, 2] as number[] | null,
} as const;
export type LinePresetKey = keyof typeof LINE_PRESETS | "custom";
/** Erkennt, welchem Preset ein `dash`-Array entspricht (sonst „custom"). */
export function presetOfDash(dash: number[] | null | undefined): LinePresetKey {
const eq = (a: number[] | null, b: number[] | null): boolean => {
if (a === null || b === null) return a === b;
return a.length === b.length && a.every((v, i) => v === b[i]);
};
const d = dash ?? null;
if (eq(d, LINE_PRESETS.solid)) return "solid";
if (eq(d, LINE_PRESETS.dash)) return "dash";
if (eq(d, LINE_PRESETS.dot)) return "dot";
if (eq(d, LINE_PRESETS.dashDot)) return "dashDot";
return "custom";
}