Skip to content

Expand geometric MAPPED_ITEMs as transformed instances (ENG-4019) - #32

Open
LK wants to merge 4 commits into
masterfrom
feat/mapped-item-instancing
Open

LK wants to merge 4 commits into
masterfrom
feat/mapped-item-instancing

Conversation

@LK

@LK LK commented Oct 14, 2026 •

Copy link
Copy Markdown

Expands geometric MAPPED_ITEMs into transformed instances (ENG-4019). Before this, they were silently ignored: mixed files lost their mapped bodies, and models whose bodies were all mapped had every part stacked at the origin by the identity fallback.

Behaviour

  • A MAPPED_ITEM in a visited SHAPE_REPRESENTATION / ADVANCED_BREP_SHAPE_REPRESENTATION / MANIFOLD_SURFACE_SHAPE_REPRESENTATION queues its mapped_representation at mat * target * origin^-1. Tessellation, instance copies, colours and unit scaling reuse the existing path, so both triangulate() and tessellate_step_bytes() get it. This includes representations reached through SHAPE_REPRESENTATION_RELATIONSHIP and nested mapped items.
  • MAPPED_ITEMs in presentation models (DRAUGHTING_MODEL, PRESENTATION_VIEW, PRESENTATION_AREA) are never reached, so they stay ignored.
  • Origin and target can each be AXIS2_PLACEMENT_3D or CARTESIAN_TRANSFORMATION_OPERATOR_3D.
    • Placements follow ISO 10303-42 build_axes: an omitted axis is +Z, and an axis along X without a reference direction uses +Y.
    • Operators support uniform scale. When axis2 is omitted they complete a right-handed frame, as OpenCascade does. An explicit axis2 can still mirror.
    • Any other form logs a warning and is skipped.
  • Mirrored copies reverse their triangle winding, and scaled copies keep unit normals. A body is meshed to the tolerance needed by its largest scaled copy.
  • Cycles through mapped items are detected and skipped with a warning.
  • The identity fallback still runs when nothing was placed. It leaves out only bodies behind mapped items whose placement couldn't be read, so those no longer stack at the origin.
  • A relationship into a representation that holds only MAPPED_ITEMs is now followed. If both ends belong to products it is not followed, so nothing is instanced twice. Relationships into representations that hold bodies are unchanged.

Testing

  • cargo test --release -p triangulate -p step -p nurbs -p cdt: all pass. Four new integration tests in triangulate/tests/examples.rs, built by editing cuboid.step / cube_hole.step:
    • mapped-only model through both entry points;
    • nested mapped items, including a scaled and mirrored operator;
    • operator and placement frame edge cases;
    • a mapped instance alongside a direct body, plus a cyclic variant;
    • presentation-only mapped items, which leave the mesh unchanged;
    • fallback behaviour;
    • tolerance for scaled copies.
  • Origin and target placements are asymmetric (rotation plus translation, origin not identity), so a wrong composition order fails. Each fix was also mutation-checked: undoing it fails the matching test.
  • STL output for the three checked-in example models is byte-identical to master.
  • Not run: the KiCad corpus and the OpenCascade oracle comparison.

Notes

  • If a mapped representation is also a top-level product's representation, it's meshed both at identity and as mapped instances.
  • When the target is an operator, this uses target * origin^-1. OpenCascade uses the operator alone in that case; the two differ only when the origin isn't identity.
  • axis2_placement_3d, which is still used for assembly placements, still collapses an axis along X that has no reference direction. That is unchanged from master.

https://ampcode.com/threads/T-01a13904-410c-738a-9919-3b52df6a9383


Note

Medium Risk
Changes core STEP placement traversal and mesh instancing; wrong transforms or relationship direction would misplace geometry, though behavior is heavily covered by new integration tests.

Overview
STEP triangulation now expands geometric MAPPED_ITEMs in shape representations into extra instances at target × origin⁻¹, instead of ignoring them or leaving mapped-only models stacked at the origin via the identity fallback.

Traversal follows nested maps and SHAPE_REPRESENTATION_RELATIONSHIP edges ranked by content (solids vs map-only reps), with cycle detection and skipping of solids reachable only through unresolved placements. Placements use ISO-style AXIS2_PLACEMENT_3D (default +Z, first_proj_axis) and CARTESIAN_TRANSFORMATION_OPERATOR_3D (uniform scale, right-handed frame when axis2 is omitted).

Instance copying now handles mirrors (swap triangle winding) and scales (renormalize normals, tighten mesh tolerance for the largest scaled copy). Integration tests in examples.rs cover nested/scaled maps, relationship wiring, presentation-only maps, and fallback behavior.

Reviewed by Cursor Bugbot for commit 1146432. Bugbot is set up for automated code reviews on this repo. Configure here.

LK and others added 4 commits October 14, 2026 06:24
A MAPPED_ITEM in a shape representation the traversal visits now queues
its mapped_representation at mat * target * origin^-1, so tessellation,
instance copies, colours and unit scaling reuse the existing path for
both triangulate() and tessellate_step_bytes(). Presentation-only mapped
items (draughting models, views) are still never reached.

- origin/target: AXIS2_PLACEMENT_3D or CARTESIAN_TRANSFORMATION_OPERATOR_3D
  (ISO 10303-42 base_axis, uniform scale); anything else warns and skips.
- Scaled instances keep unit normals; mirrored ones reverse winding.
- Cycles through mapped items are detected per path and reported.
- The mesh-every-solid-at-identity fallback no longer fires once mapped
  items were found, so mapped-only models are not stacked at the origin.
- SHAPE_REPRESENTATION_RELATIONSHIPs to representations that hold only
  MAPPED_ITEMs are now followed.

ENG-4019

Amp-Thread-ID: https://ampcode.com/threads/T-01a13904-410c-738a-9919-3b52df6a9383
Co-authored-by: Amp <amp@ampcode.com>
- Identity fallback: leave out only solids instanced by MAPPED_ITEMs whose
  placement could not be read, instead of disabling the fallback whenever
  any MAPPED_ITEM was seen (mapped wireframes emptied the mesh).
- CARTESIAN_TRANSFORMATION_OPERATOR_3D without axis2 completes a
  right-handed frame, as OpenCascade does, rather than mirroring
  (axis3 = -z) or failing (axis3 = x).
- Scaled instances: mesh shared geometry to tolerance / largest scale.
- Relationships between two MAPPED_ITEM-only representations are oriented
  from the product's representation, or warned about when ambiguous.
- Test: check the coloured mesh's vertex count before comparing positions.

ENG-4019

Amp-Thread-ID: https://ampcode.com/threads/T-01a13904-410c-738a-9919-3b52df6a9383
Co-authored-by: Amp <amp@ampcode.com>
- A MAPPED_ITEM placement may omit its axis (ISO 10303-42 default +Z);
  previously the instance was dropped and its solid left out of the
  fallback, emptying meshes master drew at identity.
- first_proj_axis treats directions within round-off of ±X as X, so
  printed noise no longer flips an operator's frame by 180 degrees.
- Do not link a product's MAPPED_ITEM-only representation from another
  of its product representations; it is already traversed directly.

ENG-4019

Amp-Thread-ID: https://ampcode.com/threads/T-01a13904-410c-738a-9919-3b52df6a9383
Co-authored-by: Amp <amp@ampcode.com>
- An AXIS2_PLACEMENT_3D along X without ref_direction no longer
  collapses (axis2_placement_3d's (1,0,0) default made x = z, y = 0):
  mapping placements now take their reference direction from
  first_proj_axis, and a reference parallel to the axis is an error
  instead of a singular frame.
- Treat axes within 1e-6 rad of X as along X when choosing the default
  reference, covering single-precision exporter noise.

ENG-4019

Amp-Thread-ID: https://ampcode.com/threads/T-01a13904-410c-738a-9919-3b52df6a9383
Co-authored-by: Amp <amp@ampcode.com>
@LK LK changed the title Expand geometric MAPPED_ITEMs as transformed instances Expand geometric MAPPED_ITEMs as transformed instances (ENG-4019) Oct 14, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1146432. Configure here.

r1.0, r2.0
);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapped-item relationship chains dropped

Medium Severity

A SHAPE_REPRESENTATION_RELATIONSHIP between two representations that contain only MAPPED_ITEMs is dropped when neither end is a product representation. A product can still reach the first representation, but the next mapped-item representation in the chain is never queued, so its solids are not instanced.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1146432. Configure here.

fn instance_scale(mat: &DMat4) -> f64 {
let scale = glm::mat4_to_mat3(mat).determinant().abs().cbrt();
if (scale - 1.0).abs() > 1e-5 { scale } else { 1.0 }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singular instances yield non-finite normals

Low Severity

instance_handedness divides by instance_scale for every instance matrix, and that scale is |det|^(1/3). A singular linear part has determinant 0, so the scale is 0 and every transformed normal is multiplied by infinity, producing Inf or NaN normals.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1146432. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant