refactor: 抽 osmassets 包,要素注册表,材质单一定义源 (P0-P3)
generate_scene.py 从 1271 → 816 行 (-455)
P0: 纯函数搬家
- osmassets/osm.py: parse_osm / Projector / parse_height
- osmassets/geom.py: clip_polygon / point_in_polygon / distance_to_ring / sample_tree_row 等
- blender/tests/test_pure.py: 42 个 unittest (脱离 bpy 运行)
P1: 单一定义源
- osmassets/catalog.py: ROAD_LAYERS + MATERIALS (含 cesium 导出参数)
- 对接 osm2streets_scene_style.json 做图层一致性 warning
- 干掉 road_mats / layer_z / 材质参数三份副本
P2: 要素注册表
- osmassets/{water,grass,scrub}.py: 每个要素一个 assemble() 函数
- build() 中的 if/elif 链收缩为注册表调用
- 计数器集中到 counts 字典
P3: 材质契约化
- catalog.py 扩展 CESIUM_EXPORT 段 (tint/metallic/emission)
- 标记已发现的死条目 Office White Metal Facade (四表各一组)
校验:
- parity.js + scene_digest.py + glb-digest.js 三位一体
- control-1 vs p0/p1/p2a/p2b/p3-counts: 两区域全 PARITY OK
- 42 个纯 Python 测试全部通过
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
121
scripts/glb-digest.js
Normal file
121
scripts/glb-digest.js
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
// Structural digest of a GLB, for the osmassets refactor parity check.
|
||||
//
|
||||
// Byte-comparing the GLB is too strict: Blender packs images in hash-map order
|
||||
// and the buffer padding shifts with it, so two runs of identical code can
|
||||
// differ. This reads the glTF JSON chunk instead and reports the parts that
|
||||
// carry meaning downstream in Cesium — node/mesh/material identity and PBR
|
||||
// values — plus buffer lengths as a coarse size check.
|
||||
//
|
||||
// node scripts/glb-digest.js <file.glb> [--out digest.json]
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function readGlbJson(file) {
|
||||
const buffer = fs.readFileSync(file);
|
||||
if (buffer.length < 12 || buffer.readUInt32LE(0) !== 0x46546c67) {
|
||||
throw new Error(`Not a GLB (bad magic): ${file}`);
|
||||
}
|
||||
const total = buffer.readUInt32LE(8);
|
||||
let offset = 12;
|
||||
while (offset + 8 <= Math.min(total, buffer.length)) {
|
||||
const chunkLength = buffer.readUInt32LE(offset);
|
||||
const chunkType = buffer.readUInt32LE(offset + 4);
|
||||
const start = offset + 8;
|
||||
if (chunkType === 0x4e4f534a) {
|
||||
return JSON.parse(buffer.slice(start, start + chunkLength).toString("utf8"));
|
||||
}
|
||||
offset = start + chunkLength;
|
||||
}
|
||||
throw new Error(`No JSON chunk found in ${file}`);
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
if (typeof value === "number") return Number(value.toFixed(6));
|
||||
if (Array.isArray(value)) return value.map(round);
|
||||
return value;
|
||||
}
|
||||
|
||||
function materialDigest(material) {
|
||||
const pbr = material.pbrMetallicRoughness || {};
|
||||
return {
|
||||
name: material.name || null,
|
||||
baseColorFactor: round(pbr.baseColorFactor || null),
|
||||
metallicFactor: round(pbr.metallicFactor ?? null),
|
||||
roughnessFactor: round(pbr.roughnessFactor ?? null),
|
||||
hasBaseColorTexture: Boolean(pbr.baseColorTexture),
|
||||
hasNormalTexture: Boolean(material.normalTexture),
|
||||
emissiveFactor: round(material.emissiveFactor || null),
|
||||
emissiveStrength: round(
|
||||
material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? null,
|
||||
),
|
||||
alphaMode: material.alphaMode || null,
|
||||
doubleSided: material.doubleSided ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function digest(file) {
|
||||
const gltf = readGlbJson(file);
|
||||
const meshes = (gltf.meshes || []).map((mesh) => ({
|
||||
name: mesh.name || null,
|
||||
primitives: (mesh.primitives || []).map((primitive) => ({
|
||||
material: primitive.material ?? null,
|
||||
attributes: Object.keys(primitive.attributes || {}).sort(),
|
||||
// Vertex/index counts live on the accessors; they are the real geometry
|
||||
// fingerprint and stay stable regardless of buffer layout.
|
||||
count: gltf.accessors?.[primitive.attributes?.POSITION]?.count ?? null,
|
||||
indices: gltf.accessors?.[primitive.indices]?.count ?? null,
|
||||
})),
|
||||
}));
|
||||
return {
|
||||
file: path.basename(file),
|
||||
fileBytes: fs.statSync(file).size,
|
||||
counts: {
|
||||
nodes: (gltf.nodes || []).length,
|
||||
meshes: meshes.length,
|
||||
materials: (gltf.materials || []).length,
|
||||
images: (gltf.images || []).length,
|
||||
accessors: (gltf.accessors || []).length,
|
||||
},
|
||||
extensionsUsed: (gltf.extensionsUsed || []).slice().sort(),
|
||||
buffers: (gltf.buffers || []).map((buffer) => buffer.byteLength),
|
||||
nodes: (gltf.nodes || [])
|
||||
.map((node) => ({
|
||||
name: node.name || null,
|
||||
mesh: node.mesh ?? null,
|
||||
translation: round(node.translation || null),
|
||||
rotation: round(node.rotation || null),
|
||||
scale: round(node.scale || null),
|
||||
extras: node.extras ?? null,
|
||||
}))
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name))),
|
||||
meshes: meshes.sort((a, b) => String(a.name).localeCompare(String(b.name))),
|
||||
materials: (gltf.materials || [])
|
||||
.map(materialDigest)
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name))),
|
||||
images: (gltf.images || [])
|
||||
.map((image) => ({ name: image.name || null, mimeType: image.mimeType || null }))
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name))),
|
||||
};
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const file = argv.find((arg) => !arg.startsWith("--"));
|
||||
if (!file) {
|
||||
console.error("usage: node scripts/glb-digest.js <file.glb> [--out digest.json]");
|
||||
process.exit(1);
|
||||
}
|
||||
const outIndex = argv.indexOf("--out");
|
||||
const result = digest(path.resolve(file));
|
||||
const text = `${JSON.stringify(result, null, 2)}\n`;
|
||||
if (outIndex >= 0 && argv[outIndex + 1]) {
|
||||
const out = path.resolve(argv[outIndex + 1]);
|
||||
fs.mkdirSync(path.dirname(out), { recursive: true });
|
||||
fs.writeFileSync(out, text);
|
||||
console.log(`GLB digest: ${out}`);
|
||||
} else {
|
||||
process.stdout.write(text);
|
||||
}
|
||||
Reference in New Issue
Block a user