#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
Offset Line/Arc segments in a tool path to compensate for tool trail offset.
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from future_builtins import *
import math
import logging
import geom
from . import toolpath
from . import util
logger = logging.getLogger(__name__)
[docs]def offset_path(path, offset, min_arc_dist, g1_tolerance=None):
"""Recalculate path to compensate for a trailing tangential offset.
This will shift all of the segments by `offset` amount. Arcs will
be recalculated to correct for the shift offset.
Args:
path: The path to recalculate.
offset: The amount of tangential tool trail.
min_arc_dist: The minimum distance between two connected
segment end points that can be bridged with an arc.
A line will be used if the distance is less than this.
g1_tolerance: The angle tolerance to determine if two segments
are g1 continuous.
Returns:
A new path
Raises:
:class:`cam.toolpath.ToolpathException`: if the path contains segment
types other than Line or Arc.
"""
if geom.float_eq(offset, 0.0):
return path;
offset_path = []
prev_seg = None
prev_offset_seg = None
for seg in path:
if seg.p1 == seg.p2:
# Skip zero length segments
continue
if isinstance(seg, geom.Line):
# Line segments are easy - just shift them forward by offset
offset_seg = seg.shift(offset)
elif isinstance(seg, geom.Arc):
offset_seg = offset_arc(seg, offset)
else:
raise toolpath.ToolpathException('Unrecognized path segment type.')
# Fix discontinuities caused by offsetting non-G1 segments
if prev_seg is not None:
if prev_offset_seg.p2 != offset_seg.p1:
seg_distance = prev_offset_seg.p2.distance(offset_seg.p1)
# If the distance between the two segments is less than the
# minimum arc distance or if the segments are G1 continuous
# then just insert a connecting line.
if (seg_distance < min_arc_dist
or geom.segments_are_g1(prev_offset_seg, offset_seg,
g1_tolerance)):
connect_seg = geom.Line(prev_offset_seg.p2, offset_seg.p1)
else:
# Insert an arc in tool path to rotate the tool to the next
# starting tangent when the segments are not G1 continuous.
# TODO: avoid creating tiny segments by extending
# offset segment.
p1 = prev_offset_seg.p2
p2 = offset_seg.p1
angle = prev_seg.p2.angle2(p1, p2)
# TODO: This should be a straight line if the arc is tiny
connect_seg = geom.Arc(p1, p2, offset, angle, prev_seg.p2)
# if connect_seg.length() < 0.01:
# logger.debug('tiny arc! length= %f, radius=%f, angle=%f', connect_seg.length(), connect_seg.radius, connect_seg.angle)
connect_seg.inline_start_angle = prev_seg.end_tangent_angle()
connect_seg.inline_end_angle = seg.start_tangent_angle()
offset_path.append(connect_seg)
prev_offset_seg = connect_seg
elif (geom.segments_are_g1(prev_seg, seg, g1_tolerance) and
not hasattr(prev_seg, 'ignore_g1') and
not hasattr(seg, 'ignore_g1')):
# Add hint for smoothing pass
prev_offset_seg.g1 = True
prev_seg = seg
prev_offset_seg = offset_seg
offset_path.append(offset_seg)
# Compensate for starting angle
start_angle = (offset_path[0].p1 - path[0].p1).angle()
offset_path[0].inline_start_angle = start_angle
return offset_path
[docs]def offset_arc(arc, offset):
"""Offset the arc by the specified offset.
"""
start_angle = arc.start_tangent_angle()
end_angle = arc.end_tangent_angle()
p1 = arc.p1 + geom.P.from_polar(offset, start_angle)
p2 = arc.p2 + geom.P.from_polar(offset, end_angle)
radius = math.hypot(offset, arc.radius)
offset_arc = geom.Arc(p1, p2, radius, arc.angle, arc.center)
offset_arc.inline_start_angle = start_angle
offset_arc.inline_end_angle = end_angle
return offset_arc
[docs]def fix_G1_path(path, tolerance, line_flatness):
"""
"""
new_path = []
if len(path) < 2:
return path
seg1 = path[0]
cp1 = seg1.p1
for seg2 in path[1:]:
if getattr(seg1, 'g1', False):
arcs, cp1 = smoothing_arcs(seg1, seg2, cp1,
tolerance=tolerance, max_depth=1,
line_flatness=line_flatness)
new_path.extend(arcs)
else:
cp1 = seg2.p1
new_path.append(seg1)
seg1 = seg2
# Process last segment...
if getattr(seg1, 'g1', False):
arcs, cp1 = smoothing_arcs(seg1, None, cp1,
tolerance=tolerance, max_depth=1,
line_flatness=line_flatness)
new_path.extend(arcs)
else:
new_path.append(seg1)
return new_path
[docs]def smoothing_arcs(seg1, seg2, cp1=None,
tolerance=0.0001, line_flatness=0.0001,
max_depth=1, match_arcs=True):
"""Create circular smoothing biarcs between two segments
that are not currently G1 continuous.
Args:
seg1: First path segment containing first and second points.
Can be a geom.Line or geom.Arc.
seg2: Second path segment containing second and third points.
Can be a geom.Line or geom.Arc.
cp1: Control point computed from previous invocation.
tolerance: Biarc matching tolerance.
line_flatness: Curve to line tolerance.
max_depth: Max Bezier subdivision recursion depth.
match_arcs: Attempt to more closely match existing arc segments.
Default is True.
Returns:
A tuple containing a list of biarc segments and the control point
for the next curve.
"""
curve, cp1 = geom.bezier.smoothing_curve(seg1, seg2, cp1, match_arcs)
# geom.debug.draw_bezier(curve, color='#00ff44') #DEBUG
biarc_segs = curve.biarc_approximation(tolerance=tolerance,
max_depth=max_depth,
line_flatness=line_flatness)
if not biarc_segs:
return ((seg1,), seg1.p2)
# Compute total arc length of biarc approximation
biarc_length = 0
for seg in biarc_segs:
biarc_length += seg.length()
# Fix inline rotation hints for each new arc segment.
a_start = util.seg_start_angle(seg1)
a_end = a_start
sweep = geom.normalize_angle(util.seg_end_angle(seg1) - a_start, center=0.0)
sweep_scale = sweep / biarc_length
for arc in biarc_segs:
a_end = a_start + (arc.length() * sweep_scale)
arc.inline_start_angle = a_start
arc.inline_end_angle = a_end
a_start = a_end
return (biarc_segs, cp1)