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,251 @@
|
||||
// Transformations-System (Bewegen/Spiegeln/Drehen) auf der Auswahl, im Stil von
|
||||
// Vectorworks: Geste „Basispunkt → Ziel", Live-Vorschau, und eine Modusleiste
|
||||
// U/I/O/P, die die Vervielfältigung steuert:
|
||||
// U move — nur das Original transformieren.
|
||||
// I copy — Original bleibt + eine transformierte Kopie.
|
||||
// O array — Original bleibt + N Kopien, je um den vollen Schritt versetzt.
|
||||
// P distribute — Original bleibt + N Kopien gleichmäßig zwischen Original und Ziel.
|
||||
//
|
||||
// Bezeichner englisch, UI-Text via t() (CONVENTIONS.md). Reine Funktionen + Geometrie;
|
||||
// die App hält den State und wendet `commitTransform` immutabel an.
|
||||
|
||||
import type {
|
||||
Drawing2D,
|
||||
Drawing2DGeom,
|
||||
Project,
|
||||
Vec2,
|
||||
Wall,
|
||||
} from "../model/types";
|
||||
import { wallTypeThickness } from "../model/types";
|
||||
import { wallCorners } from "../model/geometry";
|
||||
import type { DraftShape } from "./types";
|
||||
import { uniqueId } from "./types";
|
||||
|
||||
export type TransformOp = "move" | "rotate" | "mirror";
|
||||
export type CopyMode = "move" | "copy" | "array" | "distribute"; // U / I / O / P
|
||||
|
||||
/** Welche Auswahl transformiert wird. */
|
||||
export interface TransformSelection {
|
||||
wallIds: string[];
|
||||
drawingId: string | null;
|
||||
}
|
||||
|
||||
/** Wie viele Klickpunkte die Operation braucht (Drehen = 3, sonst 2). */
|
||||
export const requiredPoints = (op: TransformOp): number => (op === "rotate" ? 3 : 2);
|
||||
|
||||
/**
|
||||
* Punkt-Transformation einer Operation bei „Anteil" t (0..1 oder Vielfache):
|
||||
* • move — Verschiebung um t·Vektor (pts[0]→pts[1]).
|
||||
* • rotate — Drehung um pts[0] um t·(Winkel(pts[2]) − Winkel(pts[1])).
|
||||
* • mirror — Spiegelung an der Achse pts[0]→pts[1] (t wird ignoriert).
|
||||
*/
|
||||
export function transformFn(op: TransformOp, pts: Vec2[], t: number): (p: Vec2) => Vec2 {
|
||||
if (op === "move") {
|
||||
const vx = (pts[1].x - pts[0].x) * t;
|
||||
const vy = (pts[1].y - pts[0].y) * t;
|
||||
return (p) => ({ x: p.x + vx, y: p.y + vy });
|
||||
}
|
||||
if (op === "rotate") {
|
||||
const c = pts[0];
|
||||
const a0 = Math.atan2(pts[1].y - c.y, pts[1].x - c.x);
|
||||
const a1 = Math.atan2(pts[2].y - c.y, pts[2].x - c.x);
|
||||
const ang = (a1 - a0) * t;
|
||||
const cos = Math.cos(ang), sin = Math.sin(ang);
|
||||
return (p) => {
|
||||
const dx = p.x - c.x, dy = p.y - c.y;
|
||||
return { x: c.x + dx * cos - dy * sin, y: c.y + dx * sin + dy * cos };
|
||||
};
|
||||
}
|
||||
// mirror: Spiegelung an der Geraden pts[0]→pts[1].
|
||||
const a = pts[0], b = pts[1];
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const len2 = dx * dx + dy * dy || 1;
|
||||
return (p) => {
|
||||
// Projektion von p auf die Achse, dann gespiegelt.
|
||||
const tt = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
||||
const fx = a.x + dx * tt, fy = a.y + dy * tt;
|
||||
return { x: 2 * fx - p.x, y: 2 * fy - p.y };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Die „Anteile" t der KOPIEN (ohne Original) je Modus. Beim Spiegeln ergeben
|
||||
* Array/Verteilen keine Reihe (Spiegelung ist selbstinvers) → genau eine Kopie.
|
||||
*/
|
||||
export function copyTs(op: TransformOp, mode: CopyMode, count: number): number[] {
|
||||
if (mode === "move") return [];
|
||||
if (op === "mirror") return [1]; // copy/array/distribute → eine gespiegelte Kopie
|
||||
if (mode === "copy") return [1];
|
||||
const n = Math.max(1, Math.floor(count));
|
||||
if (mode === "array") return Array.from({ length: n }, (_, k) => k + 1);
|
||||
// distribute: n Kopien gleichmäßig zwischen Original (0) und Ziel (1).
|
||||
return Array.from({ length: n }, (_, k) => (k + 1) / n);
|
||||
}
|
||||
|
||||
/** Wird beim Modus „move" (U) das Original selbst transformiert? */
|
||||
export const movesOriginal = (mode: CopyMode): boolean => mode === "move";
|
||||
|
||||
// ── Geometrie einer Auswahl ─────────────────────────────────────────────────
|
||||
|
||||
interface SelItem {
|
||||
kind: "wall";
|
||||
wall: Wall;
|
||||
}
|
||||
interface SelDrawingItem {
|
||||
kind: "drawing";
|
||||
drawing: Drawing2D;
|
||||
}
|
||||
type AnyItem = SelItem | SelDrawingItem;
|
||||
|
||||
function gatherItems(project: Project, sel: TransformSelection): AnyItem[] {
|
||||
const items: AnyItem[] = [];
|
||||
const ids = new Set(sel.wallIds);
|
||||
for (const w of project.walls) if (ids.has(w.id)) items.push({ kind: "wall", wall: w });
|
||||
if (sel.drawingId) {
|
||||
const d = project.drawings2d.find((x) => x.id === sel.drawingId);
|
||||
if (d) items.push({ kind: "drawing", drawing: d });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
const mvGeom = (geom: Drawing2DGeom, fn: (p: Vec2) => Vec2): Drawing2DGeom => {
|
||||
switch (geom.shape) {
|
||||
case "line":
|
||||
return { ...geom, a: fn(geom.a), b: fn(geom.b) };
|
||||
case "polyline":
|
||||
return { ...geom, pts: geom.pts.map(fn) };
|
||||
case "rect": {
|
||||
// Achsparallel nur bei reiner Verschiebung erhaltbar; sonst → Polylinie.
|
||||
const c = [
|
||||
geom.min,
|
||||
{ x: geom.max.x, y: geom.min.y },
|
||||
geom.max,
|
||||
{ x: geom.min.x, y: geom.max.y },
|
||||
].map(fn);
|
||||
const axisAligned =
|
||||
Math.abs(c[0].y - c[1].y) < 1e-6 &&
|
||||
Math.abs(c[1].x - c[2].x) < 1e-6 &&
|
||||
Math.abs(c[2].y - c[3].y) < 1e-6;
|
||||
if (axisAligned) {
|
||||
const xs = c.map((p) => p.x), ys = c.map((p) => p.y);
|
||||
return {
|
||||
shape: "rect",
|
||||
min: { x: Math.min(...xs), y: Math.min(...ys) },
|
||||
max: { x: Math.max(...xs), y: Math.max(...ys) },
|
||||
};
|
||||
}
|
||||
return { shape: "polyline", pts: c, closed: true };
|
||||
}
|
||||
case "circle":
|
||||
return { ...geom, center: fn(geom.center) };
|
||||
case "arc":
|
||||
return { ...geom, center: fn(geom.center) };
|
||||
case "text":
|
||||
return { ...geom, at: fn(geom.at) };
|
||||
}
|
||||
};
|
||||
|
||||
/** Vorschau-Formen für ein Element unter der Punkt-Transformation `fn`. */
|
||||
function itemPreview(project: Project, item: AnyItem, fn: (p: Vec2) => Vec2): DraftShape[] {
|
||||
if (item.kind === "wall") {
|
||||
const w = item.wall;
|
||||
const wt = project.wallTypes.find((t) => t.id === w.wallTypeId);
|
||||
const th = wt ? wallTypeThickness(wt) : 0.2;
|
||||
const a = fn(w.start), b = fn(w.end);
|
||||
return [
|
||||
{ kind: "poly", pts: wallCorners(a, b, th), closed: true },
|
||||
{ kind: "line", a, b },
|
||||
];
|
||||
}
|
||||
const g = mvGeom(item.drawing.geom, fn);
|
||||
switch (g.shape) {
|
||||
case "line":
|
||||
return [{ kind: "line", a: g.a, b: g.b }];
|
||||
case "polyline": {
|
||||
const out: DraftShape[] = [];
|
||||
for (let i = 0; i < g.pts.length - 1; i++) out.push({ kind: "line", a: g.pts[i], b: g.pts[i + 1] });
|
||||
if (g.closed && g.pts.length > 2) out.push({ kind: "line", a: g.pts[g.pts.length - 1], b: g.pts[0] });
|
||||
return out;
|
||||
}
|
||||
case "rect":
|
||||
return [{ kind: "poly", pts: [g.min, { x: g.max.x, y: g.min.y }, g.max, { x: g.min.x, y: g.max.y }], closed: true }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorschau aller transformierten Instanzen (bewegtes Original bei U + alle
|
||||
* Kopien). `pts` sind die bisher gesetzten Punkte inkl. Cursor (Länge =
|
||||
* requiredPoints). Liefert die Stütz-/Basis-Punkte separat für Marker.
|
||||
*/
|
||||
export function transformPreview(
|
||||
project: Project,
|
||||
sel: TransformSelection,
|
||||
op: TransformOp,
|
||||
pts: Vec2[],
|
||||
mode: CopyMode,
|
||||
count: number,
|
||||
): DraftShape[] {
|
||||
const items = gatherItems(project, sel);
|
||||
const shapes: DraftShape[] = [];
|
||||
const instances: number[] = [...copyTs(op, mode, count)];
|
||||
if (movesOriginal(mode)) instances.push(1); // U: Original an Endlage zeigen
|
||||
for (const t of instances) {
|
||||
const fn = transformFn(op, pts, t);
|
||||
for (const item of items) shapes.push(...itemPreview(project, item, fn));
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
// ── Commit ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Wendet die Transformation immutabel auf das Projekt an (Original + Kopien). */
|
||||
export function commitTransform(
|
||||
project: Project,
|
||||
sel: TransformSelection,
|
||||
op: TransformOp,
|
||||
pts: Vec2[],
|
||||
mode: CopyMode,
|
||||
count: number,
|
||||
): Project {
|
||||
const items = gatherItems(project, sel);
|
||||
if (items.length === 0) return project;
|
||||
const wallIds = new Set(sel.wallIds);
|
||||
|
||||
let walls = project.walls;
|
||||
let drawings2d = project.drawings2d;
|
||||
|
||||
// U (move): Original an die Endlage (t=1) transformieren.
|
||||
if (movesOriginal(mode)) {
|
||||
const fn = transformFn(op, pts, 1);
|
||||
walls = walls.map((w) => (wallIds.has(w.id) ? { ...w, start: fn(w.start), end: fn(w.end) } : w));
|
||||
if (sel.drawingId) {
|
||||
drawings2d = drawings2d.map((d) =>
|
||||
d.id === sel.drawingId ? { ...d, geom: mvGeom(d.geom, fn) } : d,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Kopien anhängen (I/O/P bzw. mirror copy).
|
||||
const newWalls: Wall[] = [];
|
||||
const newDrawings: Drawing2D[] = [];
|
||||
for (const t of copyTs(op, mode, count)) {
|
||||
const fn = transformFn(op, pts, t);
|
||||
for (const item of items) {
|
||||
if (item.kind === "wall") {
|
||||
const w = item.wall;
|
||||
newWalls.push({ ...w, id: uniqueId("W"), start: fn(w.start), end: fn(w.end) });
|
||||
} else {
|
||||
const d = item.drawing;
|
||||
newDrawings.push({ ...d, id: uniqueId("dr2d"), geom: mvGeom(d.geom, fn) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...project,
|
||||
walls: newWalls.length ? [...walls, ...newWalls] : walls,
|
||||
drawings2d: newDrawings.length ? [...drawings2d, ...newDrawings] : drawings2d,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user