@developerz.ai/ts23d 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/bug-hunter.md +21 -0
- package/.claude/agents/docs-author.md +13 -0
- package/.claude/agents/dx-auditor.md +14 -0
- package/.claude/agents/primitive-author.md +22 -0
- package/.claude/agents/recipe-author.md +36 -0
- package/.claude/agents/visual-reviewer.md +23 -0
- package/.claude/commands/feature.md +28 -0
- package/.claude/commands/new-recipe.md +19 -0
- package/.claude/commands/release.md +14 -0
- package/.claude/commands/verify.md +9 -0
- package/.claude/skills/author-recipe/SKILL.md +93 -0
- package/CHANGELOG.md +79 -0
- package/CLAUDE.md +85 -0
- package/LICENSE +21 -0
- package/README.md +380 -2
- package/dist/chunk-11v28977.js +1575 -0
- package/dist/chunk-11v28977.js.map +30 -0
- package/dist/chunk-1ypfs7ga.js +3571 -0
- package/dist/chunk-1ypfs7ga.js.map +42 -0
- package/dist/chunk-4ddzsqtc.js +464 -0
- package/dist/chunk-4ddzsqtc.js.map +19 -0
- package/dist/chunk-a8fxakcj.js +19 -0
- package/dist/chunk-a8fxakcj.js.map +11 -0
- package/dist/chunk-gcyp9b4d.js +755 -0
- package/dist/chunk-gcyp9b4d.js.map +22 -0
- package/dist/chunk-kgvbpqqw.js +44 -0
- package/dist/chunk-kgvbpqqw.js.map +10 -0
- package/dist/chunk-kvcep7ft.js +1919 -0
- package/dist/chunk-kvcep7ft.js.map +43 -0
- package/dist/chunk-ravbb06q.js +89 -0
- package/dist/chunk-ravbb06q.js.map +11 -0
- package/dist/chunk-w1zyhj40.js +1031 -0
- package/dist/chunk-w1zyhj40.js.map +21 -0
- package/dist/chunk-wtqhq529.js +2532 -0
- package/dist/chunk-wtqhq529.js.map +43 -0
- package/dist/chunk-xnt2sdhf.js +836 -0
- package/dist/chunk-xnt2sdhf.js.map +17 -0
- package/dist/cli/args.d.ts +24 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +307 -0
- package/dist/cli/bin.js.map +14 -0
- package/dist/cli/load.d.ts +2 -0
- package/dist/cli/manifest.d.ts +49 -0
- package/dist/cli/render-cmds.d.ts +12 -0
- package/dist/cli/run.d.ts +1 -0
- package/dist/core/asset.d.ts +10 -0
- package/dist/core/definition.d.ts +148 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/family.d.ts +23 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +69 -0
- package/dist/core/index.js.map +9 -0
- package/dist/core/mesh.d.ts +35 -0
- package/dist/core/part.d.ts +114 -0
- package/dist/core/png.d.ts +1 -0
- package/dist/core/rng.d.ts +12 -0
- package/dist/core/transform.d.ts +20 -0
- package/dist/core/vec.d.ts +11 -0
- package/dist/core/world.d.ts +16 -0
- package/dist/export/bin-writer.d.ts +16 -0
- package/dist/export/container.d.ts +3 -0
- package/dist/export/glb.d.ts +38 -0
- package/dist/export/index.d.ts +4 -0
- package/dist/export/index.js +26 -0
- package/dist/export/index.js.map +9 -0
- package/dist/export/lights.d.ts +8 -0
- package/dist/export/materials.d.ts +23 -0
- package/dist/export/merge-pivots.d.ts +6 -0
- package/dist/export/metadata.d.ts +19 -0
- package/dist/export/motion.d.ts +9 -0
- package/dist/export/optimize-parts.d.ts +26 -0
- package/dist/export/primitive.d.ts +3 -0
- package/dist/export/tangents.d.ts +2 -0
- package/dist/export/textures.d.ts +22 -0
- package/dist/export/version.d.ts +2 -0
- package/dist/grammar/index.d.ts +2 -0
- package/dist/grammar/index.js +16 -0
- package/dist/grammar/index.js.map +9 -0
- package/dist/grammar/split.d.ts +21 -0
- package/dist/grammar/whyte.d.ts +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +411 -0
- package/dist/index.js.map +9 -0
- package/dist/kinds/building.d.ts +44 -0
- package/dist/kinds/faces.d.ts +27 -0
- package/dist/kinds/head-armour.d.ts +9 -0
- package/dist/kinds/head-beard.d.ts +6 -0
- package/dist/kinds/head-bust.d.ts +4 -0
- package/dist/kinds/head-extras.d.ts +15 -0
- package/dist/kinds/head-face.d.ts +6 -0
- package/dist/kinds/head-geom.d.ts +7 -0
- package/dist/kinds/head-hair.d.ts +4 -0
- package/dist/kinds/head-layout.d.ts +29 -0
- package/dist/kinds/head-mesh.d.ts +27 -0
- package/dist/kinds/head-params.d.ts +108 -0
- package/dist/kinds/head-presets.d.ts +19 -0
- package/dist/kinds/head-rim.d.ts +10 -0
- package/dist/kinds/head.d.ts +39 -0
- package/dist/kinds/hull-lines.d.ts +65 -0
- package/dist/kinds/hull-skin.d.ts +6 -0
- package/dist/kinds/hull.d.ts +14 -0
- package/dist/kinds/index.d.ts +16 -0
- package/dist/kinds/index.js +78 -0
- package/dist/kinds/index.js.map +9 -0
- package/dist/kinds/mass.d.ts +15 -0
- package/dist/kinds/openings.d.ts +30 -0
- package/dist/kinds/planned.d.ts +6 -0
- package/dist/kinds/roof-chimney.d.ts +16 -0
- package/dist/kinds/roof-shape.d.ts +64 -0
- package/dist/kinds/roof.d.ts +31 -0
- package/dist/kinds/vegetation-fronds.d.ts +15 -0
- package/dist/kinds/vegetation-impostor.d.ts +9 -0
- package/dist/kinds/vegetation-leaves.d.ts +15 -0
- package/dist/kinds/vegetation-mesh.d.ts +21 -0
- package/dist/kinds/vegetation-params.d.ts +90 -0
- package/dist/kinds/vegetation-presets.d.ts +20 -0
- package/dist/kinds/vegetation-skeleton.d.ts +20 -0
- package/dist/kinds/vegetation.d.ts +42 -0
- package/dist/kinds/vehicle.d.ts +42 -0
- package/dist/material/generator.d.ts +4 -0
- package/dist/material/index.d.ts +4 -0
- package/dist/material/index.js +38 -0
- package/dist/material/index.js.map +9 -0
- package/dist/material/paint/bond.d.ts +3 -0
- package/dist/material/paint/canvas.d.ts +32 -0
- package/dist/material/paint/cell.d.ts +4 -0
- package/dist/material/paint/cells-round.d.ts +5 -0
- package/dist/material/paint/cells.d.ts +7 -0
- package/dist/material/paint/compose.d.ts +5 -0
- package/dist/material/paint/concrete.d.ts +3 -0
- package/dist/material/paint/courses.d.ts +31 -0
- package/dist/material/paint/decode-png.d.ts +6 -0
- package/dist/material/paint/default-sheet.d.ts +2 -0
- package/dist/material/paint/foliage-broad.d.ts +5 -0
- package/dist/material/paint/foliage-common.d.ts +35 -0
- package/dist/material/paint/foliage-compound.d.ts +4 -0
- package/dist/material/paint/foliage-conifer.d.ts +5 -0
- package/dist/material/paint/foliage-raster.d.ts +26 -0
- package/dist/material/paint/foliage.d.ts +3 -0
- package/dist/material/paint/masks.d.ts +3 -0
- package/dist/material/paint/noise.d.ts +9 -0
- package/dist/material/paint/normal.d.ts +8 -0
- package/dist/material/paint/orm.d.ts +8 -0
- package/dist/material/paint/planks.d.ts +3 -0
- package/dist/material/paint/plates.d.ts +3 -0
- package/dist/material/paint/shapes.d.ts +8 -0
- package/dist/material/paint/slate.d.ts +3 -0
- package/dist/material/paint/stamp.d.ts +3 -0
- package/dist/material/paint/strip.d.ts +3 -0
- package/dist/material/paint/surface.d.ts +5 -0
- package/dist/material/paint/types.d.ts +35 -0
- package/dist/material/paint/validate.d.ts +3 -0
- package/dist/material/palette.d.ts +12 -0
- package/dist/material/shader-godot.d.ts +2 -0
- package/dist/material/shader-three.d.ts +2 -0
- package/dist/material/shader.d.ts +46 -0
- package/dist/material/trim-sheet.d.ts +22 -0
- package/dist/neural/generator.d.ts +25 -0
- package/dist/neural/images.d.ts +17 -0
- package/dist/neural/index.d.ts +2 -0
- package/dist/neural/index.js +14 -0
- package/dist/neural/index.js.map +9 -0
- package/dist/passes/bake-ao.d.ts +17 -0
- package/dist/passes/grime.d.ts +15 -0
- package/dist/passes/index.d.ts +6 -0
- package/dist/passes/index.js +22 -0
- package/dist/passes/index.js.map +9 -0
- package/dist/passes/livery.d.ts +19 -0
- package/dist/passes/optimize-cache.d.ts +15 -0
- package/dist/passes/optimize-weld.d.ts +15 -0
- package/dist/passes/optimize.d.ts +28 -0
- package/dist/passes/ray-grid.d.ts +23 -0
- package/dist/passes/weighted-normals.d.ts +8 -0
- package/dist/primitives/beam.d.ts +17 -0
- package/dist/primitives/bevel-box.d.ts +13 -0
- package/dist/primitives/box.d.ts +16 -0
- package/dist/primitives/cell.d.ts +20 -0
- package/dist/primitives/check.d.ts +12 -0
- package/dist/primitives/closed.d.ts +11 -0
- package/dist/primitives/erosion.d.ts +22 -0
- package/dist/primitives/font-5x7.d.ts +3 -0
- package/dist/primitives/heightfield.d.ts +16 -0
- package/dist/primitives/index.d.ts +23 -0
- package/dist/primitives/index.js +81 -0
- package/dist/primitives/index.js.map +9 -0
- package/dist/primitives/lathe-facets.d.ts +8 -0
- package/dist/primitives/lathe.d.ts +26 -0
- package/dist/primitives/lattice.d.ts +30 -0
- package/dist/primitives/loft.d.ts +15 -0
- package/dist/primitives/mc-table.d.ts +2 -0
- package/dist/primitives/member.d.ts +19 -0
- package/dist/primitives/noise.d.ts +34 -0
- package/dist/primitives/planar.d.ts +10 -0
- package/dist/primitives/prism.d.ts +13 -0
- package/dist/primitives/sculpt-edit.d.ts +56 -0
- package/dist/primitives/sculpt-tree.d.ts +23 -0
- package/dist/primitives/sculpt.d.ts +27 -0
- package/dist/primitives/sdf-grid.d.ts +17 -0
- package/dist/primitives/sdf-ops.d.ts +37 -0
- package/dist/primitives/sdf.d.ts +21 -0
- package/dist/primitives/strip-uv.d.ts +11 -0
- package/dist/primitives/surface-nets.d.ts +7 -0
- package/dist/primitives/sweep.d.ts +19 -0
- package/dist/primitives/terrain-color.d.ts +30 -0
- package/dist/primitives/terrain-grid.d.ts +36 -0
- package/dist/primitives/text-layout.d.ts +22 -0
- package/dist/primitives/text.d.ts +27 -0
- package/dist/primitives/tiled.d.ts +22 -0
- package/dist/primitives/transport.d.ts +17 -0
- package/dist/primitives/triangulate.d.ts +2 -0
- package/dist/primitives/value-noise.d.ts +10 -0
- package/dist/qa/checks.d.ts +11 -0
- package/dist/qa/clash.d.ts +2 -0
- package/dist/qa/floating.d.ts +5 -0
- package/dist/qa/gates.d.ts +12 -0
- package/dist/qa/geometry.d.ts +21 -0
- package/dist/qa/index.d.ts +12 -0
- package/dist/qa/index.js +78 -0
- package/dist/qa/index.js.map +9 -0
- package/dist/qa/inside.d.ts +12 -0
- package/dist/qa/inspect.d.ts +18 -0
- package/dist/qa/lods.d.ts +12 -0
- package/dist/qa/measures.d.ts +4 -0
- package/dist/qa/occlusion.d.ts +3 -0
- package/dist/qa/sanity.d.ts +7 -0
- package/dist/qa/types.d.ts +12 -0
- package/dist/render/beauty.d.ts +40 -0
- package/dist/render/camera.d.ts +33 -0
- package/dist/render/depth-map.d.ts +38 -0
- package/dist/render/font.d.ts +1 -0
- package/dist/render/ground.d.ts +13 -0
- package/dist/render/index.d.ts +10 -0
- package/dist/render/index.js +47 -0
- package/dist/render/index.js.map +9 -0
- package/dist/render/lighting.d.ts +20 -0
- package/dist/render/lod-strip.d.ts +5 -0
- package/dist/render/metrics.d.ts +37 -0
- package/dist/render/options.d.ts +28 -0
- package/dist/render/photo-match.d.ts +19 -0
- package/dist/render/raster.d.ts +25 -0
- package/dist/render/resolve.d.ts +11 -0
- package/dist/render/review-prompt.d.ts +2 -0
- package/dist/render/review-warnings.d.ts +34 -0
- package/dist/render/review.d.ts +24 -0
- package/dist/render/scene.d.ts +18 -0
- package/dist/render/shade.d.ts +22 -0
- package/dist/render/sheet.d.ts +15 -0
- package/dist/render/texture.d.ts +15 -0
- package/dist/render/thumbnail.d.ts +8 -0
- package/dist/render/vis-buffer.d.ts +18 -0
- package/dist/testing/assert.d.ts +21 -0
- package/dist/testing/compiled.d.ts +15 -0
- package/dist/testing/hash.d.ts +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +13 -0
- package/dist/testing/snapshot.d.ts +13 -0
- package/docs/README.md +52 -0
- package/docs/architecture/README.md +68 -0
- package/docs/architecture/adding-a-kind.md +44 -0
- package/docs/architecture/adding-a-primitive.md +49 -0
- package/docs/architecture/errors.md +47 -0
- package/docs/architecture/glb-production.md +214 -0
- package/docs/architecture/gltf-capabilities.md +80 -0
- package/docs/decisions/001-recipe-compiler-over-ai-meshes.md +27 -0
- package/docs/decisions/002-typescript-not-json.md +22 -0
- package/docs/decisions/003-shared-trim-sheet.md +21 -0
- package/docs/decisions/004-z-up-metres.md +21 -0
- package/docs/decisions/005-zero-runtime-deps.md +21 -0
- package/docs/decisions/006-single-package-subpath-exports.md +20 -0
- package/docs/decisions/007-cpu-contact-sheets.md +21 -0
- package/docs/decisions/008-surface-materials.md +21 -0
- package/docs/decisions/009-gltf-front-axis.md +21 -0
- package/docs/decisions/README.md +15 -0
- package/docs/idea/01-vision.md +117 -0
- package/docs/idea/02-dsl.md +343 -0
- package/docs/idea/03-ir.md +97 -0
- package/docs/idea/04-materials.md +114 -0
- package/docs/idea/05-agent-loop.md +141 -0
- package/docs/idea/06-qa.md +101 -0
- package/docs/idea/07-what-fits.md +45 -0
- package/docs/idea/08-neural-adapters.md +78 -0
- package/docs/idea/09-kinds.md +105 -0
- package/docs/landscape.md +49 -0
- package/docs/plans/roadmap.md +127 -0
- package/examples/build-with-bun.ts +41 -0
- package/llms.txt +47 -0
- package/package.json +99 -4
- package/recipes/README.md +38 -0
- package/recipes/asteroid/README.md +46 -0
- package/recipes/asteroid/asteroid.ts +336 -0
- package/recipes/boulders/README.md +28 -0
- package/recipes/boulders/boulders.ts +117 -0
- package/recipes/boxcar/README.md +46 -0
- package/recipes/boxcar/boxcar.ts +916 -0
- package/recipes/cargo_container_scifi/README.md +52 -0
- package/recipes/cargo_container_scifi/cargo_container_scifi.ts +461 -0
- package/recipes/dropship/README.md +84 -0
- package/recipes/dropship/dropship.ts +1144 -0
- package/recipes/eiffel_tower/README.md +110 -0
- package/recipes/eiffel_tower/eiffel_tower.ts +781 -0
- package/recipes/goblin_head/README.md +62 -0
- package/recipes/goblin_head/goblin_head.ts +36 -0
- package/recipes/human_head/README.md +64 -0
- package/recipes/human_head/human_head.ts +37 -0
- package/recipes/loco_440/README.md +32 -0
- package/recipes/loco_440/loco_440.ts +1404 -0
- package/recipes/material_swatches/README.md +21 -0
- package/recipes/material_swatches/material_swatches.ts +183 -0
- package/recipes/mountain_range/README.md +45 -0
- package/recipes/mountain_range/mountain_range.ts +128 -0
- package/recipes/oak/README.md +30 -0
- package/recipes/oak/oak.ts +19 -0
- package/recipes/orc_head/README.md +66 -0
- package/recipes/orc_head/orc_head.ts +39 -0
- package/recipes/palm/README.md +30 -0
- package/recipes/palm/palm.ts +19 -0
- package/recipes/pine/README.md +29 -0
- package/recipes/pine/pine.ts +19 -0
- package/recipes/signal_gantry/README.md +36 -0
- package/recipes/signal_gantry/signal_gantry.ts +522 -0
- package/recipes/station_halt/README.md +39 -0
- package/recipes/station_halt/station_halt.ts +722 -0
- package/recipes/t34/README.md +44 -0
- package/recipes/t34/t34.ts +1068 -0
- package/recipes/tiger_1/README.md +75 -0
- package/recipes/tiger_1/tiger_1.ts +1370 -0
- package/recipes/water_tower/README.md +40 -0
- package/recipes/water_tower/water_tower.ts +542 -0
- package/src/cli/args.ts +112 -0
- package/src/cli/bin.ts +7 -0
- package/src/cli/load.ts +17 -0
- package/src/cli/manifest.ts +89 -0
- package/src/cli/render-cmds.ts +66 -0
- package/src/cli/run.ts +168 -0
- package/src/core/asset.ts +128 -0
- package/src/core/definition.ts +149 -0
- package/src/core/errors.ts +45 -0
- package/src/core/family.ts +50 -0
- package/src/core/index.ts +66 -0
- package/src/core/mesh.ts +114 -0
- package/src/core/part.ts +130 -0
- package/src/core/png.ts +49 -0
- package/src/core/rng.ts +49 -0
- package/src/core/transform.ts +90 -0
- package/src/core/vec.ts +26 -0
- package/src/core/world.ts +80 -0
- package/src/export/bin-writer.ts +79 -0
- package/src/export/container.ts +35 -0
- package/src/export/glb.ts +200 -0
- package/src/export/index.ts +18 -0
- package/src/export/lights.ts +49 -0
- package/src/export/materials.ts +126 -0
- package/src/export/merge-pivots.ts +146 -0
- package/src/export/metadata.ts +59 -0
- package/src/export/motion.ts +70 -0
- package/src/export/optimize-parts.ts +80 -0
- package/src/export/primitive.ts +35 -0
- package/src/export/tangents.ts +75 -0
- package/src/export/textures.ts +88 -0
- package/src/export/version.ts +6 -0
- package/src/grammar/index.ts +4 -0
- package/src/grammar/split.ts +87 -0
- package/src/grammar/whyte.ts +20 -0
- package/src/index.ts +14 -0
- package/src/kinds/building.ts +153 -0
- package/src/kinds/faces.ts +138 -0
- package/src/kinds/head-armour.ts +151 -0
- package/src/kinds/head-beard.ts +133 -0
- package/src/kinds/head-bust.ts +221 -0
- package/src/kinds/head-extras.ts +150 -0
- package/src/kinds/head-face.ts +222 -0
- package/src/kinds/head-geom.ts +31 -0
- package/src/kinds/head-hair.ts +152 -0
- package/src/kinds/head-layout.ts +63 -0
- package/src/kinds/head-mesh.ts +157 -0
- package/src/kinds/head-params.ts +153 -0
- package/src/kinds/head-presets.ts +152 -0
- package/src/kinds/head-rim.ts +128 -0
- package/src/kinds/head.ts +171 -0
- package/src/kinds/hull-lines.ts +160 -0
- package/src/kinds/hull-skin.ts +83 -0
- package/src/kinds/hull.ts +209 -0
- package/src/kinds/index.ts +63 -0
- package/src/kinds/mass.ts +110 -0
- package/src/kinds/openings.ts +141 -0
- package/src/kinds/planned.ts +12 -0
- package/src/kinds/roof-chimney.ts +100 -0
- package/src/kinds/roof-shape.ts +230 -0
- package/src/kinds/roof.ts +250 -0
- package/src/kinds/vegetation-fronds.ts +126 -0
- package/src/kinds/vegetation-impostor.ts +118 -0
- package/src/kinds/vegetation-leaves.ts +168 -0
- package/src/kinds/vegetation-mesh.ts +174 -0
- package/src/kinds/vegetation-params.ts +147 -0
- package/src/kinds/vegetation-presets.ts +184 -0
- package/src/kinds/vegetation-skeleton.ts +168 -0
- package/src/kinds/vegetation.ts +199 -0
- package/src/kinds/vehicle.ts +144 -0
- package/src/material/generator.ts +56 -0
- package/src/material/index.ts +29 -0
- package/src/material/paint/bond.ts +60 -0
- package/src/material/paint/canvas.ts +91 -0
- package/src/material/paint/cell.ts +39 -0
- package/src/material/paint/cells-round.ts +119 -0
- package/src/material/paint/cells.ts +111 -0
- package/src/material/paint/compose.ts +53 -0
- package/src/material/paint/concrete.ts +62 -0
- package/src/material/paint/courses.ts +85 -0
- package/src/material/paint/decode-png.ts +100 -0
- package/src/material/paint/default-sheet.ts +61 -0
- package/src/material/paint/foliage-broad.ts +126 -0
- package/src/material/paint/foliage-common.ts +93 -0
- package/src/material/paint/foliage-compound.ts +135 -0
- package/src/material/paint/foliage-conifer.ts +143 -0
- package/src/material/paint/foliage-raster.ts +196 -0
- package/src/material/paint/foliage.ts +23 -0
- package/src/material/paint/masks.ts +44 -0
- package/src/material/paint/noise.ts +52 -0
- package/src/material/paint/normal.ts +52 -0
- package/src/material/paint/orm.ts +95 -0
- package/src/material/paint/planks.ts +56 -0
- package/src/material/paint/plates.ts +72 -0
- package/src/material/paint/shapes.ts +28 -0
- package/src/material/paint/slate.ts +52 -0
- package/src/material/paint/stamp.ts +40 -0
- package/src/material/paint/strip.ts +42 -0
- package/src/material/paint/surface.ts +60 -0
- package/src/material/paint/types.ts +51 -0
- package/src/material/paint/validate.ts +58 -0
- package/src/material/palette.ts +43 -0
- package/src/material/shader-godot.ts +77 -0
- package/src/material/shader-three.ts +103 -0
- package/src/material/shader.ts +100 -0
- package/src/material/trim-sheet.ts +100 -0
- package/src/neural/generator.ts +37 -0
- package/src/neural/images.ts +27 -0
- package/src/neural/index.ts +10 -0
- package/src/passes/bake-ao.ts +183 -0
- package/src/passes/grime.ts +37 -0
- package/src/passes/index.ts +8 -0
- package/src/passes/livery.ts +66 -0
- package/src/passes/optimize-cache.ts +175 -0
- package/src/passes/optimize-weld.ts +147 -0
- package/src/passes/optimize.ts +85 -0
- package/src/passes/ray-grid.ts +237 -0
- package/src/passes/weighted-normals.ts +125 -0
- package/src/primitives/beam.ts +60 -0
- package/src/primitives/bevel-box.ts +140 -0
- package/src/primitives/box.ts +90 -0
- package/src/primitives/cell.ts +115 -0
- package/src/primitives/check.ts +49 -0
- package/src/primitives/closed.ts +74 -0
- package/src/primitives/erosion.ts +183 -0
- package/src/primitives/font-5x7.ts +117 -0
- package/src/primitives/heightfield.ts +101 -0
- package/src/primitives/index.ts +56 -0
- package/src/primitives/lathe-facets.ts +45 -0
- package/src/primitives/lathe.ts +174 -0
- package/src/primitives/lattice.ts +131 -0
- package/src/primitives/loft.ts +174 -0
- package/src/primitives/mc-table.ts +41 -0
- package/src/primitives/member.ts +130 -0
- package/src/primitives/noise.ts +157 -0
- package/src/primitives/planar.ts +55 -0
- package/src/primitives/prism.ts +85 -0
- package/src/primitives/sculpt-edit.ts +212 -0
- package/src/primitives/sculpt-tree.ts +226 -0
- package/src/primitives/sculpt.ts +80 -0
- package/src/primitives/sdf-grid.ts +131 -0
- package/src/primitives/sdf-ops.ts +145 -0
- package/src/primitives/sdf.ts +83 -0
- package/src/primitives/strip-uv.ts +26 -0
- package/src/primitives/surface-nets.ts +161 -0
- package/src/primitives/sweep.ts +162 -0
- package/src/primitives/terrain-color.ts +85 -0
- package/src/primitives/terrain-grid.ts +137 -0
- package/src/primitives/text-layout.ts +85 -0
- package/src/primitives/text.ts +197 -0
- package/src/primitives/tiled.ts +93 -0
- package/src/primitives/transport.ts +85 -0
- package/src/primitives/triangulate.ts +42 -0
- package/src/primitives/value-noise.ts +62 -0
- package/src/qa/checks.ts +110 -0
- package/src/qa/clash.ts +177 -0
- package/src/qa/floating.ts +63 -0
- package/src/qa/gates.ts +126 -0
- package/src/qa/geometry.ts +187 -0
- package/src/qa/index.ts +28 -0
- package/src/qa/inside.ts +114 -0
- package/src/qa/inspect.ts +107 -0
- package/src/qa/lods.ts +97 -0
- package/src/qa/measures.ts +67 -0
- package/src/qa/occlusion.ts +166 -0
- package/src/qa/sanity.ts +137 -0
- package/src/qa/types.ts +17 -0
- package/src/render/beauty.ts +115 -0
- package/src/render/camera.ts +142 -0
- package/src/render/depth-map.ts +191 -0
- package/src/render/font.ts +76 -0
- package/src/render/ground.ts +87 -0
- package/src/render/index.ts +34 -0
- package/src/render/lighting.ts +95 -0
- package/src/render/lod-strip.ts +34 -0
- package/src/render/metrics.ts +147 -0
- package/src/render/options.ts +81 -0
- package/src/render/photo-match.ts +28 -0
- package/src/render/raster.ts +143 -0
- package/src/render/resolve.ts +109 -0
- package/src/render/review-prompt.ts +59 -0
- package/src/render/review-warnings.ts +92 -0
- package/src/render/review.ts +103 -0
- package/src/render/scene.ts +119 -0
- package/src/render/shade.ts +199 -0
- package/src/render/sheet.ts +88 -0
- package/src/render/texture.ts +97 -0
- package/src/render/thumbnail.ts +75 -0
- package/src/render/vis-buffer.ts +119 -0
- package/src/testing/assert.ts +115 -0
- package/src/testing/compiled.ts +31 -0
- package/src/testing/hash.ts +14 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/snapshot.ts +37 -0
- package/src/types/gltf-validator.d.ts +13 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
**Every failure is a `Ts23dError` with a stable `code`, a `cause` in the message, and a `fix`.** The reader is usually an agent: act on `fix`, don't parse prose. Codes are stable across minor versions. Source: `src/core/errors.ts`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
T3D_PART_UNKNOWN: part 'gun_barrel' parents to 'gun', which does not exist yet
|
|
7
|
+
fix: add the parent (ctx.group or ctx.part) before its children
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`--json`: `{"ok":false,"error":{"code":"…","message":"…","fix":"…"}}`, exit 1.
|
|
11
|
+
|
|
12
|
+
## Library codes
|
|
13
|
+
|
|
14
|
+
| Code | Thrown by | Cause | Fix |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `T3D_NOT_IMPLEMENTED` | Anything still in a `planned.ts` (`creature`), `/neural`, and whatever the roadmap lists as unbuilt | Declared API, not built yet. The message names the doc tracking it | Use a shipped alternative ([roadmap](../plans/roadmap.md)), or implement it ([adding a primitive](adding-a-primitive.md), [adding a kind](adding-a-kind.md)) |
|
|
17
|
+
| `T3D_GRAMMAR_SYNTAX` | `parseSplit`, `split`, `parseWhyte` | Bad split term, empty split, a repeat with stretch terms, more than one repeat; or a Whyte string that isn't three even numbers | Split: `sym:<metres>`, `sym:~`, `sym:~<weight>`, `{a:1 \| b:2}*`, joined by `\|`. Whyte: `'4-4-0'`, `'2-8-2'` |
|
|
18
|
+
| `T3D_GRAMMAR_OVERFLOW` | `layoutSplit`, `split`, `facade` | Fixed terms sum to more than the strip length | Shrink a fixed term or make one a stretch (`sym:~`) |
|
|
19
|
+
| `T3D_PART_DUPLICATE` | `ctx.part`, `ctx.group`, `ctx.use` | A part name added twice in one asset | Suffix the loop index: `wheel_0`, `wheel_1`; give each `ctx.use` a unique `prefix` |
|
|
20
|
+
| `T3D_PART_UNKNOWN` | `ctx.part`, `ctx.group` | `parent` names a part not added yet | Add the parent first |
|
|
21
|
+
| `T3D_RECIPE_INVALID` | `defineAsset`, `ctx.part`, every primitive and kind | Asset name not `[a-z][a-z0-9_]*`; a part with both `yaw` and `motion`; or a primitive/kind given impossible input (zero-length edge, bevel ≥ half a side, roof pitch out of range, SDF grid over the cell cap) | Rename to snake_case; put yaw on a parent `ctx.group`, motion on the child; otherwise the printed `fix:` names the bad option and its valid range |
|
|
22
|
+
| `T3D_NO_ASSET_EXPORT` | CLI loader | The module exports no `defineAsset` result | `export const my_thing = defineAsset({ … })`. Named export, no default |
|
|
23
|
+
| `T3D_QA_FAILED` | `/testing` assertions (`assertQaPass`, `assertBudget`, `assertMeasure`, …) | A recipe test found QA findings; the message lists every one. The CLI itself reports findings and exits 1 instead | Fix each finding's `fix:` line |
|
|
24
|
+
| `T3D_RENDER_OPTIONS` | `beauty`, `t3d hero` / `review` flags, render metrics | Bad image size, camera angle or render option | The printed `fix:` names the valid range |
|
|
25
|
+
|
|
26
|
+
## Repo tooling codes
|
|
27
|
+
|
|
28
|
+
In `ErrorCode`, raised by this repo's scripts and test tooling rather than by the library at runtime.
|
|
29
|
+
|
|
30
|
+
| Code | Where | Cause | Fix |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `T3D_AMBIENT_RANDOM` | Test preload (`scripts/test-setup.ts`) | Code under test called `Math.random` | Use `ctx.rng` or `createRng(seed)` |
|
|
33
|
+
| `T3D_RELEASE` | `scripts/release.ts` | Bad version string, missing `## [Unreleased]`, or tree not stamped at the version | The printed `fix:` (usually `bun run release <x.y.z>`) |
|
|
34
|
+
| `T3D_BENCH` | `scripts/bench.ts --write` | `README.md` has no bench block | The printed `fix:`; the markers are `<!-- bench:start -->` / `<!-- bench:end -->` on their own lines |
|
|
35
|
+
|
|
36
|
+
Guards (`boundaries`, `determinism`, `filesize`, `recipes`) print `file:line`, message and `fix:` without a code.
|
|
37
|
+
|
|
38
|
+
## QA findings are not errors
|
|
39
|
+
|
|
40
|
+
A gate returns `Finding { gate, part?, message, fix }`. Findings don't throw; `t3d build` prints them all and exits 1. Gate list: [06-qa](../idea/06-qa.md).
|
|
41
|
+
|
|
42
|
+
## Adding a code
|
|
43
|
+
|
|
44
|
+
1. Add it to `ErrorCode` in `src/core/errors.ts`. `T3D_` + area + problem, upper snake case.
|
|
45
|
+
2. Throw `new Ts23dError(code, { cause, fix })`. `cause` names the offending value; `fix` is an action, ideally a literal snippet.
|
|
46
|
+
3. Add a row here in the same PR.
|
|
47
|
+
4. Never rename or reuse a code. Retire by leaving it in the union, documented as unused.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Production GLB
|
|
2
|
+
|
|
3
|
+
**A ts23d GLB is production-ready when it passes glTF-Validator with zero errors and warnings, faces glTF +Z, uses only core glTF 2.0 by default, and keeps engine-bound names stable across LODs. Compression, instancing and draft physics are opt-in profiles, never the default.** Godot 4 refuses any file whose `extensionsRequired` lists something it does not implement, and in 2026-10 that includes `KHR_mesh_quantization`, `EXT_meshopt_compression` and `KHR_draco_mesh_compression` ([Godot `_parse_gltf_extensions` and `get_supported_gltf_extensions_hashset`](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp)). Those extensions must be required, so a "drop into any engine" default cannot use them ([KHR_mesh_quantization §Overview](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization), [KHR_draco_mesh_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression)).
|
|
4
|
+
|
|
5
|
+
As of 2026-10. The exporter here is `src/export/glb.ts` (`toGlb`), `bin-writer.ts` (`BinWriter`), `motion.ts` (`motionChannel`) and `merge-pivots.ts` (`mergeToPivots`). These were read on 2026-10-25, while another agent was adding emitter `extras` to `glb.ts`. The spec is cited as [glTF 2.0 Specification.adoc](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc) by section anchor, written **[spec §anchor]**. The rendered copy is at [registry.khronos.org](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html).
|
|
6
|
+
|
|
7
|
+
**Status updated 2026-10-25 after the exporter work (G1, G2, G4, G5, G10, surfaces, `rest` clip). Sections (b) and (d) keep the original findings, with an update block on top.**
|
|
8
|
+
|
|
9
|
+
Status legend: **done** · **partial** · **missing** · **n/a (not yet)**. *Verified by*: `code` means read in the exporter, `val` means glTF-Validator 2.0.0-dev.3.10 output, and `insp` means the byte-level inspection script run on the tiger_1 GLBs (section b).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## (a) Production checklist
|
|
14
|
+
|
|
15
|
+
### Must
|
|
16
|
+
|
|
17
|
+
| # | Rule | Source | Status | Verified by |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| M1 | 0 validator errors and 0 warnings | [glTF-Validator ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: 0 / 0 on all 6 tiger GLBs; glTF-Validator now runs in the test suite (`src/export/validator.test.ts`, devDependency) and caught a sealed-buffer bug, since fixed | val |
|
|
20
|
+
| M2 | `POSITION` accessors carry `min`/`max` that match the data exactly (`ACCESSOR_MIN_MISMATCH`/`MAX_MISMATCH` are errors) | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes): "Accessors for the `POSITION` semantic MUST have their bounds… defined" | **done**: `BinWriter.accessor(…, bounds=true)` computes bounds from the same `Float32Array` | code, val |
|
|
21
|
+
| M3 | Animation sampler `input` has `min`/`max` and strictly increasing times | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) | **done**: `motionChannel` passes `bounds=true` | code, val |
|
|
22
|
+
| M4 | Vertex attribute offsets are 4-byte aligned; buffer views ≥ accessor extent; no `byteStride` on non-vertex views | [spec §data-alignment](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#data-alignment) | **done**: `BinWriter.push` pads every view to 4 bytes and views are tightly packed | code, val |
|
|
23
|
+
| M5 | GLB: JSON chunk padded with `0x20`, BIN chunk padded with `0x00`, lengths multiple of 4, header length equals file size | [spec §glb-file-format-specification](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#glb-file-format-specification) | **done**: `toGlb` tail and `BinWriter.bytes` | code, val |
|
|
24
|
+
| M6 | `bufferView.target` set for vertex (34962) and index (34963) data; unset for animation data | [ISSUES.md `BUFFER_VIEW_TARGET_MISSING`](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done** | code, val (no hint) |
|
|
25
|
+
| M7 | Node hierarchy is disjoint strict trees: one parent at most, no cycles | [spec §nodes-and-hierarchy](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#nodes-and-hierarchy) | **done**: single root, parents resolved by name; `T3D_PART_UNKNOWN` rejects forward references at authoring | code, val |
|
|
26
|
+
| M8 | Normals unit length; rotation quaternions normalised (`ACCESSOR_VECTOR3_NON_UNIT`, `ROTATION_NON_UNIT`) | [ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: 0 non-unit normals out of 37,749 | val, insp |
|
|
27
|
+
| M9 | Indices use u8/u16/u32 and never contain the type's max value (primitive restart) | [spec §meshes-topology](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-topology) | **done**: u16 when ≤ 65,535 vertices, else u32 (G4) | code, val |
|
|
28
|
+
| M10 | Y-up, metres, **front faces +Z, left faces +X** | [spec §coordinate-system-and-units](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#coordinate-system-and-units): "the front side of a glTF asset faces +Z, the left side of a glTF asset faces +X" | **done** (G1): root quaternion `[-0.5,-0.5,-0.5,0.5]` maps authoring +X front → glTF +Z, +Z up → +Y, +Y left → +X, checked numerically. Was `[-√½,0,0,√½]` (assets faced glTF +X, the left side). [ADR 009](../decisions/009-gltf-front-axis.md) | code, insp |
|
|
29
|
+
| M11 | `COLOR_0` is linear and in [0,1]; it multiplies baseColor | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes), [spec §metallic-roughness-material](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#metallic-roughness-material): "encoded with the linear transfer function" | **done**: `src/material/palette.ts` converts sRGB hex to linear; measured range 0.009–1.0 | code, insp |
|
|
30
|
+
| M12 | Every node that engine code binds to (pivots) has a stable, unique name **in every LOD** | Engines bind by node path (glTFast/Godot/Unreal all keep node names) | **partial**: names are unique and stable at LOD0/LOD1 (24 nodes). LOD2 has only `tiger_1, body, turret, gun`, so the 18 wheel/sprocket/idler pivots disappear and game code that drives `wheel_l_0` breaks at LOD2 (G3) | insp |
|
|
31
|
+
| M13 | No accidental engine name hints: no node or mesh name ends in a Godot suffix (`-col`, `_col`, `$col`, `-colonly`, `-convcol`, `-convcolonly`, `-noimp`, `-occ`, `-occonly`, `-navmesh`, `-vehicle`, `-wheel`, `-rigid`), and no clip name starts or ends with `loop`/`cycle` unless looping is intended. Suffix matching is case-insensitive and accepts `-`, `$` or `_` | [Godot: node type customization](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html) | **missing as a gate**. No current recipe violates it (repo grep), but a part named `spare_wheel` would import into Godot as a `VehicleWheel3D` (G6) | code (grep) |
|
|
32
|
+
| M14 | A looping clip's last key equals its first key | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) clamps outside the input range; it does not define looping, so any seam is the asset's fault | **done** (G2): clip length 4 s, each motion rounded to whole cycles (`cyclesOf`), spins get ≥ 12 keys per turn, so the last key equals the first. Was a fixed 2 s clip that popped 216° on `rps 0.8` | code, insp |
|
|
33
|
+
|
|
34
|
+
### Should
|
|
35
|
+
|
|
36
|
+
| # | Rule | Source | Status | Verified by |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| S1 | Smallest index type that fits: u16 when the primitive has at most 65,535 vertices (index 65,535 is the restart value, so the highest usable index is 65,534), u8 is legal but slow on some GPUs | [spec §meshes-topology](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-topology) | **done** (G4): u16 indices whenever the primitive has ≤ 65,535 vertices | code, insp |
|
|
39
|
+
| S2 | `COLOR_0` as normalised `unorm16` VEC4 (8 B/v) instead of float VEC4 (16 B/v). This is core glTF, needs no extension, and every loader supports it | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes): COLOR_n is float32, unorm8 or unorm16 | **done** (G5): COLOR_0 is normalised u16 RGBA (8 B/v). Was float VEC4, 27 % of LOD0 | code, insp |
|
|
40
|
+
| S3 | No unused attributes (`UNUSED_OBJECT`) | [ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: the painted trim sheet is bound (embedded or external), so `TEXCOORD_0` is used. Infos remain only with `--sheet none` | val |
|
|
41
|
+
| S4 | Reuse one `mesh` index across nodes with identical local geometry; engines then share one mesh asset | [spec §meshes-overview](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-overview) (nodes reference meshes by index) | **missing** (G9) | insp |
|
|
42
|
+
| S5 | `asset.copyright`, plus provenance in `asset.extras` (recipe, ts23d version, seed, content hash) | [spec §asset](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#asset): "metadata MAY be stored in… `generator` or `copyright`" | **partial**: `generator: "@developerz.ai/ts23d"` has no version; there is no copyright and no provenance | code |
|
|
43
|
+
| S6 | Clip names follow engine conventions: Godot loops a clip only if its name starts or ends with `loop`/`cycle` | [Godot node type customization](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html); [`gltf_document.cpp` `begins_with("loop")`](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp) | **missing** (G7): clips are `rest` and `run`; Godot imports `run` non-looping | code |
|
|
44
|
+
| S7 | Default to no clip, or a harmless idle; viewers autoplay the first clip | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) (runtime behaviour undefined); Babylon `animationStartMode = FIRST` ([source](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/loaders/src/glTF/glTFFileLoader.pure.ts)); model-viewer `autoplay` plays the first ([docs](https://modelviewer.dev/docs/index.html)) | **done**: `toGlb` exports no clip unless `animations: true`. When it does (and `t3d build` does by default), the first clip is `rest` (static, two keys), so viewers open paused and `run` plays on request | code |
|
|
45
|
+
| S8 | One sampler `input` accessor shared by channels with the same keyframe times | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) | **done** (G10): time accessors shared per key count | code |
|
|
46
|
+
| S9 | Root transform: rotation-only on one named root; no scale | [Godot import configuration: Apply Root Scale](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/import_configuration.html) | **done** (rotation only; corrected quaternion, see M10) | code |
|
|
47
|
+
| S10 | `TANGENT` only when a normal map is bound. Without one, omit it (the spec says it "SHOULD NOT" be present without normals). With one, either ship MikkTSpace tangents or accept the `MESH_PRIMITIVE_GENERATED_TANGENT_SPACE` warning | [spec §meshes-usage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-usage): tangents not specified means "calculated using default MikkTSpace algorithms" | **done**: `TANGENT` is emitted only when a sheet (with its normal map) is bound | code |
|
|
48
|
+
| S11 | Collision proxies available in a form each engine imports (section 6) | Per-engine (section c) | **missing** | code |
|
|
49
|
+
| S12 | LODs consumable without hand work: one file with MSFT_lod plus `MSFT_screencoverage`, and/or `_LODn` names, plus the per-LOD files | [MSFT_lod](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod) | **partial**: three separate files `tiger_1.lod{0,1,2}.glb` with `extras.lod`; no switch distances | code |
|
|
50
|
+
|
|
51
|
+
### Nice
|
|
52
|
+
|
|
53
|
+
| # | Rule | Source | Status |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| N1 | `--profile web`: `KHR_mesh_quantization` + `EXT_meshopt_compression` (or `KHR_meshopt_compression`, Release Candidate) for three.js/Babylon/glTFast | [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression) (Complete, Ratified); [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_meshopt_compression) | missing |
|
|
56
|
+
| N2 | `EXT_mesh_gpu_instancing` for bolts, rivets and track links in a web/Unity profile | [EXT_mesh_gpu_instancing](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing): "files that use the extension should specify it in `extensionsRequired`" | missing |
|
|
57
|
+
| N3 | KTX2 (`KHR_texture_basisu`) trim sheet: UASTC for normal and ORM, ETC1S for base colour | [KTX Artist Guide](https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXArtistGuide.md) | n/a (no textures yet) |
|
|
58
|
+
| N4 | `KHR_materials_emissive_strength` for lit windows and headlights | [spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_emissive_strength) (Ratified) | missing |
|
|
59
|
+
| N5 | `KHR_xmp_json_ld` with `dc:creator`, `dc:rights`, `xmpRights:UsageTerms` | [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) (Ratified) | missing |
|
|
60
|
+
| N6 | `KHR_physics_rigid_bodies` + `KHR_implicit_shapes` (in `extensionsUsed` only, never required) | Draft PRs [#2424](https://github.com/KhronosGroup/glTF/pull/2424), [#2370](https://github.com/KhronosGroup/glTF/pull/2370), both open | missing (wait) |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## (b) Validator results: tiger_1
|
|
65
|
+
|
|
66
|
+
**Update 2026-10-25, after G1/G2/G4/G5/G10:** tiger_1 is now 88,176 / 16,304 / 648 triangles (the recipe was reworked). LOD0 dropped from 13.0 MB to 11.0 MB with 0 validator errors. Build plus QA at 3 LODs, contact sheet included, takes about 4.7 s. The table below is the original run on the older, smaller recipe.
|
|
67
|
+
|
|
68
|
+
**Verdict: VALID. 0 errors, 0 warnings, 0 hints on all six files. The only infos are `UNUSED_OBJECT` on `TEXCOORD_0` (one per primitive), because no texture is bound.** The real problems (sideways front, u32 indices, fat colours, loop seam, LOD2 losing pivots) are outside the validator's scope and were found by the inspection script.
|
|
69
|
+
|
|
70
|
+
The build was `bun run t3d build recipes/tiger_1/tiger_1.ts --out …/glbcheck`, then again with `--animations`. Both print `QA FAIL` (clashes and floating parts in the recipe, not the exporter) and both still write all three GLBs. The validator was npm `gltf-validator@2.0.0-dev.3.10` (`validateBytes`, `maxIssues: 0`), run from the scratchpad.
|
|
71
|
+
|
|
72
|
+
| File | Bytes | Err | Warn | Info | Hint | Draw calls | Verts | Tris | Anim |
|
|
73
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
74
|
+
| `tiger_1.lod0.glb` | 2,209,336 | 0 | 0 | 23 | 0 | 23 | 37,749 | 31,148 | 0 |
|
|
75
|
+
| `tiger_1.lod1.glb` | 426,376 | 0 | 0 | 23 | 0 | 23 | 6,592 | 7,212 | 0 |
|
|
76
|
+
| `tiger_1.lod2.glb` | 36,012 | 0 | 0 | 3 | 0 | 3 | 584 | 384 | 0 |
|
|
77
|
+
| `--animations` lod0 | 2,226,572 | 0 | 0 | 23 | 0 | 23 | 37,749 | 31,148 | 1 (`run`, 20 rotation channels, LINEAR, 0–2 s) |
|
|
78
|
+
| `--animations` lod1 | 443,576 | 0 | 0 | 23 | 0 | 23 | 6,592 | 7,212 | 1 |
|
|
79
|
+
| `--animations` lod2 | 36,012 | 0 | 0 | 3 | 0 | 3 | 584 | 384 | 0 (no motion pivots left) |
|
|
80
|
+
|
|
81
|
+
All infos have the same form:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "code": "UNUSED_OBJECT", "severity": 2, "pointer": "/meshes/N/primitives/0/attributes/TEXCOORD_0" }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Findings from the inspection script, which the validator does not check:
|
|
88
|
+
|
|
89
|
+
| Check | LOD0 result | Rule |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| u32 indices on primitives with fewer than 65k verts | 23/23 | S1 |
|
|
92
|
+
| Byte split: POSITION / TEXCOORD_0 / COLOR_0 / indices (NORMAL ≈ POSITION) | 453 K / 302 K / **604 K** / 374 K | S1, S2 |
|
|
93
|
+
| Degenerate triangles | 0 | (`ACCESSOR_INDEX_TRIANGLE_DEGENERATE`) |
|
|
94
|
+
| Non-unit normals (more than 5e-4 off) | 0 | M8 |
|
|
95
|
+
| COLOR_0 range | 0.0092–1.0, alpha always 1 | M11, S2 |
|
|
96
|
+
| Root rotation | `[-0.7071, 0, 0, 0.7071]`: authoring +X goes to glTF +X (left) | M10 |
|
|
97
|
+
| Node names, LOD0 / LOD2 | 24 / 4 (all wheels gone at LOD2) | M12 |
|
|
98
|
+
| Spin loop seam (`rps 0.8`) | key 0 `[0,0,0,1]`, key 24 `[0,-0.951,0,0.309]` | M14 |
|
|
99
|
+
| Animation time accessors | 20 identical | S8 |
|
|
100
|
+
| `extensionsUsed` | none (core-only, which is good for Godot) | — |
|
|
101
|
+
|
|
102
|
+
Re-run: write the two `.mjs` scripts below into a scratch directory with `npm i gltf-validator`, then run `node validate.mjs out/*.glb`. `gltf-validator` is now a devDependency used by `src/export/validator.test.ts`; ADR 005 restricts runtime dependencies only.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## (c) Per-engine import table
|
|
107
|
+
|
|
108
|
+
"glTF" below means the engine's default glTF importer, as of 2026-10.
|
|
109
|
+
|
|
110
|
+
| | **Unity (glTFast 6.x)** | **Unreal 5 (Interchange glTF)** | **Godot 4** | **three.js (GLTFLoader)** | **Babylon.js** |
|
|
111
|
+
|---|---|---|---|---|---|
|
|
112
|
+
| Importer | glTFast is Unity's glTF package. Unity has no built-in glTF importer ([glTFast manual](https://docs.unity3d.com/Packages/com.unity.cloud.gltfast@6.14/manual/index.html)) | Interchange is the default for glTF static meshes ([format support](https://dev.epicgames.com/documentation/en-us/unreal-engine/gltf-file-format-support-in-unreal-engine)) | Built-in `GLTFDocument` | `examples/jsm` loader | `@babylonjs/loaders` |
|
|
113
|
+
| Naming | Node names become GameObject names. Mesh name becomes the asset name | Mesh name becomes the Static Mesh asset name. Interchange has renamed collision meshes in the past ([UE-248981](https://issues.unrealengine.com/issue/UE-248981), fixed May 2025) | Node names are kept. Suffix hints are stripped and acted on (`-col`, `-noimp`, …), with `-`, `$` or `_` and case-insensitive ([docs](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html)) | `Object3D.name` = node name (sanitised) | `TransformNode.name` = node name |
|
|
114
|
+
| Collision | None imported. Add colliders in-engine | Prefixes `UBX_` box, `UCP_` capsule, `USP_` sphere, `UCX_` convex, matched to the unprefixed mesh name (`UCX_body`, `UCX_body_01`) ([Interchange Generic Mesh Pipeline](https://api.unrealengine.com/INT/PythonAPI/class/InterchangeGenericMeshPipeline.html), [FBX pipeline](https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-static-mesh-pipeline-in-unreal-engine)). **glTF path unverified:** test-import a fixture | `-col` adds a trimesh body, `-convcol` a convex one; `-colonly` and `-convcolonly` replace the mesh with a collider; `-rigid` makes a RigidBody3D. `OMI_physics_body`/`OMI_physics_shape` are the import extensions it declares. `KHR_physics_rigid_bodies` / `KHR_collision_shapes` appear only in its JSON-pointer mapping so far ([physics ext source](https://github.com/godotengine/godot/blob/master/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp)) | None | None (no physics import) |
|
|
115
|
+
| LOD | MSFT_lod is listed "not investigated", so only LOD0 loads ([features](https://github.com/Unity-Technologies/com.unity.cloud.gltfast/blob/main/Packages/com.unity.cloud.gltfast/Documentation~/features.md)). Unity's `_LOD0` naming applies to the model importer (FBX), not glTFast ([manual](https://docs.unity3d.com/Manual/importing-lod-meshes.html)) | No glTF LOD import found. Import LOD1/2 files into LOD slots, or auto-generate them | Ignores MSFT_lod (not in the supported set). Generates its own LODs on import ("Generate LODs", [import configuration](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/import_configuration.html)). Use [visibility ranges](https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html) for hand LODs | MSFT_lod not supported ([GLTFLoader docs](https://threejs.org/docs/pages/GLTFLoader.html)). Build `THREE.LOD` from the per-LOD files | **MSFT_lod supported** with progressive loading and `maxLODsToLoad` ([source](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.pure.ts)) |
|
|
116
|
+
| Animation | Legacy Animation ✅, Mecanim partial (clips not assigned), Playables ⛔ ([features](https://github.com/Unity-Technologies/com.unity.cloud.gltfast/blob/main/Packages/com.unity.cloud.gltfast/Documentation~/features.md)) | Imports node animation as Level Sequence or anim assets (verify on fixture) | AnimationPlayer. A clip loops only if its name starts or ends with `loop`/`cycle` ([source](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp)) | `gltf.animations` holds `AnimationClip`s; no autoplay | Autoplays the **first** clip by default (`animationStartMode = FIRST`) |
|
|
117
|
+
| Quantization / meshopt / Draco | ✅ / ✅ (needs package) / ✅ (needs package) | Exporter supports quantization; import support for the three is undocumented | ⛔ / ⛔ / ⛔: the file **fails to import** if they are required | ✅ / ✅ (`setMeshoptDecoder`; `KHR_meshopt_compression` also built in) / ✅ (`setDRACOLoader`) | ✅ / ✅ / ✅ |
|
|
118
|
+
| KTX2 basisu | ✅ (KTX package) | Undocumented | ✅ (`gltf_document_extension_texture_ktx`) | ✅ (`setKTX2Loader`) | ✅ |
|
|
119
|
+
| GPU instancing | ✅ (no custom `_ID` attrs) | Undocumented | ⛔ (not in supported set) | ✅ (`InstancedMesh`) | ✅ |
|
|
120
|
+
| Emissive strength | ⛔ (blank in features table) | Undocumented | ✅ | ✅ | ✅ |
|
|
121
|
+
| `extras` | Kept in the glTFast API; not on components | Interchange metadata (verify) | `GLTFNode` metadata (opt-in) | `userData` | `metadata` (`ExtrasAsMetadata`) |
|
|
122
|
+
|
|
123
|
+
**Consequence:** the default GLB uses core glTF only. A `web` profile adds compression and instancing. An engine profile adds collision naming. Where this table says Unreal is "undocumented" or "verify", import a test file before relying on the behaviour.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Topics 3–7: rules
|
|
128
|
+
|
|
129
|
+
### 3. Optimisation extensions: when to use
|
|
130
|
+
|
|
131
|
+
| Extension | Use when | Don't use when | Registry status |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `KHR_mesh_quantization` | Web/Unity/Babylon profile. Positions as int16 with a node scale/offset dequantisation, normals as int8 normalised (stride 4) | Default export (Godot refuses it because it must be required) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization)) |
|
|
134
|
+
| `EXT_meshopt_compression` | Web profile. Pair with quantization. Decoding is fast, it compresses animation (quaternion filter 2) too, and a `fallback` buffer can keep it optional | Godot targets (unless a fallback buffer is shipped, which doubles the size) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression)). `KHR_meshopt_compression` is the RC successor with a v1 bitstream and colour filter ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_meshopt_compression)) |
|
|
135
|
+
| `KHR_draco_mesh_compression` | Only when a target lacks meshopt. Geometry only, slower decode, must be required without a fallback | Prefer meshopt for everything we ship ([meshoptimizer gltfpack](https://github.com/zeux/meshoptimizer/blob/master/gltf/README.md)) | Ratified |
|
|
136
|
+
| `KHR_texture_basisu` | When the trim sheet ships. UASTC for normal and ORM, ETC1S for base colour ([KTX Artist Guide](https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXArtistGuide.md)). Keep a PNG `source` fallback so it stays optional | Required without a fallback in the default profile | Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu)) |
|
|
137
|
+
| `KHR_texture_transform` | Only if strips get tiled by material instead of UVs | Our trim sheet bakes the UV rect into `TEXCOORD_0`, so it is not needed | Ratified |
|
|
138
|
+
| `EXT_mesh_gpu_instancing` | Bolts, rivets, track links, fence posts: ≥ ~16 copies of one sub-mesh in a web/Unity profile | Default (no fallback, so it should be required; Godot lacks it) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing)) |
|
|
139
|
+
| `MSFT_lod` + `extras.MSFT_screencoverage` | Ship a single-file LOD set: extension on the LOD0 root node, `ids` → LOD1/LOD2 roots, coverage like `[0.25, 0.08, 0.01]`. It can live in `extensionsUsed` only, because non-supporting loaders load the highest LOD | — (safe additive; costs about 20 % more bytes) | Vendor ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod)) |
|
|
140
|
+
|
|
141
|
+
### 4. Materials
|
|
142
|
+
|
|
143
|
+
| Rule | Source | Us |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| ORM packing: one linear texture, R = occlusion (`occlusionTexture`), G = roughness, B = metallic (`metallicRoughnessTexture`), both pointing at the same texture | [spec §additional-textures-occlusion](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#additional-textures-occlusion) note: "could point to the same texture that packs all three" | Planned ORM in ADR 003. Bind both slots to one texture index |
|
|
146
|
+
| baseColor and emissive textures are sRGB; ORM and normal are linear | [spec §metallic-roughness-material](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#metallic-roughness-material) | Painter must write the albedo as sRGB and the ORM as linear |
|
|
147
|
+
| `COLOR_0` is linear and multiplies baseColor (texture × factor × COLOR_0) | same | done |
|
|
148
|
+
| Foliage cards: `alphaMode: "MASK"`, `alphaCutoff` (default 0.5), `doubleSided: true` | [spec §alpha-coverage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#alpha-coverage), [§double-sided](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#double-sided) | **done**: `surface: 'cutout'` ([ADR 008](../decisions/008-surface-materials.md)) |
|
|
149
|
+
| Tangents: omit them and let the importer run MikkTSpace (the spec default) unless a baked normal map needs bit-exact tangents. Our tiling trim normals don't | [spec §meshes-usage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-usage) | correct today |
|
|
150
|
+
| Lit windows: `emissiveFactor` up to 1 plus `KHR_materials_emissive_strength`. Unity glTFast ignores the extension and falls back to strength 1 | [spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_emissive_strength) | missing |
|
|
151
|
+
|
|
152
|
+
### 5. Animation
|
|
153
|
+
|
|
154
|
+
| Rule | Source |
|
|
155
|
+
|---|---|
|
|
156
|
+
| glTF defines no autoplay, loop or order. Loaders choose: Babylon and model-viewer play the first clip; three and Godot don't autoplay | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) |
|
|
157
|
+
| LINEAR on `rotation` is slerp; keep consecutive keys under 180° apart (we use 15° steps or finer) | same, [§interpolation-slerp](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#interpolation-slerp) |
|
|
158
|
+
| Loops: last key = first key, and the clip length is an integer number of periods for every channel | consequence of clamping (same section) |
|
|
159
|
+
| One clip per behaviour, named `<verb>_loop` (Godot loop flag); keep pivot-driving clips out of the first slot | Godot source above |
|
|
160
|
+
| Each (node, path) appears at most once per animation | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) |
|
|
161
|
+
|
|
162
|
+
### 6. Collision and physics
|
|
163
|
+
|
|
164
|
+
| Approach | Engines | Ship how |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| Godot suffix nodes `<pivot>-colonly` / `-convcolonly` | Godot | Engine profile `godot`; the collider node is a child of the pivot |
|
|
167
|
+
| Unreal prefix meshes `UCX_<mesh>_NN`, `UBX_`, `USP_`, `UCP_` | Unreal (FBX documented; glTF via Interchange unverified) | Engine profile `unreal`; the collision mesh is a sibling, named after the render mesh |
|
|
168
|
+
| `extras.ts23d.collision` on a hidden child node (box/convex hull data) | Unity, three, Babylon (scripts read it) | Default: no visible mesh. Shapes go in extras so nothing renders by accident |
|
|
169
|
+
| `KHR_physics_rigid_bodies` + `KHR_implicit_shapes` | None import it yet (Godot imports the OMI equivalents) | Draft PRs [#2424](https://github.com/KhronosGroup/glTF/pull/2424) and [#2370](https://github.com/KhronosGroup/glTF/pull/2370), open as of 2026-10. Emit in `extensionsUsed` only, after ratification |
|
|
170
|
+
|
|
171
|
+
A collider mesh in a generic GLB renders as visible geometry in Unity, three and Babylon. So collision geometry is never in the default visual file. It goes in extras, an engine profile, or a sibling `*.collision.glb`.
|
|
172
|
+
|
|
173
|
+
### 7. Metadata
|
|
174
|
+
|
|
175
|
+
| Field | Rule | Source |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `asset.generator` | `"@developerz.ai/ts23d <version>"` | [spec §asset](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#asset) |
|
|
178
|
+
| `asset.copyright` | From the recipe (`license`/`author`), else `"ts23d recipe <name>, MIT"` per repo license | same |
|
|
179
|
+
| `asset.extras.ts23d` | `{ recipe, version, seed, lod, lodCount, units:"m", up:"+Y", front:"+Z", sourceUp:"+Z", sourceFront:"+X", contentHash }` | [spec §specifying-extensions](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#specifying-extensions) and the `extras` property (any JSON) |
|
|
180
|
+
| LOD switch distances | `extras.MSFT_screencoverage` on the MSFT_lod node, plus `extras.ts23d.lodScreenCoverage` in each per-LOD file | [MSFT_lod](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod) |
|
|
181
|
+
| Rights | `KHR_xmp_json_ld` packet with `dc:creator`, `dc:rights`, `xmpRights:UsageTerms` (in `extensionsUsed` only) | [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) |
|
|
182
|
+
|
|
183
|
+
Today the exporter writes `extras` at the glTF **root** (`kind, lod, units, up`), not in `asset.extras`. Root `extras` are legal, but many importers surface only node and `asset` extras.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## (d) Prioritised gap list
|
|
188
|
+
|
|
189
|
+
`ZERO-DEP WIN` means pure gain: core glTF only, no new dependency, valid in every engine, byte-deterministic. Every test lives next to the file it tests and runs in `bun run verify`.
|
|
190
|
+
|
|
191
|
+
| P | Gap | Exact change | Test that proves it | Tag |
|
|
192
|
+
|---|---|---|---|---|
|
|
193
|
+
| **G1** | Asset faces glTF +X (glTF's left) instead of +Z (M10) | `src/export/glb.ts` `toGlb`: root `rotation` changes from `[-Math.SQRT1_2,0,0,Math.SQRT1_2]` to `[-0.5,-0.5,-0.5,0.5]`, which maps authoring +X front to +Z, +Y left to +X and +Z up to +Y (checked numerically). Amend ADR 004's "one −90° X rotation" line, and set `extras.up/front`. Breaking for anyone who has already placed assets: CHANGELOG | `glb.test.ts`: rotate `[1,0,0]` by the root quaternion and expect `[0,0,1]`; rotate `[0,1,0]` and expect `[1,0,0]` | **done** · ZERO-DEP WIN (breaking) |
|
|
194
|
+
| **G2** | Loop seam: spin ends at 1.6 rev (M14) | `src/export/motion.ts` `motionChannel`: replace the fixed `LOOP_SECONDS` with a clip length `D` passed from `toGlb`. Per channel, round cycles to `n = max(1, round(rate·D))` and sample `max(24, 8n)` keys so steps stay under 45°. Record the true rate in `extras.ts23d.motion`. `toGlb` picks `D` = the smallest of 2, 4 or 8 s that keeps every channel's rounding error under 10 % | `motion.test.ts` (new): for spin `rps 0.8`, the first and last output quaternions are equal up to sign (`|dot| > 0.9999`); same for sway and bob | **done** · ZERO-DEP WIN |
|
|
195
|
+
| **G3** | Pivots vanish at LOD2 (M12) | `src/cli/run.ts`: collect the LOD0 pivot list (names, parent, `at`) and pass it as `GlbOptions.pivots`. `src/export/glb.ts` `toGlb` emits an empty node for each pivot missing from this LOD, under its LOD0 parent | `glb.test.ts`: an asset whose LOD2 build omits `wheel_0` still has node `wheel_0` in the LOD2 GLB, and its node-name set equals LOD0's | ZERO-DEP WIN |
|
|
196
|
+
| **G4** | u32 indices everywhere (S1) | `src/export/bin-writer.ts` `BinWriter.accessor`: accept `Uint16Array` and set `componentType: 5123`. `toGlb` converts to `Uint16Array` when `positions.length/3 <= 65535`. `push` already 4-aligns the following view | `glb.test.ts`: the box crate's index accessor has `componentType === 5123`; a 70k-vertex mesh keeps 5125; scratch run of glTF-Validator stays 0/0 | **done** · ZERO-DEP WIN (−187 KB on tiger LOD0) |
|
|
197
|
+
| **G5** | COLOR_0 float VEC4 is 27 % of the file (S2) | `bin-writer.ts`: `accessor` takes `Uint16Array` + `normalized: true` (componentType 5123, add `normalized`). `glb.ts` `toGlb` writes the colour as `Math.round(c·65535)` into VEC4 unorm16 (8 B/v, aligned) | `glb.test.ts`: the COLOR_0 accessor has `componentType 5123`, `normalized: true`, and the decoded value is within 1/65535 of the source | **done** · ZERO-DEP WIN (−302 KB on tiger LOD0) |
|
|
198
|
+
| **G6** | No guard against Godot name hints (M13) | New gate `src/qa/` `engineNames` (tier 3; QA already sees all part names): fail on `/[-_$](col|colonly|convcol|convcolonly|noimp|occ|occonly|navmesh|vehicle|wheel|rigid)$/i`, with `fix: rename to <name>_part or opt in via exportHint`. Exporter-side mirror is optional | `qa/*.test.ts`: a part named `spare_wheel` makes `runQa` report `engine-names`; `wheel_l_0` passes | ZERO-DEP WIN |
|
|
199
|
+
| **G7** | Clip named `run`: Godot won't loop it, and it is the first clip that viewers autoplay (S6) | `src/export/glb.ts` `toGlb`: name the clip `motion_loop` and set `extras.ts23d.loop: true` | `glb.test.ts`: `animations[0].name` matches `/loop$/` | ZERO-DEP WIN |
|
|
200
|
+
| **G8** | No provenance or copyright (S5, section 7) | `glb.ts` `toGlb`: `asset.generator` with the package version (import from `package.json` at build or a `VERSION` const in core). Add `asset.copyright` and `asset.extras.ts23d = {recipe, version, seed, lod, lodCount, up, front, units}`. Move root `extras` into it, keeping the old root `extras` one minor release for compatibility | `glb.test.ts`: `asset.copyright` is a non-empty string; `asset.extras.ts23d.front === '+Z'`; determinism test still passes | ZERO-DEP WIN |
|
|
201
|
+
| **G9** | Identical geometry not shared (S4) | `src/export/glb.ts` `toGlb`: hash each merged mesh (positions, normals, uvs, colors, indices bytes) and reuse the `meshes[]` index on a hit. Today only 4 wheel pairs match, because grime noise is seeded per part. Follow-up: let `kinds/vehicle.ts` seed grime per wheel class so all 16 road wheels share a few meshes | `glb.test.ts`: two `ctx.use` copies of the same sub-asset yield two nodes with the same `mesh` index | ZERO-DEP WIN |
|
|
202
|
+
| **G10** | 20 duplicate animation time accessors (S8) | `motion.ts` `motionChannel`: take an `inputs: Map<string, number>` cache keyed by `(D, keyCount)` from `toGlb` | `glb.test.ts`: crate with two spinning parts has `new Set(samplers.map(s=>s.input)).size === 1` | **done** · ZERO-DEP WIN |
|
|
203
|
+
| G11 | LODs are three files and engines get no switch distances (S12) | `src/cli/run.ts` + new `toGlbLodSet(compiled[])` in `src/export/glb.ts`. One GLB with LOD0 root `extensions.MSFT_lod.ids` → LOD1/LOD2 roots (named `<asset>_LOD1`, `_LOD2`, excluded from `scenes[0].nodes`) and `extras.MSFT_screencoverage` from a new `budget.screenCoverage` (default `[0.25, 0.08, 0.01]`). `extensionsUsed: ["MSFT_lod"]`, never required. Keep the per-LOD files | `glb.test.ts`: `extensionsUsed` has `MSFT_lod`; `extensionsRequired` is undefined; the `ids` length is 2; validator 0/0. Load it in Babylon in the apps gallery smoke test | ZERO-DEP WIN |
|
|
204
|
+
| G12 | No collision export (S11) | `src/core/asset.ts`: `ctx.collider(name, {shape:'box'|'convex', of?: part})`. `src/export/glb.ts` gains `GlbOptions.engine: 'generic'|'godot'|'unreal'`. Generic writes `extras.ts23d.collision` on an empty child; godot writes a `<pivot>-convcolonly` mesh node; unreal writes a `UCX_<mesh>_01` sibling mesh. CLI `--engine` | `glb.test.ts` per profile: node and mesh names match the convention, and the generic profile has no extra mesh. Manual: import a fixture into Godot 4 and UE 5 and check that a collider appears | ZERO-DEP (needs an API addition) |
|
|
205
|
+
| G13 | Single opaque material blocks foliage cutouts and lit windows (section 4) | `src/material`: add part flags `surface: 'cutout'|'emissive'`. `glb.ts` `toGlb` writes up to three materials: `ts23d_trim`; `ts23d_trim_cutout` (`alphaMode:"MASK"`, `alphaCutoff:0.5`, `doubleSided:true`); `ts23d_trim_emissive` (`emissiveFactor`, `KHR_materials_emissive_strength` in `extensionsUsed` only). `mergeToPivots` splits primitives per material. Needs an ADR 003 amendment ("exactly one material") | `glb.test.ts`: a cutout part lands in a primitive whose material has `alphaMode MASK`; with no flagged parts the GLB still has exactly 1 material | **partial**: cutout, glass, translucent shipped as `surface`; emissive missing · ZERO-DEP (ADR change) |
|
|
206
|
+
| G14 | Web profile size (N1, N2) | New optional subpath `src/export/web/` (ADR 005 "behind an optional subpath"): quantize positions (int16 plus a node dequant transform), normals (int8, stride 4) and UVs (unorm16); meshopt encode (own port of the codec or the `meshoptimizer` npm package as an optional peer); `EXT_mesh_gpu_instancing` for repeated sub-parts. All of these go in `extensionsRequired`, so never for Godot | Snapshot: `--profile web` tiger LOD0 is under 40 % of default bytes; validator 0/0; three.js gallery loads it with `setMeshoptDecoder` | **runtime dep (optional)** |
|
|
207
|
+
| G15 | Textures (N3) | With the M2 painter: PNG baseColor (sRGB) + ORM + normal, plus an optional KTX2 profile (UASTC for ORM/normal, ETC1S for albedo) with a PNG fallback `source` | Validator 0/0 with `hasTextures: true` and no `UNUSED_OBJECT` on TEXCOORD_0 | **done** (PNG, embedded or external; KTX2 profile still missing) · dep for KTX2 (optional) |
|
|
208
|
+
| G16 | Physics extensions (N6) | Wait for #2424/#2370 to ratify. Then emit from the G12 collider data in `extensionsUsed` only | Godot import test | wait |
|
|
209
|
+
|
|
210
|
+
**Combined size effect of G4 + G5 on tiger LOD0:** 2,209 KB drops to about 1,720 KB (−22 %) with no extension, no dependency and no engine risk. G9 and G10 add a little more.
|
|
211
|
+
|
|
212
|
+
### Inspection scripts (for reproducibility; not in the repo)
|
|
213
|
+
|
|
214
|
+
`validate.mjs`: `validateBytes(bytes, { maxIssues: 0 })` from `gltf-validator@2.0.0-dev.3.10`, printing `numErrors/numWarnings/numInfos/numHints`, `info`, and issue counts by code. `inspect.mjs` parses the GLB and reports index component type against vertex count, per-attribute byte totals, normal length, COLOR_0 range, duplicate geometry signatures, root rotation, node names, and animation sampler input sharing and first/last keys.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# What a GLB can carry, and what ts23d writes
|
|
2
|
+
|
|
3
|
+
**glTF 2.0 / GLB stays the output format.** It is open and royalty-free, one compact binary file, and
|
|
4
|
+
first-class in Godot, three.js and Babylon, imported by Unreal (Interchange) and Unity (glTFast). FBX is
|
|
5
|
+
proprietary (Autodesk SDK) and inconsistent across versions; USD/USDZ is built for film-style scene
|
|
6
|
+
composition and Apple AR (a later USDZ export is possible); OBJ/STL carry no hierarchy, motion or
|
|
7
|
+
materials. This page maps the whole glTF surface onto the DSL: shipped, next, later, never.
|
|
8
|
+
|
|
9
|
+
Status legend: **shipped** = on `main` with a test · **next** = the current roadmap slice ·
|
|
10
|
+
**later** = wanted, unscheduled · **no** = out of scope, with the reason.
|
|
11
|
+
|
|
12
|
+
## Core glTF 2.0
|
|
13
|
+
|
|
14
|
+
| Capability | glTF | ts23d | Status |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| Named node hierarchy, pivots | `nodes` | `ctx.part` / `ctx.group`, parent + pivot | shipped |
|
|
17
|
+
| Static batching | one mesh per pivot | `mergeToPivots` | shipped |
|
|
18
|
+
| Vertex colour albedo | `COLOR_0` (u16 normalised) | every primitive's `color` | shipped |
|
|
19
|
+
| PBR metal/rough + normal + ORM | `pbrMetallicRoughness`, `normalTexture`, `occlusionTexture` | procedural trim sheet | shipped |
|
|
20
|
+
| Alpha modes | `OPAQUE` / `MASK` / `BLEND` | surfaces opaque / cutout / glass / translucent | shipped |
|
|
21
|
+
| Node animation (TRS) | `animations` | `spin`, `slide`, `sway` → `rest` + `run` clips | shipped |
|
|
22
|
+
| Sockets, emitters | `extras` on empty nodes | `ctx.socket`, `ctx.emitter` | shipped |
|
|
23
|
+
| Skins (bones) | `skins`, `JOINTS_0` / `WEIGHTS_0`, `inverseBindMatrices` | `ctx.bone` + skinned sculpt (below) | later |
|
|
24
|
+
| Morph targets | `targets` + `weights` | face shapes: blink, jaw open, snarl | later |
|
|
25
|
+
| Cameras | `cameras` | preview / hero camera per asset | later |
|
|
26
|
+
|
|
27
|
+
### Skins: how ts23d will rig without auto-rigging
|
|
28
|
+
Heat-diffusion auto-rigging (Pinocchio, Baran & Popović 2007; Blender "bone heat"; Maya heat map) guesses
|
|
29
|
+
which bone owns which vertex, and fails on messy meshes. ts23d knows. A sculpted character is an ordered
|
|
30
|
+
list of SDF edits (Dreams' model: Evans, SIGGRAPH 2015). Tag each edit with its bone, and the per-vertex
|
|
31
|
+
smooth-min blend weights that already mix the edits' colours *are* the skin weights. Keep the strongest
|
|
32
|
+
4 per vertex (`JOINTS_0`/`WEIGHTS_0`; Unreal's Interchange importer truncates to 4) and renormalise.
|
|
33
|
+
`skin.joints` order must match `inverseBindMatrices`; that ordering is the classic broken-skin bug, so
|
|
34
|
+
a test pins it. Rigid parts (`ctx.part`) stay node-animated: a tank has no skin.
|
|
35
|
+
|
|
36
|
+
## Ratified extensions worth writing
|
|
37
|
+
|
|
38
|
+
| Extension | Use in ts23d | Status |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `KHR_materials_emissive_strength` | lit windows, lamps, sci-fi panel glow | shipped |
|
|
41
|
+
| `KHR_lights_punctual` | headlamps, street lamps, cockpit lights as light nodes | shipped |
|
|
42
|
+
| `KHR_materials_variants` | liveries / camo schemes as switchable sets in one file | later: variants swap materials, and ts23d colour is vertex colour, so a livery needs a per-asset baked albedo first |
|
|
43
|
+
| `KHR_xmp_json_ld` | licence, author, generator version, `sources` inside the file | shipped |
|
|
44
|
+
| `KHR_materials_clearcoat` | glossy vehicle paint over the base | later |
|
|
45
|
+
| `KHR_materials_transmission` + `ior` + `volume` | real glass (bottles, cockpit canopies) | later |
|
|
46
|
+
| `KHR_materials_sheen` | cloth, banners, upholstery | later |
|
|
47
|
+
| `KHR_texture_transform` | strip offset / scale without re-baking UVs | later |
|
|
48
|
+
| `EXT_mesh_gpu_instancing` | rivets, bolts, scatter trees: one mesh, N transforms | later |
|
|
49
|
+
| `KHR_mesh_quantization` + `EXT_meshopt_compression` | `web` profile (smaller files) | later (opt-in: Godot rejects *required* extensions) |
|
|
50
|
+
| `MSFT_lod` | three LODs in one file with screen-coverage hints | later |
|
|
51
|
+
| `KHR_animation_pointer` | animate material / light values (blinking lamp, heat glow) | later |
|
|
52
|
+
| `KHR_texture_basisu` | KTX2 GPU-compressed trim sheet | later |
|
|
53
|
+
| `KHR_draco_mesh_compression` | — | no: meshopt is faster to decode and preferred |
|
|
54
|
+
| `KHR_gaussian_splatting` | — | no: we generate meshes, not captures |
|
|
55
|
+
| `KHR_interactivity` | — | no: behaviour is engine code, out of scope |
|
|
56
|
+
|
|
57
|
+
## Physics (drafts: write behind a flag, plus engine conventions)
|
|
58
|
+
|
|
59
|
+
`KHR_physics_rigid_bodies` and `KHR_collision_shapes` (formerly `KHR_implicit_shapes`) are **Khronos
|
|
60
|
+
review drafts**, with importers in Blender, Godot and Babylon. ts23d will write:
|
|
61
|
+
- **Collision shapes** per part: box / sphere / capsule / cylinder fitted from the part's own geometry,
|
|
62
|
+
or a convex hull. Also as engine-convention nodes for importers without the extension:
|
|
63
|
+
`UCX_<name>` (Unreal) and `<name>-col` / `-colonly` (Godot).
|
|
64
|
+
- **Mass and centre of mass** from real volume × material density (steel 7850 kg/m³, oak 750,
|
|
65
|
+
brick 1900). A 57 t Tiger I is a QA check, not a guess: `real.mass` + tolerance.
|
|
66
|
+
- **Physics material**: friction / restitution per surface kind.
|
|
67
|
+
Status: later, behind `--physics`.
|
|
68
|
+
|
|
69
|
+
## Everything else: `extras`
|
|
70
|
+
Any JSON fits in `extras` on any object. ts23d uses `extras.ts23d` for sockets, emitters, the
|
|
71
|
+
generator version and provenance. Engines read it on import (Godot `meta`, Unity glTFast extras
|
|
72
|
+
callbacks, three.js `userData`).
|
|
73
|
+
|
|
74
|
+
## Sources
|
|
75
|
+
- Khronos extension registry: https://github.com/KhronosGroup/glTF/blob/main/extensions/README.md
|
|
76
|
+
- Skins tutorial: https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_020_Skins.html
|
|
77
|
+
- Unreal Interchange 4-influence truncation: https://forums.unrealengine.com/t/interchange-gltf-importer-truncates-skeletal-mesh-bone-influences-to-4-per-vertex/2745391
|
|
78
|
+
- glTF physics drafts: https://github.com/eoineoineoin/glTF_Physics
|
|
79
|
+
- Automatic rigging: Baran & Popović, "Automatic Rigging and Animation of 3D Characters", SIGGRAPH 2007
|
|
80
|
+
- Dreams SDF sculpting: Alex Evans, SIGGRAPH 2015, https://advances.realtimerendering.com/s2015/AlexEvans_SIGGRAPH-2015-sml.pdf
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 001 — Assets are compiled from recipes, not generated by AI mesh models
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The pipeline this project is inspired by started on AI mesh generation (Meshy plus an image model) and froze it. Measured pains:
|
|
9
|
+
- Credits per attempt; iteration multiplies cost.
|
|
10
|
+
- No reproducibility: same prompt, different mesh. Nothing to diff, nothing to regenerate in CI.
|
|
11
|
+
- One fused mesh: no named parts, no pivots, so nothing animates without manual work.
|
|
12
|
+
- A texture per asset: a draw call per asset, inconsistent texel density.
|
|
13
|
+
- Every mesh needed repair: scale, pivots, holes, topology.
|
|
14
|
+
It replaced generation with a recipe compiler, and those problems went away for hard-surface assets.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
The core path is a deterministic compiler: recipe source → mesh IR → GLB. AI enters as the **author** of the recipe (a coding agent), not as the mesh generator. Mesh-generating models are an optional, cached adapter for what recipes do badly ([idea/08](../idea/08-neural-adapters.md)).
|
|
18
|
+
|
|
19
|
+
## Alternatives considered
|
|
20
|
+
- **AI mesh generation as the core** — why not: the five pains above; outputs aren't reproducible or partitioned into named parts.
|
|
21
|
+
- **Procedural modelling in a DCC (Blender geometry nodes, Houdini)** — why not: a heavy external runtime, node graphs agents edit poorly, and no plain-text diff.
|
|
22
|
+
- **Kitbashing from asset libraries** — why not: licence per asset, mismatched styles and texel density.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
- Easier: free iteration, reproducible builds, named animatable parts, one material, CI gates.
|
|
26
|
+
- Harder: organic and sculpted forms (people, animals) are a weak spot. Answered by the planned `creature` kind and `/neural` adapter.
|
|
27
|
+
- Revisit when: an open-weight model outputs part-segmented, reproducible, low-poly meshes with consistent UVs on CPU.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 002 — Recipes are TypeScript modules, not JSON
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The predecessor pipeline used JSON recipes. They worked, then grew: repeat syntax for windows, `$ref` for shared wheels, variant tables for streets, an expression mini-language for derived sizes. Each was a worse copy of a programming-language feature, with no type checker, no editor support and a schema the agent had to learn. The authors are coding agents, and TypeScript is what they write most fluently.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
An asset is a TypeScript module exporting `defineAsset({ …, build(ctx) })`. Loops, functions, imports and types are the composition model. String mini-languages (split grammar, Whyte notation) stay as small, parsed helpers called *from* TS, where the domain notation is better than code.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **JSON / YAML recipes** — why not: every growth step re-invents loops, references and expressions, untyped.
|
|
15
|
+
- **A custom DSL with its own parser** — why not: an agent has never seen it; we'd own a language, tooling and error messages.
|
|
16
|
+
- **Python** — why not: a second runtime next to a TS/Bun toolchain and a browser gallery; recipes must also run in the browser.
|
|
17
|
+
- **Lua / sandboxed scripting** — why not: weaker typing and agent fluency, an extra interpreter.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
- Easier: composition (`ctx.use`), shared helpers, typed options with docs on hover, the same module runs in the CLI, tests and the browser gallery.
|
|
21
|
+
- Harder: a recipe is code, so it can do anything code can, including breaking determinism. Mitigated by the determinism rules and a sealed `Math.random` in tests. Running untrusted recipes means running untrusted code ([SECURITY.md](../../SECURITY.md)).
|
|
22
|
+
- Revisit when: recipes must be authored by non-programmers in a GUI. Then emit TS from the GUI; don't add a data format.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 003 — Every asset shares one trim-sheet material
|
|
2
|
+
|
|
3
|
+
Status: superseded by 008
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
Per-asset textures meant a material and a draw call per asset, texel density that varied by asset size, and a new texture for every recolour. Game scenes with hundreds of props pay for that in draw calls and memory.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
One 1024² trim sheet (albedo + normal + ORM) holds tiling strips and fixed decal cells. Every primitive maps UVs into it by world size (metres per rect). Hue comes from linear vertex colour multiplied over the greyscale sheet. Every GLB carries exactly one material, `ts23d_trim`. As of 2026-10 the material has no texture bound; the painter is M2.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Per-asset unwrapped textures** — why not: a draw call per asset, uneven texel density, needs a painter per asset.
|
|
15
|
+
- **Flat colour only, no textures** — why not: caps visual quality; no brick, grain or rivet detail.
|
|
16
|
+
- **Texture arrays / bindless per asset** — why not: engine-specific, defeats "drop the GLB into any engine".
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: a whole scene batches into one material; palette swaps are recipe edits; equal texel density everywhere.
|
|
20
|
+
- Harder: unique details (a specific logo, a painted mural) must fit a cell; a hero asset can't have its own unwrap.
|
|
21
|
+
- Revisit when: a target engine needs per-asset PBR for hero assets. Then add a second, opt-in path; keep the trim sheet the default.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 004 — Author in metres, Z up, +X front
|
|
2
|
+
|
|
3
|
+
Status: accepted; export orientation superseded by 009
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
Recipes are written from reference data: drawings, spec sheets, Wikipedia infoboxes. Those are in metres (or convertible), in plan + elevation. glTF is Y-up; engines differ (Unity Y-up left-handed, Unreal Z-up, Godot Y-up, Blender Z-up). Every asset also needs one agreed "front" so a scene can place and orient it.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
Author in metres, right-handed, **Z up, +X front**, +Y left, ground at z = 0. The GLB writer applies one −90° X rotation on the root node to meet glTF's Y-up; part nodes keep their Z-up numbers.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Y-up, matching glTF** — why not: plan coordinates become (x, z) and height becomes y, so every architectural and vehicle dimension is transposed while writing. More agent mistakes.
|
|
15
|
+
- **Centimetres or engine units** — why not: reference data is in metres; a factor of 100 is a silent class of bugs.
|
|
16
|
+
- **−Y or +Y front (Blender convention)** — why not: +X front keeps "length along X" for vehicles and ships, matching how specs list L × W × H.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: dimensions drop straight in from references; `SIDE -Y` in the contact sheet shows the profile with the front on the right.
|
|
20
|
+
- Harder: one root rotation to remember when reading raw glTF nodes. `extras.up` records it.
|
|
21
|
+
- Revisit when: never, short of a 1.0 → 2.0 break. It's in every recipe.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 005 — Zero runtime dependencies
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
Recipes run in the CLI, in tests and in the browser gallery. Output must be byte-deterministic. Each dependency adds supply-chain risk, version drift that can change output bytes, and install weight. The things we need (a glTF writer, a PNG encoder, a rasteriser, a PRNG) are each a few hundred lines.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
`dependencies` is empty. The library writes GLB, PNG (via `node:zlib`, present in Bun and Node) and rasterises on its own. Dev tooling (Biome, TypeScript, lefthook) and the gallery app's `three` + `vite` are dev or workspace-private dependencies. Optional integrations (neural adapters, engine adapters) talk over HTTP or are separate entry points, never required imports.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **`@gltf-transform/core` for export** — why not: excellent, but its output bytes move with its versions; we need to own byte determinism.
|
|
15
|
+
- **`sharp` / `canvas` for PNGs** — why not: native binaries, platform-specific installs, heavier than the problem.
|
|
16
|
+
- **`three` for geometry** — why not: pulls a renderer into a compiler, and its geometry classes aren't the IR we want.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: `bun add` installs one package; no native builds; nothing upstream changes our bytes.
|
|
20
|
+
- Harder: we maintain a GLB writer, PNG encoder and rasteriser. Each is small and tested.
|
|
21
|
+
- Revisit when: a needed format (Draco, KTX2, meshopt) costs more to own than to depend on. Then add it behind an optional subpath.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 006 — One npm package with subpath exports
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The library has ten modules (core, grammar, material, primitives, neural, kinds, qa, export, render, cli). Publishing them as separate packages means versioning them in lockstep, one npm OIDC trusted publisher per package, and a bootstrap publish for every new package (a package added after a release run stays unpublished until someone does it by hand). The sibling `ultimate` repo pays that tax across 31 packages.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
Publish one package, `@developerz.ai/ts23d`. Modules are subpaths: `@developerz.ai/ts23d/core`, `/grammar`, `/render`, and so on, via an `exports` wildcard. The root re-exports everything. Module boundaries are enforced inside the repo by `scripts/boundaries.ts`, not by package boundaries.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **A package per module (`@ts23d/core`, …)** — why not: N trusted publishers, N bootstraps, lockstep versioning machinery, for no user benefit at this size.
|
|
15
|
+
- **One package, root export only** — why not: no way to import one layer (e.g. `/render` in a tool) without the rest in view; worse tree-shaking story.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
- Easier: one version, one tag, one trusted publisher, one bootstrap ([PUBLISHING.md](../../PUBLISHING.md)).
|
|
19
|
+
- Harder: boundaries are a repo lint, not a package graph; a consumer can import any subpath.
|
|
20
|
+
- Revisit when: a module needs its own release cadence or its own heavy optional dependency (e.g. a neural adapter with a native runtime).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 007 — Agent feedback is a CPU-rendered contact sheet
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The agent loop needs to see the asset. Options are many images (one per view) or one. Each image read costs context and a turn. Proportion errors show only from some angles (height in elevation, footprint in plan). The loop must also run in CI and on headless VPSes with no GPU and no display.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
`t3d shot` renders **one** PNG: six labelled orthographic views (3/4 front, front, side, top, 3/4 rear, LOD2 far) on a shared metre grid, with a header giving bounding-box dimensions, grid step and LOD. Rendering is a z-buffered CPU rasteriser with flat Lambert shading over vertex colour: no GPU, no WebGL, no native dependency.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Headless GPU (Puppeteer + three.js, or EGL)** — why not: a browser or GPU driver in CI, non-deterministic pixels across drivers, slow cold start.
|
|
15
|
+
- **One image per view** — why not: six reads instead of one; the agent compares across turns instead of within one image.
|
|
16
|
+
- **Perspective beauty renders** — why not: perspective hides proportion errors; a metre grid under an orthographic view makes them measurable.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: one read shows every side; runs anywhere Bun runs; deterministic pixels.
|
|
20
|
+
- Harder: no textures, shadows or AO in the sheet; it judges form, not surface. The gallery shows the real material.
|
|
21
|
+
- Revisit when: surface quality (texture painter, M2) needs judging in the loop. Then add a textured view to the sheet, still on the CPU.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 008 — One trim sheet, up to four surface materials
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: 003
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
ADR 003 put every asset on exactly one material. Real assets need glass (windows, lenses), cutouts (leaves, grilles, fences) and translucent parts (bottles, liquids). One opaque material can't shade those, and per-asset materials would bring back the draw-call and texture cost 003 removed.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
Keep ONE shared texture set (the trim sheet) and vertex-colour tint. A part picks `surface: 'opaque' | 'cutout' | 'glass' | 'translucent'`; each GLB carries at most four materials, one per surface in use, differing only in shading model. Glass and translucent use `KHR_materials_transmission`, `_volume` and `_ior`, with an alpha-blend fallback. Batching splits per pivot and surface (`<pivot>_<surface>`).
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Opaque only (keep 003 as is)** — why not: no windows, foliage or bottles that read correctly.
|
|
15
|
+
- **Per-part materials** — why not: draw calls scale with parts again.
|
|
16
|
+
- **Fake glass via vertex alpha on the opaque material** — why not: engines sort and shade it wrongly; no refraction.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: windows, foliage and bottles in the same pipeline; a shampoo bottle is 2 draw calls.
|
|
20
|
+
- Harder: transmission isn't supported everywhere; the fallback looks flatter.
|
|
21
|
+
- Revisit when: an engine target needs a fifth shading model (e.g. emissive-only, subsurface).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 009 — Exported GLBs face glTF +Z
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: 004 (export orientation only; authoring stays metres, Z up, +X front)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
ADR 004 exported with one −90° X rotation on the root (`[-√½, 0, 0, √½]`). That makes glTF Y-up, but maps authoring +X front to glTF **+X**, which the glTF spec defines as the asset's *left* ("the front side of a glTF asset faces +Z, the left side faces +X", [spec §coordinate-system-and-units](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#coordinate-system-and-units)). Every asset faced sideways in engines that honour model-front. Found by [glb-production.md](../architecture/glb-production.md) (M10, G1).
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
The root node's rotation is `[-0.5, -0.5, -0.5, 0.5]`. It maps authoring +X (front) → glTF +Z, +Z (up) → +Y, +Y (left) → +X, verified numerically in the exporter tests. Authoring conventions from ADR 004 are unchanged; part nodes keep their Z-up numbers under the root.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Keep `[-√½, 0, 0, √½]`** — why not: violates the spec's front axis; assets face left in every engine that uses model-front.
|
|
15
|
+
- **Author in +Z front (or Y-up, +Z front) so no root rotation is needed** — why not: breaks every recipe's readable numbers. Length along X, height along Z is how specs and drawings read; transposing them invites the unit and axis mistakes ADR 004 exists to prevent.
|
|
16
|
+
- **Bake the rotation into every vertex** — why not: part pivots and node translations would no longer match the recipe's numbers, and engines bind to those nodes.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: assets face forward in Godot, Unity (glTFast), three.js, Babylon and Unreal without per-asset fixes.
|
|
20
|
+
- Harder: **breaking** for anyone who placed earlier GLBs by hand: rotate them −90° about Y. Raw node data is still Z-up below the root.
|
|
21
|
+
- Revisit when: never, short of a glTF spec change.
|