Skip to content

Commit 2e2db9a

Browse files
WIP: non-blocking show using trame/vtk (#1786)
* Adding fig * Mypy fixes * Fix tests * Run the asyncio loop in a thread * Revert some changes in vis * Smaller coros and bg color * Refactor into a singleton * Update deps * Implemented pop and fixed clear * Styling fix * Fix pop * Add simple data * Get rid of vtk msgs * Wait for clear * Display axis labels * Adding smoke test for fig * Run gui tests in appveyor * mypy fix * Misc fixes * Test GUI only on win * Coverage tweak * Fix test * Mypy fix * Change zoom reset behavior --------- Co-authored-by: adam-urbanczyk <13981538+adam-urbanczyk@users.noreply.github.com>
1 parent 8431073 commit 2e2db9a

File tree

11 files changed

+419
-39
lines changed

11 files changed

+419
-39
lines changed

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ build: false
3737
test_script:
3838
- mamba run -n cadquery black . --diff --check
3939
- mamba run -n cadquery mypy cadquery
40-
- mamba run -n cadquery pytest -v --cov
40+
- mamba run -n cadquery pytest -v --gui --cov
4141

4242
on_success:
4343
- mamba run -n cadquery codecov

cadquery/fig.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
from asyncio import (
2+
new_event_loop,
3+
set_event_loop,
4+
run_coroutine_threadsafe,
5+
AbstractEventLoop,
6+
)
7+
from concurrent.futures import Future
8+
from typing import Optional
9+
from threading import Thread
10+
from itertools import chain
11+
from webbrowser import open_new_tab
12+
13+
from typish import instance_of
14+
15+
from trame.app import get_server
16+
from trame.app.core import Server
17+
from trame.widgets import html, vtk as vtk_widgets, client
18+
from trame.ui.html import DivLayout
19+
20+
from . import Shape
21+
from .vis import style, Showable, ShapeLike, _split_showables
22+
23+
from vtkmodules.vtkRenderingCore import (
24+
vtkRenderer,
25+
vtkRenderWindow,
26+
vtkRenderWindowInteractor,
27+
vtkProp3D,
28+
)
29+
30+
31+
from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget
32+
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor
33+
34+
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
35+
36+
FULL_SCREEN = "position:absolute; left:0; top:0; width:100vw; height:100vh;"
37+
38+
39+
class Figure:
40+
41+
server: Server
42+
win: vtkRenderWindow
43+
ren: vtkRenderer
44+
view: vtk_widgets.VtkRemoteView
45+
shapes: dict[ShapeLike, list[vtkProp3D]]
46+
actors: list[vtkProp3D]
47+
loop: AbstractEventLoop
48+
thread: Thread
49+
empty: bool
50+
last: Optional[
51+
tuple[
52+
list[ShapeLike], list[vtkProp3D], Optional[list[vtkProp3D]], list[vtkProp3D]
53+
]
54+
]
55+
56+
_instance = None
57+
_initialized: bool = False
58+
59+
def __new__(cls, *args, **kwargs):
60+
61+
if not cls._instance:
62+
cls._instance = object.__new__(cls)
63+
64+
return cls._instance
65+
66+
def __init__(self, port: int = 18081):
67+
68+
if self._initialized:
69+
return
70+
71+
self.loop = new_event_loop()
72+
set_event_loop(self.loop)
73+
74+
# vtk boilerplate
75+
renderer = vtkRenderer()
76+
win = vtkRenderWindow()
77+
w, h = win.GetScreenSize()
78+
win.SetSize(w, h)
79+
win.AddRenderer(renderer)
80+
win.OffScreenRenderingOn()
81+
82+
inter = vtkRenderWindowInteractor()
83+
inter.SetInteractorStyle(vtkInteractorStyleTrackballCamera())
84+
inter.SetRenderWindow(win)
85+
86+
# background
87+
renderer.SetBackground(1, 1, 1)
88+
renderer.GradientBackgroundOn()
89+
90+
# axes
91+
axes = vtkAxesActor()
92+
axes.SetDragable(0)
93+
94+
orient_widget = vtkOrientationMarkerWidget()
95+
96+
orient_widget.SetOrientationMarker(axes)
97+
orient_widget.SetViewport(0.9, 0.0, 1.0, 0.2)
98+
orient_widget.SetZoom(1.1)
99+
orient_widget.SetInteractor(inter)
100+
orient_widget.SetCurrentRenderer(renderer)
101+
orient_widget.EnabledOn()
102+
orient_widget.InteractiveOff()
103+
104+
self.axes = axes
105+
self.orient_widget = orient_widget
106+
self.win = win
107+
self.ren = renderer
108+
109+
self.shapes = {}
110+
self.actors = []
111+
112+
# server
113+
server = get_server("CQ-server")
114+
server.client_type = "vue3"
115+
116+
# layout
117+
with DivLayout(server):
118+
client.Style("body { margin: 0; }")
119+
120+
with html.Div(style=FULL_SCREEN):
121+
self.view = vtk_widgets.VtkRemoteView(
122+
win, interactive_ratio=1, interactive_quality=100
123+
)
124+
125+
server.state.flush()
126+
127+
self.server = server
128+
self.loop = new_event_loop()
129+
130+
def _run_loop():
131+
set_event_loop(self.loop)
132+
self.loop.run_forever()
133+
134+
self.thread = Thread(target=_run_loop, daemon=True)
135+
self.thread.start()
136+
137+
coro = server.start(
138+
thread=True,
139+
exec_mode="coroutine",
140+
port=port,
141+
open_browser=False,
142+
show_connection_info=False,
143+
)
144+
145+
if coro:
146+
self._run(coro)
147+
148+
# prevent reinitialization
149+
self._initialized = True
150+
151+
# view is initialized as empty
152+
self.empty = True
153+
self.last = None
154+
155+
# open webbrowser
156+
open_new_tab(f"http://localhost:{port}")
157+
158+
def _run(self, coro) -> Future:
159+
160+
return run_coroutine_threadsafe(coro, self.loop)
161+
162+
def show(self, *showables: Showable | vtkProp3D | list[vtkProp3D], **kwargs):
163+
"""
164+
Show objects.
165+
"""
166+
167+
# split objects
168+
shapes, vecs, locs, props = _split_showables(showables)
169+
170+
pts = style(vecs, **kwargs)
171+
axs = style(locs, **kwargs)
172+
173+
for s in shapes:
174+
# do not show markers by default
175+
if "markersize" not in kwargs:
176+
kwargs["markersize"] = 0
177+
178+
actors = style(s, **kwargs)
179+
self.shapes[s] = actors
180+
181+
for actor in actors:
182+
self.ren.AddActor(actor)
183+
184+
for prop in chain(props, axs):
185+
self.actors.append(prop)
186+
self.ren.AddActor(prop)
187+
188+
if vecs:
189+
self.actors.append(*pts)
190+
self.ren.AddActor(*pts)
191+
192+
# store to enable pop
193+
self.last = (shapes, axs, pts if vecs else None, props)
194+
195+
async def _show():
196+
self.view.update()
197+
198+
self._run(_show())
199+
200+
# zoom to fit on 1st object added
201+
if self.empty:
202+
self.fit()
203+
self.empty = False
204+
205+
return self
206+
207+
def fit(self):
208+
"""
209+
Update view to fit all objects.
210+
"""
211+
212+
async def _show():
213+
self.ren.ResetCamera()
214+
self.view.update()
215+
216+
self._run(_show())
217+
218+
return self
219+
220+
def clear(self, *shapes: Shape | vtkProp3D):
221+
"""
222+
Clear specified objects. If no arguments are passed, clears all objects.
223+
"""
224+
225+
async def _clear():
226+
227+
if len(shapes) == 0:
228+
self.ren.RemoveAllViewProps()
229+
230+
self.actors.clear()
231+
self.shapes.clear()
232+
233+
for s in shapes:
234+
if instance_of(s, ShapeLike):
235+
for a in self.shapes[s]:
236+
self.ren.RemoveActor(a)
237+
238+
del self.shapes[s]
239+
else:
240+
self.actors.remove(s)
241+
self.ren.RemoveActor(s)
242+
243+
self.view.update()
244+
245+
# reset last, bc we don't want to keep track of what was removed
246+
self.last = None
247+
future = self._run(_clear())
248+
future.result()
249+
250+
return self
251+
252+
def pop(self):
253+
"""
254+
Clear the last showable.
255+
"""
256+
257+
async def _pop():
258+
259+
(shapes, axs, pts, props) = self.last
260+
261+
for s in shapes:
262+
for act in self.shapes.pop(s):
263+
self.ren.RemoveActor(act)
264+
265+
for act in chain(axs, props):
266+
self.ren.RemoveActor(act)
267+
self.actors.remove(act)
268+
269+
if pts:
270+
self.ren.RemoveActor(*pts)
271+
self.actors.remove(*pts)
272+
273+
self.view.update()
274+
275+
self._run(_pop())
276+
277+
return self

cadquery/occ_impl/assembly.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
vtkActor,
3636
vtkPolyDataMapper as vtkMapper,
3737
vtkRenderer,
38-
vtkAssembly,
38+
vtkProp3D,
3939
)
4040

4141
from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType
@@ -315,9 +315,9 @@ def toVTKAssy(
315315
linewidth: float = 2,
316316
tolerance: float = 1e-3,
317317
angularTolerance: float = 0.1,
318-
) -> vtkAssembly:
318+
) -> List[vtkProp3D]:
319319

320-
rv = vtkAssembly()
320+
rv: List[vtkProp3D] = []
321321

322322
for shape, _, loc, col_ in assy:
323323

@@ -358,7 +358,7 @@ def toVTKAssy(
358358
actor.GetProperty().SetColor(*col[:3])
359359
actor.GetProperty().SetOpacity(col[3])
360360

361-
rv.AddPart(actor)
361+
rv.append(actor)
362362

363363
mapper = vtkMapper()
364364
mapper.AddInputDataObject(data_edges)
@@ -370,9 +370,8 @@ def toVTKAssy(
370370
actor.GetProperty().SetLineWidth(linewidth)
371371
actor.SetVisibility(edges)
372372
actor.GetProperty().SetColor(*edgecolor[:3])
373-
actor.GetProperty().SetLineWidth(edgecolor[3])
374373

375-
rv.AddPart(actor)
374+
rv.append(actor)
376375

377376
return rv
378377

0 commit comments

Comments
 (0)