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:
2026-06-30 20:52:27 +02:00
commit ca859c4aa4
157 changed files with 37921 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
// Probe für den Tab-Feld-Zyklus (Rhino-Präzisionseingabe §2.7).
//
// Treibt headless:
// A) Tab → „line"⏎ → Startpunkt „0,0"⏎ → „3"⏎ (lockt Länge, weiter zu Winkel)
// → „45"⏎ (lockt Winkel ⇒ committet) ⇒ EXAKTE 3-m-Linie unter 45°.
// B) Tab → „rect"⏎ → erste Ecke „0,-3"⏎ → Maus nach rechts-oben (Quadrant)
// → „4"⏎ (Breite) → „2"⏎ (Höhe ⇒ committet) ⇒ 4×2-Rechteck.
//
// Verifikation: liest die gerenderten `.plan-svg line.draw2d`-Endpunkte. Die
// Plan-Transform ist toScreen(p) = { x: p.x*90, y: -p.y*90 } (PX_PER_M=90, Y
// geflippt), also gilt für Modell-Längen/-Winkel:
// modelLen = pixelLen / 90
// modelAngle = atan2(-(y2-y1), x2-x1) (Y-Flip rückgängig)
// Screenshot scripts/probe-tab-fields.png zeigt Linie + Rechteck + Feld-Boxen.
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const PX_PER_M = 90;
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 2 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
await sleep(600);
// Liest die draw2d-Segmente (in toScreen-Koordinaten = x*90 / -y*90) und rechnet
// in Modell-Länge/-Winkel zurück.
async function segments() {
const raw = await page.evaluate(() =>
[...document.querySelectorAll(".plan-svg line.draw2d")].map((l) => ({
x1: +l.getAttribute("x1"),
y1: +l.getAttribute("y1"),
x2: +l.getAttribute("x2"),
y2: +l.getAttribute("y2"),
})),
);
return raw.map((s) => {
const dxPx = s.x2 - s.x1;
const dyPx = s.y2 - s.y1;
const lenM = Math.hypot(dxPx, dyPx) / PX_PER_M;
// Y-Flip rückgängig: Modell-dy = -(Screen-dy).
let ang = (Math.atan2(-dyPx, dxPx) * 180) / Math.PI;
if (ang <= -0.0001) ang += 360; // 0..360
return { lenM: +lenM.toFixed(4), angleDeg: +ang.toFixed(2) };
});
}
// Liest die aktuellen Feld-Boxen der Command-Line (Label + Wert/„—" + aktiv).
async function fieldBoxes() {
return page.evaluate(() =>
[...document.querySelectorAll(".cmdline-field")].map((f) => ({
label: f.querySelector(".cmdline-field-label")?.textContent || "",
val: f.querySelector(".cmdline-field-val")?.textContent || "",
active: f.classList.contains("active"),
})),
);
}
async function focusCmdline() {
await page.mouse.click(600, 400); // in den Body, damit Tab nicht ein Feld trifft
await sleep(80);
await page.keyboard.press("Tab");
await sleep(120);
}
async function typeLine(text) {
await page.type(".cmdline-input", text, { delay: 12 });
await page.keyboard.press("Enter");
await sleep(140);
}
const box = await page.evaluate(() => {
const r = document.querySelector(".plan-svg").getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});
const cx = box.x + box.w / 2;
const cy = box.y + box.h / 2;
// ── A) Linie über Felder: Länge 3, Winkel 45° ───────────────────────────────
await focusCmdline();
await typeLine("line");
const promptLine = await page.evaluate(
() => document.querySelector(".cmdline-prompt")?.textContent || "",
);
await typeLine("0,0"); // Startpunkt → Schritt „end" (Feld-Modus aktiv)
const fieldsAtStart = await fieldBoxes();
// Maus woanders hin (zeigt: ungelockte Felder folgen der Maus, gelockte nicht).
await page.mouse.move(cx + 120, cy - 30);
await sleep(80);
await typeLine("3"); // lockt Länge → weiter zu Winkel
const fieldsAfterLen = await fieldBoxes();
// Screenshot MITTEN im Befehl: zeigt die Feld-Boxen (Länge=3, Winkel aktiv „—")
// in der Command-Line + die schon gelockte Vorschau-Linie.
await page.screenshot({ path: "scripts/probe-tab-fields-midcmd.png" });
await typeLine("45"); // lockt Winkel ⇒ committet die Linie
await sleep(150);
const segsA = await segments();
await page.screenshot({ path: "scripts/probe-tab-fields.png" });
// ── B) Rechteck über Felder: Breite 4, Höhe 2 ───────────────────────────────
await focusCmdline();
await typeLine("rect");
await typeLine("0,-3"); // erste Ecke (unter dem Ursprung, freie Fläche)
// Maus nach rechts-oben → Quadrant +x,+y (bestimmt Vorzeichen der Locks).
await page.mouse.move(cx + 150, cy - 80);
await sleep(80);
await typeLine("4"); // Breite
const rectFieldsAfterW = await fieldBoxes();
await typeLine("2"); // Höhe ⇒ committet
await sleep(150);
// Rechteck wird als 4 draw2d-Linien gerendert; sammeln + auf Auswahl umschalten,
// damit das PNG das fertige Rechteck zeigt.
const allSegs = await segments();
await page.screenshot({ path: "scripts/probe-tab-fields-rect.png" });
console.log("prompt after 'line':", JSON.stringify(promptLine));
console.log("fields at end-step start:", JSON.stringify(fieldsAtStart));
console.log("fields after locking length=3:", JSON.stringify(fieldsAfterLen));
console.log("LINE segments (model len/angle):", JSON.stringify(segsA));
console.log("rect fields after width=4:", JSON.stringify(rectFieldsAfterW));
console.log("ALL segments after rect (len/angle):", JSON.stringify(allSegs));
// ── Assertions ───────────────────────────────────────────────────────────────
const line3 = segsA.find((s) => Math.abs(s.lenM - 3) < 0.02 && Math.abs(s.angleDeg - 45) < 0.5);
console.log(
line3
? `PASS: line length≈3 (${line3.lenM}) angle≈45 (${line3.angleDeg})`
: "FAIL: no 3m/45° line found",
);
// Rechteck: zwei horizontale Kanten der Länge 4 + zwei vertikale der Länge 2.
const horiz4 = allSegs.filter(
(s) => Math.abs(s.lenM - 4) < 0.03 && (Math.abs(s.angleDeg) < 1 || Math.abs(s.angleDeg - 180) < 1 || Math.abs(s.angleDeg - 360) < 1),
);
const vert2 = allSegs.filter(
(s) => Math.abs(s.lenM - 2) < 0.03 && (Math.abs(s.angleDeg - 90) < 1 || Math.abs(s.angleDeg - 270) < 1),
);
console.log(
horiz4.length >= 2 && vert2.length >= 2
? `PASS: rect 4×2 (horiz4=${horiz4.length}, vert2=${vert2.length})`
: `FAIL: rect not 4×2 (horiz4=${horiz4.length}, vert2=${vert2.length})`,
);
console.log("=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();