Commit 84ab9bff authored by sumpfralle's avatar sumpfralle

added support for automatically distributed support bridges at corners or at edges


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@959 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent f7b6f7ee
...@@ -21,6 +21,7 @@ Version 0.4.1 - UNRELEASED ...@@ -21,6 +21,7 @@ Version 0.4.1 - UNRELEASED
* visibility of 3D view items is now configurable in the 3D window * visibility of 3D view items is now configurable in the 3D window
* via a button and via the context menu * via a button and via the context menu
* improved stability of remote processing: disconnected nodes should not cause problems anymore * improved stability of remote processing: disconnected nodes should not cause problems anymore
* automatically distributed support bridges can now be placed at corners or edges
Version 0.4.0.1 - 2010-10-24 Version 0.4.0.1 - 2010-10-24
* disabled parallel processing for Windows standalone executable * disabled parallel processing for Windows standalone executable
......
...@@ -216,7 +216,10 @@ ...@@ -216,7 +216,10 @@
<col id="0" translatable="yes">grid pattern</col> <col id="0" translatable="yes">grid pattern</col>
</row> </row>
<row> <row>
<col id="0" translatable="yes">automatic distribution</col> <col id="0" translatable="yes">automatic distribution (edges)</col>
</row>
<row>
<col id="0" translatable="yes">automatic distribution (corners)</col>
</row> </row>
</data> </data>
</object> </object>
......
...@@ -142,6 +142,7 @@ class Polygon(TransformableContainer): ...@@ -142,6 +142,7 @@ class Polygon(TransformableContainer):
Currently this works only for line groups in an xy-plane. Currently this works only for line groups in an xy-plane.
Returns zero for empty line groups or for open line groups. Returns zero for empty line groups or for open line groups.
Returns negative values for inner hole. Returns negative values for inner hole.
TODO: "get_area" is wrong by some factor - check the result!
""" """
if not self._points: if not self._points:
return 0 return 0
...@@ -163,6 +164,55 @@ class Polygon(TransformableContainer): ...@@ -163,6 +164,55 @@ class Polygon(TransformableContainer):
self._area_cache = result / 2 self._area_cache = result / 2
return self._area_cache return self._area_cache
def get_barycenter(self):
area = self.get_area()
if not area:
return None
# TODO: for now we just calculate the "middle of the outline" - the "barycenter" code below needs to be fixed
return Point((self.maxx + self.minx) / 2, (self.maxy + self.miny) / 2,
(self.maxz + self.minz) / 2)
# see: http://stackoverflow.com/questions/2355931/compute-the-centroid-of-a-3d-planar-polygon/2360507
# first: calculate cx and y
cxy, cxz, cyx, cyz, czx, czy = (0, 0, 0, 0, 0, 0)
for index in range(len(self._points)):
p1 = self._points[index]
p2 = self._points[(index + 1) % len(self._points)]
cxy += (p1.x + p2.x) * (p1.x * p2.y - p1.y * p2.x)
cxz += (p1.x + p2.x) * (p1.x * p2.z - p1.z * p2.x)
cyx += (p1.y + p2.y) * (p1.y * p2.x - p1.x * p2.y)
cyz += (p1.y + p2.y) * (p1.y * p2.z - p1.z * p2.y)
czx += (p1.z + p2.z) * (p1.z * p2.x - p1.x * p2.z)
czy += (p1.z + p2.z) * (p1.z * p2.y - p1.y * p2.z)
if self.minz == self.maxz:
return Point(cxy / (6 * area), cyx / (6 * area), self.minz)
elif self.miny == self.maxy:
return Point(cxz / (6 * area), self.miny, czx / (6 * area))
elif self.minz == self.maxz:
return Point(self.minx, cyz / (6 * area), czy / (6 * area))
else:
# calculate area of xy projection
area_xy = self.get_plane_projection(Plane(Point(0, 0, 0),
Point(0, 0, 1))).get_area()
area_xz = self.get_plane_projection(Plane(Point(0, 0, 0),
Point(0, 1, 0))).get_area()
area_yz = self.get_plane_projection(Plane(Point(0, 0, 0),
Point(1, 0, 0))).get_area()
if 0 in (area_xy, area_xz, area_yz):
log.info("Failed assumtion: zero-sized projected area - " + \
"%s / %s / %s" % (area_xy, area_xz, area_yz))
return Point(0, 0, 0)
if abs(cxy / area_xy - cxz / area_xz) > epsilon:
log.info("Failed assumption: barycenter xy/xz - %s / %s" % \
(cxy / area_xy, cxz / area_xz))
if abs(cyx / area_xy - cyz / area_yz) > epsilon:
log.info("Failed assumption: barycenter yx/yz - %s / %s" % \
(cyx / area_xy, cyz / area_yz))
if abs(czx / area_xz - czy / area_yz) > epsilon:
log.info("Failed assumption: barycenter zx/zy - %s / %s" % \
(czx / area_xz, cyz / area_yz))
return Point(cxy / (6 * area_xy), cyx / (6 * area_xy),
czx / (6 * area_xz))
def get_length(self): def get_length(self):
""" add the length of all lines within the polygon """ add the length of all lines within the polygon
""" """
...@@ -306,6 +356,7 @@ class Polygon(TransformableContainer): ...@@ -306,6 +356,7 @@ class Polygon(TransformableContainer):
self.minz = min(self.minz, point.z) self.minz = min(self.minz, point.z)
self.maxz = max(self.maxz, point.z) self.maxz = max(self.maxz, point.z)
self._lines_cache = None self._lines_cache = None
self._area_cache = None
def reset_cache(self): def reset_cache(self):
self._cached_offset_polygons = {} self._cached_offset_polygons = {}
......
...@@ -145,7 +145,7 @@ PREFERENCES_DEFAULTS = { ...@@ -145,7 +145,7 @@ PREFERENCES_DEFAULTS = {
""" the listed items will be loaded/saved via the preferences file in the """ the listed items will be loaded/saved via the preferences file in the
user's home directory on startup/shutdown""" user's home directory on startup/shutdown"""
GRID_TYPES = {"none": 0, "grid": 1, "automatic": 2} GRID_TYPES = {"none": 0, "grid": 1, "automatic_edge": 2, "automatic_corner": 3}
POCKETING_TYPES = ["none", "holes", "enclosed"] POCKETING_TYPES = ["none", "holes", "enclosed"]
MAX_UNDO_STATES = 10 MAX_UNDO_STATES = 10
...@@ -1185,11 +1185,13 @@ class ProjectGui: ...@@ -1185,11 +1185,13 @@ class ProjectGui:
@gui_activity_guard @gui_activity_guard
def update_support_grid_controls(self, widget=None): def update_support_grid_controls(self, widget=None):
controls = {"GridProfileExpander": ("grid", "automatic"), controls = {"GridProfileExpander": ("grid", "automatic_edge",
"automatic_corner"),
"GridPatternExpander": ("grid", ), "GridPatternExpander": ("grid", ),
"GridPositionExpander": ("grid", ), "GridPositionExpander": ("grid", ),
"GridManualShiftExpander": ("grid", ), "GridManualShiftExpander": ("grid", ),
"GridAverageDistanceExpander": ("automatic", ), "GridAverageDistanceExpander": ("automatic_edge",
"automatic_corner"),
} }
grid_type = self.settings.get("support_grid_type") grid_type = self.settings.get("support_grid_type")
if grid_type == GRID_TYPES["grid"]: if grid_type == GRID_TYPES["grid"]:
...@@ -1202,9 +1204,8 @@ class ProjectGui: ...@@ -1202,9 +1204,8 @@ class ProjectGui:
self.settings.get("support_grid_distance_x")) self.settings.get("support_grid_distance_x"))
self.update_support_grid_manual_model() self.update_support_grid_manual_model()
self.switch_support_grid_manual_selector() self.switch_support_grid_manual_selector()
elif grid_type == GRID_TYPES["automatic"]: elif grid_type in (GRID_TYPES["automatic_edge"],
pass GRID_TYPES["automatic_corner"], GRID_TYPES["none"]):
elif grid_type == GRID_TYPES["none"]:
pass pass
elif grid_type < 0: elif grid_type < 0:
# not initialized # not initialized
...@@ -1246,7 +1247,8 @@ class ProjectGui: ...@@ -1246,7 +1247,8 @@ class ProjectGui:
offset_y=s.get("support_grid_offset_y"), offset_y=s.get("support_grid_offset_y"),
adjustments_x=self.grid_adjustments_x, adjustments_x=self.grid_adjustments_x,
adjustments_y=self.grid_adjustments_y) adjustments_y=self.grid_adjustments_y)
elif grid_type == GRID_TYPES["automatic"]: elif grid_type in (GRID_TYPES["automatic_edge"],
GRID_TYPES["automatic_corner"]):
if (s.get("support_grid_thickness") > 0) \ if (s.get("support_grid_thickness") > 0) \
and (s.get("support_grid_height") > 0) \ and (s.get("support_grid_height") > 0) \
and (s.get("support_grid_average_distance") > 0) \ and (s.get("support_grid_average_distance") > 0) \
...@@ -1258,13 +1260,15 @@ class ProjectGui: ...@@ -1258,13 +1260,15 @@ class ProjectGui:
if not bounds is None: if not bounds is None:
minz = bounds.get_absolute_limits( minz = bounds.get_absolute_limits(
reference=self.model.get_bounds())[0][2] reference=self.model.get_bounds())[0][2]
corner_start = (grid_type == GRID_TYPES["automatic_corner"])
support_grid = pycam.Toolpath.SupportGrid.get_support_distributed( support_grid = pycam.Toolpath.SupportGrid.get_support_distributed(
s.get("model"), minz, s.get("model"), minz,
s.get("support_grid_average_distance"), s.get("support_grid_average_distance"),
s.get("support_grid_minimum_bridges"), s.get("support_grid_minimum_bridges"),
s.get("support_grid_thickness"), s.get("support_grid_thickness"),
s.get("support_grid_height"), s.get("support_grid_height"),
s.get("support_grid_length")) s.get("support_grid_length"),
start_at_corners=corner_start)
elif grid_type == GRID_TYPES["none"]: elif grid_type == GRID_TYPES["none"]:
pass pass
s.set("support_grid", support_grid) s.set("support_grid", support_grid)
...@@ -3605,13 +3609,16 @@ class ProjectGui: ...@@ -3605,13 +3609,16 @@ class ProjectGui:
offset_y=self.settings.get("support_grid_offset_y"), offset_y=self.settings.get("support_grid_offset_y"),
adjustments_x=self.grid_adjustments_x, adjustments_x=self.grid_adjustments_x,
adjustments_y=self.grid_adjustments_y) adjustments_y=self.grid_adjustments_y)
elif grid_type == GRID_TYPES["automatic"]: elif grid_type in (GRID_TYPES["automatic_edge"],
GRID_TYPES["automatic_corner"]):
corner_start = (grid_type == GRID_TYPES["automatic_corner"])
toolpath_settings.set_support_distributed( toolpath_settings.set_support_distributed(
self.settings.get("support_grid_average_distance"), self.settings.get("support_grid_average_distance"),
self.settings.get("support_grid_minimum_bridges"), self.settings.get("support_grid_minimum_bridges"),
self.settings.get("support_grid_thickness"), self.settings.get("support_grid_thickness"),
self.settings.get("support_grid_height"), self.settings.get("support_grid_height"),
self.settings.get("support_grid_length")) self.settings.get("support_grid_length"),
start_at_corner=corner_start)
elif grid_type == GRID_TYPES["none"]: elif grid_type == GRID_TYPES["none"]:
pass pass
else: else:
......
...@@ -582,6 +582,7 @@ class ToolpathSettings: ...@@ -582,6 +582,7 @@ class ToolpathSettings:
"average_distance": float, "average_distance": float,
"minimum_bridges": int, "minimum_bridges": int,
"length": float, "length": float,
"start_at_corner": bool,
}, },
"Program": { "Program": {
"unit": str, "unit": str,
...@@ -660,13 +661,14 @@ class ToolpathSettings: ...@@ -660,13 +661,14 @@ class ToolpathSettings:
self.support_grid["adjustments_y"] = adjustments_y self.support_grid["adjustments_y"] = adjustments_y
def set_support_distributed(self, average_distance, minimum_bridges, def set_support_distributed(self, average_distance, minimum_bridges,
thickness, height, length): thickness, height, length, start_at_corner=False):
self.support_grid["type"] = "distributed" self.support_grid["type"] = "distributed"
self.support_grid["average_distance"] = average_distance self.support_grid["average_distance"] = average_distance
self.support_grid["minimum_bridges"] = minimum_bridges self.support_grid["minimum_bridges"] = minimum_bridges
self.support_grid["thickness"] = thickness self.support_grid["thickness"] = thickness
self.support_grid["height"] = height self.support_grid["height"] = height
self.support_grid["length"] = length self.support_grid["length"] = length
self.support_grid["start_at_corner"] = start_at_corner
def get_support_grid(self): def get_support_grid(self):
result = {} result = {}
......
...@@ -26,6 +26,7 @@ from pycam.Geometry.Triangle import Triangle ...@@ -26,6 +26,7 @@ from pycam.Geometry.Triangle import Triangle
from pycam.Geometry.Plane import Plane from pycam.Geometry.Plane import Plane
from pycam.Geometry.Model import Model from pycam.Geometry.Model import Model
from pycam.Geometry.utils import number from pycam.Geometry.utils import number
import pycam.Geometry
def _get_triangles_for_face(pts): def _get_triangles_for_face(pts):
...@@ -151,12 +152,8 @@ def get_support_grid(minx, maxx, miny, maxy, z_plane, dist_x, dist_y, thickness, ...@@ -151,12 +152,8 @@ def get_support_grid(minx, maxx, miny, maxy, z_plane, dist_x, dist_y, thickness,
return grid_model return grid_model
def get_support_distributed(model, z_plane, average_distance, def get_support_distributed(model, z_plane, average_distance,
min_bridges_per_polygon, thickness, height, length): min_bridges_per_polygon, thickness, height, length,
def is_near_list(point_list, point, distance): start_at_corners=False):
for p in point_list:
if p.sub(point).norm <= distance:
return True
return False
if (average_distance == 0) or (length == 0) or (thickness == 0) \ if (average_distance == 0) or (length == 0) or (thickness == 0) \
or (height == 0): or (height == 0):
return return
...@@ -166,62 +163,159 @@ def get_support_distributed(model, z_plane, average_distance, ...@@ -166,62 +163,159 @@ def get_support_distributed(model, z_plane, average_distance,
else: else:
polygons = model.get_waterline_contour(Plane(Point(0, 0, z_plane), polygons = model.get_waterline_contour(Plane(Point(0, 0, z_plane),
Vector(0, 0, 1))).get_polygons() Vector(0, 0, 1))).get_polygons()
bridge_positions = []
# minimum required distance between two bridge start points # minimum required distance between two bridge start points
avoid_distance = 1.5 * (abs(length) + thickness) avoid_distance = 1.5 * (abs(length) + thickness)
if start_at_corners:
bridge_calculator = _get_corner_bridges
else:
bridge_calculator = _get_edge_bridges
for polygon in polygons: for polygon in polygons:
# no grid for _small_ inner polygons # no grid for _small_ inner polygons
# TODO: calculate a reasonable factor (see below) # TODO: calculate a reasonable factor (see below)
if polygon.is_closed and (not polygon.is_outer()) \ if polygon.is_closed and (not polygon.is_outer()) \
and (abs(polygon.get_area()) < 25000 * thickness ** 2): and (abs(polygon.get_area()) < 25000 * thickness ** 2):
continue continue
lines = polygon.get_lines() bridges = bridge_calculator(polygon, z_plane, min_bridges_per_polygon,
poly_lengths = polygon.get_lengths() average_distance, avoid_distance)
num_of_bridges = max(min_bridges_per_polygon, for pos, direction in bridges:
int(round(sum(poly_lengths) / average_distance))) _add_cuboid_to_model(result, pos, direction.mul(length), height,
real_average_distance = sum(poly_lengths) / num_of_bridges thickness)
max_line_index = poly_lengths.index(max(poly_lengths)) return result
positions = []
current_line_index = max_line_index
distance_processed = poly_lengths[current_line_index] / 2 class _BridgeCorner(object):
positions.append(current_line_index) # currently we only use the xy plane
while len(positions) < num_of_bridges: up_vector = Vector(0, 0, 1)
def __init__(self, barycenter, location, p1, p2, p3):
self.location = location
self.position = p2
self.direction = pycam.Geometry.get_bisector(p1, p2, p3,
self.up_vector).normalized()
preferred_direction = p2.sub(barycenter).normalized()
# direction_factor: 0..1 (bigger -> better)
direction_factor = (preferred_direction.dot(self.direction) + 1) / 2
angle = pycam.Geometry.get_angle_pi(p1, p2, p3,
self.up_vector, pi_factor=True)
# angle_factor: 0..1 (bigger -> better)
if angle > 0.5:
# use only angles > 90 degree
angle_factor = angle / 2.0
else:
angle_factor = 0
# priority: 0..1 (bigger -> better)
self.priority = angle_factor * direction_factor
def get_position_priority(self, other_location, average_distance):
return self.priority / (1 + self.get_distance(other_location) / \
average_distance)
def get_distance(self, other_location):
return min(abs(other_location - self.location),
abs(1 + other_location - self.location))
def __str__(self):
return "%s (%s) - %s" % (self.position, self.location, self.priority)
def _get_corner_bridges(polygon, z_plane, min_bridges, average_distance, avoid_distance):
""" try to place support bridges at corners of a polygon
Priorities:
- bigger corner angles are preferred
- directions pointing away from the center of the polygon are preferred
"""
center = polygon.get_barycenter()
points = polygon.get_points()
lines = polygon.get_lines()
poly_lengths = polygon.get_lengths()
outline = sum(poly_lengths)
rel_avoid_distance = avoid_distance / outline
corner_positions = []
length_sum = 0
for l in poly_lengths:
corner_positions.append(length_sum / outline)
length_sum += l
num_of_bridges = int(max(min_bridges, round(outline / average_distance)))
rel_average_distance = 1.0 / num_of_bridges
corners = []
for index in range(len(polygon.get_points())):
p1 = points[(index - 1) % len(points)]
p2 = points[index % len(points)]
p3 = points[(index + 1) % len(points)]
corner = _BridgeCorner(center, corner_positions[index], p1, p2, p3)
if corner.priority > 0:
# ignore sharp corners
corners.append(corner)
bridge_corners = []
for index in range(num_of_bridges):
preferred_position = index * rel_average_distance
suitable_corners = []
for corner in corners:
if corner.get_distance(preferred_position) < rel_average_distance:
# check if the corner is too close to neighbouring corners
if (not bridge_corners) or \
((bridge_corners[-1].get_distance(corner.location) >= rel_avoid_distance) and \
(bridge_corners[0].get_distance(corner.location) >= rel_avoid_distance)):
suitable_corners.append(corner)
get_priority = lambda corner: corner.get_position_priority(
preferred_position, rel_average_distance)
suitable_corners.sort(key=get_priority, reverse=True)
if suitable_corners:
bridge_corners.append(suitable_corners[0])
corners.remove(suitable_corners[0])
return [(c.position, c.direction) for c in bridge_corners]
def _get_edge_bridges(polygon, z_plane, min_bridges, average_distance, avoid_distance):
def is_near_list(point_list, point, distance):
for p in point_list:
if p.sub(point).norm <= distance:
return True
return False
lines = polygon.get_lines()
poly_lengths = polygon.get_lengths()
num_of_bridges = max(min_bridges,
int(round(sum(poly_lengths) / average_distance)))
real_average_distance = sum(poly_lengths) / num_of_bridges
max_line_index = poly_lengths.index(max(poly_lengths))
positions = []
current_line_index = max_line_index
distance_processed = poly_lengths[current_line_index] / 2
positions.append(current_line_index)
while len(positions) < num_of_bridges:
current_line_index += 1
current_line_index %= len(poly_lengths)
# skip lines that are not at least twice as long as the grid width
while (distance_processed + poly_lengths[current_line_index] \
< real_average_distance):
distance_processed += poly_lengths[current_line_index]
current_line_index += 1 current_line_index += 1
current_line_index %= len(poly_lengths) current_line_index %= len(poly_lengths)
# skip lines that are not at least twice as long as the grid width positions.append(current_line_index)
while (distance_processed + poly_lengths[current_line_index] \ distance_processed += poly_lengths[current_line_index]
< real_average_distance): distance_processed %= real_average_distance
distance_processed += poly_lengths[current_line_index] result = []
current_line_index += 1 bridge_positions = []
current_line_index %= len(poly_lengths) for line_index in positions:
positions.append(current_line_index) position = polygon.get_middle_of_line(line_index)
distance_processed += poly_lengths[current_line_index] # skip bridges that are close to another existing bridge
distance_processed %= real_average_distance if is_near_list(bridge_positions, position, avoid_distance):
for line_index in positions: line = polygon.get_lines()[line_index]
position = polygon.get_middle_of_line(line_index) # calculate two alternative points on the same line
# skip bridges that are close to another existing bridge position1 = position.add(line.p1).div(2)
if is_near_list(bridge_positions, position, avoid_distance): position2 = position.add(line.p2).div(2)
line = polygon.get_lines()[line_index] if is_near_list(bridge_positions, position1, avoid_distance):
# calculate two alternative points on the same line if is_near_list(bridge_positions, position2,
position1 = position.add(line.p1).div(2) avoid_distance):
position2 = position.add(line.p2).div(2) # no valid alternative - we skip this bridge
if is_near_list(bridge_positions, position1, avoid_distance): continue
if is_near_list(bridge_positions, position2,
avoid_distance):
# no valid alternative - we skip this bridge
continue
else:
# position2 is OK
position = position2
else: else:
# position1 is OK # position2 is OK
position = position1 position = position2
# append the original position (ignoring z_plane) else:
bridge_positions.append(position) # position1 is OK
# move the point to z_plane position = position1
position = Point(position.x, position.y, z_plane) # append the original position (ignoring z_plane)
bridge_dir = lines[line_index].dir.cross( bridge_positions.append(position)
polygon.plane.n).normalized().mul(length) # move the point to z_plane
_add_cuboid_to_model(result, position, bridge_dir, height, thickness) position = Point(position.x, position.y, z_plane)
bridge_dir = lines[line_index].dir.cross(
polygon.plane.n).normalized()
result.append((position, bridge_dir))
return result return result
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