Fiexed highlighting of operations
This commit is contained in:
Generated
+45
-45
@@ -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">{
|
||||
"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>
|
||||
}</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" />
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user