- sketch enhacements

This commit is contained in:
bklronin
2026-07-04 12:10:58 +02:00
parent 01833e4af2
commit 6ba742ddf5
5 changed files with 1652 additions and 104 deletions
+135
View File
@@ -687,6 +687,141 @@ class OCCRenderer(Renderer):
qcol = Quantity_Color(*color, Quantity_TOC_RGB)
self._view.SetBackgroundColor(qcol)
# ─── Workplane visualization ────────────────────────────────────────
_WORKPLANE_BASE_ID: str = "__workplane__"
_WORKPLANE_GRID_SIZE: float = 200.0 # total extent of the visual plane
def show_workplane_plane(
self,
origin: Tuple[float, float, float] = (0, 0, 0),
normal: Tuple[float, float, float] = (0, 0, 1),
x_dir: Tuple[float, float, float] = (1, 0, 0),
size: float = 200.0,
name: Optional[str] = None,
) -> str:
"""Display a semi-transparent planar grid at the workplane location.
The plane is a single large rectangular face with a grid of lines
drawn on it, rendered at low opacity so it does not obscure the
existing bodies. Returns an object ID that can be passed to
:meth:`remove_workplane_plane`.
If *name* is provided it is used as the object ID; otherwise a
unique one is generated. Passing an existing workplane ID will
replace that workplane visual in place.
"""
import numpy as np
from OCP.gp import gp_Pnt, gp_Dir
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.gp import gp_Pln
from OCP.AIS import AIS_Shape
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
from OCP.Graphic3d import Graphic3d_MaterialAspect, Graphic3d_NameOfMaterial
obj_id = name or f"{self._WORKPLANE_BASE_ID}_{uuid.uuid4().hex[:8]}"
# Compute orthonormal basis.
n = np.asarray(normal, dtype=float)
n = n / np.linalg.norm(n)
x = np.asarray(x_dir, dtype=float)
x = x - np.dot(x, n) * n
x_norm = np.linalg.norm(x)
if x_norm < 1e-9:
fallback = np.array([1.0, 0.0, 0.0]) if abs(n[0]) < 0.9 else np.array([0.0, 1.0, 0.0])
x = fallback - np.dot(fallback, n) * n
x_norm = np.linalg.norm(x)
x = x / x_norm
y = np.cross(n, x)
hs = size / 2.0
# Four corners of the plane in local coords.
corners_3d = [
(
origin[0] + (-hs) * x[0] + (-hs) * y[0],
origin[1] + (-hs) * x[1] + (-hs) * y[1],
origin[2] + (-hs) * x[2] + (-hs) * y[2],
),
(
origin[0] + (hs) * x[0] + (-hs) * y[0],
origin[1] + (hs) * x[1] + (-hs) * y[1],
origin[2] + (hs) * x[2] + (-hs) * y[2],
),
(
origin[0] + (hs) * x[0] + (hs) * y[0],
origin[1] + (hs) * x[1] + (hs) * y[1],
origin[2] + (hs) * x[2] + (hs) * y[2],
),
(
origin[0] + (-hs) * x[0] + (hs) * y[0],
origin[1] + (-hs) * x[1] + (hs) * y[1],
origin[2] + (-hs) * x[2] + (hs) * y[2],
),
]
# Build the planar face: point + normal direction.
pln = gp_Pln(
gp_Pnt(*origin),
gp_Dir(n[0], n[1], n[2]),
)
# Use gp_Pnt for the corners to make a bounded face.
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakePolygon
mp = BRepBuilderAPI_MakePolygon()
for c in corners_3d:
mp.Add(gp_Pnt(*c))
mp.Close()
wire = mp.Wire()
face_maker = BRepBuilderAPI_MakeFace(pln, wire)
face = face_maker.Face()
# Check if a workplane with this id already exists and remove it.
existing = self._objects.get(obj_id)
if existing is not None:
self.remove_object(existing)
ais = AIS_Shape(face)
# Semi-transparent blue-ish tint.
ais.SetColor(Quantity_Color(0.55, 0.75, 0.95, Quantity_TOC_RGB)) # light blue
try:
ais.SetTransparency(0.75)
except Exception:
pass
ais.SetDisplayMode(1) # shaded
# Draw the plane with a slight polygon offset so it doesn't z-fight.
try:
ais.SetPolygonOffsets(3, 1.0, -1.0)
except Exception:
pass
self._context.Display(ais, True)
# Deactivate selection on the workplane plane so that face-picking
# (``pick_planar_face``) does NOT intercept the workplane's face
# when the user clicks through it to select a body face underneath.
# The workplane is decorative reference geometry — it must never
# be a target of face-pick mode.
try:
self._context.Deactivate(ais)
except Exception:
pass
robj = OCCRenderObject(
obj_id=obj_id,
ais_shape=ais,
ais_type="workplane",
color=RenderColor(0.55, 0.75, 0.95),
)
self._objects[obj_id] = robj
self._view.Redraw()
return obj_id
def remove_workplane_plane(self, obj_id: str) -> bool:
"""Remove a workplane plane visual by its object ID."""
robj = self._objects.get(obj_id)
if robj is not None:
return self.remove_object(robj)
return False
def take_screenshot(self) -> bytes:
return b""