Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem, dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en) sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// Editier-Operationen Split / Join / Segment-Löschen für 2D-Zeichenelemente
|
||||
// (Drawing2D). Wandeln Drawing2D ↔ {pts, closed}, rufen den reinen Geometrie-
|
||||
// Kernel (`../geometry/kernel2d`) und liefern neue Drawing2D-Listen. Stil-,
|
||||
// Kategorie- und Level-Attribute der Quelle werden geerbt.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CLAUDE.md).
|
||||
|
||||
import type { Drawing2D, Drawing2DGeom, Vec2 } from "../model/types";
|
||||
import {
|
||||
joinChains,
|
||||
removeSegment,
|
||||
splitAtIntersections,
|
||||
splitClosedByChord,
|
||||
splitPolylineAtParam,
|
||||
vecEqual,
|
||||
} from "../geometry/kernel2d";
|
||||
|
||||
/** Eine Polylinien-Darstellung eines Drawing2D (für die Kernel-Funktionen). */
|
||||
export interface PolyForm {
|
||||
pts: Vec2[];
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wandelt die Geometrie eines Drawing2D in eine Polylinien-Form `{pts, closed}`.
|
||||
* Nur line/polyline/rect werden unterstützt (circle/arc/text → `null`).
|
||||
*/
|
||||
export function toPolyForm(geom: Drawing2DGeom): PolyForm | null {
|
||||
switch (geom.shape) {
|
||||
case "line":
|
||||
return { pts: [geom.a, geom.b], closed: false };
|
||||
case "polyline":
|
||||
return { pts: geom.pts.slice(), closed: geom.closed };
|
||||
case "rect":
|
||||
return {
|
||||
pts: [
|
||||
geom.min,
|
||||
{ x: geom.max.x, y: geom.min.y },
|
||||
geom.max,
|
||||
{ x: geom.min.x, y: geom.max.y },
|
||||
],
|
||||
closed: true,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus einer Polylinien-Form die passende Drawing2D-Geometrie. Zwei Punkte +
|
||||
* offen → `line`; sonst `polyline` (geschlossen oder offen). Rechtecke werden
|
||||
* NICHT rekonstruiert (nach dem Schneiden sind die Teile beliebige Polygone).
|
||||
*/
|
||||
export function fromPolyForm(form: PolyForm): Drawing2DGeom {
|
||||
const pts = form.pts;
|
||||
if (pts.length === 2 && !form.closed) {
|
||||
return { shape: "line", a: pts[0], b: pts[1] };
|
||||
}
|
||||
return { shape: "polyline", pts, closed: form.closed };
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
/** Eindeutige Drawing2D-ID (zeitbasiert + Zähler, damit ein Split nicht kollidiert). */
|
||||
function newId(prefix: string): string {
|
||||
counter += 1;
|
||||
return `${prefix}-${Date.now()}-${counter}`;
|
||||
}
|
||||
|
||||
/** Übernimmt Stil-/Kategorie-/Level-Attribute der Quelle auf ein neues Drawing2D. */
|
||||
function inherit(src: Drawing2D, geom: Drawing2DGeom): Drawing2D {
|
||||
const out: Drawing2D = {
|
||||
id: newId("d2d"),
|
||||
type: "drawing2d",
|
||||
levelId: src.levelId,
|
||||
categoryCode: src.categoryCode,
|
||||
geom,
|
||||
};
|
||||
if (src.lineStyleId !== undefined) out.lineStyleId = src.lineStyleId;
|
||||
if (src.color !== undefined) out.color = src.color;
|
||||
if (src.weightMm !== undefined) out.weightMm = src.weightMm;
|
||||
// Füllung/Schraffur nur an geschlossene Ergebnisse weitergeben.
|
||||
const closed =
|
||||
geom.shape === "polyline" ? geom.closed : geom.shape === "rect";
|
||||
if (closed) {
|
||||
if (src.hatchId !== undefined) out.hatchId = src.hatchId;
|
||||
if (src.fillColor !== undefined) out.fillColor = src.fillColor;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split eines Drawing2D gegen alle ANDEREN Elemente (`others`, als PolyForm).
|
||||
* • Trifft das Ziel andere Elemente, wird an den Schnittpunkten geteilt
|
||||
* (geschlossen + 2 Schnitte → zwei GESCHLOSSENE Polygone via Sehne).
|
||||
* • Ohne Schnitt: Fallback — an der Kanten-MITTE der „längsten" Kante teilen
|
||||
* (offen → zwei Stücke; geschlossen → an einer Kantenmitte + der gegenüber-
|
||||
* liegenden Kantenmitte zu zwei geschlossenen Hälften).
|
||||
* Liefert ≥2 neue Drawing2D, oder `[src]` (unverändert), wenn keine Teilung
|
||||
* möglich war (z. B. nicht-polyline Form). Stil/Kategorie/Level werden geerbt.
|
||||
*/
|
||||
export function splitDrawing(
|
||||
src: Drawing2D,
|
||||
others: PolyForm[],
|
||||
): Drawing2D[] {
|
||||
const form = toPolyForm(src.geom);
|
||||
if (!form) return [src];
|
||||
|
||||
const otherPts = others.map((o) =>
|
||||
// Geschlossene als Ring (erster==letzter) übergeben, damit der Kernel die
|
||||
// Schlusskante als Kante erkennt.
|
||||
o.closed && o.pts.length > 2 ? [...o.pts, o.pts[0]] : o.pts,
|
||||
);
|
||||
|
||||
let pieces = splitAtIntersections(form.pts, form.closed, otherPts);
|
||||
|
||||
// Kein Schnitt-Ergebnis (eine Liste, identisch zur Quelle) → Fallback.
|
||||
const noCut = pieces.length < 2;
|
||||
if (noCut) {
|
||||
pieces = fallbackSplit(form);
|
||||
}
|
||||
|
||||
if (pieces.length < 2) return [src];
|
||||
|
||||
// Jedes Stück: geschlossen bleibt geschlossen NUR, wenn das Original
|
||||
// geschlossen war UND der Schnitt zwei geschlossene Hälften erzeugte
|
||||
// (Sehnen-Split / Fallback-geschlossen). Bei offenem Aufschneiden sind die
|
||||
// Stücke offen.
|
||||
const piecesClosed = form.closed && isClosedSplit(form, pieces);
|
||||
return pieces.map((pts) =>
|
||||
inherit(src, fromPolyForm({ pts, closed: piecesClosed })),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristik: erzeugte der Schnitt zwei GESCHLOSSENE Hälften (Sehnen-Split)?
|
||||
* Wahr, wenn das Original geschlossen war und genau zwei Stücke entstanden
|
||||
* (Chord-Split) — diese sollen wieder geschlossen sein. Bei mehr Stücken
|
||||
* (geschlossen an >2 Schnitten aufgetrennt) bleiben sie offen.
|
||||
*/
|
||||
function isClosedSplit(form: PolyForm, pieces: Vec2[][]): boolean {
|
||||
return form.closed && pieces.length === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback ohne Schnitt mit anderen Elementen: an der Mitte teilen.
|
||||
* • Offen: längste Kante an t=0.5 in zwei offene Stücke.
|
||||
* • Geschlossen: an den Mitten zweier „gegenüberliegender" Kanten in zwei
|
||||
* geschlossene Polygone (Sehne zwischen den beiden Kantenmitten).
|
||||
*/
|
||||
function fallbackSplit(form: PolyForm): Vec2[][] {
|
||||
const n = form.pts.length;
|
||||
if (n < 2) return [form.pts];
|
||||
if (!form.closed) {
|
||||
// Längste Kante suchen.
|
||||
let best = 0;
|
||||
let bestLen = -1;
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const d = dist(form.pts[i], form.pts[i + 1]);
|
||||
if (d > bestLen) {
|
||||
bestLen = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return splitPolylineAtParam(form.pts, false, best, 0.5);
|
||||
}
|
||||
// Geschlossen: Kante 0 und die „gegenüberliegende" Kante.
|
||||
const i = 0;
|
||||
const j = Math.max(1, Math.floor(n / 2)) % n;
|
||||
if (i === j) return [form.pts];
|
||||
// Direkt den Kernel-Chord-Split nutzen.
|
||||
const res = splitClosedHalf(form.pts, i, j);
|
||||
return res ?? [form.pts];
|
||||
}
|
||||
|
||||
/** Geschlossenes Polygon an den Mitten der Kanten i und j halbieren. */
|
||||
function splitClosedHalf(pts: Vec2[], i: number, j: number): Vec2[][] | null {
|
||||
// Reuse: splitClosedByChord ist im Kernel; hier über splitAtIntersections-Form
|
||||
// nachzubauen wäre umständlich — direkt importieren.
|
||||
const res = chordSplit(pts, i, 0.5, j, 0.5);
|
||||
return res;
|
||||
}
|
||||
|
||||
function chordSplit(
|
||||
pts: Vec2[],
|
||||
i: number,
|
||||
ti: number,
|
||||
j: number,
|
||||
tj: number,
|
||||
): Vec2[][] | null {
|
||||
const r = splitClosedByChord(pts, i, ti, j, tj);
|
||||
return r ? [r[0], r[1]] : null;
|
||||
}
|
||||
|
||||
const dist = (a: Vec2, b: Vec2): number =>
|
||||
Math.hypot(a.x - b.x, a.y - b.y);
|
||||
|
||||
/**
|
||||
* Join: verbindet mehrere Drawing2D an koinzidenten Endpunkten zu Polylinien
|
||||
* (geschlossen, wenn sich eine Kette schließt). Nicht-polyline Elemente
|
||||
* (circle/arc/text) werden unverändert durchgereicht. Liefert die NEUE Menge
|
||||
* der Drawing2D, die die Eingabe-Elemente ersetzt. Attribute werden vom ersten
|
||||
* verbundenen Quell-Element geerbt.
|
||||
*/
|
||||
export function joinDrawings(srcs: Drawing2D[]): Drawing2D[] {
|
||||
const joinable: { form: PolyForm; src: Drawing2D }[] = [];
|
||||
const passthrough: Drawing2D[] = [];
|
||||
for (const d of srcs) {
|
||||
const form = toPolyForm(d.geom);
|
||||
if (form) joinable.push({ form, src: d });
|
||||
else passthrough.push(d);
|
||||
}
|
||||
if (joinable.length === 0) return srcs;
|
||||
|
||||
const chains = joinChains(joinable.map((j) => j.form));
|
||||
const template = joinable[0].src;
|
||||
const joined = chains.map((c) =>
|
||||
inherit(template, fromPolyForm({ pts: c.pts, closed: c.closed })),
|
||||
);
|
||||
return [...joined, ...passthrough];
|
||||
}
|
||||
|
||||
/**
|
||||
* Segment-Löschen (Schere): entfernt das Segment `edgeIndex` aus einem
|
||||
* Drawing2D. Geschlossene Form → offene Polylinie. Liefert das geänderte
|
||||
* Drawing2D, oder `null`, wenn weniger als 2 Punkte übrig blieben (Element
|
||||
* komplett löschen) bzw. die Form nicht unterstützt wird.
|
||||
*/
|
||||
export function cutSegment(src: Drawing2D, edgeIndex: number): Drawing2D | null {
|
||||
const form = toPolyForm(src.geom);
|
||||
if (!form) return src; // nicht unterstützte Form unverändert lassen
|
||||
const res = removeSegment(form.pts, form.closed, edgeIndex);
|
||||
if (res.pts.length < 2) return null;
|
||||
return inherit(src, fromPolyForm(res));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Index der Kante eines Drawing2D, die dem Modellpunkt `p` am
|
||||
* nächsten liegt (für Alt+Klick = Segment-Löschen). `null`, wenn die Form keine
|
||||
* Kanten hat (circle/arc/text) oder zu wenige Punkte. Reuse: Polyline-Form +
|
||||
* Punkt-Strecke-Distanz.
|
||||
*/
|
||||
export function nearestEdgeIndex(geom: Drawing2DGeom, p: Vec2): number | null {
|
||||
const form = toPolyForm(geom);
|
||||
if (!form || form.pts.length < 2) return null;
|
||||
const edges: [number, Vec2, Vec2][] = [];
|
||||
for (let i = 0; i < form.pts.length - 1; i++) {
|
||||
edges.push([i, form.pts[i], form.pts[i + 1]]);
|
||||
}
|
||||
if (form.closed && form.pts.length > 2) {
|
||||
edges.push([form.pts.length - 1, form.pts[form.pts.length - 1], form.pts[0]]);
|
||||
}
|
||||
let best: number | null = null;
|
||||
let bestD = Infinity;
|
||||
for (const [i, a, b] of edges) {
|
||||
const d = segDist(p, a, b);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Abstand Punkt → Strecke (lokal, klein gehalten). */
|
||||
function segDist(p: Vec2, a: Vec2, b: Vec2): number {
|
||||
const abx = b.x - a.x;
|
||||
const aby = b.y - a.y;
|
||||
const l2 = abx * abx + aby * aby;
|
||||
let t = l2 > 0 ? ((p.x - a.x) * abx + (p.y - a.y) * aby) / l2 : 0;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const cx = a.x + abx * t;
|
||||
const cy = a.y + aby * t;
|
||||
return Math.hypot(p.x - cx, p.y - cy);
|
||||
}
|
||||
|
||||
// `vecEqual` re-export für etwaige Aufrufer (Auswahl-Vergleich).
|
||||
export { vecEqual };
|
||||
Reference in New Issue
Block a user