protocols.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842
  1. r"""protocols is a module that contains a set of VTK Web related
  2. protocols that can be combined together to provide a flexible way to define
  3. very specific web application.
  4. """
  5. from __future__ import absolute_import, division, print_function
  6. import os, sys, logging, types, inspect, traceback, re, base64, time
  7. from vtkmodules.vtkWebCore import vtkWebInteractionEvent
  8. from vtkmodules.web.errors import WebDependencyMissingError
  9. from vtkmodules.web.render_window_serializer import (
  10. serializeInstance,
  11. SynchronizationContext,
  12. getReferenceId,
  13. initializeSerializers,
  14. )
  15. try:
  16. from wslink import schedule_callback
  17. from wslink import register as exportRpc
  18. from wslink.websocket import LinkProtocol
  19. except ImportError:
  20. raise WebDependencyMissingError()
  21. # =============================================================================
  22. #
  23. # Base class for any VTK Web based protocol
  24. #
  25. # =============================================================================
  26. class vtkWebProtocol(LinkProtocol):
  27. def getApplication(self):
  28. return self.getSharedObject("app")
  29. # no need for a setApplication anymore, but keep for compatibility
  30. def setApplication(self, app):
  31. pass
  32. def mapIdToObject(self, id):
  33. """
  34. Maps global-id for a vtkObject to the vtkObject instance. May return None if the
  35. id is not valid.
  36. """
  37. id = int(id)
  38. if id <= 0:
  39. return None
  40. return self.getApplication().GetObjectIdMap().GetVTKObject(id)
  41. def getGlobalId(self, obj):
  42. """
  43. Return the id for a given vtkObject
  44. """
  45. return self.getApplication().GetObjectIdMap().GetGlobalId(obj)
  46. def freeObject(self, obj):
  47. """
  48. Delete the given vtkObject from the objectIdMap. Returns true if delete succeeded.
  49. """
  50. return self.getApplication().GetObjectIdMap().FreeObject(obj)
  51. def freeObjectById(self, id):
  52. """
  53. Delete the vtkObject corresponding to the given objectId from the objectIdMap.
  54. Returns true if delete succeeded.
  55. """
  56. return self.getApplication().GetObjectIdMap().FreeObjectById(id)
  57. def getView(self, vid):
  58. """
  59. Returns the view for a given view ID, if vid is None then return the
  60. current active view.
  61. :param vid: The view ID
  62. :type vid: str
  63. """
  64. v = self.mapIdToObject(vid)
  65. if not v:
  66. # Use active view is none provided.
  67. v = self.getApplication().GetObjectIdMap().GetActiveObject("VIEW")
  68. if not v:
  69. raise Exception("no view provided: %s" % vid)
  70. return v
  71. def setActiveView(self, view):
  72. """
  73. Set a vtkRenderWindow to be the active one
  74. """
  75. self.getApplication().GetObjectIdMap().SetActiveObject("VIEW", view)
  76. # =============================================================================
  77. #
  78. # Handle Mouse interaction on any type of view
  79. #
  80. # =============================================================================
  81. class vtkWebMouseHandler(vtkWebProtocol):
  82. @exportRpc("viewport.mouse.interaction")
  83. def mouseInteraction(self, event):
  84. """
  85. RPC Callback for mouse interactions.
  86. """
  87. view = self.getView(event["view"])
  88. buttons = 0
  89. if event["buttonLeft"]:
  90. buttons |= vtkWebInteractionEvent.LEFT_BUTTON
  91. if event["buttonMiddle"]:
  92. buttons |= vtkWebInteractionEvent.MIDDLE_BUTTON
  93. if event["buttonRight"]:
  94. buttons |= vtkWebInteractionEvent.RIGHT_BUTTON
  95. modifiers = 0
  96. if event["shiftKey"]:
  97. modifiers |= vtkWebInteractionEvent.SHIFT_KEY
  98. if event["ctrlKey"]:
  99. modifiers |= vtkWebInteractionEvent.CTRL_KEY
  100. if event["altKey"]:
  101. modifiers |= vtkWebInteractionEvent.ALT_KEY
  102. if event["metaKey"]:
  103. modifiers |= vtkWebInteractionEvent.META_KEY
  104. pvevent = vtkWebInteractionEvent()
  105. pvevent.SetButtons(buttons)
  106. pvevent.SetModifiers(modifiers)
  107. if "x" in event:
  108. pvevent.SetX(event["x"])
  109. if "y" in event:
  110. pvevent.SetY(event["y"])
  111. if "scroll" in event:
  112. pvevent.SetScroll(event["scroll"])
  113. if event["action"] == "dblclick":
  114. pvevent.SetRepeatCount(2)
  115. # pvevent.SetKeyCode(event["charCode"])
  116. retVal = self.getApplication().HandleInteractionEvent(view, pvevent)
  117. del pvevent
  118. if event["action"] == "down":
  119. self.getApplication().InvokeEvent("StartInteractionEvent")
  120. if event["action"] == "up":
  121. self.getApplication().InvokeEvent("EndInteractionEvent")
  122. if retVal:
  123. self.getApplication().InvokeEvent("UpdateEvent")
  124. return retVal
  125. @exportRpc("viewport.mouse.zoom.wheel")
  126. def updateZoomFromWheel(self, event):
  127. if "Start" in event["type"]:
  128. self.getApplication().InvokeEvent("StartInteractionEvent")
  129. renderWindow = self.getView(event["view"])
  130. if renderWindow and "spinY" in event:
  131. zoomFactor = 1.0 - event["spinY"] / 10.0
  132. camera = renderWindow.GetRenderers().GetFirstRenderer().GetActiveCamera()
  133. fp = camera.GetFocalPoint()
  134. pos = camera.GetPosition()
  135. delta = [fp[i] - pos[i] for i in range(3)]
  136. camera.Zoom(zoomFactor)
  137. pos2 = camera.GetPosition()
  138. camera.SetFocalPoint([pos2[i] + delta[i] for i in range(3)])
  139. renderWindow.Modified()
  140. if "End" in event["type"]:
  141. self.getApplication().InvokeEvent("EndInteractionEvent")
  142. # =============================================================================
  143. #
  144. # Basic 3D Viewport API (Camera + Orientation + CenterOfRotation
  145. #
  146. # =============================================================================
  147. class vtkWebViewPort(vtkWebProtocol):
  148. @exportRpc("viewport.camera.reset")
  149. def resetCamera(self, viewId):
  150. """
  151. RPC callback to reset camera.
  152. """
  153. view = self.getView(viewId)
  154. renderer = view.GetRenderers().GetFirstRenderer()
  155. renderer.ResetCamera()
  156. self.getApplication().InvalidateCache(view)
  157. self.getApplication().InvokeEvent("UpdateEvent")
  158. return str(self.getGlobalId(view))
  159. @exportRpc("viewport.axes.orientation.visibility.update")
  160. def updateOrientationAxesVisibility(self, viewId, showAxis):
  161. """
  162. RPC callback to show/hide OrientationAxis.
  163. """
  164. view = self.getView(viewId)
  165. # FIXME seb: view.OrientationAxesVisibility = (showAxis if 1 else 0);
  166. self.getApplication().InvalidateCache(view)
  167. self.getApplication().InvokeEvent("UpdateEvent")
  168. return str(self.getGlobalId(view))
  169. @exportRpc("viewport.axes.center.visibility.update")
  170. def updateCenterAxesVisibility(self, viewId, showAxis):
  171. """
  172. RPC callback to show/hide CenterAxesVisibility.
  173. """
  174. view = self.getView(viewId)
  175. # FIXME seb: view.CenterAxesVisibility = (showAxis if 1 else 0);
  176. self.getApplication().InvalidateCache(view)
  177. self.getApplication().InvokeEvent("UpdateEvent")
  178. return str(self.getGlobalId(view))
  179. @exportRpc("viewport.camera.update")
  180. def updateCamera(self, view_id, focal_point, view_up, position, forceUpdate=True):
  181. view = self.getView(view_id)
  182. camera = view.GetRenderers().GetFirstRenderer().GetActiveCamera()
  183. camera.SetFocalPoint(focal_point)
  184. camera.SetViewUp(view_up)
  185. camera.SetPosition(position)
  186. if forceUpdate:
  187. self.getApplication().InvalidateCache(view)
  188. self.getApplication().InvokeEvent("UpdateEvent")
  189. # =============================================================================
  190. #
  191. # Provide Image delivery mechanism (deprecated - will be removed in VTK 10+)
  192. #
  193. # =============================================================================
  194. class vtkWebViewPortImageDelivery(vtkWebProtocol):
  195. @exportRpc("viewport.image.render")
  196. def stillRender(self, options):
  197. """
  198. RPC Callback to render a view and obtain the rendered image.
  199. """
  200. beginTime = int(round(time.time() * 1000))
  201. view = self.getView(options["view"])
  202. size = [view.GetSize()[0], view.GetSize()[1]]
  203. # use existing size, overridden only if options["size"] is set.
  204. resize = size != options.get("size", size)
  205. if resize:
  206. size = options["size"]
  207. if size[0] > 0 and size[1] > 0:
  208. view.SetSize(size)
  209. t = 0
  210. if options and "mtime" in options:
  211. t = options["mtime"]
  212. quality = 100
  213. if options and "quality" in options:
  214. quality = options["quality"]
  215. localTime = 0
  216. if options and "localTime" in options:
  217. localTime = options["localTime"]
  218. reply = {}
  219. app = self.getApplication()
  220. if t == 0:
  221. app.InvalidateCache(view)
  222. reply["image"] = app.StillRenderToString(view, t, quality)
  223. # Check that we are getting image size we have set. If not, wait until we
  224. # do. The render call will set the actual window size.
  225. tries = 10
  226. while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
  227. app.InvalidateCache(view)
  228. reply["image"] = app.StillRenderToString(view, t, quality)
  229. tries -= 1
  230. reply["stale"] = app.GetHasImagesBeingProcessed(view)
  231. reply["mtime"] = app.GetLastStillRenderToMTime()
  232. reply["size"] = [view.GetSize()[0], view.GetSize()[1]]
  233. reply["format"] = "jpeg;base64"
  234. reply["global_id"] = str(self.getGlobalId(view))
  235. reply["localTime"] = localTime
  236. endTime = int(round(time.time() * 1000))
  237. reply["workTime"] = endTime - beginTime
  238. return reply
  239. # =============================================================================
  240. #
  241. # Provide publish-based Image delivery mechanism
  242. #
  243. # =============================================================================
  244. class vtkWebPublishImageDelivery(vtkWebProtocol):
  245. def __init__(self, decode=True):
  246. super(vtkWebPublishImageDelivery, self).__init__()
  247. self.trackingViews = {}
  248. self.lastStaleTime = 0
  249. self.staleHandlerCount = 0
  250. self.deltaStaleTimeBeforeRender = 0.5 # 0.5s
  251. self.decode = decode
  252. self.viewsInAnimations = []
  253. self.targetFrameRate = 30.0
  254. self.minFrameRate = 12.0
  255. self.maxFrameRate = 30.0
  256. def pushRender(self, vId, ignoreAnimation=False):
  257. if vId not in self.trackingViews:
  258. return
  259. if not self.trackingViews[vId]["enabled"]:
  260. return
  261. if not ignoreAnimation and len(self.viewsInAnimations) > 0:
  262. return
  263. if "originalSize" not in self.trackingViews[vId]:
  264. view = self.getView(vId)
  265. self.trackingViews[vId]["originalSize"] = list(view.GetSize())
  266. if "ratio" not in self.trackingViews[vId]:
  267. self.trackingViews[vId]["ratio"] = 1
  268. ratio = self.trackingViews[vId]["ratio"]
  269. mtime = self.trackingViews[vId]["mtime"]
  270. quality = self.trackingViews[vId]["quality"]
  271. size = [int(s * ratio) for s in self.trackingViews[vId]["originalSize"]]
  272. reply = self.stillRender(
  273. {"view": vId, "mtime": mtime, "quality": quality, "size": size}
  274. )
  275. stale = reply["stale"]
  276. if reply["image"]:
  277. # depending on whether the app has encoding enabled:
  278. if self.decode:
  279. reply["image"] = base64.standard_b64decode(reply["image"])
  280. reply["image"] = self.addAttachment(reply["image"])
  281. reply["format"] = "jpeg"
  282. # save mtime for next call.
  283. self.trackingViews[vId]["mtime"] = reply["mtime"]
  284. # echo back real ID, instead of -1 for 'active'
  285. reply["id"] = vId
  286. self.publish("viewport.image.push.subscription", reply)
  287. if stale:
  288. self.lastStaleTime = time.time()
  289. if self.staleHandlerCount == 0:
  290. self.staleHandlerCount += 1
  291. schedule_callback(
  292. self.deltaStaleTimeBeforeRender, lambda: self.renderStaleImage(vId)
  293. )
  294. else:
  295. self.lastStaleTime = 0
  296. def renderStaleImage(self, vId):
  297. self.staleHandlerCount -= 1
  298. if self.lastStaleTime != 0:
  299. delta = time.time() - self.lastStaleTime
  300. if delta >= self.deltaStaleTimeBeforeRender:
  301. self.pushRender(vId)
  302. else:
  303. self.staleHandlerCount += 1
  304. schedule_callback(
  305. self.deltaStaleTimeBeforeRender - delta + 0.001,
  306. lambda: self.renderStaleImage(vId),
  307. )
  308. def animate(self):
  309. if len(self.viewsInAnimations) == 0:
  310. return
  311. nextAnimateTime = time.time() + 1.0 / self.targetFrameRate
  312. for vId in self.viewsInAnimations:
  313. self.pushRender(vId, True)
  314. nextAnimateTime -= time.time()
  315. if self.targetFrameRate > self.maxFrameRate:
  316. self.targetFrameRate = self.maxFrameRate
  317. if nextAnimateTime < 0:
  318. if nextAnimateTime < -1.0:
  319. self.targetFrameRate = 1
  320. if self.targetFrameRate > self.minFrameRate:
  321. self.targetFrameRate -= 1.0
  322. schedule_callback(0.001, lambda: self.animate())
  323. else:
  324. if self.targetFrameRate < self.maxFrameRate and nextAnimateTime > 0.005:
  325. self.targetFrameRate += 1.0
  326. schedule_callback(nextAnimateTime, lambda: self.animate())
  327. @exportRpc("viewport.image.animation.fps.max")
  328. def setMaxFrameRate(self, fps=30):
  329. self.maxFrameRate = fps
  330. @exportRpc("viewport.image.animation.fps.get")
  331. def getCurrentFrameRate(self):
  332. return self.targetFrameRate
  333. @exportRpc("viewport.image.animation.start")
  334. def startViewAnimation(self, viewId="-1"):
  335. sView = self.getView(viewId)
  336. realViewId = str(self.getGlobalId(sView))
  337. self.viewsInAnimations.append(realViewId)
  338. if len(self.viewsInAnimations) == 1:
  339. self.animate()
  340. @exportRpc("viewport.image.animation.stop")
  341. def stopViewAnimation(self, viewId="-1"):
  342. sView = self.getView(viewId)
  343. realViewId = str(self.getGlobalId(sView))
  344. if realViewId in self.viewsInAnimations:
  345. self.viewsInAnimations.remove(realViewId)
  346. @exportRpc("viewport.image.push")
  347. def imagePush(self, options):
  348. sView = self.getView(options["view"])
  349. realViewId = str(self.getGlobalId(sView))
  350. # Make sure an image is pushed
  351. self.getApplication().InvalidateCache(sView)
  352. self.pushRender(realViewId)
  353. # Internal function since the reply[image] is not
  354. # JSON(serializable) it can not be an RPC one
  355. def stillRender(self, options):
  356. """
  357. RPC Callback to render a view and obtain the rendered image.
  358. """
  359. beginTime = int(round(time.time() * 1000))
  360. view = self.getView(options["view"])
  361. size = view.GetSize()[0:2]
  362. resize = size != options.get("size", size)
  363. if resize:
  364. size = options["size"]
  365. if size[0] > 10 and size[1] > 10:
  366. view.SetSize(size)
  367. t = 0
  368. if options and "mtime" in options:
  369. t = options["mtime"]
  370. quality = 100
  371. if options and "quality" in options:
  372. quality = options["quality"]
  373. localTime = 0
  374. if options and "localTime" in options:
  375. localTime = options["localTime"]
  376. reply = {}
  377. app = self.getApplication()
  378. if t == 0:
  379. app.InvalidateCache(view)
  380. if self.decode:
  381. stillRender = app.StillRenderToString
  382. else:
  383. stillRender = app.StillRenderToBuffer
  384. reply_image = stillRender(view, t, quality)
  385. # Check that we are getting image size we have set if not wait until we
  386. # do. The render call will set the actual window size.
  387. tries = 10
  388. while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
  389. app.InvalidateCache(view)
  390. reply_image = stillRender(view, t, quality)
  391. tries -= 1
  392. if (
  393. not resize
  394. and options
  395. and ("clearCache" in options)
  396. and options["clearCache"]
  397. ):
  398. app.InvalidateCache(view)
  399. reply_image = stillRender(view, t, quality)
  400. reply["stale"] = app.GetHasImagesBeingProcessed(view)
  401. reply["mtime"] = app.GetLastStillRenderToMTime()
  402. reply["size"] = view.GetSize()[0:2]
  403. reply["memsize"] = reply_image.GetDataSize() if reply_image else 0
  404. reply["format"] = "jpeg;base64" if self.decode else "jpeg"
  405. reply["global_id"] = str(self.getGlobalId(view))
  406. reply["localTime"] = localTime
  407. if self.decode:
  408. reply["image"] = reply_image
  409. else:
  410. # Convert the vtkUnsignedCharArray into a bytes object, required by Autobahn websockets
  411. reply["image"] = memoryview(reply_image).tobytes() if reply_image else None
  412. endTime = int(round(time.time() * 1000))
  413. reply["workTime"] = endTime - beginTime
  414. return reply
  415. @exportRpc("viewport.image.push.observer.add")
  416. def addRenderObserver(self, viewId):
  417. sView = self.getView(viewId)
  418. if not sView:
  419. return {"error": "Unable to get view with id %s" % viewId}
  420. realViewId = str(self.getGlobalId(sView))
  421. if not realViewId in self.trackingViews:
  422. observerCallback = lambda *args, **kwargs: self.pushRender(realViewId)
  423. startCallback = lambda *args, **kwargs: self.startViewAnimation(realViewId)
  424. stopCallback = lambda *args, **kwargs: self.stopViewAnimation(realViewId)
  425. tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
  426. tagStart = self.getApplication().AddObserver(
  427. "StartInteractionEvent", startCallback
  428. )
  429. tagStop = self.getApplication().AddObserver(
  430. "EndInteractionEvent", stopCallback
  431. )
  432. # TODO do we need self.getApplication().AddObserver('ResetActiveView', resetActiveView())
  433. self.trackingViews[realViewId] = {
  434. "tags": [tag, tagStart, tagStop],
  435. "observerCount": 1,
  436. "mtime": 0,
  437. "enabled": True,
  438. "quality": 100,
  439. }
  440. else:
  441. # There is an observer on this view already
  442. self.trackingViews[realViewId]["observerCount"] += 1
  443. self.pushRender(realViewId)
  444. return {"success": True, "viewId": realViewId}
  445. @exportRpc("viewport.image.push.observer.remove")
  446. def removeRenderObserver(self, viewId):
  447. sView = self.getView(viewId)
  448. if not sView:
  449. return {"error": "Unable to get view with id %s" % viewId}
  450. realViewId = str(self.getGlobalId(sView))
  451. observerInfo = None
  452. if realViewId in self.trackingViews:
  453. observerInfo = self.trackingViews[realViewId]
  454. if not observerInfo:
  455. return {"error": "Unable to find subscription for view %s" % realViewId}
  456. observerInfo["observerCount"] -= 1
  457. if observerInfo["observerCount"] <= 0:
  458. for tag in observerInfo["tags"]:
  459. self.getApplication().RemoveObserver(tag)
  460. del self.trackingViews[realViewId]
  461. return {"result": "success"}
  462. @exportRpc("viewport.image.push.quality")
  463. def setViewQuality(self, viewId, quality, ratio=1):
  464. sView = self.getView(viewId)
  465. if not sView:
  466. return {"error": "Unable to get view with id %s" % viewId}
  467. realViewId = str(self.getGlobalId(sView))
  468. observerInfo = None
  469. if realViewId in self.trackingViews:
  470. observerInfo = self.trackingViews[realViewId]
  471. if not observerInfo:
  472. return {"error": "Unable to find subscription for view %s" % realViewId}
  473. observerInfo["quality"] = quality
  474. observerInfo["ratio"] = ratio
  475. # Update image size right now!
  476. if "originalSize" in self.trackingViews[realViewId]:
  477. size = [
  478. int(s * ratio) for s in self.trackingViews[realViewId]["originalSize"]
  479. ]
  480. if hasattr(sView, "SetSize"):
  481. sView.SetSize(size)
  482. else:
  483. sView.ViewSize = size
  484. return {"result": "success"}
  485. @exportRpc("viewport.image.push.original.size")
  486. def setViewSize(self, viewId, width, height):
  487. sView = self.getView(viewId)
  488. if not sView:
  489. return {"error": "Unable to get view with id %s" % viewId}
  490. realViewId = str(self.getGlobalId(sView))
  491. observerInfo = None
  492. if realViewId in self.trackingViews:
  493. observerInfo = self.trackingViews[realViewId]
  494. if not observerInfo:
  495. return {"error": "Unable to find subscription for view %s" % realViewId}
  496. observerInfo["originalSize"] = [width, height]
  497. return {"result": "success"}
  498. @exportRpc("viewport.image.push.enabled")
  499. def enableView(self, viewId, enabled):
  500. sView = self.getView(viewId)
  501. if not sView:
  502. return {"error": "Unable to get view with id %s" % viewId}
  503. realViewId = str(self.getGlobalId(sView))
  504. observerInfo = None
  505. if realViewId in self.trackingViews:
  506. observerInfo = self.trackingViews[realViewId]
  507. if not observerInfo:
  508. return {"error": "Unable to find subscription for view %s" % realViewId}
  509. observerInfo["enabled"] = enabled
  510. return {"result": "success"}
  511. @exportRpc("viewport.image.push.invalidate.cache")
  512. def invalidateCache(self, viewId):
  513. sView = self.getView(viewId)
  514. if not sView:
  515. return {"error": "Unable to get view with id %s" % viewId}
  516. self.getApplication().InvalidateCache(sView)
  517. self.getApplication().InvokeEvent("UpdateEvent")
  518. return {"result": "success"}
  519. # =============================================================================
  520. #
  521. # Provide Geometry delivery mechanism (WebGL) (deprecated - will be removed in VTK 10+)
  522. #
  523. # =============================================================================
  524. class vtkWebViewPortGeometryDelivery(vtkWebProtocol):
  525. @exportRpc("viewport.webgl.metadata")
  526. def getSceneMetaData(self, view_id):
  527. view = self.getView(view_id)
  528. data = self.getApplication().GetWebGLSceneMetaData(view)
  529. return data
  530. @exportRpc("viewport.webgl.data")
  531. def getWebGLData(self, view_id, object_id, part):
  532. view = self.getView(view_id)
  533. data = self.getApplication().GetWebGLBinaryData(view, str(object_id), part - 1)
  534. return data
  535. # =============================================================================
  536. #
  537. # Provide File/Directory listing
  538. #
  539. # =============================================================================
  540. class vtkWebFileBrowser(vtkWebProtocol):
  541. def __init__(
  542. self, basePath, name, excludeRegex=r"^\.|~$|^\$", groupRegex=r"[0-9]+\."
  543. ):
  544. """
  545. Configure the way the WebFile browser will expose the server content.
  546. - basePath: specify the base directory that we should start with
  547. - name: Name of that base directory that will show up on the web
  548. - excludeRegex: Regular expression of what should be excluded from the list of files/directories
  549. """
  550. self.baseDirectory = basePath
  551. self.rootName = name
  552. self.pattern = re.compile(excludeRegex)
  553. self.gPattern = re.compile(groupRegex)
  554. @exportRpc("file.server.directory.list")
  555. def listServerDirectory(self, relativeDir="."):
  556. """
  557. RPC Callback to list a server directory relative to the basePath
  558. provided at start-up.
  559. """
  560. path = [self.rootName]
  561. if len(relativeDir) > len(self.rootName):
  562. relativeDir = relativeDir[len(self.rootName) + 1 :]
  563. path += relativeDir.replace("\\", "/").split("/")
  564. currentPath = os.path.join(self.baseDirectory, relativeDir)
  565. result = {
  566. "label": relativeDir,
  567. "files": [],
  568. "dirs": [],
  569. "groups": [],
  570. "path": path,
  571. }
  572. if relativeDir == ".":
  573. result["label"] = self.rootName
  574. for file in os.listdir(currentPath):
  575. if os.path.isfile(os.path.join(currentPath, file)) and not re.search(
  576. self.pattern, file
  577. ):
  578. result["files"].append({"label": file, "size": -1})
  579. elif os.path.isdir(os.path.join(currentPath, file)) and not re.search(
  580. self.pattern, file
  581. ):
  582. result["dirs"].append(file)
  583. # Filter files to create groups
  584. files = result["files"]
  585. files.sort()
  586. groups = result["groups"]
  587. groupIdx = {}
  588. filesToRemove = []
  589. for file in files:
  590. fileSplit = re.split(self.gPattern, file["label"])
  591. if len(fileSplit) == 2:
  592. filesToRemove.append(file)
  593. gName = "*.".join(fileSplit)
  594. if gName in groupIdx:
  595. groupIdx[gName]["files"].append(file["label"])
  596. else:
  597. groupIdx[gName] = {"files": [file["label"]], "label": gName}
  598. groups.append(groupIdx[gName])
  599. for file in filesToRemove:
  600. gName = "*.".join(re.split(self.gPattern, file["label"]))
  601. if len(groupIdx[gName]["files"]) > 1:
  602. files.remove(file)
  603. else:
  604. groups.remove(groupIdx[gName])
  605. return result
  606. # =============================================================================
  607. #
  608. # Provide an updated geometry delivery mechanism which better matches the
  609. # client-side rendering capability we have in vtk.js
  610. #
  611. # =============================================================================
  612. class vtkWebLocalRendering(vtkWebProtocol):
  613. def __init__(self, **kwargs):
  614. super(vtkWebLocalRendering, self).__init__()
  615. initializeSerializers()
  616. self.context = SynchronizationContext()
  617. self.trackingViews = {}
  618. self.mtime = 0
  619. # RpcName: getArray => viewport.geometry.array.get
  620. @exportRpc("viewport.geometry.array.get")
  621. def getArray(self, dataHash, binary=False):
  622. if binary:
  623. return self.addAttachment(self.context.getCachedDataArray(dataHash, binary))
  624. return self.context.getCachedDataArray(dataHash, binary)
  625. # RpcName: addViewObserver => viewport.geometry.view.observer.add
  626. @exportRpc("viewport.geometry.view.observer.add")
  627. def addViewObserver(self, viewId):
  628. sView = self.getView(viewId)
  629. if not sView:
  630. return {"error": "Unable to get view with id %s" % viewId}
  631. realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
  632. def pushGeometry(newSubscription=False):
  633. stateToReturn = self.getViewState(realViewId, newSubscription)
  634. stateToReturn["mtime"] = 0 if newSubscription else self.mtime
  635. self.mtime += 1
  636. return stateToReturn
  637. if not realViewId in self.trackingViews:
  638. observerCallback = lambda *args, **kwargs: self.publish(
  639. "viewport.geometry.view.subscription", pushGeometry()
  640. )
  641. tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
  642. self.trackingViews[realViewId] = {"tags": [tag], "observerCount": 1}
  643. else:
  644. # There is an observer on this view already
  645. self.trackingViews[realViewId]["observerCount"] += 1
  646. self.publish("viewport.geometry.view.subscription", pushGeometry(True))
  647. return {"success": True, "viewId": realViewId}
  648. # RpcName: removeViewObserver => viewport.geometry.view.observer.remove
  649. @exportRpc("viewport.geometry.view.observer.remove")
  650. def removeViewObserver(self, viewId):
  651. sView = self.getView(viewId)
  652. if not sView:
  653. return {"error": "Unable to get view with id %s" % viewId}
  654. realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
  655. observerInfo = None
  656. if realViewId in self.trackingViews:
  657. observerInfo = self.trackingViews[realViewId]
  658. if not observerInfo:
  659. return {"error": "Unable to find subscription for view %s" % realViewId}
  660. observerInfo["observerCount"] -= 1
  661. if observerInfo["observerCount"] <= 0:
  662. for tag in observerInfo["tags"]:
  663. self.getApplication().RemoveObserver(tag)
  664. del self.trackingViews[realViewId]
  665. return {"result": "success"}
  666. # RpcName: getViewState => viewport.geometry.view.get.state
  667. @exportRpc("viewport.geometry.view.get.state")
  668. def getViewState(self, viewId, newSubscription=False):
  669. sView = self.getView(viewId)
  670. if not sView:
  671. return {"error": "Unable to get view with id %s" % viewId}
  672. self.context.setIgnoreLastDependencies(newSubscription)
  673. # Get the active view and render window, use it to iterate over renderers
  674. renderWindow = sView
  675. renderer = renderWindow.GetRenderers().GetFirstRenderer()
  676. camera = renderer.GetActiveCamera()
  677. renderWindowId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
  678. viewInstance = serializeInstance(
  679. None, renderWindow, renderWindowId, self.context, 1
  680. )
  681. viewInstance["extra"] = {
  682. "vtkRefId": getReferenceId(renderWindow),
  683. "centerOfRotation": camera.GetFocalPoint(),
  684. "camera": getReferenceId(camera),
  685. }
  686. self.context.setIgnoreLastDependencies(False)
  687. self.context.checkForArraysToRelease()
  688. if viewInstance:
  689. return viewInstance
  690. return None