850 lines
31 KiB
Markdown
850 lines
31 KiB
Markdown
# Fluency CAD - Improved Sketcher Technical Documentation
|
|
|
|
## Table of Contents
|
|
1. [Overview](#overview)
|
|
2. [Architecture](#architecture)
|
|
3. [Core Components](#core-components)
|
|
4. [Geometry System](#geometry-system)
|
|
5. [Constraint Solving](#constraint-solving)
|
|
6. [Coordinate Systems](#coordinate-systems)
|
|
7. [Interaction System](#interaction-system)
|
|
8. [Rendering System](#rendering-system)
|
|
9. [Snapping System](#snapping-system)
|
|
10. [Working Plane Integration](#working-plane-integration)
|
|
11. [API Reference](#api-reference)
|
|
12. [Performance Considerations](#performance-considerations)
|
|
13. [Troubleshooting](#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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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)
|
|
```python
|
|
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
|
|
```python
|
|
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 with robust mode management:
|
|
|
|
#### 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 (enables point dragging)
|
|
|
|
### Selection and Deletion System
|
|
|
|
The sketcher now includes a comprehensive selection and deletion system that allows users to select and remove elements from the sketch.
|
|
|
|
#### Selection Methods
|
|
|
|
1. **Single Element Selection**: Click on individual points or lines to select/deselect them
|
|
2. **Rectangle Selection**: Click and drag to create a selection rectangle for multiple elements
|
|
3. **Visual Feedback**: Selected elements are highlighted in yellow with increased size
|
|
|
|
#### Deletion Methods
|
|
|
|
1. **Keyboard Deletion**: Press Delete or Backspace to remove selected elements
|
|
2. **Proper Cleanup**: Elements are removed from both the sketch and constraint solver
|
|
3. **Dependency Handling**: Lines are deleted before points to maintain geometric integrity
|
|
|
|
#### Implementation Details
|
|
|
|
The selection system is implemented through the following components:
|
|
|
|
- **Selection Tracking**: `selected_elements` list tracks currently selected elements
|
|
- **Rectangle Selection**: `selection_rect_start` and `selection_rect_end` track rectangle selection bounds
|
|
- **Visual Feedback**: Modified drawing methods highlight selected elements in yellow
|
|
- **Keyboard Support**: `keyPressEvent` handles Delete/Backspace keys
|
|
- **Deletion Method**: `delete_selected_elements` handles removal of elements from sketch and solver
|
|
|
|
#### Selection Workflow
|
|
|
|
1. **Default Selection Mode**: The sketcher defaults to selection mode when no drawing tool is active
|
|
2. **Element Selection**:
|
|
- Click on points or lines to select/deselect them (they turn yellow)
|
|
- Click and drag to create a rectangle selection for multiple elements
|
|
3. **Element Deletion**:
|
|
- Press Delete or Backspace to remove all selected elements
|
|
- Elements are removed from both the sketch and constraint solver
|
|
4. **Visual Feedback**:
|
|
- Selected elements are highlighted in yellow
|
|
- Rectangle selection is shown with a yellow dashed border
|
|
|
|
#### Constraints Handling
|
|
|
|
When elements are deleted:
|
|
- Lines are removed first to avoid issues with points being used by lines
|
|
- Points are only removed if they are not used by any remaining lines
|
|
- The constraint solver is re-run after deletion to update remaining constraints
|
|
- Proper error handling ensures the UI remains responsive even if solver operations fail
|
|
|
|
### Mode Management System
|
|
|
|
The mode system has been enhanced to provide intuitive selection and deletion functionality:
|
|
|
|
#### Mode Compatibility
|
|
- Python `None` is automatically converted to `SketchMode.NONE` for backward compatibility
|
|
- The `set_mode()` method ensures the mode is always a valid `SketchMode` enum value
|
|
- Mode changes reset all interaction buffers and state
|
|
|
|
#### Default Selection Behavior
|
|
- `SketchMode.NONE` now serves as the default selection mode
|
|
- When no drawing tool is active, the sketcher is in selection mode by default
|
|
- Users can click on elements to select/deselect them (they turn yellow)
|
|
- Users can click and drag to create rectangle selections
|
|
- Pressing Delete or Backspace removes all selected elements
|
|
|
|
#### Right-Click Behavior
|
|
- Right-clicking **always** exits any active mode and returns to `SketchMode.NONE`
|
|
- This enables point dragging and prevents unintended geometry creation
|
|
- The mode reset happens directly in the sketcher, not through main app signals
|
|
|
|
#### Point Dragging Safety
|
|
- Point dragging is **only** enabled when in `SketchMode.NONE` mode
|
|
- Left-clicks in `NONE` mode check for draggable points first
|
|
- If no point is found, the click is processed as a selection operation
|
|
|
|
### Mouse Event Handling
|
|
|
|
#### Click Processing Flow
|
|
```python
|
|
def mousePressEvent(self, event):
|
|
local_pos = self._viewport_to_local(event.pos())
|
|
|
|
if event.button() == Qt.LeftButton:
|
|
self._handle_left_click(local_pos)
|
|
elif event.button() == Qt.RightButton:
|
|
self._handle_right_click(local_pos)
|
|
elif event.button() == Qt.MiddleButton:
|
|
self._start_panning(event.pos())
|
|
```
|
|
|
|
#### Enhanced Left-Click Handler
|
|
```python
|
|
def _handle_left_click(self, pos: QPoint):
|
|
# Safety check for NONE mode (dragging enabled)
|
|
if self.current_mode == SketchMode.NONE or self.current_mode is None:
|
|
point = self.sketch.get_point_near(pos, self.snap_settings.snap_distance)
|
|
if point:
|
|
self._start_point_drag(point, pos)
|
|
return
|
|
else:
|
|
# No point found - ignore click to prevent unintended drawing
|
|
return
|
|
|
|
# Handle active drawing/constraint modes
|
|
if self.current_mode == SketchMode.LINE:
|
|
self._handle_line_creation(pos)
|
|
elif self.current_mode == SketchMode.HORIZONTAL:
|
|
self._handle_horizontal_constraint(pos)
|
|
# ... other modes
|
|
```
|
|
|
|
#### Right-Click Mode Reset
|
|
```python
|
|
def _handle_right_click(self, pos: QPoint):
|
|
# Reset interaction state
|
|
self._reset_interaction_state()
|
|
|
|
# Force mode to NONE to enable dragging
|
|
self.current_mode = SketchMode.NONE
|
|
|
|
# Emit signal to inform main app
|
|
self.constraint_applied.emit()
|
|
```
|
|
|
|
### 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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
@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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
@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:
|
|
```python
|
|
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:
|
|
```python
|
|
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:**
|
|
```python
|
|
widget = ImprovedSketchWidget()
|
|
widget.show()
|
|
```
|
|
|
|
**Mode Control:**
|
|
```python
|
|
# Set drawing modes
|
|
widget.set_mode(SketchMode.LINE)
|
|
widget.set_mode(SketchMode.NONE) # Enable selection/dragging
|
|
widget.set_mode(None) # Also converted to SketchMode.NONE
|
|
|
|
# Construction geometry
|
|
widget.set_construction_mode(True)
|
|
```
|
|
|
|
**Snapping Control:**
|
|
```python
|
|
widget.set_snap_mode(SnapMode.POINT, True)
|
|
widget.toggle_snap_mode(SnapMode.MIDPOINT, enabled)
|
|
```
|
|
|
|
**View Control:**
|
|
```python
|
|
widget.zoom_to_fit()
|
|
```
|
|
|
|
**Sketch Access:**
|
|
```python
|
|
sketch = widget.get_sketch()
|
|
widget.set_sketch(imported_sketch)
|
|
```
|
|
|
|
### Sketch Management
|
|
|
|
#### ImprovedSketch
|
|
|
|
**Geometry Addition:**
|
|
```python
|
|
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:**
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
#### Mode Handling Problems
|
|
**Symptoms**: Unintended line creation when dragging, tools not deactivating properly
|
|
**Causes**: Mode not properly reset to NONE, Python None vs SketchMode.NONE confusion
|
|
**Solutions**:
|
|
- Always right-click to exit active modes
|
|
- Ensure `set_mode(None)` is converted to `SketchMode.NONE`
|
|
- Verify mode state after tool deactivation in main app
|
|
|
|
#### Point Dragging Issues
|
|
**Symptoms**: Cannot drag points, dragging creates unwanted lines
|
|
**Causes**: Mode not set to NONE, safety checks preventing drag detection
|
|
**Solutions**:
|
|
- Verify current mode is `SketchMode.NONE` before attempting to drag
|
|
- Right-click to ensure proper mode exit from drawing tools
|
|
- Check that point detection threshold is appropriate
|
|
|
|
#### 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:
|
|
```python
|
|
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
|
|
|
|
---
|
|
|
|
## Recent Improvements (2025-08-16)
|
|
|
|
### Mode Handling Enhancements
|
|
|
|
Significant improvements have been made to the mode management system:
|
|
|
|
#### Fixed Issues
|
|
1. **Unintended Line Creation**: Resolved issue where dragging with line tool deactivated would still create lines
|
|
2. **Mode Reset Reliability**: Right-click now reliably exits any active mode and returns to NONE
|
|
3. **Backward Compatibility**: Python `None` mode values are automatically converted to `SketchMode.NONE`
|
|
4. **Safety Checks**: Added comprehensive checks to prevent drawing operations in NONE mode
|
|
|
|
#### Implementation Details
|
|
- Enhanced `_handle_right_click()` to directly set mode to NONE
|
|
- Added safety checks in `_handle_left_click()` for NONE mode behavior
|
|
- Improved `set_mode()` method to handle None input gracefully
|
|
- Added comprehensive debug logging for mode transitions
|
|
|
|
#### Integration Improvements
|
|
- Fixed main app integration where constraint modes were prematurely reset
|
|
- Ensured persistent constraint behavior until explicit user cancellation
|
|
- Maintained UI button state consistency with actual sketcher mode
|
|
|
|
These improvements ensure reliable mode transitions and prevent common user frustrations with unintended geometry creation.
|
|
|
|
## 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.
|
|
|
|
With the recent mode handling improvements, the sketcher now provides a more reliable and intuitive user experience, with proper separation between drawing modes and selection/manipulation operations.
|