Improved render previews
This commit is contained in:
@@ -17,6 +17,86 @@ from .base import Renderer, RenderObject, RenderColor
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _compute_viewport_aligned_xdir(
|
||||
normal: Tuple[float, float, float],
|
||||
view: Any = None,
|
||||
) -> Tuple[float, float, float]:
|
||||
"""Compute an in-plane x_dir that appears screen-horizontal.
|
||||
|
||||
When *view* (a V3d_View) is provided, derives a "screen-right" direction
|
||||
from the camera's Eye/At/Up vectors and projects it onto the face plane.
|
||||
This makes the sketch U-axis align with what the user sees as horizontal
|
||||
on screen, eliminating the 90° rotation caused by OCC internal edge order.
|
||||
|
||||
Falls back to projecting world +X onto the plane when view is None
|
||||
(headless tests or unavailable camera state).
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
n = np.asarray(normal, dtype=float)
|
||||
n /= np.linalg.norm(n)
|
||||
|
||||
# Derive screen-right from camera: cross(view_up, -view_dir) gives the
|
||||
# direction that appears horizontal on screen.
|
||||
if view is not None:
|
||||
try:
|
||||
eye_obj = view.Eye()
|
||||
at_obj = view.At()
|
||||
up_obj = view.Up()
|
||||
|
||||
eye = np.array([eye_obj.X(), eye_obj.Y(), eye_obj.Z()], dtype=float)
|
||||
at = np.array([at_obj.X(), at_obj.Y(), at_obj.Z()], dtype=float)
|
||||
up = np.array([up_obj.X(), up_obj.Y(), up_obj.Z()], dtype=float)
|
||||
|
||||
view_dir = at - eye
|
||||
vd_norm = np.linalg.norm(view_dir)
|
||||
if vd_norm > 1e-9:
|
||||
view_dir /= vd_norm
|
||||
else:
|
||||
view_dir = np.array([0.0, 0.0, 1.0])
|
||||
|
||||
up_norm = np.linalg.norm(up)
|
||||
if up_norm < 1e-9:
|
||||
up = np.array([0.0, 1.0, 0.0])
|
||||
else:
|
||||
up /= up_norm
|
||||
|
||||
# Screen-right in world coords: cross(view_up, view_dir) gives
|
||||
# the direction that appears horizontal on screen.
|
||||
screen_right = np.cross(up, view_dir)
|
||||
sr_norm = np.linalg.norm(screen_right)
|
||||
if sr_norm > 1e-9:
|
||||
screen_right /= sr_norm
|
||||
else:
|
||||
screen_right = np.array([1.0, 0.0, 0.0])
|
||||
except Exception:
|
||||
screen_right = np.array([1.0, 0.0, 0.0])
|
||||
else:
|
||||
# No camera available — use world +X as best guess.
|
||||
screen_right = np.array([1.0, 0.0, 0.0])
|
||||
|
||||
# Project screen-right onto the face plane (remove normal component).
|
||||
x_dir = screen_right - np.dot(screen_right, n) * n
|
||||
xn = np.linalg.norm(x_dir)
|
||||
if xn < 1e-9:
|
||||
# screen_right is parallel to normal — pick any orthogonal direction.
|
||||
fallback = np.array([0.0, 1.0, 0.0])
|
||||
x_dir = fallback - np.dot(fallback, n) * n
|
||||
xn = np.linalg.norm(x_dir)
|
||||
if xn > 1e-9:
|
||||
unit = x_dir / xn
|
||||
return (float(unit[0]), float(unit[1]), float(unit[2]))
|
||||
# Last resort: cross with any non-parallel axis.
|
||||
for ax in (np.array([1, 0, 0]), np.array([0, 1, 0]), np.array([0, 0, 1])):
|
||||
x_dir = ax - np.dot(ax, n) * n
|
||||
xn = np.linalg.norm(x_dir)
|
||||
if xn > 1e-9:
|
||||
unit = x_dir / xn
|
||||
return (float(unit[0]), float(unit[1]), float(unit[2]))
|
||||
# Should never reach here for a valid normal.
|
||||
return (1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCCRenderObject(RenderObject):
|
||||
"""Internal object state for the OCC renderer."""
|
||||
@@ -1046,28 +1126,12 @@ class OCCRenderer(Renderer):
|
||||
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())
|
||||
# x_dir: project a "screen-horizontal" direction onto the face plane
|
||||
# so that the sketch's U-axis appears horizontal in the current 3D
|
||||
# viewport. This eliminates the 90° rotation that occurred when we
|
||||
# used the first edge / plane X axis (which follow OCC internal order,
|
||||
# not the viewer perspective).
|
||||
x_dir = _compute_viewport_aligned_xdir((nx, ny, nz), self._view)
|
||||
|
||||
# Identify the displayed body that owns this face, so the host can
|
||||
# auto-target it as the cut/union body when the user extrudes the
|
||||
@@ -1258,25 +1322,8 @@ class OCCRenderer(Renderer):
|
||||
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 from first edge.
|
||||
x_dir = None
|
||||
try:
|
||||
expl = TopExp_Explorer(face, TopAbs_EDGE_TYPE)
|
||||
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:
|
||||
px = pln.XAxis().Direction()
|
||||
x_dir = (px.X(), px.Y(), px.Z())
|
||||
# x_dir: viewport-aligned so connector gizmo matches screen.
|
||||
x_dir = _compute_viewport_aligned_xdir((nx, ny, nz), self._view)
|
||||
|
||||
return [{
|
||||
"type": "planar_face",
|
||||
|
||||
Reference in New Issue
Block a user