Fix sketcher mode handling to prevent unintended line creation during drag operations

Major changes:
- Fixed right-click handler to directly set mode to NONE instead of relying on main app signal handling
- Added safety checks in left-click handler to prevent drawing when no draggable point is found in NONE mode
- Enhanced mode compatibility by treating Python None as SketchMode.NONE in set_mode() method
- Added comprehensive debug logging for mode changes and interaction state tracking
- Resolved integration issue where persistent constraint modes were prematurely reset by main app
- Ensured point dragging is only enabled in NONE mode, preventing accidental polyline creation

This fixes the reported issue where deactivating the line tool would still create lines when dragging,
and ensures proper mode transitions between drawing tools and selection/drag mode.
This commit is contained in:
bklronin
2025-08-16 22:30:18 +02:00
parent 54261bb8fd
commit 11d053fda4
886 changed files with 168708 additions and 51 deletions

236
main.py
View File

@@ -11,7 +11,7 @@ from Gui import Ui_fluencyCAD # Import the generated GUI module
from drawing_modules.vtk_widget import VTKWidget
import numpy as np
from drawing_modules.draw_widget_solve import SketchWidget
from drawing_modules.improved_sketcher import ImprovedSketchWidget, SketchMode, SnapMode
from sdf import *
from python_solvespace import SolverSystem, ResultFlag
from mesh_modules import simple_mesh, vesta_mesh, interactor_mesh
@@ -93,7 +93,7 @@ class MainWindow(QMainWindow):
size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.custom_3D_Widget.setSizePolicy(size_policy)
self.sketchWidget = SketchWidget()
self.sketchWidget = ImprovedSketchWidget()
layout2 = self.ui.sketch_tab.layout() # Get the layout of self.ui.gl_canvas
layout2.addWidget(self.sketchWidget)
size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
@@ -120,36 +120,44 @@ class MainWindow(QMainWindow):
self.ui.pb_flip_face.pressed.connect(self.on_flip_face)
###Modes
self.ui.pb_linetool.clicked.connect(self.sketchWidget.act_line_mode)
self.ui.pb_con_ptpt.clicked.connect(self.sketchWidget.act_constrain_pt_pt_mode)
self.ui.pb_con_line.clicked.connect(self.sketchWidget.act_constrain_pt_line_mode)
self.ui.pb_con_horiz.clicked.connect(self.sketchWidget.act_constrain_horiz_line_mode)
self.ui.pb_con_vert.clicked.connect(self.sketchWidget.act_constrain_vert_line_mode)
self.ui.pb_con_dist.clicked.connect(self.sketchWidget.act_constrain_distance_mode)
self.ui.pb_con_mid.clicked.connect(self.sketchWidget.act_constrain_mid_point_mode)
###Modes - Updated for improved sketcher
# Add a selection/drag mode button if available in UI, otherwise use existing button
# For now, we'll assume there might be a selection button - adapt as needed
# self.ui.pb_select.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.NONE))
self.ui.pb_linetool.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.LINE))
self.ui.pb_rectool.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.RECTANGLE))
self.ui.pb_circtool.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.CIRCLE))
self.ui.pb_con_ptpt.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.COINCIDENT_PT_PT))
self.ui.pb_con_line.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.COINCIDENT_PT_LINE))
self.ui.pb_con_horiz.clicked.connect(lambda: self.sketchWidget.set_mode(SketchMode.HORIZONTAL))
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))
### Operations
self.ui.pb_extrdop.pressed.connect(self.send_extrude)
self.ui.pb_cutop.pressed.connect(self.send_cut)
self.ui.pb_del_body.pressed.connect(self.del_body)
self.sketchWidget.constrain_done.connect(self.draw_op_complete)
# Connect new sketcher signals
self.sketchWidget.constraint_applied.connect(self.on_constraint_applied)
self.sketchWidget.geometry_created.connect(self.on_geometry_created)
self.sketchWidget.sketch_modified.connect(self.on_sketch_modified)
self.setFocusPolicy(Qt.StrongFocus)
self.send_command.connect(self.custom_3D_Widget.on_receive_command)
self.ui.actionNew_Project.triggered.connect(self.new_project)
self.ui.pb_enable_construct.clicked.connect(self.sketchWidget.on_construct_change)
self.ui.pb_enable_construct.clicked.connect(lambda checked: self.sketchWidget.set_construction_mode(checked))
self.project = Project()
self.new_project()
### SNAPS
self.ui.pb_snap_midp.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("mpoint", checked))
self.ui.pb_snap_horiz.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("horiz", checked))
self.ui.pb_snap_vert.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("vert", checked))
self.ui.pb_snap_angle.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("angle", checked))
self.ui.pb_enable_snap.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("point", checked))
self.ui.pb_snap_midp.toggled.connect(lambda checked: self.sketchWidget.set_snap_mode(SnapMode.MIDPOINT, checked))
self.ui.pb_snap_horiz.toggled.connect(lambda checked: self.sketchWidget.set_snap_mode(SnapMode.HORIZONTAL, checked))
self.ui.pb_snap_vert.toggled.connect(lambda checked: self.sketchWidget.set_snap_mode(SnapMode.VERTICAL, checked))
self.ui.pb_snap_angle.toggled.connect(lambda checked: self.sketchWidget.set_snap_mode(SnapMode.ANGLE, checked))
self.ui.pb_enable_snap.toggled.connect(lambda checked: self.sketchWidget.set_snap_mode(SnapMode.POINT, checked))
### COMPOS
### COMPOS
@@ -275,10 +283,8 @@ class MainWindow(QMainWindow):
#Save original for editing later
sketch.original_sketch = sketch_from_widget
#Get parameters
points = [point for point in sketch_from_widget.points if hasattr(point, 'is_helper') and not point.is_helper]
sketch.convert_points_for_sdf(points)
# Use the new geometry conversion method that handles circles, lines, and points
sketch.convert_geometry_for_sdf(sketch_from_widget)
sketch.id = sketch_from_widget.id
sketch.filter_lines_for_interactor(sketch_from_widget.lines)
@@ -294,7 +300,7 @@ class MainWindow(QMainWindow):
# Deactivate drawing
self.ui.pb_linetool.setChecked(False)
self.sketchWidget.line_mode = False
self.sketchWidget.set_mode(None)
items = self.ui.sketch_list.findItems(sketch.id, Qt.MatchExactly)[0]
self.ui.sketch_list.setCurrentItem(items)
@@ -361,16 +367,43 @@ class MainWindow(QMainWindow):
self.send_command.emit("flip")
def draw_op_complete(self):
# safely disable the line modes
# safely disable all drawing and constraint modes
self.ui.pb_linetool.setChecked(False)
self.ui.pb_rectool.setChecked(False)
self.ui.pb_circtool.setChecked(False)
# Disable all constraint buttons
self.ui.pb_con_ptpt.setChecked(False)
self.ui.pb_con_line.setChecked(False)
self.ui.pb_con_horiz.setChecked(False)
self.ui.pb_con_vert.setChecked(False)
self.ui.pb_con_dist.setChecked(False)
self.ui.pb_con_mid.setChecked(False)
self.ui.pb_con_perp.setChecked(False)
self.sketchWidget.mouse_mode = None
self.sketchWidget.reset_buffers()
# Reset the sketch widget mode
self.sketchWidget.set_mode(None)
def on_geometry_created(self, geometry):
"""Handle geometry creation from the improved sketcher."""
print(f"Geometry created: {geometry}")
def on_sketch_modified(self):
"""Handle sketch modifications from the improved sketcher."""
print("Sketch modified")
def on_constraint_applied(self):
"""Handle constraint application - only reset UI if exiting constraint mode."""
# Only call draw_op_complete if we're actually exiting constraint mode
# This allows persistent constraint behavior - constraints stay active until right-click
current_mode = self.sketchWidget.current_mode
# Only reset if we're going back to NONE mode (right-click exit)
if current_mode == SketchMode.NONE:
self.draw_op_complete()
else:
# We're still in a constraint mode, don't reset the UI buttons
print(f"Constraint applied, staying in mode: {current_mode}")
def draw_mesh(self):
@@ -428,10 +461,8 @@ class MainWindow(QMainWindow):
#print(sketch)
points = sketch.sdf_points
# detect loop that causes problems in mesh generation
if points[-1] == points[0]:
print("overlap")
del points[-1]
# Note: Closed polygons should have first == last point for proper SDF generation
# No need to remove 'overlapping' points as they're intentionally closed
dialog = ExtrudeDialog(self)
if dialog.exec():
@@ -694,11 +725,130 @@ class Sketch:
def convert_points_for_sdf(self, points):
points_for_sdf = []
for point in points:
if point.is_helper is False:
if hasattr(point, 'is_helper') and point.is_helper is False:
print("point", point)
points_for_sdf.append(self.translate_points_tup(point.ui_point))
# Handle improved sketcher Point2D objects
if hasattr(point, 'x') and hasattr(point, 'y'):
points_for_sdf.append((point.x, point.y))
else:
# Fallback for old-style point objects
points_for_sdf.append(self.translate_points_tup(point.ui_point))
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
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:
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:
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"""
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)
# 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)
return ordered_points
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
def filter_lines_for_interactor(self, lines):
### Filter lines that are not meant to be drawn for the interactor like contruction lines
@@ -792,16 +942,22 @@ class Interactor:
Translates coordinates."""
points_for_interact = []
for point_to_poly in input_lines:
from_coord_start = window.sketchWidget.from_quadrant_coords_no_center(point_to_poly.crd1.ui_point)
from_coord_end = window.sketchWidget.from_quadrant_coords_no_center(point_to_poly.crd2.ui_point)
start_draw = self.translate_points_tup(from_coord_start)
end_draw = self.translate_points_tup(from_coord_end)
line = start_draw, end_draw
points_for_interact.append(line)
for line in input_lines:
# Handle improved sketcher Line2D objects with start/end Point2D objects
if hasattr(line, 'start') and hasattr(line, 'end'):
start_point = (line.start.x, line.start.y)
end_point = (line.end.x, line.end.y)
else:
# Fallback for old-style line objects
from_coord_start = window.sketchWidget.from_quadrant_coords_no_center(line.crd1.ui_point)
from_coord_end = window.sketchWidget.from_quadrant_coords_no_center(line.crd2.ui_point)
start_point = self.translate_points_tup(from_coord_start)
end_point = self.translate_points_tup(from_coord_end)
line_tuple = (start_point, end_point)
points_for_interact.append(line_tuple)
print("packed_lines", points_for_interact)
self.lines = points_for_interact
@dataclass