Native FreeCAD assemblies
schemaxis.freecad.assembly/1 creates an editable built-in FreeCAD assembly. It uses
Assembly::AssemblyObject, Assembly::JointGroup, the installed
JointObject.GroundedJoint and JointObject.Joint implementations, and the
compiled assembly.solve() API. It does not approximate an assembly with a
compound, fuse, manually assigned final placements or authored Python proxies.
FreeCAD 1.1.x is the qualified runtime family; qualification currently uses the
installed 1.1.3 build.
The standalone adaptor lives in schemaxis.adaptors.freecad.assembly and exposes
plan(document), schema(), run(document, directory, *, executable=None, timeout=120, gui_executable=None) and verify(directory, require_result=True).
Planning, schema and verification do not import native application modules.
run requires a fresh output directory. executable is an absolute freecadcmd
launcher (or FREECAD_CMD); gui_executable optionally supplies the GUI launcher.
The timeout applies separately to each native process. Cancellation terminates
the process owned by that stage.
CLI and console#
The CLI recognises the assembly document version for planning, execution, verification and provider queries. From the repository root:
PYTHONPATH=src python -m schemaxis plan examples/schemaxis/freecad-assembly.json
PYTHONPATH=src python -m schemaxis query examples/schemaxis/freecad-assembly.json
PYTHONPATH=src python -m schemaxis schema --provider freecad --section assembly
PYTHONPATH=src python -m schemaxis run examples/schemaxis/freecad-assembly.json \
--output-directory evidence/assembly-cli-example \
--freecad /path/to/freecadcmd \
--freecad-gui /path/to/freecad
PYTHONPATH=src python -m schemaxis verify evidence/assembly-cli-example
Choose a fresh output directory for run. --freecad-gui is optional: it adds
the native joint task editors and image evidence before independent reopening.
Omitting it performs command-line native execution and verification. Assembly
node inspection remains unsupported. The common runtime
adds finite analytic visits and scheduled workflow participation, including resume.
The FreeCAD console opens assembly JSON and supports complete document-field
edits, undo/redo, configured batch previews and saving verified .FCStd output:
PYTHONPATH=src python -m schemaxis console --provider freecad \
--workspace .schemaxis-assembly \
--freecad /path/to/freecadcmd
Then enter these console commands:
open examples/schemaxis/freecad-assembly.json
set document dimensions '{"plateThickness":7,"postHeight":20}'
list
preview
save assembly-edited.FCStd
save assembly-edited.json
set document FIELD JSON replaces a complete field, including parts,
components, grounded, joints or authoring containers; the candidate is
validated before committing. list reports component instances. preview
uses the configured batch assembly runner; without an executable it only stores
the authored input and plan. Saving .FCStd requires a successful verified
checkpoint for the current revision; saving JSON retains the authored assembly.
The console's batch configuration takes freecadcmd; the optional GUI decoration
flag shown above belongs to CLI run, not the console command.
Live assembly execution is explicitly unsupported. Do not use live for an
assembly; unload an existing live client before a configured batch preview.
Node modelling commands such as add or node-level set do not apply to assembly
documents. See the console guide for session and checkpoint rules.
Declared parts, instances and joints#
See the complete example. Its two
primitive part definitions are instantiated as four reusable App::Link
components: two plates and two posts. Three fixed joints connect them to one
grounded component.
{
"version": "schemaxis.freecad.assembly/1",
"parts": [{"id": "plate", "type": "box", "options": {"length": 40, "width": 30, "height": 5}}],
"components": [{"id": "base", "part": "plate"}, {"id": "lid", "part": "plate"}],
"grounded": "base",
"joints": [{"id": "mount", "type": "fixed",
"parent": {"component": "base", "face": "top"},
"child": {"component": "lid", "face": "bottom"}, "offset": [0, 0, 20]}]
}
parts: 1–32 typed definitions. Aboxacceptslength,width,height; acylinderacceptsradius,height. Dimensions are 0.01–10000 mm.components: 2–32 instances with unique IDs and a declaredpartreference. Optionalpositionis XYZ in mm;rotationis yaw about +Z in degrees, limited to ±360. Non-ground placements are solver starting guesses. The grounded component's placement is the assembly's world anchor.grounded: exactly one component ID, represented by a real native grounded joint.joints: exactly one fixed parent joint for every other component. Joints can be authored in any order; planning orders them from the root. Cycles, duplicate child constraints, disconnected components and grounding a child are rejected.- Joint
parent/childendpoints selecttoporbottom, the planar cap at local Z=height or Z=0. Native face IDs are resolved from cap geometry and checked on reopening; authored input does not assumeFaceNordering. offsetis an XYZ offset in the parent's local connector frame, bounded to ±100000 mm per coordinate. The fixed joint makes the two connector frames coincide. For these caps both connector frames have local +Z; the contract does not use outward face normals to flip a component.unitsdefaults tomm; no other units are accepted. Pure document references/functions indimensions,definitions,settingsand the declaration resolve throughschemaxis.documents.resolve_document.
Changing one native source part updates every linked instance. Joint references
remain actual native subelement references, and native joint attachment offsets
are editable in FreeCAD. With gui_executable, the saved FCStd also includes the
installed ground/joint view providers and native joint task editors, plus a
rendered image and full application-window screenshot as auxiliary evidence.
Without that optional stage the model, links, joint properties and solver are
editable, but this adaptor does not claim the GUI task-editor setup.
Assembling modelled parts at named mounts#
Instead of primitive constructors, every part of an assembly may name a verified
PartDesign part publication. The assembly then contains
one editable copy of each part's complete history (its design parameters,
sketches, Pad/Pocket features and expressions), repeated App::Link instances,
named mounting frames and real fixed joints. The two forms never mix in one
document: either every part has type/options or every part has
source/mounts.
{
"version": "schemaxis.freecad.assembly/1",
"parts": [
{"id": "plate", "source": {"$artifact": "plate", "index": 2}, "mounts": {
"top": {"position": [0, 0, {"terms": {"thickness": 1}}]},
"side": {"position": [{"terms": {"width": 0.5}}, 0, {"terms": {"thickness": 0.5}}],
"z": "+X", "x": "-Z"}}},
{"id": "spacer", "source": {"$artifact": "spacer", "index": 2}, "mounts": {
"bottom": {}, "top": {"position": [0, 0, {"terms": {"thickness": 1}}]}}}
],
"components": [
{"id": "base", "part": "plate", "rotation": 30},
{"id": "spacer", "part": "spacer"},
{"id": "cover", "part": "plate"}
],
"grounded": "base",
"joints": [
{"id": "baseToSpacer", "type": "fixed",
"parent": {"component": "base", "mount": "top"},
"child": {"component": "spacer", "mount": "bottom"},
"offset": [5, 0, 0], "clocking": 90},
{"id": "spacerToCover", "type": "fixed",
"parent": {"component": "spacer", "mount": "top"},
"child": {"component": "cover", "mount": "side"}, "offset": [0, 0, 2]}
]
}
See the complete workflow, which builds both parts and then the assembly.
source: themodel.FCStdpublication of a verifiedschemaxis.freecad/1run with exactly one PartDesign Body whose solid tip is the output. Inside a workflow use{"$artifact": "<part operation>", "index": 2}; the part route publishes STEP (0), STL (1) and FCStd (2). Standalone, give the file path. The planner reads that run's own receipts and refuses symbolic links, other routes, legacy Part-route outputs, design studies, units other than mm, any other publication index, unbounded or malformed archives and bytes that differ from the receipt (UNSUPPORTED_SOURCE,INPUT_CHANGED). It also refuses a part whose values the saved document cannot store exactly: a parameter or literal with more than 16 decimal places, or a binding coefficient or offset with more than 15 significant digits. FreeCAD built that part's stored geometry from the unrounded value, so the assembly's recompute would produce different geometry. Re-run the part with representable values.mounts: 1–16 named part-local frames.position(default[0, 0, 0]) has three entries, each a number in mm or a binding to that part's own parameters,{"terms": {"thickness": 1}, "offset": 0}, with the parameter limits. A bound coordinate becomes a native expression on the mount, so editing the part's parameter in FreeCAD moves the mount and everything attached to it.zandxchoose the mount's axes from+X -X +Y -Y +Z -Z(defaults+Zand+X;xis required whenzis not ±Z and must be perpendicular to it);ycompletes a right-handed frame. Mount IDs must differ from the part's own feature names.componentsandgroundedare as for primitive parts. Component and joint IDs must not reuse FreeCAD helper labels such asOrigin,X-axisorGround.- Joint endpoints name a
mount.offset(mm) is a translation in the parent mount frame andclocking(degrees, ±360, default 0) turns about the parent mount's +Z after that translation. Both are written to the parent connector (Offset1); the child connector (Offset2) stays identity.
In Studio. The generic part form offers three kinds: box, cylinder and
Modelled part. The form's source field is a plain path. A workflow source,
{"$artifact": "<part operation>", "index": 2}, has to be written in the JSON
editor. Named mounts and joint parent/child endpoints are edited as JSON
values; joint endpoints offer Face endpoint or Mount endpoint. Studio has no
dedicated mount editor or sourced-assembly template. Begin from the
workflow example.
The host planner remains the authority for every sourced rule.
Frame convention. A fixed joint makes the child mount frame coincide with the parent mount frame:
worldChild = worldParent · parentMount · T(offset) · Rz(clocking) · inverse(childMount)
Mounts are frames, not surface normals. To seat a part on another, give both mounts the same +Z. To hang or flip a part, choose opposing axes deliberately:
seated (both +Z up) flipped (parent mount -Z)
┌─────────┐ child ┌─────────┐ parent
└────▲────┘ bottom: z=+Z └────┬────┘ under: z=-Z, x=+X
┌────▲────┐ top: z=+Z ┌────▼────┐ top: z=+Z on the child,
└─────────┘ parent └─────────┘ child hangs below, upside down
There is no automatic opposing-normal rule.
In the FreeCAD GUI. The joint task panel's simple Offset and Rotation
fields edit the child connector (Offset2). The authored offset and clocking
appear under Show advanced offsets as the first connector (Offset1). A GUI edit
of the simple fields therefore composes as inverse(childMount · Offset2). Manual
GUI edits are never synchronized back into the authored JSON.
Closed source envelope. Before any FreeCAD process can open a source, the host inspects the saved file. This happens at planning, on the bytes about to be copied, on the retained copy immediately before the first native stage, and again during verification. Only the form that the qualified PartDesign route writes is admitted:
- the fixed FreeCAD XML prolog, with no DOCTYPE, entity or extra declaration;
- the closed element vocabulary of that route;
- for each object type, exactly its persistent and transient properties and
extensions; the only dynamic properties allowed are the declared
dim_<parameter>lengths; - references and expressions that stay inside the source, naming whole objects only (no face, edge or vertex sub-element references);
- archive members that are exactly
Document.xml, the minimal view record, the string table and the payloads the document names; - syntactically valid BREP, element-map and string-table payloads.
Python objects, external file links, unknown properties, extensions or types,
extra, missing, duplicate or escaping members, and corrupt payloads are refused
with UNSUPPORTED_SOURCE (or INPUT_CHANGED for changed bytes) before FreeCAD
starts. Each source is limited to 64 MiB stored and 512 MiB expanded. All
sources of one assembly together are limited to 128 MiB stored and 512 MiB
expanded (ASSEMBLY_LIMIT).
What execution does. run verifies each producer again, copies its FCStd
byte-for-byte into sources/, and runs three separate native processes. For a
sourced assembly the timeout bounds the whole run. The capture process opens each
retained copy alone, records every stored history shape, recomputes the feature
history and compares each recomputed shape with the stored one. The comparison
covers validity, topology counts, every vertex position (matched one-to-one),
each face's surface type and area, each edge's curve type and length, volume,
area, bounds and centre of mass. Numbers may differ by at most 64 units in the
last place at the part's size, which is floating-point noise. A shape that
passes is consistent with its history, but this is not a proof of identical
geometry. A substituted or inconsistent stored shape, such as a centred slot
turned a quarter turn that keeps every summary measure, fails before anything is
built. A
capture process measures each copy alone. A build process embeds each copy
(Document.copyObject of the complete document), adds the mounts and joints,
solves, perturbs every ungrounded component, solves again and saves. A reopen
process inspects the saved state before any recompute or solve, because a
recompute alone re-solves the assembly. The whole-run timeout and cancellation
are checked again after final verification, so a run that overruns or is
cancelled there is reported as RUNNER_TIMED_OUT or RUNNER_CANCELLED, not as
success. FreeCAD renames the second copy of
colliding names (N0 becomes N001, SchemaxisParameters becomes
SchemaxisParameters001); the adaptor records that map and never assumes it.
The saved file also carries a minimal first-open view state: links, the
assembly, the ground and each source tip are visible, and the installed joint
view providers are named so the joint editor opens without a GUI stage. The
optional GUI capture stage (--freecad-gui) is refused for sourced assemblies
(UNSUPPORTED_STAGE) because it would replace that verified state.
What verification checks. In addition to the primitive checks below:
- Every embedded source is the producer's saved objects under one bijective rename: identical types, properties, dependencies and expressions. No object reference may leave its own source. The only accepted differences are labels set by the adaptor, visibility, mounts appended to the Body, the sketch attacher engine FreeCAD selects on copy, property status bits and last-digit origin-helper placements.
- Mount expressions must match the exact millimetre-qualified affine grammar. FreeCAD silently accepts unitless text on these coordinates, so a unitless expression with the right value is still rejected.
- Only the controlled ground and joint proxies may exist, no link may name an external file, and the GUI view state must be exactly the minimal record.
- Component placements, mount frames and connector coincidence are compared with an independent closed-form transform. Volume and area must equal the producer receipt. The centre of mass must equal the transformed centre measured on the captured copy alone.
- World bounds are compared exactly only when the pose is a signed axis permutation (quarter turns). After any other rotation a pocket may remove the corners that define the box, so bounds are only checked to lie inside the rotated producer box. That check is labelled containment, not equality.
- Every payload member of the saved assembly is bound to independent evidence
or to a stated structural rule, and the archive may contain nothing else:
- Embedded BREP shapes, for every history feature (not only the tip), must equal
the retained producer's bytes. The only exception is that a producer
-0.0…token may appear as0.0…at the same position, the one change FreeCAD makes when copying. - Element maps are bound by structure: the same section skeleton and child records, the same postfix tables (object tags remapped, string-hasher identifiers masked), the same postfix-table relationships, tags that name only the same source's objects, and string references inside the merged table.
- A datum shape must be the fixed planar template at its saved mount placement.
- Link placement and scale lists must be empty.
- Embedded BREP shapes, for every history feature (not only the tip), must equal
the retained producer's bytes. The only exception is that a producer
- The source files and their retained copies must keep their digests. Workflow resume reuses an assembly only while its sources are unchanged.
Verification and evidence#
PYTHONPATH=src python tools/qualify_freecad_assembly.py
The default evidence root is evidence/assembly-20260920. Completed evidence is
never overwritten; choose --directory for another run. --headless omits the
optional GUI stage. The qualification script supplements the integrated CLI and
console paths with shared-part editing and fresh GUI-reopening checks.
Sourced assemblies have their own native qualifiers, which need a fresh directory and never overwrite evidence:
PYTHONPATH=src python3 tools/qualify_freecad_assembly_sources.py \
--directory evidence/assembly-sources --executable /path/to/freecadcmd
PYTHONPATH=src python3 tools/qualify_freecad_assembly_sources_gui.py \
--assembly evidence/assembly-sources/workflow/assembly/batch-0/attempt-0001 \
--directory evidence/assembly-sources-gui --gui-executable /path/to/freecad --xvfb /path/to/Xvfb
The first runs a workflow of five part producers (two with colliding native names, a corner-removing pocket, a circle Pad and a literal Body) and two assemblies. It then checks:
- resume reuse and invalidation after a source byte changes;
- a literal analytic volume, area, bounds and centre-of-mass oracle;
- that rotated corner-pocket bounds are containment only, while quarter turns are exact;
- deliberate convention changes;
- a shared-source native edit with a fresh reopen;
- a relocated copy opened with every input unavailable;
- saved-file tamper and malformed-archive negatives, and native reopening of tampered copies;
- that the GUI capture stage is refused.
The second uses a private display for first-open visibility, the native joint editor, embedded sketch editing, a native edit with save and reopen, and the child-side behaviour of the joint panel's simple Offset field.
model.FCStd is the main publication. authored.json, plan.json, built.json,
verification.json, per-process submissions and logs retain the inputs and
verification evidence. Successful verification requires all of the following:
- A real native Assembly and JointGroup with the exact declared joint/component
membership; active attached
Fixedjoints and one native ground. - Correct primitive dimensions and component source links, including repeated instances pointing to the same native source object.
- Component world positions, orientation axes, bounds and solid volumes matching host-calculated analytic expectations. Fixed connector translations and rotations must coincide. Checks use relative tolerance 1e-7 and absolute tolerance 1e-6.
- Every non-ground component is deliberately translated and rotated, followed
by an explicit
assembly.solve()call, with no intervening recompute or connector presolve. Return code zero and analytically restored placements are required. Numeric before/perturbed/after poses are retained. - A separate
freecadcmdprocess opens the FCStd, checks its native definition and saved placements, then repeats the solver perturbation test. Verification binds the evidence to the native file's SHA-256 digest.
The qualification script additionally changes the shared plate thickness from
5 to 7 mm in the reopened native document, recomputes/solves, saves
edited.FCStd, and independently reopens that file. It checks all updated
placements and both linked plate volumes against the updated analytic model.
With the GUI enabled, another GUI process verifies that the saved native joint
editors restore and that the assembly remains visible and solvable.
Limits and provenance#
This version deliberately supports fixed trees only. Revolute joints, closed-loop or redundant constraints, face or edge selection on modelled parts, mixing primitive and sourced parts, subassemblies, imported STEP or third-party FCStd parts, external document links, live console or design-study execution, interference checking, contact, motion and multibody dynamics are unsupported. Sourced parts are limited to verified single-Body phase-one publications; mounts attach to the Body origin XY plane with signed-axis orientations.
FreeCAD re-encodes the element maps of the second and later sources against the
assembly's merged string hasher, and even regenerates them on recompute. The
encoded name entries inside those maps are therefore counted and isolated, not
decoded. Decoding them would need FreeCAD's internal element-map encoding. The
qualified route does not reference elements by mapped name: sketches and mounts
attach to origin planes, joints reference whole mount datums, and sources may
not contain sub-element references. A same-structure exchange of name entries
leaves geometry, placements and the solve unchanged, and recompute regenerates
the maps. Before the first recompute, however, such an exchange can make a mapped
edge name resolve to a different edge, which would affect a new name-based
reference created in the GUI before recomputing. The source envelope is applied to new assembly
admission only; the phase-one part verifier itself is unchanged. expectedDegreesOfFreedom: 0 describes the fixed rooted tree; it is
not a reported numerical DOF diagnostic from FreeCAD. Solver success and the
perturbation tests establish the qualified static cases, not universal solver
convergence. This adaptor does not reverse-sync manual FCStd edits into authored
JSON; use updated input for a new verified publication.
Public installed API usage was inspected in FreeCAD's
Mod/Assembly/AssemblyTests/TestCore.py, JointObject.py and
assembly.solve.__doc__. These supplied API names, reference conventions and
solver status meanings. The adaptor calls the installed implementations; it
does not copy their solver or joint implementation source.