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
+21
View File
@@ -0,0 +1,21 @@
import puppeteer from "puppeteer";
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: 1440, height: 900, deviceScaleFactor: 2 });
const logs=[]; page.on("pageerror",e=>logs.push("[PAGEERROR] "+e.message)); page.on("console",m=>logs.push("[console] "+m.text()));
try { await page.goto("http://localhost:5173/", { waitUntil:"networkidle0", timeout:8000 }); } catch(e){ logs.push("[goto] "+e.message); }
await page.evaluate(()=>{ try{localStorage.removeItem("cad.layout");}catch{} });
await page.reload({waitUntil:"domcontentloaded"}).catch(()=>{});
await page.waitForSelector(".plan-svg polygon",{timeout:20000});
await new Promise(r=>setTimeout(r,800));
const dom = await page.evaluate(()=>({
docks: [...document.querySelectorAll(".dock")].map(d=>d.className),
hasLeft: !!document.querySelector(".dock-left"),
hasRight: !!document.querySelector(".dock-right"),
resTriggerActive: document.querySelector(".res-trigger")?.classList.contains("active") ?? null,
layout: (()=>{try{return JSON.parse(localStorage.getItem("cad.layout"));}catch{return "ERR";}})(),
bodyChildren: [...(document.querySelector(".body")?.children||[])].map(c=>c.className),
}));
console.log(JSON.stringify(dom,null,2));
console.log("LOGS:\n"+logs.join("\n"));
await browser.close();
+10
View File
@@ -0,0 +1,10 @@
import puppeteer from "puppeteer";
const b = await puppeteer.launch({ headless:"new", args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p = await b.newPage(); await p.setViewport({width:1500,height:950,deviceScaleFactor:2});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
try{ await p.goto("http://localhost:5173/",{waitUntil:"networkidle0",timeout:15000}); }catch{}
await new Promise(r=>setTimeout(r,1000));
const info = await p.evaluate(()=>({rootLen:document.getElementById("root")?.innerHTML.length??-1, viteOverlay: !!document.querySelector("vite-error-overlay"), bodyText: document.body.innerText.slice(0,80)}));
await p.screenshot({path:"scripts/probe-now.png"});
console.log(JSON.stringify(info), "errs:", errs.slice(0,2).join(" | ")||"none");
await b.close();
+4
View File
@@ -0,0 +1,4 @@
// Bündel-Einstieg für check-terrain.mjs — re-exportiert die zu testenden
// Funktionen, damit esbuild ein einzelnes ESM-Bundle bauen kann.
export { parseDxf } from "../src/io/dxfParser";
export { generateTerrainFromContours } from "../src/model/terrain";
+21
View File
@@ -0,0 +1,21 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1500,height:950,deviceScaleFactor:2});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
try{await p.goto("http://localhost:5173/",{waitUntil:"networkidle0",timeout:15000});}catch{}
await new Promise(r=>setTimeout(r,1000));
// Browser-Kontextmenü unterdrückt?
const ctxPrevented=await p.evaluate(()=>{const e=new MouseEvent("contextmenu",{bubbles:true,cancelable:true});return !document.body.dispatchEvent(e);});
// Marquee: Links-Drag über den Plan
const svg=await p.$(".plan-svg"); const box=await svg.boundingBox();
await p.mouse.move(box.x+box.width*0.25,box.y+box.height*0.25);
await p.mouse.down(); await p.mouse.move(box.x+box.width*0.85,box.y+box.height*0.85,{steps:8}); await p.mouse.up();
await new Promise(r=>setTimeout(r,300));
const sel=await p.evaluate(()=>{const m=document.body.innerText.match(/Auswahl:?\s*(\d+)?\s*W\w*/i);return m?m[0]:"(keine)";});
await p.screenshot({path:"scripts/verify-de.png"});
// Englisch
const langOk=await p.evaluate(()=>{if(window.__i18n){window.__i18n.setLanguage("en");return true;}return false;});
await new Promise(r=>setTimeout(r,400));
await p.screenshot({path:"scripts/verify-en.png"});
console.log("ctxmenu unterdrückt:",ctxPrevented,"| Marquee-Auswahl:",sel,"| lang-switch:",langOk,"| errs:",errs.slice(0,2).join(" | ")||"keine");
await b.close();
+88
View File
@@ -0,0 +1,88 @@
// Node-seitige Geometrie-Prüfung (kein Browser): generiert eine minimale,
// gültige Test-DXF (Höhenlinien auf Z=400/401/402), schickt sie durch parseDxf
// → generateTerrainFromContours und bestätigt ein plausibles TIN.
//
// Die TS-Quellen werden mit esbuild on-the-fly zu einem ESM-Bundle gebaut und
// importiert (kein tsx nötig).
import { build } from "esbuild";
import { writeFileSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
const outdir = mkdtempSync(join(tmpdir(), "cad-terrain-"));
const outfile = join(outdir, "bundle.mjs");
await build({
entryPoints: [join(process.cwd(), "scripts/_terrain-entry.ts")],
bundle: true,
format: "esm",
platform: "node",
outfile,
logLevel: "warning",
});
const mod = await import(pathToFileURL(outfile).href);
const { parseDxf, generateTerrainFromContours } = mod;
// ── Minimal gültige Test-DXF: drei LWPOLYLINE-Höhenlinien (Z via Elevation 38) ──
// Jede Kontur ist ein leicht versetztes Dreieck/Quadrat auf einer eigenen Höhe.
function ring(z, pts, layer) {
// LWPOLYLINE mit Elevation-Gruppe (38) und 90=Vertex-Zahl, 70=closed(1).
let s = `0\nLWPOLYLINE\n8\n${layer}\n38\n${z}\n90\n${pts.length}\n70\n1\n`;
for (const [x, y] of pts) s += `10\n${x}\n20\n${y}\n`;
return s;
}
const dxf =
`0\nSECTION\n2\nENTITIES\n` +
ring(400, [[0, 0], [10, 0], [10, 10], [0, 10]], "contours") +
ring(401, [[2, 2], [8, 2], [8, 8], [2, 8]], "contours") +
ring(402, [[4, 4], [6, 4], [6, 6], [4, 6]], "contours") +
`0\nENDSEC\n0\nEOF\n`;
const parsed = parseDxf(dxf);
const contourSets = parsed.contours;
const allContours = contourSets.flatMap((cs) => cs.contours);
console.log("=== DXF-Parse ===");
console.log("ContourSets:", contourSets.length, "Konturen:", allContours.length);
console.log("Z-Werte:", allContours.map((c) => c.z).join(", "));
console.log("Meshes (importiert):", parsed.meshes.length);
const terrain = generateTerrainFromContours(allContours);
const vertCount = terrain.positions.length / 3;
const triCount = terrain.indices.length / 3;
let minZ = Infinity, maxZ = -Infinity, nan = 0;
for (let i = 0; i < terrain.positions.length; i++) {
const v = terrain.positions[i];
if (!Number.isFinite(v)) nan++;
}
for (let i = 2; i < terrain.positions.length; i += 3) {
minZ = Math.min(minZ, terrain.positions[i]);
maxZ = Math.max(maxZ, terrain.positions[i]);
}
console.log("\n=== TIN ===");
console.log("Vertices:", vertCount, "Dreiecke:", triCount);
console.log("Z-Range:", minZ, "..", maxZ);
console.log("NaN/Infinity-Werte:", nan);
const ok =
contourSets.length === 1 &&
allContours.length === 3 &&
vertCount > 0 &&
triCount > 0 &&
nan === 0 &&
Math.abs(minZ - 400) < 1e-6 &&
Math.abs(maxZ - 402) < 1e-6;
// Zusatz: leere Eingabe → leeres Mesh (kein Crash).
const empty = generateTerrainFromContours([]);
const emptyOk = empty.positions.length === 0 && empty.indices.length === 0;
console.log("\nLeere-Eingabe → leeres Mesh:", emptyOk ? "OK" : "FEHLER");
console.log("\n" + (ok && emptyOk ? "✅ TIN plausibel" : "❌ TIN NICHT plausibel"));
process.exit(ok && emptyOk ? 0 : 1);
+51
View File
@@ -0,0 +1,51 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
page.on("console", (m) => { if (m.text().includes("DBG")) logs.push(m.text()); });
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
const clickTool = (label) => page.evaluate((l) => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === l)?.click(), label);
const click = async (x, y) => { await page.mouse.move(x, y); await page.mouse.click(x, y); await new Promise((r) => setTimeout(r, 60)); };
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, cy = box.y + box.h / 2;
// Ein kleines Linien-Raster in den Raum zeichnen (3 horizontale + 3 vertikale).
await clickTool("Linie");
await new Promise((r) => setTimeout(r, 100));
const L = box.x + box.w * 0.34, Rr = box.x + box.w * 0.66;
const T = box.y + box.h * 0.42, Bb = box.y + box.h * 0.72;
for (let i = 0; i <= 2; i++) {
const y = T + ((Bb - T) * i) / 2;
await click(L, y); await click(Rr, y);
}
for (let i = 0; i <= 2; i++) {
const x = L + ((Rr - L) * i) / 2;
await click(x, T); await click(x, Bb);
}
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 100));
const draw2dPlan = await page.evaluate(() => document.querySelectorAll(".plan-svg line.draw2d").length);
// Obergeschoss ausblenden (Augen-Icon), damit der EG-Boden mit dem Raster oben
// offen sichtbar ist.
await page.evaluate(() => {
let row = [...document.querySelectorAll("*")].find((e) => e.children.length === 0 && e.textContent.trim() === "Obergeschoss");
while (row && !row.querySelector?.(".eye")) row = row.parentElement;
row?.querySelector(".eye")?.click();
});
await new Promise((r) => setTimeout(r, 200));
// In die Perspektive wechseln und rendern lassen.
await clickTool("Perspektive");
await new Promise((r) => setTimeout(r, 1200));
await page.screenshot({ path: "scripts/probe-3d2d.png" });
console.log("draw2dPlan lines:", draw2dPlan);
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+30
View File
@@ -0,0 +1,30 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const b = await puppeteer.launch({ headless: "new", args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"] });
const page = await b.newPage();
await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 2 });
const logs = []; page.on("pageerror", e => logs.push(e.message));
page.on("dialog", async d => { await d.accept("3"); });
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(()=>{});
await new Promise(r=>setTimeout(r,600));
const clickTool = l => page.evaluate(x => [...document.querySelectorAll("button")].find(b=>b.textContent.trim()===x)?.click(), l);
const click = async (x,y)=>{ await page.mouse.move(x,y); await page.mouse.click(x,y); await new Promise(r=>setTimeout(r,70)); };
const nLines = () => page.evaluate(()=>document.querySelectorAll(".plan-svg line.draw2d").length);
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, cy=box.y+box.h/2;
await clickTool("Linie"); await new Promise(r=>setTimeout(r,80));
const A=[cx-120,cy-140], B=[cx+20,cy-140];
await click(...A); await click(...B);
await clickTool("Auswahl"); await new Promise(r=>setTimeout(r,60));
await click((A[0]+B[0])/2, A[1]); // select midpoint
const sel = await page.evaluate(()=>document.querySelectorAll(".plan-svg .plan-grip").length);
await page.keyboard.press("m"); await new Promise(r=>setTimeout(r,80));
await page.keyboard.press("o"); await new Promise(r=>setTimeout(r,200));
const mode = await page.evaluate(()=>document.querySelector(".transform-bar .tf-mode.active .tf-mode-label")?.textContent);
await click(A[0], A[1]); // base
await click(A[0], A[1]+70); // dest (down by ~70px each step)
await new Promise(r=>setTimeout(r,150));
const n = await nLines();
await page.screenshot({ path:"scripts/probe-array.png" });
console.log("selectedGrips:", sel, "activeMode:", mode, "lines(expect 4):", n, logs.length?logs.join("|"):"(no err)");
await b.close();
+93
View File
@@ -0,0 +1,93 @@
// Probe für das Rhino-artige Befehlssystem (Tier 0 + Line).
// Treibt: Tab → „line"⏎ → „0,0"⏎ → „r3,0"⏎ und screenshotet das Ergebnis.
// Erwartung: eine deutlich DUNKLE 3-m-Linie im Plan + Command-Line zeigt Prompts.
//
// Headless Puppeteer (swiftshader für WebGL). Zweiter Lauf mit „3<45" prüft die
// polare Eingabe. Vorlage: scripts/probe-tools.mjs.
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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);
// Zählt 2D-Zeichenlinien + liefert die computed stroke der ersten (Tinte-Check).
async function inkInfo() {
return page.evaluate(() => {
const lines = [...document.querySelectorAll(".plan-svg line.draw2d")];
const first = lines[0];
const stroke = first ? getComputedStyle(first).stroke : null;
return { count: lines.length, stroke };
});
}
// Tippt einen Befehl/Eine Zeile in die fokussierte Command-Line + Enter.
async function typeLine(text) {
await page.type(".cmdline-input", text, { delay: 12 });
await page.keyboard.press("Enter");
await sleep(150);
}
// 1) Tab → Command-Line fokussieren (globaler Tab-Handler in App).
// Erst in den Body klicken, damit Tab nicht von einem anderen Feld geschluckt wird.
await page.mouse.click(600, 400);
await sleep(80);
await page.keyboard.press("Tab");
await sleep(120);
const focused1 = await page.evaluate(
() => document.activeElement?.className || "",
);
// 2) Befehl + zwei getippte Koordinaten. Startpunkt 1,7 liegt ÜBER dem Raum
// (freies Papier), damit die 3-m-Linie eindeutig sichtbar ist (nicht von einer
// Wandkante überdeckt). Verlangt: 1,7 ⏎ r3,0 ⏎ → 3-m-Linie nach rechts.
await typeLine("line");
const promptAfterLine = await page.evaluate(
() => document.querySelector(".cmdline-prompt")?.textContent || "",
);
await typeLine("1,2");
const promptAfterStart = await page.evaluate(
() => document.querySelector(".cmdline-prompt")?.textContent || "",
);
await typeLine("r3,0");
await sleep(200);
const ink1 = await inkInfo();
await page.screenshot({ path: "scripts/probe-command-line.png" });
// 3) Zweiter Lauf: polare Eingabe 3<45 (neue Linie ab Ursprung).
await page.mouse.click(600, 400);
await sleep(80);
await page.keyboard.press("Tab");
await sleep(120);
await typeLine("line");
await typeLine("1,1");
await typeLine("3<45");
await sleep(200);
const ink2 = await inkInfo();
await page.screenshot({ path: "scripts/probe-command-line-polar.png" });
console.log("focused after Tab:", focused1);
console.log("prompt after 'line':", JSON.stringify(promptAfterLine));
console.log("prompt after '0,0':", JSON.stringify(promptAfterStart));
console.log("ink (after 0,0 + r3,0):", JSON.stringify(ink1));
console.log("ink (after polar 3<45):", JSON.stringify(ink2));
console.log("=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+11
View File
@@ -0,0 +1,11 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1.5});
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,1200));
const opened=await p.evaluate(()=>{const l=[...document.querySelectorAll(".nav-label")].find(e=>/Erdgeschoss|Obergeschoss|Detail|Ansicht|Schnitt/.test(e.textContent||""));if(!l)return "no-row";const row=l.parentElement;const r=row.getBoundingClientRect();const ev=new MouseEvent("contextmenu",{bubbles:true,cancelable:true,clientX:r.x+20,clientY:r.y+r.height/2,button:2});row.dispatchEvent(ev);return "dispatched";});
await new Promise(r=>setTimeout(r,400));
const info=await p.evaluate(()=>{const m=document.querySelector(".ctx-menu");if(!m)return{menu:false};const icons=[...m.querySelectorAll(".ctx-item-icon .material-symbols-outlined")];const cs=icons[0]?getComputedStyle(icons[0]):null;return{menu:true,nItems:m.querySelectorAll(".ctx-item").length,nIcons:icons.length,iconPx:cs?.fontSize,glyphFont:/Material Symbols/.test(cs?.fontFamily||""),ligature:icons[0]?.textContent,hoverBg:getComputedStyle(m.querySelector(".ctx-item")).getPropertyValue("--ctx-hover")};});
await p.screenshot({path:"scripts/probe-ctx.png"});
console.log("trigger:",opened,JSON.stringify(info));
await b.close();
+11
View File
@@ -0,0 +1,11 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1400,height:880,deviceScaleFactor:1.5});
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,1000));
// ersten Dropdown-Trigger (Detailgrad) klicken
const tr=await p.$(".tb-dd-trigger"); if(tr){const r=await tr.boundingBox();await p.mouse.click(r.x+r.width/2,r.y+r.height/2);await new Promise(t=>setTimeout(t,350));}
const info=await p.evaluate(()=>{const menu=document.querySelector(".tb-dd-menu, .ctx-menu");if(!menu)return{open:false};const item=menu.querySelector(".ctx-item, .tb-dd-item");const cs=item?getComputedStyle(item):null;const ic=menu.querySelector(".ctx-item-icon, .material-symbols-outlined");return{open:true,itemFont:cs?.fontSize,itemPad:cs?.padding,menuFont:getComputedStyle(menu).fontSize,radius:getComputedStyle(menu).borderRadius,iconFont:ic?getComputedStyle(ic).fontSize:"(none)"};});
await p.screenshot({path:"scripts/probe-dd-size.png"});
console.log(JSON.stringify(info));
await b.close();
+67
View File
@@ -0,0 +1,67 @@
// Probe: belegt den kontextabhängigen „Darstellung"-Dropdown.
// 1) Grundriss + „Schwarz-Weiss" (mono) → scripts/probe-plan-mono.png
// 2) Grundriss + „Farbig" (Referenz) → scripts/probe-plan-color.png
// 3) Perspektive + „Weiss" (Clay) → scripts/probe-3d-white.png
// 4) Perspektive + „Schattiert" (Ref.) → scripts/probe-3d-shaded.png
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
// Den Ansichts-Umschalter-Button per sichtbarem Text klicken.
const clickByText = async (sel, text) => {
const handle = await page.evaluateHandle(
(sel, text) =>
[...document.querySelectorAll(sel)].find(
(el) => el.textContent.trim() === text,
),
sel,
text,
);
const el = handle.asElement();
if (!el) throw new Error(`Button "${text}" (${sel}) nicht gefunden`);
await el.click();
};
// Den „Darstellung"-Dropdown (per title) auf einen Wert setzen.
const setStyle = async (value) => {
const sel = await page.$('select.layout-menu[title*="Darstellungsart der"]');
if (!sel) throw new Error("Darstellung-Dropdown nicht gefunden");
await sel.select(value);
await new Promise((r) => setTimeout(r, 600));
};
const shot = (name) => page.screenshot({ path: `scripts/${name}` });
// 1) Grundriss aktiv (Default), Schwarz-Weiss.
await clickByText("button.view-btn", "Grundriss");
await new Promise((r) => setTimeout(r, 400));
await setStyle("color");
await shot("probe-plan-color.png");
await setStyle("mono");
await shot("probe-plan-mono.png");
// 2) Perspektive, Weiss vs. Schattiert.
await clickByText("button.view-btn", "Perspektive");
await new Promise((r) => setTimeout(r, 800));
await setStyle("shaded");
await shot("probe-3d-shaded.png");
await setStyle("white");
await new Promise((r) => setTimeout(r, 600));
await shot("probe-3d-white.png");
console.log("Screenshots geschrieben: probe-plan-color/mono, probe-3d-shaded/white");
console.log("\n=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+15
View File
@@ -0,0 +1,15 @@
import puppeteer from "puppeteer";
const b = await puppeteer.launch({ headless:"new", args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"] });
const p = await b.newPage(); await p.setViewport({width:1400,height:900,deviceScaleFactor:2});
const logs=[]; p.on("pageerror",e=>logs.push(e.message));
await p.goto("http://localhost:5174/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,800));
// Default = Display (Haarlinien). Screenshot.
await p.screenshot({ path:"scripts/probe-display.png" });
// Auf Print umschalten.
await p.evaluate(()=>[...document.querySelectorAll("button")].find(x=>x.textContent.trim()==="Print")?.click());
await new Promise(r=>setTimeout(r,400));
await p.screenshot({ path:"scripts/probe-print.png" });
const dispActive = await p.evaluate(()=>!![...document.querySelectorAll(".view-btn.active")].find(b=>b.textContent.trim()==="Print"));
console.log("printActive:", dispActive, logs.length?logs.join("|"):"(no err)");
await b.close();
+48
View File
@@ -0,0 +1,48 @@
import puppeteer from "puppeteer";
// Wie probe.mjs, aber klickt zusätzlich die Zeichnungsebene "Grundriss OG"
// im Navigator und schießt scripts/probe-doc.png.
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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}`));
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
// Navigator-Zeile "Grundriss OG" anklicken.
const clicked = await page.evaluate(() => {
const rows = [...document.querySelectorAll(".nav-row")];
const target = rows.find((r) => r.textContent.includes("Grundriss OG"));
if (target) {
target.click();
return true;
}
return false;
});
logs.push(clicked ? "[INFO] 'Grundriss OG' geklickt" : "[WARN] 'Grundriss OG' nicht gefunden");
await new Promise((r) => setTimeout(r, 800));
await page.screenshot({ path: "scripts/probe-doc.png" });
const info = await page.evaluate(() => {
const title = document.querySelector(".content-single .pane-title")?.textContent ?? "none";
const caption = document.querySelector(".content-single .plan-legend")?.textContent ?? "none";
const polygons = document.querySelectorAll(".content-single .plan-svg polygon").length;
return { title, caption, polygons };
});
console.log("=== Drawing-Layer-Ansicht ===", JSON.stringify(info));
console.log("\n=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+134
View File
@@ -0,0 +1,134 @@
// Probe für das DOSSIER-Dokumentmodell (Augen-Navigator + Mehrgeschoss-3D).
// Erfasst fünf Zustände in separate PNGs (deviceScaleFactor 2):
// (a) Standard: EG Grundriss mit Augen-Navigator (EG aktiv, andere gedimmt).
// (b) Perspektive mit EG + OG sichtbar → zwei Räume vertikal gestapelt.
// (c) OG-Auge ausgeschaltet → nur EG bleibt im 3D.
// (d) EG Geschosshöhe auf 3.5 → größerer Abstand zu OG; "+ Ebene" anklicken
// → neue Kategorie erscheint im Navigator.
// (e) Zeichnungsebene "Detail 1" → leerer Zeichnungs-Platzhalter.
//
// Aufruf: node scripts/probe-dossier.mjs
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: 1280, height: 820, 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();
}
// Klick auf das Augen-Symbol einer Zeichnungsebene mit gegebenem Namen.
async function toggleLevelEyeByName(name) {
const ok = await page.evaluate((n) => {
const row = [...document.querySelectorAll(".nav-row")].find((r) =>
r.querySelector(".nav-label")?.textContent.trim().includes(n),
);
const eye = row?.querySelector(".eye");
if (!eye) return false;
eye.click();
return true;
}, name);
if (!ok) throw new Error(`Augen-Schalter nicht gefunden für Ebene: ${name}`);
}
// Diagnose: aktueller Navigator-Zustand (für Querprüfung der Screenshots).
const navState = () =>
page.evaluate(() => {
const levels = [...document.querySelectorAll(".nav-row")].map((r) => ({
name: r.querySelector(".nav-label")?.textContent.trim(),
active: r.classList.contains("active"),
dimmed: r.classList.contains("hidden"),
elev: r.querySelector(".nav-elev")?.textContent.trim() ?? null,
}));
const cats = [...document.querySelectorAll(".cat-row")].map((r) =>
r.querySelector(".cat-name")?.textContent.trim(),
);
const canvas = document.querySelector(".viewport canvas");
return {
levels,
catCount: cats.length,
catNames: cats,
hasCanvas: !!canvas,
egHeight: document.querySelector(".props-strip input")?.value ?? null,
};
});
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
await sleep(700);
// ── (a) Standard: EG Grundriss mit Augen-Navigator ──────────────────────────
await page.screenshot({ path: `${OUT}/probe-a-eg-grundriss.png` });
logs.push("(a) STATE " + JSON.stringify(await navState()));
// ── (b) Perspektive mit EG + OG sichtbar ───────────────────────────────────
await clickByText(".view-btn", "Perspektive");
await sleep(1400);
await page.screenshot({ path: `${OUT}/probe-b-eg-og-perspektive.png` });
logs.push("(b) STATE " + JSON.stringify(await navState()));
// ── (c) OG-Auge ausschalten → nur EG bleibt im 3D ──────────────────────────
await toggleLevelEyeByName("Obergeschoss");
await sleep(1400);
await page.screenshot({ path: `${OUT}/probe-c-only-eg-3d.png` });
logs.push("(c) STATE " + JSON.stringify(await navState()));
// OG wieder einblenden, damit (d) den Stapel-Abstand zeigt.
await toggleLevelEyeByName("Obergeschoss");
await sleep(400);
// ── (d) EG Geschosshöhe auf 3.5 → größerer Abstand zu OG ───────────────────
// EG ist die Standardauswahl; die props-strip zeigt das aktive Geschoss.
await page.evaluate(() => {
const input = document.querySelector(".props-strip input");
if (!input) throw new Error("Geschosshöhe-Feld nicht gefunden");
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
).set;
setter.call(input, "3.5");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
});
await sleep(1400);
// "+ Ebene" anklicken → neue Kategorie erscheint im Navigator.
await clickByText(".nav-add", "+ Ebene");
await sleep(500);
await page.screenshot({ path: `${OUT}/probe-d-height-and-addlayer.png` });
logs.push("(d) STATE " + JSON.stringify(await navState()));
// ── (e) Zeichnungsebene "Detail 1" → leerer Zeichnungs-Platzhalter ─────────
await clickByText(".nav-label", "Detail 1");
await sleep(500);
await page.screenshot({ path: `${OUT}/probe-e-detail-drawing.png` });
logs.push("(e) STATE " + JSON.stringify(await navState()));
console.log(logs.join("\n"));
await browser.close();
+36
View File
@@ -0,0 +1,36 @@
import puppeteer from "puppeteer";
const URL=process.env.PROBE_URL||"http://localhost:5173/";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto(URL,{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
const lineCount=()=>p.$$eval(".plan-svg line",ls=>ls.length).catch(()=>-1);
const focusCmd=async()=>{ await p.mouse.click(640,760); await p.keyboard.press("Tab"); await new Promise(r=>setTimeout(r,80)); };
const typeLine=async t=>{ await p.type(".cmdline-input",t,{delay:8}); await p.keyboard.press("Enter"); await new Promise(r=>setTimeout(r,90)); };
const prompt=()=>p.evaluate(()=>document.querySelector(".cmdline-prompt")?.textContent||"");
const l0=await lineCount();
// Polyline (offener Zug, Enter beendet)
await focusCmd(); await typeLine("polyline");
const pp=await prompt();
await typeLine("1,1"); await typeLine("r2,0"); await typeLine("r0,1.5");
await p.keyboard.press("Enter"); await new Promise(r=>setTimeout(r,120)); // beenden
const l1=await lineCount();
// Rectangle
await focusCmd(); await typeLine("rect");
const rp=await prompt();
await typeLine("0.5,3"); await typeLine("r3,1.5");
const l2=await lineCount();
// Circle (Radius getippt)
await focusCmd(); await typeLine("circle");
const cp=await prompt();
await typeLine("4,4"); await typeLine("0.8");
const l3=await lineCount();
await p.screenshot({path:"scripts/probe-draw-commands.png"});
console.log(JSON.stringify({
polyPrompt:pp, rectPrompt:rp, circPrompt:cp,
lines_start:l0, after_polyline:l1, after_rect:l2, after_circle:l3,
polyAdded:l1-l0, rectAdded:l2-l1, circleAdded:l3-l2, errs
},null,2));
await b.close();
+50
View File
@@ -0,0 +1,50 @@
import puppeteer from "puppeteer";
const PORT = process.env.PORT || "5173";
const b = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const p = await b.newPage();
await p.setViewport({ width: 1400, height: 420, deviceScaleFactor: 2 });
await p.goto(`http://localhost:${PORT}/`, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {});
await new Promise((r) => setTimeout(r, 800));
// Detailgrad-Dropdown finden (Trigger mit aria-label = Detailgrad-Hint) und öffnen.
// Wir klicken den Trigger in der Detailgrad-Gruppe.
const opened = await p.evaluate(() => {
const groups = [...document.querySelectorAll(".tb-group")];
const detailGroup = groups.find((g) =>
(g.getAttribute("aria-label") || "").includes("Detailgrad"),
);
const trig = detailGroup?.querySelector(".tb-dd-trigger");
if (!trig) return { ok: false };
trig.click();
return { ok: true, label: trig.textContent };
});
await new Promise((r) => setTimeout(r, 250));
const info = await p.evaluate(() => {
const menu = document.querySelector(".tb-dd-menu");
if (!menu) return { menu: false };
const cs = getComputedStyle(menu);
const items = [...menu.querySelectorAll(".tb-dd-item")].map((it) => ({
label: it.querySelector(".ctx-item-label")?.textContent,
active: it.getAttribute("aria-selected") === "true",
isActive: it.classList.contains("is-active"),
}));
return {
menu: true,
bg: cs.backgroundColor,
radius: cs.borderRadius,
items,
};
});
const stillOpen = await p.evaluate(() => !!document.querySelector(".tb-dd-menu"));
console.log(JSON.stringify({ opened, info, stillOpen }, null, 2));
// Mount-Animation (ctx-pop) für den Screenshot deaktivieren, damit das Popover
// im Headless-Capture sicher voll deckend gerendert ist.
await p.addStyleTag({ content: ".tb-dd-menu{animation:none !important;}" });
await new Promise((r) => setTimeout(r, 120));
await p.screenshot({ path: "scripts/probe-dropdown.png", clip: { x: 0, y: 0, width: 1400, height: 320 } });
await b.close();
+68
View File
@@ -0,0 +1,68 @@
// Browser-Laufzeittest: echter DWG-Import über den App-Import-Pfad.
// Lädt eine echte DWG über das versteckte File-Input (uploadFile), wartet bis
// LibreDWG-WASM geparst hat, und screenshotet den Import-Dialog mit der ECHTEN
// Zusammenfassung (Konturen/Layer) statt des ODA-Hinweises.
import puppeteer from "puppeteer";
const URL = process.env.URL || "http://localhost:5173/";
const SAMPLE =
"/tmp/claude-1000/-home-karim-cad/65983130-c639-428b-bd68-85eafeb48d98/scratchpad/sample.dwg";
const b = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const p = await b.newPage();
await p.setViewport({ width: 1300, height: 900, deviceScaleFactor: 2 });
const logs = [];
p.on("pageerror", (e) => logs.push("ERR:" + e.message));
p.on("console", (m) => {
if (m.type() === "error") logs.push("CONSOLE:" + m.text());
});
await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {});
await new Promise((r) => setTimeout(r, 700));
// File-Input mit accept=".dxf,.dwg" finden und die echte DWG hochladen.
const input = await p.$('input[type=file][accept*=".dwg"]');
if (!input) {
console.log("FAIL: DWG-File-Input nicht gefunden.");
await b.close();
process.exit(1);
}
await input.uploadFile(SAMPLE);
// Auf Ladehinweis screenshoten (kann sehr kurz sein), dann auf Ergebnis warten.
await new Promise((r) => setTimeout(r, 150));
await p.screenshot({ path: "scripts/probe-dwg-loading.png" }).catch(() => {});
// Warten, bis der Dialog die echte Zusammenfassung zeigt (oder bis Timeout).
let summaryText = "";
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 250));
summaryText = await p.evaluate(() => {
const sum = document.querySelector(".imp-summary");
const hint = document.querySelector(".imp-dwg-hint");
return JSON.stringify({
summary: sum ? sum.textContent : null,
hint: hint ? hint.textContent : null,
});
});
const parsed = JSON.parse(summaryText);
if (parsed.summary) break; // echte Zusammenfassung erschienen
}
await p.screenshot({ path: "scripts/probe-dwg-import.png" });
const parsed = JSON.parse(summaryText);
console.log("Dialog-Zusammenfassung:", parsed.summary);
console.log("ODA-Hinweis (sollte null sein bei Erfolg):", parsed.hint);
console.log("Logs:", logs.length ? logs.join(" | ") : "(keine)");
if (!parsed.summary) {
console.log("FAIL: keine echte Zusammenfassung — DWG nicht geparst.");
await b.close();
process.exit(1);
}
console.log("OK: DWG über den Import-Pfad geparst, echte Zusammenfassung sichtbar.");
await b.close();
+95
View File
@@ -0,0 +1,95 @@
// Node-Laufzeittest für den echten DWG-Import-Pfad (LibreDWG-WASM).
//
// Bündelt src/io/dwgParser.ts (samt Model-Typen) per esbuild zu einem ESM-Modul
// und ruft parseDwg() mit echten DWG-Beispieldateien auf. Loggt die erkannten
// Konturen/Meshes — spiegelt damit exakt das, was der Import-Dialog anzeigen
// würde (dieselbe parseDwg-Funktion, die auch der Browser-Import nutzt).
//
// Aufruf: node scripts/probe-dwg.mjs [pfad/zur.dwg ...]
// Ohne Argumente werden einige bekannte System-Beispiele probiert.
import { build } from "esbuild";
import { readFile, writeFile, unlink, access } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");
// parseDwg + Abhängigkeiten zu einem temporären ESM-Bundle backen. WASM-Lib
// bleibt extern (wird zur Laufzeit aus node_modules geladen → echtes WASM).
const outFile = resolve(root, "scripts/_dwgparser.bundle.mjs");
await build({
entryPoints: [resolve(root, "src/io/dwgParser.ts")],
bundle: true,
format: "esm",
platform: "node",
outfile: outFile,
external: ["@mlightcad/libredwg-web"],
logLevel: "warning",
});
const { parseDwg } = await import(outFile + "?t=" + Date.now());
const candidates =
process.argv.slice(2).length > 0
? process.argv.slice(2)
: [
"/home/karim/Downloads/02-15-cad-blocks-net-bushes-elevation.dwg",
"/home/karim/ssd/Nextcloud2 (Copy)/00.2 Situation Fluhmatt Bodengeschosse.dwg",
"/tmp/probe.dwg",
];
let anyOk = false;
for (const path of candidates) {
try {
await access(path);
} catch {
console.log(`SKIP (nicht gefunden): ${path}`);
continue;
}
try {
const buf = await readFile(path);
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const t0 = Date.now();
const res = await parseDwg(ab);
const dt = Date.now() - t0;
const contourSets = res.contours;
const contourCount = contourSets.reduce(
(s, set) => s + set.contours.length,
0,
);
const layers = new Set();
for (const set of contourSets)
for (const c of set.contours) if (c.layer) layers.add(c.layer);
const triCount = res.meshes.reduce((s, m) => s + m.indices.length / 3, 0);
console.log(`\nOK ${path} (${dt} ms)`);
console.log(` Konturen-Sätze: ${contourSets.length}`);
console.log(` Konturen: ${contourCount}`);
console.log(` Meshes: ${res.meshes.length} (${triCount} Dreiecke)`);
console.log(
` Layer: ${
layers.size ? [...layers].slice(0, 12).join(", ") : "(keine)"
}`,
);
// Beispiel-Kontur (erste mit ≥2 Punkten) ausgeben.
const sample = contourSets
.flatMap((s) => s.contours)
.find((c) => c.pts.length >= 2);
if (sample) {
console.log(
` Beispiel-Kontur: ${sample.pts.length} Pkt, closed=${sample.closed}, z=${sample.z}, layer=${sample.layer ?? "-"}`,
);
}
anyOk = true;
} catch (err) {
console.log(`\nFEHLER ${path}:`, err?.message ?? err);
}
}
await unlink(outFile).catch(() => {});
if (!anyOk) {
console.log("\nKeine DWG erfolgreich gelesen.");
process.exit(1);
}
+46
View File
@@ -0,0 +1,46 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 700));
const click = async (x, y) => { await page.mouse.move(x, y); await page.mouse.click(x, y); await new Promise((r) => setTimeout(r, 100)); };
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 }; });
// 1) Auswahl-Werkzeug ist Default: die OBERE Wand des Beispiel-Raums anklicken
// → Eckpunkt-Griffe (Quadrate) + 1 dreieckiger Kanten-Anfasser erscheinen.
const tx = box.x + box.w * 0.5;
const ty = box.y + box.h * 0.26; // mittig auf das obere Wandband
await click(tx, ty);
const c1 = await page.evaluate(() => ({
grips: document.querySelectorAll(".plan-svg .plan-grip").length,
edges: document.querySelectorAll(".plan-svg .plan-edge-grip").length,
}));
await page.screenshot({ path: "scripts/probe-edge-grips-selected.png" });
// 2) Den Kanten-Anfasser (sitzt etwas OBERHALB der Kante, da Außennormale nach
// oben zeigt) nach OBEN ziehen → die ganze Wand verschiebt sich senkrecht.
const handleX = tx;
const handleY = ty - 13; // EDGE_GRIP_OFFSET_PX nach außen (oben)
await page.mouse.move(handleX, handleY);
await page.mouse.down();
await page.mouse.move(handleX, handleY - 100, { steps: 10 });
await page.mouse.up();
await new Promise((r) => setTimeout(r, 200));
const c2 = await page.evaluate(() => ({
grips: document.querySelectorAll(".plan-svg .plan-grip").length,
edges: document.querySelectorAll(".plan-svg .plan-edge-grip").length,
}));
await page.screenshot({ path: "scripts/probe-edge-grips-dragged.png" });
console.log("after select:", JSON.stringify(c1));
console.log("after drag:", JSON.stringify(c2));
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+43
View File
@@ -0,0 +1,43 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5173/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
// Screen-bbox aller Wand-Polygone bestimmen, dann auf die obere Bandkante klicken.
const bb=await p.evaluate(()=>{
const polys=[...document.querySelectorAll(".plan-svg polygon")];
if(!polys.length) return null;
let minX=1e9,minY=1e9,maxX=-1e9,maxY=-1e9;
for(const pl of polys){ const r=pl.getBoundingClientRect(); minX=Math.min(minX,r.left);minY=Math.min(minY,r.top);maxX=Math.max(maxX,r.right);maxY=Math.max(maxY,r.bottom);}
return {minX,minY,maxX,maxY};
});
let sel=false;
if(bb){
const cx=(bb.minX+bb.maxX)/2;
// ein paar Punkte auf der oberen Bandkante probieren
for(const dy of [4,8,12,16]){
await p.mouse.click(cx, bb.minY+dy); await new Promise(r=>setTimeout(r,120));
const k=await p.evaluate(()=>document.querySelector(".objinfo-kind")?.textContent||"");
if(/Wand/.test(k)){ sel=true; break; }
}
}
const dims=()=>p.evaluate(()=>[...document.querySelectorAll(".objinfo-input")].map(i=>i.value));
const before=await dims();
const tri=await p.evaluate(()=>{
const el=document.querySelector(".plan-edge-grip"); if(!el) return null;
const pts=el.getAttribute("points").trim().split(/\s+/).map(s=>s.split(",").map(Number));
const svg=el.ownerSVGElement; const ctm=svg.getScreenCTM();
const cs=pts.map(([x,y])=>{const pt=svg.createSVGPoint();pt.x=x;pt.y=y;const sp=pt.matrixTransform(ctm);return [sp.x,sp.y];});
return {cx:cs.reduce((a,c)=>a+c[0],0)/3, cy:cs.reduce((a,c)=>a+c[1],0)/3, count:document.querySelectorAll(".plan-edge-grip").length};
});
let after=before, dragged=false;
if(tri){
await p.mouse.move(tri.cx,tri.cy); await p.mouse.down();
for(let i=1;i<=6;i++){ await p.mouse.move(tri.cx, tri.cy - i*8); await new Promise(r=>setTimeout(r,20)); }
await p.mouse.up(); await new Promise(r=>setTimeout(r,200));
after=await dims(); dragged=true;
}
await p.screenshot({path:"scripts/probe-edge2.png"});
console.log(JSON.stringify({wallSelected:sel, edgeGripCount:tri&&tri.count, dims_before:before, dims_after:after, errs}, null, 2));
await b.close();
+248
View File
@@ -0,0 +1,248 @@
// Probe für die Tier-1-Editierbefehle: Move / Copy / Offset.
//
// Treibt das Rhino-artige Befehlssystem headless und prüft die EXAKTE Geometrie
// aus dem gerenderten SVG (Drawing2D → `line.draw2d`, Screen→Modell via
// model = {x: x/90, y: -y/90}; PX_PER_M=90, Y-flip). Schritte:
// 1) line „0,0"→„r4,0" → eine waagerechte 4-m-Linie bei y=0
// 2) Linie anklicken (selektieren) → move „0,0"→„0,2" → Linie liegt bei y=2
// 3) copy „0,0"→„r2,0" → zwei Linien (Original + Kopie 2 m rechts)
// 4) eine Polylinie zeichnen + selektieren → offset Distanz 0.5 + Seite
// → zweite, parallel um 0.5 versetzte Kurve
//
// Vorlage: scripts/probe-command-line.mjs / probe-draw-commands.mjs.
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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: 1280, height: 820, deviceScaleFactor: 1 });
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);
const PX = 90; // PX_PER_M
// Liest alle Drawing2D-Liniensegmente als Modell-Koordinaten (Screen→Modell).
async function segs() {
return page.evaluate((PX) => {
const out = [];
for (const ln of document.querySelectorAll(".plan-svg line.draw2d")) {
// Bildschirm-CTM: SVG-User → Screen. Wir wollen Modell, daher rechnen wir
// über die Element-Endpunkte im SVG-User-Raum (x1/y1/x2/y2) zurück.
const x1 = parseFloat(ln.getAttribute("x1"));
const y1 = parseFloat(ln.getAttribute("y1"));
const x2 = parseFloat(ln.getAttribute("x2"));
const y2 = parseFloat(ln.getAttribute("y2"));
out.push({
a: { x: x1 / PX, y: -y1 / PX },
b: { x: x2 / PX, y: -y2 / PX },
});
}
return out;
}, PX);
}
// Klick-Zentrum eines .draw2d-Segments in Client-Pixeln (über BoundingClientRect),
// damit die Auswahl pan/zoom-unabhängig trifft. `idx` = welches Segment.
async function clickSegment(idx) {
const box = await page.evaluate((idx) => {
const lines = document.querySelectorAll(".plan-svg line.draw2d");
const ln = lines[idx];
if (!ln) return null;
const r = ln.getBoundingClientRect();
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
}, idx);
if (!box) {
logs.push(`[WARN] Segment ${idx} nicht gefunden (übersprungen)`);
return false;
}
await page.mouse.click(box.x, box.y);
await sleep(120);
return true;
}
// Fokussiert die Command-Line DIREKT (ohne Plan-Klick — ein Plan-Klick würde die
// Auswahl löschen, die Move/Copy/Offset gerade brauchen). Das Input ist immer im
// DOM; ein Fokus genügt, danach landet getippter Text in der Engine.
const focusCmd = async () => {
await page.focus(".cmdline-input");
await sleep(60);
};
const typeLine = async (t) => {
await page.type(".cmdline-input", t, { delay: 8 });
await page.keyboard.press("Enter");
await sleep(110);
};
const prompt = () =>
page.evaluate(() => document.querySelector(".cmdline-prompt")?.textContent || "");
// Hilfsfunktion: nahe Gleichheit.
const near = (a, b, eps = 0.02) => Math.abs(a - b) <= eps;
// ── 1) Linie zeichnen: 0,0 → r4,0 (waagerecht, y=0) ─────────────────────────
await focusCmd();
await typeLine("line");
await typeLine("0,0");
await typeLine("r4,0");
await sleep(150);
const afterLine = await segs();
// ── 2) Linie selektieren + Move 0,0 → 0,2 ───────────────────────────────────
await clickSegment(0); // die eine Linie wählen
const gripsAfterClick = await page.evaluate(
() => document.querySelectorAll(".plan-svg .plan-grip").length,
);
await focusCmd();
const gripsAfterFocus = await page.evaluate(
() => document.querySelectorAll(".plan-svg .plan-grip").length,
);
const moveSelInfo = gripsAfterClick;
logs.push(`[DBG] grips afterClick=${gripsAfterClick} afterFocus=${gripsAfterFocus}`);
await typeLine("move");
const movePrompt = await prompt();
await typeLine("0,0");
const moveTargetPrompt = await prompt();
await typeLine("0,2");
await sleep(150);
const afterMove = await segs();
await page.screenshot({ path: "scripts/probe-edit-move.png" });
// ── 3) Copy: 0,0 → r2,0 (Auswahl bleibt nach Move bestehen) ─────────────────
// Move hält die Auswahl (selectedDrawingId unverändert). Falls nicht, neu wählen.
await clickSegment(0);
await focusCmd();
await typeLine("copy");
const copyPrompt = await prompt();
await typeLine("0,0");
logs.push(`[DBG] copy after base: segs=${(await segs()).length}`);
await typeLine("r2,0");
logs.push(`[DBG] copy after r2,0: segs=${(await segs()).length}`);
await page.keyboard.press("Enter"); // Copy-Serie beenden
await sleep(150);
const afterCopy = await segs();
logs.push(`[DBG] copy after Enter: segs=${afterCopy.length}`);
await page.screenshot({ path: "scripts/probe-edit-copy.png" });
// ── 4) Offset einer Polylinie um 0.5 ────────────────────────────────────────
// Frische Polylinie: ein offener L-Zug (3,-3)→(7,-3)→(7,-1). Tief unten (y<0),
// damit sie sich nicht mit den obigen Linien überlagert.
await focusCmd();
await typeLine("polyline");
await typeLine("3,-3");
await typeLine("7,-3");
await typeLine("7,-1");
await page.keyboard.press("Enter"); // offenen Zug beenden
await sleep(150);
const afterPoly = await segs();
logs.push(`[DBG] afterPoly segs=${JSON.stringify(afterPoly.map((s) => ({ a: { x: +s.a.x.toFixed(2), y: +s.a.y.toFixed(2) }, b: { x: +s.b.x.toFixed(2), y: +s.b.y.toFixed(2) } })))}`);
// Eines der Polylinien-Segmente selektieren (das letzte gezeichnete Segment).
// Wir suchen ein Segment mit y≈-3 (die untere waagerechte Kante).
const polyIdx = await page.evaluate((PX) => {
const lines = [...document.querySelectorAll(".plan-svg line.draw2d")];
for (let i = 0; i < lines.length; i++) {
const ln = lines[i];
const y1 = -parseFloat(ln.getAttribute("y1")) / PX;
const y2 = -parseFloat(ln.getAttribute("y2")) / PX;
if (Math.abs(y1 + 3) < 0.05 && Math.abs(y2 + 3) < 0.05) return i;
}
return -1;
}, PX);
await clickSegment(polyIdx);
const offsetSelGrips = await page.evaluate(
() => document.querySelectorAll(".plan-svg .plan-grip").length,
);
await focusCmd();
await typeLine("offset");
const offsetPrompt = await prompt();
await typeLine("0.5"); // Distanz tippen (Seite via Vorzeichen/Default links)
await sleep(150);
const afterOffset = await segs();
await page.screenshot({ path: "scripts/probe-edit-offset.png" });
// ── Auswertung ───────────────────────────────────────────────────────────────
function summarize(list) {
return list.map((s) => ({
a: { x: +s.a.x.toFixed(3), y: +s.a.y.toFixed(3) },
b: { x: +s.b.x.toFixed(3), y: +s.b.y.toFixed(3) },
}));
}
// Move-Check: nach Move soll genau eine Linie existieren, beide Endpunkte y≈2.
const moveLine = afterMove.find(
(s) => near(s.a.y, 2) && near(s.b.y, 2) && (near(s.a.x, 0) || near(s.b.x, 0)),
);
const moveOk =
afterLine.length === afterMove.length && // gleiche Anzahl (kein neues Element)
!!moveLine &&
near(Math.abs(moveLine.b.x - moveLine.a.x), 4); // Länge bleibt 4 m
// Copy-Check: nach Copy zwei waagerechte Linien bei y≈2; eine bei x∈[0,4], eine
// um 2 m nach rechts (x∈[2,6]).
const horizAtY2 = afterCopy.filter((s) => near(s.a.y, 2) && near(s.b.y, 2));
const xsMin = horizAtY2.map((s) => Math.min(s.a.x, s.b.x)).sort((p, q) => p - q);
const copyOk =
afterCopy.length === afterMove.length + 1 && // genau ein Segment dazu
horizAtY2.length === 2 &&
near(xsMin[0], 0) &&
near(xsMin[1], 2);
// Offset-Check: das Offset fügt eine versetzte Polylinie hinzu. Wir prüfen, dass
// eine NEUE untere Kante bei y≈-3±0.5 existiert (parallel, Abstand 0.5). Die
// Seite (oben/unten) ergibt sich aus der Default-Klickseite; wir akzeptieren
// beide Vorzeichen, prüfen aber den Abstand = 0.5.
const addedByOffset = afterOffset.length - afterPoly.length;
// Untere Original-Kante: y=-3, von x=3..7. Versetzte Kante: y=-3±0.5.
const bottomEdges = afterOffset.filter(
(s) => near(s.a.y, s.b.y) && Math.abs(s.a.x - s.b.x) > 1.0, // waagerechtes Segment
);
const ysBottom = [...new Set(bottomEdges.map((s) => +s.a.y.toFixed(3)))].sort(
(p, q) => p - q,
);
// Erwartung: y=-3 (Original) + eine parallele Kante 0.5 entfernt.
const hasParallel = ysBottom.some(
(y) => near(Math.abs(y - -3), 0.5) && !near(y, -3),
);
const offsetOk = addedByOffset >= 2 && hasParallel;
console.log(
JSON.stringify(
{
prompts: { movePrompt, moveTargetPrompt, copyPrompt, offsetPrompt },
counts: {
afterLine: afterLine.length,
afterMove: afterMove.length,
afterCopy: afterCopy.length,
afterPoly: afterPoly.length,
afterOffset: afterOffset.length,
addedByOffset,
},
grips: { moveSelInfo, offsetSelGrips },
moveLine: moveLine ? summarize([moveLine])[0] : null,
horizAtY2: summarize(horizAtY2),
ysBottom,
checks: { moveOk, copyOk, offsetOk },
verdict: moveOk && copyOk && offsetOk ? "PASS" : "FAIL",
},
null,
2,
),
);
if (logs.length) {
console.log("=== Logs ===");
console.log(logs.join("\n"));
}
await browser.close();
+69
View File
@@ -0,0 +1,69 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
const clickTool = (label) => page.evaluate((l) => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === l)?.click(), label);
const click = async (x, y, opts) => { await page.mouse.move(x, y); await page.mouse.click(x, y, opts); await new Promise((r) => setTimeout(r, 60)); };
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, cy = box.y + box.h / 2;
// ── 1) Aktive Ebene auf "60 Plangrafik" (#c0a040) setzen, Linie zeichnen ──
await page.evaluate(() => {
const sel = [...document.querySelectorAll("select")].find((s) => [...s.options].some((o) => o.value === "60"));
if (sel) sel.id = "ebene-test";
});
await page.select("#ebene-test", "60");
await new Promise((r) => setTimeout(r, 100));
await clickTool("Linie");
await new Promise((r) => setTimeout(r, 80));
await click(cx - 150, cy - 120); await click(cx + 120, cy - 60);
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 80));
const lineColor = await page.evaluate(() => document.querySelector(".plan-svg line.draw2d")?.getAttribute("stroke"));
// ── 2) Parallel verschieben: Linie selektieren, Körper greifen + ziehen ──
const mid = [(cx - 150 + cx + 120) / 2, (cy - 120 + cy - 60) / 2];
await click(mid[0], mid[1]); // selektieren
const before = await page.evaluate(() => { const l = document.querySelector(".plan-svg line.draw2d"); return { x1: +l.getAttribute("x1"), y1: +l.getAttribute("y1") }; });
await page.mouse.move(mid[0], mid[1]); await page.mouse.down();
await page.mouse.move(mid[0] + 60, mid[1] + 120, { steps: 6 }); await page.mouse.up();
await new Promise((r) => setTimeout(r, 100));
const after = await page.evaluate(() => { const l = document.querySelector(".plan-svg line.draw2d"); return { x1: +l.getAttribute("x1"), y1: +l.getAttribute("y1") }; });
const moved = Math.hypot(after.x1 - before.x1, after.y1 - before.y1) > 5;
// ── 3) Shift-Ortho: Endpunkt-Griff mit Shift ziehen → achsparallel ──
// Neue Linie zeichnen.
await clickTool("Linie");
await new Promise((r) => setTimeout(r, 60));
const a = [cx - 180, cy + 90], b = [cx - 40, cy + 140];
await click(...a); await click(...b);
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 60));
await click((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); // selektieren
// Endpunkt b mit Shift senkrecht-ish wegziehen → sollte exakt H oder V werden.
await page.keyboard.down("Shift");
await page.mouse.move(b[0], b[1]); await page.mouse.down();
await page.mouse.move(b[0] + 140, b[1] + 20, { steps: 8 }); // fast horizontal
await page.mouse.up();
await page.keyboard.up("Shift");
await new Promise((r) => setTimeout(r, 100));
// Die gerade bearbeitete Linie ist die ZWEITE draw2d-Linie.
const orthoLine = await page.evaluate(() => {
const ls = document.querySelectorAll(".plan-svg line.draw2d");
const l = ls[ls.length - 1];
return { x1: +l.getAttribute("x1"), y1: +l.getAttribute("y1"), x2: +l.getAttribute("x2"), y2: +l.getAttribute("y2") };
});
const dx = Math.abs(orthoLine.x2 - orthoLine.x1), dy = Math.abs(orthoLine.y2 - orthoLine.y1);
const isOrtho = dx < 1 || dy < 1; // exakt H oder V
await page.screenshot({ path: "scripts/probe-edit2.png" });
console.log("lineColor:", lineColor, "(expect #c0a040)");
console.log("parallelMoved:", moved);
console.log("shiftOrtho dx,dy:", dx.toFixed(2), dy.toFixed(2), "isOrtho:", isOrtho);
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+14
View File
@@ -0,0 +1,14 @@
import { firefox } from "playwright";
const browser = await firefox.launch({ firefoxUserPrefs: { "webgl.disabled": true } });
const page = await browser.newPage();
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto("http://localhost:5173/", { waitUntil: "networkidle", timeout: 20000 }); }
catch (e) { logs.push(`[GOTO-ERROR] ${e.message}`); }
await page.waitForTimeout(1500);
const root = await page.evaluate(() => { const r = document.getElementById("root"); return r ? r.innerHTML.length : -1; });
console.log("=== mit WebGL DEAKTIVIERT ===");
console.log("#root innerHTML Länge:", root, root === 0 ? "→ KOMPLETT WEISS (reproduziert!)" : "");
console.log(logs.join("\n"));
await browser.close();
+21
View File
@@ -0,0 +1,21 @@
import { firefox } from "playwright";
const browser = await firefox.launch({ firefoxUserPrefs: { "webgl.disabled": true } });
const page = await browser.newPage({ viewport: { width: 1200, height: 760 } });
const logs = [];
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto("http://localhost:5173/", { waitUntil: "networkidle", timeout: 20000 });
await page.waitForTimeout(1200);
const info = await page.evaluate(() => ({
rootLen: document.getElementById("root")?.innerHTML.length ?? -1,
hasFallback: !!document.querySelector(".viewport-fallback"),
hasPlanSvg: !!document.querySelector(".plan-svg polygon"),
header: document.querySelector(".brand")?.textContent?.trim().slice(0, 20),
}));
await page.screenshot({ path: "scripts/probe-ff-verify.png" });
console.log("=== Firefox OHNE WebGL — nach Fix ===");
console.log("root innerHTML Länge:", info.rootLen, info.rootLen > 0 ? "✅ nicht weiß" : "❌ weiß");
console.log("Fallback-Meldung sichtbar:", info.hasFallback ? "✅" : "❌");
console.log("Grundriss-SVG gerendert:", info.hasPlanSvg ? "✅" : "❌");
console.log("Header:", JSON.stringify(info.header));
console.log("PageErrors:", logs.length ? logs.join("\n") : "✅ keine");
await browser.close();
+31
View File
@@ -0,0 +1,31 @@
import { firefox } from "playwright";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const browser = await firefox.launch();
const page = await browser.newPage({ viewport: { 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}\n${e.stack || ""}`));
page.on("requestfailed", (r) =>
logs.push(`[REQFAIL] ${r.url()} :: ${r.failure()?.errorText}`),
);
try {
await page.goto(URL, { waitUntil: "networkidle", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
await page.waitForTimeout(1500);
const rootHtml = await page.evaluate(() => {
const r = document.getElementById("root");
return r ? r.innerHTML.slice(0, 300) : "NO #root";
});
await page.screenshot({ path: "scripts/probe-ff.png" });
console.log("=== FIREFOX CONSOLE / ERRORS ===");
console.log(logs.length ? logs.join("\n") : "(keine Logs)");
console.log("\n=== #root innerHTML (erste 300 Zeichen) ===");
console.log(rootHtml);
await browser.close();
+65
View File
@@ -0,0 +1,65 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
const clickTool = (label) =>
page.evaluate((l) => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === l)?.click(), label);
const click = async (x, y, opts) => { await page.mouse.move(x, y); await page.mouse.click(x, y, opts); await new Promise((r) => setTimeout(r, 80)); };
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, cy = box.y + box.h / 2;
// 1) Eine Wand zeichnen (A→B), Rechtsklick beendet.
await clickTool("Wand");
await new Promise((r) => setTimeout(r, 120));
const A = [cx - 160, cy - 150], B = [cx + 160, cy - 150];
await click(...A); await click(...B);
await page.mouse.click(B[0], B[1], { button: "right" });
await new Promise((r) => setTimeout(r, 150));
// 2) Auswahl-Werkzeug, Wand in der Mitte anklicken → Griffe erscheinen.
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 120));
await click(cx, cy - 150);
const gripsAfterSelect = await page.evaluate(() => document.querySelectorAll(".plan-svg .plan-grip").length);
await page.screenshot({ path: "scripts/probe-grips-selected.png" });
// 3) Den End-Griff bei B nach unten ziehen.
await page.mouse.move(B[0], B[1]);
await page.mouse.down();
await page.mouse.move(B[0], B[1] + 90, { steps: 6 });
await page.mouse.move(B[0] - 30, B[1] + 130, { steps: 6 });
await page.mouse.up();
await new Promise((r) => setTimeout(r, 150));
await page.screenshot({ path: "scripts/probe-grips-dragged.png" });
// 4) Eine Linie zeichnen, auswählen, Endpunkt-Griff prüfen.
await clickTool("Linie");
await new Promise((r) => setTimeout(r, 100));
const L1 = [cx - 180, cy + 120], L2 = [cx + 150, cy + 160];
await click(...L1); await click(...L2);
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 100));
// Mittig auf die Linie klicken (Mittelpunkt der beiden Endpunkte).
await click((L1[0] + L2[0]) / 2, (L1[1] + L2[1]) / 2);
const gripsLine = await page.evaluate(() => document.querySelectorAll(".plan-svg .plan-grip").length);
await page.screenshot({ path: "scripts/probe-grips-line.png" });
// 5) Linie löschen (Entf).
await page.keyboard.press("Delete");
await new Promise((r) => setTimeout(r, 120));
const draw2dAfterDelete = await page.evaluate(() => document.querySelectorAll(".plan-svg line.draw2d").length);
console.log("gripsAfterSelect:", gripsAfterSelect);
console.log("gripsLine:", gripsLine);
console.log("draw2dAfterDelete:", draw2dAfterDelete);
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+132
View File
@@ -0,0 +1,132 @@
// Probe: DXF-Import-Dialog. Erzeugt eine kleine Test-DXF (LWPOLYLINE auf 2
// Layern), füttert sie in das versteckte Datei-Input, screenshotet den offenen
// Dialog, wählt „Neue Zeichnung" + Importieren und screenshotet den Grundriss.
import puppeteer from "puppeteer";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const URL = process.env.PROBE_URL || "http://localhost:5174/";
// ── Minimale Test-DXF: 2 LWPOLYLINE auf Layern „Waende" und „Moeblierung". ──
const dxf = `0
SECTION
2
ENTITIES
0
LWPOLYLINE
8
Waende
90
5
70
1
10
0
20
0
10
8
20
0
10
8
20
5
10
0
20
5
10
0
20
0
0
LWPOLYLINE
8
Moeblierung
90
4
70
0
10
2
20
1
10
4
20
1
10
4
20
3
10
2
20
3
0
ENDSEC
0
EOF
`;
const dxfPath = join(tmpdir(), "probe-import.dxf");
writeFileSync(dxfPath, dxf);
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: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
// Datei in das versteckte DXF-Input legen → onChange öffnet den Dialog.
const input = await page.$('input[type="file"][accept*=".dxf"]');
if (!input) throw new Error("DXF-Datei-Input nicht gefunden");
await input.uploadFile(dxfPath);
await page.waitForSelector(".imp-dialog", { timeout: 5000 });
await new Promise((r) => setTimeout(r, 300));
await page.screenshot({ path: "scripts/probe-import-dialog.png" });
console.log("Dialog-Screenshot: scripts/probe-import-dialog.png");
// Zusammenfassung auslesen (Konturen/Layer sichtbar?).
const summary = await page.$$eval(".imp-summary li", (lis) =>
lis.map((l) => l.textContent),
);
console.log("Zusammenfassung:", JSON.stringify(summary));
// Sicherstellen, dass „Neue Zeichnung" gewählt ist (Default), dann Importieren.
const importBtn = await page.$(".imp-btn.primary");
const disabled = await page.evaluate((b) => b.disabled, importBtn);
console.log("Import-Button disabled?", disabled);
await importBtn.click();
await new Promise((r) => setTimeout(r, 600));
// Dialog sollte geschlossen sein; Grundriss/Drawing-Ansicht der neuen Ebene.
const dialogGone = (await page.$(".imp-dialog")) === null;
console.log("Dialog geschlossen?", dialogGone);
// Anzahl Zeichnungsebenen-Zeilen + neuer „Zeichnung"-Eintrag prüfen (best effort).
const drawingCount = await page.evaluate(() => {
const project = window.__store?.getState?.()?.project;
if (!project) return null;
return {
drawingLevels: project.drawingLevels.map((z) => `${z.name}/${z.kind}`),
drawings2d: project.drawings2d.length,
};
});
console.log("Projekt:", JSON.stringify(drawingCount));
await page.screenshot({ path: "scripts/probe-import-plan.png" });
console.log("Plan-Screenshot: scripts/probe-import-plan.png");
console.log("\n=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+20
View File
@@ -0,0 +1,20 @@
import puppeteer from "puppeteer";
const b = await puppeteer.launch({ headless:"new", args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"] });
const p = await b.newPage(); await p.setViewport({width:1300,height:850,deviceScaleFactor:2});
const logs=[]; p.on("pageerror",e=>logs.push(e.message));
await p.goto("http://localhost:5174/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
// Ressourcen öffnen.
await p.evaluate(()=>[...document.querySelectorAll("button")].find(x=>x.textContent.trim().toLowerCase().includes("ressourc"))?.click());
await new Promise(r=>setTimeout(r,300));
// Tab "Linien".
await p.evaluate(()=>[...document.querySelectorAll(".res-tab-btn")].find(x=>/Linien|Lines/.test(x.textContent))?.click());
await new Promise(r=>setTimeout(r,200));
const before = await p.evaluate(()=>document.querySelectorAll(".res-line-preview").length);
const input = await p.$('.res-tab input[type=file]');
await input.uploadFile("/tmp/sample.lin");
await new Promise(r=>setTimeout(r,400));
const after = await p.evaluate(()=>document.querySelectorAll(".res-line-preview").length);
await p.screenshot({ path:"scripts/probe-import.png" });
console.log("lineStyles before:", before, "after:", after, "(expect +3)", logs.length?logs.join("|"):"(no err)");
await b.close();
+12
View File
@@ -0,0 +1,12 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5173/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
const clickText=async t=>{const h=await p.evaluateHandle(t=>[...document.querySelectorAll("button")].find(b=>b.textContent.trim()===t),t);const el=h.asElement();if(el){await el.click();return true;}return false;};
await clickText("Perspektive"); await new Promise(r=>setTimeout(r,500));
await clickText("Isometrie"); await new Promise(r=>setTimeout(r,800));
await p.screenshot({path:"scripts/probe-iso.png"});
console.log("iso done, errs:", errs.join("|")||"none");
await b.close();
+30
View File
@@ -0,0 +1,30 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const b = await puppeteer.launch({ headless:"new", args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"] });
const p = await b.newPage();
await p.setViewport({ width:1200, height:800, deviceScaleFactor:2 });
await p.emulateMediaFeatures([{ name:"prefers-color-scheme", value:"dark" }]);
const logs=[]; p.on("pageerror",e=>logs.push(e.message));
await p.goto(URL,{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,600));
// Auf den "Ebenen"-Tab wechseln (TabStrip braucht Pointer-Events).
await p.evaluate(()=>{ const t=[...document.querySelectorAll(".tab")].find(e=>e.textContent.trim()==="Ebenen"); ["pointerdown","pointerup","click"].forEach(ev=>t.dispatchEvent(new MouseEvent(ev,{bubbles:true}))); });
await new Promise(r=>setTimeout(r,250));
const initialActive = await p.evaluate(()=>document.querySelector(".cat-row.active .cat-name")?.textContent ?? null);
// "Türen/Fenster" anklicken.
await p.evaluate(()=>{ const r=[...document.querySelectorAll(".cat-name")].find(e=>e.textContent.includes("Türen"))?.closest(".cat-row"); r?.click(); });
await new Promise(r=>setTimeout(r,250));
const afterActive = await p.evaluate(()=>document.querySelector(".cat-row.active .cat-name")?.textContent ?? null);
const statusLayer = await p.evaluate(()=>{ const f=[...document.querySelectorAll(".sb-field")].find(x=>x.textContent.trim().startsWith("EBENE")||x.textContent.includes("Ebene")); return f?.querySelector(".sb-val")?.textContent; });
// Jetzt eine Linie zeichnen → sollte auf Kategorie 21 (blau #5080c8) landen.
await p.evaluate(()=>[...document.querySelectorAll("button")].find(x=>x.textContent.trim()==="Linie")?.click());
await new Promise(r=>setTimeout(r,80));
const box = await p.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, cy=box.y+box.h/2;
for(const [x,y] of [[cx-120,cy-30],[cx+120,cy+30]]){ await p.mouse.move(x,y); await p.mouse.click(x,y); await new Promise(r=>setTimeout(r,60)); }
await p.evaluate(()=>[...document.querySelectorAll("button")].find(x=>x.textContent.trim()==="Auswahl")?.click());
await new Promise(r=>setTimeout(r,80));
const lineColor = await p.evaluate(()=>document.querySelector(".plan-svg line.draw2d")?.getAttribute("stroke"));
await p.screenshot({ path:"scripts/probe-layer.png" });
console.log("initialActive:", initialActive, "afterActive:", afterActive, "statusLayer:", statusLayer, "lineColor(expect #5080c8):", lineColor, logs.length?logs.join("|"):"(no err)");
await b.close();
+28
View File
@@ -0,0 +1,28 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
await page.evaluate(() => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === "Linie")?.click());
await new Promise((r) => setTimeout(r, 150));
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, cy = box.y + box.h / 2;
// Zwei-Klick-Strecke quer durch den Raum.
for (const [x, y] of [[cx - 200, cy - 40], [cx + 200, cy + 90]]) {
await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.up();
await new Promise((r) => setTimeout(r, 100));
}
await page.evaluate(() => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === "Auswahl")?.click());
await new Promise((r) => setTimeout(r, 200));
const draw2d = await page.evaluate(() => document.querySelectorAll(".plan-svg line.draw2d").length);
await page.screenshot({ path: "scripts/probe-line.png" });
console.log("draw2d lines:", draw2d);
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+30
View File
@@ -0,0 +1,30 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5173/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
const focusCmd=async()=>{ const el=await p.$(".cmdline-input"); const bx=await el.boundingBox(); await p.mouse.click(bx.x+bx.width/2, bx.y+bx.height/2); await new Promise(r=>setTimeout(r,60)); };
const typeLine=async t=>{ await p.type(".cmdline-input",t,{delay:8}); await p.keyboard.press("Enter"); await new Promise(r=>setTimeout(r,90)); };
// 1) Linie zeichnen (1,4)->(4,4)
await focusCmd(); await typeLine("line"); await typeLine("1,2.5"); await typeLine("r3,0");
// 2) gerenderte draw2d-Linie finden, Mittelpunkt klicken (selektieren)
const mid=await p.evaluate(()=>{
const l=[...document.querySelectorAll(".plan-svg line.draw2d")][0]; if(!l) return null;
const svg=l.ownerSVGElement, ctm=svg.getScreenCTM();
const pt=(x,y)=>{const q=svg.createSVGPoint();q.x=x;q.y=y;const s=q.matrixTransform(ctm);return[s.x,s.y];};
const a=pt(+l.getAttribute("x1"),+l.getAttribute("y1")), b=pt(+l.getAttribute("x2"),+l.getAttribute("y2"));
return {x:(a[0]+b[0])/2, y:(a[1]+b[1])/2};
});
if(mid){ await p.mouse.click(mid.x, mid.y); await new Promise(r=>setTimeout(r,150)); }
const selKind=await p.evaluate(()=>document.querySelector(".objinfo-kind")?.textContent||"none");
// 3) offset 0.5
await focusCmd(); await typeLine("offset"); await typeLine("0.5");
// 4) Linien (Modellkoordinaten) auslesen
const segs=await p.evaluate(()=>[...document.querySelectorAll(".plan-svg line.draw2d")].map(l=>{
// toScreen: x*90, -y*90 → invert: model = (vbx/90, -vby/90)
return {x1:+l.getAttribute("x1")/90, y1:-l.getAttribute("y1")/90, x2:+l.getAttribute("x2")/90, y2:-l.getAttribute("y2")/90};
}));
await p.screenshot({path:"scripts/probe-offset2.png"});
console.log(JSON.stringify({selKind, segCount:segs.length, segs, errs},null,2));
await b.close();
+58
View File
@@ -0,0 +1,58 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
const clickTool = (label) =>
page.evaluate((l) => [...document.querySelectorAll("button")].find((x) => x.textContent.trim() === l)?.click(), label);
const svgBox = () => page.evaluate(() => { const r = document.querySelector(".plan-svg").getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; });
const clickAt = async (x, y) => { await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.up(); await new Promise((r) => setTimeout(r, 70)); };
const box = await svgBox();
const cx = box.x + box.w / 2, cy = box.y + box.h / 2;
// 1) Rechteck (zwei Ecken).
await clickTool("Rechteck");
await new Promise((r) => setTimeout(r, 120));
await clickAt(cx - 210, cy - 110);
await clickAt(cx - 40, cy + 40);
// 2) Polylinie (offen, 4 Punkte, Rechtsklick beendet).
await clickTool("Polylinie");
await new Promise((r) => setTimeout(r, 120));
const poly = [[cx + 40, cy - 100], [cx + 200, cy - 100], [cx + 200, cy + 30], [cx + 90, cy + 60]];
for (const [x, y] of poly) await clickAt(x, y);
await page.mouse.move(cx + 90, cy + 60);
await page.mouse.click(cx + 90, cy + 60, { button: "right" });
await new Promise((r) => setTimeout(r, 150));
// 3) Snap-Menü öffnen (Fang) und screenshoten.
await page.evaluate(() => {
const b = [...document.querySelectorAll("button")].find((x) => x.textContent.trim().startsWith("Fang"));
if (b) b.click();
});
await new Promise((r) => setTimeout(r, 150));
const popover = await page.evaluate(() => !!document.querySelector(".snap-popover"));
await page.screenshot({ path: "scripts/probe-phase2-snapmenu.png" });
// Menü schließen.
await page.keyboard.press("Escape");
await new Promise((r) => setTimeout(r, 100));
await clickTool("Auswahl");
await new Promise((r) => setTimeout(r, 150));
const counts = await page.evaluate(() => ({
draw2dLines: document.querySelectorAll(".plan-svg line.draw2d").length,
}));
await page.screenshot({ path: "scripts/probe-phase2.png" });
console.log("popoverOpened:", popover);
console.log("counts:", JSON.stringify(counts));
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+42
View File
@@ -0,0 +1,42 @@
import puppeteer from "puppeteer";
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: 1400, height: 900, deviceScaleFactor: 2 });
const errs = [];
page.on("pageerror", (e) => errs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto("http://localhost:5173/", { waitUntil: "networkidle0", timeout: 20000 }); }
catch { /* networkidle timeout ok */ }
await new Promise((r) => setTimeout(r, 1200));
// 1) Plan zoom: wheel-in over the plan center, then screenshot
const svg = await page.$(".plan-svg");
if (svg) {
const box = await svg.boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.wheel({ deltaY: -600 });
await new Promise((r) => setTimeout(r, 300));
}
await page.screenshot({ path: "scripts/probe-zoom.png" });
// 2) Open Ressourcen manager, screenshot the table
const opened = await page.evaluate(() => {
const el = [...document.querySelectorAll("button, .res-trigger, [role=button]")]
.find((b) => /Ressourcen/i.test(b.textContent || ""));
if (el) { el.click(); return true; }
return false;
});
await new Promise((r) => setTimeout(r, 600));
await page.screenshot({ path: "scripts/probe-rm.png" });
const info = await page.evaluate(() => ({
managerOpen: !!document.querySelector(".res-drawer, .res-table, [class*=res-]"),
hasTable: !!document.querySelector(".res-table, table, .res-thead"),
triggerFound: [...document.querySelectorAll("button")].some((b)=>/Ressourcen/i.test(b.textContent||"")),
}));
console.log("opened trigger:", opened, JSON.stringify(info));
console.log("pageerrors:", errs.length ? errs.join("\n") : "none");
await browser.close();
+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();
+27
View File
@@ -0,0 +1,27 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
const focusCmd=async()=>{const el=await p.$(".cmdline-input");const bx=await el.boundingBox();await p.mouse.click(bx.x+bx.width/2,bx.y+bx.height/2);await new Promise(r=>setTimeout(r,60));};
const typeLine=async t=>{await p.type(".cmdline-input",t,{delay:6});await p.keyboard.press("Enter");await new Promise(r=>setTimeout(r,80));};
const lineCount=()=>p.$$eval(".plan-svg line.draw2d",ls=>ls.length).catch(()=>-1);
// zwei Linien zeichnen
await focusCmd(); await typeLine("line"); await typeLine("1,2.2"); await typeLine("r3,0");
await focusCmd(); await typeLine("line"); await typeLine("1,2.8"); await typeLine("r3,0");
const drawn=await lineCount();
// Screen-bbox der draw2d-Linien
const bb=await p.evaluate(()=>{const ls=[...document.querySelectorAll(".plan-svg line.draw2d")];const svg=ls[0].ownerSVGElement,ctm=svg.getScreenCTM();const pt=(x,y)=>{const q=svg.createSVGPoint();q.x=x;q.y=y;const s=q.matrixTransform(ctm);return[s.x,s.y];};let xs=[],ys=[];for(const l of ls){const a=pt(+l.getAttribute("x1"),+l.getAttribute("y1")),c=pt(+l.getAttribute("x2"),+l.getAttribute("y2"));xs.push(a[0],c[0]);ys.push(a[1],c[1]);}return{minX:Math.min(...xs),maxX:Math.max(...xs),minY:Math.min(...ys),maxY:Math.max(...ys)};});
// Marquee links→rechts (window) um beide Linien
await p.mouse.move(bb.minX-20, bb.minY-20); await p.mouse.down();
await p.mouse.move(bb.maxX+20, bb.maxY+20, {steps:6}); await new Promise(r=>setTimeout(r,60));
await p.mouse.up(); await new Promise(r=>setTimeout(r,200));
const selHi=await p.$$eval(".plan-svg .plan-sel-draw",e=>e.length).catch(()=>0);
const statusSel=await p.evaluate(()=>document.querySelector("footer,.statusbar")?.textContent?.match(/Auswahl[^|]*/)?.[0]||"");
// Löschen
await p.keyboard.press("Delete"); await new Promise(r=>setTimeout(r,200));
const afterDel=await lineCount();
await p.screenshot({path:"scripts/probe-sel.png"});
console.log(JSON.stringify({drawnLines:drawn, selHighlightPrims:selHi, statusSel, linesAfterDelete:afterDel, errs},null,2));
await b.close();
+300
View File
@@ -0,0 +1,300 @@
// Probe für Shell-Phase: native-App-Verhalten, Marquee-Auswahl, i18n.
// Headless, dpr 2, swiftshader. Schreibt PNGs nach scripts/ und meldet je
// Test (a)-(e) ein Ergebnis als JSON auf stdout (letzte Zeile).
import puppeteer from "puppeteer";
const TARGET = "http://localhost:5173/";
const OUT = new URL("./", import.meta.url).pathname;
const results = {};
const log = (k, v) => {
results[k] = v;
console.log(`[${k}]`, JSON.stringify(v));
};
const browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--use-gl=swiftshader",
"--enable-unsafe-swiftshader",
"--disable-dev-shm-usage",
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 2 });
page.on("pageerror", (e) => console.log("[pageerror]", e.message));
page.on("console", (m) => {
const t = m.text();
if (m.type() === "error") console.log("[console.error]", t);
});
try {
await page.goto(TARGET, { waitUntil: "networkidle0", timeout: 8000 });
} catch {
// networkidle-Timeout ignorieren (Dev-Server hält Verbindungen offen).
}
await page.waitForSelector(".plan-svg", { timeout: 15000 });
await new Promise((r) => setTimeout(r, 800));
// ── (a) Deutsch: Sichtbarer Text unverändert ────────────────────────────
const ger = await page.evaluate(() => {
const body = document.body.innerText;
return {
grundriss: body.includes("Grundriss"),
perspektive: body.includes("Perspektive"),
auswahl: body.includes("Auswahl"),
lang: window.__i18n?.getLanguage?.() ?? null,
};
});
await page.screenshot({ path: OUT + "probe-shell-a-de.png" });
log("a_german", {
...ger,
ok: ger.grundriss && ger.perspektive && ger.lang === "de",
});
// ── (b) Native-App: contextmenu unterdrückt, user-select ────────────────
const native = await page.evaluate(() => {
const ev = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
});
const prevented = !document.body.dispatchEvent(ev);
const bodySel = getComputedStyle(document.body).userSelect ||
getComputedStyle(document.body).webkitUserSelect;
return { defaultPrevented: prevented, bodyUserSelect: bodySel };
});
log("b_native_contextmenu", native);
// Ressourcen-Fenster öffnen, dann ein <input> auf user-select:text prüfen.
// Die Ressourcen-Aktion in der Topbar anklicken.
const openedResources = await page.evaluate(() => {
const btns = [...document.querySelectorAll("button, [role=button], .nav-tab, .topbar *")];
const hit = btns.find((b) => /Ressourcen/i.test(b.textContent || ""));
if (hit) { hit.click(); return true; }
return false;
});
await new Promise((r) => setTimeout(r, 500));
// Falls noch kein input sichtbar, einen Manager-Tab öffnen (Bauteile etc.).
const inputSel = await page.evaluate(() => {
const inp = document.querySelector("input");
if (!inp) return { found: false };
const cs = getComputedStyle(inp);
return {
found: true,
userSelect: cs.userSelect || cs.webkitUserSelect,
};
});
await page.screenshot({ path: OUT + "probe-shell-b-resources.png" });
log("b_input_userselect", {
openedResources,
...inputSel,
ok:
native.defaultPrevented === true &&
native.bodyUserSelect === "none" &&
inputSel.userSelect === "text",
});
// Ressourcen-Fenster wieder schließen, damit der Plan frei liegt (ESC).
await page.keyboard.press("Escape");
await new Promise((r) => setTimeout(r, 300));
// ── (c) Marquee: Links-Drag über den Raum wählt mehrere Wände ───────────
const planBox = await page.evaluate(() => {
const svg = document.querySelector(".plan-svg");
const r = svg.getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});
// Crossing-Marquee (rechts→rechts unten nach links oben): ein großes Rechteck,
// das den ganzen Raum kreuzt und damit ALLE vier Wände berührt. Rechts→links
// = crossing window (berührend), robust gegen exakte Wandlage.
const cx = planBox.x + planBox.w / 2;
const cy = planBox.y + planBox.h / 2;
const x0 = cx + planBox.w * 0.42;
const y0 = cy + planBox.h * 0.4;
const x1 = cx - planBox.w * 0.42;
const y1 = cy - planBox.h * 0.4;
await page.mouse.move(x0, y0);
await page.mouse.down({ button: "left" });
// Mehrere Zwischenschritte, damit pointermove + Schwelle greifen.
for (let i = 1; i <= 8; i++) {
await page.mouse.move(
x0 + ((x1 - x0) * i) / 8,
y0 + ((y1 - y0) * i) / 8,
);
await new Promise((r) => setTimeout(r, 20));
}
await page.mouse.up({ button: "left" });
await new Promise((r) => setTimeout(r, 400));
const marquee = await page.evaluate(() => {
const highlights = document.querySelectorAll(".plan-selected").length;
const footer = document.body.innerText;
const m = footer.match(/Auswahl:\s*(\d+)\s*(Wand|Wände)/);
return {
highlightPolys: highlights,
footerMatch: m ? m[0] : null,
footerN: m ? Number(m[1]) : 0,
};
});
await page.screenshot({ path: OUT + "probe-shell-c-marquee.png" });
log("c_marquee", {
...marquee,
ok: marquee.footerN >= 2 && marquee.highlightPolys >= 1,
});
// Einzel-Klick wählt genau eine Wand. Eine echte Wandfläche im DOM finden
// (gefülltes <polygon> innerhalb der SVG, das nicht Hintergrund/Auswahl ist),
// dann auf ihren Schwerpunkt in Client-Koordinaten klicken — deterministisch.
const wallPoint = await page.evaluate(() => {
const svg = document.querySelector(".plan-svg");
const polys = [...svg.querySelectorAll("polygon")].filter(
(p) =>
!p.classList.contains("plan-selected") &&
!p.classList.contains("plan-bg"),
);
const ctm = svg.getScreenCTM();
let best = null;
let bestArea = 0;
for (const poly of polys) {
const pts = poly.getAttribute("points");
if (!pts) continue;
const nums = pts.trim().split(/[\s,]+/).map(Number);
let cxv = 0,
cyv = 0,
n = 0;
let area = 0;
for (let i = 0; i + 1 < nums.length; i += 2) {
cxv += nums[i];
cyv += nums[i + 1];
n++;
}
if (n < 3) continue;
// grobe Bounding-Box-Fläche als Größenmaß
const xs = nums.filter((_, i) => i % 2 === 0);
const ys = nums.filter((_, i) => i % 2 === 1);
area =
(Math.max(...xs) - Math.min(...xs)) *
(Math.max(...ys) - Math.min(...ys));
if (area > bestArea) {
bestArea = area;
// SVG-User-Space → Client über CTM transformieren.
const ptObj = svg.createSVGPoint();
ptObj.x = cxv / n;
ptObj.y = cyv / n;
const sp = ptObj.matrixTransform(ctm);
best = { x: sp.x, y: sp.y };
}
}
return best;
});
if (wallPoint) {
await page.mouse.click(wallPoint.x, wallPoint.y, { button: "left" });
await new Promise((r) => setTimeout(r, 350));
}
const single = await page.evaluate(() => {
const footer = document.body.innerText;
const m = footer.match(/Auswahl:\s*(\d+)\s*(Wand|Wände)/);
return {
footerMatch: m ? m[0] : null,
footerN: m ? Number(m[1]) : 0,
highlightPolys: document.querySelectorAll(".plan-selected").length,
};
});
await page.screenshot({ path: OUT + "probe-shell-c2-single.png" });
log("c_single_click", { ...single, ok: single.footerN === 1 });
// ── (d) Rechtsklick auf den Plan öffnet UNSER ContextMenu (.ctx-menu) ────
// Erst sicherstellen, dass eine Wand gewählt ist (Einzelklick oben hat das
// getan). Rechtsklick auf den Plan.
await page.evaluate(
({ x, y }) => {
const svg = document.querySelector(".plan-svg");
const ev = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 2,
});
svg.dispatchEvent(ev);
},
{ x: cx, y: planBox.y + planBox.h * 0.22 },
);
await new Promise((r) => setTimeout(r, 300));
const planCtx = await page.evaluate(() => ({
ctxMenu: document.querySelectorAll(".ctx-menu").length,
}));
await page.screenshot({ path: OUT + "probe-shell-d-plan-context.png" });
log("d_plan_context", { ...planCtx, ok: planCtx.ctxMenu >= 1 });
// Menü schliessen.
await page.mouse.click(planBox.x + 4, planBox.y + 4, { button: "left" });
await new Promise((r) => setTimeout(r, 200));
// Rechtsklick auf eine Ebenen-Zeile (linke Liste) öffnet ebenfalls .ctx-menu.
const layerCtx = await page.evaluate(() => {
const row =
document.querySelector(".nav-list .cat-name")?.closest("div") ||
document.querySelector(".nav-list > *");
if (!row) return { found: false, ctxMenu: 0 };
const r = row.getBoundingClientRect();
const ev = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
clientX: r.x + 10,
clientY: r.y + r.height / 2,
button: 2,
});
row.dispatchEvent(ev);
return { found: true };
});
await new Promise((r) => setTimeout(r, 300));
const layerCtxCount = await page.evaluate(() => ({
ctxMenu: document.querySelectorAll(".ctx-menu").length,
}));
await page.screenshot({ path: OUT + "probe-shell-d2-layer-context.png" });
log("d_layer_context", {
...layerCtx,
...layerCtxCount,
ok: layerCtx.found && layerCtxCount.ctxMenu >= 1,
});
await page.mouse.click(planBox.x + 4, planBox.y + 4, { button: "left" });
await new Promise((r) => setTimeout(r, 200));
// ── (e) i18n auf Englisch schalten + Screenshot ─────────────────────────
const enFlip = await page.evaluate(() => {
if (!window.__i18n?.setLanguage) return { exposed: false };
window.__i18n.setLanguage("en");
return { exposed: true, lang: window.__i18n.getLanguage() };
});
await new Promise((r) => setTimeout(r, 400));
const enText = await page.evaluate(() => {
const body = document.body.innerText;
return {
plan: body.includes("Plan"),
perspective: body.includes("Perspective"),
stillGerman: body.includes("Grundriss"),
};
});
await page.screenshot({ path: OUT + "probe-shell-e-en.png" });
log("e_english", {
...enFlip,
...enText,
ok:
enFlip.exposed === true &&
enFlip.lang === "en" &&
enText.perspective &&
!enText.stillGerman,
});
// Zurück auf Deutsch.
await page.evaluate(() => window.__i18n?.setLanguage("de"));
const allOk = Object.values(results).every((r) => r.ok !== false);
console.log("\nRESULT " + JSON.stringify({ allOk, results }));
} finally {
await browser.close();
}
+23
View File
@@ -0,0 +1,23 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1280,height:820,deviceScaleFactor:1});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,800));
const focus=async()=>{const el=await p.$(".cmdline-input");const bx=await el.boundingBox();await p.mouse.click(bx.x+bx.width/2,bx.y+bx.height/2);await new Promise(r=>setTimeout(r,50));};
const cmd=async t=>{await p.type(".cmdline-input",t,{delay:5});await p.keyboard.press("Enter");await new Promise(r=>setTimeout(r,70));};
await focus();await cmd("line");await cmd("1,2.2");await cmd("4,2.2");
await focus();await cmd("line");await cmd("1,2.8");await cmd("4,2.8");
// Bildschirmkoordinaten der zwei Linien
const mids=await p.evaluate(()=>{const ls=[...document.querySelectorAll(".plan-svg line.draw2d")];const svg=ls[0].ownerSVGElement,ctm=svg.getScreenCTM();const pt=(x,y)=>{const q=svg.createSVGPoint();q.x=x;q.y=y;const s=q.matrixTransform(ctm);return[s.x,s.y];};return ls.map(l=>{const a=pt(+l.getAttribute("x1"),+l.getAttribute("y1")),c=pt(+l.getAttribute("x2"),+l.getAttribute("y2"));return[(a[0]+c[0])/2,(a[1]+c[1])/2];});});
const selCount=()=>p.$$eval(".plan-svg .plan-sel-draw",e=>e.length).catch(()=>0);
// Linie A klicken
await p.mouse.click(mids[0][0],mids[0][1]); await new Promise(r=>setTimeout(r,150));
const after1=await selCount();
// Linie B shift-klicken
await p.keyboard.down("Shift"); await p.mouse.click(mids[1][0],mids[1][1]); await p.keyboard.up("Shift");
await new Promise(r=>setTimeout(r,150));
const after2=await selCount();
await p.screenshot({path:"scripts/probe-shiftsel.png"});
console.log(JSON.stringify({afterClickA:after1, afterShiftClickB:after2, errs:errs.slice(0,2)}));
await b.close();
+166
View File
@@ -0,0 +1,166 @@
// Verifikation Split / Join / Segment-Löschen (Ctrl+S / Ctrl+J / Alt+Klick).
// Liest das Modell über den TEMP-Hook window.__drawings2d (id + geom).
import puppeteer from "puppeteer";
const b = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const p = await b.newPage();
await p.setViewport({ width: 1280, height: 820, deviceScaleFactor: 1 });
const errs = [];
p.on("pageerror", (e) => errs.push(e.message));
await p.goto("http://localhost:5187/", { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {});
await new Promise((r) => setTimeout(r, 800));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const focusCmd = async () => {
const el = await p.$(".cmdline-input");
const bx = await el.boundingBox();
await p.mouse.click(bx.x + bx.width / 2, bx.y + bx.height / 2);
await sleep(60);
};
const typeLine = async (t) => {
await p.type(".cmdline-input", t, { delay: 5 });
await p.keyboard.press("Enter");
await sleep(80);
};
// Laufenden Befehl beenden (sonst übernimmt er die linke Maustaste der Plan-
// Ansicht und Klicks selektieren nicht). Escape im Eingabefeld bricht ab.
const endCommand = async () => {
// Eingabefeld behält Fokus nach typeLine → Escape bricht den Befehl ab.
await p.keyboard.press("Escape");
await sleep(80);
await p.keyboard.press("Escape");
await sleep(80);
};
const drawings = () => p.evaluate(() => window.__drawings2d || []);
// Modellpunkt (Meter) → Client-Pixel über das aktuelle SVG-CTM (umgekehrt).
const modelToClient = (mx, my) =>
p.evaluate(
(mx, my) => {
const svg = document.querySelector(".plan-svg");
// toScreen aus PlanView: x*40, -y*40 (PX_PER_M=40, Y gespiegelt). Wir nutzen
// direkt das CTM: viewBox-Punkt (x*40, -y*40) → Bildschirm.
const ctm = svg.getScreenCTM();
const q = svg.createSVGPoint();
q.x = mx * 90;
q.y = -my * 90;
const s = q.matrixTransform(ctm);
return [s.x, s.y];
},
mx,
my,
);
const ctrlPress = async (key) => {
await p.keyboard.down("Control");
await p.keyboard.press(key);
await p.keyboard.up("Control");
await sleep(250);
};
const isClosed = (g) =>
g.shape === "rect" || (g.shape === "polyline" && g.closed);
// ── 1) SPLIT RECHTECK ────────────────────────────────────────────────────────
// Rechteck (0,0)-(4,4) + quer kreuzende vertikale Linie x=2.
await focusCmd();
await typeLine("rect");
await typeLine("0,0");
await typeLine("4,4");
await focusCmd();
await typeLine("line");
await typeLine("2,-1");
await typeLine("2,5");
await endCommand();
await sleep(150);
const before = await drawings();
// Rechteck wählen: auf eine Rechteck-Kante klicken (untere Kante y=0, x=1).
let [cx, cy] = await modelToClient(1, 0);
await p.mouse.click(cx, cy);
await sleep(150);
await ctrlPress("s");
await sleep(200);
const afterSplit = await drawings();
// Erwartung: Rechteck (1 closed) ersetzt durch 2 closed Elemente; Linie bleibt.
const closedAfter = afterSplit.filter((d) => isClosed(d.geom));
await p.screenshot({ path: "scripts/probe-splitjoin-1-split-rect.png" });
// ── 2) JOIN zwei Linien ──────────────────────────────────────────────────────
await p.reload({ waitUntil: "networkidle0" });
await sleep(800);
await focusCmd();
await typeLine("line");
await typeLine("-3,-3");
await typeLine("0,-3");
await focusCmd();
await typeLine("line");
await typeLine("0,-3");
await typeLine("0,0");
await endCommand();
await sleep(150);
const beforeJoin = await drawings();
// Beide wählen: erste klicken, dann Shift+zweite.
let [ax, ay] = await modelToClient(-1.5, -3);
await p.mouse.click(ax, ay);
await sleep(100);
let [bx2, by2] = await modelToClient(0, -1.5);
await p.keyboard.down("Shift");
await p.mouse.click(bx2, by2);
await p.keyboard.up("Shift");
await sleep(150);
await ctrlPress("j");
await sleep(200);
const afterJoin = await drawings();
const polylines = afterJoin.filter((d) => d.geom.shape === "polyline");
await p.screenshot({ path: "scripts/probe-splitjoin-2-join.png" });
// ── 3) ALT-SEGMENT (Schere) ──────────────────────────────────────────────────
await p.reload({ waitUntil: "networkidle0" });
await sleep(800);
await focusCmd();
await typeLine("rect");
await typeLine("0,0");
await typeLine("4,4");
await endCommand();
await sleep(150);
const beforeCut = await drawings();
// Alt+Klick auf die untere Kante (y=0, x=2).
let [ex, ey] = await modelToClient(2, 0);
await p.keyboard.down("Alt");
await p.mouse.click(ex, ey);
await p.keyboard.up("Alt");
await sleep(200);
const afterCut = await drawings();
const cutGeom = afterCut[0]?.geom;
await p.screenshot({ path: "scripts/probe-splitjoin-3-alt-cut.png" });
console.log(
JSON.stringify(
{
split: {
beforeCount: before.length,
afterCount: afterSplit.length,
closedCountAfter: closedAfter.length,
geomsAfter: afterSplit.map((d) => ({ shape: d.geom.shape, closed: isClosed(d.geom) })),
},
join: {
beforeCount: beforeJoin.length,
afterCount: afterJoin.length,
polylineCount: polylines.length,
joinedPts: polylines.map((d) => d.geom.pts?.length),
},
altCut: {
beforeShape: beforeCut[0]?.geom.shape,
afterCount: afterCut.length,
afterShape: cutGeom?.shape,
afterClosed: cutGeom ? isClosed(cutGeom) : null,
afterPts: cutGeom?.pts?.length,
},
errs,
},
null,
2,
),
);
await b.close();
+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();
+136
View File
@@ -0,0 +1,136 @@
// Probe: Kontext/Gelände-Integration (Phase 2, UI).
//
// 1) Erzeugt eine kleine Test-DXF (LWPOLYLINE-Höhenlinien Z=400/401/402).
// 2) Treibt den Import über das versteckte DXF-<input type=file> (uploadFile) —
// derselbe Pfad wie der Panel-Button „DXF importieren".
// 3) Screenshot Grundriss: Konturen als dezente Linien sichtbar.
// 4) Site-Panel-Tab aktivieren, „Gelände erzeugen" klicken.
// 5) In die Perspektive/Isometrie wechseln, orbiten, Screenshot: TIN im 3D.
import puppeteer from "puppeteer";
import { writeFileSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
// ── Test-DXF: drei geschlossene LWPOLYLINE-Höhenlinien (Elevation-Gruppe 38) ──
function ring(z, pts, layer) {
let s = `0\nLWPOLYLINE\n8\n${layer}\n38\n${z}\n90\n${pts.length}\n70\n1\n`;
for (const [x, y] of pts) s += `10\n${x}\n20\n${y}\n`;
return s;
}
// Etwas größere Konturen, damit sie im Plan gut sichtbar sind (Meter).
const dxf =
`0\nSECTION\n2\nENTITIES\n` +
ring(400, [[0, 0], [20, 0], [20, 20], [0, 20]], "contours") +
ring(401, [[4, 4], [16, 4], [16, 16], [4, 16]], "contours") +
ring(402, [[8, 8], [12, 8], [12, 12], [8, 12]], "contours") +
`0\nENDSEC\n0\nEOF\n`;
const dir = mkdtempSync(join(tmpdir(), "cad-dxf-"));
const dxfPath = join(dir, "contours.dxf");
writeFileSync(dxfPath, dxf);
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: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
await wait(600);
// Frisches Default-Layout erzwingen (neuer „site"-Tab), localStorage leeren.
await page.evaluate(() => {
try { localStorage.clear(); } catch { /* ignore */ }
});
await page.reload({ waitUntil: "networkidle0", timeout: 20000 });
await wait(600);
const clickByText = async (sel, re) => {
const el = (
await page.evaluateHandle(
(sel, reSrc) => {
const re = new RegExp(reSrc);
return [...document.querySelectorAll(sel)].find((e) => re.test(e.textContent.trim()));
},
sel,
re.source,
)
).asElement();
if (!el) throw new Error(`kein ${sel} matcht ${re}`);
await el.click();
return el;
};
// ── 2) Import über das versteckte DXF-Input ───────────────────────────────────
const input = await page.$('input[type=file][accept*=".dxf"]');
if (!input) throw new Error("DXF-Datei-Input nicht gefunden");
await input.uploadFile(dxfPath);
await wait(700);
// ── 3) Grundriss-Screenshot (Konturen sichtbar) ──────────────────────────────
// „Einpassen" über die Plan-Zoom-Buttons, damit die Konturen gerahmt werden.
await page.evaluate(() => {
const btn = [...document.querySelectorAll("button")].find((b) =>
/einpassen|fit||/i.test(b.textContent || b.title || ""),
);
btn?.click();
});
await wait(500);
await page.screenshot({ path: "scripts/probe-terrain-ui-a-plan.png" });
// ── 4) Site-Panel öffnen + „Gelände erzeugen" ────────────────────────────────
// Den „Gelände/Terrain"-Tab in der rechten Gruppe aktivieren (TabStrip nutzt
// .tab-Buttons; ein Klick aktiviert den Tab über den Drag-Controller).
await clickByText(".tab", /^(Gelände|Terrain)$/);
await wait(400);
await page.screenshot({ path: "scripts/probe-terrain-ui-b-panel.png" });
// „Gelände erzeugen"-Button im Site-Panel klicken.
const genClicked = await page.evaluate(() => {
const btn = [...document.querySelectorAll(".site-panel .site-btn")].find((b) =>
/Gelände erzeugen|Generate terrain/i.test(b.textContent),
);
if (btn) { btn.click(); return true; }
return false;
});
console.log("Gelände-erzeugen geklickt:", genClicked);
await wait(600);
// Kontext-Objekte zählen (Liste im Site-Panel).
const rows = await page.evaluate(
() => document.querySelectorAll(".site-panel .site-row").length,
);
const badges = await page.evaluate(() =>
[...document.querySelectorAll(".site-panel .site-badge")].map((b) => b.textContent.trim()),
);
console.log("Site-Listenzeilen:", rows, "Badges:", JSON.stringify(badges));
// ── 5) Isometrie → 3D-Screenshot (TIN sichtbar) ──────────────────────────────
// Der „Isometrie"-view-btn setzt view3d=iso UND wechselt zugleich in die
// Perspektive (echter Ansichtswechsel) — ein Klick genügt.
await clickByText("button.view-btn", /^Isometrie$|^Isometric$/);
await wait(1200);
const vp = await page.evaluate(() => {
const r = document.querySelector(".viewport")?.getBoundingClientRect();
return r ? { x: r.x, y: r.y, w: r.width, h: r.height } : null;
});
if (vp) {
const cx = vp.x + vp.w / 2, cy = vp.y + vp.h / 2;
await page.mouse.move(cx, cy);
await page.mouse.down({ button: "middle" });
await page.mouse.move(cx + 120, cy + 30, { steps: 12 });
await page.mouse.up({ button: "middle" });
await wait(500);
}
await page.screenshot({ path: "scripts/probe-terrain-ui-c-3d.png" });
console.log("logs:", logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+66
View File
@@ -0,0 +1,66 @@
// Probe: Gelände-TIN (Kontext-Schicht) im 3D sichtbar.
// Seedet über window.__seedTerrain() einen Höhenlinien-Satz + abgeleitetes TIN,
// wechselt in die Perspektive, orbitet ein wenig und screenshotet.
// Erwartung: eine plausible Geländefläche (TIN) im 3D.
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
// 1) Gelände seeden (echte siteSlice-Actions über window.__seedTerrain).
const seed = await page.evaluate(() => window.__seedTerrain?.());
console.log("seed:", JSON.stringify(seed));
await wait(400);
// 2) In die Perspektive wechseln (löst Szenen-Neuaufbau inkl. Kontext aus).
const clickByText = async (sel, text) => {
const handle = await page.evaluateHandle(
(sel, text) =>
[...document.querySelectorAll(sel)].find((el) => el.textContent.trim() === text),
sel,
text,
);
const el = handle.asElement();
if (!el) throw new Error(`Button "${text}" (${sel}) nicht gefunden`);
await el.click();
};
await clickByText("button.view-btn", "Perspektive");
await wait(1000);
// 3) Isometrie für einen guten Blick auf die Geländefläche.
await clickByText("button.view-btn", "Isometrie");
await wait(900);
// 4) Etwas orbiten (mittlere Maustaste über dem Viewport).
const vp = await page.evaluate(() => {
const r = document.querySelector(".viewport")?.getBoundingClientRect();
return r ? { x: r.x, y: r.y, w: r.width, h: r.height } : null;
});
if (vp) {
const cx = vp.x + vp.w / 2;
const cy = vp.y + vp.h / 2;
await page.mouse.move(cx, cy);
await page.mouse.down({ button: "middle" });
await page.mouse.move(cx + 120, cy + 40, { steps: 12 });
await page.mouse.up({ button: "middle" });
await wait(500);
}
await page.screenshot({ path: "scripts/probe-terrain.png" });
// Diagnostik: Anzahl Meshes in der Szene ist im Headless nicht direkt lesbar;
// wir berichten Seed-Resultat + Konsolenfehler.
console.log("logs:", logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+18
View File
@@ -0,0 +1,18 @@
import puppeteer from "puppeteer";
const b = await puppeteer.launch({ headless:"new", args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"] });
const p = await b.newPage(); await p.setViewport({width:1300,height:850,deviceScaleFactor:2});
const logs=[]; p.on("pageerror",e=>logs.push(e.message));
await p.goto("http://localhost:5174/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,800));
const info = await p.evaluate(()=>({
toolsPanel: !!document.querySelector(".tools-panel"),
toolRows: document.querySelectorAll(".tools-panel .tool-row").length,
activeTab: [...document.querySelectorAll(".tab")].find(t=>t.classList.contains("active"))?.textContent?.trim() ?? null,
}));
// Wand-Werkzeug im Panel klicken → wallType-Dropdown sollte erscheinen.
await p.evaluate(()=>{ const r=[...document.querySelectorAll(".tools-panel .tool-row")].find(x=>/Wand/.test(x.textContent)); r?.click(); });
await new Promise(r=>setTimeout(r,200));
const afterWall = await p.evaluate(()=>({ active: [...document.querySelectorAll(".tool-row.active")].map(x=>x.textContent.trim()), hasWallTypeSelect: !!document.querySelector(".tools-panel select") }));
await p.screenshot({ path:"scripts/probe-tools-panel.png" });
console.log("info:", JSON.stringify(info), "afterWall:", JSON.stringify(afterWall), logs.length?logs.join("|"):"(no err)");
await b.close();
+80
View File
@@ -0,0 +1,80 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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}`));
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
await new Promise((r) => setTimeout(r, 600));
// 1) Wand-Werkzeug aktivieren (Button per Text finden).
const clickedWall = await page.evaluate(() => {
const btns = [...document.querySelectorAll("button")];
const b = btns.find((x) => x.textContent.trim() === "Wand");
if (b) { b.click(); return true; }
return false;
});
await new Promise((r) => setTimeout(r, 200));
// 2) SVG-Box ermitteln und eine L-Polylinie zeichnen (3 Punkte + Rechtsklick).
const box = await page.evaluate(() => {
const svg = document.querySelector(".plan-svg");
const r = 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;
const pts = [
[cx - 160, cy - 120],
[cx - 160, cy + 60],
[cx + 80, cy + 60],
];
for (const [x, y] of pts) {
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.up();
await new Promise((r) => setTimeout(r, 80));
}
// Hover, damit das letzte lebende Segment + Snap-Marker zu sehen sind.
await page.mouse.move(cx + 80, cy - 60);
await new Promise((r) => setTimeout(r, 120));
await page.screenshot({ path: "scripts/probe-tools-drawing.png" });
// Rechtsklick = Polylinie abschließen (commit).
await page.mouse.click(cx + 80, cy - 60, { button: "right" });
await new Promise((r) => setTimeout(r, 200));
// 3) Zurück auf Auswahl und Ergebnis screenshoten.
await page.evaluate(() => {
const b = [...document.querySelectorAll("button")].find(
(x) => x.textContent.trim() === "Auswahl",
);
if (b) b.click();
});
await new Promise((r) => setTimeout(r, 200));
// Wand-/Drawing-Zählung aus dem DOM (Polygone mit Wandbezug sind schwer zu
// zählen; stattdessen Anzahl Pfade/Polygone als grober Indikator).
const counts = await page.evaluate(() => ({
polygons: document.querySelectorAll(".plan-svg polygon").length,
lines: document.querySelectorAll(".plan-svg line").length,
}));
await page.screenshot({ path: "scripts/probe-tools.png" });
console.log("clickedWall:", clickedWall);
console.log("counts:", JSON.stringify(counts));
console.log("=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+16
View File
@@ -0,0 +1,16 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1400,height:200,deviceScaleFactor:3});
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,700));
const tb=await p.$(".topbar"); const box=await tb.boundingBox();
await p.screenshot({path:"scripts/probe-topbar.png", clip:{x:0,y:0,width:1400,height:Math.ceil(box.height)+8}});
// zähle die Icon-Buttons + ihre computed Größe/Farbe
const info=await p.evaluate(()=>{
const icons=[...document.querySelectorAll(".view-icon")];
const svg=icons[0]?.querySelector("svg");
const cs=icons[0]?getComputedStyle(icons[0]):null;
return { count:icons.length, cellW:cs?.width, cellH:cs?.height, color:cs?.color, svgW:svg?.getAttribute("width"), gridRows: document.querySelector(".view-grid")?getComputedStyle(document.querySelector(".view-grid")).gridTemplateColumns:null };
});
console.log(JSON.stringify(info));
await b.close();
+62
View File
@@ -0,0 +1,62 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
page.on("dialog", async (d) => { await d.accept("3"); }); // Kopienanzahl-Prompt → 3
try { await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }); } catch (e) { logs.push(`[GOTO] ${e.message}`); }
await new Promise((r) => setTimeout(r, 600));
const clickTool = (l) => page.evaluate((x) => [...document.querySelectorAll("button")].find((b) => b.textContent.trim() === x)?.click(), l);
const click = async (x, y) => { await page.mouse.move(x, y); await page.mouse.click(x, y); await new Promise((r) => setTimeout(r, 60)); };
const nLines = () => page.evaluate(() => document.querySelectorAll(".plan-svg line.draw2d").length);
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, cy = box.y + box.h / 2;
// Eine Linie zeichnen + selektieren.
await clickTool("Linie"); await new Promise((r) => setTimeout(r, 80));
const A = [cx - 160, cy - 120], B = [cx - 40, cy - 120];
await click(...A); await click(...B);
await clickTool("Auswahl"); await new Promise((r) => setTimeout(r, 60));
await click((A[0] + B[0]) / 2, A[1]); // selektieren
const n0 = await nLines();
// ── Modusleiste: 'm' drücken → Bar erscheint (Screenshot) ──
await page.keyboard.press("m");
await new Promise((r) => setTimeout(r, 120));
const barVisible = await page.evaluate(() => !!document.querySelector(".transform-bar"));
await page.screenshot({ path: "scripts/probe-transform-bar.png" });
// ── Move (U, default): Basispunkt → Ziel ──
await click(A[0], A[1]); // Basispunkt
await click(A[0] + 80, A[1] + 140); // Ziel
await new Promise((r) => setTimeout(r, 120));
const nMove = await nLines();
const movedPos = await page.evaluate(() => { const l = document.querySelector(".plan-svg line.draw2d"); return +l.getAttribute("y1"); });
// ── Copy (I): m, i, base, dest → +1 ──
await click((A[0] + B[0]) / 2 + 80, A[1] + 140); // selektieren (verschobene Linie)
await page.keyboard.press("m");
await page.keyboard.press("i");
await new Promise((r) => setTimeout(r, 60));
await click(cx, cy); await click(cx + 100, cy + 40);
await new Promise((r) => setTimeout(r, 120));
const nCopy = await nLines();
// ── Array (O=3): select a line, m, o(→3), base, dest → +3 ──
await click(cx + 50, cy + 20); // selektieren (eine der Linien)
await page.keyboard.press("m");
await page.keyboard.press("o");
await new Promise((r) => setTimeout(r, 150));
await click(cx - 50, cy + 120); await click(cx - 20, cy + 150);
await new Promise((r) => setTimeout(r, 150));
const nArray = await nLines();
await page.screenshot({ path: "scripts/probe-transform.png" });
console.log("n0(initial):", n0, "barVisible:", barVisible);
console.log("nMove(expect ==n0):", nMove, "movedY:", movedPos);
console.log("nCopy(expect n0+1):", nCopy);
console.log("nArray(expect nCopy+3):", nArray);
console.log(logs.length ? logs.join("\n") : "(no errors)");
await browser.close();
+54
View File
@@ -0,0 +1,54 @@
import puppeteer from "puppeteer";
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: 1500, height: 950, deviceScaleFactor: 2 });
const errs = [];
page.on("pageerror", (e) => errs.push(`[PAGEERROR] ${e.message}`));
try { await page.goto("http://localhost:5173/", { waitUntil: "networkidle0", timeout: 20000 }); } catch {}
await new Promise((r) => setTimeout(r, 1200));
const footerScale = () => page.evaluate(() => {
const t = document.body.innerText;
const m = t.match(/MASSSTAB\s*(1:\d+)/i);
return m ? m[1] : "?";
});
const before = await footerScale();
// Massstab-Select auf 1:50 setzen
const changed = await page.evaluate(() => {
const selects = [...document.querySelectorAll("select")];
for (const s of selects) {
const opt = [...s.options].find((o) => /1:50\b/.test(o.textContent || o.value));
if (opt) {
s.value = opt.value;
s.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
}
return false;
});
await new Promise((r) => setTimeout(r, 500));
const after = await footerScale();
// Ressourcen öffnen
const resOpened = await page.evaluate(() => {
const b = [...document.querySelectorAll("button")].find((x) => /Ressourcen/i.test(x.textContent || ""));
if (b) { b.click(); return true; }
return false;
});
await new Promise((r) => setTimeout(r, 500));
const resInfo = await page.evaluate(() => {
const ov = document.querySelector(".res-drawer, .res-overlay, [class*=overlay], [class*=drawer]");
const z = ov ? getComputedStyle(ov).zIndex : null;
return { hasOverlay: !!ov, z };
});
await page.screenshot({ path: "scripts/probe-ressourcen-window.png" });
console.log("Massstab-Select gefunden:", changed);
console.log("Footer Massstab vorher:", before, "→ nachher:", after, after !== before ? "✅ ändert sich" : "⚠️ gleich");
console.log("Ressourcen geöffnet:", resOpened, "Overlay:", resInfo.hasOverlay, "z-index:", resInfo.z);
console.log("PageErrors:", errs.length ? errs.join("\n") : "keine");
await browser.close();
+84
View File
@@ -0,0 +1,84 @@
// Probe: belegt die Ansichts-3D-Presets (Front/Top/Seite/Persp./Iso) +
// Kamera-FOV. Wechselt in die Perspektive, klickt je Preset den Button und
// screenshotet. Die PNGs müssen sich deutlich unterscheiden.
// probe-view3d-perspective.png — 3/4-Blick
// probe-view3d-top.png — Draufsicht
// probe-view3d-front.png — frontal
// probe-view3d-side.png — seitlich
// probe-view3d-iso.png — isometrisch
// probe-view3d-fov.png — Kamera-Popover mit FOV-Regler (weit)
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
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: 1400, height: 900, deviceScaleFactor: 1 });
const logs = [];
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
const clickByText = async (sel, text) => {
const handle = await page.evaluateHandle(
(sel, text) =>
[...document.querySelectorAll(sel)].find(
(el) => el.textContent.trim() === text,
),
sel,
text,
);
const el = handle.asElement();
if (!el) throw new Error(`Button "${text}" (${sel}) nicht gefunden`);
await el.click();
};
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const shot = (name) => page.screenshot({ path: `scripts/${name}` });
// In die Perspektive wechseln.
await clickByText("button.view-btn", "Perspektive");
await wait(1000);
// Default-Preset (perspective) — Referenz.
await shot("probe-view3d-perspective.png");
// Presets durchschalten (per sichtbarem Button-Text in der 3D-Gruppe).
for (const [label, file] of [
["Oben", "probe-view3d-top.png"],
["Front", "probe-view3d-front.png"],
["Seite", "probe-view3d-side.png"],
["Isometrie", "probe-view3d-iso.png"],
["Perspektive", "probe-view3d-perspective2.png"],
]) {
await clickByText("button.view-btn", label);
await wait(700);
await shot(file);
}
// Kamera-Popover öffnen + FOV weit aufdrehen (sichtbarer Effekt).
await clickByText("button.tb-btn", "Kamera");
await wait(300);
const range = await page.$(".tb-popover input[type=range]");
if (range) {
await page.evaluate((el) => {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
).set;
setter.call(el, "90");
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}, range);
await wait(500);
}
await shot("probe-view3d-fov.png");
console.log("Screenshots: probe-view3d-{perspective,top,front,side,iso,fov}.png");
console.log("\n=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+13
View File
@@ -0,0 +1,13 @@
import puppeteer from "puppeteer";
const b=await puppeteer.launch({headless:"new",args:["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader"]});
const p=await b.newPage(); await p.setViewport({width:1400,height:880,deviceScaleFactor:1.25});
const errs=[]; p.on("pageerror",e=>errs.push(e.message));
await p.goto("http://localhost:5187/",{waitUntil:"networkidle0",timeout:20000}).catch(()=>{});
await new Promise(r=>setTimeout(r,1200));
// auf eine Wandkante des Rechtecks klicken (linke Wand)
const hit=await p.evaluate(()=>{const polys=[...document.querySelectorAll(".plan-svg polygon, .plan-svg path")];if(!polys.length)return null;const svg=document.querySelector(".plan-svg");const r=svg.getBoundingClientRect();return{x:r.x+r.width*0.34,y:r.y+r.height*0.5};});
if(hit){await p.mouse.click(hit.x,hit.y);await new Promise(r=>setTimeout(r,400));}
const hasWallSec=await p.evaluate(()=>!!document.querySelector(".objinfo-panel")&&/Aufbau|Referenz|UK|OK/.test(document.querySelector(".objinfo-panel")?.textContent||""));
await p.screenshot({path:"scripts/probe-wall-sel.png"});
console.log("wallSection:",hasWallSec,"errs:",errs.slice(0,2));
await b.close();
+186
View File
@@ -0,0 +1,186 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5187/";
const b = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const p = await b.newPage();
await p.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1 });
const errs = [];
p.on("pageerror", (e) => errs.push(e.message));
p.on("console", (m) => { if (m.type() === "error") errs.push("[console] " + m.text()); });
await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {});
await new Promise((r) => setTimeout(r, 800));
const clickTab = (label) =>
p.evaluate((x) => {
const el = [...document.querySelectorAll("button, .tab, [role=tab]")].find(
(b) => b.textContent.trim() === x,
);
el?.click();
return !!el;
}, label);
// Object-Info-Tab aktivieren (falls vorhanden).
const tabOk = await clickTab("Objekt-Info");
await new Promise((r) => setTimeout(r, 200));
// Eine Wand im Grundriss anklicken (obere Bandkante eines Wandpolygons).
async function selectWall() {
const bb = await p.evaluate(() => {
const polys = [...document.querySelectorAll(".plan-svg polygon")];
if (!polys.length) return null;
let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
for (const pl of polys) {
const r = pl.getBoundingClientRect();
minX = Math.min(minX, r.left); minY = Math.min(minY, r.top);
maxX = Math.max(maxX, r.right); maxY = Math.max(maxY, r.bottom);
}
return { minX, minY, maxX, maxY };
});
if (!bb) return false;
const cx = (bb.minX + bb.maxX) / 2;
for (const dy of [4, 8, 12, 16, 20]) {
await p.mouse.click(cx, bb.minY + dy);
await new Promise((r) => setTimeout(r, 120));
const k = await p.evaluate(
() => document.querySelector(".objinfo-kind")?.textContent || "",
);
if (/Wand/.test(k)) return true;
}
return false;
}
const selected = await selectWall();
// (1) Wand-Abschnitt sichtbar?
const hasWallSection = await p.evaluate(() =>
[...document.querySelectorAll(".objinfo-section-label")].some(
(e) => e.textContent.trim() === "Wand",
),
);
const hasRefDropdown = await p.evaluate(
() => !!document.querySelector(".objinfo-head-ref"),
);
await p.screenshot({ path: "scripts/probe-wallattr-1-panel.png" });
// Plan-Footprint VORHER (bbox aller Wandpolygone).
const planBBox = () =>
p.evaluate(() => {
const polys = [...document.querySelectorAll(".plan-svg polygon")];
let minY = 1e9, maxY = -1e9;
for (const pl of polys) {
const r = pl.getBoundingClientRect();
minY = Math.min(minY, r.top); maxY = Math.max(maxY, r.bottom);
}
return { minY: Math.round(minY), maxY: Math.round(maxY) };
});
const beforeRef = await planBBox();
// (2) Referenzlinie auf „Außen (links)" stellen über das Kopf-Dropdown.
async function pickDropdown(triggerSel, optionText) {
const ok = await p.evaluate((sel) => {
const t = document.querySelector(sel);
if (!t) return false;
t.click();
return true;
}, triggerSel);
if (!ok) return false;
await new Promise((r) => setTimeout(r, 150));
const picked = await p.evaluate((txt) => {
const item = [...document.querySelectorAll(".tb-dd-menu .tb-dd-item")].find(
(i) => i.textContent.trim().includes(txt),
);
item?.click();
return !!item;
}, optionText);
await new Promise((r) => setTimeout(r, 200));
return picked;
}
const refPicked = await pickDropdown(
".objinfo-head-ref .tb-dd-trigger",
"Außen",
);
await new Promise((r) => setTimeout(r, 250));
const afterRef = await planBBox();
await p.screenshot({ path: "scripts/probe-wallattr-2-refline-aussen.png" });
// (3) OK auf „eigene Höhe" → Z-Wert ändern, dann 3D.
// Den OK-Block (zweite objinfo-wall-anchor) auf „Eigene Höhe" schalten.
const okCustom = await p.evaluate(() => {
const blocks = [...document.querySelectorAll(".objinfo-wall-anchor")];
const okBlock = blocks[1];
if (!okBlock) return false;
const btn = [...okBlock.querySelectorAll(".objinfo-seg-btn")].find(
(b) => b.textContent.trim() === "Eigene Höhe",
);
btn?.click();
return !!btn;
});
await new Promise((r) => setTimeout(r, 200));
// Z-Feld setzen (5.0 m) im OK-Block.
const heightBefore = await p.evaluate(
() =>
[...document.querySelectorAll(".objinfo-field")]
.find((f) => f.querySelector(".objinfo-flabel")?.textContent === "Höhe")
?.querySelector(".objinfo-fval")?.textContent || "",
);
const setZ = await p.evaluate(() => {
const blocks = [...document.querySelectorAll(".objinfo-wall-anchor")];
const okBlock = blocks[1];
const input = okBlock?.querySelector("input.objinfo-input");
if (!input) return false;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
).set;
setter.call(input, "5");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
input.blur();
return true;
});
await new Promise((r) => setTimeout(r, 250));
const heightAfter = await p.evaluate(
() =>
[...document.querySelectorAll(".objinfo-field")]
.find((f) => f.querySelector(".objinfo-flabel")?.textContent === "Höhe")
?.querySelector(".objinfo-fval")?.textContent || "",
);
await p.screenshot({ path: "scripts/probe-wallattr-3-ok-custom.png" });
// 3D (Isometrie) umschalten und Screenshot — der Umschalter ist ein Icon-Button
// mit title „Isometrie …" in der Oberleiste (kein Tab).
await p.evaluate(() => {
const el = [...document.querySelectorAll("button[title]")].find((b) =>
(b.getAttribute("title") || "").startsWith("Isometrie"),
);
el?.click();
});
await new Promise((r) => setTimeout(r, 1000));
await p.screenshot({ path: "scripts/probe-wallattr-4-3d.png" });
console.log(
JSON.stringify(
{
tabOk,
selected,
hasWallSection,
hasRefDropdown,
refPicked,
beforeRef,
afterRef,
footprintShifted: beforeRef.minY !== afterRef.minY || beforeRef.maxY !== afterRef.maxY,
okCustom,
setZ,
heightBefore,
heightAfter,
errs,
},
null,
2,
),
);
await b.close();
+47
View File
@@ -0,0 +1,47 @@
import puppeteer from "puppeteer";
const URL = process.env.PROBE_URL || "http://localhost:5173/";
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
});
const page = await browser.newPage();
// HiDPI simulieren — das ist die Bedingung, die die Schleife auslöste.
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}`));
try {
await page.goto(URL, { waitUntil: "networkidle0", timeout: 20000 });
} catch (e) {
logs.push(`[GOTO-ERROR] ${e.message}`);
}
const measure = () =>
page.evaluate(() => {
const c = document.querySelector(".viewport canvas");
const v = document.querySelector(".viewport");
const r = c?.getBoundingClientRect();
return {
canvasCss: r ? `${Math.round(r.width)}x${Math.round(r.height)}` : "none",
canvasBuffer: c ? `${c.width}x${c.height}` : "none",
viewport: v ? `${Math.round(v.clientWidth)}x${Math.round(v.clientHeight)}` : "none",
bodyScrollH: document.body.scrollHeight,
};
});
const m1 = await measure();
await new Promise((r) => setTimeout(r, 1200));
const m2 = await measure();
await page.screenshot({ path: "scripts/probe.png" });
const stable = JSON.stringify(m1) === JSON.stringify(m2);
console.log("=== Messung 1 ===", JSON.stringify(m1));
console.log("=== Messung 2 ===", JSON.stringify(m2));
console.log(stable ? "✅ STABIL — keine Schleife" : "❌ INSTABIL — wächst/schrumpft noch");
console.log("\n=== Logs ===");
console.log(logs.length ? logs.join("\n") : "(keine)");
await browser.close();
+192
View File
@@ -0,0 +1,192 @@
// Numerische Verifikation des 2D-Kernels (esbuild → node). Kein UI nötig.
import {
segmentIntersect,
offsetPolyline,
offsetSegment,
trimSegment,
extendSegment,
filletCorner,
pointSegmentDistance,
lineCircleIntersect,
circleCircleIntersect,
signedArea,
isCCW,
joinChains,
removeSegment,
splitAtIntersections,
splitClosedByChord,
splitPolylineAtParam,
vecEqual,
} from "../src/geometry/kernel2d.ts";
let pass = 0;
let fail = 0;
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) <= eps;
function check(name: string, cond: boolean, detail?: unknown) {
if (cond) { pass++; console.log(" ok " + name); }
else { fail++; console.log(" FAIL " + name, JSON.stringify(detail)); }
}
// 1) Strecken-Schnitt: X durch (1,1)
{
const h = segmentIntersect({x:0,y:0},{x:2,y:2},{x:0,y:2},{x:2,y:0});
check("segmentIntersect cross at (1,1)", !!h && approx(h!.point.x,1) && approx(h!.point.y,1) && approx(h!.t,0.5), h);
}
// 2) parallele Strecken → null
{
const h = segmentIntersect({x:0,y:0},{x:2,y:0},{x:0,y:1},{x:2,y:1});
check("segmentIntersect parallel -> null", h === null);
}
// 3) kein Schnitt innerhalb der Segmente → null
{
const h = segmentIntersect({x:0,y:0},{x:1,y:0},{x:2,y:-1},{x:2,y:1});
check("segmentIntersect outside -> null", h === null);
}
// 4) Offset einer Strecke nach links (d>0): +y
{
const [a,b] = offsetSegment({x:0,y:0},{x:1,y:0},0.5);
check("offsetSegment left +0.5y", approx(a.y,0.5) && approx(b.y,0.5) && approx(a.x,0) && approx(b.x,1), {a,b});
}
// 5) Offset geschlossenes Quadrat: Konvention +leftNormal zeigt bei CCW NACH
// INNEN (CONVENTIONS.md), also d>0 = innen → (1,1)..(3,3); d<0 = außen → (-1,-1)..(5,5).
{
const sq = [{x:0,y:0},{x:4,y:0},{x:4,y:4},{x:0,y:4}]; // CCW
const inn = offsetPolyline(sq, 1, true);
const okIn = inn.length===4 &&
approx(inn[0].x,1)&&approx(inn[0].y,1)&&
approx(inn[1].x,3)&&approx(inn[1].y,1)&&
approx(inn[2].x,3)&&approx(inn[2].y,3)&&
approx(inn[3].x,1)&&approx(inn[3].y,3);
check("offsetPolyline square inward by 1 (CCW +n=inside)", okIn, inn);
const out = offsetPolyline(sq, -1, true);
const okOut = out.length===4 && approx(out[0].x,-1)&&approx(out[0].y,-1)&&approx(out[2].x,5)&&approx(out[2].y,5);
check("offsetPolyline square outward by 1 (d<0)", okOut, out);
}
// 6) Offset offene L-Polylinie (Gehrung am Innenknick)
{
const L = [{x:0,y:0},{x:4,y:0},{x:4,y:4}];
const off = offsetPolyline(L, 1, false); // links
// Endkanten verschoben; mittlerer Punkt = Gehrung bei (3,1) für links-offset
check("offsetPolyline open L mitre", off.length===3 && approx(off[1].x,3) && approx(off[1].y,1), off);
}
// 7) Trim: Strecke 0..4 von Cutter bei x=1 und x=3, Pick im Mittelstück → 2 Reststücke
{
const cutters = [
{pts:[{x:1,y:-1},{x:1,y:1}], closed:false},
{pts:[{x:3,y:-1},{x:3,y:1}], closed:false},
];
const rem = trimSegment({x:0,y:0},{x:4,y:0},cutters,{x:2,y:0});
const ok = rem.length===2 &&
approx(rem[0][0].x,0)&&approx(rem[0][1].x,1)&&
approx(rem[1][0].x,3)&&approx(rem[1][1].x,4);
check("trimSegment removes middle piece", ok, rem);
}
// 8) Extend: Strecke 0..1 auf x-Achse, Ende bis Cutter bei x=3
{
const cutters=[{pts:[{x:3,y:-1},{x:3,y:1}],closed:false}];
const ex = extendSegment({x:0,y:0},{x:1,y:0},"end",cutters);
check("extendSegment to x=3", !!ex && approx(ex![1].x,3) && approx(ex![1].y,0), ex);
}
// 9) Extend start-Ende rückwärts bis x=-2
{
const cutters=[{pts:[{x:-2,y:-1},{x:-2,y:1}],closed:false}];
const ex = extendSegment({x:0,y:0},{x:1,y:0},"start",cutters);
check("extendSegment start to x=-2", !!ex && approx(ex![0].x,-2), ex);
}
// 10) Fillet 90°-Ecke, r=1 → setback 1, center (1,1)
{
const f = filletCorner({x:0,y:0},{x:5,y:0},{x:0,y:5},1);
const ok = !!f && approx(f!.tangentA.x,1)&&approx(f!.tangentA.y,0)&&
approx(f!.tangentB.x,0)&&approx(f!.tangentB.y,1)&&
approx(f!.center.x,1)&&approx(f!.center.y,1)&&approx(f!.radius,1);
check("filletCorner 90deg r1", ok, f);
}
// 11) Fillet kollinear → null
{
const f = filletCorner({x:0,y:0},{x:1,y:0},{x:-1,y:0},1);
check("filletCorner collinear -> null", f === null);
}
// 12) pointSegmentDistance
{
check("pointSegmentDistance perpendicular", approx(pointSegmentDistance({x:1,y:2},{x:0,y:0},{x:4,y:0}),2));
}
// 13) Linie durch Kreis (Mittelpunkt-Sekante) → 2 Punkte ±r
{
const ps = lineCircleIntersect({x:-5,y:0},{x:5,y:0},{x:0,y:0},2);
const xs = ps.map(p=>p.x).sort((a,b)=>a-b);
check("lineCircleIntersect secant", ps.length===2 && approx(xs[0],-2) && approx(xs[1],2), ps);
}
// 14) Zwei Kreise (Abstand 2, r=√2 je) schneiden sich bei (1,±1)
{
const ps = circleCircleIntersect({x:0,y:0},Math.SQRT2,{x:2,y:0},Math.SQRT2);
const ok = ps.length===2 && ps.every(p=>approx(Math.abs(p.x),1)&&approx(Math.abs(p.y),1)) && approx(ps[0].x,1);
check("circleCircleIntersect at (1,±1)", ok, ps);
}
// 15) Wicklung
{
const ccw = [{x:0,y:0},{x:4,y:0},{x:4,y:4},{x:0,y:4}];
check("signedArea/isCCW", approx(signedArea(ccw),16) && isCCW(ccw) && !isCCW([...ccw].reverse()));
}
// ── Split / Join / Segment-Löschen ───────────────────────────────────────────
const V = (x: number, y: number) => ({ x, y });
// 16) splitPolylineAtParam: offene Linie → 2 Stücke an (5,0)
{
const parts = splitPolylineAtParam([V(0,0),V(10,0)], false, 0, 0.5);
check("splitPolyline open -> 2 pieces", parts.length===2 && approx(parts[0][1].x,5) && approx(parts[1][0].x,5), parts);
}
// 17) splitPolylineAtParam: geschlossenes Quadrat → eine offene Kette von/bis Schnitt
{
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
const parts = splitPolylineAtParam(sq, true, 0, 0.5);
const c = parts[0];
check("splitPolyline closed -> 1 open chain", parts.length===1 && vecEqual(c[0],V(2,0)) && vecEqual(c[c.length-1],V(2,0)), c);
}
// 18) splitClosedByChord: Quadrat unten/oben → 2 geschlossene Hälften
{
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
const res = splitClosedByChord(sq, 0, 0.5, 2, 0.5);
const has=(p:{x:number;y:number}[],q:{x:number;y:number})=>p.some(r=>vecEqual(r,q));
const ok = !!res && res[0].length>=3 && res[1].length>=3 &&
has(res![0],V(2,0)) && has(res![0],V(2,4)) && has(res![1],V(2,0)) && has(res![1],V(2,4));
check("splitClosedByChord -> 2 closed halves", ok, res);
check("splitClosedByChord same edge -> null", splitClosedByChord(sq,1,0.5,1,0.7)===null);
}
// 19) removeSegment: geschlossenes Quadrat → offen, Start nach Kante
{
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
const r = removeSegment(sq, true, 0);
check("removeSegment closed -> open chain", r.closed===false && r.pts.length===4 && vecEqual(r.pts[0],V(4,0)), r);
}
// 20) splitAtIntersections: Rechteck × querende Linie → 2 geschlossene Hälften
{
const rect=[V(0,0),V(4,0),V(4,4),V(0,4)];
const cutter=[V(2,-1),V(2,5)];
const pieces = splitAtIntersections(rect, true, [cutter]);
check("splitAtIntersections rect×line -> 2 closed", pieces.length===2 && pieces[0].length>=3 && pieces[1].length>=3, pieces);
}
// 21) splitAtIntersections: offene Linie × Cutter → 2 Stücke an (5,0)
{
const pieces = splitAtIntersections([V(0,0),V(10,0)], false, [[V(5,-2),V(5,2)]]);
check("splitAtIntersections open line -> 2 pieces", pieces.length===2 && approx(pieces[0][pieces[0].length-1].x,5), pieces);
}
// 22) joinChains: zwei Linien mit gemeinsamem Endpunkt → eine Polylinie
{
const out = joinChains([{pts:[V(0,0),V(5,0)],closed:false},{pts:[V(5,0),V(5,5)],closed:false}]);
check("joinChains 2 chains -> 1 polyline (3 pts)", out.length===1 && out[0].pts.length===3 && out[0].closed===false, out);
}
// 23) joinChains: vier Kanten schließen sich → eine geschlossene Schleife
{
const out = joinChains([
{pts:[V(0,0),V(4,0)],closed:false},
{pts:[V(4,0),V(4,4)],closed:false},
{pts:[V(4,4),V(0,4)],closed:false},
{pts:[V(0,4),V(0,0)],closed:false},
]);
check("joinChains loop -> closed ring (4 pts)", out.length===1 && out[0].closed===true && out[0].pts.length===4, out);
}
console.log(`\nKERNEL2D: ${pass} ok, ${fail} fail`);
if (fail > 0) process.exit(1);