Skip to content

Commit 6772bfe

Browse files
Control point visualization tools (#1765)
* 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]>
1 parent 9e47392 commit 6772bfe

File tree

6 files changed

+188
-6
lines changed

6 files changed

+188
-6
lines changed

cadquery/occ_impl/shapes.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
BRepBuilderAPI_RightCorner,
8989
BRepBuilderAPI_RoundCorner,
9090
BRepBuilderAPI_MakeSolid,
91+
BRepBuilderAPI_NurbsConvert,
9192
)
9293

9394
# properties used to store mass calculation result
@@ -1488,6 +1489,15 @@ def toSplines(
14881489

14891490
return self.__class__(result)
14901491

1492+
def toNURBS(self: T,) -> T:
1493+
"""
1494+
Return a NURBS representation of a given shape.
1495+
"""
1496+
1497+
bldr = BRepBuilderAPI_NurbsConvert(self.wrapped, Copy=True)
1498+
1499+
return self.__class__(bldr.Shape())
1500+
14911501
def toVtkPolyData(
14921502
self,
14931503
tolerance: Optional[float] = None,
@@ -5888,7 +5898,11 @@ def loft(
58885898
#%% diagnotics
58895899

58905900

5891-
def check(s: Shape, results: Optional[List[Tuple[List[Shape], Any]]] = None) -> bool:
5901+
def check(
5902+
s: Shape,
5903+
results: Optional[List[Tuple[List[Shape], Any]]] = None,
5904+
tol: Optional[float] = None,
5905+
) -> bool:
58925906
"""
58935907
Check if a shape is valid.
58945908
"""
@@ -5901,6 +5915,9 @@ def check(s: Shape, results: Optional[List[Tuple[List[Shape], Any]]] = None) ->
59015915

59025916
rv = analyzer.IsValid()
59035917

5918+
if tol:
5919+
analyzer.SetFuzzyValue(tol)
5920+
59045921
# output detailed results if requested
59055922
if results is not None:
59065923
results.clear()

cadquery/vis.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
from . import Shape, Workplane, Assembly, Sketch, Compound, Color, Vector, Location
1+
from . import (
2+
Shape,
3+
Workplane,
4+
Assembly,
5+
Sketch,
6+
Compound,
7+
Color,
8+
Vector,
9+
Location,
10+
Face,
11+
Edge,
12+
)
213
from .occ_impl.assembly import _loc2vtk, toVTK
314

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

617
from typish import instance_of
718

819
from OCP.TopoDS import TopoDS_Shape
20+
from OCP.Geom import Geom_BSplineSurface
921

1022
from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget
1123
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor
@@ -29,6 +41,8 @@
2941
DEFAULT_COLOR = [1, 0.8, 0, 1]
3042
DEFAULT_PT_SIZE = 7.5
3143
DEFAULT_PT_COLOR = "darkviolet"
44+
DEFAULT_CTRL_PT_COLOR = "crimson"
45+
DEFAULT_CTRL_PT_SIZE = 7.5
3246

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

146160

161+
def ctrlPts(
162+
s: Union[Face, Edge],
163+
size: float = DEFAULT_CTRL_PT_SIZE,
164+
color: str = DEFAULT_CTRL_PT_COLOR,
165+
) -> vtkActor:
166+
"""
167+
Convert Edge or Face to a vtkActor representing control points.
168+
"""
169+
170+
rv = vtkActor()
171+
172+
mapper = vtkPolyDataMapper()
173+
points = vtkPoints()
174+
cells = vtkCellArray()
175+
data = vtkPolyData()
176+
177+
data.SetPoints(points)
178+
data.SetVerts(cells)
179+
data.SetLines(cells)
180+
181+
if isinstance(s, Face):
182+
183+
if isinstance(s._geomAdaptor(), Geom_BSplineSurface):
184+
surf = cast(Geom_BSplineSurface, s._geomAdaptor())
185+
else:
186+
raise ValueError(
187+
f"Only NURBS surfaces are supported, encountered {s._geomAdaptor()}"
188+
)
189+
190+
Nu = surf.NbUPoles()
191+
Nv = surf.NbVPoles()
192+
193+
u_periodic = surf.IsUPeriodic()
194+
v_periodic = surf.IsVPeriodic()
195+
196+
# add points
197+
for i in range(Nu):
198+
for j in range(Nv):
199+
pt = surf.Pole(i + 1, j + 1)
200+
points.InsertNextPoint(pt.X(), pt.Y(), pt.Z())
201+
202+
# u edges
203+
for j in range(Nv):
204+
for i in range(Nu - 1):
205+
cells.InsertNextCell(2, (Nv * i + j, Nv * (i + 1) + j))
206+
207+
if u_periodic:
208+
cells.InsertNextCell(2, (Nv * (i + 1) + j, 0 + j))
209+
210+
# v edges
211+
for i in range(Nu):
212+
for j in range(Nv - 1):
213+
cells.InsertNextCell(2, (Nv * i + j, Nv * i + j + 1))
214+
215+
if v_periodic:
216+
cells.InsertNextCell(2, (Nv * i + j + 1, Nv * i + 0))
217+
218+
else:
219+
220+
if s.geomType() == "BSPLINE":
221+
curve = s._geomAdaptor().BSpline()
222+
223+
else:
224+
raise ValueError(
225+
f"Only NURBS curves are supported, encountered {s.geomType()}"
226+
)
227+
228+
for pt in curve.Poles():
229+
points.InsertNextPoint(pt.X(), pt.Y(), pt.Z())
230+
231+
N = curve.NbPoles()
232+
233+
for i in range(N - 1):
234+
cells.InsertNextCell(2, (i, i + 1))
235+
236+
if curve.IsPeriodic():
237+
cells.InsertNextCell(2, (i + 1, 0))
238+
239+
mapper.SetInputData(data)
240+
241+
rv.SetMapper(mapper)
242+
243+
props = rv.GetProperty()
244+
props.SetColor(vtkNamedColors().GetColor3d(color))
245+
props.SetPointSize(size)
246+
props.SetLineWidth(size / 3)
247+
props.SetRenderPointsAsSpheres(True)
248+
249+
return rv
250+
251+
147252
def show(
148253
*objs: Showable,
149254
scale: float = 0.2,

doc/_static/ctrl_pts.png

961 KB
Loading

doc/vis.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Note that currently the show function is blocking.
8080
Screenshots
8181
===========
8282

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

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

98+
Control points
99+
==============
100+
101+
:meth:`~cadquery.vis.ctrlPts` allows to visualize control points of surfaces and curves.
102+
103+
.. code-block:: python
104+
105+
from cadquery.func import *
106+
from cadquery.vis import *
107+
108+
c = circle(1).toSplines()
109+
spine = spline([(0, 0, 0), (-3, -3, 5)], tgts=[(0, 0, 1), (0, -1, 0)])
110+
f = sweep(c, spine)
111+
112+
show(
113+
f,
114+
ctrlPts(f),
115+
spine.moved(x=7),
116+
ctrlPts(spine.moved(x=7), color="green"),
117+
alpha=0.0,
118+
)
119+
120+
.. image:: _static/ctrl_pts.png
121+
122+
Note that for some geometries explicit conversion to spline representation might be needed.
123+
:meth:`~cadquery.Shape.toSplines` performs approximate conversion and :meth:`~cadquery.vis.toNURBS`
124+
performs exact one.
125+
126+
98127
Jupyter/JupterLab
99128
=================
100129

tests/test_free_functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ def test_check():
692692

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

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

697697
res = []
698698

tests/test_vis.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from cadquery import Workplane, Assembly, Sketch, Location, Vector
2-
from cadquery.vis import show, show_object, vtkAxesActor
2+
from cadquery.func import circle, sweep, spline, plane, torus, loft
3+
from cadquery.vis import show, show_object, vtkAxesActor, ctrlPts
34

45
import cadquery.vis as vis
56

67
from vtkmodules.vtkRenderingCore import (
78
vtkRenderWindow,
89
vtkRenderWindowInteractor,
910
vtkWindowToImageFilter,
11+
vtkActor,
1012
)
1113
from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor
1214
from vtkmodules.vtkIOImage import vtkPNGWriter
1315

14-
from pytest import fixture
16+
from pytest import fixture, raises
1517
from path import Path
1618

1719

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

135137
with tmpdir:
136138
show(wp, interact=False, screenshot="img.png", trihedron=False, gradient=False)
139+
140+
141+
def test_ctrlPts():
142+
143+
c = circle(1)
144+
145+
# non-NURBS objects throw
146+
with raises(ValueError):
147+
ctrlPts(c)
148+
149+
# control points of a curve
150+
a1 = ctrlPts(c.toNURBS())
151+
assert isinstance(a1, vtkActor)
152+
153+
# control points of a non-periodic curve
154+
a2 = ctrlPts(c.trim(0, 1).toNURBS())
155+
assert isinstance(a2, vtkActor)
156+
157+
# non-NURBS objects throw
158+
with raises(ValueError):
159+
ctrlPts(plane(1, 1))
160+
161+
# control points of a surface
162+
a3 = ctrlPts(sweep(c.trim(0, 1), spline((0, 0, 0), (0, 0, 1))))
163+
assert isinstance(a3, vtkActor)
164+
165+
# control points of a u,v periodic surface
166+
a4 = ctrlPts(torus(5, 1).faces().toNURBS())
167+
assert isinstance(a4, vtkActor)

0 commit comments

Comments
 (0)