[Bug]: a curve-sweep component with an attachment renders as a straight cylinder, and its swept geometry is emitted as dead code

Author: atishfadteCreated Aug 27, 2026Updated Aug 27, 2026
Labelstriage: needs-review

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I confirmed this is a reproducible behavior problem, not a request for help using the project.

What happened?

A component authored as primitive: "curve-sweep" with an explicit spine renders as a straight tapered cylinder when it also carries an attachment block. The generator emits the correct buildCurveSweepGeometry({...}) call with the authored spine, but that call never executes: curve-sweep is in ATTACHMENT_PRIMITIVES, so makeAttachmentEndpoint(attachment) returns non-null and the ternary takes the CylinderGeometry branch. The swept geometry is emitted as dead code.

I am filing this as a question rather than a straightforward bug, because test_attachment_does_not_override_primitive.py::test_every_attachment_primitive_still_takes_the_endpoint_path asserts this behaviour deliberately for all five members of ATTACHMENT_PRIMITIVES — so it looks intentional, and I did not want to "fix" a documented decision. But the outcome is the same failure the fix that added that gate describes in its own comment:

Deriving geometry from it unconditionally silently overrode the authored primitive: an ellipsoid head, a tapered-sweep ear and a swept tail all came out as cylinders between their two endpoints, the spec validated, the factory built, and the wrong shape shipped.

That reasoning applies to curve-sweep exactly as it does to the primitives the gate rescued. It is also the specific defect swept_arc_gate.py exists to catch:

A curve claim ("curled into a hook, not a straight cone") needs swept_arc_gate.py: silhouette IoU passes a straight cone occupying roughly the right cells.

Note the asymmetry: tapered-sweep keeps its swept geometry (there is a dedicated test for it), while curve-sweep does not. If that difference is intended, it would be worth a comment saying why.

Reproduction steps

bash
# 1. Author a component with primitive "curve-sweep", a geometryDescriptor spine, and an
#    attachment block carrying parentSocket / localStart / localEnd / contactType /
#    embedDepth / gapTolerance (the structural pass requires an attachment on child parts).
# 2. Generate the factory:
python3 forge/stage3_build/generate_threejs_factory.py spec.json --pass-id blockout --out model.ts

# 3. Both of these are present in the output -- the second one wins at runtime:
grep -n "buildCurveSweepGeometry\|makeAttachmentEndpoint(attachment_claw" model.ts
#   const endpoint_claw_2 = makeAttachmentEndpoint(attachment_claw_2);
#   ... ? new THREE.CylinderGeometry(...) : buildCurveSweepGeometry({"spine": [...]})

Reference image or input characteristics

Hard-surface object (claw hammer), 512×512 PNG, front view. The claw is a curved hook that sweeps down and left — the single most identity-defining feature of the silhouette.

Relevant output and evidence

Runtime probe of the built model in headless Chromium — the component named "Curved claw", authored curve-sweep:

{
  "name": "Curved claw",
  "geometryType": "CylinderGeometry",
  "params": { "radiusTop": 0.033, "radiusBottom": 0.06, "height": 0.284,
              "radialSegments": 32, "heightSegments": 12 },
  "vertices": 559
}

In the side-by-side comparison sheet the reference claw is a curved hook and the render is a straight angled rod. diagnose_render.py scored silhouetteIoU 0.281 against the 0.85 threshold for that render, though proportion errors of mine contributed to that number too.

Suggested direction

If endpoint-derived geometry is meant for axial members, curve-sweep may not belong in ATTACHMENT_PRIMITIVES — its whole purpose is a spine that is not a straight line between two endpoints. If it does belong there, then emitting the buildCurveSweepGeometry call as unreachable code is misleading; refusing at spec time, or dropping the dead branch, would at least make the choice visible instead of shipping a silently wrong shape.

Happy to send a PR either way once you say which behaviour you want.

Environment

  • Windows 11, Python 3.13.14, three r180, headless Chromium (SwiftShader)
  • Commit: 441af85