Commit b9e1bdbd authored by sumpfralle's avatar sumpfralle

added basic support for engravings with an offset ("around the contour")


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@418 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent e184a5f7
...@@ -21,7 +21,7 @@ You should have received a copy of the GNU General Public License ...@@ -21,7 +21,7 @@ You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>. along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
""" """
from pycam.Geometry import Triangle, Line from pycam.Geometry import Triangle, Line, Point
from utils import INFINITE from utils import INFINITE
try: try:
...@@ -208,3 +208,88 @@ class ContourModel(BaseModel): ...@@ -208,3 +208,88 @@ class ContourModel(BaseModel):
def get_line_groups(self): def get_line_groups(self):
return self._line_groups return self._line_groups
def get_offset_model(self, offset):
""" calculate a contour model that surrounds the current model with
a given offset.
This is mainly useful for engravings that should not proceed _on_ the
lines but besides these.
"""
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)
factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq()
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
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))
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)
# 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
for line in new_group:
result.append(line)
return result
...@@ -357,7 +357,8 @@ class ProjectGui: ...@@ -357,7 +357,8 @@ class ProjectGui:
if objname != "SettingEnableODE": if objname != "SettingEnableODE":
self.gui.get_object(objname).connect("toggled", self.handle_process_settings_change) self.gui.get_object(objname).connect("toggled", self.handle_process_settings_change)
for objname in ("SafetyHeightControl", "OverlapPercentControl", for objname in ("SafetyHeightControl", "OverlapPercentControl",
"MaterialAllowanceControl", "MaxStepDownControl"): "MaterialAllowanceControl", "MaxStepDownControl",
"EngraveOffsetControl"):
self.gui.get_object(objname).connect("value-changed", self.handle_process_settings_change) self.gui.get_object(objname).connect("value-changed", self.handle_process_settings_change)
self.gui.get_object("ProcessSettingName").connect("changed", self.handle_process_settings_change) self.gui.get_object("ProcessSettingName").connect("changed", self.handle_process_settings_change)
# get/set functions for the current tool/process/bounds/task # get/set functions for the current tool/process/bounds/task
...@@ -899,7 +900,8 @@ class ProjectGui: ...@@ -899,7 +900,8 @@ class ProjectGui:
all_controls = ("PathDirectionX", "PathDirectionY", "PathDirectionXY", all_controls = ("PathDirectionX", "PathDirectionY", "PathDirectionXY",
"SimpleCutter", "PolygonCutter", "ContourCutter", "SimpleCutter", "PolygonCutter", "ContourCutter",
"PathAccumulator", "ZigZagCutter", "MaxStepDownControl", "PathAccumulator", "ZigZagCutter", "MaxStepDownControl",
"MaterialAllowanceControl", "OverlapPercentControl") "MaterialAllowanceControl", "OverlapPercentControl",
"EngraveOffsetControl")
active_controls = { active_controls = {
"DropCutter": ("PathAccumulator", "ZigZagCutter", "PathDirectionX", "DropCutter": ("PathAccumulator", "ZigZagCutter", "PathDirectionX",
"PathDirectionY", "MaterialAllowanceControl", "PathDirectionY", "MaterialAllowanceControl",
...@@ -908,7 +910,8 @@ class ProjectGui: ...@@ -908,7 +910,8 @@ class ProjectGui:
"PathDirectionX", "PathDirectionY", "PathDirectionXY", "PathDirectionX", "PathDirectionY", "PathDirectionXY",
"MaxStepDownControl", "MaterialAllowanceControl", "MaxStepDownControl", "MaterialAllowanceControl",
"OverlapPercentControl"), "OverlapPercentControl"),
"EngraveCutter": ("SimpleCutter", "MaxStepDownControl"), "EngraveCutter": ("SimpleCutter", "MaxStepDownControl",
"EngraveOffsetControl"),
} }
for one_control in all_controls: for one_control in all_controls:
get_obj(one_control).set_sensitive(one_control in active_controls[cutter_name]) get_obj(one_control).set_sensitive(one_control in active_controls[cutter_name])
...@@ -1201,7 +1204,8 @@ class ProjectGui: ...@@ -1201,7 +1204,8 @@ class ProjectGui:
if self.gui.get_object("UnitChangeProcesses").get_active(): if self.gui.get_object("UnitChangeProcesses").get_active():
# scale the process settings # scale the process settings
for process in self.process_list: for process in self.process_list:
for key in ("safety_height", "material_allowance", "step_down"): for key in ("safety_height", "material_allowance",
"step_down", "engrave_offset"):
process[key] *= factor process[key] *= factor
if self.gui.get_object("UnitChangeBounds").get_active(): if self.gui.get_object("UnitChangeBounds").get_active():
# scale the boundaries and keep their center # scale the boundaries and keep their center
...@@ -1666,7 +1670,8 @@ class ProjectGui: ...@@ -1666,7 +1670,8 @@ class ProjectGui:
for objname, key in (("SafetyHeightControl", "safety_height"), for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap_percent"), ("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"), ("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down")): ("MaxStepDownControl", "step_down"),
("EngraveOffsetControl", "engrave_offset")):
settings[key] = self.gui.get_object(objname).get_value() settings[key] = self.gui.get_object(objname).get_value()
return settings return settings
...@@ -1689,7 +1694,8 @@ class ProjectGui: ...@@ -1689,7 +1694,8 @@ class ProjectGui:
for objname, key in (("SafetyHeightControl", "safety_height"), for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap_percent"), ("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"), ("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down")): ("MaxStepDownControl", "step_down"),
("EngraveOffsetControl", "engrave_offset")):
self.gui.get_object(objname).set_value(settings[key]) self.gui.get_object(objname).set_value(settings[key])
@gui_activity_guard @gui_activity_guard
...@@ -2089,7 +2095,8 @@ class ProjectGui: ...@@ -2089,7 +2095,8 @@ class ProjectGui:
process_settings["material_allowance"], process_settings["material_allowance"],
process_settings["safety_height"], process_settings["safety_height"],
process_settings["overlap_percent"] / 100.0, process_settings["overlap_percent"] / 100.0,
process_settings["step_down"]) process_settings["step_down"],
process_settings["engrave_offset"])
return toolpath_settings return toolpath_settings
......
...@@ -136,6 +136,7 @@ tool_radius: 1.0 ...@@ -136,6 +136,7 @@ tool_radius: 1.0
[ProcessDefault] [ProcessDefault]
path_direction: x path_direction: x
safety_height: 5 safety_height: 5
engrave_offset: 0.0
[Process0] [Process0]
name: Remove material name: Remove material
...@@ -229,6 +230,7 @@ process: 3 ...@@ -229,6 +230,7 @@ process: 3
"material_allowance": float, "material_allowance": float,
"overlap_percent": int, "overlap_percent": int,
"step_down": float, "step_down": float,
"engrave_offset": float,
"tool": object, "tool": object,
"process": object, "process": object,
"bounds": object, "bounds": object,
...@@ -247,7 +249,7 @@ process: 3 ...@@ -247,7 +249,7 @@ process: 3
"speed"), "speed"),
"process": ("name", "path_generator", "path_postprocessor", "process": ("name", "path_generator", "path_postprocessor",
"path_direction", "safety_height", "material_allowance", "path_direction", "safety_height", "material_allowance",
"overlap_percent", "step_down"), "overlap_percent", "step_down", "engrave_offset"),
"bounds": ("name", "type", "x_low", "x_high", "y_low", "bounds": ("name", "type", "x_low", "x_high", "y_low",
"y_high", "z_low", "z_high"), "y_high", "z_low", "z_high"),
"task": ("name", "tool", "process", "bounds", "enabled"), "task": ("name", "tool", "process", "bounds", "enabled"),
...@@ -463,6 +465,7 @@ class ToolpathSettings: ...@@ -463,6 +465,7 @@ class ToolpathSettings:
"safety_height": float, "safety_height": float,
"overlap": float, "overlap": float,
"step_down": float, "step_down": float,
"engrave_offset": float,
}, },
} }
...@@ -538,7 +541,7 @@ class ToolpathSettings: ...@@ -538,7 +541,7 @@ class ToolpathSettings:
def set_process_settings(self, generator, postprocessor, path_direction, def set_process_settings(self, generator, postprocessor, path_direction,
material_allowance=0.0, safety_height=0.0, overlap=0.0, material_allowance=0.0, safety_height=0.0, overlap=0.0,
step_down=1.0): step_down=1.0, engrave_offset=0.0):
self.process_settings = { self.process_settings = {
"generator": generator, "generator": generator,
"postprocessor": postprocessor, "postprocessor": postprocessor,
...@@ -547,6 +550,7 @@ class ToolpathSettings: ...@@ -547,6 +550,7 @@ class ToolpathSettings:
"safety_height": safety_height, "safety_height": safety_height,
"overlap": overlap, "overlap": overlap,
"step_down": step_down, "step_down": step_down,
"engrave_offset": engrave_offset,
} }
def get_process_settings(self): def get_process_settings(self):
......
...@@ -2032,7 +2032,7 @@ It will spare some material close to the model (depending on the "step down" val ...@@ -2032,7 +2032,7 @@ It will spare some material close to the model (depending on the "step down" val
<child> <child>
<object class="GtkTable" id="table7"> <object class="GtkTable" id="table7">
<property name="visible">True</property> <property name="visible">True</property>
<property name="n_rows">4</property> <property name="n_rows">5</property>
<property name="n_columns">2</property> <property name="n_columns">2</property>
<property name="column_spacing">2</property> <property name="column_spacing">2</property>
<property name="row_spacing">2</property> <property name="row_spacing">2</property>
...@@ -2164,6 +2164,37 @@ This operation is not available for engraving.</property> ...@@ -2164,6 +2164,37 @@ This operation is not available for engraving.</property>
<property name="y_options">GTK_FILL</property> <property name="y_options">GTK_FILL</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="EngraveOffsetLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Engrave Offset:</property>
</object>
<packing>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="EngraveOffsetControl">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">EngraveOffsetValue</property>
<property name="digits">2</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
</object> </object>
</child> </child>
</object> </object>
...@@ -5182,4 +5213,7 @@ Any selected group of dimensions will be scaled accordingly.</property> ...@@ -5182,4 +5213,7 @@ Any selected group of dimensions will be scaled accordingly.</property>
<property name="upper">100</property> <property name="upper">100</property>
<property name="step_increment">0.10000000000000001</property> <property name="step_increment">0.10000000000000001</property>
</object> </object>
<object class="GtkAdjustment" id="EngraveOffsetValue">
<property name="upper">1000</property>
</object>
</interface> </interface>
...@@ -45,13 +45,13 @@ def generate_toolpath_from_settings(model, tp_settings, callback=None): ...@@ -45,13 +45,13 @@ def generate_toolpath_from_settings(model, tp_settings, callback=None):
bounds, process["path_direction"], process["generator"], bounds, process["path_direction"], process["generator"],
process["postprocessor"], process["material_allowance"], process["postprocessor"], process["material_allowance"],
process["safety_height"], process["overlap"], process["safety_height"], process["overlap"],
process["step_down"], grid["distance"], grid["thickness"], process["step_down"], process["engrave_offset"], grid["distance"],
grid["height"], backend, callback) grid["thickness"], grid["height"], backend, callback)
def generate_toolpath(model, tool_settings=None, def generate_toolpath(model, tool_settings=None,
bounds=None, direction="x", path_generator="DropCutter", bounds=None, direction="x", path_generator="DropCutter",
path_postprocessor="ZigZagCutter", material_allowance=0.0, path_postprocessor="ZigZagCutter", material_allowance=0.0,
safety_height=None, overlap=0.0, step_down=0.0, safety_height=None, overlap=0.0, step_down=0.0, engrave_offset=0.0,
support_grid_distance=None, support_grid_thickness=None, support_grid_distance=None, support_grid_thickness=None,
support_grid_height=None, calculation_backend=None, callback=None): support_grid_height=None, calculation_backend=None, callback=None):
""" abstract interface for generating a toolpath """ abstract interface for generating a toolpath
...@@ -79,6 +79,8 @@ def generate_toolpath(model, tool_settings=None, ...@@ -79,6 +79,8 @@ def generate_toolpath(model, tool_settings=None,
@value overlap: the overlap between two adjacent tool paths (0 <= overlap < 1) @value overlap: the overlap between two adjacent tool paths (0 <= overlap < 1)
@type step_down: float @type step_down: float
@value step_down: maximum height of each layer (for PushCutter) @value step_down: maximum height of each layer (for PushCutter)
@type engrave_offset: float
@value engrave_offset: toolpath distance to the contour model
@type support_grid_distance: float @type support_grid_distance: float
@value support_grid_distance: grid size of remaining support material @value support_grid_distance: grid size of remaining support material
@type support_grid_thickness: float @type support_grid_thickness: float
...@@ -127,6 +129,10 @@ def generate_toolpath(model, tool_settings=None, ...@@ -127,6 +129,10 @@ def generate_toolpath(model, tool_settings=None,
support_grid_distance, support_grid_thickness, support_grid_distance, support_grid_thickness,
support_grid_height) support_grid_height)
trimesh_model += support_grid_model trimesh_model += support_grid_model
# Adapt the contour_model to the engraving offset. This offset is
# considered to be part of the material_allowance.
if (not contour_model is None) and (engrave_offset > 0):
contour_model = contour_model.get_offset_model(engrave_offset)
# Due to some weirdness the height of the drill must be bigger than the object's size. # Due to some weirdness the height of the drill must be bigger than the object's size.
# Otherwise some collisions are not detected. # Otherwise some collisions are not detected.
cutter_height = 4 * (maxy - miny) cutter_height = 4 * (maxy - miny)
...@@ -137,7 +143,9 @@ def generate_toolpath(model, tool_settings=None, ...@@ -137,7 +143,9 @@ def generate_toolpath(model, tool_settings=None,
physics = _get_physics(trimesh_model, cutter, calculation_backend) physics = _get_physics(trimesh_model, cutter, calculation_backend)
if isinstance(physics, basestring): if isinstance(physics, basestring):
return physics return physics
generator = _get_pathgenerator_instance(trimesh_model, contour_model, cutter, path_generator, path_postprocessor, material_allowance, safety_height, physics) generator = _get_pathgenerator_instance(trimesh_model, contour_model,
cutter, path_generator, path_postprocessor, material_allowance,
safety_height, physics)
if isinstance(generator, basestring): if isinstance(generator, basestring):
return generator return generator
if (overlap < 0) or (overlap >= 1): if (overlap < 0) or (overlap >= 1):
...@@ -209,7 +217,8 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, pathgenera ...@@ -209,7 +217,8 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, pathgenera
return "Invalid postprocessor (%s) for 'EngraveCutter' - it should be one of these: %s" % (processor, PATH_POSTPROCESSORS) return "Invalid postprocessor (%s) for 'EngraveCutter' - it should be one of these: %s" % (processor, PATH_POSTPROCESSORS)
if not contour_model: if not contour_model:
return "The EngraveCutter requires a contour model (e.g. from a DXF file)." return "The EngraveCutter requires a contour model (e.g. from a DXF file)."
return EngraveCutter.EngraveCutter(cutter, trimesh_model, contour_model, processor, physics=physics) return EngraveCutter.EngraveCutter(cutter, trimesh_model,
contour_model, processor, physics=physics)
else: else:
return "Invalid path generator (%s): not one of %s" % (pathgenerator, PATH_GENERATORS) return "Invalid path generator (%s): not one of %s" % (pathgenerator, PATH_GENERATORS)
......
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