Reuse a vendor STEP or an STL

Plenty of the parts a machine needs are not yours to model: a bought bearing published as STEP, a bracket someone shared as an STL, a whole vendor assembly. Two leaf kinds bring such files into a project as ordinary parts, and one command scaffolds an assembly from a STEP document’s structure.

A part from an STL

Commit the .stl beside the module that declares it, and name it:

from machinome.node import StlNode

class Bracket(StlNode):

    stl_source = 'bracket.stl'

That is the whole declaration for a well-behaved file. render() is not an extension point; the part is the mesh. The node materializes its own artifact from the file, and that artifact is what the assembly, a fusion, the viewer, the export and the tests all see. Two things make it stale, and both are tracked: the file, and the module that declares the node, because adjust() below can change the geometry.

Watertight, or knowingly not

A mesh with holes in its surface encloses nothing, so an open mesh fails the build naming the file and the defect, and no artifact is written:

Bracket: /home/me/rc-car/parts/bracket.stl is not watertight --
3 open edges leave the surface unclosed, so the mesh encloses no
solid. Repair the mesh, or declare `require_watertight = False` on
Bracket to admit it knowingly.

Nothing is repaired for you. If the mesh is known to be open and wanted anyway, say so with require_watertight = False. The flag governs admission only; it never changes geometry.

A body from a pack

A print pack holds several parts in one file. Declare which body the node is:

class Wheel(StlNode):

    stl_source = 'pack.stl'
    body = 0

body indexes the file’s connected components, ordered by centroid (x, then y, then z), which reads roughly as plate order. A node that omits it on a multi-body file fails with the pack’s inventory, one line per body with its centroid, bounds and volume, so the failure is the discovery tool. Extraction keeps the file’s coordinates; placing the body is the assembly’s job.

Correcting a mesh

Downloaded meshes arrive in inches, upside down, or far from the origin. Corrections are code: implement adjust(), which receives the selected body as a trimesh mesh and returns the corrected one.

class Bracket(StlNode):

    stl_source = 'bracket.stl'

    def adjust(self, mesh):
        mesh.apply_scale(25.4)
        mesh.apply_translation(-mesh.centroid)
        return mesh

Whatever the hook returns is what the artifact holds, and what the watertight gate judges.

What a mesh costs

An StlNode is faceted, and it makes any fusion holding it faceted: the fusion routes through OpenSCAD and CGAL, which needs the openscad binary and can take minutes on a dense mesh. Assembling imported parts without fusing them costs nothing extra. Mesh-only is the doctrine for imported STLs: a mesh is not a boundary representation, and rebuilding one from triangles guesses at intent. Model the part in a CAD backend if you need it exact.

A part from a STEP document

STEP is what every CAD package and every vendor publishes, and a STEP product is a boundary representation the moment it is read. StepNode is exact: shape(), the .brep artifact, exact fusion, the spatial assertions and tessellation precision all come for free, with no external tool.

from machinome.node import StepNode

class Bracket(StepNode):

    step_source = 'vendor/bracket.step'

Selecting a product

A STEP document is a tree of named products. part names the one this node is:

class Gearbox(StepNode):

    step_source = 'vendor/gearbox.step'
    part = 'Output_Shaft'

A file with exactly one candidate product, one part alone or one part wrapped in an assembly root, needs no part. A document with several candidates and no part, or a part the document does not have, fails with the document’s inventory: one line per product with its kind, occurrence count, solid count, bounds and volume. A sub-assembly is a selectable product too, arriving with its components at their internal placements.

Exporters often give several distinct products one generic name. Naming a shared name fails describing every match with an index; declare part_index beside part to choose, 1-based, in document order.

The part arrives in its own frame

A selected product’s geometry is its own, unplaced shape, never an occurrence’s located copy. Placing it is your assembly’s job, with the same operations as any node. To take the placements from the document itself, see import-step below.

Correcting and admitting

adjust() receives the selected product as a CadQuery Shape and returns the corrected one. A product that holds no solid after adjust, faces only, fails the build naming what it does hold; there is no admission flag, because a face-only B-rep would break every fusion and volume assertion. If a vendor published bare surfaces, sew them knowingly:

from machinome.node.adapters.step import solids_from_faces

class Battery(StepNode):

    step_source = 'vendor/robot.step'
    part = 'Battery'

    def adjust(self, shape):
        return solids_from_faces(shape, tolerance=0.05)

solids_from_faces sews within the tolerance and wraps each closed shell in a solid. It does not guarantee the shells close or that the tolerance is right for this file. A subclass that declares no color takes the product’s colour from the document, converted from linear RGB to the sRGB hex the project writes everywhere else.

Reading a document is the expensive part, fourteen to seventeen seconds measured on a 35 MB vendor assembly. A document is read at most once per file per process and shared by every node selecting from it; a node whose artifacts are current never triggers a read.

Tessellation precision

Every exact leaf writes its STL by tessellating its solid, and may declare how finely:

class OutputShaft(StepNode):

    step_source = 'vendor/actuator.step'
    part = 'Output_Shaft'
    angular_deflection = 0.5

linear_deflection (mm, default 0.1) bounds the distance between the mesh and the surface; angular_deflection (radians, default 0.1) bounds the angle between adjacent facet normals. Vendor geometry is fillets and threads, and 0.1 radian over such a part costs an order of magnitude for surface a viewer cannot see: the shaft above measured 19.9 MB at the default and 1.8 MB at 0.5. Only the mesh changes; the .brep and shape() are identical whatever is declared. A coarser mesh moves faceted-kernel verdicts and changes the printed-piece id, and never touches the exact kernel. A fusion declares its own precision and does not inherit a child’s.

Scaffold an assembly from a STEP document

$ machinome import-step vendor/robot.step --into robot --model robot

reads the document’s assembly structure and writes two files of project-owned source: parts.py, one StepNode subclass per part product, and assembly.py, one AssemblyNode per assembly product, declaring one child per occurrence and a render() that places each at the document’s own transform. Every generated class is a product, never a name, so a shared name gets a part_index. The generated model is a machine at rest: no driver, no simulate(). Which joints move is a design decision you make in the source the command hands you.

The command never overwrites, refuses a document with a mirrored or scaled placement, and prints the manifest line to add rather than editing pyproject.toml. Command line reference has its options.