Skip to content

Commit 2698db5

Browse files
Per item styling in show + camera position (#1778)
* 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 * Adding per item styling to show * Refactor styling * Mypy fixes * Add some tests and fix prop handling * Fix alpha handling for non-shapes * Test camera position settings * Test camera position * Coverage tweaks * Add some docs * Add screenshots * Filename typo fix * Fix wrong kwarg name * Apply suggestions from code review Co-authored-by: Jeremy Wright <[email protected]> * Better edge styling * Apply suggestions --------- Co-authored-by: adam-urbanczyk <[email protected]> Co-authored-by: Jeremy Wright <[email protected]>
1 parent 5056dfc commit 2698db5

File tree

6 files changed

+344
-36
lines changed

6 files changed

+344
-36
lines changed

cadquery/occ_impl/assembly.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
vtkActor,
3232
vtkPolyDataMapper as vtkMapper,
3333
vtkRenderer,
34+
vtkAssembly,
3435
)
3536

3637
from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType
@@ -286,6 +287,76 @@ def _loc2vtk(
286287
return trans, (rot[1], rot[2], rot[0])
287288

288289

290+
def toVTKAssy(
291+
assy: AssemblyProtocol,
292+
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
293+
edgecolor: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
294+
edges: bool = True,
295+
linewidth: float = 2,
296+
tolerance: float = 1e-3,
297+
angularTolerance: float = 0.1,
298+
) -> vtkAssembly:
299+
300+
rv = vtkAssembly()
301+
302+
for shape, _, loc, col_ in assy:
303+
304+
col = col_.toTuple() if col_ else color
305+
306+
trans, rot = _loc2vtk(loc)
307+
308+
data = shape.toVtkPolyData(tolerance, angularTolerance)
309+
310+
# extract faces
311+
extr = vtkExtractCellsByType()
312+
extr.SetInputDataObject(data)
313+
314+
extr.AddCellType(VTK_LINE)
315+
extr.AddCellType(VTK_VERTEX)
316+
extr.Update()
317+
data_edges = extr.GetOutput()
318+
319+
# extract edges
320+
extr = vtkExtractCellsByType()
321+
extr.SetInputDataObject(data)
322+
323+
extr.AddCellType(VTK_TRIANGLE)
324+
extr.Update()
325+
data_faces = extr.GetOutput()
326+
327+
# remove normals from edges
328+
data_edges.GetPointData().RemoveArray("Normals")
329+
330+
# add both to the vtkAssy
331+
mapper = vtkMapper()
332+
mapper.AddInputDataObject(data_faces)
333+
334+
actor = vtkActor()
335+
actor.SetMapper(mapper)
336+
actor.SetPosition(*trans)
337+
actor.SetOrientation(*rot)
338+
actor.GetProperty().SetColor(*col[:3])
339+
actor.GetProperty().SetOpacity(col[3])
340+
341+
rv.AddPart(actor)
342+
343+
mapper = vtkMapper()
344+
mapper.AddInputDataObject(data_edges)
345+
346+
actor = vtkActor()
347+
actor.SetMapper(mapper)
348+
actor.SetPosition(*trans)
349+
actor.SetOrientation(*rot)
350+
actor.GetProperty().SetLineWidth(linewidth)
351+
actor.SetVisibility(edges)
352+
actor.GetProperty().SetColor(*edgecolor[:3])
353+
actor.GetProperty().SetLineWidth(edgecolor[3])
354+
355+
rv.AddPart(actor)
356+
357+
return rv
358+
359+
289360
def toVTK(
290361
assy: AssemblyProtocol,
291362
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
@@ -343,8 +414,6 @@ def toVTK(
343414
actor.SetMapper(mapper)
344415
actor.SetPosition(*trans)
345416
actor.SetOrientation(*rot)
346-
actor.GetProperty().SetColor(0, 0, 0)
347-
actor.GetProperty().SetLineWidth(2)
348417

349418
renderer.AddActor(actor)
350419

cadquery/vis.py

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
Face,
1111
Edge,
1212
)
13-
from .occ_impl.assembly import _loc2vtk, toVTK
13+
from .occ_impl.assembly import _loc2vtk, toVTKAssy
1414

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

@@ -26,19 +26,22 @@
2626
vtkMapper,
2727
vtkRenderWindowInteractor,
2828
vtkActor,
29-
vtkProp,
29+
vtkProp3D,
3030
vtkPolyDataMapper,
3131
vtkAssembly,
3232
vtkRenderWindow,
3333
vtkWindowToImageFilter,
34+
vtkRenderer,
35+
vtkPropCollection,
3436
)
3537
from vtkmodules.vtkCommonCore import vtkPoints
3638
from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData
3739
from vtkmodules.vtkCommonColor import vtkNamedColors
3840
from vtkmodules.vtkIOImage import vtkPNGWriter
3941

4042

41-
DEFAULT_COLOR = [1, 0.8, 0, 1]
43+
DEFAULT_COLOR = (1, 0.8, 0)
44+
DEFAULT_EDGE_COLOR = (0, 0, 0)
4245
DEFAULT_PT_SIZE = 7.5
4346
DEFAULT_PT_COLOR = "darkviolet"
4447
DEFAULT_CTRL_PT_COLOR = "crimson"
@@ -50,15 +53,20 @@
5053

5154
ShapeLike = Union[Shape, Workplane, Assembly, Sketch, TopoDS_Shape]
5255
Showable = Union[
53-
ShapeLike, List[ShapeLike], Vector, List[Vector], vtkProp, List[vtkProp]
56+
ShapeLike, List[ShapeLike], Vector, List[Vector], vtkProp3D, List[vtkProp3D]
5457
]
5558

5659

57-
def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly:
60+
def _to_assy(
61+
*objs: ShapeLike,
62+
color: Tuple[float, float, float] = DEFAULT_COLOR,
63+
alpha: float = 1,
64+
) -> Assembly:
65+
"""
66+
Convert shapes to Assembly.
67+
"""
5868

59-
assy = Assembly(
60-
color=Color(DEFAULT_COLOR[0], DEFAULT_COLOR[1], DEFAULT_COLOR[2], alpha)
61-
)
69+
assy = Assembly(color=Color(*color, alpha))
6270

6371
for obj in objs:
6472
if isinstance(obj, (Shape, Workplane, Assembly)):
@@ -75,15 +83,15 @@ def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly:
7583

7684
def _split_showables(
7785
objs,
78-
) -> Tuple[List[ShapeLike], List[Vector], List[Location], List[vtkProp]]:
86+
) -> Tuple[List[ShapeLike], List[Vector], List[Location], List[vtkProp3D]]:
7987
"""
8088
Split into showables and others.
8189
"""
8290

8391
rv_s: List[ShapeLike] = []
8492
rv_v: List[Vector] = []
8593
rv_l: List[Location] = []
86-
rv_a: List[vtkProp] = []
94+
rv_a: List[vtkProp3D] = []
8795

8896
for el in objs:
8997
if instance_of(el, ShapeLike):
@@ -92,7 +100,7 @@ def _split_showables(
92100
rv_v.append(el)
93101
elif isinstance(el, Location):
94102
rv_l.append(el)
95-
elif isinstance(el, vtkProp):
103+
elif isinstance(el, vtkProp3D):
96104
rv_a.append(el)
97105
elif isinstance(el, list):
98106
tmp1, tmp2, tmp3, tmp4 = _split_showables(el) # split recursively
@@ -158,6 +166,28 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkAssembly:
158166
return rv
159167

160168

169+
def _to_vtk_shapes(
170+
obj: List[ShapeLike],
171+
color: Tuple[float, float, float] = DEFAULT_COLOR,
172+
edgecolor: Tuple[float, float, float] = DEFAULT_EDGE_COLOR,
173+
edges: bool = True,
174+
linewidth: float = 2,
175+
alpha: float = 1,
176+
tolerance: float = 1e-3,
177+
) -> vtkAssembly:
178+
"""
179+
Convert Shapes to vtkAssembly.
180+
"""
181+
182+
return toVTKAssy(
183+
_to_assy(*obj, color=color, alpha=alpha),
184+
edgecolor=(*edgecolor, 1),
185+
edges=edges,
186+
linewidth=linewidth,
187+
tolerance=tolerance,
188+
)
189+
190+
161191
def ctrlPts(
162192
s: Union[Face, Edge],
163193
size: float = DEFAULT_CTRL_PT_SIZE,
@@ -249,6 +279,105 @@ def ctrlPts(
249279
return rv
250280

251281

282+
def _iterate_actors(obj: Union[vtkProp3D, vtkActor, vtkAssembly]) -> Iterable[vtkActor]:
283+
"""
284+
Iterate over vtkActors, other props are ignored.
285+
"""
286+
if isinstance(obj, vtkActor):
287+
yield obj
288+
elif isinstance(obj, vtkAssembly):
289+
coll = vtkPropCollection()
290+
obj.GetActors(coll)
291+
292+
coll.InitTraversal()
293+
for i in range(0, coll.GetNumberOfItems()):
294+
prop = coll.GetNextProp()
295+
if isinstance(prop, vtkActor):
296+
yield prop
297+
298+
299+
def style(
300+
obj: Showable,
301+
scale: float = 0.2,
302+
alpha: float = 1,
303+
tolerance: float = 1e-2,
304+
edges: bool = True,
305+
mesh: bool = False,
306+
specular: bool = True,
307+
markersize: float = 5,
308+
linewidth: float = 2,
309+
spheres: bool = False,
310+
tubes: bool = False,
311+
color: str = "gold",
312+
edgecolor: str = "black",
313+
meshcolor: str = "lightgrey",
314+
vertexcolor: str = "cyan",
315+
**kwargs,
316+
) -> Union[vtkActor, vtkAssembly]:
317+
"""
318+
Apply styling to CQ objects. To be used in conjunction with show.
319+
"""
320+
321+
# styling functions
322+
def _apply_style(actor):
323+
props = actor.GetProperty()
324+
props.SetEdgeColor(vtkNamedColors().GetColor3d(meshcolor))
325+
props.SetVertexColor(vtkNamedColors().GetColor3d(vertexcolor))
326+
props.SetPointSize(markersize)
327+
props.SetLineWidth(linewidth)
328+
props.SetRenderPointsAsSpheres(spheres)
329+
props.SetRenderLinesAsTubes(tubes)
330+
props.SetEdgeVisibility(mesh)
331+
332+
if specular:
333+
props.SetSpecular(SPECULAR)
334+
props.SetSpecularPower(SPECULAR_POWER)
335+
props.SetSpecularColor(SPECULAR_COLOR)
336+
337+
def _apply_color(actor):
338+
props = actor.GetProperty()
339+
props.SetColor(vtkNamedColors().GetColor3d(color))
340+
props.SetOpacity(alpha)
341+
342+
# split showables
343+
shapes, vecs, locs, actors = _split_showables([obj,])
344+
345+
# convert to a prop
346+
rv: Union[vtkActor, vtkAssembly]
347+
348+
if shapes:
349+
rv = _to_vtk_shapes(
350+
shapes,
351+
color=vtkNamedColors().GetColor3d(color),
352+
edgecolor=vtkNamedColors().GetColor3d(edgecolor),
353+
edges=edges,
354+
linewidth=linewidth,
355+
alpha=alpha,
356+
tolerance=tolerance,
357+
)
358+
359+
# apply style to every actor
360+
for a in _iterate_actors(rv):
361+
_apply_style(a)
362+
363+
elif vecs:
364+
rv = _to_vtk_pts(vecs)
365+
_apply_style(rv)
366+
_apply_color(rv)
367+
elif locs:
368+
rv = _to_vtk_axs(locs, scale=scale)
369+
else:
370+
rv = vtkAssembly()
371+
372+
for p in actors:
373+
for a in _iterate_actors(p):
374+
_apply_style(a)
375+
_apply_color(a)
376+
rv.AddPart(a)
377+
378+
return rv
379+
380+
252381
def show(
253382
*objs: Showable,
254383
scale: float = 0.2,
@@ -262,14 +391,15 @@ def show(
262391
zoom: float = 1.0,
263392
roll: float = -35,
264393
elevation: float = -45,
394+
position: Optional[Tuple[float, float, float]] = None,
395+
focus: Optional[Tuple[float, float, float]] = None,
265396
width: Union[int, float] = 0.5,
266397
height: Union[int, float] = 0.5,
267398
trihedron: bool = True,
268399
bgcolor: tuple[float, float, float] = (1, 1, 1),
269400
gradient: bool = True,
270401
xpos: Union[int, float] = 0,
271402
ypos: Union[int, float] = 0,
272-
**kwrags: Any,
273403
):
274404
"""
275405
Show CQ objects using VTK. This functions optionally allows to make screenshots.
@@ -286,7 +416,8 @@ def show(
286416
axs = _to_vtk_axs(locs, scale=scale)
287417

288418
# assy+renderer
289-
renderer = toVTK(assy, tolerance=tolerance)
419+
renderer = vtkRenderer()
420+
renderer.AddActor(toVTKAssy(assy, tolerance=tolerance))
290421

291422
# VTK window boilerplate
292423
win = vtkRenderWindow()
@@ -350,13 +481,6 @@ def show(
350481
# use FXXAA
351482
renderer.UseFXAAOn()
352483

353-
# set camera
354-
camera = renderer.GetActiveCamera()
355-
camera.Roll(roll)
356-
camera.Elevation(elevation)
357-
renderer.ResetCamera()
358-
camera.Zoom(zoom)
359-
360484
# add pts and locs
361485
renderer.AddActor(pts)
362486
renderer.AddActor(axs)
@@ -365,6 +489,20 @@ def show(
365489
for p in props:
366490
renderer.AddActor(p)
367491

492+
# set camera
493+
camera = renderer.GetActiveCamera()
494+
camera.Roll(roll)
495+
camera.Elevation(elevation)
496+
camera.Zoom(zoom)
497+
498+
if position or focus:
499+
if position:
500+
camera.SetPosition(*position)
501+
if focus:
502+
camera.SetFocalPoint(*focus)
503+
else:
504+
renderer.ResetCamera()
505+
368506
# initialize and set size
369507
inter.Initialize()
370508

doc/_static/show_camera_position.png

638 KB
Loading

doc/_static/show_styling.png

989 KB
Loading

0 commit comments

Comments
 (0)