// Vermisst eine .glb aus three-d-stage (Quelle oder aufbereitete Datei) für die Platzierung: // // node .claude/skills/placing-editor-models/scripts/measure-model.mjs [bericht.json] // // Gibt eine kurze Übersicht aus und schreibt optional das volle JSON, das render-site.py für die // Grundriss-Überlagerung liest. Dreiecke werden je Knoten gezählt: three-d-stage benutzt Meshes // mehrfach, die Summe über die Meshes allein unterschätzt, was gezeichnet wird (Hood Yard: 0,75 statt // 2,9 Mio.). import { createHash } from "node:crypto"; import { open, stat, writeFile } from "node:fs/promises"; import { NodeIO } from "@gltf-transform/core"; import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; import { getBounds } from "@gltf-transform/functions"; import { MeshoptDecoder } from "meshoptimizer"; import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../../../../tools/assets/bumpExtension.mjs"; const [source, reportPath] = process.argv.slice(2); if (!source) { console.error("Aufruf: node measure-model.mjs [bericht.json]"); process.exit(1); } /** asset.generator aus dem JSON-Block der .glb; NodeIO überschreibt ihn beim Einlesen. */ async function readGenerator(path) { const file = await open(path, "r"); try { const header = Buffer.alloc(20); await file.read(header, 0, 20, 0); const json = Buffer.alloc(header.readUInt32LE(12)); await file.read(json, 0, json.length, 20); return JSON.parse(json.toString("utf8")).asset?.generator ?? "(unbekannt)"; } finally { await file.close(); } } await MeshoptDecoder.ready; const io = new NodeIO() .registerExtensions([...ALL_EXTENSIONS, EXTMaterialsBump]) .registerDependencies({ "meshopt.decoder": MeshoptDecoder }); const document = await io.read(source); const root = document.getRoot(); const round = (values) => values.map((value) => Number(value.toFixed(2))); const materials = new Map(); for (const node of root.listNodes()) { for (const primitive of node.getMesh()?.listPrimitives() ?? []) { const material = primitive.getMaterial(); const name = material?.getName() ?? "(ohne Material)"; const entry = materials.get(name) ?? { name, drawnTriangles: 0, primitives: 0, alphaMode: material?.getAlphaMode() ?? "OPAQUE", bump: Boolean(material?.getExtension(EXT_MATERIALS_BUMP)), }; entry.drawnTriangles += (primitive.getIndices()?.getCount() ?? primitive.getAttribute("POSITION").getCount()) / 3; entry.primitives++; materials.set(name, entry); } } const textureSizes = {}; const hashes = new Set(); let gpuBytes = 0; for (const texture of root.listTextures()) { const [width, height] = texture.getSize() ?? [0, 0]; const key = `${width}x${height} ${texture.getMimeType()}`; textureSizes[key] = (textureSizes[key] ?? 0) + 1; hashes.add(createHash("md5").update(texture.getImage() ?? new Uint8Array()).digest("hex")); gpuBytes += (width * height * 4 * 4) / 3; } // Baugruppen: die Kinder des obersten Knotens mit Unterbaum (bei three-d-stage z. B. ground, container_row, // fence). Ihre Außenmaße ergeben den Grundriss für die Überlagerung auf Karte und Satellitenbild. const scene = root.listScenes()[0]; const top = scene.listChildren(); const groupParents = top.length === 1 && top[0].listChildren().length > 0 ? top[0].listChildren() : top; const groups = groupParents .filter((node) => node.getMesh() || node.listChildren().length > 0) .map((node) => { const { min, max } = getBounds(node); return { name: node.getName() || "(unbenannt)", min: round(min), max: round(max) }; }); const { min, max } = getBounds(scene); const report = { file: source, megabytes: Number(((await stat(source)).size / 1e6).toFixed(1)), generator: await readGenerator(source), extensions: root.listExtensionsUsed().map((extension) => extension.extensionName), rootTranslation: top.length === 1 ? round(top[0].getTranslation()) : null, bounds: { min: round(min), max: round(max) }, textures: { count: root.listTextures().length, unique: hashes.size, sizes: textureSizes, gpuMB: Math.round(gpuBytes / 1e6) }, drawnTriangles: [...materials.values()].reduce((sum, entry) => sum + entry.drawnTriangles, 0), materials: [...materials.values()].sort((a, b) => b.drawnTriangles - a.drawnTriangles), groups, }; console.log(`${report.file}: ${report.megabytes} MB, ${report.generator}`); console.log(`Erweiterungen: ${report.extensions.join(", ")}`); console.log(`Außenmaße: x ${report.bounds.min[0]} … ${report.bounds.max[0]}, y ${report.bounds.min[1]} … ${report.bounds.max[1]}, z ${report.bounds.min[2]} … ${report.bounds.max[2]}`); console.log(`Wurzel-Verschiebung: ${JSON.stringify(report.rootTranslation)}`); console.log(`Texturen: ${report.textures.count} (${report.textures.unique} verschieden), ${JSON.stringify(report.textures.sizes)}, ~${report.textures.gpuMB} MB Grafikspeicher`); console.log(`Gezeichnete Dreiecke: ${report.drawnTriangles}`); console.table(report.materials.map(({ name, drawnTriangles, primitives, alphaMode, bump }) => ({ name, drawnTriangles, primitives, alphaMode, bump }))); console.table(report.groups.map(({ name, min: lo, max: hi }) => ({ name, x: `${lo[0]} … ${hi[0]}`, y: `${lo[1]} … ${hi[1]}`, z: `${lo[2]} … ${hi[2]}` }))); if (reportPath) { await writeFile(reportPath, JSON.stringify(report, null, 2)); console.log(`JSON: ${reportPath}`); }