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 @@ - + - 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."""