- UI refinement, button position ui file as source no dirty drafting anymore
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
# Fluency CAD — Agent Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Fluency CAD 2.0** is a parametric CAD application built on **OpenCASCADE Technology (OCCT)** with a modern **pygfx-based 3D renderer**. It provides 2D sketching with SolveSpace constraint solving, boolean operations, STEP/IGES/STL import/export, and exact BRep geometry.
|
||||
|
||||
- Language: **Python 3.10+**
|
||||
- GUI: **PySide6** (Qt6)
|
||||
- Geometry Kernel: **OCP** (cadquery-ocp — OpenCASCADE Python bindings)
|
||||
- Constraint Solver: **python_solvespace**
|
||||
- Renderer: **pygfx** (WebGPU) + **OCCRenderer** (native OCC AIS display)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/fluency/
|
||||
├── __init__.py # Package entry (version, imports)
|
||||
├── main.py # Application entry point, MainWindow, Sketch2DWidget (~5000 lines)
|
||||
├── sketch_solver.py # SolveSpace constraint solver wrapper (legacy)
|
||||
├── geometry/
|
||||
│ └── base.py # Abstract interfaces: GeometryKernel, SketchInterface, data classes
|
||||
├── geometry_occ/
|
||||
│ ├── kernel.py # OCGeometryKernel (extrude, boolean, fillet, import/export, mesh)
|
||||
│ └── sketch.py # OCCSketch with SolveSpace integration (face detection, constraints)
|
||||
├── models/
|
||||
│ └── data_model.py # Project, Component, Sketch, Body dataclasses
|
||||
├── rendering/
|
||||
│ ├── base.py # Abstract Renderer interface
|
||||
│ ├── occ_renderer.py # OCC AIS renderer (preferred — smooth BRep display)
|
||||
│ └── pygfx_renderer.py # Legacy pygfx renderer
|
||||
├── utils/ # Utility modules
|
||||
└── widgets/ # Custom widgets
|
||||
tests/
|
||||
└── test_geometry.py # Comprehensive test suite (52+ tests)
|
||||
```
|
||||
|
||||
### Key Classes & Responsibilities
|
||||
|
||||
| Class | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| `OCGeometryKernel` | `kernel.py` | OCC shape ops: extrude, boolean, fillet, mesh, import/export |
|
||||
| `OCCSketch` | `sketch.py` | 2D sketch with SolveSpace solver, face detection, workplane |
|
||||
| `OCCSketchEntity` | `sketch.py` | Entity (point/line/circle/arc) with solver handle, is_construction, is_external |
|
||||
| `Sketch2DWidget` | `main.py` | Qt widget for interactive 2D sketching (draw, snap, constrain) |
|
||||
| `MainWindow` | `main.py` | Main application window, toolbars, 3D viewer, operations |
|
||||
| `OCCRenderer` | `occ_renderer.py` | Native OCC AIS display (shaded + edges, face pick) |
|
||||
| `Sketch` | `data_model.py` | Data model: workplane, occ_sketch ref, source_body_id |
|
||||
| `Body` | `data_model.py` | 3D solid with geometry, visibility, render object |
|
||||
| `Component` | `data_model.py` | Container for sketches and bodies |
|
||||
| `Project` | `data_model.py` | Top-level container with kernel |
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User draws in Sketch2DWidget
|
||||
→ OCCSketch entities created in solver
|
||||
→ Constraint solving (python_solvespace)
|
||||
→ OCCSketch.get_geometry() → detect_faces() → build_face_geometry()
|
||||
→ OCGeometryKernel.extrude() → BRepPrimAPI_MakePrism
|
||||
→ Boolean operations → Body added to Component
|
||||
→ OCCRenderer.add_shape() → AIS display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install editable
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run app
|
||||
python -m fluency.main
|
||||
|
||||
# Run tests (52 tests)
|
||||
python -m pytest tests/test_geometry.py -v
|
||||
|
||||
# Run single test
|
||||
python -m pytest tests/test_geometry.py::TestOCCSketch::test_workplane_extrude_with_hole -xvs
|
||||
|
||||
# Quck geometry test (raw OCC, no Qt)
|
||||
python -c "from fluency.geometry_occ.sketch import OCCSketch; ..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### General
|
||||
- Line length: **100 chars** (black/ruff config)
|
||||
- Target Python: **3.10+** (uses `from __future__ import annotations`, walrus, pattern matching)
|
||||
- Docstrings: Google/NumPy style preferred
|
||||
- Logging: `logger = logging.getLogger(__name__)` with `logging.DEBUG` level
|
||||
|
||||
### OCC / OCP
|
||||
- Always use `is not None` for OCP objects — `TopoDS_Shape.__bool__` can be falsy even for valid shapes
|
||||
- `BRepBuilderAPI_MakeFace.Add(wire)` expects a `TopoDS_Wire`. `wire.Reversed()` returns `TopoDS_Shape` → cast via `_TopoDS.Wire_s(wire.Reversed())`
|
||||
- Face normal direction: check `face.Orientation()` vs `TopAbs_REVERSED` — a REVERSED face's outward normal is the NEGATION of the surface axis
|
||||
- `TopoDS_Wire_s(shape)`, `TopoDS_Face_s(shape)` — use `_s` suffix from OCP for downcasts
|
||||
- Mesh: `BRepMesh_IncrementalMesh(shape, tol, False, 0.15, True)` — default deflection 0.15 rad for smooth curves
|
||||
|
||||
### Extrude / Cut Workflow
|
||||
- Snapshot `list(self._current_component.bodies.items())` **BEFORE** `add_body()` — the new body must not be in the target set
|
||||
- Cut targets the **source body** (`sketch._source_body_id` from face pick), not `bodies[0]`
|
||||
- The fix: apply boolean to **target** geometry, then remove tool body
|
||||
- Plain extrude with holes: inner wires must have **OPPOSITE** geometric winding to the outer wire (see `build_face_geometry` and `_loop_signed_area`)
|
||||
|
||||
### Sketch / Solvers
|
||||
- `python_solvespace` has NO remove API for entities/constraints. Deleting requires: drop from `_points`/`_lines`, prune `_constraint_log`, `_rebuild_solver()` (recreates entire system), `_rebuild_labels()`, re-solve
|
||||
- `_constraint_log`: each entry is `{"type": str, "ids": tuple[int,...], "params": tuple, "labels": set[str]}`
|
||||
- Constraint labels: stored on **point** entities for paintEvent rendering; rebuilt via `_rebuild_labels()`
|
||||
- Line constraints (`horizontal`/`vertical`/`parallel`/`perpendicular`) need the **line's** solver handle, not a point's. Use `_find_line_sketch_entity()` to get the correct handle
|
||||
- External entities (underlay): `is_external=True`, `is_construction=True`, fixed in solver (always `dragged`). Stored in `_external_entity_ids`, excluded from `_line_segments()`, `get_polygon_points()`, `get_closed_loops()`, `detect_faces()`, `get_geometry()`
|
||||
|
||||
### Face Detection
|
||||
- `get_closed_loops()`: uses snapped-coordinate graph (`_SNAP_TOL = 1e-4`) from line endpoint adjacency. Only accepts simple cycles (all nodes degree 2)
|
||||
- `detect_faces()`: even-odd nesting rule via `_loop_contains`. Even depth = outer boundary, odd = hole
|
||||
- `_loop_rep_point`: midpoint between centroid and first vertex. **Fragile** — can land inside a nested shape for certain geometries (e.g., a small hole near the centroid's direction from the first vertex)
|
||||
- `_loop_signed_area`: shoelace formula for polygons, `πr²` (positive = CCW) for circles
|
||||
|
||||
### Rendering
|
||||
- **OCCRenderer** is the main renderer (not pygfx). Uses `AIS_Shape`, `V3d_Viewer`, `AIS_InteractiveContext`
|
||||
- Face pick: `pick_planar_face(x, y)` → `MoveTo` → `DetectedShape` → `TopoDS_Face_s` → `BRepAdaptor_Surface` plane check
|
||||
- Highlight: `highlight_face(face)` creates a transparent AIS overlay; `clear_face_highlight()` removes it
|
||||
- Preview: `preview_shape(shape)` for live transparent extrude preview
|
||||
- Navigation: Left=orbit, Middle=pan, Wheel=zoom. **Right is RESERVED** — check user before reassigning
|
||||
|
||||
### Paint-Event Safety
|
||||
- Every constraint-tag rendering loop wraps each entry in `try/except` so a bad entry (dangling id, corrupted geometry) doesn't crash the entire paint event
|
||||
- `_point_world()` and `_entity_anchor()` return `None` (not raise) for malformed input
|
||||
|
||||
---
|
||||
|
||||
## Known Bugs & Fix Patterns
|
||||
|
||||
### 1. Hole Orientation in Extrusion (FIXED 2026-07-03)
|
||||
**Symptom**: Inner shapes (circle/triangle/slot) inside a rectangle become solid islands instead of holes when extruding, depending on drag direction.
|
||||
|
||||
**Root Cause**: `wire_loop` in `build_face_geometry` unconditionally reversed hole wires (`w.Reversed()`). When the outer polygon was CW-winding (e.g., dragging from top-left to bottom-right), the reversed inner had the SAME effective direction as the outer, making OCC treat it as solid.
|
||||
|
||||
**Fix**: Added `_loop_signed_area()` to compute geometric winding. Hole wires are only reversed when their natural winding matches the outer's (ensuring opposite winding for holes).
|
||||
|
||||
**Relevant code**: `sketch.py`, `build_face_geometry()` and `_loop_signed_area()`
|
||||
|
||||
### 2. _loop_rep_point Fragility (KNOWN)
|
||||
**Symptom**: Face detection fails when a nested shape contains the outer loop's representative point (midpoint between centroid and first vertex).
|
||||
|
||||
**Would-be fix**: Use a guaranteed-interior point (maximum inscribed circle center or perturbed centroid) instead of the centroid-first-vertex midpoint.
|
||||
|
||||
### 3. Extrude Cut / Target Selection (FIXED 2026-06-29)
|
||||
**Symptom**: Cut created a separate "cavity-shaped" body next to the original instead of modifying the target.
|
||||
|
||||
**Fix**: Boolean result stored on TARGET body geometry; tool body removed from component. Auto-target via `sketch._source_body_id`.
|
||||
|
||||
### 4. Workplane Preservation (FIXED 2026-06-29)
|
||||
**Symptom**: Sketch placed on a face lost its workplane after being added to component.
|
||||
|
||||
**Fix**: Copy `occ_sketch` workplane fields into `Sketch` dataclass BEFORE `apply_workplane()`.
|
||||
|
||||
---
|
||||
|
||||
## API Quirks
|
||||
|
||||
- **`QPoint(0,0)`**: falsy via `isNull()` in PySide6 → always use `is not None` for `Optional[QPoint]`
|
||||
- **`QMouseEvent`/`QPainterPath`**: live in `PySide6.QtGui` (NOT `QtCore`)
|
||||
- **`BRepBuilderAPI_MakeFace.Add()`**: needs `TopoDS_Wire`. `wire.Reversed()` returns `TopoDS_Shape` — cast via `TopoDS_Wire_s()`
|
||||
- **python_solvespace**: NO entity/constraint remove API — workaround via `_rebuild_solver()`. Parameters read via `solver.params(handle.params)` → returns `(x, y)` tuple
|
||||
- **OCGeometryKernel.extrude**: unwraps `OCCGeometryObject`, raw `TopoDS_Shape`, or cadquery `Workplane`. Always use `is not None` for the shape (not truthiness)
|
||||
- **Sketch._source_body_id**: dynamic attribute set on `Sketch` dataclass, set during face-pick flow
|
||||
- **`_get_shape(obj)`**: returns `obj.shape.wrapped` for `OCCGeometryObject`, `obj.shape` for raw shapes, `None` for empty. Use `is not None` guards everywhere
|
||||
|
||||
---
|
||||
|
||||
## Memory / Agent Context
|
||||
|
||||
This project has extensive Pi memory (hermes-memory) for:
|
||||
- `project="fluency"` with `target="failure"`: bugs, fixes, corrections, insights
|
||||
- `project="fluency"` with `target="memory"`: conventions, decisions, workflow patterns
|
||||
- Available skills: `fix-cad-app-pipeline`, `refactor-from-cadquery-to-ocp`
|
||||
|
||||
Key memory queries for debugging:
|
||||
- "hole orientation" → `_loop_signed_area` / `build_face_geometry` fix
|
||||
- "extrude cut auto-target" → cut/target body fix
|
||||
- "workplane preservation" → _add_sketch_to_component fix
|
||||
- "_loop_rep_point" → face detection fragility
|
||||
- "paint-event safety" → try/except per entry pattern
|
||||
- "solver rebuild" → delete workflow via _rebuild_solver
|
||||
- "face pick origin" → pick_planar_face face bbox centre
|
||||
|
||||
---
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
```python
|
||||
# Direct OCC test (no Qt)
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace
|
||||
from OCP.gp import gp_Pnt
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism
|
||||
from OCP.GProp import GProp_GProps; from OCP.BRepGProp import BRepGProp
|
||||
|
||||
# Build test shape, extrude, verify volume
|
||||
mp = BRepBuilderAPI_MakePolygon(); ...
|
||||
g = GProp_GProps(); BRepGProp.VolumeProperties_s(shape, g)
|
||||
assert abs(g.Mass() - expected) < 0.1
|
||||
```
|
||||
|
||||
```python
|
||||
# Sketch-based test
|
||||
from fluency.geometry_occ.sketch import OCCSketch
|
||||
from fluency.geometry_occ.kernel import OCGeometryKernel
|
||||
|
||||
sk = OCCSketch()
|
||||
# ... add points, lines, circles ...
|
||||
sk.solve()
|
||||
geom = sk.get_geometry()
|
||||
solid = OCGeometryKernel().extrude(geom, 10.0)
|
||||
```
|
||||
Reference in New Issue
Block a user