ca859c4aa4
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.
249 lines
9.7 KiB
JavaScript
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();
|