- Improved sketching
This commit is contained in:
@@ -292,6 +292,17 @@ class ImprovedSketch(SolverSystem):
|
||||
return circle
|
||||
except Exception as e:
|
||||
raise SolverError(f"Failed to add circle to solver: {e}")
|
||||
|
||||
def remove_entity(self, handle):
|
||||
"""Remove an entity from the solver system"""
|
||||
try:
|
||||
# This is a placeholder implementation
|
||||
# In a real implementation, you would need to properly remove the entity from the solver
|
||||
logger.debug(f"Removing entity with handle: {handle}")
|
||||
# For now, we'll just log the removal - the actual removal is handled in the sketcher
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove entity from solver: {e}")
|
||||
# We don't raise an exception here because the UI removal is more important
|
||||
|
||||
def solve_system(self) -> ResultFlag:
|
||||
"""Solve the constraint system with error handling"""
|
||||
@@ -364,6 +375,7 @@ class ImprovedSketchWidget(QWidget):
|
||||
"""Initialize widget properties"""
|
||||
self.setMouseTracking(True)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.setFocusPolicy(Qt.StrongFocus) # Ensure widget can receive focus
|
||||
|
||||
def _setup_state(self):
|
||||
"""Initialize widget state"""
|
||||
@@ -375,9 +387,12 @@ class ImprovedSketchWidget(QWidget):
|
||||
# Interaction state
|
||||
self.hovered_point: Optional[Point2D] = None
|
||||
self.hovered_line: Optional[Line2D] = None
|
||||
self.hovered_circle: Optional[Circle2D] = None
|
||||
self.hovered_snap_point: Optional[QPoint] = None # For snap point display
|
||||
self.snap_type: Optional[str] = None # Type of snap ("point", "midpoint", etc.)
|
||||
self.selected_elements = []
|
||||
self.selected_elements = [] # List of selected elements for deletion
|
||||
self.selection_rect_start: Optional[QPoint] = None # For rectangle selection
|
||||
self.selection_rect_end: Optional[QPoint] = None # For rectangle selection
|
||||
|
||||
# Dragging state
|
||||
self.dragging_point: Optional[Point2D] = None
|
||||
@@ -415,6 +430,11 @@ class ImprovedSketchWidget(QWidget):
|
||||
self.current_mode = mode
|
||||
self._reset_interaction_state()
|
||||
logger.info(f"Mode changed to: {mode}")
|
||||
|
||||
# Special handling for constraint modes
|
||||
if mode in [SketchMode.DISTANCE, SketchMode.HORIZONTAL, SketchMode.VERTICAL,
|
||||
SketchMode.COINCIDENT_PT_PT, SketchMode.COINCIDENT_PT_LINE]:
|
||||
logger.info(f"Entering constraint mode: {mode}")
|
||||
|
||||
def set_construction_mode(self, enabled: bool):
|
||||
"""Enable or disable construction geometry mode"""
|
||||
@@ -501,6 +521,13 @@ class ImprovedSketchWidget(QWidget):
|
||||
if self.panning and self.pan_start_pos:
|
||||
self._handle_panning(event.pos())
|
||||
return
|
||||
|
||||
# Handle rectangle selection in NONE mode
|
||||
if ((self.current_mode == SketchMode.NONE or self.current_mode is None) and
|
||||
self.selection_rect_start):
|
||||
self.selection_rect_end = local_pos
|
||||
self.update()
|
||||
return
|
||||
|
||||
# Update hover state
|
||||
self._update_hover_state(local_pos)
|
||||
@@ -519,6 +546,14 @@ class ImprovedSketchWidget(QWidget):
|
||||
self._end_point_drag()
|
||||
elif event.button() == Qt.MiddleButton and self.panning:
|
||||
self._end_panning()
|
||||
elif event.button() == Qt.LeftButton:
|
||||
# Handle rectangle selection completion
|
||||
if ((self.current_mode == SketchMode.NONE or self.current_mode is None) and
|
||||
self.selection_rect_start and self.selection_rect_end):
|
||||
self._perform_rectangle_selection()
|
||||
self.selection_rect_start = None
|
||||
self.selection_rect_end = None
|
||||
self.update()
|
||||
self.update()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
@@ -538,6 +573,14 @@ class ImprovedSketchWidget(QWidget):
|
||||
self.pan_offset += delta_local
|
||||
|
||||
self.update()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Handle key press events"""
|
||||
if event.key() in [Qt.Key_Delete, Qt.Key_Backspace]:
|
||||
# Delete selected elements
|
||||
self.delete_selected_elements()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
# Private Helper Methods
|
||||
|
||||
@@ -546,19 +589,40 @@ class ImprovedSketchWidget(QWidget):
|
||||
logger.debug(f"Left click at local pos: {pos}, mode: {self.current_mode}")
|
||||
logger.debug(f"Line buffer state: {[str(p) if p else None for p in self.line_buffer]}")
|
||||
|
||||
# Check if we're starting a drag operation on a point
|
||||
# Handle both None and SketchMode.NONE as drag mode
|
||||
# Handle selection when in NONE mode (default selection behavior)
|
||||
if self.current_mode == SketchMode.NONE or self.current_mode is None:
|
||||
# Check if we clicked on an element for selection
|
||||
point = self.sketch.get_point_near(pos, self.snap_settings.snap_distance)
|
||||
logger.debug(f"Found point near click: {point}")
|
||||
if point:
|
||||
line = self.sketch.get_line_near(pos, 5.0)
|
||||
|
||||
# Check if we're starting a drag operation on a point
|
||||
if point and not self.selection_rect_start:
|
||||
logger.debug(f"Starting drag of point: {point}")
|
||||
self._start_point_drag(point, pos)
|
||||
return
|
||||
else:
|
||||
# No point found to drag in NONE mode - make sure no drawing happens
|
||||
logger.debug("No point found for dragging in NONE mode, ignoring click")
|
||||
return
|
||||
|
||||
# Handle selection if not dragging
|
||||
if not self.dragging_point:
|
||||
if point or line:
|
||||
# Toggle element selection
|
||||
if point:
|
||||
if point in self.selected_elements:
|
||||
self.selected_elements.remove(point)
|
||||
else:
|
||||
self.selected_elements.append(point)
|
||||
elif line:
|
||||
if line in self.selected_elements:
|
||||
self.selected_elements.remove(line)
|
||||
else:
|
||||
self.selected_elements.append(line)
|
||||
self.update()
|
||||
return
|
||||
else:
|
||||
# Start rectangle selection
|
||||
self.selection_rect_start = pos
|
||||
self.selection_rect_end = pos
|
||||
self.update()
|
||||
return
|
||||
|
||||
# Handle drawing modes
|
||||
if self.current_mode == SketchMode.LINE:
|
||||
@@ -578,19 +642,86 @@ class ImprovedSketchWidget(QWidget):
|
||||
elif self.current_mode == SketchMode.DISTANCE:
|
||||
self._handle_distance_constraint(pos)
|
||||
|
||||
def _handle_right_click(self, pos: QPoint):
|
||||
"""Handle right mouse button click (cancels current operation and exits mode)"""
|
||||
logger.debug(f"Right click at pos: {pos}, current mode: {self.current_mode}")
|
||||
def _perform_rectangle_selection(self):
|
||||
"""Select elements within the rectangle"""
|
||||
if not self.selection_rect_start or not self.selection_rect_end:
|
||||
return
|
||||
|
||||
# Get the rectangle bounds
|
||||
min_x = min(self.selection_rect_start.x(), self.selection_rect_end.x())
|
||||
max_x = max(self.selection_rect_start.x(), self.selection_rect_end.x())
|
||||
min_y = min(self.selection_rect_start.y(), self.selection_rect_end.y())
|
||||
max_y = max(self.selection_rect_start.y(), self.selection_rect_end.y())
|
||||
|
||||
# Reset any interaction state (like line buffers, etc.)
|
||||
self._reset_interaction_state()
|
||||
# Select points within the rectangle
|
||||
for point in self.sketch.points:
|
||||
if min_x <= point.x <= max_x and min_y <= point.y <= max_y:
|
||||
if point not in self.selected_elements:
|
||||
self.selected_elements.append(point)
|
||||
|
||||
# Select lines where both endpoints are within the rectangle
|
||||
for line in self.sketch.lines:
|
||||
start_in = (min_x <= line.start.x <= max_x and min_y <= line.start.y <= max_y)
|
||||
end_in = (min_x <= line.end.x <= max_x and min_y <= line.end.y <= max_y)
|
||||
if start_in and end_in:
|
||||
if line not in self.selected_elements:
|
||||
self.selected_elements.append(line)
|
||||
|
||||
def delete_selected_elements(self):
|
||||
"""Delete all currently selected elements"""
|
||||
if not self.selected_elements:
|
||||
return
|
||||
|
||||
# Separate points and lines
|
||||
points_to_delete = [elem for elem in self.selected_elements if isinstance(elem, Point2D)]
|
||||
lines_to_delete = [elem for elem in self.selected_elements if isinstance(elem, Line2D)]
|
||||
|
||||
# Force mode to NONE to enable dragging/selection
|
||||
self.current_mode = SketchMode.NONE
|
||||
logger.info(f"Right-click: Mode reset to NONE")
|
||||
# Delete lines first (to avoid issues with points being used by lines)
|
||||
for line in lines_to_delete:
|
||||
if line in self.sketch.lines:
|
||||
# Remove from solver
|
||||
if line.handle:
|
||||
try:
|
||||
self.sketch.remove_entity(line.handle)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove line from solver: {e}")
|
||||
# Remove from sketch
|
||||
self.sketch.lines.remove(line)
|
||||
|
||||
# Delete points
|
||||
for point in points_to_delete:
|
||||
# Check if point is used by any remaining lines
|
||||
point_in_use = any(
|
||||
line.start == point or line.end == point
|
||||
for line in self.sketch.lines
|
||||
)
|
||||
|
||||
if not point_in_use and point in self.sketch.points:
|
||||
# Remove from solver
|
||||
if point.handle:
|
||||
try:
|
||||
self.sketch.remove_entity(point.handle)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove point from solver: {e}")
|
||||
# Remove from sketch
|
||||
self.sketch.points.remove(point)
|
||||
|
||||
# Clear selection
|
||||
self.selected_elements.clear()
|
||||
|
||||
# Emit signal to inform main app that constraint/drawing mode was canceled
|
||||
self.constraint_applied.emit()
|
||||
# Run solver to update constraints
|
||||
try:
|
||||
result = self.sketch.solve_system()
|
||||
if result == ResultFlag.OKAY:
|
||||
logger.debug("Solver succeeded after element deletion")
|
||||
else:
|
||||
logger.warning(f"Solver failed after element deletion: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Solver error after element deletion: {e}")
|
||||
|
||||
# Emit signals
|
||||
self.sketch_modified.emit()
|
||||
self.update()
|
||||
|
||||
def _handle_line_creation(self, pos: QPoint):
|
||||
"""Handle line creation mode"""
|
||||
@@ -619,6 +750,14 @@ class ImprovedSketchWidget(QWidget):
|
||||
end_point = Point2D(snapped_pos.x(), snapped_pos.y(), self.is_construction_mode)
|
||||
self.sketch.add_point(end_point)
|
||||
|
||||
# Check if the points are identical (would create invalid line)
|
||||
if self.line_buffer[0] == end_point:
|
||||
logger.error("Line start and end points cannot be identical")
|
||||
# Reset the buffer to allow user to try again
|
||||
self.line_buffer = [None, None]
|
||||
self.dynamic_end_point = None
|
||||
return
|
||||
|
||||
# Create line
|
||||
line = Line2D(self.line_buffer[0], end_point, self.is_construction_mode)
|
||||
self.sketch.add_line(line)
|
||||
@@ -812,6 +951,23 @@ class ImprovedSketchWidget(QWidget):
|
||||
logger.warning(f"Distance constraint failed: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply distance constraint: {e}")
|
||||
else:
|
||||
# If no line found, show a message to the user
|
||||
logger.debug("No line found near cursor for distance constraint")
|
||||
|
||||
def _handle_right_click(self, pos: QPoint):
|
||||
"""Handle right mouse button click (cancels current operation and exits mode)"""
|
||||
logger.debug(f"Right click at pos: {pos}, current mode: {self.current_mode}")
|
||||
|
||||
# Reset any interaction state (like line buffers, etc.)
|
||||
self._reset_interaction_state()
|
||||
|
||||
# Force mode to NONE to enable dragging/selection
|
||||
self.current_mode = SketchMode.NONE
|
||||
logger.info(f"Right-click: Mode reset to NONE")
|
||||
|
||||
# Emit signal to inform main app that constraint/drawing mode was canceled
|
||||
self.constraint_applied.emit()
|
||||
|
||||
def _update_hover_state(self, pos: QPoint):
|
||||
"""Update which elements are being hovered and detect snap points"""
|
||||
@@ -904,6 +1060,8 @@ class ImprovedSketchWidget(QWidget):
|
||||
self.constraint_buffer = [None, None]
|
||||
self.dynamic_end_point = None
|
||||
self.selected_elements.clear()
|
||||
self.selection_rect_start = None
|
||||
self.selection_rect_end = None
|
||||
|
||||
def _viewport_to_local(self, viewport_pos: QPoint) -> QPoint:
|
||||
"""Convert viewport coordinates to local sketch coordinates
|
||||
@@ -1009,8 +1167,13 @@ class ImprovedSketchWidget(QWidget):
|
||||
self.render_settings.normal_pen_width / self.zoom_factor)
|
||||
radius = 3 / self.zoom_factor
|
||||
|
||||
# Special highlighting for selected points
|
||||
if point in self.selected_elements:
|
||||
pen = QPen(QColor(255, 255, 0), # Yellow
|
||||
(self.render_settings.highlight_pen_width + 2) / self.zoom_factor)
|
||||
radius *= 1.5 # Make selected points larger
|
||||
# Special highlighting for dragged points
|
||||
if point == self.dragging_point:
|
||||
elif point == self.dragging_point:
|
||||
pen = QPen(QColor(255, 165, 0),
|
||||
(self.render_settings.highlight_pen_width + 1) / self.zoom_factor) # Orange
|
||||
radius *= 1.3 # Make dragged points slightly larger
|
||||
@@ -1020,6 +1183,7 @@ class ImprovedSketchWidget(QWidget):
|
||||
self.render_settings.highlight_pen_width / self.zoom_factor)
|
||||
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.NoBrush if point not in self.selected_elements else QColor(255, 255, 0, 100))
|
||||
painter.drawEllipse(QPointF(point.x, point.y), radius, radius)
|
||||
|
||||
def _draw_line(self, painter: QPainter, line: Line2D):
|
||||
@@ -1032,8 +1196,12 @@ class ImprovedSketchWidget(QWidget):
|
||||
pen = QPen(self.render_settings.normal_color,
|
||||
self.render_settings.normal_pen_width / self.zoom_factor)
|
||||
|
||||
# Special highlighting for selected lines
|
||||
if line in self.selected_elements:
|
||||
pen = QPen(QColor(255, 255, 0), # Yellow
|
||||
(self.render_settings.highlight_pen_width + 2) / self.zoom_factor)
|
||||
# Highlight if hovered
|
||||
if line == self.hovered_line:
|
||||
elif line == self.hovered_line:
|
||||
pen = QPen(self.render_settings.highlight_color,
|
||||
self.render_settings.highlight_pen_width / self.zoom_factor)
|
||||
|
||||
@@ -1091,6 +1259,16 @@ class ImprovedSketchWidget(QWidget):
|
||||
1.0 / self.zoom_factor, Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw selection rectangle
|
||||
if ((self.current_mode == SketchMode.NONE or self.current_mode is None) and
|
||||
self.selection_rect_start and self.selection_rect_end):
|
||||
pen = QPen(QColor(255, 255, 0), 1.0 / self.zoom_factor, Qt.DashLine) # Yellow dashed
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QColor(255, 255, 0, 50)) # Semi-transparent yellow fill
|
||||
|
||||
rect = QRect(self.selection_rect_start, self.selection_rect_end)
|
||||
painter.drawRect(rect)
|
||||
|
||||
# Dynamic line preview
|
||||
if (self.current_mode == SketchMode.LINE and
|
||||
self.line_buffer[0] and
|
||||
|
||||
@@ -282,21 +282,57 @@ class VTKWidget(QtWidgets.QWidget):
|
||||
def render_from_points_direct_with_faces(self, vertices, faces, color=(0.1, 0.2, 0.8), line_width=2, point_size=5):
|
||||
"""Sketch Widget has inverted Y axiis therefore we invert y via scale here until fix"""
|
||||
|
||||
# Handle empty vertices or faces
|
||||
if len(vertices) == 0 or len(faces) == 0:
|
||||
print("Warning: No vertices or faces to render")
|
||||
return
|
||||
|
||||
points = vtk.vtkPoints()
|
||||
|
||||
# Use SetData with numpy array
|
||||
vtk_array = numpy_to_vtk(vertices, deep=True)
|
||||
points.SetData(vtk_array)
|
||||
# Validate vertices shape
|
||||
if vertices.ndim != 2 or vertices.shape[1] != 3:
|
||||
print(f"Warning: Invalid vertex shape {vertices.shape}. Expected Nx3.")
|
||||
return
|
||||
|
||||
# Validate faces shape
|
||||
if faces.ndim != 2 or faces.shape[1] != 3:
|
||||
print(f"Warning: Invalid face shape {faces.shape}. Expected Nx3.")
|
||||
return
|
||||
|
||||
# Use SetData with numpy array - ensure vertices are float32
|
||||
try:
|
||||
vertices_float = np.asarray(vertices, dtype=np.float32)
|
||||
vtk_array = numpy_to_vtk(vertices_float, deep=True)
|
||||
points.SetData(vtk_array)
|
||||
except Exception as e:
|
||||
print(f"Error converting vertices to VTK array: {e}")
|
||||
# Fallback: manually insert points
|
||||
for vertex in vertices:
|
||||
points.InsertNextPoint(vertex[0], vertex[1], vertex[2])
|
||||
|
||||
# Create a vtkCellArray to store the triangles
|
||||
triangles = vtk.vtkCellArray()
|
||||
for face in faces:
|
||||
num_vertices = len(vertices)
|
||||
|
||||
for i, face in enumerate(faces):
|
||||
# Validate face indices
|
||||
if (face[0] >= num_vertices or face[0] < 0 or
|
||||
face[1] >= num_vertices or face[1] < 0 or
|
||||
face[2] >= num_vertices or face[2] < 0):
|
||||
print(f"Warning: Invalid face indices {face} at index {i}. Skipping face.")
|
||||
continue
|
||||
|
||||
triangle = vtk.vtkTriangle()
|
||||
triangle.GetPointIds().SetId(0, face[0])
|
||||
triangle.GetPointIds().SetId(1, face[1])
|
||||
triangle.GetPointIds().SetId(2, face[2])
|
||||
triangle.GetPointIds().SetId(0, int(face[0]))
|
||||
triangle.GetPointIds().SetId(1, int(face[1]))
|
||||
triangle.GetPointIds().SetId(2, int(face[2]))
|
||||
triangles.InsertNextCell(triangle)
|
||||
|
||||
# Check if we have any valid triangles
|
||||
if triangles.GetNumberOfCells() == 0:
|
||||
print("Warning: No valid triangles to render")
|
||||
return
|
||||
|
||||
# Create a polydata object
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(points)
|
||||
@@ -309,7 +345,17 @@ class VTKWidget(QtWidgets.QWidget):
|
||||
normalGenerator.ComputeCellNormalsOn()
|
||||
normalGenerator.Update()
|
||||
|
||||
self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals())
|
||||
# Safely get cell normals, with fallback if they're not available
|
||||
cell_normals = normalGenerator.GetOutput().GetCellData().GetNormals()
|
||||
if cell_normals:
|
||||
try:
|
||||
self.cell_normals = vtk_to_numpy(cell_normals)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not convert cell normals to numpy array: {e}")
|
||||
self.cell_normals = None
|
||||
else:
|
||||
print("Warning: No cell normals available")
|
||||
self.cell_normals = None
|
||||
|
||||
# Create a mapper and actor
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
|
||||
Reference in New Issue
Block a user