Files
DOSSIER-STANDALONE/scripts/probe-edit-commands.mjs
karim ca859c4aa4 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.
2026-06-30 20:52:27 +02:00

249 lines
9.7 KiB
JavaScript

// 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();