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
* visibility of 3D view items is now configurable in the 3D window
* via a button and via the context menu
* 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
* disabled parallel processing for Windows standalone executable
......
......@@ -216,7 +216,10 @@
<col id="0" translatable="yes">grid pattern</col>
</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>
</data>
</object>
......
......@@ -142,6 +142,7 @@ class Polygon(TransformableContainer):
Currently this works only for line groups in an xy-plane.
Returns zero for empty line groups or for open line groups.
Returns negative values for inner hole.
TODO: "get_area" is wrong by some factor - check the result!
"""
if not self._points:
return 0
......@@ -163,6 +164,55 @@ class Polygon(TransformableContainer):
self._area_cache = result / 2
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):
""" add the length of all lines within the polygon
"""
......@@ -306,6 +356,7 @@ class Polygon(TransformableContainer):
self.minz = min(self.minz, point.z)
self.maxz = max(self.maxz, point.z)
self._lines_cache = None
self._area_cache = None
def reset_cache(self):
self._cached_offset_polygons = {}
......
......@@ -145,7 +145,7 @@ PREFERENCES_DEFAULTS = {
""" the listed items will be loaded/saved via the preferences file in the
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"]
MAX_UNDO_STATES = 10
......@@ -1185,11 +1185,13 @@ class ProjectGui:
@gui_activity_guard
def update_support_grid_controls(self, widget=None):
controls = {"GridProfileExpander": ("grid", "automatic"),
controls = {"GridProfileExpander": ("grid", "automatic_edge",
"automatic_corner"),
"GridPatternExpander": ("grid", ),
"GridPositionExpander": ("grid", ),
"GridManualShiftExpander": ("grid", ),
"GridAverageDistanceExpander": ("automatic", ),
"GridAverageDistanceExpander": ("automatic_edge",
"automatic_corner"),
}
grid_type = self.settings.get("support_grid_type")
if grid_type == GRID_TYPES["grid"]:
......@@ -1202,9 +1204,8 @@ class ProjectGui:
self.settings.get("support_grid_distance_x"))
self.update_support_grid_manual_model()
self.switch_support_grid_manual_selector()
elif grid_type == GRID_TYPES["automatic"]:
pass
elif grid_type == GRID_TYPES["none"]:
elif grid_type in (GRID_TYPES["automatic_edge"],
GRID_TYPES["automatic_corner"], GRID_TYPES["none"]):
pass
elif grid_type < 0:
# not initialized
......@@ -1246,7 +1247,8 @@ class ProjectGui:
offset_y=s.get("support_grid_offset_y"),
adjustments_x=self.grid_adjustments_x,
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) \
and (s.get("support_grid_height") > 0) \
and (s.get("support_grid_average_distance") > 0) \
......@@ -1258,13 +1260,15 @@ class ProjectGui:
if not bounds is None:
minz = bounds.get_absolute_limits(
reference=self.model.get_bounds())[0][2]
corner_start = (grid_type == GRID_TYPES["automatic_corner"])
support_grid = pycam.Toolpath.SupportGrid.get_support_distributed(
s.get("model"), minz,
s.get("support_grid_average_distance"),
s.get("support_grid_minimum_bridges"),
s.get("support_grid_thickness"),
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"]:
pass
s.set("support_grid", support_grid)
......@@ -3605,13 +3609,16 @@ class ProjectGui:
offset_y=self.settings.get("support_grid_offset_y"),
adjustments_x=self.grid_adjustments_x,
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(
self.settings.get("support_grid_average_distance"),
self.settings.get("support_grid_minimum_bridges"),
self.settings.get("support_grid_thickness"),
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"]:
pass
else:
......
......@@ -582,6 +582,7 @@ class ToolpathSettings:
"average_distance": float,
"minimum_bridges": int,
"length": float,
"start_at_corner": bool,
},
"Program": {
"unit": str,
......@@ -660,13 +661,14 @@ class ToolpathSettings:
self.support_grid["adjustments_y"] = adjustments_y
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["average_distance"] = average_distance
self.support_grid["minimum_bridges"] = minimum_bridges
self.support_grid["thickness"] = thickness
self.support_grid["height"] = height
self.support_grid["length"] = length
self.support_grid["start_at_corner"] = start_at_corner
def get_support_grid(self):
result = {}
......
......@@ -26,6 +26,7 @@ from pycam.Geometry.Triangle import Triangle
from pycam.Geometry.Plane import Plane
from pycam.Geometry.Model import Model
from pycam.Geometry.utils import number
import pycam.Geometry
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,
return grid_model
def get_support_distributed(model, z_plane, average_distance,
min_bridges_per_polygon, thickness, height, length):
def is_near_list(point_list, point, distance):
for p in point_list:
if p.sub(point).norm <= distance:
return True
return False
min_bridges_per_polygon, thickness, height, length,
start_at_corners=False):
if (average_distance == 0) or (length == 0) or (thickness == 0) \
or (height == 0):
return
......@@ -166,62 +163,159 @@ def get_support_distributed(model, z_plane, average_distance,
else:
polygons = model.get_waterline_contour(Plane(Point(0, 0, z_plane),
Vector(0, 0, 1))).get_polygons()
bridge_positions = []
# minimum required distance between two bridge start points
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:
# no grid for _small_ inner polygons
# TODO: calculate a reasonable factor (see below)
if polygon.is_closed and (not polygon.is_outer()) \
and (abs(polygon.get_area()) < 25000 * thickness ** 2):
continue
lines = polygon.get_lines()
poly_lengths = polygon.get_lengths()
num_of_bridges = max(min_bridges_per_polygon,
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:
bridges = bridge_calculator(polygon, z_plane, min_bridges_per_polygon,
average_distance, avoid_distance)
for pos, direction in bridges:
_add_cuboid_to_model(result, pos, direction.mul(length), height,
thickness)
return result
class _BridgeCorner(object):
# currently we only use the xy plane
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 %= 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 %= len(poly_lengths)
positions.append(current_line_index)
distance_processed += poly_lengths[current_line_index]
distance_processed %= real_average_distance
for line_index in positions:
position = polygon.get_middle_of_line(line_index)
# skip bridges that are close to another existing bridge
if is_near_list(bridge_positions, position, avoid_distance):
line = polygon.get_lines()[line_index]
# calculate two alternative points on the same line
position1 = position.add(line.p1).div(2)
position2 = position.add(line.p2).div(2)
if is_near_list(bridge_positions, position1, avoid_distance):
if is_near_list(bridge_positions, position2,
avoid_distance):
# no valid alternative - we skip this bridge
continue
else:
# position2 is OK
position = position2
positions.append(current_line_index)
distance_processed += poly_lengths[current_line_index]
distance_processed %= real_average_distance
result = []
bridge_positions = []
for line_index in positions:
position = polygon.get_middle_of_line(line_index)
# skip bridges that are close to another existing bridge
if is_near_list(bridge_positions, position, avoid_distance):
line = polygon.get_lines()[line_index]
# calculate two alternative points on the same line
position1 = position.add(line.p1).div(2)
position2 = position.add(line.p2).div(2)
if is_near_list(bridge_positions, position1, avoid_distance):
if is_near_list(bridge_positions, position2,
avoid_distance):
# no valid alternative - we skip this bridge
continue
else:
# position1 is OK
position = position1
# append the original position (ignoring z_plane)
bridge_positions.append(position)
# move the point to z_plane
position = Point(position.x, position.y, z_plane)
bridge_dir = lines[line_index].dir.cross(
polygon.plane.n).normalized().mul(length)
_add_cuboid_to_model(result, position, bridge_dir, height, thickness)
# position2 is OK
position = position2
else:
# position1 is OK
position = position1
# append the original position (ignoring z_plane)
bridge_positions.append(position)
# move the point to z_plane
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
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