Skip to content

Example: 5-Axis DED Welding

This is a line-by-line walkthrough of toolpath_engine/examples/demo_5axis_ded.py — a complete directed energy deposition (DED) welding job on a cylindrical workpiece.

The example demonstrates every major subsystem: geometry, machine definition, all four strategies, composable orientation, G-code output, collision checking, and the external engine wrapper.


1. Named positions and frames

home     = Position(0, 0, 300, name="home")
approach = Position(50, 0, 150, name="approach")
safe_z   = Variable("safe_z", 250, unit="mm")

wcs = Frame.from_origin_and_z(
    "workpiece",
    origin=(100, 100, 0),
    z_axis=(0, 0, 1),
)

Position and Variable are named primitives — they make process scripts readable and searchable. Frame defines a coordinate system for a workpiece or fixture offset.


2. Geometry

cylinder = Surface.cylinder(
    center=(0, 0, 0), axis=(0, 0, 1),
    radius=40, height=80,
    name="deposition_surface",
)

helix = Curve.helix(
    center=(0, 0, 0), radius=40,
    pitch=5, turns=4, num_points_per_turn=72,
)

ring     = Curve.circle(center=(0, 0, 20), radius=40, num_points=64)
top_plate = Surface.plane(origin=(0, 0, 80), normal=(0, 0, 1), size=60)

The cylinder is the deposition surface. The helix is the weld path — a multi-turn spiral wrapping around the cylinder at 5mm pitch per turn.

The geometry model groups everything with tags:

model = GeometryModel("weld_assembly")
model.add_surface(cylinder, tags=["weld_target"])
model.add_surface(top_plate, tags=["coat_target"])
model.add_curve(helix, tags=["weld_path"])

3. Machine kinematics

machine = Machine.gantry_5axis_ac(
    name="DED_5axis",
    travel=(500, 500, 400),
    a_limits=(-120, 120),
)
machine.set_tool_offset(z=100)
machine.config["process"]       = "DED"
machine.config["wire_diameter"] = 1.2

This is a standard 5-axis gantry with an AC rotary table. The 100mm tool offset accounts for the DED nozzle length. Process metadata goes in config — it doesn't affect kinematics but is preserved in YAML and available to post-processing.


4. Toolpath strategies

Four strategies are demonstrated:

# Follow the helical weld path
helix_paths = FollowCurveStrategy().generate(
    curve=helix, feed_rate=600, spacing=1.0, path_type="deposit",
)
helix_paths.set_param("wire_feed", 3.0)
helix_paths.set_param("laser_power", 2000)

# Single-pass ring weld
ring_paths = FollowCurveStrategy().generate(
    curve=ring, feed_rate=800, path_type="deposit",
)

# Raster fill on the top plate
fill_paths = RasterFillStrategy().generate(
    surface=top_plate, spacing=3.0, feed_rate=500,
    step_size=0.5, path_type="deposit",
)

# Contour-parallel passes from a circular boundary
boundary = Curve.circle(center=(0, 0, 80), radius=30, num_points=64)
contour_paths = ContourParallelStrategy().generate(
    boundary=boundary, stepover=3.0, num_passes=4,
    feed_rate=700, path_type="deposit",
)

Each strategy returns a ToolpathCollection. Process parameters set on a collection propagate to all points.


5. Orientation rules

# Helix: normal to cylinder, 10° lead, collision-safe
helix_paths.orient(to_normal(cylinder))
helix_paths.orient(lead(10))
helix_paths.orient(avoid_collision(machine, max_tilt=45))

# Ring: normal to cylinder only
ring_paths.orient(to_normal(cylinder))

# Top plate fill: fixed Z-down (3-axis behavior)
fill_paths.orient(fixed(0, 0, -1))

The helix uses the most complex chain: surface-normal orientation (5-axis), a forward lead angle (prevents wire-ahead-of-pool issues in DED), and a collision safety clamp.

The fill path is treated as 3-axis — no need for 5-axis orientation on a flat surface.


6. Assembling the full job

full_collection = ToolpathCollection(name="ded_weld_job")

# Approach
full_collection.add(Toolpath.rapid_to(home))
full_collection.add(Toolpath.rapid_to(approach))

# Helix passes
for tp in helix_paths:
    full_collection.add(tp)

# Retract, then ring pass
full_collection.add(Toolpath.rapid_to(approach))
for tp in ring_paths:
    full_collection.add(tp)

# Return home
full_collection.add(Toolpath.rapid_to(approach))
full_collection.add(Toolpath.rapid_to(home))

Toolpath.rapid_to(position) creates a single-point rapid move toolpath. Interleaving these with the welding passes produces the correct approach/retract sequence in the G-code output.


7. G-code output

config = PostConfig(
    program_number=1001,
    use_tcp=True,
    safe_start=["G90", "G21", "G17", "G40", "G54"],
    program_end=["M5", "G49", "G28 G91 Z0", "M30"],
)
config.param_codes["laser_power"] = "S"
config.param_codes["wire_feed"]   = "E"

post  = PostProcessor(machine, config)
gcode = post.process(full_collection, resolve_ik=False)

resolve_ik=False is used here because the example uses analytical geometry. In production with a real machine, set resolve_ik=True to solve IK for each point.


8. Collision check

checker    = CollisionChecker(max_tilt_deg=60)
sim_result = checker.run(full_collection, machine=machine)

print("PASS" if sim_result.success else "FAIL")
for msg in sim_result.messages:
    print(msg)

CollisionChecker verifies that no toolpath point has a tool axis tilt exceeding the limit. The full simulation plugin interface supports custom collision geometry — see toolpath_engine/simulation/__init__.py.


9. External engine wrapper

wrapper = EngineWrapper("demo_slicer")

@wrapper.before_slice
def log_input(geometry, params):
    return geometry, params          # pre-process inputs

@wrapper.on_each_point
def add_layer_time(point):
    point.process_params["layer_time"] = 2.5
    return point                     # augment each point

@wrapper.after_slice
def log_output(collection):
    return collection                # post-process output

wrapper.set_engine(my_external_slicer)
result = wrapper.run("model.stl", {"layer_height": 0.3})

The wrapper pattern lets you hook into any external slicer or CAM engine and use UTDE's kinematics and post-processing on the output.