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
+204
View File
@@ -0,0 +1,204 @@
// Probe für die Migration auf Component/HatchStyle/LineStyle + den Ressourcen-
// Manager. Erfasst (deviceScaleFactor 2, headless, SwiftShader) vier Zustände:
//
// (a) Standard-App: EG-Grundriss (mehrschichtige Wände, Schraffuren, saubere
// Ecken, Tür) — beweist, dass die Modell-Migration das Verhalten erhält.
// (b) Ressourcen-Manager → Tab „Bauteile": Liste der Components MIT ihren
// joinPriority-Werten.
// (c) Live-Edit: Farbe eines Bauteils (Backstein) ändern → der Grundriss
// aktualisiert sich live auf die neue Farbe (Vorher/Nachher + DOM-Fills).
// (d) Tab „Schraffuren" und Tab „Linien" mit editierbaren Feldern.
//
// Aufruf: node scripts/probe-resources.mjs
// Hinweis: networkidle-GOTO-Timeout wird ignoriert (Vite-HMR hält Verbindungen
// offen); wir warten stattdessen auf konkrete DOM-Knoten.
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const OUT = "scripts";
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: 1360, height: 900, 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));
// Klick auf das Element, dessen Textinhalt `text` enthält (innerhalb selector).
async function clickByText(selector, text) {
const handle = await page.evaluateHandle(
(sel, t) =>
[...document.querySelectorAll(sel)].find((el) =>
el.textContent.includes(t),
),
selector,
text,
);
const el = handle.asElement();
if (!el) throw new Error(`Element nicht gefunden: ${selector} ~ "${text}"`);
await el.click();
}
// Setzt den Wert eines Inputs so, dass Reacts onChange feuert.
async function setInputValue(elHandle, value) {
await page.evaluate(
(el, v) => {
const proto =
el.type === "color"
? window.HTMLInputElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, "value").set;
setter.call(el, v);
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
},
elHandle,
value,
);
}
// Liest die Füllfarben aller Grundriss-Polygone (Component-Poché) + Anzahl.
const planFills = () =>
page.evaluate(() => {
const polys = [...document.querySelectorAll(".plan-svg polygon")];
const fills = polys
.map((p) => p.getAttribute("fill"))
.filter((f) => f && !f.startsWith("url("));
const patterns = [...document.querySelectorAll(".plan-svg defs pattern")]
.length;
const arcs = document.querySelectorAll(".plan-svg .door-swing").length;
const leaves = document.querySelectorAll(".plan-svg .door-leaf").length;
return {
polyCount: polys.length,
fills,
uniqueFills: [...new Set(fills)],
patternDefs: patterns,
doorArcs: arcs,
doorLeaves: leaves,
};
});
// Liest die Bauteil-Tabelle des Managers: Name + joinPriority je Zeile.
const componentRows = () =>
page.evaluate(() => {
const rows = [...document.querySelectorAll(".res-list .res-row")];
return rows.map((r) => {
const name = r.querySelector('input[type="text"]')?.value ?? null;
const prio = r.querySelector(".res-prio input")?.value ?? null;
const color = r.querySelector('input[type="color"]')?.value ?? null;
const hatch =
r.querySelector("select")?.selectedOptions?.[0]?.textContent ?? null;
return { name, joinPriority: prio, color, hatch };
});
});
// Liest die aktiven Tab-Beschriftungen + welcher Tab aktiv ist.
const tabState = () =>
page.evaluate(() => {
const tabs = [...document.querySelectorAll(".res-tab-btn")].map((b) => ({
label: b.textContent.trim(),
active: b.classList.contains("active"),
}));
const rowCount = document.querySelectorAll(".res-list .res-row").length;
const fieldLabels = [
...document.querySelectorAll(".res-list .res-row:first-child .res-field-label"),
].map((s) => s.textContent.trim());
const inputCount = document.querySelectorAll(
".res-list .res-row:first-child input, .res-list .res-row:first-child select",
).length;
return { tabs, rowCount, firstRowFieldLabels: fieldLabels, firstRowInputCount: inputCount };
});
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 8000 });
} catch (e) {
logs.push(`[GOTO-INFO ignoriert] ${e.message}`);
}
// Auf den gerenderten Grundriss warten (statt networkidle).
await page.waitForSelector(".plan-svg polygon", { timeout: 15000 });
await sleep(500);
// ── (a) Standard: EG-Grundriss ──────────────────────────────────────────────
const fillsBefore = await planFills();
logs.push("(a) PLAN-VORHER " + JSON.stringify(fillsBefore));
await page.screenshot({ path: `${OUT}/probe-resources-a-grundriss.png` });
// ── (b) Ressourcen-Manager öffnen → Bauteile-Tab ────────────────────────────
await clickByText(".res-trigger", "Ressourcen");
await page.waitForSelector(".res-drawer", { timeout: 8000 });
await page.waitForSelector(".res-list .res-row", { timeout: 8000 });
await sleep(300);
logs.push("(b) TAB " + JSON.stringify(await tabState()));
logs.push("(b) BAUTEILE " + JSON.stringify(await componentRows()));
await page.screenshot({ path: `${OUT}/probe-resources-b-bauteile.png` });
// ── (c) Live-Edit: Backstein-Farbe auf knalliges Magenta ────────────────────
// Backstein-Zeile finden (Name-Textfeld == "Backstein"), dessen color-Input setzen.
const NEW_COLOR = "#ff00cc";
const colorHandle = await page.evaluateHandle((target) => {
const row = [...document.querySelectorAll(".res-list .res-row")].find(
(r) => r.querySelector('input[type="text"]')?.value === target,
);
return row?.querySelector('input[type="color"]') ?? null;
}, "Backstein");
if (!colorHandle.asElement())
throw new Error("Backstein-Farbfeld nicht gefunden");
await setInputValue(colorHandle.asElement(), NEW_COLOR);
await sleep(300);
logs.push("(c) BAUTEILE-NACH-EDIT " + JSON.stringify(await componentRows()));
// Screenshot mit offenem Manager (zeigt neuen Swatch im Manager).
await page.screenshot({ path: `${OUT}/probe-resources-c1-manager-edit.png` });
// Manager schließen → Grundriss frei sichtbar, Live-Update prüfen.
await page.evaluate(() => {
const overlay = document.querySelector(".res-overlay");
// auf das Overlay (außerhalb der Schublade) klicken schließt den Manager
overlay?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Falls der Overlay-Klick nicht griff, den ×-Knopf nutzen.
await sleep(200);
const stillOpen = await page.$(".res-drawer");
if (stillOpen) {
await page.evaluate(() => document.querySelector(".res-close")?.click());
}
await page.waitForSelector(".res-drawer", { hidden: true, timeout: 5000 }).catch(() => {});
await sleep(400);
const fillsAfter = await planFills();
logs.push("(c) PLAN-NACHHER " + JSON.stringify(fillsAfter));
const colorApplied = fillsAfter.fills.includes(NEW_COLOR);
logs.push(
`(c) LIVE-UPDATE ${colorApplied ? "OK" : "FEHLT"} — neue Farbe ${NEW_COLOR} ` +
`${colorApplied ? "im" : "NICHT im"} Grundriss; ` +
`uniqueFills vorher=${JSON.stringify(fillsBefore.uniqueFills)} ` +
`nachher=${JSON.stringify(fillsAfter.uniqueFills)}`,
);
await page.screenshot({ path: `${OUT}/probe-resources-c2-plan-updated.png` });
// ── (d) Schraffuren-Tab und Linien-Tab ──────────────────────────────────────
await clickByText(".res-trigger", "Ressourcen");
await page.waitForSelector(".res-drawer", { timeout: 8000 });
await sleep(200);
await clickByText(".res-tab-btn", "Schraffuren");
await page.waitForSelector(".res-list .res-row", { timeout: 8000 });
await sleep(300);
logs.push("(d) SCHRAFFUREN " + JSON.stringify(await tabState()));
await page.screenshot({ path: `${OUT}/probe-resources-d1-schraffuren.png` });
await clickByText(".res-tab-btn", "Linien");
await page.waitForSelector(".res-list .res-row", { timeout: 8000 });
await sleep(300);
logs.push("(d) LINIEN " + JSON.stringify(await tabState()));
await page.screenshot({ path: `${OUT}/probe-resources-d2-linien.png` });
console.log(logs.join("\n"));
await browser.close();