Files
metabuilder/exploded-diagrams/scripts/complexity-score.ts
johndoe6345789 8ea14c5e3d feat: add headless geometry validation and preview scripts
CLI tools for fast iteration without browser:
- validate-geometry.ts: JSON schema validation
- geometry-stats.ts: Metrics and warnings (breeze block detection)
- complexity-score.ts: Visual interest scoring with breakdown
- ascii-preview.ts: Terminal-based top-down/front view
- validate-assembly.sh: Batch validate all parts

Updated 3D_GEOMETRY_AUTHORING_GUIDE.md with headless workflow section.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 22:07:42 +00:00

116 lines
3.4 KiB
TypeScript

#!/usr/bin/env npx tsx
import { readFileSync } from 'fs'
interface Geometry3D {
type: string
subtract?: boolean
intersect?: boolean
offsetX?: number
offsetY?: number
offsetZ?: number
fill?: string
[key: string]: unknown
}
function complexityScore(filePath: string): { score: number; breakdown: Record<string, number>; warnings: string[] } {
const data = JSON.parse(readFileSync(filePath, 'utf-8'))
const geoms: Geometry3D[] = data.geometry3d || []
const breakdown: Record<string, number> = {}
const warnings: string[] = []
let score = 0
// Base score for primitive count (5 pts each, max 50)
const primitiveScore = Math.min(geoms.length * 5, 50)
breakdown['primitives'] = primitiveScore
score += primitiveScore
// Bonus for variety of types (10 pts each, max 40)
const types = new Set(geoms.map(g => g.type))
const varietyScore = Math.min(types.size * 10, 40)
breakdown['type_variety'] = varietyScore
score += varietyScore
// Bonus for external features - non-subtractions with offsets (15 pts each, max 60)
const externalFeatures = geoms.filter(g =>
!g.subtract && !g.intersect && (g.offsetX || g.offsetY || g.offsetZ)
)
const externalScore = Math.min(externalFeatures.length * 15, 60)
breakdown['external_features'] = externalScore
score += externalScore
// Bonus for color variations (8 pts each, max 32)
const colors = new Set(geoms.map(g => g.fill).filter(Boolean))
const colorScore = Math.min(colors.size * 8, 32)
breakdown['color_variety'] = colorScore
score += colorScore
// Penalty: only subtractions from single base (-30)
const unionCount = geoms.filter(g => !g.subtract && !g.intersect).length
if (unionCount <= 1 && geoms.length > 1) {
breakdown['single_base_penalty'] = -30
score -= 30
warnings.push('Single base shape with only boolean operations')
}
// Penalty: no external features but has subtractions (-20)
if (externalFeatures.length === 0 && geoms.some(g => g.subtract)) {
breakdown['no_external_penalty'] = -20
score -= 20
warnings.push('No external features - internal subtractions are invisible')
}
return { score: Math.max(0, score), breakdown, warnings }
}
// Main
const file = process.argv[2]
if (!file) {
console.error('Usage: npx tsx complexity-score.ts <path-to-part.json>')
process.exit(1)
}
try {
const { score, breakdown, warnings } = complexityScore(file)
console.log(`\nComplexity Score for: ${file}`)
console.log('─'.repeat(50))
// Show breakdown
Object.entries(breakdown).forEach(([key, value]) => {
const sign = value >= 0 ? '+' : ''
const label = key.replace(/_/g, ' ')
console.log(` ${label}: ${sign}${value}`)
})
console.log('─'.repeat(50))
console.log(` TOTAL: ${score}`)
console.log('─'.repeat(50))
// Rating
if (score < 30) {
console.log('Rating: ⚠ LOW - likely looks like a breeze block')
} else if (score < 60) {
console.log('Rating: → MODERATE - acceptable but could be improved')
} else if (score < 100) {
console.log('Rating: ✓ GOOD - visually interesting')
} else {
console.log('Rating: ★ EXCELLENT - highly detailed')
}
// Warnings
if (warnings.length > 0) {
console.log('\nWarnings:')
warnings.forEach(w => console.log(`${w}`))
}
// Exit with error if score too low
if (score < 30) {
process.exit(1)
}
} catch (err) {
console.error(`✗ Error: ${(err as Error).message}`)
process.exit(1)
}