Release 1.1.0 — metadata v1.1 (@meta/@ui), mechatron assembly integration, pure-python core split#43
Merged
Merged
Conversation
added 28 commits
May 6, 2026 16:35
Implements step 1 of the metadata v1.1 rollout (Option A):
- Bump default schema string from metadata-namespace-v0.1 to v1.1 across
metadata.py, geometry_json.py serializer, and assembly STYLE_GUIDE.md
- Add assembly namespace helpers (RFC § 7):
get_assembly_metadata, set_assembly,
add_bolt_pattern, add_datum, add_surface, add_keepout
- Add operation namespace helpers (RFC § 8):
get_operation_metadata, set_operation
- Validate enum vocabularies at write time:
joint_kind, bolt-pattern ring, datum/surface/keepout kinds,
fastener thread/head, operation kind/policy/feature_kind
- Fix latent _ensure_root bug: empty dicts were being replaced rather
than populated in place, causing namespace mutations to be lost. The
pre-existing v1.0 helpers happened to dodge this because they always
routed through get_solid_metadata/get_surface_metadata which kept the
reference alive in the entity list slot. The new direct-on-dict
callsites surfaced the bug.
- Consolidate duplicate __all__ exports (the second one was shadowing
the first and dropping half the public surface).
Tests: 13 metadata tests pass (6 existing + 7 new for v1.1 round-trip,
validation, no-create semantics, coexistence). Full DSL+IO test suite
(260 tests across geometry_json, dsl_lexer, dsl_parser, dsl_runtime,
dsl_checker, dsl_packaging, metadata_utils) is green. The 22 unrelated
test failures in test_brep_edge_select and test_phase2_builtins
pre-exist on origin/main.
Deferred to follow-up commits: _provenance sidecar emitter, DSL
grammar wiring (@bolt_pattern, @cutter, etc.), pluggable resolver
entry points.
Brings the workbench-v2 @ui parser work onto feature/metadata-v1.1 as the canonical foundation, cleaned up and fully documented. ## What changed ### Parser (src/yapcad/dsl/parser.py) - _parse_parameter() now recognises an optional @ui(...) decorator between the type annotation and the default value. - New _parse_param_ui_hint(): parses @ui(key=value, ...) kwargs into a plain dict of JSON-safe literals. Raises a clear error if a non-'ui' name is used. - New _ui_hint_value(): reduces literals, unary-minus, and list literals to Python scalars; stringifies non-literal expressions as a fallback. ### AST (src/yapcad/dsl/ast.py) - Parameter gains ui_hint: Optional[dict] = None. Documented as evaluator-transparent (ignored by interpreter and type-checker). ### Service (service/) - schemas.py: add DslUiEvalRequest / DslUiEvalResponse models. - routes/dsl.py: - /commands now includes ui_hint in each param dict when present. - New /ui_eval endpoint: evaluates a command and returns its scalar/list result (for @ui-driven commands that emit derived values, not geometry). ### Docs (docs/dsl_reference.md) - New 'Parameter Decorators' section with full @ui(...) reference: recognised keys (widget, label, snap, min, max, step, group), syntax example, service API shape, and /ui_eval description. - Cross-reference added in the 'Escape Hatches' section. ### Tests (tests/test_dsl_parser.py) - TestParameterUiHint: 7 cases covering basic parse, numeric bounds, negative min, plain params, mixed params, real mounting_plate_2d.dsl round-trip, and wrong decorator name rejection. All pass. ## Design notes - @ui is ONLY valid on command parameters, not on top-level function defs. Top-level decorators remain @native-only. - The evaluator is untouched — ui_hint is viewer metadata, not geometry logic. - This is the stable base for the Step 3 metadata annotation work: @meta(...) and assembly namespace helpers will be layered on top of this same decorator infrastructure.
Adds @meta(...) as the canonical way to attach assembly/operation namespace metadata to a command definition. Layered on top of the @ui parameter decorator infrastructure from the previous commit. ## What changed ### AST (src/yapcad/dsl/ast.py) - FunctionDef gains meta_hint: Optional[dict] = None Merged dict from all @meta decorators on the command; evaluator-transparent. ### Parser (src/yapcad/dsl/parser.py) - _parse_meta_decorator(): parses @meta(key=value, ...) into a flat dict. Supports both plain keys (label) and dotted namespace paths (assembly.joint_kind, operation.feature_kind). - _parse_meta_key(): consumes IDENTIFIER (DOT IDENTIFIER)* to produce the dot-joined key string. Accepts type-keyword tokens (surface, solid, float, ...) as key segments via _consume_identifier_or_type_keyword(). - _consume_identifier_or_type_keyword(): helper shared by _parse_meta_key. - Module-level decorator loop: peeks at the token after '@' to route @meta decorators to _parse_meta_decorator() and all others to _parse_decorator(). Multiple @meta decorators are merged with dict.update() — later wins on collision. Non-meta decorators (@Native etc.) are unaffected. - _parse_function_def(): accepts meta_hint kwarg, stores in FunctionDef. - _parse_command(): legacy command path stores meta_hint when provided. ### Service (service/routes/dsl.py) - /commands endpoint includes meta_hint in the command descriptor when present. The AST serialiser (ast_to_dict) emits it automatically via dataclasses.fields. ### Docs (docs/dsl_reference.md) - New @meta(...) subsection in 'Parameter Decorators': key syntax table, dotted vs flat examples, namespace convention table (assembly.* / operation.*), service API JSON shape, evaluator-transparent note. - Cross-reference in 'Escape Hatches' updated to mention @meta alongside @ui. ### Tests (tests/test_dsl_parser.py) - TestCommandMetaHint: 11 cases covering single dotted, stacked merge, flat keys, mixed flat+dotted, collision resolution (later wins), numeric/bool values, negative numerics, type-keyword key segments, no-meta=None, combined with @ui on params, and multi-command isolation. All 11 pass. - Full parser + metadata + geometry_json suite: 76 passed, 2 skipped, 0 failures. ## Design notes - Flat storage (dotted strings, not nested dicts) — namespace routing delegated to v1.1 helpers (get_assembly_metadata, get_operation_metadata). - @meta is ONLY on commands/defs, never on parameters (that is @ui's domain). - @Native co-existence: the decorator loop correctly distinguishes @meta from @Native by peeking at the identifier token value before consuming. - Pre-existing bug: @Native on 'def' (vs native python {} block) was already broken before this commit; not in scope here.
New module yapcad.dsl.meta_apply closes the loop between the @meta(...) DSL decorator (flat dict of dotted keys) and the v1.1 metadata namespace helpers in yapcad.metadata. ## What changed ### New module (src/yapcad/dsl/meta_apply.py) - apply_meta_hint(solid, meta_hint): main entry point. Retrieves the solid's metadata dict via get_solid_metadata(create=True), then routes each key: - assembly.* → _apply_assembly() → set_assembly() for scalars, verbatim storage for unknown fields, ValueError for list fields (bolt_patterns / datums / surfaces / keepouts) that require dedicated helpers. - operation.* → _apply_operation() — per-field dispatch with individual enum validation; avoids the set_operation() required-kind constraint so fields can be applied in any dict iteration order. - layer/tags → _apply_root(), with tag list-append and string-coerce. - plain keys → _apply_root() verbatim. - unknown.* → stored under that namespace dict for forward-compat. - apply_meta_hint_to_raw(meta, meta_hint): convenience variant for callers that already have the metadata dict rather than the raw solid. - Bool coercion: "true"/"1"/"yes" strings → True for no_cut/through/consume. - target_filter: accepts list or comma-separated string. ### DSL init (src/yapcad/dsl/__init__.py) - apply_meta_hint and apply_meta_hint_to_raw exported from yapcad.dsl. ### Docs (docs/dsl_reference.md) - v1.1 enum vocabulary table added to @meta namespace section: assembly.joint_kind, operation.kind, operation.policy, operation.feature_kind. - Notes that values outside the enum set raise ValueError at apply time. ### Tests (tests/test_dsl_meta_apply.py) - 29 cases: assembly scalar routing, assembly list-field rejection, operation per-field routing with enum validation, root fields (layer/tags/free-form), unknown namespace verbatim storage, combined/stacked hint, idempotency, empty-hint noop, TypeError on bad input, enum rejection for both namespaces, apply_meta_hint_to_raw variants, and full DSL round-trip (parse @meta → apply_meta_hint → assert metadata). All 29 pass. ## Design notes - operation.* fields are dispatched individually rather than via set_operation() to avoid the required-kind contract; each field is validated independently. - Structured assembly list fields (bolt_patterns etc.) intentionally not supported via flat @meta — they require the dedicated add_*() helpers. - forward-compat: unknown dotted namespaces stored verbatim, not rejected.
Closes the gap left by the meta_apply v1 work — the four assembly list-shaped fields (bolt_patterns, datums, surfaces, keepouts) used to raise ValueError when set via @meta, with a 'use the dedicated helpers directly' pointer. Now @meta dispatches each list entry to the corresponding yapcad.metadata helper. src/yapcad/dsl/parser.py: - _ui_hint_value now recurses into DictLiteral (previously stringified). Required for list-of-dict @meta values to survive parsing with shape. - _parse_meta_decorator tolerates NEWLINE tokens between args and honors trailing commas before the closing ')'. Multi-line @meta(...) with list-of-dict values would otherwise error at 'expected key name in @meta, found RPAREN'. src/yapcad/dsl/meta_apply.py: - _ASSEMBLY_LIST_HELPERS dispatch table maps each list-shaped field to its dedicated helper (add_bolt_pattern / add_datum / add_surface / add_keepout). - _apply_assembly_list_field walks each entry, validates it's a dict, and forwards as kwargs to the helper. TypeErrors from helpers are re-wrapped with field name + index so DSL authors can pinpoint typo'd keys ('assembly.bolt_patterns[2] rejected: …'). - _apply_assembly dispatches list-shaped fields through the new path. Old 'raise ValueError(...)' is gone. Helper-raised ValueErrors (enum validation, missing keys, vector lengths) propagate unchanged. - Module docstring documents the supported list-of-dict syntax with an end-to-end example. tests/test_dsl_meta_apply.py: - Replaced obsolete test_assembly_list_field_raises with two new tests for the new type-checking contract. - New TestApplyMetaHintAssemblyListFields class with 10 cases covering all four list fields, error paths, and mixed list+scalar+root in a single hint dict. - New TestDslRoundTrip::test_parse_and_apply_list_fields — full DSL source → parse → meta_hint → apply round-trip with multi-line @meta args and trailing commas. .gitignore: added workbench/node_modules/ (was being swept by git add -A). Verification: - tests/test_dsl_meta_apply.py: 41 passed (12 new) - tests/ -k 'dsl': 314 passed, 2 skipped (0 regressions) - tests/ -k 'metadata': 17 passed What this unlocks: DSL surface for Phase 1 of yapcad-assembly-integration is expressible without new keywords. The Phase 1 'datum' keyword from the integration plan remains on the roadmap as sugar that compiles down to this same @meta shape; the underlying capability is in.
Exposes yapcad.assembly to the DSL. Five new builtins land enough of
the Assembly surface for DSL authors to declare, validate, and report
on multi-part assemblies without dropping into Python:
assembly(name) -> assembly handle
add_part(asm, solid, name) -> asm (chainable)
add_mate(asm, kind, part_a, datum_a,
part_b, datum_b) -> asm (chainable)
validate_assembly(asm) -> bool
assembly_report(asm) -> string
add_part is the key integration point with the v1.1 metadata layer
that landed in 7b301ed / dced15e:
@meta(
assembly.datums=[
{"id": "shaft", "kind": "axis", "direction": [0,0,1]},
{"id": "neck", "kind": "bolt_circle", "R_mm": 149.4,
"z_mm": 317.3, "direction": [0,0,1]},
],
)
command FORWARD_BULKHEAD() -> solid: ...
let bulkhead = FORWARD_BULKHEAD()
let asm = assembly("forward_section")
add_part(asm, bulkhead, "bulkhead") # datums auto-lifted from @meta
add_part reads assembly.datums from the solid's metadata and converts
each entry into a Datum on the resulting PartDefinition:
- v1.1 kind Datum.DatumType
axis AXIS
plane PLANE
bolt_circle CIRCLE (radius = R_mm)
point POINT
frame FRAME
edge (skipped \u2014 no Datum equivalent)
- Origin: [R_mm, 0, z_mm, 1] in part-local coordinates
- Direction: from metadata 'direction' if present, else default +Z
(Datum validates AXIS/PLANE/CIRCLE require a non-zero vector)
Files:
src/yapcad/dsl/types.py:
+ ASSEMBLY = GeometricPrimitiveType("assembly", dimension=None)
Opaque handle type for Assembly instances. Treated as a top-tier
type so it cannot be confused with SOLID/SURFACE/etc.
src/yapcad/dsl/runtime/values.py:
+ assembly_val(asm)
Constructor wrapping an Assembly instance as a Value with type=ASSEMBLY.
src/yapcad/dsl/runtime/builtins.py:
+ _register_assembly_functions() invoked from _register_all().
+ Lazy imports yapcad.assembly.* inside the method so DSL runtime
startup is unaffected when assembly builtins are unused.
+ Helpers _datum_from_metadata_entry, _part_def_from_solid build
PartDefinition from v1.1 metadata. Robust to malformed entries
(missing id, unsupported kind \u2014 entry skipped silently).
+ Five BuiltinFunction registrations.
tests/test_dsl_assembly_builtins.py (NEW, 394 lines, 18 tests):
- TestAssemblyBuiltin: constructor returns Assembly handle with name.
- TestAddPartBuiltin (7 tests): bare add_part, axis/plane/bolt_circle
datum lifting, unsupported-kind skip, id-less skip, duplicate-name
AssemblyError.
- TestAddMateBuiltin (4 tests): concentric/revolute kinds, invalid-kind
error with valid-kinds list, error message contents.
- TestValidateAndReportBuiltins (3 tests): empty / single-part valid,
report contains assembly name.
- TestAssemblyBuiltinsEndToEnd (2 tests):
* Two-part concentric assembly built entirely through builtins
(datums lifted from @meta, mate added, validate=True, report
mentions both parts).
* add_mate fails fast (AssemblyError) when referencing an unknown
part \u2014 better than waiting for validate_assembly to flag it.
Verification:
tests/test_dsl_assembly_builtins.py 18/18 pass (new)
tests/test_dsl_meta_apply.py 41/41 pass (existing)
pytest -k 'dsl' 332 passed, 2 skipped
(+18 vs pre, 0 regressions)
pytest -k 'metadata' 18/18 pass (no regressions)
pytest -k 'assembly and not dsl' 7 passed / 19 errors
(errors are pre-existing in
test_assembly_constraint_validator;
verified by stashing my changes
and re-running \u2014 same 19 errors
on pristine main)
What this unlocks (Phase 2 of yapcad-assembly-integration.md):
A part DSL file can now declare datums via @meta, and an assembly
DSL file can pull those parts together with mates \u2014 all without
manual Python. Validate at DSL eval time, get a textual report for
debugging, fail fast on bad references.
Roadmap from here:
- Phase 1b: 'datum'/'assembly'/'mate' sugar keywords (parser-only
work that lowers to @meta + the builtins above).
- Phase 3: emit_assembly(asm) -> JSON consumable by the Mechatron
assembly-graph MCP on port 8081.
- Phase 4: WBS auto-generation walking the validated assembly graph.
- Phase 5: live collision check in build pipeline.
The file (391 lines, 19 tests) was committed as part of 7ac3ba6 'feat: add multi-body assembly constraint system' on 2026-02-13. Every test in the file imported `robot_printer.kinematic_chain` / `robot_printer.assembly_constraint_validator`, which was a separate project the original author was developing in parallel to yapCAD \u2014 never part of this repo. Consequence: every test in the file errored at collection time with `ModuleNotFoundError: No module named 'robot_printer'` since the commit landed, including on a pristine yapCAD checkout. The file was not testing yapCAD's own assembly system (which has its own working coverage in test_assembly*.py); it was testing a downstream consumer's validator that imported the yapCAD primitives. With the consumer project unavailable in-tree, the tests have never run and never can. Deletion path verified: - File is recoverable from git (7ac3ba6 and any preceding tag). - yapCAD's assembly system has independent test coverage: test_assembly_assembly.py, test_assembly_datum.py, test_assembly_face_mate.py, test_assembly_kinematic_*.py (7 passing tests in the 'assembly and not dsl' filter today). - Two example files (examples/assembly_complete_workflow.py, examples/datum_example.py) reference `robot_printer/...` paths in geometry_source strings, but those are descriptive only \u2014 they parse and run today and are NOT touched by this commit. Net effect on pytest output: pytest -k 'assembly and not dsl' before: 7 passed, 19 errors after: 7 passed, 0 errors
…ration)
Round-trip-validated against Mechatron's AssemblyGraph::load_json
(crates/assembly-graph/src/graph.rs save_json/load_json codec).
New module: yapcad.assembly.mechatron_export
- to_mechatron_snapshot(assembly) -> dict
- to_mechatron_json(assembly, *, indent=2) -> str
Maps yapCAD's Assembly/PartDefinition/Datum/Mate onto Mechatron's
Part/Datum/Interface/Joint/MateConstraint schema, including:
* PascalCase DatumType (axis -> Axis, plane -> Plane, ...)
* PascalCase Material enum with PETG fallback
* homogeneous [x,y,z,w] -> Point3D/Vec3D {x,y,z}
* yapCAD MateType -> mechatron Joint variant for motion mates
(revolute, prismatic, spherical, rigid)
* yapCAD MateType -> mechatron MateConstraint variant for
semantic mates (concentric, coincident, tangent, angle)
* yapCAD mate kinds without a Mechatron equivalent (parallel,
perpendicular, distance, planar, ...) are dropped from
mate_constraints rather than emitting unloadable JSON
New DSL builtin:
- emit_assembly(asm) -> string : Mechatron-shaped JSON
Cleanup: deleted examples/assembly_complete_workflow.py and
examples/datum_example.py — both depended on the never-imported
robot_printer module (vestigial Jeremy-era artifacts; matches the
similarly-vestigial test file removed in cff854a).
Tests: tests/test_assembly_mechatron_export.py (24 cases) covers:
- empty assembly shape
- part materials + insertion order
- all DatumType -> PascalCase mappings
- homogeneous coordinate drop
- all motion-mate -> Joint mappings (rigid/revolute/prismatic/spherical)
- all semantic-mate -> MateConstraint mappings (concentric/coincident/tangent)
- unmappable mate kinds dropped
- DSL builtin end-to-end via @meta(assembly.datums=[...])
- deterministic output
Round-trip validation: a one-shot ignored cargo test (not committed)
loaded the exporter output through AssemblyGraph::load_json and
asserted parts/interfaces materialized correctly. The schema mismatch
between my initial draft (free-form {kind,...}) and Mechatron's
#[serde(tag=type)] MateConstraint shape was caught and fixed before
landing.
Full suite: 373 passed, 2 skipped.
Fills three gaps that surfaced when wiring the assembly-export pipeline
to a real DSL file (the Hydra modular-robot arm at
hydra/designs/arm-segmented/design-a/arm_segmented_a.dsl):
1. Type-checker registrations (src/yapcad/dsl/symbols.py +
src/yapcad/dsl/types.py)
The Phase 2 builtins (assembly, add_part, add_mate,
validate_assembly, assembly_report) and the Phase 3 emit_assembly
were only registered with the runtime BuiltinRegistry. The
type-checker has its own parallel SymbolTable that drives DSL
parse-time validation, and it had no idea those functions or the
'assembly' type existed. Result: every DSL file that used them
failed type-checking with E270/E299 errors before the runtime
could ever run. Registered the 6 builtins and exposed the
ASSEMBLY type via BUILTIN_TYPES so authors can declare
'asm: assembly = assembly("...")' the same way they declare
'x: solid = ...'. The Phase 2 builtins tests bypassed the parser
by calling call_builtin() directly, which masked this gap.
2. @meta hint -> emitted-solid bridge wired in the runtime
(src/yapcad/dsl/runtime/interpreter.py)
Phase 1c added meta_apply.apply_meta_hint to translate a
FunctionDef.meta_hint (the dict the parser builds from
@meta(...) decorators) into the v1.1 metadata namespace on a
solid, but the runtime interpreter never called it. Result:
@meta(assembly.datums=[...]) on a command parsed and
type-checked fine but produced solids with empty assembly
metadata, so add_part() saw zero datums to lift. Added
Interpreter._apply_command_meta_hint and invoked it from both
the top-level execute() path and from _call_command() (so the
metadata also reaches nested calls like
add_part(asm, SHOULDER(...), 'shoulder')).
3. Mechatron joint axis derived from datums
(src/yapcad/assembly/mechatron_export.py)
The Phase 3 exporter emitted Joint::Revolute / Joint::Prismatic
with a hard-coded +Z axis placeholder, on the (untested)
assumption that Mechatron's solver would re-derive the axis from
datum geometry. AssemblyGraph::load_json does NOT do that — it
trusts the JSON. Added _resolve_mate_axis() to look up the
parent-side datum's direction vector on the assembly and use it
as the joint axis. Falls back to +Z when the datum lacks a
direction (preserving prior behaviour for edge cases). With this
in place, the Hydra elbow's X-axis revolute joint round-trips as
Joint::Revolute { axis: [1.0, 0.0, 0.0] } through Mechatron.
End-to-end verification (one-shot, not committed to either repo):
yapCAD DSL (HYDRA_ARM_ASSEMBLY) → emit_assembly → 6.7 KB JSON →
Mechatron AssemblyGraph::load_json → 4 parts, 6 interfaces,
1 Revolute joint on the elbow with the correct X-axis.
Existing tests: 373 passed (DSL + metadata + assembly), no
regressions. The 24 unit tests for mechatron_export still cover
the +Z fallback path.
…tor, geoms, AxisAlign, context-aware Concentric
Took the Phase-3 exporter from "loads in Mechatron with warnings" to
"validates clean and forward-kinematics correctly" using the Hydra
arm as the test bed. Five changes:
1. **Merge same-pair mates into one Interface**
When DSL authors stack mates between the same parts (e.g. concentric
+ coincident to fully constrain a Fixed mount), Mechatron expects
them on one Interface, not several. The exporter now groups by
(parent_part, child_part) in original order, picks the first
motion mate (REVOLUTE / PRISMATIC / SPHERICAL / RIGID) as the
Joint, and emits the rest as mate_constraints. yapCAD's previous
12-mate Hydra spec collapses from 6 interfaces to 3.
2. **insertion_vector derivation**
Mechatron's validator errors when an interface has no
insertion_vector. The exporter now looks at the parent-side datum
referenced by Coincident-style mates; if it is a PLANE or CIRCLE,
the insertion direction is taken as -normal ("the child moves
opposite the parent face's outward normal to seat against it").
Falls back to no field when no PLANE/CIRCLE datum is available;
Mechatron will warn but not reject.
3. **PartGeom export via @meta(assembly.geoms=[...])**
New side-channel on PartDefinition that add_part populates from
the assembly metadata namespace. The exporter converts each entry
to a Mechatron PartGeom (Box / Cylinder / Sphere / Capsule / Mesh),
handling the millimetre → metre unit conversion (PartGeom is in
metres per MuJoCo SI convention while datum positions are in mm).
Eliminates the "no visual geometry" warnings; cylindrical Hydra
modules render in the dashboard as 60mm-OD cylinders.
4. **PARALLEL → AxisAlign**
yapCAD's PARALLEL MateType (accepts AXIS/PLANE datums) now maps to
Mechatron's AxisAlign MateConstraint variant. AxisAlign has a
unilateral shape (child_datum + target_axis, no parent_datum), so
the exporter special-cases it and reads target_axis from the
parent-side datum's direction vector.
5. **Context-aware CONCENTRIC mapping**
This is the subtle one. Mechatron's mate-DOF accounting:
- Concentric: 3T + 2R, NO composite rule fires
- AxisCoincident: 2T + 2R, but composite rule "AxisCoincident +
Coincident => +1R axial lock" promotes total rotational DOFs
constrained from 2 to 3
For a Fixed mount, we WANT the composite rule to fire (so the
joint comes out fully constrained at 6/6 DOFs). For a Revolute
joint, we DON'T want it (the joint needs exactly 1 free rotational
DOF; 6/6 mate-locked + 1 joint DOF = over-constrained).
The exporter now picks the variant per mate group:
- When the joint is Fixed: CONCENTRIC -> AxisCoincident
- When the joint is motion: CONCENTRIC -> Concentric
joint_is_motion is plumbed from _interface_for_mate_group
into _mate_constraint_for_mate to support this.
Tests: 29 unit cases (added 5, refactored 2). Full yapcad regression
still passes. End-to-end Hydra round trip:
yapCAD DSL (HYDRA_ARM_ASSEMBLY)
-> mechatron AssemblyGraph::load_json
-> assembly.validate valid:True, 0 structural issues
-> assembly.solve_all_mates converged:True
-> assembly.forward_kinematics
shoulder @ z=0
upper_arm @ z=120
elbow @ z=240
forearm @ z=302 (or rotated about flex_axis at z=279 when
flex != 0; forearm z-axis tracks the joint
angle correctly: 0deg=[0,0,1], 45deg=[0,-.707,.707],
90deg=[0,-1,0])
Hydra DSL changes live in
hydra/designs/arm-segmented/design-a/arm_segmented_a.dsl (not part of
this commit; the Hydra repo isn't versioned).
…ff-axis features
The axisymmetric (R_mm, z_mm) origin convention sufficed for cylindrical
parts (bulkheads, tank domes, the Hydra arm) but breaks down for parts
whose features aren't on a single radial plane:
- pentagonal/hexagonal robot body modules with N face-attachment points
- chassis with off-axis mount bosses
- any non-axisymmetric part
Added an additive 'origin_mm: [x, y, z]' kwarg to:
- yapcad.metadata.add_datum() — accepts + persists the value into
assembly.datums entries
- _datum_from_metadata_entry in dsl/runtime/builtins.py — when
origin_mm is present, overrides the (R_mm, 0, z_mm) derivation
Backward compatible: existing R_mm/z_mm entries are unaffected. When
both are provided, origin_mm wins (lets authors keep R_mm for the
circle-radius role while overriding centre placement).
Driven by Rich's directive that the Hydra body — pentagonal prism with
7 connector-face datums (5 on the rectangular sides + 2 on the
pentagonal end caps) — be authored as @meta-driven DSL. The new helper
lets a single .dsl define all 7 mount points without falling back to
hand-coded Python.
… FDM Parts with process='WAAM', 'CNC', 'Cast', etc. now export correctly. Previously all parts were exported as process='FDM' regardless. Fix: read part_def.process attribute if set, fall back to 'FDM'.
…v1.1) Wire @meta(...) hint validation into _check_command. The checker now emits W-series warnings (not hard errors, per the RFC forward-compat clause) for: W310 — unknown top-level namespace in a @meta key W311 — unknown field within the assembly namespace W312 — unknown field within the operation namespace Known fields are imported from meta_apply's private sets so the validator and the runtime stay in sync automatically. All 92 checker + meta_apply + metadata tests pass. Pre-existing brep-edge failures are unrelated (present on main).
…malisation (Step 4 — v1.1)
Adds src/yapcad/dsl/transforms/metadata.py, an AstTransform that runs
*before* interpretation and validates/normalises every FunctionDef.meta_hint
dict against the v1.1 assembly/operation namespace schema.
What it does:
1. Validates enum fields at AST time (operation.kind, operation.policy,
operation.feature_kind, assembly.joint_kind) — errors surfaced before
any solid is built.
2. Normalises coercible values in place:
- bool-as-string ('true'/'false') → real bool (no_cut, through, consume)
- operation.priority → float
- operation.target_filter string/csv → list
3. Cross-field consistency checks:
- operation namespace without operation.kind → error
- assembly.no_cut=true + operation.kind='subtract' → warning (contradictory)
- operation without target_filter → info (will apply to ALL parents)
4. Unknown namespaces / unknown fields → warning (forward-compat; stored verbatim)
5. Assembly list-of-dict fields (bolt_patterns, datums, surfaces, keepouts)
get structural checks (must be list of dicts); per-entry validation
remains with meta_apply.py helpers at runtime.
Diagnostics are stored on the Module object under _metadata_transform_diagnostics
(accessible via get_transform_diagnostics(module)) and logged via the
yapcad.dsl.transforms.metadata logger.
The transform is idempotent: running twice produces identical AST state.
Coercions write back into the live meta_hint dict so the runtime
apply_meta_hint call in the interpreter trusts the normalised values.
Also exports MetadataTransform, MetaDiagnostic, get_transform_diagnostics
from yapcad.dsl.transforms.__init__.
Tests: 65 tests in test_metadata_transform.py, all pass.
Combined with Steps 2+3: 157/157 metadata-layer tests green.
…v1.1) - New src/yapcad/io/meta_yaml.py: dump_metadata_yaml(), load_metadata_yaml(), sidecar_path_for() - Writes <part>.meta.yaml alongside STEP/STL exports - _provenance block: yapcad_version, exported_at, source_dsl + sha256, source_command - No-op when part carries no metadata - Requires PyYAML (ImportError with install hint if absent) - write_step_with_meta() in io/step.py — thin wrapper that calls dump_metadata_yaml after write_step - write_stl_with_meta() in io/stl.py — same for STL - Both exported from yapcad.io.__init__ - 15/15 tests green (round-trip, provenance sha256, backwards compat v1.0, path computation, exporter hooks, emit_sidecar=False skip, no-metadata no-op)
… — v1.1)
Implements RFC §16 pluggable assembly resolver strategy.
Changes:
- src/yapcad/assembly/resolver.py (new)
- AssemblyResolver ABC with shared _collect_cutters/_validate_cutters/_apply_boolean helpers
- BasicResolver: bucket → sort by priority → apply sequentially per target (spec §8.2)
- StagedResolver: multi-pass resolver; operations tagged with stage= processed in
declared order; operations missing stage= default to last stage
- KinematicResolver: v1.2 placeholder (raises NotImplementedError)
- RESOLVER_REGISTRY keyed by 'basic'/'staged'; external resolvers via
yapcad.assembly_resolvers entry points (mirrors boolean/ENGINE_REGISTRY)
- resolve_assembly() convenience function; YAPCAD_ASSEMBLY_RESOLVER env var
selection (mirrors YAPCAD_BOOLEAN_ENGINE)
- Correct consume=True semantics: consumed cutters dropped from output parts
- src/yapcad/assembly/__init__.py
- Export all resolver public symbols
- tests/test_assembly_resolver.py (new, 29 tests all green)
- BasicResolver: pass-through, single cut, consume/retain, priority order,
multi-target, multi-cutter, error/policy (strict/warn/ignore), boolean failure
- StagedResolver: stage order, unknown stage fallback, no-stage fallback, empty-
stages validation, single-stage equivalence to basic
- KinematicResolver placeholder check
- resolve_assembly dispatch: env var selection, env var errors, explicit override
- Registry: built-in names, get_resolver, unknown name
- RFC §12 acceptance criteria: priority ordering, consume=False retention, no_cut
Resolvers are first-class public API. Mechatron may supply its own resolver via
the yapcad.assembly_resolvers entry point without forking yapCAD.
…med-arg fix + OCC resolver fallback
Step 7 of RFC §11 migration plan — forward bulkhead as proof case.
New files (in agentic-1 repo, not tracked here):
designs/forward-bulkhead/forward_bulkhead_cutouts_v1.dsl
designs/forward-bulkhead/forward_bulkhead_assembly_v1.yaml
yapCAD changes:
- interpreter.py: _eval_function_call now resolves named_arguments into
positional order when calling intra-module commands. Fixes the gap where
RADIAL_BOX_CUTTER(tang_w=101.6, ...) was called with args=[] because
_eval_function_call only iterated call.arguments (positional), silently
dropping call.named_arguments. RFC §3 decorator-sugar gap noted in DSL.
- resolver.py: _apply_boolean now falls back to direct BRepAlgoAPI_Cut/
Common/Fuse when geom3d.boolean3d is not importable. Fixes resolver in
yapcad-brep env (OCC available, geom3d module not yet wired).
- test_step7_forward_bulkhead_migration.py: 11 tests covering:
1. Parse + checker (4 tests)
2. Runtime execution + metadata (3 tests including volume + position)
3. Resolver integration: BasicResolver applies 3 subtracts in priority
order, volume decreases, applied_operations in [lox_man_vent,
main_chute_door, fwd_accessory] order
4. meta.yaml round-trip
5. Cross-check gate: CUTOUTS-GHOST-SPEC.json FB entries match DSL
All 11 pass on yapcad-brep.
Next: Step 8 — repo-wide migration; retire FB entries from CUTOUTS-GHOST-SPEC.json.
Add structural load-case and bolt-pattern support to Assembly: - yapcad.assembly.load_case: LoadCase / LoadAttach / BoltPattern dataclasses - Assembly.add_load_case/get_load_case and add_bolt_pattern/get_bolt_pattern - yapcad.package.analysis.mechatron_fea_setup: reads bolt_patterns and load_cases from a mechatron graph.json and produces per-bolt world coords, bolt-spring stiffness lookup, and resolved load-attach descriptors for a downstream FEA solver (per-hole-datum path + legacy PCD fallback). Part of the metadata-v1.1 mechatron assembly integration (Phases 3-4).
…tures Prepare the test suite for public release by removing references to the internal design project and replacing live-design dependencies with generic, self-contained fixtures: - tests/fixtures/sample_assembly_graph.json: synthetic assembly graph (generic widget parts) driving test_mechatron_fea_setup.py — no longer reads a live external graph.json. Assertions are deterministic against invented data. - tests/fixtures/enclosure_shell_cutouts_v1.dsl: generic @meta subtract-cutter DSL. test_step7 renamed to test_step7_subtract_cutter_migration.py and now validates the public cutter/resolver/meta.yaml capability against the fixture instead of an internal DSL + spec file. Drops the internal-only retirement-gate cross-check test. - test_meta_yaml_exporter.py: scrub project-specific tags to neutral values. Verified: grep for internal project/path references across src/ and tests/ returns nothing.
yapCAD's core is pure Python; solid-modeling (BREP) features layer on top of OpenCASCADE via pythonocc-core, which is distributed via conda-forge (not reliably pip-installable). - pyproject.toml: add [project.optional-dependencies] 'brep' (pythonocc-core >=7.7) and 'full' extras, with a comment documenting that OCC comes from conda. Core 'dependencies' remain OCC-free. - src/yapcad/__init__.py: expose yapcad.has_brep() (lazily calls yapcad.brep.occ_available()) so downstream can branch on OCC availability without eagerly importing brep at package load. - pytest.ini: register 'requires_occ' marker so the pure-python lane runs via pytest -m 'not requires_occ'. - tests/conftest.py: auto-tag OCC-guarded tests (skipif reason mentions OCC) with requires_occ, so the marker stays in sync without touching every file.
…EBOARD_ROOT The /api/whiteboard/save and /api/whiteboard/read routes previously hardcoded a local ~/Projects/whiteboard path. Gate the feature behind the YAPCAD_WHITEBOARD_ROOT environment variable: when unset, both routes return 501 and never touch the filesystem — there is no default path baked into the release. When set, it points at a git working tree and the feature works as before (with the existing path-traversal guards).
- service/routes/dsl.py: expanded DSL execution routes (metadata-aware run, schema-validated request/response models). - service/models/schemas.py: request/response schema additions for the DSL endpoints. - service/main.py: wire up the routes. - docs/dsl_guide.md: DSL usage guide.
# Conflicts: # pyproject.toml
Document the metadata-v1.1 feature set (@meta/@ui decorators, checker warnings W310/311/312, AST MetadataTransform, .meta.yaml sidecar, resolver registry, mechatron assembly integration Phases 2-4, off-axis feature datums, assembly load_case/bolt_pattern, FEA setup bridge) and the pure-python-core / brep optional-dependency packaging split.
When yapCAD is imported without pythonocc-core present, emit a single, suppressible YapcadBrepUnavailableWarning (a UserWarning subclass) explaining that BREP/boolean/STEP/solid features are unavailable and how to enable them via conda-forge. Fires once, never when OCC is present, and probing failures are swallowed so import is always safe. Supports the pure-Python PyPI install tier.
Prepare the pure-Python core for its first PyPI publication: - pyproject.toml: enrich PyPI-facing metadata — keywords, full classifier set (Dev Status, audiences, Python 3.10-3.12, Topic :: Scientific/Engineering, 3D Modeling), and project.urls (Repository, Issues, Changelog). Drop the License classifier (superseded by the SPDX license expression per PEP 639). Distribution name normalizes to 'yapcad'. - README.rst: document the two install tiers prominently (pip pure-Python core with a limitation callout vs conda + pythonocc-core for full BREP); note the import-time capability warning; drop the stale 'python setup.py install'. - CHANGELOG.rst: note first PyPI release of the pure-Python core + the install-time capability warning. Validated: python -m build produces yapcad-1.1.0 sdist+wheel; twine check PASSES both; clean-venv pip install imports, warns, has_brep()==False, and 2D/ DSL ops work.
… compat Root causes (all version-skew against pythonocc-core 7.7.2, not geometry bugs): 1. brep_edge_select._get_unique_edges: called TopExp.MapShapes_s() (7.8+/OCP naming) inside a try/except ImportError that silently fell back to the TopExp_Explorer path, which returns each edge once per adjacent face. Result: every edge duplicated (box reported 24 edges, not 12), breaking all 20 edge-selection tests. Now tries 7.7.x lowercase topexp.MapShapes() first, falls back to the _s static style, and no longer masks failures with a silent explorer fallback. Also handles Extent()/Size() rename. 2. test_phase2_builtins: imported GProp/BRepGProp from OCP (the CadQuery binding, not installed here) and passed yapCAD list-solids directly to VolumeProperties_s. Rewritten to use yapcad.geom3d.volumeof() which handles both bindings and solid types. Also fixed a latent test bug: sphere() takes a diameter, so the shell-vs-sphere comparison must use sphere(30.0), not sphere(15.0) — the old comparison passed a half-size sphere and would fail for any correct shell volume. 3. Removed tests/test_assembly_constraint_validator.py: imports robot_printer.kinematic_chain, a module that has never existed in this repo (vestigial import from an internal project). All 19 tests errored at collection since introduction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Next release version of yapCAD with substantial extensions to DSL functionality and BREP operations support, with DSL extensions to support annotation for assembly/mating metadata.
Highlights
origin_mm)LoadCase/BoltPatternregistries onAssembly+ FEA setup bridgepip install yapcadworks without conda;has_brep()capability gate; one-time import warning when OCC absent;brep/fullextras;requires_occpytest markerValidation
has_brep()→ False, 2D geom workstwine check: PASSED for sdist + wheelRelease plan after merge: tag
v1.1.0on the merge commit → GitHub release with CHANGELOG notes → PyPI upload (resumes the existingyapCADPyPI project, last published 0.6.1).