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,18 +163,113 @@ def get_support_distributed(model, z_plane, average_distance, ...@@ -166,18 +163,113 @@ 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
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() lines = polygon.get_lines()
poly_lengths = polygon.get_lengths() poly_lengths = polygon.get_lengths()
num_of_bridges = max(min_bridges_per_polygon, 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))) int(round(sum(poly_lengths) / average_distance)))
real_average_distance = sum(poly_lengths) / num_of_bridges real_average_distance = sum(poly_lengths) / num_of_bridges
max_line_index = poly_lengths.index(max(poly_lengths)) max_line_index = poly_lengths.index(max(poly_lengths))
...@@ -197,6 +289,8 @@ def get_support_distributed(model, z_plane, average_distance, ...@@ -197,6 +289,8 @@ def get_support_distributed(model, z_plane, average_distance,
positions.append(current_line_index) positions.append(current_line_index)
distance_processed += poly_lengths[current_line_index] distance_processed += poly_lengths[current_line_index]
distance_processed %= real_average_distance distance_processed %= real_average_distance
result = []
bridge_positions = []
for line_index in positions: for line_index in positions:
position = polygon.get_middle_of_line(line_index) position = polygon.get_middle_of_line(line_index)
# skip bridges that are close to another existing bridge # skip bridges that are close to another existing bridge
...@@ -221,7 +315,7 @@ def get_support_distributed(model, z_plane, average_distance, ...@@ -221,7 +315,7 @@ def get_support_distributed(model, z_plane, average_distance,
# move the point to z_plane # move the point to z_plane
position = Point(position.x, position.y, z_plane) position = Point(position.x, position.y, z_plane)
bridge_dir = lines[line_index].dir.cross( bridge_dir = lines[line_index].dir.cross(
polygon.plane.n).normalized().mul(length) polygon.plane.n).normalized()
_add_cuboid_to_model(result, position, bridge_dir, height, thickness) 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