Commit f1b0f4a7 authored by sumpfralle's avatar sumpfralle

added extrusion for 2D models


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1053 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 0da11571
Version 0.6 - UNRELEASED
* added extrusion for 2D models
Version 0.5 - 2011-03-28
* Toolpaths:
* added adaptive positioning for DropCutter strategy (improves precision)
......
......@@ -55,7 +55,7 @@ Windows: select Python 2.5 in the following dialog.
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Development Status :: Development Status :: 4 - Beta",
"Development Status :: 4 - Beta",
"License :: OSI Approved :: GNU General Public License (GPL)",
"Topic :: Scientific/Engineering",
"Environment :: Win32 (MS Windows)",
......
......@@ -294,6 +294,28 @@
</row>
</data>
</object>
<object class="GtkListStore" id="ExtrusionTypeModel">
<columns>
<!-- column-name key -->
<column type="gchararray"/>
<!-- column-name label -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">radius_up</col>
<col id="1" translatable="yes">Radius (up)</col>
</row>
<row>
<col id="0" translatable="yes">radius_down</col>
<col id="1" translatable="yes">Radius (down)</col>
</row>
<row>
<col id="0" translatable="yes">skewed</col>
<col id="1" translatable="yes">Chamfer</col>
</row>
</data>
</object>
<object class="GtkWindow" id="ProjectWindow">
<property name="title" translatable="yes">PyCAM</property>
<property name="role">pycam-main</property>
......@@ -1010,11 +1032,13 @@ caused by careless DXF/SVG exporting programs.</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ExtrudeSphereButton">
<property name="label" translatable="yes">Extrude (sphere)</property>
<object class="GtkButton" id="ExtrudeButton">
<property name="label" translatable="yes">Extrude ...</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Extrude the current 2D model into a 3D model.
Spherical and skewed extrusion are supported.</property>
</object>
<packing>
<property name="expand">False</property>
......@@ -6346,7 +6370,7 @@ Everyone else should use the option for M3/M5 (see above).</property>
<object class="GtkLabel" id="GCodeTouchOffLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Touch off and tool length measurement</property>
<property name="use_markup">True</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
......@@ -9528,4 +9552,206 @@ upon interesting bugs and weird results.</property>
<property name="tooltip">Paste a model from clipboard</property>
<property name="stock_id">gtk-paste</property>
</object>
<object class="GtkDialog" id="ExtrusionDialog">
<property name="border_width">5</property>
<property name="title" translatable="yes">Extrude 2D model</property>
<property name="role">pycam-extrusion</property>
<property name="type_hint">normal</property>
<property name="has_separator">False</property>
<child internal-child="vbox">
<object class="GtkVBox" id="dialog-vbox11">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkTable" id="table18">
<property name="visible">True</property>
<property name="n_rows">4</property>
<property name="n_columns">2</property>
<property name="column_spacing">3</property>
<property name="row_spacing">4</property>
<child>
<object class="GtkLabel" id="ExtrusionTypeLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Type:</property>
</object>
<packing>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ExtrusionHeightLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Height:</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ExtrusionWidthLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Width:</property>
</object>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="ExtrusionTypeSelector">
<property name="visible">True</property>
<property name="model">ExtrusionTypeModel</property>
<property name="active">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext9"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="ExtrusionHeight">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">ExtrusionHeightValue</property>
<property name="digits">3</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="ExtrusionWidth">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">ExtrusionWidthValue</property>
<property name="digits">3</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ExtrusionGridLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Accuracy:</property>
</object>
<packing>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="ExtrusionGrid">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">ExtrusionGridValue</property>
<property name="digits">3</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog-action_area10">
<property name="visible">True</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="ExtrusionCancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ExtrusionSubmit">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">ExtrusionCancel</action-widget>
<action-widget response="0">ExtrusionSubmit</action-widget>
</action-widgets>
</object>
<object class="GtkAdjustment" id="ExtrusionHeightValue">
<property name="value">1</property>
<property name="lower">0.001</property>
<property name="upper">1000</property>
<property name="step_increment">0.5</property>
</object>
<object class="GtkAdjustment" id="ExtrusionWidthValue">
<property name="value">1</property>
<property name="upper">1000</property>
<property name="step_increment">0.5</property>
</object>
<object class="GtkAdjustment" id="ExtrusionGridValue">
<property name="value">1</property>
<property name="lower">0.001</property>
<property name="upper">1000</property>
<property name="step_increment">0.1</property>
</object>
</interface>
......@@ -693,7 +693,7 @@ class ContourModel(BaseModel):
else:
return False
def extrude_to_sphere(self, height_factor=1.0, stepping=None, callback=None):
def extrude(self, stepping=None, func=None, callback=None):
""" do a spherical extrusion of a 2D model.
This is mainly useful for extruding text in a visually pleasent way ...
BEWARE: currently not working correctly - see "calculate_point_height" below ...
......@@ -717,8 +717,7 @@ class ContourModel(BaseModel):
if callback and callback():
return None
group = PolygonGroup(poly, children, callback=callback)
new_model = group.extrude_to_sphere(height_factor=height_factor,
stepping=stepping)
new_model = group.extrude(func=func, stepping=stepping)
if new_model:
model += new_model
return model
......@@ -731,58 +730,160 @@ class PolygonGroup(object):
self.inner = inner_list
self.callback = callback
self.lines = outer.get_lines()
self.z_level = self.lines[0].p1.z
for poly in inner_list:
self.lines.extend(poly.get_lines())
def extrude_to_sphere(self, height_factor=1.0, stepping=None):
def extrude(self, func=None, stepping=None):
if stepping is None:
stepping = min(self.outer.maxx - self.outer.minx,
self.outer.maxy - self.outer.miny) / 50
self.outer.maxy - self.outer.miny) / 80
grid = []
for line in self._get_grid_matrix(stepping=stepping):
line_points = []
for x, y in line:
z = self.calculate_point_height(x, y, height_factor)
if z is None:
line_points.append(None)
else:
line_points.append(Point(x, y, z))
if self.callback:
self.callback()
z = self.calculate_point_height(x, y, func)
line_points.append((x, y, z))
if self.callback and self.callback():
return None
grid.append(line_points)
# calculate the triangles within the grid
model = Model()
for line in range(len(grid) - 1):
for row in range(len(grid[0]) - 1):
p1 = grid[line][row]
p2 = grid[line][row + 1]
p3 = grid[line + 1][row]
p4 = grid[line + 1][row + 1]
if p1 and p2 and p3:
model.append(Triangle(p1, p2, p3))
if p2 and p4 and p3:
model.append(Triangle(p2, p4, p3))
# TODO: the connections between the outline and these triangles needs to be done
coords = []
coords.append(grid[line][row])
coords.append(grid[line][row + 1])
coords.append(grid[line + 1][row + 1])
coords.append(grid[line + 1][row])
items = self._fill_grid_positions(coords)
for item in items:
model.append(item)
if self.callback and self.callback():
return None
return model
def _get_closest_line_collision(self, probe_line):
min_dist = None
min_cp = None
for line in self.lines:
cp, dist = probe_line.get_intersection(line)
if cp and ((min_dist is None) or (dist < min_dist)):
min_dist = dist
min_cp = cp
if min_dist > 0:
return min_cp
else:
return None
def _fill_grid_positions(self, coords):
""" Try to find suitable alternatives, if any of the corners of this
square grid is not valid.
The current strategy: find the points of intersection with the contour
on all incomplete edges of the square.
The _good_ strategy would be: crop the square by using all related
lines of the contour.
"""
def get_line(i1, i2):
a = list(coords[i1 % 4])
b = list(coords[i2 % 4])
# the contour points of the model will always be at level zero
a[2] = self.z_level
b[2] = self.z_level
return Line(Point(*a), Point(*b))
valid_indices = [index for index, p in enumerate(coords)
if not p[2] is None]
none_indices = [index for index, p in enumerate(coords) if p[2] is None]
valid_count = len(valid_indices)
final_points = []
if valid_count == 0:
final_points.extend([None, None, None, None])
elif valid_count == 1:
fan_points = []
for index in range(4):
if index in none_indices:
probe_line = get_line(valid_indices[0], index)
cp = self._get_closest_line_collision(probe_line)
if cp:
fan_points.append(cp)
final_points.append(cp)
else:
final_points.append(Point(*coords[index]))
# check if the three fan_points are in line
if len(fan_points) == 3:
fan_points.sort()
if Line(fan_points[0], fan_points[2]).is_point_inside(
fan_points[1]):
final_points.remove(fan_points[1])
elif valid_count == 2:
if sum(valid_indices) % 2 == 0:
# the points are on opposite corners
# The strategy below is not really good, but this special case
# is hardly possible, anyway.
for index in range(4):
if index in valid_indices:
final_points.append(Point(*coords[index]))
else:
probe_line = get_line(index - 1, index)
cp = self._get_closest_line_collision(probe_line)
final_points.append(cp)
else:
for index in range(4):
if index in valid_indices:
final_points.append(Point(*coords[index]))
else:
if ((index + 1) % 4) in valid_indices:
other_index = index + 1
else:
other_index = index - 1
probe_line = get_line(other_index, index)
cp = self._get_closest_line_collision(probe_line)
final_points.append(cp)
elif valid_count == 3:
for index in range(4):
if index in valid_indices:
final_points.append(Point(*coords[index]))
else:
# add two points
for other_index in (index - 1, index + 1):
probe_line = get_line(other_index, index)
cp = self._get_closest_line_collision(probe_line)
final_points.append(cp)
else:
final_points.extend([Point(*coord) for coord in coords])
valid_points = [p for p in final_points if not p is None]
if len(valid_points) < 3:
result = []
elif len(valid_points) == 3:
result = [Triangle(*valid_points)]
else:
# create a simple star-like fan of triangles - not perfect, but ok
result = []
start = valid_points.pop(0)
while len(valid_points) > 1:
p2, p3 = valid_points[0:2]
result.append(Triangle(start, p2, p3))
valid_points.pop(0)
return result
def _get_grid_matrix(self, stepping):
x_dim = self.outer.maxx - self.outer.minx
y_dim = self.outer.maxy - self.outer.miny
x_points_num = int(max(4, math.ceil(x_dim / stepping)))
y_points_num = int(max(4, math.ceil(y_dim / stepping)))
x_step = x_dim / x_points_num
y_step = y_dim / y_points_num
x_step = x_dim / (x_points_num - 1)
y_step = y_dim / (y_points_num - 1)
grid = []
for x_index in range(x_points_num):
line = []
for y_index in range(y_points_num):
x_value = self.outer.minx + x_index * x_step + x_step / 2
y_value = self.outer.miny + y_index * y_step + y_step / 2
x_value = self.outer.minx + x_index * x_step
y_value = self.outer.miny + y_index * y_step
line.append((x_value, y_value))
grid.append(line)
return grid
def calculate_point_height(self, x, y, height_factor):
def calculate_point_height(self, x, y, func):
point = Point(x, y, self.outer.minz)
if not self.outer.is_point_inside(point):
return None
......@@ -793,23 +894,17 @@ class PolygonGroup(object):
line_distances = []
for line in self.lines:
if line.dir.cross(point.sub(line.p1)).z > 0:
# TODO: this is currently somehow broken
close_points = []
close_point = line.closest_point(point)
if not line.is_point_inside(close_point):
continue
dist = line.dist_to_point(point)
direction = close_point.sub(point)
line_distances.append((dist, direction))
line_distances.sort(key=lambda item: item[0])
dist1, vector1 = line_distances.pop(0)
for dist2, vector2 in line_distances:
if vector1.dot(vector2) <= 0:
break
else:
# no suitable line found
return None
radius = (dist1 + dist2) / 2
min_dist = min(dist1, dist2)
angle = math.acos((radius - min_dist) / radius)
return height_factor * math.sin(angle) * radius
close_points.append(line.p1)
close_points.append(line.p2)
else:
close_points.append(close_point)
for p in close_points:
direction = point.sub(p)
dist = direction.norm
line_distances.append(dist)
line_distances.sort()
return self.z_level + func(line_distances[0])
......@@ -405,6 +405,19 @@ class ProjectGui:
self.gui.get_object("ProcessPoolWindowClose").connect("clicked", self.toggle_process_pool_window, False)
self.gui.get_object("ProcessPoolRefreshInterval").set_value(3)
self.process_pool_model = self.gui.get_object("ProcessPoolStatisticsModel")
# extrusion dialog
self._extrusion_dialog_position = None
self._extrusion_dialog_visible = False
self.extrusion_dialog_window = self.gui.get_object("ExtrusionDialog")
self.extrusion_dialog_window.connect("delete-event",
self.toggle_extrusion_dialog, False)
self.gui.get_object("ExtrusionCancel").connect("clicked",
self.toggle_extrusion_dialog, False)
self.gui.get_object("ExtrusionSubmit").connect("clicked",
self.extrude_model)
self.gui.get_object("ExtrusionHeight").set_value(1)
self.gui.get_object("ExtrusionWidth").set_value(1)
self.gui.get_object("ExtrusionGrid").set_value(0.5)
# "font dialog" window
self.font_dialog_window = self.gui.get_object("FontDialog")
self.font_dialog_window.connect("delete-event",
......@@ -575,8 +588,8 @@ class ProjectGui:
100 / 25.4, False)
self.gui.get_object("Projection2D").connect("clicked",
self.projection_2d)
self.gui.get_object("ExtrudeSphereButton").connect("clicked",
self.extrude_sphere)
self.gui.get_object("ExtrudeButton").connect("clicked",
self.toggle_extrusion_dialog, True)
# support grid
support_grid_type_control = self.gui.get_object(
"SupportGridTypesControl")
......@@ -1189,14 +1202,19 @@ class ProjectGui:
def update_model_type_related_controls(self):
is_reversible = (not self.model is None) \
and hasattr(self.model, "reverse_directions")
controls_2d = ("ToggleModelDirectionButton", "DirectionsGuessButton",
# TODO: currently disabled: "ExtrudeSphereButton",
)
controls_2d = ("ToggleModelDirectionButton", "DirectionsGuessButton")
for control in controls_2d:
if is_reversible:
self.gui.get_object(control).show()
else:
self.gui.get_object(control).hide()
is_extrudable = (not self.model is None) \
and hasattr(self.model, "extrude")
extrude_button = self.gui.get_object("ExtrudeButton")
if is_extrudable:
extrude_button.show()
else:
extrude_button.hide()
is_projectable = (not self.model is None) \
and hasattr(self.model, "get_waterline_contour")
if is_projectable:
......@@ -2174,6 +2192,52 @@ class ProjectGui:
# don't close the window - just hide it (for "delete-event")
return True
@progress_activity_guard
@gui_activity_guard
def extrude_model(self, widget=None):
self.update_progress_bar("Calculating extrusion")
extrusion_type_selector = self.gui.get_object("ExtrusionTypeSelector")
type_model = extrusion_type_selector.get_model()
type_active = extrusion_type_selector.get_active()
if type_active >= 0:
type_string = type_model[type_active][0]
height = self.gui.get_object("ExtrusionHeight").get_value()
width = self.gui.get_object("ExtrusionWidth").get_value()
grid_size = self.gui.get_object("ExtrusionGrid").get_value()
if type_string == "radius_up":
func = lambda x: height * math.sqrt((width ** 2 - max(0, width - x) ** 2))
elif type_string == "radius_down":
func = lambda x: height * (1 - math.sqrt((width ** 2 - min(width, x) ** 2)) / width)
elif type_string == "skewed":
func = lambda x: height * min(1, x / width)
else:
log.error("Unknown extrusion type selected: %s" % type_string)
return
self.toggle_extrusion_dialog(False)
model = self.model.extrude(stepping=grid_size, func=func,
callback=self.update_progress_bar)
if model:
self.load_model(model)
else:
self.toggle_extrusion_dialog(True)
def toggle_extrusion_dialog(self, widget=None, event=None, state=None):
if state is None:
# the "delete-event" issues the additional "event" argument
state = event
if state is None:
state = not self._extrusion_dialog_visible
if state:
if self._extrusion_dialog_position:
self.extrusion_dialog_window.move(*self._extrusion_dialog_position)
self.extrusion_dialog_window.show()
else:
self._extrusion_dialog_position = self.extrusion_dialog_window.get_position()
self.extrusion_dialog_window.hide()
self._extrusion_dialog_visible = state
# don't close the window - just hide it (for "delete-event")
return True
@gui_activity_guard
def toggle_preferences_window(self, widget=None, event=None, state=None):
if state is None:
......@@ -2875,14 +2939,6 @@ class ProjectGui:
plane_z = 0
return Plane(Point(0, 0, plane_z), Vector(0, 0, 1))
@progress_activity_guard
@gui_activity_guard
def extrude_sphere(self, widget=None):
self.update_progress_bar("Calculating spherical extrusion")
model = self.model.extrude_to_sphere(callback=self.update_progress_bar)
if model:
self.load_model(model)
@progress_activity_guard
@gui_activity_guard
def projection_2d(self, widget=None):
......
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