Fiexed highlighting of operations

This commit is contained in:
bklronin
2026-08-19 00:11:24 +02:00
parent b184ade967
commit 6b6f7de5ab
4 changed files with 155 additions and 206 deletions
+45 -45
View File
@@ -5,10 +5,10 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- tech drawing and render improv"> <list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- tech drawing and render improv">
<change beforePath="$PROJECT_DIR$/src/fluency/io/project_io.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/io/project_io.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/WARP.md" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/technical_drawing.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/technical_drawing.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/fluency/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/main_window.py" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -45,48 +45,48 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"Python.2dtest.executor": "Run", &quot;Python.2dtest.executor&quot;: &quot;Run&quot;,
"Python.3d_windows.executor": "Run", &quot;Python.3d_windows.executor&quot;: &quot;Run&quot;,
"Python.Unnamed.executor": "Run", &quot;Python.Unnamed.executor&quot;: &quot;Run&quot;,
"Python.base.executor": "Run", &quot;Python.base.executor&quot;: &quot;Run&quot;,
"Python.data_model.executor": "Run", &quot;Python.data_model.executor&quot;: &quot;Run&quot;,
"Python.debug_dragging.executor": "Run", &quot;Python.debug_dragging.executor&quot;: &quot;Run&quot;,
"Python.draw_widget2d.executor": "Run", &quot;Python.draw_widget2d.executor&quot;: &quot;Run&quot;,
"Python.draw_widget_solve.executor": "Run", &quot;Python.draw_widget_solve.executor&quot;: &quot;Run&quot;,
"Python.fluency.executor": "Run", &quot;Python.fluency.executor&quot;: &quot;Run&quot;,
"Python.fluencyb.executor": "Run", &quot;Python.fluencyb.executor&quot;: &quot;Run&quot;,
"Python.gl_widget.executor": "Run", &quot;Python.gl_widget.executor&quot;: &quot;Run&quot;,
"Python.gui_ui.executor": "Run", &quot;Python.gui_ui.executor&quot;: &quot;Run&quot;,
"Python.kernel.executor": "Run", &quot;Python.kernel.executor&quot;: &quot;Run&quot;,
"Python.main.executor": "Run", &quot;Python.main.executor&quot;: &quot;Run&quot;,
"Python.main_window.executor": "Run", &quot;Python.main_window.executor&quot;: &quot;Run&quot;,
"Python.meshtest.executor": "Run", &quot;Python.meshtest.executor&quot;: &quot;Run&quot;,
"Python.occ_renderer.executor": "Run", &quot;Python.occ_renderer.executor&quot;: &quot;Run&quot;,
"Python.occ_to_mesh.executor": "Run", &quot;Python.occ_to_mesh.executor&quot;: &quot;Run&quot;,
"Python.render_backend.executor": "Run", &quot;Python.render_backend.executor&quot;: &quot;Run&quot;,
"Python.side_fluency.executor": "Run", &quot;Python.side_fluency.executor&quot;: &quot;Run&quot;,
"Python.simple_mesh.executor": "Run", &quot;Python.simple_mesh.executor&quot;: &quot;Run&quot;,
"Python.sketch.executor": "Run", &quot;Python.sketch.executor&quot;: &quot;Run&quot;,
"Python.technical_drawing_widget.executor": "Run", &quot;Python.technical_drawing_widget.executor&quot;: &quot;Run&quot;,
"Python.vtk_widget.executor": "Run", &quot;Python.vtk_widget.executor&quot;: &quot;Run&quot;,
"Python.vulkan.executor": "Run", &quot;Python.vulkan.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.OpenProjectViewOnStart": "true", &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"RunOnceActivity.typescript.service.memoryLimit.init": "true", &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
"codeWithMe.voiceChat.enabledByDefault": "false", &quot;codeWithMe.voiceChat.enabledByDefault&quot;: &quot;false&quot;,
"git-widget-placeholder": "feature/occ-migration", &quot;git-widget-placeholder&quot;: &quot;feature/occ-migration&quot;,
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency", &quot;last_opened_file_path&quot;: &quot;/Volumes/Data_drive/Programming/fluency/src/fluency&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings" &quot;settings.editor.selected.configurable&quot;: &quot;project.propVCSSupport.DirectoryMappings&quot;
} }
}]]></component> }</component>
<component name="QodanaReportsService"> <component name="QodanaReportsService">
<option name="descriptions"> <option name="descriptions">
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" /> <ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
-147
View File
@@ -1,147 +0,0 @@
# WARP.md
This file provides guidance to WARP (warp.dev) when working with code in this repository.
## Project Overview
Fluency is a CAD (Computer Aided Design) application built with Python/PySide6 that provides parametric 3D modeling through a timeline-based project system. The application combines 2D sketching with constraint solving, 3D visualization using VTK, and SDF (Signed Distance Function) based mesh generation.
## Common Commands
### Development Environment Setup
```bash
# Activate virtual environment (if exists)
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
```
### Running the Application
```bash
# Run the main application
python main.py
# Run with debugging
python -u main.py
```
### UI Development
```bash
# Convert Qt Designer UI file to Python code
pyside6-uic gui.ui > Gui.py -g python
```
### Building Executable
The project uses Nuitka for compilation (configured in `main.py` header):
```bash
# Build standalone executable
nuitka --standalone --plugin-enable=pyside6 --plugin-enable=numpy --macos-create-app-bundle main.py
```
### Testing
```bash
# Run mesh generation test
python meshtest.py
```
## Architecture Overview
### Core Components
#### Main Application (`main.py`)
- **MainWindow**: Central UI controller that manages all widgets and user interactions
- **Project System**: Hierarchical structure: `Project → Timeline → Component → Sketch/Body`
- **Signal-based Communication**: Qt signals coordinate between 2D sketching and 3D rendering
#### Project Hierarchy
```
Project
├── Timeline (list of Components)
└── Component
├── Sketches (dict)
├── Bodies (dict)
└── Connectors (for assembly)
```
#### Drawing Modules (`drawing_modules/`)
- **SketchWidget** (`draw_widget_solve.py`): 2D parametric sketching with SolverSpace constraint solving
- **VTKWidget** (`vtk_widget.py`): 3D visualization and mesh interaction using VTK
- **PyVistaWidget** (`vysta_widget.py`): Alternative 3D rendering backend
#### Mesh Generation (`mesh_modules/`)
- **VESTA** (`vesta_mesh.py`): Multi-threaded SDF-to-mesh conversion using marching cubes
- **Interactor Mesh** (`interactor_mesh.py`): Simplified edge-based meshes for 3D selection
- **Simple Mesh** (`simple_mesh.py`): Basic mesh utilities
### Data Flow Architecture
#### 2D to 3D Pipeline
1. **2D Sketching**: User draws in SketchWidget using Qt coordinate system
2. **Constraint Solving**: SolverSpace resolves geometric constraints
3. **SDF Generation**: Sketch converted to Signed Distance Functions for 3D operations
4. **Mesh Generation**: VESTA generates triangle meshes from SDF using marching cubes
5. **3D Rendering**: VTK displays both solid meshes and interactive edges
#### Signal Flow (from `doc/flow.md`)
- 2D QPoint → cartesian space → SolverSpace dict → constraint solving → display
- 3D mesh selection → projection to 2D → sketch widget integration
### Key Classes
#### Core Data Structures
- **Sketch**: 2D geometric data with origin, normal, points, and constraints
- **Body**: 3D mesh representation containing SDF objects and interactor meshes
- **Component**: Container grouping related sketches and bodies
- **Interactor**: Simplified edge-based mesh for 3D manipulation
#### Constraint Solving
The application uses `python_solvespace` for parametric constraint solving:
- Point-to-point constraints
- Distance constraints
- Horizontal/vertical line constraints
- Point-to-line constraints
### Technology Stack
- **GUI**: PySide6 (Qt for Python)
- **3D Graphics**: VTK for rendering, PyVista as alternative
- **Constraint Solving**: SolverSpace for parametric geometry
- **Mesh Generation**: SDF library with custom VESTA marching cubes implementation
- **Scientific Computing**: NumPy for mathematical operations
## Development Workflow
### Adding New Sketch Tools
1. Add UI button in `gui.ui`
2. Convert UI: `pyside6-uic gui.ui > Gui.py -g python`
3. Connect signal in `MainWindow.__init__()`
4. Implement tool logic in `SketchWidget`
### Adding New 3D Operations
1. Extend operation buttons in the Modify group
2. Implement operation logic using SDF functions
3. Update Body creation and timeline management
4. Handle interactor mesh generation for selection
### Debugging Tips
- Monitor solver results through `SolverSystem` status
- Use VTK's built-in debugging for rendering issues
- Check coordinate transformations between 2D sketch and 3D space
- Verify SDF function outputs before mesh generation
### File Structure
- `main.py`: Application entry point and main window
- `Gui.py`: Auto-generated UI code (do not edit directly)
- `gui.ui`: Qt Designer UI definition file
- `drawing_modules/`: 2D and 3D rendering widgets
- `mesh_modules/`: Mesh generation and processing
- `doc/`: Architecture and command documentation
## Dependencies
Primary external libraries:
- `PySide6`: Qt GUI framework
- `vtk`: 3D visualization toolkit
- `python-solvespace`: Constraint solving
- `sdf`: Signed Distance Function operations
- `numpy`: Numerical computations
- `scikit-image`: Marching cubes algorithm
- `names`: Random name generation for sketches
+6 -2
View File
@@ -1426,14 +1426,18 @@ class OCCRenderer(Renderer):
ais.SetColor(Quantity_Color(*color, Quantity_TOC_RGB)) ais.SetColor(Quantity_Color(*color, Quantity_TOC_RGB))
ais.SetDisplayMode(1) # shaded ais.SetDisplayMode(1) # shaded
try: try:
ais.SetTransparency(0.65) ais.SetTransparency(0.2)
except Exception: except Exception:
logger.debug("op highlight transparency set failed", exc_info=True) logger.debug("op highlight transparency set failed", exc_info=True)
try:
ais.SetSelectability(0)
except Exception:
logger.debug("op highlight selectability set failed", exc_info=True)
try: try:
ais.SetPolygonOffsets(3, 1.0, -0.5) ais.SetPolygonOffsets(3, 1.0, -0.5)
except Exception: except Exception:
logger.debug("op highlight polygon offset failed", exc_info=True) logger.debug("op highlight polygon offset failed", exc_info=True)
self._context.Display(ais, True) self._context.Display(ais, False)
self._op_highlight_ais = ais self._op_highlight_ais = ais
if self._view is not None: if self._view is not None:
self._view.Redraw() self._view.Redraw()
+104 -12
View File
@@ -8,7 +8,7 @@ import os
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from PySide6.QtCore import Qt, Slot, QSize, QSettings from PySide6.QtCore import Qt, Slot, QSize, QSettings, QTimer
from PySide6.QtGui import ( from PySide6.QtGui import (
QAction, QAction,
QColor, QColor,
@@ -1383,6 +1383,10 @@ def _replay_body_features(
"skipping chamfer" "skipping chamfer"
) )
continue continue
geom = kernel.chamfer(geom, feat.radius, edges=edges)
if geom is None:
return None
continue
if feat.operation in ("array", "pattern"): if feat.operation in ("array", "pattern"):
# Pattern needs no sketch — it repeats the running solid. # Pattern needs no sketch — it repeats the running solid.
@@ -1719,9 +1723,12 @@ class MainWindow(QMainWindow):
# selectable axis / centre / plane from the 3D viewport. # selectable axis / centre / plane from the 3D viewport.
self._sketch_gizmo_selection: Optional[dict] = None self._sketch_gizmo_selection: Optional[dict] = None
self._selected_body: Optional[Body] = None self._selected_body: Optional[Body] = None
# Track which body we're hiding while an operation-history highlight
# is active, so we can restore it when the selection changes.
self._op_highlight_body_id: Optional[str] = None self._op_highlight_body_id: Optional[str] = None
# Auto-clears the operation-history highlight 1 s after it is shown so
# the 3D view returns to its normal, fully-selectable state.
self._op_highlight_timer = QTimer(self)
self._op_highlight_timer.setSingleShot(True)
self._op_highlight_timer.timeout.connect(self._on_op_highlight_timeout)
# Body-highlight state: when no operation is selected we tint the # Body-highlight state: when no operation is selected we tint the
# selected body light-blue; save its original colour to restore later. # selected body light-blue; save its original colour to restore later.
self._body_highlight_id: Optional[str] = None self._body_highlight_id: Optional[str] = None
@@ -2685,6 +2692,12 @@ class MainWindow(QMainWindow):
""" """
if feat.operation in ("cut", "union"): if feat.operation in ("cut", "union"):
return self._compute_tool_shape(body, features, index, feat) return self._compute_tool_shape(body, features, index, feat)
# Fillet / chamfer: show a cube covering the corner where the op
# was applied, not the whole body.
if feat.operation in ("fillet", "chamfer"):
shape = self._compute_fillet_bbox_shape(body, features, index, feat)
if shape is not None:
return shape
# Default: show the intermediate body after this operation. # Default: show the intermediate body after this operation.
geom = _replay_body_features( geom = _replay_body_features(
self._kernel, body, features[: index + 1], self._kernel, body, features[: index + 1],
@@ -2748,6 +2761,80 @@ class MainWindow(QMainWindow):
return None return None
return self._kernel._get_shape(tool_geom) return self._kernel._get_shape(tool_geom)
def _compute_fillet_bbox_shape(
self, body: Body, features: List[Feature], index: int, feat: Feature
) -> Optional[Any]:
"""Return a cube covering where a fillet/chamfer op was applied.
Replays the body *before* the op, resolves the edges the op
touches (the same face-key / fingerprint resolution the replay
uses), and returns a box around their bounding volume a corner
marker, not the whole body. Returns *None* when the op spans
the whole body (scope "all") or no edges resolve, so the caller
falls back to the intermediate body.
"""
# Scope "all" fillets every edge of the body — a corner cube is
# meaningless there, so let the caller show the whole body.
if feat.scope == "all":
return None
try:
pre_geom = _replay_body_features(
self._kernel, body, features[:index],
self._through_all_length_for_geometry,
component=self._current_component,
)
except Exception:
logger.debug("fillet bbox replay failed", exc_info=True)
return None
pre = self._kernel._get_shape(pre_geom) if pre_geom is not None else None
if pre is None:
return None
# Resolve the fillet/chamfer edges on the pre-op geometry, exactly
# as the replay does (FaceKey first, fingerprint fallback).
edges: List[Any] = []
if feat.face_keys is not None:
edges = _resolve_edges_by_face_keys(
pre, feat, self._current_component
) or []
if not edges:
edges = _resolve_edges_by_fingerprint(pre, feat.edge_refs)
if not edges:
return None
from OCP.BRep import BRep_Builder
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCP.TopoDS import TopoDS_Compound
from OCP.gp import gp_Pnt
comp = TopoDS_Compound()
builder = BRep_Builder()
builder.MakeCompound(comp)
for e in edges:
builder.Add(comp, e)
bbox = Bnd_Box()
BRepBndLib.AddClose_s(comp, bbox)
if bbox.IsVoid():
return None
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# Pad by the fillet radius so the cube comfortably covers the
# round/bevel, not just the sharp pre-op edge line(s).
pad = float(feat.radius) if feat.radius else 0.0
cx, cy, cz = (xmin + xmax) / 2.0, (ymin + ymax) / 2.0, (zmin + zmax) / 2.0
w = max(2 * pad, (xmax - xmin) + 2 * pad)
h = max(2 * pad, (ymax - ymin) + 2 * pad)
d = max(2 * pad, (zmax - zmin) + 2 * pad)
box = BRepPrimAPI_MakeBox(
gp_Pnt(cx - w / 2.0, cy - h / 2.0, cz - d / 2.0), w, h, d
)
return box.Shape()
# ── Body highlight helpers ───────────────────────────────────────── # ── Body highlight helpers ─────────────────────────────────────────
@@ -2777,10 +2864,9 @@ class MainWindow(QMainWindow):
"""Enable 'Del Op' / 'Mirror Op', highlight body or selected op in 3D.""" """Enable 'Del Op' / 'Mirror Op', highlight body or selected op in 3D."""
item = self._operations_list.currentItem() item = self._operations_list.currentItem()
# Restore the body we hid for the previous operation highlight. # Drop the previous highlight and any pending auto-clear so rapid
if self._op_highlight_body_id is not None: # re-selection always starts from a clean state.
self._viewer_3d.set_visibility(self._op_highlight_body_id, True) self._op_highlight_timer.stop()
self._op_highlight_body_id = None
self._viewer_3d.clear_operation_highlight() self._viewer_3d.clear_operation_highlight()
# Restore the previous body colour highlight. # Restore the previous body colour highlight.
@@ -2805,18 +2891,24 @@ class MainWindow(QMainWindow):
body, features, index, feat body, features, index, feat
) )
if shape is not None: if shape is not None:
if body.render_object is not None and body.visible: # Overlay the operation geometry on the still-visible
self._viewer_3d.set_visibility( # body as a hot-pink flash. The overlay is
body.render_object, False # non-selectable, so face/edge/vertex picking keeps
) # working while it shows.
self._op_highlight_body_id = body.render_object
self._viewer_3d.highlight_operation(shape) self._viewer_3d.highlight_operation(shape)
# Auto-clear after 1 s so the view returns to normal
# and the user can work on the body.
self._op_highlight_timer.start(1000)
except Exception: except Exception:
logger.debug("op highlight replay failed", exc_info=True) logger.debug("op highlight replay failed", exc_info=True)
else: else:
# Base operation (or non-selectable) — highlight body light blue. # Base operation (or non-selectable) — highlight body light blue.
self._highlight_selected_body_light_blue() self._highlight_selected_body_light_blue()
def _on_op_highlight_timeout(self) -> None:
"""1 s elapsed — drop the operation highlight, restoring the normal view."""
self._viewer_3d.clear_operation_highlight()
def _on_mirror_operation(self) -> None: def _on_mirror_operation(self) -> None:
"""Mirror the body at the selected operation's point in the feature history. """Mirror the body at the selected operation's point in the feature history.