Begehbare 3D-Visualisierung des Gartens Egglihaldenstrasse 9a
Geometrie massstaeblich aus dem Gartenplan-PDF (1:100) extrahiert: Belaege, Mauern, Pflanzenstandorte und 46 Hoehenkoten. Wohnhaus nach Gartenplan und den Fassadenplaenen von EHLE Architekten. - tools/: Pipeline PDF -> Szenenmodell (MuPDF, Node) - src/: three.js-Szene mit prozeduralen Texturen, Rundgang und Uebersicht - Docker/Traefik fuer garten.aicds.ch, Gitea-Workflow auf main Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Zeichnet die extrahierte Geometrie als SVG-Draufsicht - Kontrolle, ob die
|
||||
* Transformation aus dem PDF stimmt.
|
||||
*
|
||||
* Aufruf: node tools/debug-svg.mjs [nur-farbe-hex]
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = path.join(__dirname, 'out');
|
||||
const data = JSON.parse(fs.readFileSync(path.join(OUT, 'plan-raw.json'), 'utf8'));
|
||||
const only = process.argv[2]?.toLowerCase();
|
||||
|
||||
const PAD = 1.5;
|
||||
const PXM = 34; // Pixel pro Meter
|
||||
let x0 = Infinity, z0 = Infinity, x1 = -Infinity, z1 = -Infinity;
|
||||
for (const s of data.shapes) for (const [x, z] of s.pts) {
|
||||
x0 = Math.min(x0, x); x1 = Math.max(x1, x); z0 = Math.min(z0, z); z1 = Math.max(z1, z);
|
||||
}
|
||||
x0 -= PAD; z0 -= PAD; x1 += PAD; z1 += PAD;
|
||||
const W = Math.round((x1 - x0) * PXM), H = Math.round((z1 - z0) * PXM);
|
||||
const X = (x) => ((x - x0) * PXM).toFixed(1);
|
||||
const Z = (z) => ((z - z0) * PXM).toFixed(1);
|
||||
|
||||
const out = [];
|
||||
out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">`);
|
||||
out.push(`<rect width="${W}" height="${H}" fill="#ffffff"/>`);
|
||||
|
||||
const fills = data.shapes.filter((s) => s.op === 'f' && s.area > 0.05);
|
||||
fills.sort((a, b) => b.area - a.area);
|
||||
for (const s of fills) {
|
||||
if (only && s.hex.toLowerCase() !== only) continue;
|
||||
const d = s.pts.map((p, i) => `${i ? 'L' : 'M'}${X(p[0])},${Z(p[1])}`).join('') + 'Z';
|
||||
out.push(`<path d="${d}" fill="${s.hex}" fill-opacity="0.85" stroke="#0006" stroke-width="0.4"/>`);
|
||||
}
|
||||
if (!only) {
|
||||
for (const s of data.shapes) {
|
||||
if (s.op !== 's' || s.pts.length < 2) continue;
|
||||
const d = s.pts.map((p, i) => `${i ? 'L' : 'M'}${X(p[0])},${Z(p[1])}`).join('');
|
||||
out.push(`<path d="${d}" fill="none" stroke="#333" stroke-width="0.35" stroke-opacity="0.55"/>`);
|
||||
}
|
||||
for (const sp of data.spots) {
|
||||
out.push(`<circle cx="${X(sp.x)}" cy="${Z(sp.z)}" r="2.4" fill="#d00"/>`);
|
||||
out.push(`<text x="${X(sp.x) - 0}" y="${+Z(sp.z) - 4}" font-size="9" fill="#a00" text-anchor="middle">${sp.y.toFixed(2)}</text>`);
|
||||
}
|
||||
}
|
||||
// Massstab und Achsen
|
||||
out.push(`<g stroke="#06c" stroke-width="2" fill="none">
|
||||
<path d="M${X(0)},${Z(0)} L${X(5)},${Z(0)}"/><path d="M${X(0)},${Z(0)} L${X(0)},${Z(5)}"/></g>`);
|
||||
out.push(`<text x="${X(5.2)}" y="${Z(0)}" font-size="13" fill="#06c">+X (ENE)</text>`);
|
||||
out.push(`<text x="${X(0.2)}" y="${Z(5.4)}" font-size="13" fill="#06c">+Z (SSE)</text>`);
|
||||
out.push(`<text x="12" y="20" font-size="14" fill="#333">Gartenplan → Site-Frame · ${(x1 - x0).toFixed(1)} × ${(z1 - z0).toFixed(1)} m · 1 Raster = 1 m</text>`);
|
||||
out.push('</svg>');
|
||||
|
||||
const file = path.join(OUT, only ? `debug-${only.replace('#', '')}.svg` : 'debug-plan.svg');
|
||||
fs.writeFileSync(file, out.join('\n'));
|
||||
console.log('geschrieben:', file, `${W}x${H}px`);
|
||||
Reference in New Issue
Block a user