select-cad-bim-batch.mjs 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. // Select a seated CAD BIM subset for a batched export.
  2. // The full 48k plan cannot be exported in one Blender pass, so batches are cut by tier and mask;
  3. // every batch keeps the same schema so the exporter and verifier need no changes.
  4. import { readFileSync, writeFileSync } from 'node:fs'
  5. // Accept both `--key=value` and `--key value`; a bare `--key` means true.
  6. const argv = process.argv.slice(2)
  7. const args = {}
  8. for (let i = 0; i < argv.length; i++) {
  9. if (!argv[i].startsWith('--')) continue
  10. const key = argv[i].slice(2)
  11. const eq = key.indexOf('=')
  12. if (eq >= 0) {
  13. args[key.slice(0, eq)] = key.slice(eq + 1)
  14. } else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
  15. args[key] = argv[++i]
  16. } else {
  17. args[key] = true
  18. }
  19. }
  20. const source = JSON.parse(readFileSync('tools/city-massing/city/cad-bim/seated-plan.json', 'utf8'))
  21. let buildings = source.buildings
  22. // `--exclude <manifest.json>` removes buildings already exported in an earlier batch, so
  23. // successive batches tile the seated plan without duplication.
  24. let excludedIds = new Set()
  25. if (args.exclude) {
  26. for (const path of String(args.exclude).split(',')) {
  27. const manifest = JSON.parse(readFileSync(path, 'utf8'))
  28. for (const cell of manifest.cells) for (const id of cell.buildingIds) excludedIds.add(id)
  29. }
  30. buildings = buildings.filter((b) => !excludedIds.has(b.id))
  31. }
  32. if (args.tier) buildings = buildings.filter((b) => b.tier === args.tier)
  33. if (args.mask) buildings = buildings.filter((b) => Number(b.maskId) === Number(args.mask))
  34. if (args.max) {
  35. // Deterministic pick so a re-run selects the same buildings.
  36. const hash = (id) => {
  37. let value = 2166136261
  38. for (const ch of id) value = ((value ^ ch.charCodeAt(0)) * 16777619) >>> 0
  39. return value
  40. }
  41. buildings = [...buildings].sort((a, b) => hash(a.id) - hash(b.id)).slice(0, Number(args.max))
  42. }
  43. if (!buildings.length) throw new Error('selection is empty; check --tier/--mask/--max/--exclude')
  44. const out = {
  45. ...source,
  46. buildings,
  47. batch: {
  48. tier: args.tier || 'all',
  49. mask: args.mask || 'all',
  50. selected: buildings.length,
  51. excludedByEarlierBatches: excludedIds.size,
  52. note: 'One batch of the seated CAD BIM plan for a bounded Blender export.',
  53. },
  54. }
  55. const outFile = args.out || 'tools/city-massing/city/cad-bim/seated-batch.json'
  56. writeFileSync(outFile, JSON.stringify(out, null, 2))
  57. console.log(JSON.stringify({
  58. out: outFile,
  59. selected: buildings.length,
  60. byKind: Object.fromEntries([...new Set(buildings.map((b) => b.kind))].map((k) => [k, buildings.filter((b) => b.kind === k).length])),
  61. byMask: Object.fromEntries([...new Set(buildings.map((b) => String(b.maskId)))].map((k) => [k, buildings.filter((b) => String(b.maskId) === k).length])),
  62. }))