An APS Viewer extension that uses the new Scene API to read the geometry, materials and transforms of the loaded model(s), and download them as GLB (binary glTF 2.0) or USDZ files.
Scene API data (instances → geometry + material + world transform) is converted directly into a THREE.Scene
built with a modern, privately loaded three.js. That scene is then serialized with three.js' GLTFExporter or
USDZExporter. The viewer's own global THREE is left untouched.
- APS Viewer v7 with Scene API support. The Scene API is opt-in, so enable it before initializing the viewer:
Autodesk.Viewing.FeatureFlags.set(Autodesk.Viewing.PublicFeatureFlags.SceneAPI, true); Autodesk.Viewing.Initializer(options, () => { /* ... */ });
- The only runtime dependency is three.js (currently
0.186.0). No bundler is needed.
npm install
npm run build # TypeScript → dist/
npm test # node:test with Node's built-in TypeScript support (Node 22.18+ / 24)
npm run demo # builds, then serves the repo at http://localhost:8080Open http://localhost:8080/demo/index.html and sign in with your Autodesk account (OAuth with PKCE, hard-coded
client ID of an APS "Single-Page App"). You can then browse your Fusion and Forma (ACC, BIM 360) hubs, projects and
folders, and open the latest version of a design in the viewer. The page's exact URL has to be registered as a
callback URL of that APS app. The demo UI is built with Web Awesome components.
dist/src/index.js is plain ESM that imports three and three/addons/.... Resolve those with an import map
(or your bundler), and load the module after viewer3D.js:
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/"
}
}
</script>
<script type="module">
import { EXTENSION_ID } from './dist/src/index.js'; // registers the extension
// ... after Autodesk.Viewing.Initializer and creating the viewer:
const exporter = await viewer.loadExtension(EXTENSION_ID); // 'GeometryExporterExtension'
</script>The extension adds a toolbar button that opens an Export Geometry panel with a format choice, the options below, and a progress bar with a Cancel button. The same functionality is available programmatically:
// Download a file (all loaded models by default)
await exporter.download({ format: 'glb' });
await exporter.download({ format: 'usdz', options: { recenter: true }, filename: 'building.usdz' });
// Get the Blob plus statistics and diagnostics instead
const { blob, stats, diagnostics } = await exporter.export({ format: 'glb', models: [viewer.model] });
// Log the raw Scene API data shapes of a few instances per model (for troubleshooting)
await exporter.inspect(10);| Option | Default | Meaning |
|---|---|---|
yUp |
true |
Rotate the (Z-up) viewer world so that Y is up, as glTF and USDZ expect. |
toMeters |
true |
Scale each model from its native units to meters (model.getUnitScale()). |
recenter |
false |
Move the result so that its bounding box is centered at the origin. |
includeTextures |
true |
Export diffuse texture maps (map). |
Scene
└─ root (Y-up rotation, recentering)
└─ <model name> (unit scale; one group per model)
└─ "<object name> [<dbId>]" (one mesh / line / points object per Scene API instance,
world transform from getTransformWorld, userData { dbId, instanceId })
Geometries, materials and textures shared between instances stay shared, so a GLB uses one glTF mesh for many nodes.
| Scene API material | three.js / exported as |
|---|---|
StandardMaterial |
MeshStandardMaterial: color, opacity, side, vertex colors, map; roughness ≈ √(2 / (specularPower + 2)); metal → metalness 1 |
UnlitMaterial |
MeshBasicMaterial, which becomes KHR_materials_unlit in GLB and an emissive standard material in USDZ |
LineMaterial |
LineSegments + LineBasicMaterial (GLB only; removed from USDZ) |
PointsMaterial |
Points + PointsMaterial (GLB only; removed from USDZ) |
The export never aborts on bad data. When the Scene API returns something unexpected, the exporter reports it to the browser console and then repairs the data, drops it, or skips the instance:
[GeometryExporter] index-out-of-range {model, instanceId, dbId, name} {droppedPrimitives, firstBadIndex, …, raw}
- The first 5 occurrences of each category are logged in full.
rawholds the original Scene API object, so it can be expanded in DevTools. Further occurrences are only counted. - Each export ends with one collapsed
[GeometryExporter] … export finishedgroup. It contains a statistics table (instances, exported objects, unique geometries/materials/textures, triangles, vertices), a table of occurrences per category, and the list of all entries. - Categories:
no-geometry,no-material,no-position-reader,vertex-count-zero,non-finite-position,non-finite-normal,non-finite-matrix,missing-normals,no-normal-reader,index-out-of-range,index-count-not-multiple,vertex-count-not-multiple,unknown-material-type,legacy-material,unsupported-material-feature,unexpected-color-value,unexpected-scalar-value(including non-unit normals),uv-undecodable,color-undecodable,unexpected-attribute-layout,unknown-texture-image-type,texture-draw-failed,unexpected-side-value,unexpected-wrap-value,object-tree-unavailable,unsupported-in-format,exception(a Scene API getter threw).
Checked against viewer 7.126.0 using dynamic models built with GeometryFactory (headless Chrome):
- ✅
Scene.Sideis{ Front: 0, Back: 1, Double: 2 },Scene.Wrappinguses the three.js constant values (1000–1002), andTexture.getImage()returns the sourceHTMLCanvasElement. - ✅ Committed geometry is interleaved (stride 6 floats): position (3 floats), normal (2 × uint16 normalized),
and uv (2 floats). UVs decode correctly from
getAttribute('uv'). ⚠️ After commit,getNormalReader()returnsnullandgetNormal()returnsnull, althoughhasAttribute('normal')is true. The exporter then decodes the packed normal attribute itself (spherical encoding: θ = (2u − 1)·π, z = 2v − 1; max error on a sphere is about 1e-4) and reportsno-normal-readerfor each affected geometry. If decoding isn't possible, normals are recomputed from the triangles.
Checked against a translated Revit model (rac_basic_sample_project, 733 instances):
⚠️ getMaterial()returns the viewer's internal materials, not Scene API classes: plain (legacy three.js)MeshPhongMaterialandLineBasicMaterialobjects withoutgetColor()/getOpacity()/… getters. The exporter recognizes these by theirtype(MeshPhongMaterial,MeshLambertMaterial,MeshBasicMaterial,LineBasicMaterial,LineDashedMaterial,PointCloudMaterial), reads their properties directly (color,opacity,shininess,metal,map,side, …), and reportslegacy-materialfor each one.⚠️ The same missing normal reader as above (no-normal-reader) applies to every triangle geometry.- ✅ Instance ids
0 … getCount() - 1cover all instances. Line geometry is identified correctly through its (LineBasicMaterial) material.
Checked against a translated Fusion model (wall-e.f3d, 182 instances):
-
⚠️ getMaterial()returns internal PRISM materials (type: 'PrismMaterial'), whose appearance lives inprismType-specific properties with linear colors (see the viewer'sMaterialConverterPrism). They are approximated withMeshStandardMaterial:prismTypeBase color Other PrismOpaqueopaque_albedoroughness surface_roughnessPrismMetalmetal_f0metalness 1, roughness surface_roughnessPrismLayeredlayered_diffuseroughness layered_roughness; the clear coat is droppedPrismTransparenttransparent_coloropacity derived from the transmission color PrismGlazingglazing_transmission_coloropacity derived from the transmission color, glazing_transmission_roughnessPrismWoodwood_early_colorthe procedural wood pattern is dropped Anything that isn't carried over (PRISM texture maps, emission, clear coat, refraction, procedural wood) is reported as
unsupported-material-feature, listing the affected features and map names.
Still assumed (reported when violated):
- Other file formats (Inventor, Navisworks, IFC, …) use the same internal material types. Materials of any other
type are exported with a grey fallback and reported as
unknown-material-type, including theirtypeandis*flags. - Packed 1- or 2-byte interleaved uv/color components are decoded as unsigned normalized integers and flagged
with
unexpected-attribute-layout. - Colors (
Scene.Color, 0..1) are treated as sRGB. getTransformWorld()is taken as-is. The viewer's global offset is not added back; userecenterif needed.
exporter.inspect() shows what the API actually returns for any model.
- Exporting subsets: current selection, visible or isolated objects, or explicit dbIds (with hidden and ghosted instances skipped).
- OBJ (+MTL, textures) output.
- Theming colors and model edges (
getEdgeIndices()). - Choosing individual models in a multi-model scene from the panel (possible today through the API's
modelsoption). - Normal, bump, specular and alpha maps. Only the diffuse
mapis exported, and PRISM texture maps aren't exported yet.
src/
index.ts entry point; registers the extension, re-exports the API
GeometryExporterExtension.ts viewer extension (toolbar button, panel, export/download/inspect)
exportModels.ts extract + write orchestration (no viewer UI dependencies)
diagnostics.ts unexpected-data collector and console reporting
inspect.ts raw Scene API data dump for troubleshooting
extract/extractScene.ts Scene API instances → THREE.Scene
extract/convertGeometry.ts Scene BufferGeometry → THREE.BufferGeometry (decoding + validation)
extract/convertMaterial.ts Scene materials/textures → three.js materials/textures
writers/glb.ts, usdz.ts three.js exporters
ui/ExportPanel.ts DockingPanel UI
types/scene-api.d.ts hand-written declarations for the used viewer / Scene API surface
demo/ static demo page (Autodesk sign-in, Data Management browser, viewer)
test/ node:test suites with Scene API fakes