Files
fluencyCAD/doc/SKETCHER_TECHNICAL_DOCUMENTATION.md
bklronin 11d053fda4 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.
2025-08-16 22:30:18 +02:00

24 KiB

Fluency CAD - Improved Sketcher Technical Documentation

Table of Contents

  1. Overview
  2. Architecture
  3. Core Components
  4. Geometry System
  5. Constraint Solving
  6. Coordinate Systems
  7. Interaction System
  8. Rendering System
  9. Snapping System
  10. Working Plane Integration
  11. API Reference
  12. Performance Considerations
  13. Troubleshooting

Overview

The ImprovedSketchWidget is a parametric 2D sketching system built for Fluency CAD. It provides constraint-based geometric modeling with real-time solving, integrated snapping, and seamless integration with 3D working planes. The system is built on top of the SolverSpace constraint solver and PySide6 for the user interface.

Key Features

  • Parametric Geometry: All geometry is constraint-driven and automatically updates
  • Real-time Solving: Constraints are solved dynamically as geometry is modified
  • Advanced Snapping: Multi-mode snapping system (points, midpoints, grid, angles)
  • Construction Geometry: Support for helper/construction geometry
  • Working Plane Integration: Seamless 2D/3D workflow with projected geometry
  • Interactive Dragging: Smooth point dragging with constraint preservation
  • Multiple Drawing Modes: Lines, rectangles, circles, arcs, and points

Architecture

┌─────────────────────────────────────────────────────────┐
│                ImprovedSketchWidget                     │
│  ┌─────────────────┐  ┌─────────────────────────────┐   │
│  │   User Interface │  │      Rendering System      │   │
│  │   - Mouse Events │  │   - Coordinate Transform    │   │
│  │   - Keyboard     │  │   - Geometry Drawing       │   │
│  │   - Mode Control │  │   - UI Overlays            │   │
│  └─────────────────┘  └─────────────────────────────┘   │
│           │                         │                   │
│           └─────────┬───────────────┘                   │
│                     │                                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │            Interaction System               │   │
│  │  - Snapping Engine                             │   │
│  │  - Dragging Logic                              │   │
│  │  - Selection Management                        │   │
│  └─────────────────────────────────────────────────┘   │
│                     │                                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │              Geometry System                   │   │
│  │  ┌─────────────┐  ┌─────────────────────────┐   │   │
│  │  │ Point2D     │  │      Line2D             │   │   │
│  │  │ Circle2D    │  │      Arc2D (future)     │   │   │
│  │  └─────────────┘  └─────────────────────────┘   │   │
│  └─────────────────────────────────────────────────┘   │
│                     │                                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │            ImprovedSketch                      │   │
│  │  (Enhanced SolverSystem)                       │   │
│  │  - Constraint Management                       │   │
│  │  - Solver Integration                          │   │
│  │  - Geometry Storage                            │   │
│  └─────────────────────────────────────────────────┘   │
│                     │                                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │          SolverSpace Library                   │   │
│  │  - Constraint Solving Engine                   │   │
│  │  - Geometric Relationships                     │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Core Components

1. ImprovedSketchWidget

The main widget class that handles user interaction and rendering.

Key Responsibilities:

  • Mouse and keyboard event handling
  • Mode management (line, circle, constraint modes, etc.)
  • Coordinate system transformations
  • Rendering pipeline orchestration
  • Integration with external systems (working planes)

2. ImprovedSketch

Enhanced wrapper around SolverSpace's SolverSystem.

Key Responsibilities:

  • Geometry storage and management
  • Constraint system integration
  • Solver result processing
  • Handle management for solver objects

3. Geometry Classes

Type-safe geometry representations with validation.

Classes:

  • Point2D: 2D points with solver integration
  • Line2D: 2D lines with constraint tracking
  • Circle2D: 2D circles with radius constraints

Geometry System

Point2D Class

class Point2D:
    def __init__(self, x: float, y: float, is_construction: bool = False):
        self.id = uuid.uuid4()           # Unique identifier
        self.x = float(x)                # X coordinate
        self.y = float(y)                # Y coordinate
        self.ui_point = QPoint(int(x), int(y))  # Qt UI point
        self.handle = None               # SolverSpace handle
        self.handle_nr = None            # Handle number
        self.is_helper = is_construction # Construction geometry flag

Key Features:

  • Automatic coordinate validation
  • SolverSpace handle integration
  • Construction/normal geometry support
  • Distance calculations and equality testing

Line2D Class

class Line2D:
    def __init__(self, start_point: Point2D, end_point: Point2D, is_construction: bool = False):
        self.id = uuid.uuid4()
        self.start = start_point         # Start point reference
        self.end = end_point            # End point reference
        self.handle = None              # SolverSpace handle
        self.constraints = []           # Applied constraints list
        self.is_helper = is_construction

Key Features:

  • Automatic degenerate line detection
  • Length, midpoint, and angle calculations
  • Point-on-line testing with tolerance
  • Constraint tracking and annotation

Circle2D Class

class Circle2D:
    def __init__(self, center: Point2D, radius: float, is_construction: bool = False):
        self.id = uuid.uuid4()
        self.center = center            # Center point reference
        self.radius = float(radius)     # Radius value
        self.handle = None              # SolverSpace handle
        self.constraints = []           # Applied constraints
        self.is_helper = is_construction

Constraint Solving

SolverSpace Integration

The system uses the python-solvespace library for constraint solving. The ImprovedSketch class wraps the SolverSpace API and provides:

  1. Automatic Handle Management: Each geometry object gets a unique handle
  2. Error Handling: Robust error handling for solver failures
  3. Position Updates: Automatic geometry position updates after solving

Constraint Types

Geometric Constraints

  • Coincident: Point-to-point or point-to-line coincidence
  • Horizontal: Forces lines to be horizontal
  • Vertical: Forces lines to be vertical
  • Distance: Fixes distance between points or line length
  • Parallel: Makes lines parallel (future implementation)
  • Perpendicular: Makes lines perpendicular (future implementation)

Constraint Application Workflow

def _handle_distance_constraint(self, pos: QPoint):
    line = self.sketch.get_line_near(pos)
    if line and line.handle:
        # Get user input for distance
        distance, ok = QInputDialog.getDouble(...)
        if ok:
            # Apply constraint to solver
            self.sketch.distance(line.start.handle, line.end.handle, distance, self.sketch.wp)
            # Solve system
            result = self.sketch.solve_system()
            if result == ResultFlag.OKAY:
                line.constraints.append(f"L={distance:.2f}")

Solver Workflow

  1. Constraint Addition: Constraints are added to the solver system
  2. System Solving: The solver attempts to find a valid solution
  3. Result Processing: If successful, geometry positions are updated
  4. UI Updates: The display is refreshed to show new positions

Coordinate Systems

The sketcher uses multiple coordinate systems that must be properly transformed between:

1. Sketch Coordinates (Local)

  • Origin at sketch center
  • Y-axis points up (mathematical convention)
  • Units in millimeters
  • Range: typically -1000 to +1000

2. Viewport Coordinates (Screen)

  • Origin at top-left of widget
  • Y-axis points down (computer graphics convention)
  • Units in pixels
  • Range: 0 to widget dimensions

3. Working Plane Coordinates (3D)

  • 3D coordinates projected onto 2D working plane
  • Transformation handled by external VTK system
  • Converted to sketch coordinates for display

Coordinate Transformations

Viewport to Local (Mouse Input)

def _viewport_to_local(self, viewport_pos: QPoint) -> QPoint:
    # Step 1: Subtract widget center
    center_x = self.width() / 2
    center_y = self.height() / 2
    
    # Step 2: Apply pan offset
    viewport_x = viewport_pos.x() - center_x - (self.pan_offset.x() * self.zoom_factor)
    viewport_y = viewport_pos.y() - center_y - (self.pan_offset.y() * self.zoom_factor)
    
    # Step 3: Apply inverse zoom and Y-flip
    local_x = viewport_x / self.zoom_factor
    local_y = -viewport_y / self.zoom_factor
    
    return QPoint(int(local_x), int(local_y))

Rendering Transform Setup

def _setup_coordinate_system(self, painter: QPainter):
    transform = QTransform()
    
    # Translate to center and apply pan
    center = QPointF(self.width() / 2, self.height() / 2)
    transform.translate(center.x() + self.pan_offset.x() * self.zoom_factor, 
                      center.y() + self.pan_offset.y() * self.zoom_factor)
    
    # Apply zoom and flip Y-axis
    transform.scale(self.zoom_factor, -self.zoom_factor)
    
    painter.setTransform(transform)

Interaction System

Mode-Based Interaction

The sketcher supports multiple interaction modes:

Drawing Modes

  • SketchMode.LINE: Two-point line creation
  • SketchMode.RECTANGLE: Two-corner rectangle creation
  • SketchMode.CIRCLE: Center-radius circle creation
  • SketchMode.POINT: Single point creation

Constraint Modes

  • SketchMode.COINCIDENT_PT_PT: Point-to-point coincidence
  • SketchMode.HORIZONTAL: Horizontal line constraint
  • SketchMode.VERTICAL: Vertical line constraint
  • SketchMode.DISTANCE: Distance/length constraint

Selection Mode

  • SketchMode.NONE: Selection and manipulation mode

Mouse Event Handling

Click Processing Flow

def mousePressEvent(self, event):
    local_pos = self._viewport_to_local(event.pos())
    
    if event.button() == Qt.LeftButton:
        if self.current_mode == SketchMode.NONE:
            # Check for point dragging
            point = self.sketch.get_point_near(local_pos)
            if point:
                self._start_point_drag(point, local_pos)
        else:
            # Handle drawing modes
            self._handle_left_click(local_pos)

Point Dragging System

The point dragging system is optimized for performance and maintains constraint consistency:

Drag Phases

  1. Drag Start (_start_point_drag):

    • Identifies dragged point
    • Stores initial position
    • Sets dragging state
  2. Drag Update (_handle_point_drag):

    • Updates point visual position only
    • Applies snapping
    • No solver execution (for performance)
  3. Drag End (_end_point_drag):

    • Updates solver parameters with final position
    • Runs constraint solver
    • Updates all connected geometry
    • Resets drag state
def _end_point_drag(self):
    if not self.dragging_point:
        return
    
    # Update solver parameters with final position
    if self.dragging_point.handle:
        new_x = self.dragging_point.x
        new_y = self.dragging_point.y
        self.sketch.set_params(self.dragging_point.handle.params, [new_x, new_y])
    
    # Run solver to update all connected geometry
    result = self.sketch.solve_system()
    if result == ResultFlag.OKAY:
        self.sketch_modified.emit()

Rendering System

Rendering Pipeline

The rendering system uses Qt's QPainter with a multi-layer approach:

  1. Coordinate System Setup: Apply zoom, pan, and Y-flip transforms
  2. Background Rendering: Grid, axes, and origin marker
  3. Geometry Rendering: Points, lines, circles with proper styling
  4. Dynamic Elements: Preview geometry during creation
  5. UI Overlays: Mode indicators, measurements, snap highlights

Rendering Layers

Layer 1: Background

  • Coordinate axes (dashed gray lines)
  • Grid (if enabled)
  • Origin marker (red circle)

Layer 2: Geometry

  • Construction geometry (green, dotted)
  • Normal geometry (gray, solid)
  • Constraint annotations

Layer 3: Interactive Elements

  • Hover highlights (red)
  • Dynamic previews (gray, dashed)
  • Measurements during creation

Layer 4: UI Overlays

  • Snap point indicators
  • Mode and zoom information
  • Status messages

Styling System

Rendering appearance is controlled by the RenderSettings class:

@dataclass
class RenderSettings:
    normal_pen_width: float = 2.0
    construction_pen_width: float = 1.0
    highlight_pen_width: float = 3.0
    
    normal_color = QColor(128, 128, 128)      # Gray
    construction_color = QColor(0, 255, 0)    # Green
    highlight_color = QColor(255, 0, 0)       # Red
    solver_color = QColor(0, 255, 0)          # Green
    dynamic_color = QColor(128, 128, 128)     # Gray
    text_color = QColor(255, 255, 255)        # White

Dynamic Previews

During geometry creation, dynamic previews show:

  • Line Creation: Dashed line from start to cursor with length annotation
  • Rectangle Creation: Dashed rectangle outline
  • Circle Creation: Dashed circle with radius line and annotation

Snapping System

Snap Modes

The snapping system supports multiple simultaneous snap modes:

SnapMode.POINT

  • Snaps to existing geometry points
  • Priority: Highest
  • Visual: Red circle highlight

SnapMode.MIDPOINT

  • Snaps to line midpoints
  • Priority: Medium
  • Visual: Red diamond highlight

SnapMode.GRID

  • Snaps to grid intersections
  • Priority: Lowest
  • Visual: Green cross highlight

SnapMode.HORIZONTAL/VERTICAL

  • Angular snapping (future implementation)
  • Constrains to horizontal/vertical directions

SnapMode.INTERSECTION

  • Snaps to line intersections (future implementation)

Snap Algorithm

def _get_snapped_position(self, pos: QPoint) -> QPoint:
    min_distance = float('inf')
    snapped_pos = pos
    snap_threshold = self.snap_settings.snap_distance
    
    # Point snapping (highest priority)
    if SnapMode.POINT in self.snap_settings.enabled_modes:
        for point in self.sketch.points:
            distance = math.sqrt((pos.x() - point.x)**2 + (pos.y() - point.y)**2)
            if distance < snap_threshold and distance < min_distance:
                snapped_pos = QPoint(int(point.x), int(point.y))
                min_distance = distance
    
    # Midpoint snapping (medium priority)
    if SnapMode.MIDPOINT in self.snap_settings.enabled_modes and min_distance > snap_threshold:
        for line in self.sketch.lines:
            midpoint = line.midpoint
            distance = math.sqrt((pos.x() - midpoint.x)**2 + (pos.y() - midpoint.y)**2)
            if distance < snap_threshold and distance < min_distance:
                snapped_pos = QPoint(int(midpoint.x), int(midpoint.y))
                min_distance = distance
    
    return snapped_pos

Snap Settings

@dataclass
class SnapSettings:
    snap_distance: float = 20.0      # Snap threshold in pixels
    angle_increment: float = 15.0    # Angular snap increment
    grid_spacing: float = 50.0       # Grid spacing
    enabled_modes: Set[SnapMode]     # Active snap modes

Working Plane Integration

Projected Geometry Workflow

The sketcher integrates with 3D working planes through projected geometry:

  1. 3D Geometry Selection: User selects 3D lines/points in VTK widget
  2. Plane Definition: System computes working plane from selections
  3. Geometry Projection: 3D geometry is projected onto 2D working plane
  4. Sketch Import: Projected geometry is imported as construction geometry

Projection Import Methods

convert_proj_points(proj_points)

Imports projected 3D points as 2D construction points:

def convert_proj_points(self, proj_points):
    for point_data in proj_points:
        if hasattr(point_data, 'x') and hasattr(point_data, 'y'):
            point = Point2D(point_data.x, point_data.y, True)  # Construction
            self.sketch.add_point(point)

convert_proj_lines(proj_lines)

Imports projected 3D lines as 2D construction lines:

def convert_proj_lines(self, proj_lines):
    for line_data in proj_lines:
        # Handle object format
        if hasattr(line_data, 'start') and hasattr(line_data, 'end'):
            x1, y1 = line_data.start.x, line_data.start.y
            x2, y2 = line_data.end.x, line_data.end.y
            
            # Skip degenerate lines
            if abs(x1 - x2) < 1e-6 and abs(y1 - y2) < 1e-6:
                continue
            
            start = Point2D(x1, y1, True)
            end = Point2D(x2, y2, True)
            self.sketch.add_point(start)
            self.sketch.add_point(end)
            line = Line2D(start, end, True)
            self.sketch.add_line(line)

Construction vs Normal Geometry

  • Construction Geometry:

    • Rendered in green with dotted lines
    • Used for reference and alignment
    • Created from projected 3D geometry
    • Flag: is_construction=True
  • Normal Geometry:

    • Rendered in gray with solid lines
    • Part of the actual sketch design
    • Created by user drawing actions
    • Flag: is_construction=False

API Reference

Main Widget Class

ImprovedSketchWidget

Initialization:

widget = ImprovedSketchWidget()
widget.show()

Mode Control:

widget.set_mode(SketchMode.LINE)
widget.set_construction_mode(True)

Snapping Control:

widget.set_snap_mode(SnapMode.POINT, True)
widget.toggle_snap_mode(SnapMode.MIDPOINT, enabled)

View Control:

widget.zoom_to_fit()

Sketch Access:

sketch = widget.get_sketch()
widget.set_sketch(imported_sketch)

Sketch Management

ImprovedSketch

Geometry Addition:

sketch = ImprovedSketch()
point = Point2D(10, 20)
line = Line2D(start_point, end_point)
circle = Circle2D(center_point, radius)

sketch.add_point(point)
sketch.add_line(line)
sketch.add_circle(circle)

Constraint Application:

# Distance constraint
sketch.distance(point1.handle, point2.handle, 50.0, sketch.wp)

# Coincident constraint
sketch.coincident(point1.handle, point2.handle, sketch.wp)

# Line constraints
sketch.horizontal(line.handle, sketch.wp)
sketch.vertical(line.handle, sketch.wp)

# Solve system
result = sketch.solve_system()

Signals

The widget emits several signals for integration:

# Emitted when constraint is successfully applied
widget.constraint_applied.connect(callback)

# Emitted when new geometry is created
widget.geometry_created.connect(callback)  # Parameter: geometry type string

# Emitted when sketch is modified
widget.sketch_modified.connect(callback)

Performance Considerations

Optimization Strategies

  1. Lazy Solving: Solver only runs when necessary (after constraints or drag end)
  2. Efficient Rendering: Uses Qt's optimized drawing primitives
  3. Smart Updates: Only redraws affected regions when possible
  4. Handle Caching: SolverSpace handles are cached to avoid recreation

Memory Management

  • Geometry objects use weak references where possible
  • SolverSpace handles are properly cleaned up
  • Qt objects follow parent-child hierarchy for automatic cleanup

Scalability Limits

  • Recommended maximum: ~1000 geometric entities
  • Solver performance degrades with complex constraint networks
  • Rendering remains smooth up to ~10,000 entities

Troubleshooting

Common Issues

Solver Failures

Symptoms: Constraints not applied, geometry not updating Causes: Over-constrained systems, conflicting constraints Solutions:

  • Check constraint compatibility
  • Verify geometry validity
  • Use ResultFlag inspection for error details

Coordinate Transform Issues

Symptoms: Mouse clicks don't match visual geometry Causes: Incorrect transform calculations, zoom/pan state corruption Solutions:

  • Verify _viewport_to_local and _setup_coordinate_system consistency
  • Reset view with zoom_to_fit()

Performance Problems

Symptoms: Slow dragging, UI lag Causes: Solver running during drag, excessive redraws Solutions:

  • Ensure solver only runs in _end_point_drag
  • Check render loop efficiency
  • Profile with Qt performance tools

Snap Behavior Issues

Symptoms: Inconsistent snapping, incorrect snap points Causes: Priority conflicts, threshold settings, coordinate errors Solutions:

  • Adjust snap threshold in SnapSettings
  • Verify snap priority order
  • Check coordinate conversion in snap calculations

Debug Logging

Enable detailed logging for troubleshooting:

import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('improved_sketcher')

Key log messages include:

  • Geometry addition/removal
  • Constraint application results
  • Solver execution status
  • Coordinate transformations
  • Snap calculations

Testing Guidelines

Unit Testing

  • Test geometry classes with edge cases
  • Verify coordinate transformations
  • Test constraint application logic

Integration Testing

  • Test with various sketch sizes
  • Verify working plane integration
  • Test complex constraint networks

Performance Testing

  • Measure solver execution time
  • Profile rendering performance
  • Test with large geometry sets

Conclusion

The ImprovedSketchWidget provides a robust, extensible foundation for 2D parametric sketching in Fluency CAD. Its architecture separates concerns effectively, uses proven libraries (SolverSpace, PySide6), and provides rich interaction capabilities while maintaining good performance characteristics.

The system is designed for extensibility - new geometry types, constraint types, and interaction modes can be added following the established patterns. The comprehensive API allows for both direct use and integration with larger CAD systems.