After mastering MoveJ and MoveL, you can program robots to move in straight lines and perform free movements. But the real world is not just straight lines — many industrial applications require circular motion: curved weld seams, polishing round surfaces, screw driving, and orbital inspection. That is where MoveC comes in.
MoveC — Circular Arc Motion
How It Works
MoveC creates motion along a circular arc through 3 points:
- Start point ($P_0$): Current TCP position
- Via point ($P_1$): Intermediate point on the arc
- End point ($P_2$): Target point
Three points uniquely define a plane and a circle. The controller computes:
- Circle center from 3 points
- Radius of the arc
- Plane containing the arc
- Divides the arc into many interpolation points
- At each point: solves IK (same as MoveL)
Algorithm: Finding Circle Center from 3 Points
import numpy as np
def circle_from_3_points(p0, p1, p2):
"""
Find center and radius of circle through 3 points in 3D.
Returns:
center: circle center [x, y, z]
radius: radius
normal: plane normal vector
"""
v1 = p1 - p0
v2 = p2 - p0
normal = np.cross(v1, v2)
normal = normal / np.linalg.norm(normal)
m1 = (p0 + p1) / 2
m2 = (p0 + p2) / 2
d1 = np.cross(v1, normal)
d2 = np.cross(v2, normal)
A = np.column_stack([d1, -d2])
b = m2 - m1
params = np.linalg.lstsq(A, b, rcond=None)[0]
center = m1 + params[0] * d1
radius = np.linalg.norm(center - p0)
return center, radius, normal
p0 = np.array([0.4, 0.0, 0.3])
p1 = np.array([0.3, 0.1, 0.3])
p2 = np.array([0.2, 0.0, 0.3])
center, radius, normal = circle_from_3_points(p0, p1, p2)
print(f"Center: {center}")
print(f"Radius: {radius:.4f} m")
print(f"Normal: {normal}")
URScript MoveC
# URScript
# movec(pose_via, pose_to, a=1.2, v=0.25, r=0, mode=0)
# pose_via: via point (intermediate)
# pose_to: end point (target)
# a: acceleration [m/s²]
# v: velocity [m/s]
# r: blend radius [m]
# mode: 0 = unconstrained, 1 = fixed orientation
# Simple arc
movec(p[0.3, 0.1, 0.3, 0, 3.14, 0], # Via point
p[0.2, 0.0, 0.3, 0, 3.14, 0], # End point
a=1.2, v=0.1)
# Full circle (via = opposite point through center)
movec(p[0.2, 0.0, 0.3, 0, 3.14, 0],
p[0.4, 0.0, 0.3, 0, 3.14, 0],
a=1.2, v=0.05)
Python Implementation
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def generate_arc_trajectory(p0, p1, p2, n_points=100):
"""Generate circular arc trajectory through 3 points."""
center, radius, normal = circle_from_3_points(p0, p1, p2)
v_start = p0 - center
v_end = p2 - center
u = v_start / np.linalg.norm(v_start)
w = np.cross(normal, u)
theta_start = np.arctan2(np.dot(v_start, w), np.dot(v_start, u))
theta_end = np.arctan2(np.dot(v_end, w), np.dot(v_end, u))
v_via = p1 - center
theta_via = np.arctan2(np.dot(v_via, w), np.dot(v_via, u))
if theta_end < theta_start:
if theta_via > theta_start or theta_via < theta_end:
pass
else:
theta_end += 2 * np.pi
else:
if theta_start < theta_via < theta_end:
pass
else:
theta_end -= 2 * np.pi
thetas = np.linspace(theta_start, theta_end, n_points)
positions = []
for theta in thetas:
point = center + radius * (np.cos(theta) * u + np.sin(theta) * w)
positions.append(point)
return np.array(positions), center, radius
p0 = np.array([0.4, 0.0, 0.3])
p1 = np.array([0.3, 0.1, 0.3])
p2 = np.array([0.2, 0.0, 0.3])
arc_points, center, radius = generate_arc_trajectory(p0, p1, p2)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(arc_points[:, 0], arc_points[:, 1], arc_points[:, 2],
'b-', linewidth=2, label='Arc path')
ax.scatter(*p0, c='green', s=100, zorder=5, label='Start')
ax.scatter(*p1, c='orange', s=100, zorder=5, label='Via')
ax.scatter(*p2, c='red', s=100, zorder=5, label='End')
ax.scatter(*center, c='purple', s=80, marker='+', zorder=5, label='Center')
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title(f'MoveC Arc Trajectory (r={radius:.3f}m)')
ax.legend()
plt.tight_layout()
plt.savefig('movec_arc.png', dpi=150)
plt.show()
Full Circle vs Partial Arc
Partial Arc (Most Common)
A partial circular arc — for example welding 1/4 or 1/2 of a circle:
# URScript — welding a 90-degree arc
movel(p_start_weld, a=0.5, v=0.01) # Move to weld start
set_digital_out(1, True) # Enable welding torch
movec(p_via_weld, p_end_weld, a=0.3, v=0.005) # Weld along arc
set_digital_out(1, False) # Disable welding torch
movel(p_retreat, a=1.2, v=0.25) # Retreat
Full Circle
For a complete circle, end point = start point, via point = opposite point:
def generate_full_circle(center, radius, normal, n_points=200):
"""Generate full circle trajectory."""
u = np.array([1, 0, 0])
if abs(np.dot(u, normal)) > 0.9:
u = np.array([0, 1, 0])
u = u - np.dot(u, normal) * normal
u = u / np.linalg.norm(u)
w = np.cross(normal, u)
thetas = np.linspace(0, 2*np.pi, n_points, endpoint=False)
positions = []
for theta in thetas:
point = center + radius * (np.cos(theta) * u + np.sin(theta) * w)
positions.append(point)
return np.array(positions)
circle_path = generate_full_circle(
center=np.array([0.3, 0.0, 0.3]),
radius=0.1,
normal=np.array([0, 0, 1])
)
Helix — Circular + Linear Motion
A helix (spiral) combines circular motion with linear movement along an axis. Applications:
- Screw driving: Rotate + push down
- Cylindrical polishing: Orbit around axis + move along it
- Drilling: Rotate + advance
def generate_helix(center, radius, normal, pitch, n_turns, n_points=500):
"""
Generate helix trajectory.
Args:
center: start center position
radius: helix radius
normal: axis direction (unit vector)
pitch: distance traveled along axis per revolution
n_turns: number of turns
n_points: number of trajectory points
"""
u = np.array([1, 0, 0])
if abs(np.dot(u, normal)) > 0.9:
u = np.array([0, 1, 0])
u = u - np.dot(u, normal) * normal
u = u / np.linalg.norm(u)
w = np.cross(normal, u)
total_angle = 2 * np.pi * n_turns
thetas = np.linspace(0, total_angle, n_points)
positions = []
for theta in thetas:
circle = radius * (np.cos(theta) * u + np.sin(theta) * w)
linear = (theta / (2 * np.pi)) * pitch * normal
positions.append(center + circle + linear)
return np.array(positions)
# Helix for screw driving
helix_path = generate_helix(
center=np.array([0.3, 0.2, 0.3]),
radius=0.005, # 5mm radius (small — simulating screw driving)
normal=np.array([0, 0, -1]), # Downward
pitch=0.001, # 1mm per revolution (thread pitch)
n_turns=5
)
# Helix for polishing
polishing_helix = generate_helix(
center=np.array([0.3, 0.0, 0.3]),
radius=0.05,
normal=np.array([0, 0, 1]),
pitch=0.02,
n_turns=3
)
fig = plt.figure(figsize=(14, 6))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot(helix_path[:, 0], helix_path[:, 1], helix_path[:, 2],
'b-', linewidth=1.5)
ax1.set_title('Screw Driving Helix')
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot(polishing_helix[:, 0], polishing_helix[:, 1], polishing_helix[:, 2],
'r-', linewidth=1.5)
ax2.set_title('Polishing Helix')
plt.tight_layout()
plt.savefig('helix_trajectories.png', dpi=150)
plt.show()
Orbital Paths — Inspection and Scanning
Orbital path — the robot moves on a circular orbit around an object, keeping the tool pointed toward the center. Applications:
- Quality inspection: Camera orbits around product
- 3D scanning: 360-degree LiDAR/camera
- Cylindrical spray painting: Uniform distance
def generate_orbital_path(object_center, orbit_radius, orbit_height,
n_points=100, n_layers=3, layer_spacing=0.05):
"""
Generate orbital inspection path.
Robot orbits around object, looking inward.
"""
positions = []
orientations = []
for layer in range(n_layers):
z = orbit_height + layer * layer_spacing
thetas = np.linspace(0, 2*np.pi, n_points, endpoint=False)
for theta in thetas:
x = object_center[0] + orbit_radius * np.cos(theta)
y = object_center[1] + orbit_radius * np.sin(theta)
positions.append([x, y, z])
look_dir = object_center[:2] - np.array([x, y])
yaw = np.arctan2(look_dir[1], look_dir[0])
orientations.append(yaw)
return np.array(positions), orientations
orbit_pos, orbit_orient = generate_orbital_path(
object_center=np.array([0.3, 0.0, 0.2]),
orbit_radius=0.15,
orbit_height=0.15,
n_points=50,
n_layers=3,
layer_spacing=0.04
)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot(orbit_pos[:, 0], orbit_pos[:, 1], orbit_pos[:, 2],
'b-', linewidth=1, alpha=0.7)
ax.scatter(0.3, 0.0, 0.2, c='red', s=200, marker='*', label='Object')
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('Orbital Inspection Path (3 layers)')
ax.legend()
plt.tight_layout()
plt.savefig('orbital_path.png', dpi=150)
plt.show()
Common Pitfalls
1. Collinear Points
If the 3 points lie on a straight line, a circle cannot be determined — MoveC will error.
def check_collinear(p0, p1, p2, threshold=1e-6):
"""Check if 3 points are collinear."""
v1 = p1 - p0
v2 = p2 - p0
cross = np.linalg.norm(np.cross(v1, v2))
return cross < threshold
# BAD — 3 collinear points
p0 = np.array([0.2, 0.0, 0.3])
p1 = np.array([0.3, 0.0, 0.3])
p2 = np.array([0.4, 0.0, 0.3])
print(f"Collinear: {check_collinear(p0, p1, p2)}") # True — ERROR!
2. Orientation Flipping
During circular motion, orientation can "flip" abruptly — especially when the arc exceeds 180 degrees.
Solution: Use mode=1 (fixed orientation) in URScript or divide the arc into smaller segments.
3. Radius Too Large/Small
- Radius too large — Arc approaches a straight line — use MoveL instead
- Radius too small — High centripetal acceleration — robot slows down or errors
Practical Applications
Circular Weld Seam
# URScript — circular weld seam
def circular_weld(center, radius, weld_speed=0.005):
"""Weld a circular seam."""
import math
start_x = center[0] + radius
movel(p[start_x, center[1], center[2]+0.05, 0, 3.14, 0],
a=1.2, v=0.25)
movel(p[start_x, center[1], center[2], 0, 3.14, 0],
a=0.5, v=0.05)
set_digital_out(1, True)
sleep(0.5)
movec(
p[center[0], center[1]+radius, center[2], 0, 3.14, 0],
p[center[0]-radius, center[1], center[2], 0, 3.14, 0],
a=0.3, v=weld_speed
)
movec(
p[center[0], center[1]-radius, center[2], 0, 3.14, 0],
p[start_x, center[1], center[2], 0, 3.14, 0],
a=0.3, v=weld_speed
)
set_digital_out(1, False)
movel(p[start_x, center[1], center[2]+0.05, 0, 3.14, 0],
a=1.2, v=0.25)
References
- Universal Robots. URScript Programming Language — movec function. UR Documentation
- Gasparetto, A. et al. "Path Planning and Trajectory Planning Algorithms." Springer, 2015. DOI: 10.1007/978-3-319-14705-5_1
- Lynch, K.M., Park, F.C. Modern Robotics: Mechanics, Planning, and Control. Cambridge, 2017. Link
Conclusion
MoveC extends robot programming capabilities beyond straight lines:
- Arc — circular arc through 3 points for welding, cutting
- Helix — spiral for screw driving, cylindrical polishing
- Orbital — inspection orbit around objects
In the next post, we will learn how to blend motion segments — the most important technique for optimizing cycle time while maintaining smooth motion.
Related Posts
- MoveJ & MoveL Explained — The two basic commands before learning MoveC
- Grasping & Manipulation Basics — Grasping techniques combining MoveL and MoveC
- ROS 2 Control Hardware Interface — Sending circular trajectories via ros2_control