Skip to content

Commit

Permalink
Control point visualization tools (#1765)
Browse files Browse the repository at this point in the history
* Control point visualization tools

* Add tests

* Nicer exception'

* Some docs

* Coverage tweaks

* Fix tests

* More coverage

* Update docs

* Docstring fixes

Co-authored-by: Jeremy Wright <[email protected]>

* Typo fix

---------

Co-authored-by: adam-urbanczyk <[email protected]>
Co-authored-by: Jeremy Wright <[email protected]>
  • Loading branch information
3 people authored Feb 14, 2025
1 parent 9e47392 commit 6772bfe
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 6 deletions.
19 changes: 18 additions & 1 deletion cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
BRepBuilderAPI_RightCorner,
BRepBuilderAPI_RoundCorner,
BRepBuilderAPI_MakeSolid,
BRepBuilderAPI_NurbsConvert,
)

# properties used to store mass calculation result
Expand Down Expand Up @@ -1488,6 +1489,15 @@ def toSplines(

return self.__class__(result)

def toNURBS(self: T,) -> T:
"""
Return a NURBS representation of a given shape.
"""

bldr = BRepBuilderAPI_NurbsConvert(self.wrapped, Copy=True)

return self.__class__(bldr.Shape())

def toVtkPolyData(
self,
tolerance: Optional[float] = None,
Expand Down Expand Up @@ -5888,7 +5898,11 @@ def loft(
#%% diagnotics


def check(s: Shape, results: Optional[List[Tuple[List[Shape], Any]]] = None) -> bool:
def check(
s: Shape,
results: Optional[List[Tuple[List[Shape], Any]]] = None,
tol: Optional[float] = None,
) -> bool:
"""
Check if a shape is valid.
"""
Expand All @@ -5901,6 +5915,9 @@ def check(s: Shape, results: Optional[List[Tuple[List[Shape], Any]]] = None) ->

rv = analyzer.IsValid()

if tol:
analyzer.SetFuzzyValue(tol)

# output detailed results if requested
if results is not None:
results.clear()
Expand Down
107 changes: 106 additions & 1 deletion cadquery/vis.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from . import Shape, Workplane, Assembly, Sketch, Compound, Color, Vector, Location
from . import (
Shape,
Workplane,
Assembly,
Sketch,
Compound,
Color,
Vector,
Location,
Face,
Edge,
)
from .occ_impl.assembly import _loc2vtk, toVTK

from typing import Union, Any, List, Tuple, Iterable, cast, Optional

from typish import instance_of

from OCP.TopoDS import TopoDS_Shape
from OCP.Geom import Geom_BSplineSurface

from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor
Expand All @@ -29,6 +41,8 @@
DEFAULT_COLOR = [1, 0.8, 0, 1]
DEFAULT_PT_SIZE = 7.5
DEFAULT_PT_COLOR = "darkviolet"
DEFAULT_CTRL_PT_COLOR = "crimson"
DEFAULT_CTRL_PT_SIZE = 7.5

SPECULAR = 0.3
SPECULAR_POWER = 100
Expand Down Expand Up @@ -144,6 +158,97 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkAssembly:
return rv


def ctrlPts(
s: Union[Face, Edge],
size: float = DEFAULT_CTRL_PT_SIZE,
color: str = DEFAULT_CTRL_PT_COLOR,
) -> vtkActor:
"""
Convert Edge or Face to a vtkActor representing control points.
"""

rv = vtkActor()

mapper = vtkPolyDataMapper()
points = vtkPoints()
cells = vtkCellArray()
data = vtkPolyData()

data.SetPoints(points)
data.SetVerts(cells)
data.SetLines(cells)

if isinstance(s, Face):

if isinstance(s._geomAdaptor(), Geom_BSplineSurface):
surf = cast(Geom_BSplineSurface, s._geomAdaptor())
else:
raise ValueError(
f"Only NURBS surfaces are supported, encountered {s._geomAdaptor()}"
)

Nu = surf.NbUPoles()
Nv = surf.NbVPoles()

u_periodic = surf.IsUPeriodic()
v_periodic = surf.IsVPeriodic()

# add points
for i in range(Nu):
for j in range(Nv):
pt = surf.Pole(i + 1, j + 1)
points.InsertNextPoint(pt.X(), pt.Y(), pt.Z())

# u edges
for j in range(Nv):
for i in range(Nu - 1):
cells.InsertNextCell(2, (Nv * i + j, Nv * (i + 1) + j))

if u_periodic:
cells.InsertNextCell(2, (Nv * (i + 1) + j, 0 + j))

# v edges
for i in range(Nu):
for j in range(Nv - 1):
cells.InsertNextCell(2, (Nv * i + j, Nv * i + j + 1))

if v_periodic:
cells.InsertNextCell(2, (Nv * i + j + 1, Nv * i + 0))

else:

if s.geomType() == "BSPLINE":
curve = s._geomAdaptor().BSpline()

else:
raise ValueError(
f"Only NURBS curves are supported, encountered {s.geomType()}"
)

for pt in curve.Poles():
points.InsertNextPoint(pt.X(), pt.Y(), pt.Z())

N = curve.NbPoles()

for i in range(N - 1):
cells.InsertNextCell(2, (i, i + 1))

if curve.IsPeriodic():
cells.InsertNextCell(2, (i + 1, 0))

mapper.SetInputData(data)

rv.SetMapper(mapper)

props = rv.GetProperty()
props.SetColor(vtkNamedColors().GetColor3d(color))
props.SetPointSize(size)
props.SetLineWidth(size / 3)
props.SetRenderPointsAsSpheres(True)

return rv


def show(
*objs: Showable,
scale: float = 0.2,
Expand Down
Binary file added doc/_static/ctrl_pts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 30 additions & 1 deletion doc/vis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Note that currently the show function is blocking.
Screenshots
===========

`:meth:~cadquery.vis.show` allows additionally to take screenshots in `png` format. One can specify zoom,
:meth:`~cadquery.vis.show` allows additionally to take screenshots in `png` format. One can specify zoom,
camera position and windows size.

.. code-block:: python
Expand All @@ -95,6 +95,35 @@ camera position and windows size.
NB: intermittent issues were observed with this functionality, please submit detailed bug reports in case
of problems.

Control points
==============

:meth:`~cadquery.vis.ctrlPts` allows to visualize control points of surfaces and curves.

.. code-block:: python
from cadquery.func import *
from cadquery.vis import *
c = circle(1).toSplines()
spine = spline([(0, 0, 0), (-3, -3, 5)], tgts=[(0, 0, 1), (0, -1, 0)])
f = sweep(c, spine)
show(
f,
ctrlPts(f),
spine.moved(x=7),
ctrlPts(spine.moved(x=7), color="green"),
alpha=0.0,
)
.. image:: _static/ctrl_pts.png

Note that for some geometries explicit conversion to spline representation might be needed.
:meth:`~cadquery.Shape.toSplines` performs approximate conversion and :meth:`~cadquery.vis.toNURBS`
performs exact one.


Jupyter/JupterLab
=================

Expand Down
2 changes: 1 addition & 1 deletion tests/test_free_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ def test_check():

s2 = sweep(rect(1, 1), segment((0, 0), (1, 1)))

assert not check(s2)
assert not check(s2, tol=1e-5)

res = []

Expand Down
35 changes: 33 additions & 2 deletions tests/test_vis.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from cadquery import Workplane, Assembly, Sketch, Location, Vector
from cadquery.vis import show, show_object, vtkAxesActor
from cadquery.func import circle, sweep, spline, plane, torus, loft
from cadquery.vis import show, show_object, vtkAxesActor, ctrlPts

import cadquery.vis as vis

from vtkmodules.vtkRenderingCore import (
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkWindowToImageFilter,
vtkActor,
)
from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor
from vtkmodules.vtkIOImage import vtkPNGWriter

from pytest import fixture
from pytest import fixture, raises
from path import Path


Expand Down Expand Up @@ -134,3 +136,32 @@ def test_screenshot(wp, tmpdir, monkeypatch):

with tmpdir:
show(wp, interact=False, screenshot="img.png", trihedron=False, gradient=False)


def test_ctrlPts():

c = circle(1)

# non-NURBS objects throw
with raises(ValueError):
ctrlPts(c)

# control points of a curve
a1 = ctrlPts(c.toNURBS())
assert isinstance(a1, vtkActor)

# control points of a non-periodic curve
a2 = ctrlPts(c.trim(0, 1).toNURBS())
assert isinstance(a2, vtkActor)

# non-NURBS objects throw
with raises(ValueError):
ctrlPts(plane(1, 1))

# control points of a surface
a3 = ctrlPts(sweep(c.trim(0, 1), spline((0, 0, 0), (0, 0, 1))))
assert isinstance(a3, vtkActor)

# control points of a u,v periodic surface
a4 = ctrlPts(torus(5, 1).faces().toNURBS())
assert isinstance(a4, vtkActor)

0 comments on commit 6772bfe

Please sign in to comment.