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 name="ChangeListManager">
<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$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" afterDir="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/ui/technical_drawing_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/WARP.md" beforeDir="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/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/main_window.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -45,48 +45,48 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python.2dtest.executor": "Run",
"Python.3d_windows.executor": "Run",
"Python.Unnamed.executor": "Run",
"Python.base.executor": "Run",
"Python.data_model.executor": "Run",
"Python.debug_dragging.executor": "Run",
"Python.draw_widget2d.executor": "Run",
"Python.draw_widget_solve.executor": "Run",
"Python.fluency.executor": "Run",
"Python.fluencyb.executor": "Run",
"Python.gl_widget.executor": "Run",
"Python.gui_ui.executor": "Run",
"Python.kernel.executor": "Run",
"Python.main.executor": "Run",
"Python.main_window.executor": "Run",
"Python.meshtest.executor": "Run",
"Python.occ_renderer.executor": "Run",
"Python.occ_to_mesh.executor": "Run",
"Python.render_backend.executor": "Run",
"Python.side_fluency.executor": "Run",
"Python.simple_mesh.executor": "Run",
"Python.sketch.executor": "Run",
"Python.technical_drawing_widget.executor": "Run",
"Python.vtk_widget.executor": "Run",
"Python.vulkan.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"codeWithMe.voiceChat.enabledByDefault": "false",
"git-widget-placeholder": "feature/occ-migration",
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Python.2dtest.executor&quot;: &quot;Run&quot;,
&quot;Python.3d_windows.executor&quot;: &quot;Run&quot;,
&quot;Python.Unnamed.executor&quot;: &quot;Run&quot;,
&quot;Python.base.executor&quot;: &quot;Run&quot;,
&quot;Python.data_model.executor&quot;: &quot;Run&quot;,
&quot;Python.debug_dragging.executor&quot;: &quot;Run&quot;,
&quot;Python.draw_widget2d.executor&quot;: &quot;Run&quot;,
&quot;Python.draw_widget_solve.executor&quot;: &quot;Run&quot;,
&quot;Python.fluency.executor&quot;: &quot;Run&quot;,
&quot;Python.fluencyb.executor&quot;: &quot;Run&quot;,
&quot;Python.gl_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.gui_ui.executor&quot;: &quot;Run&quot;,
&quot;Python.kernel.executor&quot;: &quot;Run&quot;,
&quot;Python.main.executor&quot;: &quot;Run&quot;,
&quot;Python.main_window.executor&quot;: &quot;Run&quot;,
&quot;Python.meshtest.executor&quot;: &quot;Run&quot;,
&quot;Python.occ_renderer.executor&quot;: &quot;Run&quot;,
&quot;Python.occ_to_mesh.executor&quot;: &quot;Run&quot;,
&quot;Python.render_backend.executor&quot;: &quot;Run&quot;,
&quot;Python.side_fluency.executor&quot;: &quot;Run&quot;,
&quot;Python.simple_mesh.executor&quot;: &quot;Run&quot;,
&quot;Python.sketch.executor&quot;: &quot;Run&quot;,
&quot;Python.technical_drawing_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.vtk_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.vulkan.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;codeWithMe.voiceChat.enabledByDefault&quot;: &quot;false&quot;,
&quot;git-widget-placeholder&quot;: &quot;feature/occ-migration&quot;,
&quot;last_opened_file_path&quot;: &quot;/Volumes/Data_drive/Programming/fluency/src/fluency&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;project.propVCSSupport.DirectoryMappings&quot;
}
}]]></component>
}</component>
<component name="QodanaReportsService">
<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" />
-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.SetDisplayMode(1) # shaded
try:
ais.SetTransparency(0.65)
ais.SetTransparency(0.2)
except Exception:
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:
ais.SetPolygonOffsets(3, 1.0, -0.5)
except Exception:
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
if self._view is not None:
self._view.Redraw()
+104 -12
View File
@@ -8,7 +8,7 @@ import os
from datetime import datetime
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 (
QAction,
QColor,
@@ -1383,6 +1383,10 @@ def _replay_body_features(
"skipping chamfer"
)
continue
geom = kernel.chamfer(geom, feat.radius, edges=edges)
if geom is None:
return None
continue
if feat.operation in ("array", "pattern"):
# Pattern needs no sketch — it repeats the running solid.
@@ -1719,9 +1723,12 @@ class MainWindow(QMainWindow):
# selectable axis / centre / plane from the 3D viewport.
self._sketch_gizmo_selection: Optional[dict] = 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
# 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
# selected body light-blue; save its original colour to restore later.
self._body_highlight_id: Optional[str] = None
@@ -2685,6 +2692,12 @@ class MainWindow(QMainWindow):
"""
if feat.operation in ("cut", "union"):
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.
geom = _replay_body_features(
self._kernel, body, features[: index + 1],
@@ -2748,6 +2761,80 @@ class MainWindow(QMainWindow):
return None
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 ─────────────────────────────────────────
@@ -2777,10 +2864,9 @@ class MainWindow(QMainWindow):
"""Enable 'Del Op' / 'Mirror Op', highlight body or selected op in 3D."""
item = self._operations_list.currentItem()
# Restore the body we hid for the previous operation highlight.
if self._op_highlight_body_id is not None:
self._viewer_3d.set_visibility(self._op_highlight_body_id, True)
self._op_highlight_body_id = None
# Drop the previous highlight and any pending auto-clear so rapid
# re-selection always starts from a clean state.
self._op_highlight_timer.stop()
self._viewer_3d.clear_operation_highlight()
# Restore the previous body colour highlight.
@@ -2805,18 +2891,24 @@ class MainWindow(QMainWindow):
body, features, index, feat
)
if shape is not None:
if body.render_object is not None and body.visible:
self._viewer_3d.set_visibility(
body.render_object, False
)
self._op_highlight_body_id = body.render_object
# Overlay the operation geometry on the still-visible
# body as a hot-pink flash. The overlay is
# non-selectable, so face/edge/vertex picking keeps
# working while it shows.
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:
logger.debug("op highlight replay failed", exc_info=True)
else:
# Base operation (or non-selectable) — highlight 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:
"""Mirror the body at the selected operation's point in the feature history.