Commit 50e2f2ba authored by sumpfralle's avatar sumpfralle

separated handling of LineGroups (for contour classes)

skip unnecessary double-check of line collisions
moved "MODEL_TRANSFORMATIONS" from pycam.Geometry.Model to pycam.Geometry.Matrix (as "TRANSFORMATIONS")
fixed accidental reversion of contour model direction caused by some model transformations


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@523 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent b829ff3e
......@@ -21,6 +21,9 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from pycam.Geometry import TransformableContainer
from pycam.Geometry.Point import Point
from pycam.Geometry.utils import epsilon
import pycam.Geometry.Matrix as Matrix
import math
......@@ -67,8 +70,8 @@ class Line(TransformableContainer):
yield self.p2
def reset_cache(self):
self._dir is None
self._len is None
self._dir = None
self._len = None
def dir(self):
if self._dir is None:
......@@ -95,6 +98,9 @@ class Line(TransformableContainer):
def dist_to_point(self, p):
return math.sqrt(self.dist_to_point_sq(p))
def is_point_in_line(self, p):
return abs(p.sub(self.p1).norm() + p.sub(self.p2).norm() - self.len()) < epsilon
def minx(self):
return min(self.p1.x, self.p2.x)
......@@ -161,6 +167,20 @@ class Line(TransformableContainer):
factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq()
except ZeroDivisionError:
# lines are parallel
# check if they are _one_ line
if a.cross(c).normsq() != 0:
# the lines are parallel with a disctance
return None
# the lines are on one straight
if self.is_point_in_line(x3):
return x3
elif self.is_point_in_line(x4):
return x4
elif line.is_point_in_line(x1):
return x1
elif line.is_point_in_line(x2):
return x2
else:
return None
if 0 <= factor <= 1:
intersection = x1.add(a.mul(factor))
......@@ -176,3 +196,242 @@ class Line(TransformableContainer):
# intersection outside of the length of line(x1, x2)
return None
class LineGroup(TransformableContainer):
def __init__(self, offset_matrix=None):
super(LineGroup, self).__init__()
self._offset_matrix = offset_matrix
self._lines = []
self._line_offsets = None
self._is_closed = False
self.maxx = None
self.minx = None
self.maxy = None
self.miny = None
self.maxz = None
self.minz = None
def append(self, line):
if not self.is_connectable(line):
raise ValueError("This line does not fit to the line group")
else:
if not self._lines or (self._lines[-1].p2 == line.p1):
self._lines.append(line)
else:
self._lines.insert(0, line)
self._update_limits(line)
self._is_closed = self._lines[0].p1 == self._lines[-1].p2
def is_connectable(self, line):
if self._is_closed:
return False
elif not self._lines:
# empty line groups can be connected with any line
return True
elif line.p1 == self._lines[-1].p2:
return True
elif line.p2 == self._lines[0].p1:
return True
else:
return False
def next(self):
for line in self._lines:
yield line
def _init_line_offsets(self):
if self._lines and self._line_offsets is None:
self._line_offsets = []
offset_matrix = self.get_offset_matrix()
# initialize all offset vectors (if necessary)
for line in self._lines:
line_dir = line.dir()
vector = (line_dir.x, line_dir.y, line_dir.z)
offset_vector = Matrix.multiply_vector_matrix(vector, offset_matrix)
offset_point = Point(offset_vector[0], offset_vector[1], offset_vector[2])
self._line_offsets.append(Line(line.p1, line.p1.add(offset_point)))
def transform_by_matrix(self, matrix, transformed_list):
if self._lines:
offset_matrix = self.get_offset_matrix()
# initialize all offset vectors (if necessary)
self._init_line_offsets()
super(LineGroup, self).transform_by_matrix(matrix, transformed_list)
# transform all offset vectors
if self._lines:
for offset in self._line_offsets:
if not id(offset) in transformed_list:
offset.transform_by_matrix(matrix, transformed_list)
# transform the offset vector of this line group
self._offset_matrix = Matrix.multiply_matrix_matrix(matrix, offset_matrix)
def get_lines(self):
return self._lines[:]
def to_OpenGL(self):
for line in self._lines:
line.to_OpenGL()
def get_offset_matrix(self):
if not self._offset_matrix is None:
return self._offset_matrix
elif not self._lines:
return None
else:
# assume that this line group forms a straight line
offset_matrix = None
# check if all lines are in one specific layer (z/y/x)
# return the respective axis rotation matrix
z_start_value = self._lines[0].minz()
on_z_level = [True for line in self._lines
if line.minz() == line.maxz() == z_start_value]
if len(on_z_level) == len(self._lines):
offset_matrix = Matrix.TRANSFORMATIONS["z"]
else:
y_start_value = self._lines[0].y
on_y_level = [True for line in self._lines
if line.miny() == line.maxy() == y_start_value]
if len(on_y_level) == len(self._lines):
offset_matrix = Matrix.TRANSFORMATIONS["y"]
else:
x_start_value = self._lines[0].x
on_x_level = [True for line in self._lines
if line.minx() == line.maxx() == x_start_value]
if len(on_x_level) == len(self._lines):
offset_matrix = Matrix.TRANSFORMATIONS["x"]
# store the result to avoid re-calculation
self._offset_matrix = offset_matrix
return offset_matrix
def _update_limits(self, line):
if self.minx is None:
self.minx = line.minx()
self.maxx = line.maxx()
self.miny = line.miny()
self.maxy = line.maxy()
self.minz = line.minz()
self.maxz = line.maxz()
else:
self.minx = min(self.minx, line.minx())
self.maxx = max(self.maxx, line.maxx())
self.miny = min(self.miny, line.miny())
self.maxy = max(self.maxy, line.maxy())
self.minz = min(self.minz, line.minz())
self.maxz = max(self.maxz, line.maxz())
def reset_cache(self):
if not self._lines:
self.minx, self.miny, self.minz = None, None, None
self.maxx, self.maxy, self.maxz = None, None, None
else:
first = self._lines[0]
# initialize the start limit with valid values
self.minx = first.minx()
self.maxx = first.maxx()
self.miny = first.miny()
self.maxy = first.maxy()
self.minz = first.minz()
self.maxz = first.maxz()
# update the limit for each line
for line in self._lines:
self._update_limits(line)
def get_offset_line_group(self, offset):
def get_parallel_line(line, line_offset, offset):
if offset == 0:
return Line(line.p1, line.p2)
else:
cross_offset = line_offset.dir().mul(offset)
# Prolong the line at the beginning and at the end - to allow
# overlaps.
in_line = line.dir().mul(offset)
return Line(line.p1.add(cross_offset).sub(in_line),
line.p2.add(cross_offset).add(in_line))
def do_lines_intersection(l1, l2):
""" calculate the new intersection between two neighbouring lines
"""
if l1.p2 == l2.p1:
# intersection is already fine
return
if (l1.p1 is None) or (l2.p1 is None):
# one line was already marked as obsolete
return
x1, x2, x3, x4 = l2.p1, l2.p2, l1.p1, l1.p2
a = x2.sub(x1)
b = x4.sub(x3)
c = x3.sub(x1)
# see http://mathworld.wolfram.com/Line-LineIntersection.html (24)
try:
factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq()
except ZeroDivisionError:
l2.p1 = None
return
if not (0 <= factor < 1):
# The intersection is always supposed to be within p1 and p2.
l2.p1 = None
else:
intersection = x1.add(a.mul(factor))
if Line(l1.p1, intersection).dir() != l1.dir():
# Remove lines that would change their direction due to the
# new intersection. These are usually lines that become
# obsolete due to a more favourable intersection of the two
# neighbouring lines. This appears at small corners.
l1.p1 = None
elif Line(intersection, l2.p2).dir() != l2.dir():
# see comment above
l2.p1 = None
elif l1.p1 == intersection:
# remove invalid lines (zero length)
l1.p1 = None
elif l2.p2 == intersection:
# remove invalid lines (zero length)
l2.p1 = None
else:
# shorten both lines according to the new intersection
l1.p2 = intersection
l2.p1 = intersection
def simplify_line_group_intersections(lines):
finished = False
new_group = lines[:]
while not finished:
if len(new_group) > 1:
# Calculate new intersections for each pair of adjacent
# lines.
for index in range(len(new_group)):
if (index == 0) and (not self._is_closed):
# skip the first line if the group is not closed
continue
# this also works for index==0 (closed groups)
l1 = new_group[index - 1]
l2 = new_group[index]
do_lines_intersection(l1, l2)
# Remove all lines that were marked as obsolete during
# intersection calculation.
clean_group = [line for line in new_group
if not line.p1 is None]
finished = len(new_group) == len(clean_group)
if (len(clean_group) == 1) and self._is_closed:
new_group = []
finished = True
else:
new_group = clean_group
return new_group
if self.get_offset_matrix() is None:
# we can't get an offset line group if the normal is invalid
return self
else:
# initialize the line offsets if necessary
self._init_line_offsets()
new_lines = []
for line, line_offset in zip(self._lines, self._line_offsets):
new_lines.append(get_parallel_line(line, line_offset, offset))
cleaned_line_group = simplify_line_group_intersections(new_lines)
if len(cleaned_line_group) == 0:
return None
else:
group = LineGroup(self.get_offset_matrix())
for line in cleaned_line_group:
group.append(line)
return group
......@@ -27,6 +27,20 @@ from pycam.Geometry.Point import Point
import math
TRANSFORMATIONS = {
"normal": ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)),
"x": ((1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0)),
"y": ((0, 0, -1, 0), (0, 1, 0, 0), (1, 0, 0, 0)),
"z": ((0, 1, 0, 0), (-1, 0, 0, 0), (0, 0, 1, 0)),
"xy": ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0)),
"xz": ((1, 0, 0, 0), (0, -1, 0, 0), (0, 0, 1, 0)),
"yz": ((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)),
"x_swap_y": ((0, 1, 0, 0), (1, 0, 0, 0), (0, 0, 1, 0)),
"x_swap_z": ((0, 0, 1, 0), (0, 1, 0, 0), (1, 0, 0, 0)),
"y_swap_z": ((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0)),
}
def get_dot_product(a, b):
""" calculate the dot product of two 3d vectors
......@@ -158,7 +172,35 @@ def multiply_vector_matrix(v, m):
@rtype: tuple(float)
@return: a tuple of 3 floats as the matrix product
"""
if len(m) == 9:
m = ((m[0], m[1], m[2]), (m[3], m[4], m[5]), (m[6], m[7], m[8]))
return (v[0] * m[0][0] + v[1] * m[0][1] + v[2] * m[0][2],
v[0] * m[1][0] + v[1] * m[1][1] + v[2] * m[1][2],
v[0] * m[2][0] + v[1] * m[2][1] + v[2] * m[2][2])
def multiply_matrix_matrix(m1, m2):
def multi(row1, col2):
return (m1[row1][0] * m2[0][col2] + m1[row1][1] * m2[1][col2] \
+ m1[row1][2] * m2[2][col2])
return ((multi(0, 0), multi(0, 1), multi(0, 2)),
(multi(1, 0), multi(1, 1), multi(1, 2)),
(multi(2, 0), multi(2, 1), multi(2, 2)))
def get_inverse_matrix(m):
_a = m[1][1] * m[2][2] - m[1][2] * m[2][1]
_b = m[0][2] * m[2][1] - m[0][1] * m[2][2]
_c = m[0][1] * m[1][2] - m[0][2] * m[1][1]
_d = m[1][2] * m[2][0] - m[1][0] * m[2][2]
_e = m[0][0] * m[2][2] - m[0][2] * m[2][0]
_f = m[0][2] * m[1][0] - m[0][0] * m[1][2]
_g = m[1][0] * m[2][1] - m[1][1] * m[2][0]
_h = m[0][1] * m[2][0] - m[0][0] * m[2][1]
_k = m[0][0] * m[1][1] - m[0][1] * m[1][0]
det = m[0][0] * _a + m[0][1] * _d + m[0][2] * _g
if det == 0:
return None
else:
return ((_a / det, _b / det, _c / det),
(_d / det, _e / det, _f / det),
(_g / det, _h / det, _k / det))
......@@ -23,27 +23,15 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.Exporters.STLExporter
from pycam.Geometry.Triangle import Triangle
from pycam.Geometry.Line import Line
from pycam.Geometry.Line import Line, LineGroup
from pycam.Geometry.Point import Point
from pycam.Geometry.TriangleKdtree import TriangleKdtree
from pycam.Geometry.Matrix import TRANSFORMATIONS
from pycam.Toolpath import Bounds
from pycam.Geometry.utils import INFINITE
from pycam.Geometry import TransformableContainer
MODEL_TRANSFORMATIONS = {
"normal": ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)),
"x": ((1, 0, 0, 0), (0, 0, 1, 0), (0, -1, 0, 0)),
"y": ((0, 0, -1, 0), (0, 1, 0, 0), (1, 0, 0, 0)),
"z": ((0, 1, 0, 0), (-1, 0, 0, 0), (0, 0, 1, 0)),
"xy": ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0)),
"xz": ((1, 0, 0, 0), (0, -1, 0, 0), (0, 0, 1, 0)),
"yz": ((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)),
"x_swap_y": ((0, 1, 0, 0), (1, 0, 0, 0), (0, 0, 1, 0)),
"x_swap_z": ((0, 0, 1, 0), (0, 1, 0, 0), (1, 0, 0, 0)),
"y_swap_z": ((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0)),
}
class BaseModel(TransformableContainer):
id = 0
......@@ -95,20 +83,26 @@ class BaseModel(TransformableContainer):
+ "support the 'export' function.") % str(type(self)))
def _update_limits(self, item):
if callable(item.minx):
minx, miny, minz = item.minx(), item.miny(), item.minz()
maxx, maxy, maxz = item.maxx(), item.maxy(), item.maxz()
else:
minx, miny, minz = item.minx, item.miny, item.minz
maxx, maxy, maxz = item.maxx, item.maxy, item.maxz
if self.minx is None:
self.minx = item.minx()
self.miny = item.miny()
self.minz = item.minz()
self.maxx = item.maxx()
self.maxy = item.maxy()
self.maxz = item.maxz()
self.minx = minx
self.miny = miny
self.minz = minz
self.maxx = maxx
self.maxy = maxy
self.maxz = maxz
else:
self.minx = min(self.minx, item.minx())
self.miny = min(self.miny, item.miny())
self.minz = min(self.minz, item.minz())
self.maxx = max(self.maxx, item.maxx())
self.maxy = max(self.maxy, item.maxy())
self.maxz = max(self.maxz, item.maxz())
self.minx = min(self.minx, minx)
self.miny = min(self.miny, miny)
self.minz = min(self.minz, minz)
self.maxx = max(self.maxx, maxx)
self.maxy = max(self.maxy, maxy)
self.maxz = max(self.maxz, maxz)
def append(self, item):
self._update_limits(item)
......@@ -135,8 +129,8 @@ class BaseModel(TransformableContainer):
self._update_limits(item)
def transform_by_template(self, direction="normal"):
if direction in MODEL_TRANSFORMATIONS.keys():
self.transform_by_matrix(MODEL_TRANSFORMATIONS[direction])
if direction in TRANSFORMATIONS.keys():
self.transform_by_matrix(TRANSFORMATIONS[direction])
def shift(self, shift_x, shift_y, shift_z):
matrix = ((1, 0, 0, shift_x), (0, 1, 0, shift_y), (0, 0, 1, shift_z))
......@@ -217,20 +211,22 @@ class ContourModel(BaseModel):
super(ContourModel, self).append(item)
if isinstance(item, Line):
for line_group in self._line_groups:
if item.p2 == line_group[0].p1:
# the line fits to the start of this group
line_group.insert(0, item)
break
elif item.p1 == line_group[-1].p2:
# the line fits to the end of this group
if line_group.is_connectable(item):
line_group.append(item)
break
else:
# add a new group with this single item
self._line_groups.append([item])
# add a single line as part of a new group
new_line_group = LineGroup()
new_line_group.append(item)
self._line_groups.append(new_line_group)
elif isinstance(item, LineGroup):
self._line_groups.append(item)
else:
# ignore any non-supported items
pass
def get_lines(self):
return sum(self._line_groups, [])
return sum([group.get_lines() for group in self._line_groups], [])
def get_line_groups(self):
return self._line_groups
......@@ -248,108 +244,16 @@ class ContourModel(BaseModel):
@returns: the new shifted model
@rtype: pycam.Geometry.Model.Model
"""
def get_parallel_line(line, offset):
if offset == 0:
return Line(line.p1, line.p2)
else:
cross = line.p2.sub(line.p1).cross(Point(0, 0, 1))
cross_offset = cross.mul(offset / cross.norm())
in_line = line.p2.sub(line.p1).normalize().mul(offset)
return Line(line.p1.add(cross_offset).sub(in_line),
line.p2.add(cross_offset).add(in_line))
def do_lines_intersection(l1, l2):
""" calculate the new intersection between two neighbouring lines
"""
if l1.p2 == l2.p1:
# intersection is already fine
return
if (l1.p1 is None) or (l2.p1 is None):
# one line was already marked as obsolete
return
x1, x2, x3, x4 = l2.p1, l2.p2, l1.p1, l1.p2
a = x2.sub(x1)
b = x4.sub(x3)
c = x3.sub(x1)
# see http://mathworld.wolfram.com/Line-LineIntersection.html (24)
try:
factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq()
except ZeroDivisionError:
l2.p1 = None
return
if not (0 <= factor < 1):
# The intersection is always supposed to be within p1 and p2.
l2.p1 = None
else:
intersection = x1.add(a.mul(factor))
if Line(l1.p1, intersection).dir() != l1.dir():
# Remove lines that would change their direction due to the
# new intersection. These are usually lines that become
# obsolete due to a more favourable intersection of the two
# neighbouring lines. This appears at small corners.
l1.p1 = None
elif Line(intersection, l2.p2).dir() != l2.dir():
# see comment above
l2.p1 = None
elif l1.p1 == intersection:
# remove invalid lines (zero length)
l1.p1 = None
elif l2.p2 == intersection:
# remove invalid lines (zero length)
l2.p1 = None
else:
# shorten both lines according to the new intersection
l1.p2 = intersection
l2.p1 = intersection
# use a cached offset model if it exists
if offset in self._cached_offset_models:
return self._cached_offset_models[offset]
result = ContourModel()
for group in self._line_groups:
closed_group = (len(group) > 1) and (group[-1].p2 == group[0].p1)
new_group = []
for line in group:
new_group.append(get_parallel_line(line, offset))
# counter for the progress callback
lines_to_be_processed = len(new_group)
finished = False
while not finished:
if len(new_group) > 1:
# Calculate new intersections for each pair of adjacent
# lines.
for index in range(len(new_group)):
if (index == 0) and (not closed_group):
# skip the first line if the group is not closed
continue
# this also works for index==0 (closed groups)
l1 = new_group[index - 1]
l2 = new_group[index]
do_lines_intersection(l1, l2)
# Don't call the "progress" callback more times than the
# number of lines in this group.
if (lines_to_be_processed > 0) \
and callback and callback():
# the user requested "quit"
return None
lines_to_be_processed -= 1
# Remove all lines that were marked as obsolete during
# intersection calculation.
clean_group = [line for line in new_group
if not line.p1 is None]
finished = len(new_group) == len(clean_group)
if (len(clean_group) == 1) and closed_group:
new_group = []
finished = True
else:
new_group = clean_group
# "fix" the progress counter (it expected as many lines as there
# are items in the group. This causes small progress jumps.
if callback:
while lines_to_be_processed > 0:
if callback():
new_group = group.get_offset_line_group(offset)
if not new_group is None:
result.append(new_group)
if callback and callback():
return None
lines_to_be_processed -= 1
for line in new_group:
result.append(line)
# cache the result
self._cached_offset_models[offset] = result
return result
......@@ -363,52 +267,32 @@ class ContourModel(BaseModel):
interrupted the operation).
Otherwise it returns False if no intersections were found.
"""
def get_bounds_of_group(group):
minx, maxx, miny, maxy = None, None, None, None
for line in group:
lminx = min(line.p1.x, line.p2.x)
lmaxx = max(line.p1.x, line.p2.x)
lminy = min(line.p1.y, line.p2.y)
lmaxy = max(line.p1.y, line.p2.y)
if (minx is None) or (minx > lminx):
minx = lminx
if (maxx is None) or (maxx > lmaxx):
maxx = lmaxx
if (miny is None) or (miny > lminy):
miny = lminy
if (maxy is None) or (maxy > lmaxy):
maxy = lmaxy
return (minx, maxx, miny, maxy)
def check_bounds_of_groups(bound1, bound2):
if ((bound1[0] <= bound2[0] <= bound1[1]) \
or (bound1[0] <= bound2[1] <= bound1[1]) \
or (bound2[0] <= bound1[0] <= bound2[1]) \
or (bound2[0] <= bound1[1] <= bound2[1])):
def check_bounds_of_groups(g1, g2):
if (g1.minx <= g2.minx <= g1.maxx) \
or (g1.minx <= g2.maxx <= g1.maxx) \
or (g2.minx <= g1.minx <= g2.maxx) \
or (g2.minx <= g1.maxx <= g2.maxx):
# the x boundaries overlap
if ((bound1[2] <= bound2[2] <= bound1[3]) \
or (bound1[2] <= bound2[3] <= bound1[3]) \
or (bound2[2] <= bound1[2] <= bound2[3]) \
or (bound2[2] <= bound1[3] <= bound2[3])):
if (g1.miny <= g2.miny <= g1.maxy) \
or (g1.miny <= g2.maxy <= g1.maxy) \
or (g2.miny <= g1.miny <= g2.maxy) \
or (g2.miny <= g1.maxy <= g2.maxy):
# also the y boundaries overlap
if (g1.minz <= g2.minz <= g1.maxz) \
or (g1.minz <= g2.maxz <= g1.maxz) \
or (g2.minz <= g1.minz <= g2.maxz) \
or (g2.minz <= g1.maxz <= g2.maxz):
# z overlaps as well
return True
return False
# check each pair of line groups for intersections
# first: cache the bounds of each group
bounds = {}
for group in self._line_groups:
bounds[id(group)] = get_bounds_of_group(group)
# now start to look for intersections
for group1 in self._line_groups:
for group2 in self._line_groups:
# don't compare a group with itself
if group1 is group2:
continue
for index, group1 in enumerate(self._line_groups[:-1]):
for group2 in self._line_groups[index+1:]:
# check if both groups overlap - otherwise skip this pair
if check_bounds_of_groups(bounds[id(group1)],
bounds[id(group2)]):
if check_bounds_of_groups(group1, group2):
# check each pair of lines for intersections
for line1 in group1:
for line2 in group2:
for line1 in group1.next():
for line2 in group2.next():
intersection = line1.get_intersection(line2)
if intersection:
return intersection
......
......@@ -78,7 +78,7 @@ class EngraveCutter:
break
for line_group in line_groups:
for line in line_group:
for line in line_group.next():
self.GenerateToolPathLinePush(self.pa_push, line, z,
draw_callback)
if progress_counter.increment():
......@@ -107,7 +107,7 @@ class EngraveCutter:
for line_group in self.contour_model.get_line_groups():
self.pa_drop.new_direction(0)
self.pa_drop.new_scanline()
for line in line_group:
for line in line_group.get_lines():
self.GenerateToolPathLineDrop(self.pa_drop, line, minz, maxz,
horiz_step, draw_callback=draw_callback)
if progress_counter.increment():
......
......@@ -157,8 +157,8 @@ def generate_toolpath(model, tool_settings=None,
if (not contour_model is None) and (engrave_offset > 0):
if not callback is None:
callback(text="Preparing contour model with offset ...")
progress_callback = ProgressCounter(len(contour_model.get_lines()),
callback).increment
progress_callback = ProgressCounter(
len(contour_model.get_line_groups()), callback).increment
else:
progress_callback = None
contour_model = contour_model.get_offset_model(engrave_offset,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment