467e9580a7
Bemassungs-Modell umgebaut: eine Reihenbemassung war bisher N unabhängige Einzel-Elemente, jetzt EIN Element mit einer Punktreihe (pts statt a/b). Alle Segmente teilen weiterhin einen Offset, aber Verschieben/Selektieren/ Löschen betrifft die ganze Kette auf einmal. Ergänzen einer bestehenden Kette folgt dem "erst selektieren, dann Befehl"- Muster (wie move/copy/offset): ist beim Start eine Masskette selektiert, sortiert der erste Klick sich per Geraden-Projektion in die Kette ein — vor dem ersten Punkt (verlängert rückwärts), nach dem letzten (verlängert vorwärts) oder zwischen zwei bestehenden Punkten (fügt ein) — und der Commit ersetzt dasselbe Element statt eine Kopie zu erzeugen.
161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
// Ursprungs-Verschiebung des GESAMTEN Projekts (Neuer Bezugspunkt) — reine,
|
||
// verlustfreie Translation aller Modell-Koordinaten um einen festen Vektor
|
||
// (dx,dy). Genutzt, um nach einem Kataster-/3D-Import (der weit weg vom
|
||
// Modell-Ursprung landen kann, s. `Project.geoAnchor`) einen neuen, praktischen
|
||
// Bezugspunkt zu setzen: die komplette Zeichnung wandert relativ zueinander
|
||
// unverändert, nur ihre absoluten Koordinaten verschieben sich. `geoAnchor`
|
||
// (falls gesetzt) wandert MIT, sodass der LV95-Bezug exakt erhalten bleibt —
|
||
// beim Export ist die Verschiebung damit jederzeit rekonstruierbar.
|
||
//
|
||
// Bewusst NUR Translation (keine Rotation): mehrere Felder im Modell sind an
|
||
// die Achsen gebunden (z. B. `Roof.ridgeAxis: "x"|"y"`) und mehrere Winkel-Felder
|
||
// (Column.rotation, Text-/Bild-Rotation, Tür-Schwenk) müssten bei einer Drehung
|
||
// mitgeführt werden — eine Rotation ist ein eigener, grösserer Schritt.
|
||
//
|
||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||
|
||
import type {
|
||
ContextObject,
|
||
Contour,
|
||
Project,
|
||
Vec2,
|
||
} from "./types";
|
||
|
||
const shift = (p: Vec2, dx: number, dy: number): Vec2 => ({ x: p.x + dx, y: p.y + dy });
|
||
const shiftAll = (pts: Vec2[], dx: number, dy: number): Vec2[] => pts.map((p) => shift(p, dx, dy));
|
||
|
||
/** Kontur (Höhenlinie): Punkte + optionale Kreis-/Bogen-Mitte verschieben. */
|
||
function shiftContour(c: Contour, dx: number, dy: number): Contour {
|
||
return {
|
||
...c,
|
||
pts: shiftAll(c.pts, dx, dy),
|
||
curve: c.curve ? { ...c.curve, cx: c.curve.cx + dx, cy: c.curve.cy + dy } : undefined,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Kontext-Objekt (importiertes Mesh/Terrain/Konturen-Satz) verschieben.
|
||
* `positions` ist ein flaches [x,y,z=Höhe, …]-Array — nur x/y (Grundriss)
|
||
* verschieben, die Höhe (z) bleibt unverändert.
|
||
*/
|
||
function shiftContextObject(obj: ContextObject, dx: number, dy: number): ContextObject {
|
||
if (obj.type === "contourSet") {
|
||
return { ...obj, contours: obj.contours.map((c) => shiftContour(c, dx, dy)) };
|
||
}
|
||
if (obj.type === "image") {
|
||
return { ...obj, min: shift(obj.min, dx, dy), max: shift(obj.max, dx, dy) };
|
||
}
|
||
const positions = obj.positions.slice();
|
||
for (let i = 0; i + 2 < positions.length; i += 3) {
|
||
positions[i] += dx;
|
||
positions[i + 1] += dy;
|
||
}
|
||
return { ...obj, positions };
|
||
}
|
||
|
||
/**
|
||
* Verschiebt SÄMTLICHE Modell-Koordinaten eines Projekts um den festen Vektor
|
||
* (dx,dy) — Wände, Decken, Dächer, Öffnungen-Host (folgt automatisch der Wand),
|
||
* Treppen, Räume, Stützen, Extrusionen, freie 2D-Geometrie, Kontext-Schicht
|
||
* (Import/Terrain/Konturen), Schnitt-/Ansichtslinien sowie den Geo-Anker
|
||
* (`geoAnchor.model`, damit der LV95-Bezug exakt erhalten bleibt). Richtungs-
|
||
* Vektoren (z. B. `Stair.dir`) und Winkel-Felder bleiben unangetastet — eine
|
||
* reine Translation ändert keine Orientierung.
|
||
*/
|
||
export function translateProject(project: Project, dx: number, dy: number): Project {
|
||
if (dx === 0 && dy === 0) return project;
|
||
return {
|
||
...project,
|
||
walls: project.walls.map((w) => ({
|
||
...w,
|
||
start: shift(w.start, dx, dy),
|
||
end: shift(w.end, dx, dy),
|
||
})),
|
||
ceilings: project.ceilings?.map((c) => ({
|
||
...c,
|
||
outline: shiftAll(c.outline, dx, dy),
|
||
openings: c.openings?.map((h) => shiftAll(h, dx, dy)),
|
||
})),
|
||
roofs: project.roofs?.map((r) => ({ ...r, outline: shiftAll(r.outline, dx, dy) })),
|
||
stairs: project.stairs?.map((s) => ({
|
||
...s,
|
||
start: shift(s.start, dx, dy),
|
||
center: s.center ? shift(s.center, dx, dy) : undefined,
|
||
})),
|
||
rooms: project.rooms?.map((r) => ({
|
||
...r,
|
||
boundary: shiftAll(r.boundary, dx, dy),
|
||
stampAnchor: r.stampAnchor ? shift(r.stampAnchor, dx, dy) : undefined,
|
||
})),
|
||
columns: project.columns?.map((c) => ({ ...c, position: shift(c.position, dx, dy) })),
|
||
extrudedSolids: project.extrudedSolids?.map((s) => ({
|
||
...s,
|
||
points: shiftAll(s.points, dx, dy),
|
||
circle: s.circle ? { ...s.circle, center: shift(s.circle.center, dx, dy) } : undefined,
|
||
})),
|
||
drawings2d: project.drawings2d.map((d) => {
|
||
const g = d.geom;
|
||
switch (g.shape) {
|
||
case "line":
|
||
return { ...d, geom: { ...g, a: shift(g.a, dx, dy), b: shift(g.b, dx, dy) } };
|
||
case "polyline":
|
||
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||
case "rect":
|
||
return { ...d, geom: { ...g, min: shift(g.min, dx, dy), max: shift(g.max, dx, dy) } };
|
||
case "circle":
|
||
return { ...d, geom: { ...g, center: shift(g.center, dx, dy) } };
|
||
case "arc":
|
||
return { ...d, geom: { ...g, center: shift(g.center, dx, dy) } };
|
||
case "ellipse":
|
||
return { ...d, geom: { ...g, center: shift(g.center, dx, dy) } };
|
||
case "bezier":
|
||
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||
case "spline":
|
||
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||
case "dimension":
|
||
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||
case "kote":
|
||
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
||
case "text":
|
||
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
||
case "image":
|
||
return { ...d, geom: { ...g, min: shift(g.min, dx, dy), max: shift(g.max, dx, dy) } };
|
||
}
|
||
}),
|
||
context: project.context?.map((o) => shiftContextObject(o, dx, dy)),
|
||
drawingLevels: project.drawingLevels.map((lvl) =>
|
||
lvl.linePoints
|
||
? { ...lvl, linePoints: [shift(lvl.linePoints[0], dx, dy), shift(lvl.linePoints[1], dx, dy)] }
|
||
: lvl,
|
||
),
|
||
geoAnchor: project.geoAnchor
|
||
? { ...project.geoAnchor, model: shift(project.geoAnchor.model, dx, dy) }
|
||
: undefined,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Der LV95-Ursprung eines Projekts: die reale Landeskoordinate, die auf den
|
||
* Modell-Nullpunkt (0,0) abgebildet wird. Aus `geoAnchor` abgeleitet
|
||
* (`origin = lv95 − model`), oder `null`, wenn KEIN Standort-Bezug existiert.
|
||
*/
|
||
export function projectGeoOrigin(project: Project): { e: number; n: number } | null {
|
||
const a = project.geoAnchor;
|
||
if (!a) return null;
|
||
return { e: a.lv95.e - a.model.x, n: a.lv95.n - a.model.y };
|
||
}
|
||
|
||
/**
|
||
* Kopie des Projekts mit SÄMTLICHER Geometrie auf die REALEN Weltkoordinaten
|
||
* (LV95) verschoben — für georeferenzierte MODELL-Exporte (IFC/DXF): das Modell
|
||
* wird lokal nahe (0,0) bearbeitet, der Export schreibt aber die wahren
|
||
* Landeskoordinaten, damit die Datei in anderer CAD-/BIM-Software lagerichtig
|
||
* sitzt. Ohne `geoAnchor` (kein Bezug) bleibt das Projekt unverändert (Identität).
|
||
* NICHT für Papier-/Layout-Exporte (die bleiben in lokalen Koordinaten).
|
||
*/
|
||
export function projectToWorldCoords(project: Project): Project {
|
||
const origin = projectGeoOrigin(project);
|
||
if (!origin) return project;
|
||
return translateProject(project, origin.e, origin.n);
|
||
}
|