Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem, dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en) sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
@@ -0,0 +1,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();
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user