- Improved sketching

This commit is contained in:
bklronin
2025-11-16 17:48:05 +01:00
parent 11d053fda4
commit d6044e551a
4 changed files with 1044 additions and 197 deletions

View File

@@ -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

View File

@@ -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()