- Tons of addtions
This commit is contained in:
@@ -41,6 +41,7 @@ class OCCRenderer(Renderer):
|
||||
self._parent_widget: Any = None
|
||||
self._last_mouse_x: int = 0
|
||||
self._last_mouse_y: int = 0
|
||||
self._nav_mode: Optional[str] = None # "rotate" | "pan" | "zoom" | None
|
||||
|
||||
def initialize(self, parent_widget: Any) -> bool:
|
||||
"""Initialise OCC viewer inside *parent_widget* (a QWidget)."""
|
||||
@@ -56,11 +57,29 @@ class OCCRenderer(Renderer):
|
||||
Aspect_DisplayConnection,
|
||||
Aspect_NeutralWindow,
|
||||
Aspect_GFM_VER,
|
||||
Aspect_TypeOfTriedronPosition,
|
||||
)
|
||||
from OCP.OpenGl import OpenGl_GraphicDriver
|
||||
from OCP.V3d import V3d_Viewer, V3d_View, V3d_TypeOfView
|
||||
from OCP.V3d import (
|
||||
V3d_Viewer,
|
||||
V3d_View,
|
||||
V3d_TypeOfView,
|
||||
V3d_DirectionalLight,
|
||||
V3d_AmbientLight,
|
||||
V3d_TypeOfOrientation,
|
||||
)
|
||||
from OCP.AIS import AIS_InteractiveContext
|
||||
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
|
||||
from OCP.Graphic3d import (
|
||||
Graphic3d_Camera,
|
||||
Graphic3d_TypeOfShadingModel,
|
||||
Graphic3d_MaterialAspect,
|
||||
Graphic3d_NameOfMaterial,
|
||||
)
|
||||
from OCP.Quantity import (
|
||||
Quantity_Color,
|
||||
Quantity_TOC_RGB,
|
||||
Quantity_NameOfColor,
|
||||
)
|
||||
logger.info("OCCRenderer imports complete")
|
||||
|
||||
hwnd = int(parent_widget.winId())
|
||||
@@ -74,17 +93,68 @@ class OCCRenderer(Renderer):
|
||||
display = Aspect_DisplayConnection()
|
||||
driver = OpenGl_GraphicDriver(display)
|
||||
viewer = V3d_Viewer(driver)
|
||||
viewer.SetDefaultLights()
|
||||
viewer.SetLightOn()
|
||||
|
||||
# ── Lighting: replace defaults with a tuned 3-light rig ───────
|
||||
# Key light (warm directional from upper-front-right) + fill
|
||||
# (cool, softer) + ambient for base lift. Gives proper shading
|
||||
# on curved BRep surfaces (cylinders, spheres).
|
||||
viewer.SetLightOff() # clear default lights first
|
||||
key = V3d_DirectionalLight(
|
||||
V3d_TypeOfOrientation.V3d_XposYnegZpos,
|
||||
Quantity_Color(1.0, 0.96, 0.88, Quantity_TOC_RGB),
|
||||
True,
|
||||
)
|
||||
fill = V3d_DirectionalLight(
|
||||
V3d_TypeOfOrientation.V3d_XnegYnegZpos,
|
||||
Quantity_Color(0.55, 0.62, 0.78, Quantity_TOC_RGB),
|
||||
True,
|
||||
)
|
||||
rim = V3d_DirectionalLight(
|
||||
V3d_TypeOfOrientation.V3d_XposYposZneg,
|
||||
Quantity_Color(0.5, 0.5, 0.55, Quantity_TOC_RGB),
|
||||
True,
|
||||
)
|
||||
ambient = V3d_AmbientLight(
|
||||
Quantity_Color(0.35, 0.35, 0.4, Quantity_TOC_RGB)
|
||||
)
|
||||
for light in (key, fill, rim, ambient):
|
||||
viewer.SetLightOn(light)
|
||||
|
||||
view = V3d_View(viewer, V3d_TypeOfView.V3d_ORTHOGRAPHIC)
|
||||
|
||||
# ── Background gradient (dark studio look) ───────────────────
|
||||
viewer.SetDefaultBgGradientColors(
|
||||
Quantity_Color(0.15, 0.15, 0.2, Quantity_TOC_RGB),
|
||||
Quantity_Color(0.25, 0.25, 0.3, Quantity_TOC_RGB),
|
||||
Quantity_Color(0.18, 0.20, 0.24, Quantity_TOC_RGB),
|
||||
Quantity_Color(0.08, 0.09, 0.11, Quantity_TOC_RGB),
|
||||
Aspect_GFM_VER,
|
||||
)
|
||||
view.SetBgGradientColors(
|
||||
Quantity_Color(0.18, 0.20, 0.24, Quantity_TOC_RGB),
|
||||
Quantity_Color(0.08, 0.09, 0.11, Quantity_TOC_RGB),
|
||||
Aspect_GFM_VER,
|
||||
True,
|
||||
)
|
||||
|
||||
# ── Rendering quality: MSAA + Phong shading + AA ─────────────
|
||||
params = view.ChangeRenderingParams()
|
||||
params.NbMsaaSamples = 4 # 4x MSAA for smoother edges
|
||||
params.IsAntialiasingEnabled = True
|
||||
params.LineFeather = 0.6
|
||||
view.SetShadingModel(Graphic3d_TypeOfShadingModel.Graphic3d_TypeOfShadingModel_Phong)
|
||||
|
||||
# ── Corner orientation trihedron (axis gizmo) ────────────────
|
||||
try:
|
||||
view.TriedronDisplay(
|
||||
Aspect_TypeOfTriedronPosition.Aspect_TOTP_LEFT_LOWER,
|
||||
Quantity_Color(Quantity_NameOfColor.Quantity_NOC_WHITE),
|
||||
0.08,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("TriedronDisplay unavailable", exc_info=True)
|
||||
|
||||
context = AIS_InteractiveContext(viewer)
|
||||
# Default display mode = shaded (AIS_Shaded = 1)
|
||||
context.SetDisplayMode(1, True)
|
||||
|
||||
# Attach OCC view to the Qt widget via the native window handle.
|
||||
win = Aspect_NeutralWindow()
|
||||
@@ -92,6 +162,8 @@ class OCCRenderer(Renderer):
|
||||
w, h = parent_widget.width(), parent_widget.height()
|
||||
win.SetSize(w, h)
|
||||
view.SetWindow(win)
|
||||
# Ensure the depth range follows the scene so nothing clips.
|
||||
view.SetAutoZFitMode(True)
|
||||
|
||||
self._viewer = viewer
|
||||
self._view = view
|
||||
@@ -138,18 +210,39 @@ class OCCRenderer(Renderer):
|
||||
|
||||
ais = AIS_Shape(shape)
|
||||
|
||||
# Order matters: set material *before* color so the per-channel color
|
||||
# overrides only the diffuse albedo and the plastic BRDF is retained.
|
||||
ais.SetMaterial(self._default_material())
|
||||
if color is not None:
|
||||
qcol = Quantity_Color(*color, Quantity_TOC_RGB)
|
||||
ais.SetColor(qcol)
|
||||
|
||||
# Enable selection of edges and faces.
|
||||
ais.SetDisplayMode(1) # 0 = wireframe, 1 = shaded
|
||||
ais.SetMaterial(
|
||||
self._default_material()
|
||||
) # use a helper to get the default material
|
||||
# Shaded display with boundary edges drawn on top for readability.
|
||||
ais.SetDisplayMode(1) # 1 = AIS_Shaded
|
||||
drawer = ais.Attributes()
|
||||
try:
|
||||
# Always draw face boundaries — makes solid edges crisp.
|
||||
drawer.SetFaceBoundaryDraw(True)
|
||||
except Exception:
|
||||
logger.debug("boundary-draw attrs unavailable", exc_info=True)
|
||||
# Slight polygon offset so boundary edges don't z-fight the surface.
|
||||
# Mode 3 = Graphic3d_POM_Fill (offset filled polygons).
|
||||
try:
|
||||
ais.SetPolygonOffsets(3, 1.0, 0.0)
|
||||
except Exception:
|
||||
logger.debug("polygon offset unavailable", exc_info=True)
|
||||
|
||||
self._context.Display(ais, True)
|
||||
self._context.Deactivate(ais)
|
||||
# Activate selection modes so the viewer can detect/pick the whole
|
||||
# shape (mode 0) and individual faces (mode for TopAbs_FACE) — needed
|
||||
# for sketch-on-surface face picking. Left-click orbits the camera
|
||||
# (see handle_mouse_press), so active selection doesn't interfere.
|
||||
try:
|
||||
from OCP.TopAbs import TopAbs_FACE
|
||||
face_mode = AIS_Shape.SelectionMode_s(TopAbs_FACE)
|
||||
self._context.Activate(ais, face_mode)
|
||||
except Exception:
|
||||
logger.debug("face selection mode activation failed", exc_info=True)
|
||||
|
||||
defcol = color or (0.5, 0.5, 0.5)
|
||||
robj = OCCRenderObject(
|
||||
@@ -170,12 +263,17 @@ class OCCRenderer(Renderer):
|
||||
return obj_id
|
||||
|
||||
def _default_material(self):
|
||||
"""Return a default Graphic3d_MaterialAspect for shading."""
|
||||
"""Return a default Graphic3d_MaterialAspect for shading.
|
||||
|
||||
Plastic gives a clean, readable matte surface that responds well to
|
||||
the 3-light rig set up in :meth:`initialize`.
|
||||
"""
|
||||
from OCP.Graphic3d import Graphic3d_MaterialAspect, Graphic3d_NameOfMaterial
|
||||
|
||||
return Graphic3d_MaterialAspect(
|
||||
mat = Graphic3d_MaterialAspect(
|
||||
Graphic3d_NameOfMaterial.Graphic3d_NOM_PLASTIC
|
||||
)
|
||||
return mat
|
||||
|
||||
# ─── Legacy mesh / wireframe (kept for backward compat) ────────────
|
||||
|
||||
@@ -278,12 +376,16 @@ class OCCRenderer(Renderer):
|
||||
"""Remove all objects from the scene."""
|
||||
if self._context is None:
|
||||
return
|
||||
# Remove all displayed AIS objects.
|
||||
from OCP.AIS import AIS_KindOfInteractive
|
||||
|
||||
objs = self._context.DisplayedObjects()
|
||||
for ais in objs:
|
||||
self._context.Remove(ais, True)
|
||||
# Remove every displayed AIS object. ``RemoveAll`` is the cleanest
|
||||
# path; fall back to iterating the displayed list if unavailable.
|
||||
try:
|
||||
self._context.RemoveAll(True)
|
||||
except Exception:
|
||||
from OCP.AIS import AIS_ListOfInteractive, AIS_KindOfInteractive
|
||||
lst = AIS_ListOfInteractive()
|
||||
self._context.DisplayedObjects(AIS_KindOfInteractive.AIS_KOI_None, -1, lst)
|
||||
for ais in lst:
|
||||
self._context.Remove(ais, True)
|
||||
self._objects.clear()
|
||||
|
||||
def update_mesh(
|
||||
@@ -327,15 +429,14 @@ class OCCRenderer(Renderer):
|
||||
target: Tuple[float, float, float] = (0, 0, 0),
|
||||
up: Tuple[float, float, float] = (0, 0, 1),
|
||||
) -> None:
|
||||
"""Set camera position and orientation."""
|
||||
"""Set camera eye, target (at) and up vectors."""
|
||||
if self._view is None:
|
||||
return
|
||||
from OCP.gp import gp_Pnt, gp_Dir, gp_Vec
|
||||
|
||||
self._view.SetProj(gp_Dir(position[0], position[1], position[2]))
|
||||
self._view.SetEye(gp_Pnt(*position))
|
||||
self._view.SetCenter(gp_Pnt(*target))
|
||||
self._view.SetUp(gp_Dir(*up))
|
||||
self._view.SetEye(float(position[0]), float(position[1]), float(position[2]))
|
||||
self._view.SetAt(float(target[0]), float(target[1]), float(target[2]))
|
||||
self._view.SetUp(float(up[0]), float(up[1]), float(up[2]))
|
||||
self._view.ZFitAll()
|
||||
self._view.Redraw()
|
||||
|
||||
def get_camera_position(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Get camera position, target, and up vector."""
|
||||
@@ -346,47 +447,89 @@ class OCCRenderer(Renderer):
|
||||
np.array([0, 0, 1]),
|
||||
)
|
||||
eye = self._view.Eye()
|
||||
center = self._view.Center()
|
||||
at = self._view.At()
|
||||
up = self._view.Up()
|
||||
return (
|
||||
np.array([eye.X(), eye.Y(), eye.Z()]),
|
||||
np.array([center.X(), center.Y(), center.Z()]),
|
||||
np.array([up.X(), up.Y(), up.Z()]),
|
||||
)
|
||||
|
||||
def _xyz(v):
|
||||
if isinstance(v, (tuple, list)):
|
||||
return np.array([float(v[0]), float(v[1]), float(v[2])])
|
||||
return np.array([v.X(), v.Y(), v.Z()])
|
||||
|
||||
return (_xyz(eye), _xyz(at), _xyz(up))
|
||||
|
||||
def fit_camera(self, padding: float = 0.05) -> None:
|
||||
"""Fit camera to show all displayed objects.
|
||||
|
||||
*padding* is the margin coefficient (0 ≤ padding < 1.0) passed to
|
||||
OCC's ``FitAll``, not a multiplicative factor. A small value like
|
||||
0.05 adds 5% margin around the bounding box.
|
||||
OCC's ``FitAll``. A small value like 0.05 adds 5% margin.
|
||||
"""
|
||||
if self._view is None:
|
||||
return
|
||||
margin = max(0.0, min(padding, 0.99))
|
||||
self._view.FitAll(margin)
|
||||
self._view.ZFitAll()
|
||||
|
||||
def set_view_orientation(self, orientation: str = "iso") -> None:
|
||||
"""Snap the camera to a standard CAD view.
|
||||
|
||||
orientation ∈ {"front","back","top","bottom","left","right","iso"}.
|
||||
Preserves the current target and distance, then refits.
|
||||
"""
|
||||
if self._view is None:
|
||||
return
|
||||
from OCP.V3d import V3d_TypeOfOrientation
|
||||
|
||||
mapping = {
|
||||
"front": V3d_TypeOfOrientation.V3d_Yneg,
|
||||
"back": V3d_TypeOfOrientation.V3d_Ypos,
|
||||
"top": V3d_TypeOfOrientation.V3d_Zpos,
|
||||
"bottom": V3d_TypeOfOrientation.V3d_Zneg,
|
||||
"left": V3d_TypeOfOrientation.V3d_Xneg,
|
||||
"right": V3d_TypeOfOrientation.V3d_Xpos,
|
||||
"iso": V3d_TypeOfOrientation.V3d_XposYnegZpos,
|
||||
}
|
||||
orient = mapping.get(orientation.lower())
|
||||
if orient is None:
|
||||
logger.warning(f"unknown view orientation: {orientation}")
|
||||
return
|
||||
self._view.SetProj(orient)
|
||||
self.fit_camera()
|
||||
self._view.Redraw()
|
||||
|
||||
def reset_camera(self) -> None:
|
||||
"""Reset to the default isometric view and fit all."""
|
||||
self.set_view_orientation("iso")
|
||||
|
||||
def set_camera_perspective(
|
||||
self, fov: float = 50.0, near: float = 0.1, far: float = 10000.0
|
||||
self, fov: float = 45.0, near: float = 0.1, far: float = 100000.0
|
||||
) -> None:
|
||||
"""Set perspective camera."""
|
||||
"""Switch to a perspective camera with the given FOV (degrees).
|
||||
|
||||
Near/far planes are auto-fit by OCC (``ZFitAll``) — the *near*/*far*
|
||||
args are accepted for API compatibility but ignored.
|
||||
"""
|
||||
if self._view is None:
|
||||
return
|
||||
from OCP.V3d import V3d_PERSPECTIVE
|
||||
from OCP.Graphic3d import Graphic3d_Camera
|
||||
|
||||
self._view.SetComputedMode(False) # manual mode
|
||||
self._view.ChangeRenderingParams()
|
||||
# perspective/orthographic toggle handled in set_camera_orthographic
|
||||
cam = self._view.Camera()
|
||||
cam.SetProjectionType(Graphic3d_Camera.Projection_Perspective)
|
||||
cam.SetFOVy(fov)
|
||||
self._view.ZFitAll()
|
||||
self._view.Redraw()
|
||||
|
||||
def set_camera_orthographic(
|
||||
self, width: float = 100.0, near: float = 0.1, far: float = 10000.0
|
||||
self, width: float = 100.0, near: float = 0.1, far: float = 100000.0
|
||||
) -> None:
|
||||
"""Set orthographic camera."""
|
||||
"""Switch to an orthographic camera."""
|
||||
if self._view is None:
|
||||
return
|
||||
from OCP.V3d import V3d_ORTHOGRAPHIC
|
||||
from OCP.Graphic3d import Graphic3d_Camera
|
||||
|
||||
self._view.SetComputedMode(False)
|
||||
cam = self._view.Camera()
|
||||
cam.SetProjectionType(Graphic3d_Camera.Projection_Orthographic)
|
||||
self._view.ZFitAll()
|
||||
self._view.Redraw()
|
||||
|
||||
# ─── Rendering ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -444,56 +587,192 @@ class OCCRenderer(Renderer):
|
||||
) -> Tuple[float, float, float]:
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
# ─── Mouse event forwarding ────────────────────────────────────────
|
||||
# ─── Face picking (for sketch-on-surface) ────────────────────────────
|
||||
|
||||
def pick_planar_face(self, x: int, y: int) -> Optional[Dict[str, Any]]:
|
||||
"""Pick the planar face under screen pixel (x, y).
|
||||
|
||||
Returns a dict with ``origin``, ``normal``, ``x_dir`` (a stable
|
||||
in-plane axis) and ``face`` (the ``TopoDS_Face``), or *None* if the
|
||||
hit is not a planar face. The plane is derived from the face's
|
||||
underlying surface via ``BRepAdaptor_Surface``; *x_dir* is taken
|
||||
from the face's first edge direction so the UV frame is aligned to
|
||||
the face geometry.
|
||||
"""
|
||||
if self._view is None or self._context is None:
|
||||
return None
|
||||
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import GeomAbs_Plane
|
||||
from OCP.TopoDS import TopoDS_Face, TopoDS
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.gp import gp_Pln, gp_Dir, gp_Pnt
|
||||
import numpy as np
|
||||
|
||||
# Detect what's under the cursor.
|
||||
self._context.MoveTo(x, y, self._view, True)
|
||||
if not self._context.HasDetected():
|
||||
return None
|
||||
|
||||
shape = self._context.DetectedShape()
|
||||
if shape is None:
|
||||
return None
|
||||
|
||||
# The detected sub-shape is returned as a TopoDS_Shape; with face
|
||||
# selection mode active it downcasts to a TopoDS_Face. Try the
|
||||
# downcast; if it isn't a face, give up (non-planar/edge/vertex hits).
|
||||
face = None
|
||||
try:
|
||||
candidate = TopoDS.Face_s(shape)
|
||||
# Verify it really is a face by building an adaptor.
|
||||
_ = BRepAdaptor_Surface(candidate)
|
||||
face = candidate
|
||||
except Exception:
|
||||
face = None
|
||||
if face is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
adaptor = BRepAdaptor_Surface(face)
|
||||
if adaptor.GetType() != GeomAbs_Plane:
|
||||
return None # non-planar faces can't host a flat UV sketch
|
||||
pln: gp_Pln = adaptor.Plane()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Plane origin: use the face's bounding-box centre projected onto the
|
||||
# plane, so the UV frame is centred on the face (nicer for sketching).
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
bbox = Bnd_Box()
|
||||
BRepBndLib.Add_s(face, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
cx, cy, cz = (xmin + xmax) / 2.0, (ymin + ymax) / 2.0, (zmin + zmax) / 2.0
|
||||
# Project the bbox centre onto the plane.
|
||||
pln_origin = pln.Location() # gp_Pnt
|
||||
n = pln.Axis().Direction()
|
||||
nx, ny, nz = n.X(), n.Y(), n.Z()
|
||||
# signed distance from bbox centre to plane
|
||||
d = (cx - pln_origin.X()) * nx + (cy - pln_origin.Y()) * ny + (cz - pln_origin.Z()) * nz
|
||||
origin = (cx - d * nx, cy - d * ny, cz - d * nz)
|
||||
|
||||
# x_dir: direction of the face's first edge (stable, in-plane).
|
||||
x_dir = None
|
||||
try:
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.BRep import BRep_Tool
|
||||
expl = TopExp_Explorer(face, TopAbs_EDGE)
|
||||
if expl.More():
|
||||
edge = TopoDS.Edge_s(expl.Current())
|
||||
v1 = TopExp.FirstVertex_s(edge, True)
|
||||
v2 = TopExp.LastVertex_s(edge, True)
|
||||
p1 = BRep_Tool.Pnt_s(v1)
|
||||
p2 = BRep_Tool.Pnt_s(v2)
|
||||
ex, ey, ez = p2.X() - p1.X(), p2.Y() - p1.Y(), p2.Z() - p1.Z()
|
||||
elen = (ex * ex + ey * ey + ez * ez) ** 0.5
|
||||
if elen > 1e-9:
|
||||
x_dir = (ex / elen, ey / elen, ez / elen)
|
||||
except Exception:
|
||||
pass
|
||||
if x_dir is None:
|
||||
# Fall back to the plane's own X axis.
|
||||
px = pln.XAxis().Direction()
|
||||
x_dir = (px.X(), px.Y(), px.Z())
|
||||
|
||||
return {
|
||||
"origin": origin,
|
||||
"normal": (nx, ny, nz),
|
||||
"x_dir": x_dir,
|
||||
"face": face,
|
||||
}
|
||||
|
||||
# ─── Mouse / keyboard event forwarding ──────────────────────────────
|
||||
#
|
||||
# CAD-style navigation:
|
||||
# • Left button drag → orbit (rotate around target)
|
||||
# • Middle button drag → pan
|
||||
# • Right button drag → dolly / zoom toward cursor
|
||||
# • Wheel → zoom toward cursor
|
||||
# • Double-click left → fit all (handled by the widget)
|
||||
|
||||
def _qt_buttons(self, event) -> Any:
|
||||
"""Return the PySide6 Qt enum module lazily."""
|
||||
from PySide6.QtCore import Qt
|
||||
return Qt
|
||||
|
||||
def handle_mouse_press(self, event) -> None:
|
||||
"""Forward a QMouseEvent to the OCC view for orbit/pan/zoom/select."""
|
||||
from OCP.Aspect import Aspect_VKeyMouse
|
||||
from OCP.AIS import AIS_SelectionScheme
|
||||
|
||||
"""Begin an orbit / pan / zoom gesture based on the pressed button."""
|
||||
if self._view is None or self._context is None:
|
||||
return
|
||||
Qt = self._qt_buttons(event)
|
||||
x, y = event.x(), event.y()
|
||||
btn = event.button()
|
||||
|
||||
# Middle mouse → start rotation
|
||||
if event.button() == 4: # Qt.MiddleButton
|
||||
self._view.StartRotation(event.x(), event.y())
|
||||
# Left mouse → try selection (OCC picks nearest shape)
|
||||
elif event.button() == 1: # Qt.LeftButton
|
||||
self._context.Select(True)
|
||||
if btn == Qt.LeftButton:
|
||||
self._nav_mode = "rotate"
|
||||
# zRotationThreshold=0.4 enables screen-Z spin outside the inner
|
||||
# circle, matching the FreeCAD/OCC viewer feel.
|
||||
self._view.StartRotation(x, y, 0.4)
|
||||
elif btn == Qt.MiddleButton:
|
||||
self._nav_mode = "pan"
|
||||
# Pan uses deltas from this starting point.
|
||||
self._view.Pan(0, 0, 1.0, True)
|
||||
elif btn == Qt.RightButton:
|
||||
self._nav_mode = "zoom"
|
||||
self._view.StartZoomAtPoint(x, y)
|
||||
else:
|
||||
self._nav_mode = None
|
||||
|
||||
self._last_mouse_x = x
|
||||
self._last_mouse_y = y
|
||||
|
||||
def handle_mouse_move(self, event) -> None:
|
||||
"""Forward mouse motion to OCC view (rotation, dynamic highlighting)."""
|
||||
from OCP.Aspect import Aspect_VKeyMouse
|
||||
|
||||
"""Continue the active gesture; otherwise just hover-detect."""
|
||||
if self._view is None or self._context is None:
|
||||
return
|
||||
|
||||
Qt = self._qt_buttons(event)
|
||||
x, y = event.x(), event.y()
|
||||
buttons = event.buttons()
|
||||
if buttons & 4: # Qt.MiddleButton
|
||||
self._view.Rotation(event.x(), event.y())
|
||||
elif buttons & 2: # Qt.RightButton
|
||||
dx = event.x() - self._last_mouse_x
|
||||
dy = event.y() - self._last_mouse_y
|
||||
self._view.Pan(dx, dy)
|
||||
# Dynamic highlighting (detect)
|
||||
self._context.MoveTo(event.x(), event.y(), self._view, True)
|
||||
|
||||
self._last_mouse_x = event.x()
|
||||
self._last_mouse_y = event.y()
|
||||
if self._nav_mode == "rotate" and (buttons & Qt.LeftButton):
|
||||
self._view.Rotation(x, y)
|
||||
elif self._nav_mode == "pan" and (buttons & Qt.MiddleButton):
|
||||
dx = x - self._last_mouse_x
|
||||
dy = y - self._last_mouse_y
|
||||
# dy negated because Qt y grows downward while OCC y grows upward.
|
||||
self._view.Pan(dx, -dy, 1.0, False)
|
||||
elif self._nav_mode == "zoom" and (buttons & Qt.RightButton):
|
||||
self._view.ZoomAtPoint(self._last_mouse_x, self._last_mouse_y, x, y)
|
||||
else:
|
||||
# Idle: dynamic highlighting under the cursor.
|
||||
self._context.MoveTo(x, y, self._view, True)
|
||||
|
||||
self._last_mouse_x = x
|
||||
self._last_mouse_y = y
|
||||
|
||||
def handle_mouse_release(self, event) -> None:
|
||||
"""End rotation/pan."""
|
||||
pass
|
||||
"""End the active gesture."""
|
||||
Qt = self._qt_buttons(event)
|
||||
if event.button() in (Qt.LeftButton, Qt.MiddleButton, Qt.RightButton):
|
||||
self._nav_mode = None
|
||||
|
||||
def handle_wheel(self, event) -> None:
|
||||
"""Zoom on scroll."""
|
||||
"""Zoom toward the cursor on scroll."""
|
||||
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:
|
||||
self._view.SetZoom(1.1)
|
||||
else:
|
||||
self._view.SetZoom(0.9)
|
||||
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()
|
||||
|
||||
def handle_resize(self, w: int, h: int) -> None:
|
||||
"""Resize the OCC view when the widget is resized."""
|
||||
|
||||
Reference in New Issue
Block a user