~/satyajit

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

mdjsonmcp

2026-11-01 · 15 min · 3d · agents · gltf · blender · agentic-coding · tools

Why read this

Solidtop 85%

Splits Construct into model and plain-Python stages, tallies what 25 scenes claim, and checks a hinge pair by hand. Exporter not run.

  • Original analysis
  • Runs on a laptop CPU
  • Explained from first principles

3D & spatialMixed licencesPractitioner tool

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
1 of 3: General advice
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 54 of 100, ranked 509 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

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, by Artyom Andreyev, with a gallery site. I cloned it at commit 2a12a11 (dated 28 October 2026) and read it.

rtmandreyev/construct@2a12a11 · snapshot 2026-11-01
tracked files
364
license
Apache-2.0
branch
main
tests
8 files
source
2.0 MB
commit date
2026-10-28
source by language
Python1.6 MB(73)JavaScript270.6 kB(8)HTML94.8 kB(4)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-11-01 at 2a12a11 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

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:

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.

StageWho does itNeeds a model or key?My status
Research (facts, reference photos)The host agent. The bundled runner "has no web search"; photos come from --referenceYes, the host'sNot in the repo
Write the scene (JSON)A model, or you, by handYes, unless by handRead the schema
Check and auditscene.py validation, audit.pyNoRead, not run
Compile to geometry, build construct.htmlscene.py, hologram.pyNoRead, not run
Visual review, critic roundsA vision model, a headless ChromiumYesNot run
Export .glb or .objexport.pyNoRead, not run
Blender add-onBlender 4.2+NoNo 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.

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.

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.

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.

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.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Construct 1.0: an agent writes a scene, a stdlib exporter writes the glTF, and nobody checks the hinges", ai.thesatyajit.com, November 2026.

bibtex
@misc{ghana2026construct3dobjects,
  author = {Satyajit Ghana},
  title  = {Construct 1.0: an agent writes a scene, a stdlib exporter writes the glTF, and nobody checks the hinges},
  url    = {https://ai.thesatyajit.com/articles/construct-3d-objects},
  year   = {2026}
}
share