Commit 449cdec0 authored by sumpfralle's avatar sumpfralle

r700@erker: lars | 2010-02-21 18:42:12 +0100

 implemented ODE support for the PushCutter


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@152 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent e7995bc1
...@@ -30,7 +30,7 @@ class BaseCutter: ...@@ -30,7 +30,7 @@ class BaseCutter:
self.maxx = location.x+self.radius self.maxx = location.x+self.radius
self.miny = location.y-self.radius self.miny = location.y-self.radius
self.maxy = location.y+self.radius self.maxy = location.y+self.radius
for shape, set_pos_func, offset in self.shape.values(): for shape, set_pos_func in self.shape.values():
set_pos_func(location.x, location.y, location.z) set_pos_func(location.x, location.y, location.z)
def intersect(self, direction, triangle): def intersect(self, direction, triangle):
......
...@@ -38,10 +38,39 @@ class CylindricalCutter(BaseCutter): ...@@ -38,10 +38,39 @@ class CylindricalCutter(BaseCutter):
""" """
radius = self.radius + additional_distance radius = self.radius + additional_distance
height = self.height + additional_distance height = self.height + additional_distance
geom = ode.GeomCylinder(None, radius, height) center_height = 0.5 * height - additional_distance
geom = ode.GeomTransform(None)
geom_drill = ode.GeomCylinder(None, radius, height)
geom_drill.setPosition((0, 0, center_height))
geom.setGeom(geom_drill)
geom.children = []
def reset_shape():
geom.children = []
def set_position(x, y, z): def set_position(x, y, z):
geom.setPosition((x, y, z)) geom.setPosition((x, y, z))
self.shape[format] = (geom, set_position, (0, 0, 0.5 * height - additional_distance)) def extend_shape(diff_x, diff_y, diff_z):
# diff_z is assumed to be zero
reset_shape()
geom_end_transform = ode.GeomTransform(None)
geom_end_transform.setBody(geom.getBody())
geom_end = ode.GeomCylinder(None, radius, height)
geom_end.setPosition((diff_x, diff_y, center_height))
geom_end_transform.setGeom(geom_end)
# create the block that connects to two cylinders at the end
geom_connect_transform = ode.GeomTransform(None)
geom_connect_transform.setBody(geom.getBody())
hypotenuse = sqrt(diff_x * diff_x + diff_y * diff_y)
cosinus = diff_x/hypotenuse
sinus = diff_y/hypotenuse
geom_connect = ode.GeomBox(None, (hypotenuse, 2.0 * radius, height))
# see http://mathworld.wolfram.com/RotationMatrix.html
geom_connect.setRotation((cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0))
geom_connect.setPosition((diff_x/2.0, diff_y/2.0, center_height))
geom_connect_transform.setGeom(geom_connect)
geom.children = [geom_connect_transform, geom_end_transform]
geom.extend_shape = extend_shape
geom.reset_shape = reset_shape
self.shape[format] = (geom, set_position)
return self.shape[format] return self.shape[format]
def to_OpenGL(self): def to_OpenGL(self):
......
...@@ -29,12 +29,55 @@ class SphericalCutter(BaseCutter): ...@@ -29,12 +29,55 @@ class SphericalCutter(BaseCutter):
if format == "ODE": if format == "ODE":
import ode import ode
radius = self.radius + additional_distance radius = self.radius + additional_distance
geom = ode.GeomCapsule(None, radius, self.height) center_height = 0.5 * self.height + radius - additional_distance
geom = ode.GeomTransform(None)
geom_drill = ode.GeomCapsule(None, radius, self.height)
geom_drill.setPosition((0, 0, center_height))
geom.setGeom(geom_drill)
geom.children = []
def reset_shape():
geom.children = []
def set_position(x, y, z): def set_position(x, y, z):
geom.setPosition((x, y, z)) geom.setPosition((x, y, z))
self.shape[format] = (geom, set_position, (0, 0, 0.5 * self.height + radius)) def extend_shape(diff_x, diff_y, diff_z):
# diff_z is assumed to be zero
reset_shape()
geom_end_transform = ode.GeomTransform(geom.getSpace())
geom_end_transform.setBody(geom.getBody())
geom_end = ode.GeomCapsule(None, radius, self.height)
geom_end.setPosition((diff_x, diff_y, center_height))
geom_end_transform.setGeom(geom_end)
# create the block that connects the two cylinders at the end
geom_connect_transform = ode.GeomTransform(geom.getSpace())
geom_connect_transform.setBody(geom.getBody())
hypotenuse = sqrt(diff_x * diff_x + diff_y * diff_y)
cosinus = diff_x/hypotenuse
sinus = diff_y/hypotenuse
geom_connect = ode.GeomBox(None, (2.0 * radius, hypotenuse, self.height))
# see http://mathworld.wolfram.com/RotationMatrix.html
rot_matrix_box = (cosinus, sinus, 0.0, -sinus, cosinus, 0.0, 0.0, 0.0, 1.0)
geom_connect.setRotation(rot_matrix_box)
geom_connect.setPosition((diff_x/2.0, diff_y/2.0, center_height))
geom_connect_transform.setGeom(geom_connect)
# create a cylinder, that connects the two half spheres at the lower end of both drills
geom_cyl_transform = ode.GeomTransform(geom.getSpace())
geom_cyl_transform.setBody(geom.getBody())
geom_cyl = ode.GeomCylinder(None, radius, hypotenuse)
# switch x and z axis of the cylinder and then rotate it according to diff_x/diff_y
rot_matrix_cyl = (rot_matrix_box[2], rot_matrix_box[1], rot_matrix_box[0],
rot_matrix_box[5], rot_matrix_box[4], rot_matrix_box[3],
rot_matrix_box[8], rot_matrix_box[7], rot_matrix_box[6])
geom_cyl.setRotation(rot_matrix_cyl)
geom_cyl.setPosition((diff_x/2.0, diff_y/2.0, radius - additional_distance))
geom_cyl_transform.setGeom(geom_cyl)
# sort the geoms in order or collision probability
geom.children = [geom_connect_transform, geom_cyl_transform, geom_end_transform]
geom.extend_shape = extend_shape
geom.reset_shape = reset_shape
self.shape[format] = (geom, set_position)
return self.shape[format] return self.shape[format]
def to_OpenGL(self): def to_OpenGL(self):
if not GL_enabled: if not GL_enabled:
return return
......
...@@ -457,11 +457,8 @@ class ProjectGui: ...@@ -457,11 +457,8 @@ class ProjectGui:
self.gui.get_object("TorusRadiusControl").set_sensitive(self.settings.get("cutter_shape") == "ToroidalCutter") self.gui.get_object("TorusRadiusControl").set_sensitive(self.settings.get("cutter_shape") == "ToroidalCutter")
# disable "step down" control, if PushCutter is not active # disable "step down" control, if PushCutter is not active
self.gui.get_object("MaxStepDownControl").set_sensitive(self.settings.get("path_generator") == "PushCutter") self.gui.get_object("MaxStepDownControl").set_sensitive(self.settings.get("path_generator") == "PushCutter")
# TODO: remove this as soon as the PushCutter supports "material allowance" # "material allowance" requires ODE support
if self.settings.get("enable_ode", True): self.gui.get_object("MaterialAllowanceControl").set_sensitive(self.settings.get("enable_ode"))
self.gui.get_object("MaterialAllowanceControl").set_sensitive(self.settings.get("path_generator") == "DropCutter")
else:
self.gui.get_object("MaterialAllowanceControl").set_sensitive(False)
@gui_activity_guard @gui_activity_guard
......
...@@ -1559,13 +1559,12 @@ Only available for the Push Cutter.</property> ...@@ -1559,13 +1559,12 @@ Only available for the Push Cutter.</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="SimpleCutter"> <object class="GtkRadioButton" id="ZigZagCutter">
<property name="label" translatable="yes">Simple Cutter</property> <property name="label" translatable="yes">ZigZag Cutter</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Practically useless path processor for testing purposes. <property name="tooltip_text" translatable="yes">Quite simple path processor for finishing operations.</property>
Only available for the Push Cutter.</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<property name="group">PathAccumulator</property> <property name="group">PathAccumulator</property>
</object> </object>
...@@ -1575,12 +1574,13 @@ Only available for the Push Cutter.</property> ...@@ -1575,12 +1574,13 @@ Only available for the Push Cutter.</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkRadioButton" id="ZigZagCutter"> <object class="GtkRadioButton" id="SimpleCutter">
<property name="label" translatable="yes">ZigZag Cutter</property> <property name="label" translatable="yes">Simple Cutter</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Quite simple path processor for finishing operations.</property> <property name="tooltip_text" translatable="yes">Practically useless path processor for testing purposes.
Only available for the Push Cutter.</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<property name="group">PathAccumulator</property> <property name="group">PathAccumulator</property>
</object> </object>
...@@ -1708,8 +1708,7 @@ Only available for the Push Cutter.</property> ...@@ -1708,8 +1708,7 @@ Only available for the Push Cutter.</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Specify the amount of material that should remain around the model. <property name="tooltip_text" translatable="yes">Specify the amount of material that should remain around the model.
This is useful for rough and semi-finish operations. This is useful for rough and semi-finish operations.
ODE support is currently required for this feature. ODE support is currently required for this feature.</property>
For now only the Drop Cutter respects this value.</property>
<property name="invisible_char">&#x2022;</property> <property name="invisible_char">&#x2022;</property>
<property name="adjustment">MaterialAllowanceValue</property> <property name="adjustment">MaterialAllowanceValue</property>
<property name="digits">2</property> <property name="digits">2</property>
...@@ -1858,7 +1857,8 @@ For now only the Drop Cutter respects this value.</property> ...@@ -1858,7 +1857,8 @@ For now only the Drop Cutter respects this value.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Requires the Python Bindings for ODE. Install the python-pyode package to enable this feature.</property> <property name="tooltip_text" translatable="yes">Requires the Python bindings for ODE (Open Dynamics Engine).
Install the "python-pyode" package to enable this feature.</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
</object> </object>
<packing> <packing>
......
...@@ -71,26 +71,46 @@ class PhysicalWorld: ...@@ -71,26 +71,46 @@ class PhysicalWorld:
self._drill_offset = position self._drill_offset = position
self._drill = shape self._drill = shape
def extend_drill(self, diff_x, diff_y, diff_z):
try:
func = self._drill.extend_shape
except ValueError:
return
func(diff_x, diff_y, diff_z)
def reset_drill(self):
try:
func = self._drill.reset_shape
except ValueError:
return
func()
def set_drill_position(self, position): def set_drill_position(self, position):
if self._drill: if self._drill:
position = (position[0] + self._drill_offset[0], position[1] + self._drill_offset[1], position[2] + self._drill_offset[2]) position = (position[0] + self._drill_offset[0], position[1] + self._drill_offset[1], position[2] + self._drill_offset[2])
self._drill.setPosition(position) self._drill.setPosition(position)
def _collision_callback(self, dummy, geom1, geom2): def _collision_callback(self, dummy, geom1, geom2):
if geom1 is self._drill: drill_body = self._drill.getBody()
if geom1.getBody() is drill_body:
obstacle = geom2 obstacle = geom2
elif geom2 is self._drill: elif geom2.getBody() is drill_body:
obstacle = geom1 obstacle = geom1
else: else:
return return
contacts = ode.collide(self._drill, obstacle) # check if the drill is made up of multiple geoms
try:
children = self._drill.children[:]
except AttributeError:
children = []
contacts = []
for shape in children + [self._drill]:
contacts.extend(ode.collide(shape, obstacle))
# break early to improve performance
if contacts:
break
if contacts: if contacts:
self._collision_detected = True self._collision_detected = True
return
for c in contacts:
# no bounce effect
c.setBounce(0)
ode.ContactJoint(self._world, self._contacts, c).attach(self._drill.getBody(), obstacle.getBody())
def check_collision(self): def check_collision(self):
self._collision_detected = False self._collision_detected = False
...@@ -98,3 +118,6 @@ class PhysicalWorld: ...@@ -98,3 +118,6 @@ class PhysicalWorld:
self._contacts.empty() self._contacts.empty()
return self._collision_detected return self._collision_detected
def get_space(self):
return self._space
from pycam.PathProcessors import * from pycam.PathProcessors import *
from pycam.Geometry import * from pycam.Geometry import *
from pycam.Geometry.utils import * from pycam.Geometry.utils import *
import pycam.PathGenerators
from pycam.Exporters.SVGExporter import SVGExporter from pycam.Exporters.SVGExporter import SVGExporter
import math
DEBUG_PUSHCUTTER = False DEBUG_PUSHCUTTER = False
DEBUG_PUSHCUTTER2 = False DEBUG_PUSHCUTTER2 = False
...@@ -21,10 +22,11 @@ class Hit: ...@@ -21,10 +22,11 @@ class Hit:
class PushCutter: class PushCutter:
def __init__(self, cutter, model, pathextractor=None): def __init__(self, cutter, model, pathextractor=None, physics=None):
self.cutter = cutter self.cutter = cutter
self.model = model self.model = model
self.pa = pathextractor self.pa = pathextractor
self.physics = physics
def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dx, dy, dz, draw_callback=None): def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dx, dy, dz, draw_callback=None):
if dx != 0: if dx != 0:
...@@ -44,14 +46,19 @@ class PushCutter: ...@@ -44,14 +46,19 @@ class PushCutter:
paths = [] paths = []
if self.physics is None:
GenerateToolPathSlice = self.GenerateToolPathSlice_triangles
else:
GenerateToolPathSlice = self.GenerateToolPathSlice_ode
while z >= minz: while z >= minz:
if dy > 0: if dy > 0:
self.pa.new_direction(0) self.pa.new_direction(0)
self.GenerateToolPathSlice(minx, maxx, miny, maxy, z, 0, dy, draw_callback) GenerateToolPathSlice(minx, maxx, miny, maxy, z, 0, dy, draw_callback)
self.pa.end_direction() self.pa.end_direction()
if dx > 0: if dx > 0:
self.pa.new_direction(1) self.pa.new_direction(1)
self.GenerateToolPathSlice(minx, maxx, miny, maxy, z, dx, 0, draw_callback) GenerateToolPathSlice(minx, maxx, miny, maxy, z, dx, 0, draw_callback)
self.pa.end_direction() self.pa.end_direction()
self.pa.finish() self.pa.finish()
...@@ -72,6 +79,108 @@ class PushCutter: ...@@ -72,6 +79,108 @@ class PushCutter:
return paths return paths
def get_free_paths_ode(self, minx, maxx, miny, maxy, z, depth=8):
""" Recursive function for splitting a line (usually along x or y) into
small pieces to gather connected paths for the PushCutter.
Strategy: check if the whole line is free (without collisions). Do a
recursive call (for the first and second half), if there was a
collision.
Usually either minx/maxx or miny/maxy should be equal, unless you want
to do a diagonal cut.
@param minx: lower limit of x
@type minx: float
@param maxx: upper limit of x; should equal minx for a cut along the x axis
@type maxx: float
@param miny: lower limit of y
@type miny: float
@param maxy: upper limit of y; should equal miny for a cut along the y axis
@type maxy: float
@param z: the fixed z level
@type z: float
@param depth: number of splits to be calculated via recursive calls; the
accuracy can be calculated as (maxx-minx)/(2^depth)
@type depth: int
@returns: a list of points that describe the tool path of the PushCutter;
each pair of points defines a collision-free path
@rtype: list(pycam.Geometry.Point.Point)
"""
points = []
# "resize" the drill along the while x/y range and check for a collision
self.physics.extend_drill(maxx-minx, maxy-miny, 0.0)
self.physics.set_drill_position((minx, miny, z))
if self.physics.check_collision():
# collision detected
if depth > 0:
middle_x = (minx + maxx)/2.0
middle_y = (miny + maxy)/2.0
group1 = self.get_free_paths_ode(minx, middle_x, miny, middle_y, z, depth-1)
group2 = self.get_free_paths_ode(middle_x, maxx, middle_y, maxy, z, depth-1)
if group1 and group2 and (group1[-1].x == group2[0].x) and (group1[-1].y == group2[0].y):
# the last couple of the first group ends where the first couple of the second group starts
# we will combine them into one couple
last = group1[-2]
first = group2[1]
combined = [last, first]
points.extend(group1[:-2])
points.extend(combined)
points.extend(group2[2:])
else:
# the two groups are not connected - just add both
points.extend(group1)
points.extend(group2)
else:
# no points to be added
pass
else:
# no collision - the line is free
points.append(Point(minx, miny, z))
points.append(Point(maxx, maxy, z))
self.physics.reset_drill()
return points
def GenerateToolPathSlice_ode(self, minx, maxx, miny, maxy, z, dx, dy, draw_callback=None):
""" only dx or (exclusive!) dy may be bigger than zero
"""
# max_deviation_x = dx/accuracy
accuracy = 20
x = minx
y = miny
# calculate the required number of steps in each direction
if dx > 0:
depth_x = math.log(accuracy * (maxx-minx) / dx) / math.log(2)
depth_x = max(math.ceil(int(depth_x)), 4)
else:
depth_y = math.log(accuracy * (maxy-miny) / dy) / math.log(2)
depth_y = max(math.ceil(int(depth_y)), 4)
while (x <= maxx) and (y <= maxy):
points = []
self.pa.new_scanline()
if dx > 0:
points = self.get_free_paths_ode(x, x, miny, maxy, z, depth=depth_x)
else:
points = self.get_free_paths_ode(minx, maxx, y, y, z, depth=depth_y)
for p in points:
self.pa.append(p)
if points:
self.cutter.moveto(points[-1])
if draw_callback:
draw_callback()
self.pa.end_scanline()
if dx > 0:
x += dx
if (x > maxx) and (x - dx < maxx):
x = maxx
else:
y += dy
if (y > maxy) and (y - dy < maxy):
y = maxy
def DropCutterTest(self, point, model): def DropCutterTest(self, point, model):
zmax = -INFINITE zmax = -INFINITE
tmax = None tmax = None
...@@ -85,7 +194,7 @@ class PushCutter: ...@@ -85,7 +194,7 @@ class PushCutter:
tmax = t tmax = t
return (zmax, tmax) return (zmax, tmax)
def GenerateToolPathSlice(self, minx, maxx, miny, maxy, z, dx, dy, draw_callback=None): def GenerateToolPathSlice_triangles(self, minx, maxx, miny, maxy, z, dx, dy, draw_callback=None):
global DEBUG_PUSHCUTTER, DEBUG_PUSHCUTTER2, DEBUG_PUSHCUTTER3 global DEBUG_PUSHCUTTER, DEBUG_PUSHCUTTER2, DEBUG_PUSHCUTTER3
c = self.cutter c = self.cutter
model = self.model model = self.model
......
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