# Construct 1.0: an agent writes a scene, a stdlib exporter writes the glTF, and nobody checks the hinges

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/construct-3d-objects
> date: 2026-11-01
> tags: 3d, agents, gltf, blender, agentic-coding, tools

A post by @cutetoxicguy described Construct 1.0 as an open-source framework in which AI agents research real-world objects and turn them into interactive 3D models with working parts, ready for Blender and game engines. The repository is [rtmandreyev/construct](https://github.com/rtmandreyev/construct), by Artyom Andreyev, with a [gallery site](https://rtmandreyev.github.io/construct/). I cloned it at commit `2a12a11` (dated 28 October 2026) and read it.

<RepoCard repo="rtmandreyev/construct" />

The headline sentence is accurate about the output and misleading about where the intelligence sits. "AI agents research" is the host agent's job, outside the repository. What the repository contains is a contract, a validator, a compiler and an exporter, all deterministic. I want to separate those, because they fail in different ways.

Labels: **measured** is something I computed from a file in the repository with my own script, **reported** is the author's statement that I did not re-run, **reasoned** is my reading of the code or arithmetic on measured values.

## What I could and could not run

The brief for this piece allowed running downloaded code. My first attempt, `python3 -m construct_hologram --help` inside the clone, was refused by the sandbox's permission layer, which classified it as running code from an external source. I did not try another route to the same command. So:

- **I did not run** `construct check`, `build`, `export`, the 254 test functions, or Blender. I produced no `.glb`. There is no Blender binary on this machine: `~/.cache/blender` holds only a shader cache.
- **I did** read the exporter (790 lines), the scene validator, the audit and the runner, and parse all 25 `examples/*/scene.json` files plus the six scenes under `skills/construct/assets/examples/` with a script of my own (Python 3, standard library, isolated mode).
- **Every structural claim about an exported file below is reasoned from `export.py`**, not read from a file the exporter wrote. Where the author reports a result I did not reproduce, I say so.

## The pipeline, and which stage needs a model

The README's flow is: you ask, the agent looks the thing up, writes a scene, Construct checks it, builds `construct.html`, the agent reviews pictures of it, and in "masterpiece" mode a separate critic model compares rounds. Export is a separate command. Reading the code, the stages divide like this.

| Stage | Who does it | Needs a model or key? | My status |
|---|---|---|---|
| Research (facts, reference photos) | The host agent. The bundled runner "has no web search"; photos come from `--reference` | Yes, the host's | Not in the repo |
| Write the scene (JSON) | A model, or you, by hand | Yes, unless by hand | Read the schema |
| Check and audit | `scene.py` validation, `audit.py` | No | Read, not run |
| Compile to geometry, build `construct.html` | `scene.py`, `hologram.py` | No | Read, not run |
| Visual review, critic rounds | A vision model, a headless Chromium | Yes | Not run |
| Export `.glb` or `.obj` | `export.py` | No | Read, not run |
| Blender add-on | Blender 4.2+ | No | No Blender |

Three facts hold the picture together. The package has no dependencies (`dependencies = []` in `pyproject.toml`), and the NOTICE says the same of third-party software. The only outbound network code I found in the package is a localhost WebSocket to a browser for captures and one `urllib` POST in `runner.py`, to an OpenAI-compatible `/chat/completions` URL you supply, with the key read from an environment variable you name. And the model never emits pixels or graphics code: the scene is data, so everything after "write the scene" can be reproduced by anyone with Python, no key needed. That is the design decision I would credit.

The repository is also an Apache-2.0 project from 1.0 on (earlier versions were MIT), with the NOTICE file required to travel with copies. The Blender add-on is GPL-3.0-or-later, as the changelog says Blender's extension platform requires, which is why the frontmatter says `mixed`.

## What a scene is

A scene is a JSON document with `format: "construct-scene"`, `version: 2`. Its top-level pieces are `sources`, `facts`, `parts`, `labels`, `animation`, `articulations` and a `presentation` block. Units are metres, Y up, the front faces +Z.

Each fact has a key, a value and a certainty, one of `verified`, `user_supplied`, `inferred` or `decorative`. The contract says a `verified` fact must cite a source id, and that an unknown fact is absent, never null. Each part carries an `id`, a `role` (`mass`, `identity`, `detail`, `ornament`, `artwork`, `structure` and so on), a `certainty`, a `stage` (the assembly order), a shape from a fixed vocabulary (box, cylinder, lathe, loft, extrude, tube, curve and so on), a position, and optionally `repeat` and `mirror`.

Joints are a separate top-level list. An articulation is `{id, type, parts[], axis, pivot, amount, order}` with `type` one of `hinge`, `slide`, `lift`, `turn`. The validator (`_articulation_check` in `scene.py`) is strict about shape and range: a hinge is within plus or minus 180 degrees, a turn within plus or minus 1,080, a slide or lift has no pivot, each id is unique, and every named part must exist. **Measured:** the shipped scenes obey it, 20 articulations across them.

The compiler's rule for composition is in `_chain_apply` and mirrored in `export.py`: a part with several articulations gets them in `order`, and the first in order is outermost, so a later one rides on the earlier. That is not a skeleton. There is no skinning, no inverse kinematics, no constraint solver. `EXPORT.md` says so itself: "Joints are mechanical (hinges, slides); there is no skinned rig."

## What the exporter writes

Reading `export.py`, the glTF has this shape.

- A root node, then one node per assembly stage (`stage-0`, `stage-1`, ...), then one node per part named with the part's id. The part's role, certainty and source go in `extras.construct`.
- A part with no articulation and no periodic motion hangs a mesh directly on its node. A part with an articulation gets a chain of pivot nodes below it, one per articulation, named `<part>:<articulation>`, with the pivot as the node's translation, and a final `<part>:mesh` node whose translation is the negated pivot. That offset is what makes the mesh rotate about the hinge rather than about its own origin.
- Hinges and turns animate as quaternion rotation on the pivot node; slides and lifts animate translation.
- `GROUP_SECONDS = 1.5`. The clip named `open` runs one 1.5 s group per distinct `order`, in order. If a scene has more than one articulation, it also writes `open:<id>` for each. Spin, swing and bob motions become a looping `idle` clip. Each vertex gets a `_PHASE` attribute (its build time, 0 to 1).

**Reasoned prediction, not an observation.** For the Yamaha scene below, the file should hold a 3.0 s `open` clip (orders 0 and 1), plus `open:lid-open` and `open:prop-rise`, each 1.5 s, and no `idle`, since neither part has a periodic motion. I could not open a file to confirm it.

### How a hinge becomes keyframes

One detail is worth checking because it decides whether the animation is smooth. `export.py` samples every articulation into `n` keyframes, with `n = max(5, int(|amount| / 15) + 2)` for a hinge or a turn and 5 for a slide or a lift. Times are even over the 1.5 s group, and the value is eased by `u * u * (3 - 2u)`. glTF plays a linear channel as a straight line between keys (a slerp for a rotation). So the file holds a polygonal approximation of the smoothstep, not the smoothstep.

I reimplemented that rule and measured the worst gap between the playback curve and the true ease. This is the sampling rule, not a measurement of a produced file.

<KeyframeError />

**Measured on the rule:** a 42 degree hinge (the Yamaha lid) has 5 keys and strays at most 1.48 degrees from the eased curve; the 85 degree FJ62 tailgate has 7 keys and strays 1.48 degrees; the 108 degree CD lid has 9 keys and 1.11 degrees; the 360 degree disc turn has 26 keys and 0.42 degrees. A 0.155 m slide, which always gets 5 keys, strays 5.5 mm. None of this is visible at playback speed, and the end poses are exact because the first and last keys are exactly 0 and 1. It is a fair design: the cost is a bounded wobble in the middle of a move, in return for a file with a handful of keys.

## Where the joints are

An "interactive model with working parts" depends on how many parts have joints at all.

**Measured, 25 scene files in `examples/`:** 7 have any articulation, for 20 in total: 4 hinges, 4 turns, 6 slides and 6 lifts. The scenes are the Aston Martin DB5 (a door, 58 degrees), the Jackson Juggernaut ET7 guitar (four: two knob turns, a push-pull, a selector hinge), the Leica M3 (a lens slide of 0.014 m and a 12 degree lock turn), the Omega Speedmaster (nine), the tea house (one sliding shoji), the Land Cruiser FJ62 (a tailgate, 85 degrees) and the Yamaha CFX (lid and prop). The other 18 scenes do not move except by a looping `idle`.

The Omega's nine are not mechanisms. They are `lift` and `slide` pairs that separate crystal, bezel, hands, dial and movement into layers: an exploded view, 53 of its 64 parts moving. The FJ62's tailgate moves 8 parts of 214, which is 3.7%.

So "working parts" in the shipped examples means: a door, a lid, a knob, a lens barrel, an exploded stack. Each is a rigid move of a named group about an axis the author wrote down. The CD jewel-case example in the bundled assets (`cd.json`, 6 parts, 5 articulations) is the cleanest: lid hinge 108 degrees, booklet lifted 0.045 m and slid 0.155 m, disc lifted 0.03 m and turned 360 degrees.

I checked that one by hand. The lid is a box from z = 0.001 to 0.0052 with its hinge on its left edge at z = 0.0052. Swung 108 degrees about the pivot, its corners span x from -0.115 to -0.067 and z from 0.0052 to 0.1415. It never drops below the hinge height, so it does not cut into the tray (top at z = 0.001). The booklet, after the slide, covers x from 0.10 to 0.22 m against a case edge at 0.071 m, 29 mm clear. **Measured** from the JSON with my own rotation code.

## A contact nobody tests

The Yamaha CFX scene has two articulations. `lid-open` is a hinge of 42 degrees about the +Z axis through (-0.8, 1.008, 0), order 0, moving the lid. `prop-rise` is a turn of -69 degrees about +Z through (0.55, 0.98, 0.45), order 1, moving the prop stick. The prop is a tube 0.99 m long that rests pointing in -X. Both are part of the same piano and are meant to meet, but the format has no way to say so. Each articulation lists its own parts, and the prop does not ride on the lid.

The lid is an extrusion that starts at the hinge height and rises 0.022 m (I read `shapes.extrude`: the profile is placed at height 0 and the top at `height`). So its underside is the plane through the hinge. I rotated the prop's free end with the right-hand rule the code uses and measured how far it lands from that plane.

<PropAndLid />

**Measured:** at the scene's 42 and -69 degrees the prop tip sits at (0.195, 1.904) and is **0.11 mm** from the lid's underside, and 1.34 m along the lid from the hinge, inside its 1.594 m length. The exact solutions are -69.018 degrees for the prop at lid 42, or 42.005 for the lid at prop -69. One degree too little on the prop leaves 6.2 mm of air. One degree of lid in either direction moves the contact by about 23 mm: at 41 the stick pokes 23.5 mm through the lid, at 43 it is 23.3 mm short.

That is good work by whoever wrote the scene. The author, or a model with a rendering loop, tuned two independent numbers until they met. But the format then forgets it, because nothing records that these two articulations are coupled, and the audit does not look. I read `audit.py` for every check it can raise. The error and warning codes are `floating`, `stage-order`, `overclaim`, `accuracy`, `data-lock`, and a set of warnings (`no-mass`, `no-identity`, `tube-outline`, `bare-surface`, `long-assembly`, `clutter`, `duplicates`, `tiny`, `invisible`, `no-surface`, `thin`, `detached`). The only one that touches articulations is `no-open-view`, which warns when a scene opens but has no view named `open`. **Reasoned:** nothing in the check stage tests an opened pose for clearance, penetration or a missing contact. `floating` is computed from rest-pose bounding boxes. Change the lid to 40 degrees and the file is still valid, still exports, still plays, and the prop goes through the lid.

The author's tests are described as replaying the exported file's animations in a small glTF evaluator and checking "every vertex of an opened joint and a spinning part lands where the compiler puts it, to a fraction of a millimetre". **Reported; I did not run them.** That is a consistency test: the file matches the compiler. It cannot know whether the compiler's pose is the right one.

## What the scenes claim about themselves

The README leads with honesty: "Every fact and part says whether it is verified, supplied by you, inferred or decorative." That is true of the files. The more useful question is what they say.

<CertaintyLedger />

**Measured:** across the 25 scenes, 87 of 2,916 parts (3.0%) are `verified`, 23 are `decorative` and the other 2,806 are `inferred`. Of 517 facts, 319 (61.7%) are `verified`. Eighteen scenes have no verified part at all. The Toyota FJ62's 214 parts are all inferred, backed by 8 verified facts out of 9. The seven that have any are three of the NetLogo runs, Saturn with 54 of 127, the Revox with 15 of 147, the Yamaha with 4 of 31, and one part of the MiniFreak.

That is the sensible way round. A web search can verify that an FJ62 is some length, but it cannot verify a profile of 214 shapes. The format admits this, and `audit.py` has teeth around it. An `overclaim` error fires when a part is `verified` or `user_supplied` but its basis facts top out lower and it cites no source. A `shape-overclaim` warning fires when a part is verified with six or more freeform points but no cited source gives shape (a spec page verifies dimensions, not an outline). An `accuracy` error fires when `subject.accuracy` is "documented" yet half the mass and identity parts are inferred. The four scenes labelled documented are Saturn and three NetLogo runs.

So the provenance layer does its job as bookkeeping. It stops a model from calling a guess a fact by omission. It does not make a guess correct, and it does not make the geometry measured: the Yamaha prop is a `verified` part whose rest direction and angle are a design choice.

## What I would trust, and what I would test

I would trust the format and the deterministic stages. A stdlib-only compiler with a published schema, a validator that reports errors with fixes, and an exporter whose output is reported to pass the Khronos glTF validator (eleven files, no errors or warnings, validator 2.0.0-dev.3.10, **reported**) is a sound base for agent-made props. The claim that an LLM never touches pixels matters: it makes the scene diffable, reviewable and re-exportable without a key.

I would not trust three things without running them.

1. **The numbers in `EXPORT.md`** (about 21,000 triangles in 27 draw calls for the merged FJ62, from about 300 unmerged; about 4,500 triangles for the MiniFreak). **Reported.** The shape of the claim is plausible from the merge code, but I cannot confirm the counts.
2. **The model results.** The README says the example constructs were made by Claude Opus 5.5, GPT-6.1 Sol or DeepSeek V4.1 Flash, and that in blind comparisons on two subjects there was "no visible improvement after about round 12". **Reported**, and unverifiable from the repository alone: the scenes carry an `authoring` block for only some of them (Saturn's records four rounds, Sainte-Chapelle's fifteen), and a critic's preference is not a measurement of a building.
3. **Blender and engine behaviour.** The author reports Blender 5.2 and Godot 4.7.2 importing the files correctly. I had neither.

If you want an object with a lid that opens, the useful workflow is the unglamorous one. Have a model write the scene, run `construct check`, export with `--merge --wire seams` for a game, open the pose in your engine, and look at every pair of parts that should touch. The repository will not look for you.

## What I did not do

I did not run any Construct command, so I cannot say whether the examples compile on a clean machine, how long the stages take, or what the `.glb` files weigh. I did not run an author loop against any model, so the claim that a fast-mode scene reaches a clean check in a few rounds is untested here. I did not open the HTML viewer or the Blender add-on. The sandbox's refusal was the reason for the first, and the remainder followed from it.

The numbers I do stand behind came from parsing the shipped JSON and reading the code: the stage split, the 20 articulations in 7 scenes, the 87 of 2,916 verified parts, the keyframe rule, and the prop's 0.11 mm.
