Surface Finish: Tool Paths and Robot Motion

SurfaceFinishService (client.surface_finish) builds a tool path on an object and turns it into robot motion, for sanding, polishing, painting, dispensing and similar tasks. It works in stages. Data from every stage is kept and can be read, and redoing a stage clears the results of later stages.

Note

SurfaceFinishService requires the surface_finish license (Activate Licenses).

Stage 1                    Stage 2                  Stage 3                       Export
GenerateNominalPath  ──▶  RefinePath  ──────────▶  CreateRobotMotion  ──────▶  GenerateRCATrajectoryFile
nominal_cartesian          refined_cartesian        basic_solution /
                                                    advanced_solution

The result is a surface_finish.Path config (type CONFIG_TYPE_SURFACE_PATH) parented to the object. Read it with SurfaceFinishService.Get or ConfigService.Get.

Stage 1: nominal path

GenerateNominalPath(GenerateNominalPathRequest) takes a header (the path name, with parent set to the object the path lies on) and one path generator. It returns path_names: one request can create several paths, for example a raster split into separate passes.

Path generators

Request field

Source

Description

manual_path

any

ManualPath: you provide CartesianWaypoint s, relative to the parent.

mesh_sketch_path

mesh

MeshSketchPath: a polyline through points on the mesh surface (parent frame), resampled at fixed_spacing or variable_spacing.

mesh_raster_path

mesh

MeshRasterPath: a raster (zig-zag) pattern covering the mesh or a polygon_border region.

edge_path

CAD

EdgePath: follows CAD edges; adjacent faces define the tool Z axis.

surface_scan_path

CAD

FaceRasterPath: a raster pattern on CAD faces.

surface_sketch_path

CAD

FaceSketchPath: a path through sketch points on CAD faces.

Mesh sketch path

path_req = GenerateNominalPathRequest(
    header=ConfigHeader(
        name="path_long_sketch",
        parent="mesh_1"
    ),
    mesh_sketch_path=MeshSketchPath(
        points=[
            Vector3(x=-0.05, y=0.0, z=0.00),
            Vector3(x=-0.45, y=0.0, z=0.00)],
        blending_radius=0.0,
        fixed_spacing=0.01,
        align_with_initial_direction=True
    )
)
path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
if (path_response.status.code == 0) and len(path_response.path_names) > 0:
    print("Successfully generated sketch path for the mesh.")
else:
    print(f"Surface finish service call failed: {path_response.status.message}")
    return

Mesh raster path

path_req = GenerateNominalPathRequest(
    header=ConfigHeader(
        name="path",
        parent="mesh_1"
    ),
    mesh_raster_path=MeshRasterPath(
        fixed_spacing=0.02,
        blending_radius=0.0,
        border_offset=0.0,
        raster_line_spacing=0.04,
        raster_path_joining_distance_threshold=0.08,
        principal_rotation_angle=0.0,
        align_with_initial_direction=True
    )
)
path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
if (path_response.status.code == 0) and len(path_response.path_names) > 0:
    print("Successfully generated raster path for the mesh.")
else:
    print(f"Surface finish service call failed: {path_response.status.message}")
    return

Key MeshRasterPath parameters (all lengths in m, angles in rad):

raster_line_spacing

Distance between raster lines.

fixed_spacing

Distance between waypoints along a line.

blending_radius

Corner blending radius.

border_offset

Inward offset from the region border.

raster_path_joining_distance_threshold

Lines closer than this are joined into one continuous path.

principal_rotation_angle or specified_direction

Raster direction: an angle about the mesh’s principal direction, or a world-frame vector projected onto the mesh.

minimum_toolpath_length

Shorter paths after splitting are dropped.

align_with_initial_direction / align_with_previous / align_with_direction

How each waypoint’s X axis is oriented.

CAD-based generators

EdgePath, FaceRasterPath and FaceSketchPath follow the CAD edges or faces listed in edges or faces. Each entry is the OCCT entry string (TDF_Label) of a TopoDS_Edge or TopoDS_Face in the STEP file. In C++, flexiv::pdk::surface_finish::ToOcctEntryString(shape, shape_tool) (<flexiv/pdk/surface_finish/occt_entry_string.hpp>) computes it. The helper exists only in builds with FLEXIV_PDK_ENABLE_OCCT_INTEGRATION=ON, and the binary release ships its header only when the library contains it.

The object must have a Visual whose mesh.filename is the STEP file. If an object has several visual files, set cad_filename (or mesh_filename) to choose one.

Waypoint spacing for CAD paths (and for MeshSketchPath) is either fixed_spacing (m) or variable_spacing, a VariableSpacing (min_spacing, max_spacing, position_tolerance, orientation_tolerance). FaceRasterPath can be bounded by an EdgeBorder (keep a distance from given edges) or a PolygonBorder.

Stage 2: refinement

Optional. RefinePath(RefinePathRequest(path_name, refinement)) modifies the nominal waypoints. Stage 3 uses refined_cartesian if it is present, otherwise nominal_cartesian. PathRefinement operations are applied in this order:

  1. reversed_order: reverse the waypoint order;

  2. align_with_previous: rotate each waypoint about its local Z to align with the previous one;

  3. rotation_offset: rotate each waypoint in its own frame;

  4. position_offset: translate each waypoint in its own frame.

Warning

Set rotation_offset to the identity (w=1) if you do not want a rotation. An unset quaternion is all zeros.

Stage 3: robot motion

CreateRobotMotion(CreateRobotMotionRequest(path_name, solver_preferences)) computes joint trajectories.

SolverPreferences

Field

Description

system_name

Required. The system that executes the path.

tcp_ecp_name

Required. The TCP (tool) or ECP (external control point) that follows the path, e.g. "system_1.arm.flange".

solver_type

SOLVER_TYPE_BASIC (result in basic_solution) or SOLVER_TYPE_ADVANCED (result in advanced_solution).

null_space

Desired elbow swivel angle and external axis positions.

external_robot_axis, external_workpiece_axis

How external axes may move (none, preset, preferred, not used).

tool_symmetry

Ignore rotation about the tool axis (for example, polishing wheels).

joint_position_seed

Start configuration. If empty, the current robot state is used.

tcp_to_attach_object, object_grasp_pose

ECP only: the robot holds the part and moves it past a fixed tool.

print("Running basic solve for the generated long path...")
try:
    for path_name in path_long_sketch_names:
        solve_req = CreateRobotMotionRequest(
            path_name=path_name,
            solver_preferences=SolverPreferences(
                system_name="system_1",
                tcp_ecp_name="system_1.arm.flange",
                solver_type=SolverPreferences.SOLVER_TYPE_BASIC
            )
        )
        solve_response = client.surface_finish.stub.CreateRobotMotion(solve_req)
        if (solve_response.status.code == 0):
            print(f"Successfully created robot motion for path {path_name}.")
        else:
            print(f"Failed to create robot motion for path {path_name}: {solve_response.status.message}")
except grpc.RpcError as e:
    print(f"Failed to solve path config: {e}")
    return

Process parameters and export

Each CartesianWaypoint carries velocity, acceleration and a wrench (force/torque to apply in contact). Edit them on the path and save it back with ConfigService.Save(SaveConfigRequest(surface_path=...)). Then GenerateRCATrajectoryFile writes a robot trajectory (.traj) file on the server, at <workcell>/linear_path/<path_name>/<path_name>.traj, where <workcell> is the active workcell’s folder in the server’s workcell directory (flexiv_pdk_data/workcell/ for the containerized server, see Set Up the PDK Server).

# velocity and acceleration for the robot motion
print("Setting Cartesian velocity, acceleration, and force for the generated path...")
try:
    for path_name in path_long_sketch_names:
        get_req = GetRequest(path_name=path_name)
        get_response = client.surface_finish.stub.Get(get_req)
        if (get_response.status.code == 0):
            path_config = get_response.path
            for waypoint in path_config.nominal_cartesian:
                waypoint.velocity = 0.1
                waypoint.acceleration = 0.2
                waypoint.wrench.force.z = 5
            for waypoint in path_config.refined_cartesian:
                waypoint.velocity = 0.1
                waypoint.acceleration = 0.2
                waypoint.wrench.force.z = 5
            save_req = SaveConfigRequest(surface_path=path_config)
            client.config.stub.Save(save_req)
            print(f"Successfully set Cartesian velocity, acceleration, and force for path {path_name}.")
        else:
            print(f"Failed to get path config for {path_name}: {get_response.status.message}")
except grpc.RpcError as e:
    print(f"Failed to get path config: {e}")
    return

# 7. Generate the RCA trajectory for the robot motion. 
# The generated trajectory .traj file will be saved in 
# the PDK server as {workcell}/linear_path/{path_name}/{path_name}.traj.
print("Generating RCA trajectory for the robot motion...")
try:
    for path_name in path_long_sketch_names:
        generate_rca_traj_req = GenerateRCATrajectoryFileRequest(
            path_name=path_name,
        )
        generate_rca_traj_response = client.surface_finish.stub.GenerateRCATrajectoryFile(generate_rca_traj_req)
        if (generate_rca_traj_response.status.code == 0):
            print(f"Successfully created RCA trajectory for path {path_name}.")
        else:
            print(f"Failed to create RCA trajectory for path {path_name}: {generate_rca_traj_response.status.message}")
except grpc.RpcError as e:
    print(f"Failed to generate RCA trajectory: {e}")
    return

Complete example

The example prints grpc.RpcError instead of raising it and does not check every status.code. Production code should check both; see Error Handling.

"""
Example: Create a toolpath sketch from a plane mesh and generate a RCA trajectory for a robot to follow the path.

This script demonstrates how to:
  1. Create a new workcell
  2. Add a plane mesh object to the scene
  3. Add a robot system to the scene
  4. Generate a sketch path for a mesh using the surface finish service
  5. Solve for the robot motion along the generated path
  6. Set Cartesian velocity, acceleration, and force for the robot motion
  7. Generate the RCA trajectory for the robot motion

  Note: To run this example, copy the plane mesh file "plane_1m_10x10.obj" in the assets directory to "flexiv_pdk_data/user_data/meshes/" on the server machine. That folder is beside your flexiv_pdk folder.
"""
import os
import grpc
import time

from flexiv.pdk import Client
from flexiv.pdk.workcell.workcell_service_pb2 import CreateRequest, NameRequest
from flexiv.pdk.config_service_pb2 import SaveConfigRequest, TypeNameConfigRequest, CONFIG_TYPE_SURFACE_PATH
from flexiv.pdk.scene.object_pb2 import Object
from flexiv.pdk.system.system_pb2 import System
from flexiv.pdk.system.components.manipulator_pb2 import Manipulator
from flexiv.pdk.config_header_pb2 import ConfigHeader
from flexiv.pdk.spatial.geometry_pb2 import SE3Pose, Vector3, Quaternion
from flexiv.pdk.spatial.shape_pb2 import Visual, Mesh, Shape

from flexiv.pdk.surface_finish.service_pb2 import GenerateNominalPathRequest, CreateRobotMotionRequest, GetRequest, GenerateRCATrajectoryFileRequest
from flexiv.pdk.surface_finish.solver_preferences_pb2 import SolverPreferences
from flexiv.pdk.surface_finish.generator.mesh_sketch_path_pb2 import MeshSketchPath

# Helper to construct header
def make_header(name, parent="world", px=0.0, py=0.0, pz=0.0, rw=1.0, rx=0.0, ry=0.0, rz=0.0):
    return ConfigHeader(
        name=name,
        parent=parent,
        pose_in_parent=SE3Pose(
            position=Vector3(x=px, y=py, z=pz),
            rotation=Quaternion(x=rx, y=ry, z=rz, w=rw)
        )
    )

def make_mesh_visual(origin, mesh_path, scale=1.0):
    return Visual(
        origin=SE3Pose(
            position=Vector3(x=origin[0], y=origin[1], z=origin[2]),
            rotation=Quaternion(x=0.0, y=0.0, z=0.0, w=1.0)
        ),
        shape=Shape(
            mesh=Mesh(
                filename=mesh_path,
                scale=Vector3(x=scale, y=scale, z=scale)
            )
        )
    )

def main():
    target = os.environ.get("PDK_SERVER_ADDR", "localhost:50052")
    print(f"Connecting to the PDK server at {target}...")

    # We use insecure channel, wrapped in our Client
    with Client(target) as client:
        # 1. Create a workcell
        workcell_name = "example_workcell"

        # Remove any existing workcell
        print(f"Removing any existing workcell with name: {workcell_name}")
        try:
            client.workcell.stub.Remove(NameRequest(name=workcell_name))
        except grpc.RpcError as e:
            print(f"Remove workcell failed or not present: {e}")

        print(f"Creating workcell: {workcell_name}")
        req = CreateRequest(name=workcell_name, description="Example workcell for linear path trajectory generation")
        try:
            client.workcell.stub.Create(req)
            client.workcell.stub.Load(NameRequest(name=workcell_name))
        except grpc.RpcError as e:
            print(f"Workcell operation failed or environment not running a server yet: {e}")        

        # 2. Add plane mesh object to the scene
        print("Adding plane mesh object to the scene...")
        obj1 = Object(header=make_header("mesh_1", px=0.8, py=0.0, pz=0.0), visual=[make_mesh_visual((0.0, 0.0, 0.0), "/home/flexiv/user_data/meshes/plane_1m_10x10.obj")])
        try:
            client.config.stub.Save(SaveConfigRequest(object=obj1))
        except grpc.RpcError:
            pass
        
        # 3. Add a robot system to the scene
        print("Saving robot system to the scene...")
        sys1 = System(
            header=make_header("system_1", px=0.05, py=0.0, pz=0.0),
            control_box_serial_number="CX01-01-T1-000001",
            manipulator=Manipulator(
                name="arm",
                arm_serial_number="A02L-00-M3-000001"
            ),
        )
        try:
            client.config.stub.Save(SaveConfigRequest(system=sys1))
        except grpc.RpcError:
            pass

        # 4. Run the surface finish service to generate a sketch path for the mesh. The specified points
        # are in the mesh's local frame, and the generated path will be saved as a new config in the scene.
        print("Generating sketch path for the mesh...")
        try:
            path_req = GenerateNominalPathRequest(
                header=ConfigHeader(
                    name="path_long_sketch",
                    parent="mesh_1"
                ),
                mesh_sketch_path=MeshSketchPath(
                    points=[
                        Vector3(x=-0.05, y=0.0, z=0.00),
                        Vector3(x=-0.45, y=0.0, z=0.00)],
                    blending_radius=0.0,
                    fixed_spacing=0.01,
                    align_with_initial_direction=True
                )
            )
            path_response = client.surface_finish.stub.GenerateNominalPath(path_req)
            if path_response.status.code != 0:
                print(f"Surface finish service call failed: {path_response.status.message}")
                return
            print("Successfully generated sketch path for the mesh.")
        except grpc.RpcError as e:
            print(f"Surface finish service call failed: {e}")
            return

        path_long_sketch_names = path_response.path_names

        
        # 5. Run Basic solve to compute the robot joint positions for the generated path
        print("Running basic solve for the generated long path...")
        try:
            for path_name in path_long_sketch_names:
                solve_req = CreateRobotMotionRequest(
                    path_name=path_name,
                    solver_preferences=SolverPreferences(
                        system_name="system_1",
                        tcp_ecp_name="system_1.arm.flange",
                        solver_type=SolverPreferences.SOLVER_TYPE_BASIC
                    )
                )
                solve_response = client.surface_finish.stub.CreateRobotMotion(solve_req)
                if (solve_response.status.code == 0):
                    print(f"Successfully created robot motion for path {path_name}.")
                else:
                    print(f"Failed to create robot motion for path {path_name}: {solve_response.status.message}")
        except grpc.RpcError as e:
            print(f"Failed to solve path config: {e}")
            return

        # 6. Get the generated path config and set the Cartesian 
        # velocity and acceleration for the robot motion
        print("Setting Cartesian velocity, acceleration, and force for the generated path...")
        try:
            for path_name in path_long_sketch_names:
                get_req = GetRequest(path_name=path_name)
                get_response = client.surface_finish.stub.Get(get_req)
                if (get_response.status.code == 0):
                    path_config = get_response.path
                    for waypoint in path_config.nominal_cartesian:
                        waypoint.velocity = 0.1
                        waypoint.acceleration = 0.2
                        waypoint.wrench.force.z = 5
                    for waypoint in path_config.refined_cartesian:
                        waypoint.velocity = 0.1
                        waypoint.acceleration = 0.2
                        waypoint.wrench.force.z = 5
                    save_req = SaveConfigRequest(surface_path=path_config)
                    client.config.stub.Save(save_req)
                    print(f"Successfully set Cartesian velocity, acceleration, and force for path {path_name}.")
                else:
                    print(f"Failed to get path config for {path_name}: {get_response.status.message}")
        except grpc.RpcError as e:
            print(f"Failed to get path config: {e}")
            return

        # 7. Generate the RCA trajectory for the robot motion. 
        # The generated trajectory .traj file will be saved in 
        # the PDK server as {workcell}/linear_path/{path_name}/{path_name}.traj.
        print("Generating RCA trajectory for the robot motion...")
        try:
            for path_name in path_long_sketch_names:
                generate_rca_traj_req = GenerateRCATrajectoryFileRequest(
                    path_name=path_name,
                )
                generate_rca_traj_response = client.surface_finish.stub.GenerateRCATrajectoryFile(generate_rca_traj_req)
                if (generate_rca_traj_response.status.code == 0):
                    print(f"Successfully created RCA trajectory for path {path_name}.")
                else:
                    print(f"Failed to create RCA trajectory for path {path_name}: {generate_rca_traj_response.status.message}")
        except grpc.RpcError as e:
            print(f"Failed to generate RCA trajectory: {e}")
            return

if __name__ == "__main__":
    main()