diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 6e73b9d..01b94f1 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -6,9 +6,8 @@
-
+
-
@@ -306,7 +305,15 @@
1783108151676
-
+
+
+ 1783159860774
+
+
+
+ 1783159860774
+
+
diff --git a/agent.md b/agent.md
new file mode 100644
index 0000000..55d4198
--- /dev/null
+++ b/agent.md
@@ -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)
+```
diff --git a/gui.ui b/gui.ui
index 55daca4..4adb991 100644
--- a/gui.ui
+++ b/gui.ui
@@ -6,8 +6,8 @@
0
0
- 2192
- 1109
+ 1941
+ 1155
@@ -15,95 +15,221 @@
- -
-
+
-
+
+
+
+ 0
+ 50
+
+
+
+ Components in Assembly
+
+
+
+ -
+
+
+
+ 0
+ 50
+
+
+
+ Joint Tools
+
+
+
-
+
+
+
+ 50
+ 50
+
+
+
+
+ 50
+ 50
+
+
+
+ + Cnct
+
+
+
+ -
+
+
+
+ 50
+ 50
+
+
+
+
+ 50
+ 50
+
+
+
+ - Cnct
+
+
+
+
+
+
+ -
+
-
+
0
0
-
- 0
-
-
-
- Sketch
-
-
-
-
-
- Code
-
-
-
-
-
-
- -
-
-
- Executive
-
-
-
-
-
-
- Load Code
-
-
-
- -
-
-
- Save code
-
-
-
- -
-
-
- Apply Code
-
-
-
- -
-
-
- Delete Code
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 4
-
-
-
-
- 12
-
-
- Model Viewer
+ Snapping Points
-
-
- -1
-
+
12
+
+ 12
+
+
+ 12
+
+
+ 12
+
+
-
+
+
+ Snp Dst
+
+
+
+ -
+
+
+ Vert
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Angl Stps
+
+
+
+ -
+
+
+ mm
+
+
+ 30
+
+
+ 10
+
+
+
+ -
+
+
+ Grid
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Horiz
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ °
+
+
+ 180
+
+
+ 15
+
+
+
+ -
+
+
+ Pnt
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ MidP
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Angles
+
+
+ true
+
+
+ false
+
+
+
@@ -158,7 +284,72 @@
- -
+
-
+
+
+
+ 0
+ 50
+
+
+
+ Component Tools
+
+
+
-
+
+
+
+ 50
+ 50
+
+
+
+
+ 50
+ 50
+
+
+
+ New
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 50
+
+
+
+
+ 50
+ 50
+
+
+
+ Qt::LeftToRight
+
+
+ Del
+
+
+
+
+
+
+ -
@@ -171,7 +362,122 @@
- -
+
-
+
+
+
+ 200
+ 16777215
+
+
+
+ Workplanes
+
+
+
-
+
+
+ <W>orking Plane at 0, 0, 0
+
+
+ WP Origin
+
+
+ W
+
+
+
+ -
+
+
+ Working Plane >P<rojection at selected edges face
+
+
+ WP Face
+
+
+ P
+
+
+ true
+
+
+
+ -
+
+
+ Flip >N<ormal of projected mesh.
+
+
+ WP Flip
+
+
+ N
+
+
+
+ -
+
+
+ false
+
+
+ Show / hide the construction lines projected from the source face
+
+
+ Underlay
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ false
+
+
+ Forget the picked source face (keep the workplane)
+
+
+ ClrFace
+
+
+
+ -
+
+
+ false
+
+
+ Convert projected construction lines into real sketch geometry
+
+
+ ToSketch
+
+
+
+ -
+
+
+ Create a new independent workplane (datum plane)
+
+
+ WP New
+
+
+ Shift+W
+
+
+
+
+
+
+ -
@@ -265,7 +571,7 @@
- -
+
-
@@ -359,60 +665,7 @@
- -
-
-
-
- 0
- 50
-
-
-
- Assembly Tools
-
-
-
-
-
-
-
- 50
- 50
-
-
-
-
- 50
- 50
-
-
-
- + Cnct
-
-
-
- -
-
-
-
- 50
- 50
-
-
-
-
- 50
- 50
-
-
-
- - Cnct
-
-
-
-
-
-
- -
+
-
Export
@@ -425,11 +678,25 @@
+ -
+
+
+ STEP
+
+
+
+ -
+
+
+ IGES
+
+
+
- -
-
+
-
+
0
@@ -437,11 +704,90 @@
- Component Tools
+ Assembly
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+
+ Sketch
+
+
+
+
+
+ Code
+
+
+
-
+
+
+ -
+
+
+ Executive
+
+
+
-
+
+
+ Load Code
+
+
+
+ -
+
+
+ Save code
+
+
+
+ -
+
+
+ Apply Code
+
+
+
+ -
+
+
+ Delete Code
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 50
+
+
+
+ Assembly Tools
+
+
-
-
+
50
@@ -455,12 +801,12 @@
- New
+ Add
-
-
+
true
@@ -486,196 +832,40 @@
Qt::LeftToRight
- Del
+ Rem
- -
-
-
-
- 200
- 16777215
-
-
-
- Workplanes
-
-
-
-
-
-
- <W>orking Plane at 0, 0, 0
-
-
- WP Origin
-
-
- W
-
-
-
- -
-
-
- Working Plane >P<rojection at selected edges face
-
-
- WP Face
-
-
- P
-
-
-
- -
-
-
- Flip >N<ormal of projected mesh.
-
-
- WP Flip
-
-
- N
-
-
-
- -
-
-
- >M<ove projected mesh workplane
-
-
- WP Mve
-
-
- M
-
-
-
-
-
-
- -
-
+
-
+
-
+
0
- 0
+ 4
-
-
- 200
- 16777215
-
+
+
+ 12
+
- Drawing
+ Model Viewer
-
-
- 10
+
+
+ -1
+
+
+ 12
-
-
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Circle
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Slot
-
-
- true
-
-
- false
-
-
- 98
-
-
-
- -
-
-
- Line >S<egment
-
-
- Line
-
-
- S
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Rctgl
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Cstrct
-
-
- true
-
-
-
- -
-
-
- Snap
-
-
- true
-
-
- true
-
-
-
- -
+
-
@@ -694,19 +884,6 @@
-
-
-
- Symetrc
-
-
- true
-
-
- false
-
-
-
- -
Vertical Constrain
@@ -722,29 +899,13 @@
- -
-
-
- Constrain Line perpendicular to another line.
-
-
- Perp_Lne
-
-
- true
-
-
- false
-
-
-
-
-
+
- Horizontal Constrain
+ Point to Middle Point Constrain
- Horiz
+ Pt_Mid_L
true
@@ -754,39 +915,7 @@
- -
-
-
- Poin to Point Constrain
-
-
- Pt_Pt
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Point to Line Constrain
-
-
- Pt_Lne
-
-
- true
-
-
- false
-
-
-
- -
+
-
Dimension of Line of Distance from Point to Line
@@ -805,13 +934,74 @@
- -
-
+
-
+
- Point to Middle Point Constrain
+ Point to Line Constrain
- Pt_Mid_L
+ Pt_Lne
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Constrain Line perpendicular to another line.
+
+
+ Perp_Lne
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Symetrc
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Horizontal Constrain
+
+
+ Horiz
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Poin to Point Constrain
+
+
+ Pt_Pt
true
@@ -824,10 +1014,10 @@
- -
-
+
-
+
-
+
0
0
@@ -838,176 +1028,125 @@
16777215
-
- QTabWidget::South
+
+ Drawing
-
- 0
-
-
-
- Setg 1
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Snapping Points
-
-
-
- 2
-
-
- 2
-
-
- 2
-
-
- 2
-
-
-
-
-
- Snp Dst
-
-
-
- -
-
-
- Vert
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- Angl Stps
-
-
-
- -
-
-
- mm
-
-
- 30
-
-
- 10
-
-
-
- -
-
-
- Grid
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Horiz
-
-
- true
-
-
- false
-
-
-
- -
-
-
- °
-
-
- 180
-
-
- 15
-
-
-
- -
-
-
- Pnt
-
-
- true
-
-
- false
-
-
-
- -
-
-
- MidP
-
-
- true
-
-
- false
-
-
-
- -
-
-
- Angles
-
-
- true
-
-
- false
-
-
-
-
-
-
-
-
-
-
- Setg 2
-
-
+
+
+ 10
+
+ -
+
+
+ Arc
+
+
+ true
+
+
+
+ -
+
+
+ Rctgl
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Circle
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Cstrct
+
+
+ true
+
+
+
+ -
+
+
+ Snap
+
+
+
+ 13
+ 16
+
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Line
+
+
+ S
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Slot
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Offset selected sketch face (duplicate + offset boundary)
+
+
+ Offst
+
+
+
+
@@ -1017,7 +1156,7 @@
0
0
- 2192
+ 1941
24
diff --git a/gui_ui.py b/gui_ui.py
new file mode 100644
index 0000000..2161302
--- /dev/null
+++ b/gui_ui.py
@@ -0,0 +1,802 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'gui.ui'
+##
+## Created by: Qt User Interface Compiler version 6.10.2
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
+ QCursor, QFont, QFontDatabase, QGradient,
+ QIcon, QImage, QKeySequence, QLinearGradient,
+ QPainter, QPalette, QPixmap, QRadialGradient,
+ QTransform)
+from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QGroupBox,
+ QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
+ QMainWindow, QMenu, QMenuBar, QPushButton,
+ QSizePolicy, QSpinBox, QStatusBar, QTabWidget,
+ QTextEdit, QVBoxLayout, QWidget)
+
+class Ui_fluencyCAD(object):
+ def setupUi(self, fluencyCAD):
+ if not fluencyCAD.objectName():
+ fluencyCAD.setObjectName(u"fluencyCAD")
+ fluencyCAD.resize(1941, 1155)
+ self.actionNew_Project = QAction(fluencyCAD)
+ self.actionNew_Project.setObjectName(u"actionNew_Project")
+ self.actionLoad_Project = QAction(fluencyCAD)
+ self.actionLoad_Project.setObjectName(u"actionLoad_Project")
+ self.actionRecent = QAction(fluencyCAD)
+ self.actionRecent.setObjectName(u"actionRecent")
+ self.centralwidget = QWidget(fluencyCAD)
+ self.centralwidget.setObjectName(u"centralwidget")
+ self.gridLayout = QGridLayout(self.centralwidget)
+ self.gridLayout.setObjectName(u"gridLayout")
+ self.compos_in_assembly_box = QGroupBox(self.centralwidget)
+ self.compos_in_assembly_box.setObjectName(u"compos_in_assembly_box")
+ self.compos_in_assembly_box.setMinimumSize(QSize(0, 50))
+
+ self.gridLayout.addWidget(self.compos_in_assembly_box, 12, 2, 1, 1)
+
+ self.joint_tools = QGroupBox(self.centralwidget)
+ self.joint_tools.setObjectName(u"joint_tools")
+ self.joint_tools.setMinimumSize(QSize(0, 50))
+ self.gridLayout_10 = QGridLayout(self.joint_tools)
+ self.gridLayout_10.setObjectName(u"gridLayout_10")
+ self.pb_add_connector = QPushButton(self.joint_tools)
+ self.pb_add_connector.setObjectName(u"pb_add_connector")
+ self.pb_add_connector.setMinimumSize(QSize(50, 50))
+ self.pb_add_connector.setMaximumSize(QSize(50, 50))
+
+ self.gridLayout_10.addWidget(self.pb_add_connector, 0, 0, 1, 1)
+
+ self.pb_remove_connector = QPushButton(self.joint_tools)
+ self.pb_remove_connector.setObjectName(u"pb_remove_connector")
+ self.pb_remove_connector.setMinimumSize(QSize(50, 50))
+ self.pb_remove_connector.setMaximumSize(QSize(50, 50))
+
+ self.gridLayout_10.addWidget(self.pb_remove_connector, 0, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.joint_tools, 12, 3, 1, 1)
+
+ self.groupBox_5 = QGroupBox(self.centralwidget)
+ self.groupBox_5.setObjectName(u"groupBox_5")
+ sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.groupBox_5.sizePolicy().hasHeightForWidth())
+ self.groupBox_5.setSizePolicy(sizePolicy)
+ self.gridLayout_11 = QGridLayout(self.groupBox_5)
+ self.gridLayout_11.setObjectName(u"gridLayout_11")
+ self.gridLayout_11.setContentsMargins(12, 12, 12, 12)
+ self.label = QLabel(self.groupBox_5)
+ self.label.setObjectName(u"label")
+
+ self.gridLayout_11.addWidget(self.label, 5, 0, 1, 1)
+
+ self.pb_snap_vert = QPushButton(self.groupBox_5)
+ self.pb_snap_vert.setObjectName(u"pb_snap_vert")
+ self.pb_snap_vert.setCheckable(True)
+ self.pb_snap_vert.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pb_snap_vert, 2, 1, 1, 1)
+
+ self.line_2 = QFrame(self.groupBox_5)
+ self.line_2.setObjectName(u"line_2")
+ self.line_2.setFrameShape(QFrame.Shape.HLine)
+ self.line_2.setFrameShadow(QFrame.Shadow.Sunken)
+
+ self.gridLayout_11.addWidget(self.line_2, 4, 0, 1, 2)
+
+ self.label_2 = QLabel(self.groupBox_5)
+ self.label_2.setObjectName(u"label_2")
+
+ self.gridLayout_11.addWidget(self.label_2, 5, 1, 1, 1)
+
+ self.spinbox_snap_distance = QSpinBox(self.groupBox_5)
+ self.spinbox_snap_distance.setObjectName(u"spinbox_snap_distance")
+ self.spinbox_snap_distance.setMaximum(30)
+ self.spinbox_snap_distance.setValue(10)
+
+ self.gridLayout_11.addWidget(self.spinbox_snap_distance, 6, 0, 1, 1)
+
+ self.pushButton_7 = QPushButton(self.groupBox_5)
+ self.pushButton_7.setObjectName(u"pushButton_7")
+ self.pushButton_7.setCheckable(True)
+ self.pushButton_7.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pushButton_7, 3, 0, 1, 1)
+
+ self.pb_snap_horiz = QPushButton(self.groupBox_5)
+ self.pb_snap_horiz.setObjectName(u"pb_snap_horiz")
+ self.pb_snap_horiz.setCheckable(True)
+ self.pb_snap_horiz.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pb_snap_horiz, 2, 0, 1, 1)
+
+ self.spinbox_angle_steps = QSpinBox(self.groupBox_5)
+ self.spinbox_angle_steps.setObjectName(u"spinbox_angle_steps")
+ self.spinbox_angle_steps.setMaximum(180)
+ self.spinbox_angle_steps.setValue(15)
+
+ self.gridLayout_11.addWidget(self.spinbox_angle_steps, 6, 1, 1, 1)
+
+ self.pushButton_8 = QPushButton(self.groupBox_5)
+ self.pushButton_8.setObjectName(u"pushButton_8")
+ self.pushButton_8.setCheckable(True)
+ self.pushButton_8.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pushButton_8, 0, 0, 1, 1)
+
+ self.pb_snap_midp = QPushButton(self.groupBox_5)
+ self.pb_snap_midp.setObjectName(u"pb_snap_midp")
+ self.pb_snap_midp.setCheckable(True)
+ self.pb_snap_midp.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pb_snap_midp, 0, 1, 1, 1)
+
+ self.pb_snap_angle = QPushButton(self.groupBox_5)
+ self.pb_snap_angle.setObjectName(u"pb_snap_angle")
+ self.pb_snap_angle.setCheckable(True)
+ self.pb_snap_angle.setAutoExclusive(False)
+
+ self.gridLayout_11.addWidget(self.pb_snap_angle, 3, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.groupBox_5, 3, 0, 1, 1)
+
+ self.groupBox = QGroupBox(self.centralwidget)
+ self.groupBox.setObjectName(u"groupBox")
+ self.gridLayout_3 = QGridLayout(self.groupBox)
+ self.gridLayout_3.setObjectName(u"gridLayout_3")
+ self.pb_revop = QPushButton(self.groupBox)
+ self.pb_revop.setObjectName(u"pb_revop")
+
+ self.gridLayout_3.addWidget(self.pb_revop, 2, 1, 1, 1)
+
+ self.pb_extrdop = QPushButton(self.groupBox)
+ self.pb_extrdop.setObjectName(u"pb_extrdop")
+
+ self.gridLayout_3.addWidget(self.pb_extrdop, 0, 0, 1, 1)
+
+ self.pb_arrayop = QPushButton(self.groupBox)
+ self.pb_arrayop.setObjectName(u"pb_arrayop")
+
+ self.gridLayout_3.addWidget(self.pb_arrayop, 2, 0, 1, 1)
+
+ self.pb_cutop = QPushButton(self.groupBox)
+ self.pb_cutop.setObjectName(u"pb_cutop")
+
+ self.gridLayout_3.addWidget(self.pb_cutop, 0, 1, 1, 1)
+
+ self.pb_combop = QPushButton(self.groupBox)
+ self.pb_combop.setObjectName(u"pb_combop")
+
+ self.gridLayout_3.addWidget(self.pb_combop, 1, 0, 1, 1)
+
+ self.pb_moveop = QPushButton(self.groupBox)
+ self.pb_moveop.setObjectName(u"pb_moveop")
+
+ self.gridLayout_3.addWidget(self.pb_moveop, 1, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.groupBox, 0, 3, 1, 1, Qt.AlignTop)
+
+ self.compo_tool_box = QGroupBox(self.centralwidget)
+ self.compo_tool_box.setObjectName(u"compo_tool_box")
+ self.compo_tool_box.setMinimumSize(QSize(0, 50))
+ self.gridLayout_9 = QGridLayout(self.compo_tool_box)
+ self.gridLayout_9.setObjectName(u"gridLayout_9")
+ self.pb_new_compo = QPushButton(self.compo_tool_box)
+ self.pb_new_compo.setObjectName(u"pb_new_compo")
+ self.pb_new_compo.setMinimumSize(QSize(50, 50))
+ self.pb_new_compo.setMaximumSize(QSize(50, 50))
+
+ self.gridLayout_9.addWidget(self.pb_new_compo, 0, 0, 1, 1)
+
+ self.pb_del_compo = QPushButton(self.compo_tool_box)
+ self.pb_del_compo.setObjectName(u"pb_del_compo")
+ self.pb_del_compo.setEnabled(True)
+ sizePolicy.setHeightForWidth(self.pb_del_compo.sizePolicy().hasHeightForWidth())
+ self.pb_del_compo.setSizePolicy(sizePolicy)
+ self.pb_del_compo.setMinimumSize(QSize(50, 50))
+ self.pb_del_compo.setMaximumSize(QSize(50, 50))
+ self.pb_del_compo.setLayoutDirection(Qt.LeftToRight)
+
+ self.gridLayout_9.addWidget(self.pb_del_compo, 0, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.compo_tool_box, 11, 0, 1, 1)
+
+ self.compo_box = QGroupBox(self.centralwidget)
+ self.compo_box.setObjectName(u"compo_box")
+ self.compo_box.setMinimumSize(QSize(0, 50))
+
+ self.gridLayout.addWidget(self.compo_box, 11, 1, 1, 2)
+
+ self.groupBox_9 = QGroupBox(self.centralwidget)
+ self.groupBox_9.setObjectName(u"groupBox_9")
+ self.groupBox_9.setMaximumSize(QSize(200, 16777215))
+ self.gridLayout_7 = QGridLayout(self.groupBox_9)
+ self.gridLayout_7.setObjectName(u"gridLayout_7")
+ self.pb_origin_wp = QPushButton(self.groupBox_9)
+ self.pb_origin_wp.setObjectName(u"pb_origin_wp")
+
+ self.gridLayout_7.addWidget(self.pb_origin_wp, 0, 0, 1, 1)
+
+ self.pb_origin_face = QPushButton(self.groupBox_9)
+ self.pb_origin_face.setObjectName(u"pb_origin_face")
+ self.pb_origin_face.setCheckable(True)
+
+ self.gridLayout_7.addWidget(self.pb_origin_face, 0, 1, 1, 1)
+
+ self.pb_flip_face = QPushButton(self.groupBox_9)
+ self.pb_flip_face.setObjectName(u"pb_flip_face")
+
+ self.gridLayout_7.addWidget(self.pb_flip_face, 1, 0, 1, 1)
+
+ self.pb_underlay = QPushButton(self.groupBox_9)
+ self.pb_underlay.setObjectName(u"pb_underlay")
+ self.pb_underlay.setEnabled(False)
+ self.pb_underlay.setCheckable(True)
+ self.pb_underlay.setChecked(True)
+
+ self.gridLayout_7.addWidget(self.pb_underlay, 3, 0, 1, 1)
+
+ self.pb_clr_face = QPushButton(self.groupBox_9)
+ self.pb_clr_face.setObjectName(u"pb_clr_face")
+ self.pb_clr_face.setEnabled(False)
+
+ self.gridLayout_7.addWidget(self.pb_clr_face, 3, 1, 1, 1)
+
+ self.pb_to_sketch = QPushButton(self.groupBox_9)
+ self.pb_to_sketch.setObjectName(u"pb_to_sketch")
+ self.pb_to_sketch.setEnabled(False)
+
+ self.gridLayout_7.addWidget(self.pb_to_sketch, 4, 0, 1, 2)
+
+ self.pb_wp_new = QPushButton(self.groupBox_9)
+ self.pb_wp_new.setObjectName(u"pb_wp_new")
+
+ self.gridLayout_7.addWidget(self.pb_wp_new, 1, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.groupBox_9, 0, 0, 1, 1)
+
+ self.groupBox_10 = QGroupBox(self.centralwidget)
+ self.groupBox_10.setObjectName(u"groupBox_10")
+ sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
+ sizePolicy1.setHorizontalStretch(0)
+ sizePolicy1.setVerticalStretch(0)
+ sizePolicy1.setHeightForWidth(self.groupBox_10.sizePolicy().hasHeightForWidth())
+ self.groupBox_10.setSizePolicy(sizePolicy1)
+ self.groupBox_10.setMaximumSize(QSize(200, 16777215))
+ self.verticalLayout_6 = QVBoxLayout(self.groupBox_10)
+ self.verticalLayout_6.setObjectName(u"verticalLayout_6")
+ self.verticalLayout_6.setContentsMargins(5, 5, 5, 5)
+ self.body_list = QListWidget(self.groupBox_10)
+ self.body_list.setObjectName(u"body_list")
+ self.body_list.setSelectionRectVisible(True)
+
+ self.verticalLayout_6.addWidget(self.body_list)
+
+ self.groupBox_8 = QGroupBox(self.groupBox_10)
+ self.groupBox_8.setObjectName(u"groupBox_8")
+ sizePolicy.setHeightForWidth(self.groupBox_8.sizePolicy().hasHeightForWidth())
+ self.groupBox_8.setSizePolicy(sizePolicy)
+ self.groupBox_8.setMaximumSize(QSize(200, 16777215))
+ self.gridLayout_8 = QGridLayout(self.groupBox_8)
+ self.gridLayout_8.setObjectName(u"gridLayout_8")
+ self.gridLayout_8.setContentsMargins(2, 2, 2, 2)
+ self.pb_del_body = QPushButton(self.groupBox_8)
+ self.pb_del_body.setObjectName(u"pb_del_body")
+
+ self.gridLayout_8.addWidget(self.pb_del_body, 0, 2, 1, 1)
+
+ self.pb_update_body = QPushButton(self.groupBox_8)
+ self.pb_update_body.setObjectName(u"pb_update_body")
+
+ self.gridLayout_8.addWidget(self.pb_update_body, 0, 0, 1, 1)
+
+ self.pb_edt_sktch_3 = QPushButton(self.groupBox_8)
+ self.pb_edt_sktch_3.setObjectName(u"pb_edt_sktch_3")
+
+ self.gridLayout_8.addWidget(self.pb_edt_sktch_3, 0, 1, 1, 1)
+
+
+ self.verticalLayout_6.addWidget(self.groupBox_8)
+
+
+ self.gridLayout.addWidget(self.groupBox_10, 8, 3, 3, 1)
+
+ self.groupBox_11 = QGroupBox(self.centralwidget)
+ self.groupBox_11.setObjectName(u"groupBox_11")
+ sizePolicy1.setHeightForWidth(self.groupBox_11.sizePolicy().hasHeightForWidth())
+ self.groupBox_11.setSizePolicy(sizePolicy1)
+ self.groupBox_11.setMaximumSize(QSize(200, 16777215))
+ self.verticalLayout_7 = QVBoxLayout(self.groupBox_11)
+ self.verticalLayout_7.setObjectName(u"verticalLayout_7")
+ self.verticalLayout_7.setContentsMargins(5, 5, 5, 5)
+ self.sketch_list = QListWidget(self.groupBox_11)
+ self.sketch_list.setObjectName(u"sketch_list")
+ sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ sizePolicy2.setHorizontalStretch(0)
+ sizePolicy2.setVerticalStretch(0)
+ sizePolicy2.setHeightForWidth(self.sketch_list.sizePolicy().hasHeightForWidth())
+ self.sketch_list.setSizePolicy(sizePolicy2)
+ self.sketch_list.setSelectionRectVisible(True)
+
+ self.verticalLayout_7.addWidget(self.sketch_list)
+
+ self.groupBox_6 = QGroupBox(self.groupBox_11)
+ self.groupBox_6.setObjectName(u"groupBox_6")
+ sizePolicy.setHeightForWidth(self.groupBox_6.sizePolicy().hasHeightForWidth())
+ self.groupBox_6.setSizePolicy(sizePolicy)
+ self.gridLayout_6 = QGridLayout(self.groupBox_6)
+ self.gridLayout_6.setObjectName(u"gridLayout_6")
+ self.gridLayout_6.setContentsMargins(2, 2, 2, 2)
+ self.pb_edt_sktch = QPushButton(self.groupBox_6)
+ self.pb_edt_sktch.setObjectName(u"pb_edt_sktch")
+
+ self.gridLayout_6.addWidget(self.pb_edt_sktch, 1, 1, 1, 1)
+
+ self.pb_nw_sktch = QPushButton(self.groupBox_6)
+ self.pb_nw_sktch.setObjectName(u"pb_nw_sktch")
+
+ self.gridLayout_6.addWidget(self.pb_nw_sktch, 1, 0, 1, 1)
+
+ self.pb_del_sketch = QPushButton(self.groupBox_6)
+ self.pb_del_sketch.setObjectName(u"pb_del_sketch")
+
+ self.gridLayout_6.addWidget(self.pb_del_sketch, 1, 2, 1, 1)
+
+
+ self.verticalLayout_7.addWidget(self.groupBox_6)
+
+
+ self.gridLayout.addWidget(self.groupBox_11, 8, 0, 3, 1)
+
+ self.groupBox_4 = QGroupBox(self.centralwidget)
+ self.groupBox_4.setObjectName(u"groupBox_4")
+ self.verticalLayout_2 = QVBoxLayout(self.groupBox_4)
+ self.verticalLayout_2.setObjectName(u"verticalLayout_2")
+ self.pushButton_2 = QPushButton(self.groupBox_4)
+ self.pushButton_2.setObjectName(u"pushButton_2")
+
+ self.verticalLayout_2.addWidget(self.pushButton_2)
+
+ self.pb_export_step = QPushButton(self.groupBox_4)
+ self.pb_export_step.setObjectName(u"pb_export_step")
+
+ self.verticalLayout_2.addWidget(self.pb_export_step)
+
+ self.pb_export_iges = QPushButton(self.groupBox_4)
+ self.pb_export_iges.setObjectName(u"pb_export_iges")
+
+ self.verticalLayout_2.addWidget(self.pb_export_iges)
+
+
+ self.gridLayout.addWidget(self.groupBox_4, 2, 3, 1, 1)
+
+ self.assembly_box = QGroupBox(self.centralwidget)
+ self.assembly_box.setObjectName(u"assembly_box")
+ self.assembly_box.setMinimumSize(QSize(0, 50))
+
+ self.gridLayout.addWidget(self.assembly_box, 12, 1, 1, 1)
+
+ self.InputTab = QTabWidget(self.centralwidget)
+ self.InputTab.setObjectName(u"InputTab")
+ sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
+ sizePolicy3.setHorizontalStretch(0)
+ sizePolicy3.setVerticalStretch(0)
+ sizePolicy3.setHeightForWidth(self.InputTab.sizePolicy().hasHeightForWidth())
+ self.InputTab.setSizePolicy(sizePolicy3)
+ self.sketch_tab = QWidget()
+ self.sketch_tab.setObjectName(u"sketch_tab")
+ self.verticalLayout_4 = QVBoxLayout(self.sketch_tab)
+ self.verticalLayout_4.setObjectName(u"verticalLayout_4")
+ self.InputTab.addTab(self.sketch_tab, "")
+ self.code_tab = QWidget()
+ self.code_tab.setObjectName(u"code_tab")
+ self.verticalLayout = QVBoxLayout(self.code_tab)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.textEdit = QTextEdit(self.code_tab)
+ self.textEdit.setObjectName(u"textEdit")
+
+ self.verticalLayout.addWidget(self.textEdit)
+
+ self.groupBox_7 = QGroupBox(self.code_tab)
+ self.groupBox_7.setObjectName(u"groupBox_7")
+ self.gridLayout_5 = QGridLayout(self.groupBox_7)
+ self.gridLayout_5.setObjectName(u"gridLayout_5")
+ self.pushButton_5 = QPushButton(self.groupBox_7)
+ self.pushButton_5.setObjectName(u"pushButton_5")
+
+ self.gridLayout_5.addWidget(self.pushButton_5, 2, 0, 1, 1)
+
+ self.pushButton_4 = QPushButton(self.groupBox_7)
+ self.pushButton_4.setObjectName(u"pushButton_4")
+
+ self.gridLayout_5.addWidget(self.pushButton_4, 2, 1, 1, 1)
+
+ self.pb_apply_code = QPushButton(self.groupBox_7)
+ self.pb_apply_code.setObjectName(u"pb_apply_code")
+
+ self.gridLayout_5.addWidget(self.pb_apply_code, 1, 0, 1, 1)
+
+ self.pushButton = QPushButton(self.groupBox_7)
+ self.pushButton.setObjectName(u"pushButton")
+
+ self.gridLayout_5.addWidget(self.pushButton, 1, 1, 1, 1)
+
+
+ self.verticalLayout.addWidget(self.groupBox_7)
+
+ self.InputTab.addTab(self.code_tab, "")
+
+ self.gridLayout.addWidget(self.InputTab, 0, 1, 11, 1)
+
+ self.assembly_tools = QGroupBox(self.centralwidget)
+ self.assembly_tools.setObjectName(u"assembly_tools")
+ self.assembly_tools.setMinimumSize(QSize(0, 50))
+ self.gridLayout_12 = QGridLayout(self.assembly_tools)
+ self.gridLayout_12.setObjectName(u"gridLayout_12")
+ self.pb_compo_to_assembly = QPushButton(self.assembly_tools)
+ self.pb_compo_to_assembly.setObjectName(u"pb_compo_to_assembly")
+ self.pb_compo_to_assembly.setMinimumSize(QSize(50, 50))
+ self.pb_compo_to_assembly.setMaximumSize(QSize(50, 50))
+
+ self.gridLayout_12.addWidget(self.pb_compo_to_assembly, 0, 0, 1, 1)
+
+ self.pb_remove_compo_from_assembly = QPushButton(self.assembly_tools)
+ self.pb_remove_compo_from_assembly.setObjectName(u"pb_remove_compo_from_assembly")
+ self.pb_remove_compo_from_assembly.setEnabled(True)
+ sizePolicy.setHeightForWidth(self.pb_remove_compo_from_assembly.sizePolicy().hasHeightForWidth())
+ self.pb_remove_compo_from_assembly.setSizePolicy(sizePolicy)
+ self.pb_remove_compo_from_assembly.setMinimumSize(QSize(50, 50))
+ self.pb_remove_compo_from_assembly.setMaximumSize(QSize(50, 50))
+ self.pb_remove_compo_from_assembly.setLayoutDirection(Qt.LeftToRight)
+
+ self.gridLayout_12.addWidget(self.pb_remove_compo_from_assembly, 0, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.assembly_tools, 12, 0, 1, 1)
+
+ self.gl_box = QGroupBox(self.centralwidget)
+ self.gl_box.setObjectName(u"gl_box")
+ sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ sizePolicy4.setHorizontalStretch(0)
+ sizePolicy4.setVerticalStretch(4)
+ sizePolicy4.setHeightForWidth(self.gl_box.sizePolicy().hasHeightForWidth())
+ self.gl_box.setSizePolicy(sizePolicy4)
+ font = QFont()
+ font.setPointSize(12)
+ self.gl_box.setFont(font)
+ self.horizontalLayout_4 = QHBoxLayout(self.gl_box)
+#ifndef Q_OS_MAC
+ self.horizontalLayout_4.setSpacing(-1)
+#endif
+ self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
+ self.horizontalLayout_4.setContentsMargins(12, -1, -1, -1)
+
+ self.gridLayout.addWidget(self.gl_box, 0, 2, 11, 1)
+
+ self.groupBox_3 = QGroupBox(self.centralwidget)
+ self.groupBox_3.setObjectName(u"groupBox_3")
+ sizePolicy.setHeightForWidth(self.groupBox_3.sizePolicy().hasHeightForWidth())
+ self.groupBox_3.setSizePolicy(sizePolicy)
+ self.groupBox_3.setMaximumSize(QSize(200, 16777213))
+ self.gridLayout_4 = QGridLayout(self.groupBox_3)
+ self.gridLayout_4.setObjectName(u"gridLayout_4")
+ self.pb_con_vert = QPushButton(self.groupBox_3)
+ self.pb_con_vert.setObjectName(u"pb_con_vert")
+ self.pb_con_vert.setCheckable(True)
+ self.pb_con_vert.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_vert, 3, 1, 1, 1)
+
+ self.pb_con_mid = QPushButton(self.groupBox_3)
+ self.pb_con_mid.setObjectName(u"pb_con_mid")
+ self.pb_con_mid.setCheckable(True)
+ self.pb_con_mid.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_mid, 2, 0, 1, 1)
+
+ self.pb_con_dist = QPushButton(self.groupBox_3)
+ self.pb_con_dist.setObjectName(u"pb_con_dist")
+ self.pb_con_dist.setCheckable(True)
+ self.pb_con_dist.setAutoExclusive(False)
+ self.pb_con_dist.setAutoRepeatDelay(297)
+
+ self.gridLayout_4.addWidget(self.pb_con_dist, 4, 0, 1, 1)
+
+ self.pb_con_line = QPushButton(self.groupBox_3)
+ self.pb_con_line.setObjectName(u"pb_con_line")
+ self.pb_con_line.setCheckable(True)
+ self.pb_con_line.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_line, 1, 1, 1, 1)
+
+ self.pb_con_perp = QPushButton(self.groupBox_3)
+ self.pb_con_perp.setObjectName(u"pb_con_perp")
+ self.pb_con_perp.setCheckable(True)
+ self.pb_con_perp.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_perp, 2, 1, 1, 1)
+
+ self.pb_con_sym = QPushButton(self.groupBox_3)
+ self.pb_con_sym.setObjectName(u"pb_con_sym")
+ self.pb_con_sym.setCheckable(True)
+ self.pb_con_sym.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_sym, 4, 1, 1, 1)
+
+ self.pb_con_horiz = QPushButton(self.groupBox_3)
+ self.pb_con_horiz.setObjectName(u"pb_con_horiz")
+ self.pb_con_horiz.setCheckable(True)
+ self.pb_con_horiz.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_horiz, 3, 0, 1, 1)
+
+ self.pb_con_ptpt = QPushButton(self.groupBox_3)
+ self.pb_con_ptpt.setObjectName(u"pb_con_ptpt")
+ self.pb_con_ptpt.setCheckable(True)
+ self.pb_con_ptpt.setAutoExclusive(False)
+
+ self.gridLayout_4.addWidget(self.pb_con_ptpt, 1, 0, 1, 1)
+
+
+ self.gridLayout.addWidget(self.groupBox_3, 4, 0, 1, 1)
+
+ self.groupBox_2 = QGroupBox(self.centralwidget)
+ self.groupBox_2.setObjectName(u"groupBox_2")
+ sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth())
+ self.groupBox_2.setSizePolicy(sizePolicy)
+ self.groupBox_2.setMaximumSize(QSize(200, 16777215))
+ self.gridLayout_2 = QGridLayout(self.groupBox_2)
+ self.gridLayout_2.setObjectName(u"gridLayout_2")
+ self.gridLayout_2.setContentsMargins(10, -1, -1, -1)
+ self.pb_arc_tool = QPushButton(self.groupBox_2)
+ self.pb_arc_tool.setObjectName(u"pb_arc_tool")
+ self.pb_arc_tool.setCheckable(True)
+
+ self.gridLayout_2.addWidget(self.pb_arc_tool, 2, 0, 1, 1)
+
+ self.pb_rectool = QPushButton(self.groupBox_2)
+ self.pb_rectool.setObjectName(u"pb_rectool")
+ self.pb_rectool.setCheckable(True)
+ self.pb_rectool.setAutoExclusive(False)
+
+ self.gridLayout_2.addWidget(self.pb_rectool, 0, 1, 1, 1)
+
+ self.pb_circtool = QPushButton(self.groupBox_2)
+ self.pb_circtool.setObjectName(u"pb_circtool")
+ self.pb_circtool.setCheckable(True)
+ self.pb_circtool.setAutoExclusive(False)
+
+ self.gridLayout_2.addWidget(self.pb_circtool, 1, 0, 1, 1, Qt.AlignTop)
+
+ self.pb_enable_construct = QPushButton(self.groupBox_2)
+ self.pb_enable_construct.setObjectName(u"pb_enable_construct")
+ self.pb_enable_construct.setCheckable(True)
+
+ self.gridLayout_2.addWidget(self.pb_enable_construct, 4, 0, 1, 1)
+
+ self.pb_enable_snap = QPushButton(self.groupBox_2)
+ self.pb_enable_snap.setObjectName(u"pb_enable_snap")
+ self.pb_enable_snap.setIconSize(QSize(13, 16))
+ self.pb_enable_snap.setCheckable(True)
+ self.pb_enable_snap.setChecked(True)
+
+ self.gridLayout_2.addWidget(self.pb_enable_snap, 4, 1, 1, 1)
+
+ self.pb_linetool = QPushButton(self.groupBox_2)
+ self.pb_linetool.setObjectName(u"pb_linetool")
+ self.pb_linetool.setCheckable(True)
+ self.pb_linetool.setAutoExclusive(False)
+
+ self.gridLayout_2.addWidget(self.pb_linetool, 0, 0, 1, 1)
+
+ self.pb_slotool = QPushButton(self.groupBox_2)
+ self.pb_slotool.setObjectName(u"pb_slotool")
+ self.pb_slotool.setCheckable(True)
+ self.pb_slotool.setAutoExclusive(False)
+
+ self.gridLayout_2.addWidget(self.pb_slotool, 1, 1, 1, 1, Qt.AlignTop)
+
+ self.line = QFrame(self.groupBox_2)
+ self.line.setObjectName(u"line")
+ self.line.setFrameShape(QFrame.Shape.HLine)
+ self.line.setFrameShadow(QFrame.Shadow.Sunken)
+
+ self.gridLayout_2.addWidget(self.line, 3, 0, 1, 2)
+
+ self.pb_offset_tool = QPushButton(self.groupBox_2)
+ self.pb_offset_tool.setObjectName(u"pb_offset_tool")
+
+ self.gridLayout_2.addWidget(self.pb_offset_tool, 2, 1, 1, 1)
+
+
+ self.gridLayout.addWidget(self.groupBox_2, 2, 0, 1, 1)
+
+ fluencyCAD.setCentralWidget(self.centralwidget)
+ self.menubar = QMenuBar(fluencyCAD)
+ self.menubar.setObjectName(u"menubar")
+ self.menubar.setGeometry(QRect(0, 0, 1941, 24))
+ self.menuFile = QMenu(self.menubar)
+ self.menuFile.setObjectName(u"menuFile")
+ self.menuSettings = QMenu(self.menubar)
+ self.menuSettings.setObjectName(u"menuSettings")
+ fluencyCAD.setMenuBar(self.menubar)
+ self.statusbar = QStatusBar(fluencyCAD)
+ self.statusbar.setObjectName(u"statusbar")
+ fluencyCAD.setStatusBar(self.statusbar)
+
+ self.menubar.addAction(self.menuFile.menuAction())
+ self.menubar.addAction(self.menuSettings.menuAction())
+ self.menuFile.addAction(self.actionNew_Project)
+ self.menuFile.addAction(self.actionLoad_Project)
+ self.menuFile.addAction(self.actionRecent)
+ self.menuFile.addSeparator()
+
+ self.retranslateUi(fluencyCAD)
+
+ self.InputTab.setCurrentIndex(0)
+
+
+ QMetaObject.connectSlotsByName(fluencyCAD)
+ # setupUi
+
+ def retranslateUi(self, fluencyCAD):
+ fluencyCAD.setWindowTitle(QCoreApplication.translate("fluencyCAD", u"fluencyCAD", None))
+ self.actionNew_Project.setText(QCoreApplication.translate("fluencyCAD", u"New", None))
+ self.actionLoad_Project.setText(QCoreApplication.translate("fluencyCAD", u"Load", None))
+ self.actionRecent.setText(QCoreApplication.translate("fluencyCAD", u"Recent", None))
+ self.compos_in_assembly_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Components in Assembly", None))
+ self.joint_tools.setTitle(QCoreApplication.translate("fluencyCAD", u"Joint Tools", None))
+ self.pb_add_connector.setText(QCoreApplication.translate("fluencyCAD", u"+ Cnct", None))
+ self.pb_remove_connector.setText(QCoreApplication.translate("fluencyCAD", u"- Cnct", None))
+ self.groupBox_5.setTitle(QCoreApplication.translate("fluencyCAD", u"Snapping Points", None))
+ self.label.setText(QCoreApplication.translate("fluencyCAD", u"Snp Dst", None))
+ self.pb_snap_vert.setText(QCoreApplication.translate("fluencyCAD", u"Vert", None))
+ self.label_2.setText(QCoreApplication.translate("fluencyCAD", u"Angl Stps", None))
+ self.spinbox_snap_distance.setSuffix(QCoreApplication.translate("fluencyCAD", u"mm", None))
+ self.pushButton_7.setText(QCoreApplication.translate("fluencyCAD", u"Grid", None))
+ self.pb_snap_horiz.setText(QCoreApplication.translate("fluencyCAD", u"Horiz", None))
+ self.spinbox_angle_steps.setSuffix(QCoreApplication.translate("fluencyCAD", u"\u00b0", None))
+ self.pushButton_8.setText(QCoreApplication.translate("fluencyCAD", u"Pnt", None))
+ self.pb_snap_midp.setText(QCoreApplication.translate("fluencyCAD", u"MidP", None))
+ self.pb_snap_angle.setText(QCoreApplication.translate("fluencyCAD", u"Angles", None))
+ self.groupBox.setTitle(QCoreApplication.translate("fluencyCAD", u"Modify", None))
+ self.pb_revop.setText(QCoreApplication.translate("fluencyCAD", u"Rev", None))
+ self.pb_extrdop.setText(QCoreApplication.translate("fluencyCAD", u"Extrd", None))
+ self.pb_arrayop.setText(QCoreApplication.translate("fluencyCAD", u"Arry", None))
+ self.pb_cutop.setText(QCoreApplication.translate("fluencyCAD", u"Cut", None))
+ self.pb_combop.setText(QCoreApplication.translate("fluencyCAD", u"Comb", None))
+ self.pb_moveop.setText(QCoreApplication.translate("fluencyCAD", u"Mve", None))
+ self.compo_tool_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Component Tools", None))
+ self.pb_new_compo.setText(QCoreApplication.translate("fluencyCAD", u"New", None))
+ self.pb_del_compo.setText(QCoreApplication.translate("fluencyCAD", u"Del", None))
+ self.compo_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Components", None))
+ self.groupBox_9.setTitle(QCoreApplication.translate("fluencyCAD", u"Workplanes", None))
+#if QT_CONFIG(tooltip)
+ self.pb_origin_wp.setToolTip(QCoreApplication.translate("fluencyCAD", u"orking Plane at 0, 0, 0", None))
+#endif // QT_CONFIG(tooltip)
+ self.pb_origin_wp.setText(QCoreApplication.translate("fluencyCAD", u"WP Origin", None))
+#if QT_CONFIG(shortcut)
+ self.pb_origin_wp.setShortcut(QCoreApplication.translate("fluencyCAD", u"W", None))
+#endif // QT_CONFIG(shortcut)
+#if QT_CONFIG(tooltip)
+ self.pb_origin_face.setToolTip(QCoreApplication.translate("fluencyCAD", u"Working Plane >PN Any:
@@ -1079,20 +1079,19 @@ class OCCRenderer(Renderer):
self._nav_mode = None
def handle_wheel(self, event) -> None:
- """Zoom toward the cursor on scroll."""
+ """Zoom centered on the viewport (body midpoint) on scroll.
+
+ Uses the view's scale factor so the zoom is always centered
+ on the viewport centre — the body never drifts to an edge.
+ """
if self._view is None:
return
- # Qt6: QWheelEvent has no .x()/.y(); use position().toPoint().
- pos = event.position().toPoint() if hasattr(event, "position") else event.pos()
- x, y = pos.x(), pos.y()
delta = event.angleDelta().y()
if delta == 0:
return
- # ZoomAtPoint(startX, startY, endX, endY): move the cursor anchor
- # by a few pixels to drive a smooth zoom centered on the pointer.
- step = 30 if delta > 0 else -30
- self._view.ZoomAtPoint(x, y, x, y - step)
- self._view.ZFitAll()
+
+ factor = 1.15 if delta > 0 else 1.0 / 1.15
+ self._view.SetScale(self._view.Scale() * factor)
def handle_resize(self, w: int, h: int) -> None:
"""Resize the OCC view when the widget is resized."""