- Improved sketching
This commit is contained in:
789
main.py
789
main.py
@@ -47,6 +47,10 @@ class ExtrudeDialog(QDialog):
|
||||
self.cut_checkbox = QCheckBox('Perform Cut')
|
||||
self.union_checkbox = QCheckBox('Combine')
|
||||
self.rounded_checkbox = QCheckBox('Round Edges')
|
||||
|
||||
# Connect the "Perform Cut" checkbox to automatically check "Combine"
|
||||
self.cut_checkbox.stateChanged.connect(self.on_cut_checkbox_changed)
|
||||
|
||||
self.seperator = create_hline()
|
||||
|
||||
# OK and Cancel buttons
|
||||
@@ -73,6 +77,11 @@ class ExtrudeDialog(QDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def on_cut_checkbox_changed(self, state):
|
||||
"""Automatically check 'Combine' when 'Perform Cut' is checked"""
|
||||
if state == Qt.Checked:
|
||||
self.union_checkbox.setChecked(True)
|
||||
|
||||
def get_values(self):
|
||||
return self.length_input.value(), self.symmetric_checkbox.isChecked() ,self.invert_checkbox.isChecked(), self.cut_checkbox.isChecked(), self.union_checkbox.isChecked(), self.rounded_checkbox.isChecked()
|
||||
|
||||
@@ -134,6 +143,9 @@ class MainWindow(QMainWindow):
|
||||
self.ui.pb_con_vert.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.VERTICAL))
|
||||
self.ui.pb_con_dist.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.DISTANCE))
|
||||
self.ui.pb_con_mid.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.MIDPOINT))
|
||||
|
||||
# Keep the construction mode button for construction mode only
|
||||
self.ui.pb_enable_construct.clicked.connect(lambda checked: self.sketchWidget.set_construction_mode(checked))
|
||||
|
||||
### Operations
|
||||
self.ui.pb_extrdop.pressed.connect(self.send_extrude)
|
||||
@@ -266,11 +278,15 @@ class MainWindow(QMainWindow):
|
||||
self.sketchWidget.convert_proj_lines(sketch.proj_lines)
|
||||
self.sketchWidget.update()
|
||||
|
||||
# CLear all selections after it has been projected
|
||||
# Clear all selections after it has been projected
|
||||
self.custom_3D_Widget.project_tosketch_points.clear()
|
||||
self.custom_3D_Widget.project_tosketch_lines.clear()
|
||||
self.custom_3D_Widget.clear_actors_projection()
|
||||
self.custom_3D_Widget.clear_actors_normals()
|
||||
|
||||
# Reset sketch widget mode to NONE after projection to prevent line mode engagement
|
||||
self.sketchWidget.set_mode(SketchMode.NONE)
|
||||
self.ui.pb_linetool.setChecked(False)
|
||||
|
||||
def add_sketch_to_compo(self):
|
||||
"""
|
||||
@@ -365,7 +381,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def on_flip_face(self):
|
||||
self.send_command.emit("flip")
|
||||
|
||||
|
||||
def draw_op_complete(self):
|
||||
# safely disable all drawing and constraint modes
|
||||
self.ui.pb_linetool.setChecked(False)
|
||||
@@ -382,7 +398,13 @@ class MainWindow(QMainWindow):
|
||||
self.ui.pb_con_perp.setChecked(False)
|
||||
|
||||
# Reset the sketch widget mode
|
||||
self.sketchWidget.set_mode(None)
|
||||
self.sketchWidget.set_mode(SketchMode.NONE)
|
||||
|
||||
# Reset construction button
|
||||
self.ui.pb_enable_construct.setChecked(False)
|
||||
|
||||
def on_geometry_created(self, geometry):
|
||||
"""Handle geometry creation from the improved sketcher."""
|
||||
|
||||
def on_geometry_created(self, geometry):
|
||||
"""Handle geometry creation from the improved sketcher."""
|
||||
@@ -406,8 +428,14 @@ class MainWindow(QMainWindow):
|
||||
print(f"Constraint applied, staying in mode: {current_mode}")
|
||||
|
||||
def draw_mesh(self):
|
||||
|
||||
name = self.ui.body_list.currentItem().text()
|
||||
current_item = self.ui.body_list.currentItem()
|
||||
if current_item is None:
|
||||
# No item selected, clear the display
|
||||
self.custom_3D_Widget.clear_body_actors()
|
||||
self.custom_3D_Widget.clear_actors_interactor()
|
||||
return
|
||||
|
||||
name = current_item.text()
|
||||
print("selected_for disp", name)
|
||||
|
||||
compo_id = self.get_activated_compo()
|
||||
@@ -438,13 +466,21 @@ class MainWindow(QMainWindow):
|
||||
print("obj_name", item_name)
|
||||
# Check if the 'operation' key exists in the model dictionary
|
||||
|
||||
if 'operation' in self.model and item_name in self.model['operation']:
|
||||
if self.model['operation'][item_name]['id'] == item_name:
|
||||
row = self.ui.body_list.row(name) # Get the row of the current item
|
||||
self.ui.body_list.takeItem(row) # Remove the item from the list widget
|
||||
self.model['operation'].pop(item_name) # Remove the item from the operation dictionary
|
||||
print(f"Removed operation: {item_name}")
|
||||
self.custom_3D_Widget.clear_mesh()
|
||||
compo_id = self.get_activated_compo()
|
||||
sel_compo = self.project.timeline[compo_id]
|
||||
|
||||
if item_name in sel_compo.bodies:
|
||||
row = self.ui.body_list.row(name) # Get the row of the current item
|
||||
self.ui.body_list.takeItem(row) # Remove the item from the list widget
|
||||
del sel_compo.bodies[item_name] # Remove the item from the operation dictionary
|
||||
print(f"Removed operation: {item_name}")
|
||||
# Clear both body actors and interactor actors
|
||||
self.custom_3D_Widget.clear_body_actors()
|
||||
self.custom_3D_Widget.clear_actors_interactor()
|
||||
# Redraw remaining meshes
|
||||
self.draw_mesh()
|
||||
else:
|
||||
print(f"Body '{item_name}' not found in component")
|
||||
|
||||
def send_extrude(self):
|
||||
# Dialog input
|
||||
@@ -488,6 +524,16 @@ class MainWindow(QMainWindow):
|
||||
sketch.normal = normal
|
||||
|
||||
f = sketch.extrude(length, is_symmetric, invert, 0)
|
||||
|
||||
# Apply fillet/rounding if requested
|
||||
if rounded:
|
||||
# Apply a small fillet radius, adjust as needed
|
||||
fillet_radius = min(length * 0.1, 2.0) # 10% of height or 2mm, whichever is smaller
|
||||
try:
|
||||
f = f.fillet(fillet_radius)
|
||||
print(f"Applied fillet with radius {fillet_radius}mm")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not apply fillet: {e}")
|
||||
|
||||
# Create body element and assign known stuff
|
||||
name_op = f"extrd-{name}"
|
||||
@@ -524,53 +570,165 @@ class MainWindow(QMainWindow):
|
||||
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0]
|
||||
self.ui.body_list.setCurrentItem(items)
|
||||
|
||||
# Handle automatic cut operation if requested
|
||||
if cut and len(sel_compo.bodies) > 1:
|
||||
# Find the most recently created body (other than the one we just created)
|
||||
body_names = list(sel_compo.bodies.keys())
|
||||
if len(body_names) > 1:
|
||||
# Get the last body created before this one
|
||||
previous_body_name = body_names[-2] # Second to last item
|
||||
previous_body = sel_compo.bodies[previous_body_name]
|
||||
|
||||
# Perform cut operation: previous_body - new_body (cut the previous body with the new extrusion)
|
||||
try:
|
||||
cut_result = previous_body.sdf_body - body.sdf_body
|
||||
|
||||
# Create a new body entry for the cut result
|
||||
cut_name = f"cut-{previous_body_name}"
|
||||
|
||||
# Create new body for the cut result
|
||||
cut_body = Body()
|
||||
cut_body.id = cut_name
|
||||
cut_body.sdf_body = cut_result
|
||||
cut_body.sketch = previous_body.sketch # Keep the sketch reference
|
||||
|
||||
# Create new interactor for the cut result
|
||||
cut_interactor = Interactor()
|
||||
|
||||
# Copy interactor properties from the previous body as a starting point
|
||||
# This preserves the original interactor information
|
||||
if hasattr(previous_body, 'interactor') and previous_body.interactor:
|
||||
# Preserve the original interactor lines (these define the sketch outline)
|
||||
cut_interactor.lines = previous_body.interactor.lines
|
||||
cut_interactor.invert = previous_body.interactor.invert
|
||||
|
||||
# Determine height from previous body for consistency
|
||||
prev_height = 50.0 # Default height
|
||||
if hasattr(previous_body.interactor, 'edges') and previous_body.interactor.edges:
|
||||
# Try to determine height from existing edges
|
||||
if len(previous_body.interactor.edges) > 0:
|
||||
edge = previous_body.interactor.edges[0]
|
||||
if len(edge) == 2 and len(edge[0]) == 3 and len(edge[1]) == 3:
|
||||
prev_height = abs(edge[1][2] - edge[0][2])
|
||||
|
||||
# Generate new interactor mesh for the cut result using the same parameters
|
||||
if cut_interactor.invert:
|
||||
cut_edges = interactor_mesh.generate_mesh(cut_interactor.lines, 0, -prev_height)
|
||||
else:
|
||||
cut_edges = interactor_mesh.generate_mesh(cut_interactor.lines, 0, prev_height)
|
||||
|
||||
cut_interactor.edges = cut_edges
|
||||
|
||||
# Copy offset vector
|
||||
if hasattr(previous_body.interactor, 'offset_vector'):
|
||||
cut_interactor.offset_vector = previous_body.interactor.offset_vector
|
||||
else:
|
||||
cut_interactor.offset_vector = [0, 0, 0]
|
||||
else:
|
||||
# Fallback: create basic interactor
|
||||
cut_interactor.lines = []
|
||||
cut_interactor.invert = False
|
||||
cut_interactor.edges = []
|
||||
cut_interactor.offset_vector = [0, 0, 0]
|
||||
|
||||
cut_body.interactor = cut_interactor
|
||||
|
||||
# Add cut body to component
|
||||
sel_compo.bodies[cut_name] = cut_body
|
||||
|
||||
# Add to UI
|
||||
self.ui.body_list.addItem(cut_name)
|
||||
cut_items = self.ui.body_list.findItems(cut_name, Qt.MatchExactly)
|
||||
if cut_items:
|
||||
self.ui.body_list.setCurrentItem(cut_items[-1])
|
||||
|
||||
# Load the interactor mesh for the cut result BEFORE cleaning up
|
||||
self.custom_3D_Widget.load_interactor_mesh(cut_edges, cut_interactor.offset_vector)
|
||||
|
||||
# Hide the original bodies that were used in the cut operation
|
||||
# Find and remove the items from the UI list
|
||||
items_to_remove = []
|
||||
for i in range(self.ui.body_list.count()):
|
||||
item = self.ui.body_list.item(i)
|
||||
if item and item.text() in [previous_body_name, name_op]:
|
||||
items_to_remove.append(item)
|
||||
|
||||
# Actually remove items from UI
|
||||
for item in items_to_remove:
|
||||
row = self.ui.body_list.row(item)
|
||||
self.ui.body_list.takeItem(row)
|
||||
|
||||
# Remove the original bodies from the component
|
||||
if previous_body_name in sel_compo.bodies:
|
||||
del sel_compo.bodies[previous_body_name]
|
||||
if name_op in sel_compo.bodies:
|
||||
del sel_compo.bodies[name_op]
|
||||
|
||||
# Clear the VTK widget and redraw with the cut result only
|
||||
self.custom_3D_Widget.clear_body_actors()
|
||||
# Don't clear interactor actors yet, as we want to show the new interactor mesh
|
||||
|
||||
print(f"Performed automatic cut: {previous_body_name} - {name_op} = {cut_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error performing automatic cut operation: {e}")
|
||||
else:
|
||||
print("Not enough bodies for cut operation")
|
||||
elif cut:
|
||||
print("Perform cut was checked, but no other bodies exist to cut with")
|
||||
|
||||
self.draw_mesh()
|
||||
|
||||
def send_cut(self):
|
||||
"""name = self.ui.body_list.currentItem().text()
|
||||
points = self.model['operation'][name]['sdf_object']
|
||||
sel_compo = self.project.timeline[self.get_activated_compo()]
|
||||
points = sel_compo.bodies[].
|
||||
self.list_selected.append(points)"""
|
||||
|
||||
"""Perform a cut operation using SDF difference"""
|
||||
selected = self.ui.body_list.currentItem()
|
||||
if not selected:
|
||||
print("No body selected for cut operation")
|
||||
return
|
||||
|
||||
name = selected.text()
|
||||
|
||||
sel_compo = self.project.timeline[self.get_activated_compo()]
|
||||
# print(sel_compo)
|
||||
body = sel_compo.bodies[name]
|
||||
# print(sketch)
|
||||
self.list_selected.append(body.sdf_body)
|
||||
|
||||
if len(self.list_selected) == 2:
|
||||
f = difference(self.list_selected[0], self.list_selected[1]) # equivalent
|
||||
# Use the SDF3 class's __sub__ operator for difference operation
|
||||
try:
|
||||
# First body is the base, second is the tool to cut with
|
||||
base_body = self.list_selected[0]
|
||||
tool_body = self.list_selected[1]
|
||||
f = base_body - tool_body # This uses the __sub__ operator
|
||||
|
||||
element = {
|
||||
'id': name,
|
||||
'type': 'cut',
|
||||
'sdf_object': f,
|
||||
}
|
||||
# Create body element and assign known stuff
|
||||
name_op = f"cut-{name}"
|
||||
|
||||
# Create body element and assign known stuff
|
||||
name_op = f"cut-{name}"
|
||||
body = Body()
|
||||
body.id = name_op
|
||||
body.sdf_body = f
|
||||
|
||||
body = Body()
|
||||
body.id = name_op
|
||||
body.sdf_body = f
|
||||
## Add to component
|
||||
sel_compo.bodies[name_op] = body
|
||||
|
||||
## Add to component
|
||||
sel_compo.bodies[name_op] = body
|
||||
|
||||
self.ui.body_list.addItem(name_op)
|
||||
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)
|
||||
self.ui.body_list.setCurrentItem(items[-1])
|
||||
self.custom_3D_Widget.clear_body_actors()
|
||||
self.draw_mesh()
|
||||
self.ui.body_list.addItem(name_op)
|
||||
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)
|
||||
if items:
|
||||
self.ui.body_list.setCurrentItem(items[-1])
|
||||
self.custom_3D_Widget.clear_body_actors()
|
||||
self.draw_mesh()
|
||||
|
||||
# Clear the selection list for next operation
|
||||
self.list_selected.clear()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error performing cut operation: {e}")
|
||||
self.list_selected.clear()
|
||||
|
||||
elif len(self.list_selected) > 2:
|
||||
self.list_selected.clear()
|
||||
print("Too many bodies selected. Please select exactly two bodies.")
|
||||
else:
|
||||
print("mindestens 2!")
|
||||
print("Please select two bodies to perform cut operation. Currently selected: ", len(self.list_selected))
|
||||
|
||||
def load_and_render(self, file):
|
||||
self.custom_3D_Widget.load_stl(file)
|
||||
@@ -722,6 +880,94 @@ class Sketch:
|
||||
print("p2", p2)
|
||||
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
|
||||
|
||||
def convert_geometry_for_sdf(self, sketch):
|
||||
"""Convert sketch geometry (points, lines, circles) to SDF polygon points"""
|
||||
import math
|
||||
points_for_sdf = []
|
||||
|
||||
# Keep track of points that are part of circles to avoid adding them as standalone points
|
||||
circle_centers = []
|
||||
|
||||
# Handle circles by converting them to separate polygons
|
||||
circle_polygons = []
|
||||
if hasattr(sketch, 'circles') and sketch.circles:
|
||||
for circle in sketch.circles:
|
||||
if not circle.is_helper:
|
||||
# Convert circle to polygon approximation
|
||||
num_segments = 32 # Number of segments for circle approximation
|
||||
center_x, center_y = circle.center.x, circle.center.y
|
||||
radius = circle.radius
|
||||
|
||||
circle_points_list = []
|
||||
for i in range(num_segments):
|
||||
angle = 2 * math.pi * i / num_segments
|
||||
x = center_x + radius * math.cos(angle)
|
||||
y = center_y + radius * math.sin(angle)
|
||||
circle_points_list.append((x, y))
|
||||
|
||||
# Close the circle by adding the first point at the end
|
||||
if circle_points_list:
|
||||
circle_points_list.append(circle_points_list[0])
|
||||
|
||||
circle_polygons.append(circle_points_list)
|
||||
|
||||
# Keep track of circle centers to avoid adding them as standalone points
|
||||
circle_centers.append(circle.center)
|
||||
|
||||
# Handle lines by creating ordered polygons from connected line segments
|
||||
rectangle_polygons = []
|
||||
if hasattr(sketch, 'lines') and sketch.lines:
|
||||
non_helper_lines = [line for line in sketch.lines if not line.is_helper]
|
||||
if non_helper_lines:
|
||||
# Group lines into separate connected components (separate polygons)
|
||||
grouped_lines = self._group_connected_lines(non_helper_lines)
|
||||
|
||||
# For each group of connected lines, trace the outline
|
||||
for i, group in enumerate(grouped_lines):
|
||||
ordered_points = self._trace_connected_lines(group)
|
||||
rectangle_polygons.append(ordered_points)
|
||||
|
||||
# Combine polygons with proper separation
|
||||
# Each polygon needs to be closed and separated from others
|
||||
all_polygons = rectangle_polygons + circle_polygons
|
||||
|
||||
for i, polygon in enumerate(all_polygons):
|
||||
points_for_sdf.extend(polygon)
|
||||
# Add a small gap between polygons if not the last one
|
||||
if i < len(all_polygons) - 1:
|
||||
# Add a duplicate point to separate polygons
|
||||
if polygon:
|
||||
points_for_sdf.append(polygon[-1])
|
||||
|
||||
# Handle individual points (if any)
|
||||
if hasattr(sketch, 'points') and sketch.points:
|
||||
for point in sketch.points:
|
||||
if hasattr(point, 'is_helper') and not point.is_helper:
|
||||
# Only add standalone points (not already part of lines/circles)
|
||||
is_standalone = True
|
||||
|
||||
# Check if point is part of any line
|
||||
if hasattr(sketch, 'lines'):
|
||||
for line in sketch.lines:
|
||||
if self._points_equal(line.start, point) or self._points_equal(line.end, point):
|
||||
is_standalone = False
|
||||
break
|
||||
|
||||
# Check if point is center of any circle
|
||||
if hasattr(sketch, 'circles'):
|
||||
for circle in sketch.circles:
|
||||
if self._points_equal(circle.center, point):
|
||||
is_standalone = False
|
||||
break
|
||||
|
||||
if is_standalone:
|
||||
points_for_sdf.append((point.x, point.y))
|
||||
|
||||
self.sdf_points = points_for_sdf
|
||||
print(f"Generated SDF points: {len(points_for_sdf)} points")
|
||||
if points_for_sdf:
|
||||
print(f"First point: {points_for_sdf[0]}, Last point: {points_for_sdf[-1]}")
|
||||
|
||||
def convert_points_for_sdf(self, points):
|
||||
points_for_sdf = []
|
||||
for point in points:
|
||||
@@ -736,115 +982,136 @@ class Sketch:
|
||||
|
||||
self.sdf_points = points_for_sdf
|
||||
|
||||
def convert_geometry_for_sdf(self, sketch):
|
||||
"""Convert sketch geometry (points, lines, circles) to SDF polygon points"""
|
||||
import math
|
||||
points_for_sdf = []
|
||||
|
||||
# Handle circles by converting them to polygons
|
||||
if hasattr(sketch, 'circles') and sketch.circles:
|
||||
for circle in sketch.circles:
|
||||
if not circle.is_helper:
|
||||
# Convert circle to polygon approximation
|
||||
num_segments = 32 # Number of segments for circle approximation
|
||||
center_x, center_y = circle.center.x, circle.center.y
|
||||
radius = circle.radius
|
||||
|
||||
# Handle individual points (if any)
|
||||
if hasattr(sketch, 'points') and sketch.points:
|
||||
# Create a set of all points that are already part of lines or circles
|
||||
used_points = set()
|
||||
|
||||
# Add line endpoint points
|
||||
if hasattr(sketch, 'lines'):
|
||||
for line in sketch.lines:
|
||||
used_points.add(line.start)
|
||||
used_points.add(line.end)
|
||||
|
||||
# Add circle points (both center and perimeter points)
|
||||
if hasattr(sketch, 'circles'):
|
||||
for circle in sketch.circles:
|
||||
used_points.add(circle.center)
|
||||
# Add perimeter points (approximated)
|
||||
num_segments = 32
|
||||
for i in range(num_segments):
|
||||
angle = 2 * math.pi * i / num_segments
|
||||
x = center_x + radius * math.cos(angle)
|
||||
y = center_y + radius * math.sin(angle)
|
||||
points_for_sdf.append((x, y))
|
||||
|
||||
# Handle lines by creating ordered polygon from connected line segments
|
||||
if hasattr(sketch, 'lines') and sketch.lines:
|
||||
non_helper_lines = [line for line in sketch.lines if not line.is_helper]
|
||||
if non_helper_lines:
|
||||
# For connected shapes like rectangles, we need to trace the outline
|
||||
# to avoid duplicate corner points
|
||||
ordered_points = self._trace_connected_lines(non_helper_lines)
|
||||
points_for_sdf.extend(ordered_points)
|
||||
|
||||
# Handle individual points (if any)
|
||||
if hasattr(sketch, 'points') and sketch.points:
|
||||
x = circle.center.x + circle.radius * math.cos(angle)
|
||||
y = circle.center.y + circle.radius * math.sin(angle)
|
||||
# We don't add perimeter points to used_points since they're not Point2D objects
|
||||
|
||||
# Add standalone points that are not part of any geometry
|
||||
for point in sketch.points:
|
||||
if hasattr(point, 'is_helper') and not point.is_helper:
|
||||
# Only add standalone points (not already part of lines/circles)
|
||||
is_standalone = True
|
||||
|
||||
# Check if point is part of any line
|
||||
if hasattr(sketch, 'lines'):
|
||||
for line in sketch.lines:
|
||||
if point == line.start or point == line.end:
|
||||
is_standalone = False
|
||||
break
|
||||
|
||||
# Check if point is center of any circle
|
||||
if hasattr(sketch, 'circles'):
|
||||
for circle in sketch.circles:
|
||||
if point == circle.center:
|
||||
is_standalone = False
|
||||
break
|
||||
|
||||
if is_standalone:
|
||||
if point not in used_points:
|
||||
points_for_sdf.append((point.x, point.y))
|
||||
|
||||
self.sdf_points = points_for_sdf
|
||||
print(f"Generated SDF points: {len(points_for_sdf)} points")
|
||||
if points_for_sdf:
|
||||
print(f"First point: {points_for_sdf[0]}, Last point: {points_for_sdf[-1]}")
|
||||
|
||||
def _trace_connected_lines(self, lines):
|
||||
"""Trace connected line segments to create ordered polygon points without duplicates"""
|
||||
|
||||
def _group_connected_lines(self, lines):
|
||||
"""Group lines into connected components (separate polygons)"""
|
||||
if not lines:
|
||||
return []
|
||||
|
||||
# Start with the first line
|
||||
current_line = lines[0]
|
||||
# Keep Y coordinate as-is to match sketcher orientation
|
||||
ordered_points = [(current_line.start.x, current_line.start.y)]
|
||||
used_lines = {current_line}
|
||||
current_point = current_line.end
|
||||
|
||||
while len(used_lines) < len(lines):
|
||||
# Add the current transition point keeping Y coordinate as-is
|
||||
point_tuple = (current_point.x, current_point.y)
|
||||
if not ordered_points or ordered_points[-1] != point_tuple:
|
||||
ordered_points.append(point_tuple)
|
||||
|
||||
# Find the next connected line
|
||||
next_line = None
|
||||
for line in lines:
|
||||
if line in used_lines:
|
||||
continue
|
||||
|
||||
# Check if this line connects to current_point
|
||||
if self._points_equal(line.start, current_point):
|
||||
next_line = line
|
||||
current_point = line.end
|
||||
break
|
||||
elif self._points_equal(line.end, current_point):
|
||||
next_line = line
|
||||
current_point = line.start
|
||||
break
|
||||
|
||||
if next_line is None:
|
||||
# No more connected lines, might be separate line segments
|
||||
break
|
||||
|
||||
used_lines.add(next_line)
|
||||
groups = []
|
||||
used_lines = set()
|
||||
|
||||
# If we didn't use all lines, add remaining line endpoints keeping Y coordinate as-is
|
||||
for line in lines:
|
||||
if line not in used_lines:
|
||||
start_point = (line.start.x, line.start.y)
|
||||
end_point = (line.end.x, line.end.y)
|
||||
if start_point not in ordered_points:
|
||||
ordered_points.append(start_point)
|
||||
if end_point not in ordered_points:
|
||||
ordered_points.append(end_point)
|
||||
# Start a new group with this line
|
||||
group = [line]
|
||||
used_lines.add(line)
|
||||
current_group_lines = {line}
|
||||
|
||||
# Find all connected lines
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for other_line in lines:
|
||||
if other_line not in used_lines:
|
||||
# Check if this line connects to any line in the current group
|
||||
for group_line in current_group_lines:
|
||||
if (self._points_equal(group_line.start, other_line.start) or
|
||||
self._points_equal(group_line.start, other_line.end) or
|
||||
self._points_equal(group_line.end, other_line.start) or
|
||||
self._points_equal(group_line.end, other_line.end)):
|
||||
group.append(other_line)
|
||||
used_lines.add(other_line)
|
||||
current_group_lines.add(other_line)
|
||||
changed = True
|
||||
break
|
||||
|
||||
groups.append(group)
|
||||
|
||||
return ordered_points
|
||||
return groups
|
||||
|
||||
def _trace_connected_lines(self, lines):
|
||||
"""Trace connected line segments to create ordered polygon points without duplicates.
|
||||
Groups lines into separate connected components and closes each polygon individually."""
|
||||
if not lines:
|
||||
return []
|
||||
|
||||
# Group lines into separate connected components (separate polygons)
|
||||
grouped_lines = self._group_connected_lines(lines)
|
||||
|
||||
# For each group of connected lines, trace the outline and close the polygon
|
||||
all_ordered_points = []
|
||||
for group in grouped_lines:
|
||||
if not group:
|
||||
continue
|
||||
|
||||
# Start with the first line in the group
|
||||
current_line = group[0]
|
||||
# Keep Y coordinate as-is to match sketcher orientation
|
||||
ordered_points = [(current_line.start.x, current_line.start.y)]
|
||||
used_lines = {current_line}
|
||||
current_point = current_line.end
|
||||
|
||||
while len(used_lines) < len(group):
|
||||
# Add the current transition point keeping Y coordinate as-is
|
||||
point_tuple = (current_point.x, current_point.y)
|
||||
if not ordered_points or ordered_points[-1] != point_tuple:
|
||||
ordered_points.append(point_tuple)
|
||||
|
||||
# Find the next connected line
|
||||
next_line = None
|
||||
for line in group:
|
||||
if line in used_lines:
|
||||
continue
|
||||
|
||||
# Check if this line connects to current_point
|
||||
if self._points_equal(line.start, current_point):
|
||||
next_line = line
|
||||
current_point = line.end
|
||||
break
|
||||
elif self._points_equal(line.end, current_point):
|
||||
next_line = line
|
||||
current_point = line.start
|
||||
break
|
||||
|
||||
if next_line is None:
|
||||
# No more connected lines in this group
|
||||
break
|
||||
|
||||
used_lines.add(next_line)
|
||||
|
||||
# Close this individual polygon if not already closed
|
||||
if len(ordered_points) > 2:
|
||||
first_point = ordered_points[0]
|
||||
last_point = ordered_points[-1]
|
||||
# Check if first and last points are the same (closed)
|
||||
if abs(first_point[0] - last_point[0]) > 1e-6 or abs(first_point[1] - last_point[1]) > 1e-6:
|
||||
# Not closed, add the first point at the end to close it
|
||||
ordered_points.append(first_point)
|
||||
|
||||
# Add this polygon's points to the overall list
|
||||
all_ordered_points.extend(ordered_points)
|
||||
|
||||
return all_ordered_points
|
||||
|
||||
def _points_equal(self, p1, p2, tolerance=1e-6):
|
||||
"""Check if two points are equal within tolerance"""
|
||||
@@ -863,13 +1130,65 @@ class Sketch:
|
||||
"""
|
||||
Extrude a 2D shape into 3D, orient it along the normal, and position it relative to the centroid.
|
||||
"""
|
||||
|
||||
# Handle zero height case
|
||||
if height <= 0:
|
||||
print("Warning: Extrude height must be positive. Using default height of 1.0")
|
||||
height = 1.0
|
||||
|
||||
# Normalize the normal vector
|
||||
normal = np.array(self.normal)
|
||||
normal = normal / np.linalg.norm(self.normal)
|
||||
# Normalize the normal vector, with fallback to default if invalid
|
||||
try:
|
||||
normal = np.array(self.normal, dtype=float)
|
||||
norm = np.linalg.norm(normal)
|
||||
if norm > 1e-10: # Check if normal is not zero
|
||||
normal = normal / norm
|
||||
else:
|
||||
print("Warning: Invalid normal vector. Using default Z-axis normal.")
|
||||
normal = np.array([0, 0, 1])
|
||||
except Exception as e:
|
||||
print(f"Warning: Error processing normal vector: {e}. Using default Z-axis normal.")
|
||||
normal = np.array([0, 0, 1])
|
||||
|
||||
# Create the 2D shape
|
||||
f = polygon(self.sdf_points)
|
||||
try:
|
||||
# Handle multiple separate polygons by creating union of shapes
|
||||
if hasattr(self, 'sdf_points') and self.sdf_points:
|
||||
# Split points into separate polygons (closed shapes)
|
||||
polygons = self._split_into_polygons(self.sdf_points)
|
||||
|
||||
if len(polygons) == 1:
|
||||
# Single polygon case
|
||||
f = polygon(polygons[0])
|
||||
elif len(polygons) > 1:
|
||||
# Multiple polygons case - create union
|
||||
shapes = []
|
||||
for poly_points in polygons:
|
||||
if len(poly_points) >= 3: # Need at least 3 points for a valid polygon
|
||||
shape = polygon(poly_points)
|
||||
shapes.append(shape)
|
||||
|
||||
# Union all shapes together
|
||||
if shapes:
|
||||
f = shapes[0]
|
||||
for shape in shapes[1:]:
|
||||
f = f | shape # Union operation
|
||||
else:
|
||||
# Fallback to a simple rectangle if no valid polygons
|
||||
fallback_points = [(-10, -10), (10, -10), (10, 10), (-10, 10)]
|
||||
f = polygon(fallback_points)
|
||||
else:
|
||||
# No valid polygons, fallback to a simple rectangle
|
||||
fallback_points = [(-10, -10), (10, -10), (10, 10), (-10, 10)]
|
||||
f = polygon(fallback_points)
|
||||
else:
|
||||
# Fallback to a simple rectangle if no points
|
||||
fallback_points = [(-10, -10), (10, -10), (10, 10), (-10, 10)]
|
||||
f = polygon(fallback_points)
|
||||
except Exception as e:
|
||||
print(f"Error creating polygon: {e}")
|
||||
# Fallback to a simple rectangle if points are invalid
|
||||
fallback_points = [(-10, -10), (10, -10), (10, 10), (-10, 10)]
|
||||
f = polygon(fallback_points)
|
||||
|
||||
# Extrude the shape along the Z-axis
|
||||
f = f.extrude(height)
|
||||
@@ -877,34 +1196,174 @@ class Sketch:
|
||||
# Center the shape along its extrusion axis
|
||||
f = f.translate((0, 0, height / 2))
|
||||
|
||||
# Orient the shape along the normal vector
|
||||
f = f.orient(normal)
|
||||
# Orient the shape along the normal vector (only if normal is not the default Z-axis)
|
||||
default_z = np.array([0, 0, 1])
|
||||
if not np.allclose(normal, default_z, atol=1e-10):
|
||||
try:
|
||||
f = f.orient(normal)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to orient shape: {e}. Shape will remain in default orientation.")
|
||||
|
||||
offset_vector = self.vector_to_centroid(None, self.origin, normal)
|
||||
# Adjust the offset vector by subtracting the inset distance along the normal direction
|
||||
adjusted_offset = offset_vector - (normal * height)
|
||||
# Calculate offset vector
|
||||
try:
|
||||
offset_vector = self.vector_to_centroid(None, self.origin, normal)
|
||||
except Exception as e:
|
||||
print(f"Warning: Error calculating offset vector: {e}. Using zero offset.")
|
||||
offset_vector = np.array([0, 0, 0])
|
||||
|
||||
# Apply translation based on invert flag
|
||||
if invert:
|
||||
# Translate the shape along the adjusted offset vector
|
||||
# Adjust the offset vector by subtracting the extrusion height along the normal direction
|
||||
adjusted_offset = offset_vector - (normal * height)
|
||||
f = f.translate(adjusted_offset)
|
||||
else:
|
||||
f = f.translate(offset_vector)
|
||||
|
||||
# If offset_length is provided, adjust the offset_vector
|
||||
if offset_length is not None:
|
||||
# Check if offset_vector is not a zero vector
|
||||
offset_vector_magnitude = np.linalg.norm(offset_vector)
|
||||
if offset_vector_magnitude > 1e-10: # Use a small threshold to avoid floating-point issues
|
||||
# Normalize the offset vector
|
||||
offset_vector_norm = offset_vector / offset_vector_magnitude
|
||||
# Scale the normalized vector by the desired length
|
||||
offset_vector = offset_vector_norm * offset_length
|
||||
f = f.translate(offset_vector)
|
||||
else:
|
||||
print("Warning: Offset vector has zero magnitude. Using original vector.")
|
||||
|
||||
# Translate the shape along the adjusted offset vector
|
||||
try:
|
||||
# Check if offset_vector is not a zero vector
|
||||
offset_vector_magnitude = np.linalg.norm(offset_vector)
|
||||
if offset_vector_magnitude > 1e-10: # Use a small threshold to avoid floating-point issues
|
||||
# Normalize the offset vector
|
||||
offset_vector_norm = offset_vector / offset_vector_magnitude
|
||||
# Scale the normalized vector by the desired length
|
||||
scaled_offset = offset_vector_norm * offset_length
|
||||
f = f.translate(scaled_offset)
|
||||
else:
|
||||
print("Warning: Offset vector has zero magnitude. Using original vector.")
|
||||
except Exception as e:
|
||||
print(f"Warning: Error applying offset length: {e}")
|
||||
|
||||
return f
|
||||
|
||||
def _split_into_polygons(self, points):
|
||||
"""Split a list of points into separate closed polygons"""
|
||||
if not points:
|
||||
return []
|
||||
|
||||
polygons = []
|
||||
current_polygon = []
|
||||
|
||||
for point in points:
|
||||
current_polygon.append(point)
|
||||
|
||||
# Check if this point closes the current polygon
|
||||
# A polygon is closed when the last point equals the first point
|
||||
if len(current_polygon) > 2 and current_polygon[0] == current_polygon[-1]:
|
||||
# This polygon is closed, add it to the list
|
||||
polygons.append(current_polygon)
|
||||
current_polygon = []
|
||||
|
||||
# If there's an incomplete polygon left, add it anyway
|
||||
if current_polygon:
|
||||
polygons.append(current_polygon)
|
||||
|
||||
return polygons
|
||||
|
||||
def _trace_connected_lines(self, lines):
|
||||
"""Trace connected line segments to create ordered polygon points without duplicates"""
|
||||
if not lines:
|
||||
return []
|
||||
|
||||
# Group lines into separate connected components (separate polygons)
|
||||
grouped_lines = self._group_connected_lines(lines)
|
||||
|
||||
# For each group of connected lines, trace the outline and close the polygon
|
||||
all_ordered_points = []
|
||||
for group in grouped_lines:
|
||||
if not group:
|
||||
continue
|
||||
|
||||
# Start with the first line in the group
|
||||
current_line = group[0]
|
||||
# Keep Y coordinate as-is to match sketcher orientation
|
||||
ordered_points = [(current_line.start.x, current_line.start.y)]
|
||||
used_lines = {current_line}
|
||||
current_point = current_line.end
|
||||
|
||||
while len(used_lines) < len(group):
|
||||
# Add the current transition point keeping Y coordinate as-is
|
||||
point_tuple = (current_point.x, current_point.y)
|
||||
if not ordered_points or ordered_points[-1] != point_tuple:
|
||||
ordered_points.append(point_tuple)
|
||||
|
||||
# Find the next connected line
|
||||
next_line = None
|
||||
for line in group:
|
||||
if line in used_lines:
|
||||
continue
|
||||
|
||||
# Check if this line connects to current_point
|
||||
if self._points_equal(line.start, current_point):
|
||||
next_line = line
|
||||
current_point = line.end
|
||||
break
|
||||
elif self._points_equal(line.end, current_point):
|
||||
next_line = line
|
||||
current_point = line.start
|
||||
break
|
||||
|
||||
if next_line is None:
|
||||
# No more connected lines in this group
|
||||
break
|
||||
|
||||
used_lines.add(next_line)
|
||||
|
||||
# Close this individual polygon if not already closed
|
||||
if len(ordered_points) > 2:
|
||||
first_point = ordered_points[0]
|
||||
last_point = ordered_points[-1]
|
||||
# Check if first and last points are the same (closed)
|
||||
if abs(first_point[0] - last_point[0]) > 1e-6 or abs(first_point[1] - last_point[1]) > 1e-6:
|
||||
# Not closed, add the first point at the end to close it
|
||||
ordered_points.append(first_point)
|
||||
|
||||
# Add this polygon's points to the overall list
|
||||
all_ordered_points.extend(ordered_points)
|
||||
|
||||
return all_ordered_points
|
||||
|
||||
def _group_connected_lines(self, lines):
|
||||
"""Group lines into connected components (separate polygons)"""
|
||||
if not lines:
|
||||
return []
|
||||
|
||||
groups = []
|
||||
used_lines = set()
|
||||
|
||||
for line in lines:
|
||||
if line not in used_lines:
|
||||
# Start a new group with this line
|
||||
group = [line]
|
||||
used_lines.add(line)
|
||||
current_group_lines = {line}
|
||||
|
||||
# Find all connected lines
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for other_line in lines:
|
||||
if other_line not in used_lines:
|
||||
# Check if this line connects to any line in the current group
|
||||
for group_line in current_group_lines:
|
||||
if (self._points_equal(group_line.start, other_line.start) or
|
||||
self._points_equal(group_line.start, other_line.end) or
|
||||
self._points_equal(group_line.end, other_line.start) or
|
||||
self._points_equal(group_line.end, other_line.end)):
|
||||
group.append(other_line)
|
||||
used_lines.add(other_line)
|
||||
current_group_lines.add(other_line)
|
||||
changed = True
|
||||
break
|
||||
|
||||
groups.append(group)
|
||||
|
||||
return groups
|
||||
|
||||
def _points_equal(self, p1, p2, tolerance=1e-6):
|
||||
"""Check if two points are equal within tolerance"""
|
||||
return abs(p1.x - p2.x) < tolerance and abs(p1.y - p2.y) < tolerance
|
||||
|
||||
@dataclass
|
||||
class Interactor:
|
||||
@@ -933,7 +1392,19 @@ class Interactor:
|
||||
center_to_centroid = np.array(centroid) - np.array(shape_center)
|
||||
|
||||
# Project this vector onto the normal to get the required translation along the normal
|
||||
translation_along_normal = np.dot(center_to_centroid, normal) * normal
|
||||
# Handle case where normal might be invalid or zero
|
||||
try:
|
||||
normal_array = np.array(normal)
|
||||
if np.linalg.norm(normal_array) > 1e-10: # Check if normal is not zero
|
||||
translation_along_normal = np.dot(center_to_centroid, normal_array) * normal_array
|
||||
else:
|
||||
# Use default Z-axis if normal is zero
|
||||
default_normal = np.array([0, 0, 1])
|
||||
translation_along_normal = np.dot(center_to_centroid, default_normal) * default_normal
|
||||
except Exception as e:
|
||||
print(f"Warning: Error in normal computation: {e}. Using default Z-axis.")
|
||||
default_normal = np.array([0, 0, 1])
|
||||
translation_along_normal = np.dot(center_to_centroid, default_normal) * default_normal
|
||||
|
||||
return translation_along_normal
|
||||
|
||||
|
||||
Reference in New Issue
Block a user