Files
karim 3d2d4d6321 2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung
(konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im
Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text-
Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback,
falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform)
fuer fluessige Interaktion ohne React-Re-Render je Frame.

Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM
(Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext-
Import, Tauri-Compute-Boundary-PoC).
2026-07-02 00:12:39 +02:00

193 lines
9.2 KiB
JavaScript

// Verifikation der drei Editier-/Eingabe-Verbesserungen:
// (a) Beim Zeichnen zeigt die Befehlszeile LIVE Länge/Winkel (Feld-Werte folgen
// der Maus); KEIN Wert-HUD mehr am Cursor (kein <text class="tool-hud">).
// (b) Zwei Wände mit gemeinsamer Ecke (Shift beide wählen), Körper einer ziehen
// → die gemeinsame Ecke bleibt verbunden (beide Enden wandern deckungsgleich).
// (c) Vertex-Griff-Drag + Tab → Wert tippen → exakter Zielpunkt (Befehlsfeld).
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.25 });
const errs = [];
p.on("pageerror", (e) => errs.push(e.message));
await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {});
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
await wait(900);
const clickTool = (label) =>
p.evaluate((l) => [...document.querySelectorAll("button.tool-row")]
.find((x) => x.textContent.trim() === l)?.click(), label);
const planBox = () => p.evaluate(() => {
const r = document.querySelector(".plan-svg").getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});
const click = async (x, y, opts) => { await p.mouse.move(x, y); await p.mouse.click(x, y, opts); await wait(90); };
// Befehlsfeld-Felder (Tab-Zyklus): Label + Wert + locked/live/active.
const cmdFields = () => p.evaluate(() =>
[...document.querySelectorAll(".cmdline-field")].map((f) => ({
label: f.querySelector(".cmdline-field-label")?.textContent?.trim() ?? "",
val: f.querySelector(".cmdline-field-val")?.textContent?.trim() ?? "",
active: f.classList.contains("active"),
locked: f.classList.contains("locked"),
live: f.classList.contains("live"),
})));
const hudCount = () => p.evaluate(() => document.querySelectorAll(".plan-svg text.tool-hud").length);
const grips = () => p.evaluate(() =>
[...document.querySelectorAll(".plan-svg rect.plan-grip")].map((r) => ({
x: Number(r.getAttribute("x")) + Number(r.getAttribute("width")) / 2,
y: Number(r.getAttribute("y")) + Number(r.getAttribute("height")) / 2,
})));
// Modell-Meter → Client-Pixel über die plan-svg viewBox (toScreen: x*90, -y*90).
const modelToClient = (m) => p.evaluate((mm) => {
const svg = document.querySelector(".plan-svg");
const r = svg.getBoundingClientRect();
const vb = svg.viewBox.baseVal;
// meet (xMidYMid): einheitliche Skala + Letterbox-Versatz.
const s = Math.min(r.width / vb.width, r.height / vb.height);
const offX = (r.width - vb.width * s) / 2;
const offY = (r.height - vb.height * s) / 2;
const sx = mm.x * 90, sy = -mm.y * 90; // toScreen
return { x: r.x + offX + (sx - vb.x) * s, y: r.y + offY + (sy - vb.y) * s };
}, m);
const wallEnds = () => p.evaluate(() => {
const proj = window.__store?.getState?.().project;
if (!proj) return null;
return proj.walls.map((w) => ({ id: w.id, start: w.start, end: w.end }));
});
const selectedWalls = () => p.evaluate(() => window.__store?.getState?.().selectedWallIds ?? []);
const box = await planBox();
const cx = box.x + box.w / 2, cy = box.y + box.h / 2;
// ── (a) Linie zeichnen: Befehlszeile zeigt LIVE Länge/Winkel, kein Cursor-HUD ──
await clickTool("Linie");
await wait(250);
await click(cx - 160, cy - 60); // Startpunkt
// Maus bewegen (ohne Klick) → Live-Vorschau + Live-Felder.
await p.mouse.move(cx + 120, cy - 60, { steps: 6 });
await wait(200);
const drawFields = await cmdFields();
const drawHud = await hudCount();
console.log("(a) draw fields:", JSON.stringify(drawFields));
console.log("(a) cursor HUD count (soll 0):", drawHud);
await p.screenshot({ path: "scripts/probe-edit-input-a-draw-live.png" });
// Linie verwerfen.
await p.keyboard.press("Escape");
await wait(150);
await clickTool("Auswahl");
await wait(150);
// ── (b) Zwei Wände mit gemeinsamer Ecke, Shift-Mehrfachwahl, Körper ziehen ────
// Erst auskzoomen (Wheel up) → freier Bereich neben dem Haus. Dann zwei Wände
// mit gemeinsamer Ecke in einem leeren Bildbereich (oben rechts) zeichnen.
await p.mouse.move(cx, cy);
for (let i = 0; i < 6; i++) { await p.mouse.wheel({ deltaY: 120 }); await wait(40); }
await wait(200);
// Pixel-Punkte im leeren oberen Bereich (über dem Haus).
const Ap = [cx - 120, box.y + 90], Cp = [cx + 40, box.y + 90], Dp = [cx + 40, box.y + 240];
const idsPre = (await wallEnds()).map((w) => w.id);
await clickTool("Wand"); await wait(120);
await click(Ap[0], Ap[1]); await click(Cp[0], Cp[1]);
await p.mouse.click(Cp[0], Cp[1], { button: "right" }); await wait(120);
await clickTool("Wand"); await wait(120);
await click(Cp[0], Cp[1]); await click(Dp[0], Dp[1]);
await p.mouse.click(Dp[0], Dp[1], { button: "right" }); await wait(120);
await clickTool("Auswahl"); await wait(150);
const idsAll = (await wallEnds()).map((w) => w.id);
const newIds = idsAll.filter((id) => !idsPre.includes(id));
console.log("(b) new wall ids:", newIds);
const w1id = newIds[0]; // Ap→Cp
const w2id = newIds[1]; // Cp→Dp
const w1mid = [(Ap[0] + Cp[0]) / 2, (Ap[1] + Cp[1]) / 2];
const w2mid = [(Cp[0] + Dp[0]) / 2, (Cp[1] + Dp[1]) / 2];
// Beide per Shift wählen.
await click(w1mid[0], w1mid[1]);
await p.keyboard.down("Shift");
await click(w2mid[0], w2mid[1]);
await p.keyboard.up("Shift");
await wait(150);
console.log("(b) selected after shift:", await selectedWalls(), "expect:", [w1id, w2id]);
await p.screenshot({ path: "scripts/probe-edit-input-b1-both-selected.png" });
const endsBefore = (await wallEnds()).filter((w) => w.id === w1id || w.id === w2id);
console.log("(b) two new walls before:", JSON.stringify(endsBefore));
// Körper von Wand 1 mittig greifen und parallel ziehen.
await p.mouse.move(w1mid[0], w1mid[1]);
await p.mouse.down();
await p.mouse.move(w1mid[0] + 40, w1mid[1] - 55, { steps: 10 });
await p.mouse.move(w1mid[0] + 60, w1mid[1] - 90, { steps: 10 });
await p.mouse.up();
await wait(250);
await p.screenshot({ path: "scripts/probe-edit-input-b2-moved.png" });
const endsAfter = (await wallEnds()).filter((w) => w.id === w1id || w.id === w2id);
console.log("(b) two new walls after:", JSON.stringify(endsAfter));
const near = (a, b) => Math.hypot(a.x - b.x, a.y - b.y) < 0.01;
let stillJoined = "n/a", moved = "n/a";
if (endsAfter.length === 2) {
const wA = endsAfter.find((w) => w.id === w1id), wB = endsAfter.find((w) => w.id === w2id);
let shared = false;
for (const a of [wA.start, wA.end]) for (const c of [wB.start, wB.end]) if (near(a, c)) shared = true;
stillJoined = shared ? "JOINED ✓" : "SPLIT ✗";
const b0 = endsBefore.find((w) => w.id === w1id);
moved = b0 && near(b0.start, wA.start) && near(b0.end, wA.end) ? "NOT MOVED ✗" : "MOVED ✓";
}
console.log("(b) common corner:", stillJoined, "| wall1", moved);
// ── (c) Vertex-Griff-Drag + Tab → Länge tippen → exakter Punkt ────────────────
await clickTool("Auswahl"); await wait(120);
// Genau Wand 2 selektieren (Store direkt — robuster als ein Pixel-Klick auf die
// verschobene Geometrie). Testet (c) isoliert: Grip-Drag + Tab + Wert.
await p.evaluate((id) => window.__store.getState().setSelectedWallIds([id]), w2id);
await p.evaluate(() => window.__store.getState().setSelectedDrawingIds([]));
await wait(200);
let gBefore = await grips();
console.log("(c) grips:", gBefore.length, "selected:", await selectedWalls());
const gripToClient = (g) => p.evaluate((gg) => {
const svg = document.querySelector(".plan-svg");
const r = svg.getBoundingClientRect();
const vb = svg.viewBox.baseVal;
const s = Math.min(r.width / vb.width, r.height / vb.height);
const offX = (r.width - vb.width * s) / 2, offY = (r.height - vb.height * s) / 2;
return { x: r.x + offX + (gg.x - vb.x) * s, y: r.y + offY + (gg.y - vb.y) * s };
}, g);
if (gBefore.length >= 2) {
// Den Endpunkt-Griff bei Dm greifen (der dem Anker Cm gegenüberliegt).
const dGrip = gBefore.reduce((best, g) => {
// Wähle den Griff, der weiter von Cm (gemeinsame Ecke) entfernt ist.
return best; // erster Versuch: nehme den ZWEITEN Griff (Index 1 = end).
}, gBefore[1]);
const gc = await gripToClient(gBefore[1]);
await p.mouse.move(gc.x, gc.y);
await p.mouse.down();
await p.mouse.move(gc.x + 35, gc.y - 15, { steps: 6 });
await wait(150);
await p.screenshot({ path: "scripts/probe-edit-input-c1-drag.png" });
// Tab öffnet das Editier-Feld + fokussiert das Befehlsfeld.
await p.keyboard.press("Tab");
await wait(200);
const editFields = await cmdFields();
console.log("(c) edit fields after Tab:", JSON.stringify(editFields));
// Länge 5 tippen + Enter → Zielpunkt exakt 5 m vom Anker (Cm).
await p.keyboard.type("5");
await p.keyboard.press("Enter");
await wait(150);
await p.mouse.up();
await wait(250);
await p.screenshot({ path: "scripts/probe-edit-input-c2-locked.png" });
const w2after = (await wallEnds()).find((w) => w.id === w2id);
if (w2after) {
const len = Math.hypot(w2after.end.x - w2after.start.x, w2after.end.y - w2after.start.y);
console.log("(c) wall2 after lock:", JSON.stringify(w2after), "len=", len.toFixed(3));
}
}
console.log("errs:", errs.slice(0, 6));
await b.close();