|
| 1 | +import vtk |
| 2 | + |
| 3 | +from trame.app import TrameApp |
| 4 | +from trame.ui.vuetify3 import SinglePageWithDrawerLayout |
| 5 | +from trame.widgets import vtklocal, trame as tw, vuetify3 as v3 |
| 6 | +from trame.decorators import change |
| 7 | +from trame.assets.remote import HttpFile |
| 8 | +from trame.assets.local import to_url |
| 9 | + |
| 10 | +# ----------------------------------------------------------------------------- |
| 11 | +# Fetch data / files |
| 12 | +# ----------------------------------------------------------------------------- |
| 13 | +BIKE = HttpFile( |
| 14 | + "bike.vtp", |
| 15 | + "https://github.com/Kitware/trame-app-bike/raw/master/data/bike.vtp", |
| 16 | +) |
| 17 | +TUNNEL = HttpFile( |
| 18 | + "tunnel.vtu", |
| 19 | + "https://github.com/Kitware/trame-app-bike/raw/master/data/tunnel.vtu", |
| 20 | +) |
| 21 | +IMAGE = HttpFile( |
| 22 | + "seeds.jpg", |
| 23 | + "https://github.com/Kitware/trame-app-bike/raw/master/data/seeds.jpg", |
| 24 | +) |
| 25 | + |
| 26 | +if not BIKE.local: |
| 27 | + BIKE.fetch() |
| 28 | + |
| 29 | +if not TUNNEL.local: |
| 30 | + TUNNEL.fetch() |
| 31 | + |
| 32 | +if not IMAGE.local: |
| 33 | + IMAGE.fetch() |
| 34 | + |
| 35 | +# ----------------------------------------------------------------------------- |
| 36 | +# Constants setup |
| 37 | +# ----------------------------------------------------------------------------- |
| 38 | +P1 = [-0.4, 0, 0.05] |
| 39 | +P2 = [-0.4, 0, 1.5] |
| 40 | + |
| 41 | +INITIAL_STATE = { |
| 42 | + "line_widget": { |
| 43 | + "p1": P1, |
| 44 | + "p2": P2, |
| 45 | + }, |
| 46 | + "trame__title": "Bike CFD", |
| 47 | + "trame__favicon": to_url(IMAGE.path), |
| 48 | +} |
| 49 | + |
| 50 | + |
| 51 | +# ----------------------------------------------------------------------------- |
| 52 | +# VTK pipeline |
| 53 | +# ----------------------------------------------------------------------------- |
| 54 | +def create_vtk_pipeline(): |
| 55 | + K_RANGE = [0.0, 15.6] |
| 56 | + resolution = 50 |
| 57 | + |
| 58 | + renderer = vtk.vtkRenderer() |
| 59 | + renderWindow = vtk.vtkRenderWindow() |
| 60 | + renderWindow.AddRenderer(renderer) |
| 61 | + renderWindow.OffScreenRenderingOn() |
| 62 | + |
| 63 | + renderWindowInteractor = vtk.vtkRenderWindowInteractor() |
| 64 | + renderWindowInteractor.SetRenderWindow(renderWindow) |
| 65 | + renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() |
| 66 | + |
| 67 | + bikeReader = vtk.vtkXMLPolyDataReader() |
| 68 | + bikeReader.SetFileName(BIKE.path) |
| 69 | + |
| 70 | + tunnelReader = vtk.vtkXMLUnstructuredGridReader() |
| 71 | + tunnelReader.SetFileName(TUNNEL.path) |
| 72 | + tunnelReader.Update() |
| 73 | + |
| 74 | + lineSeed = vtk.vtkLineSource() |
| 75 | + lineSeed.SetPoint1(*P1) |
| 76 | + lineSeed.SetPoint2(*P2) |
| 77 | + lineSeed.SetResolution(resolution) |
| 78 | + lineSeed.Update() |
| 79 | + |
| 80 | + lineWidget = vtk.vtkLineWidget2() |
| 81 | + lineWidgetRep = lineWidget.GetRepresentation() |
| 82 | + lineWidgetRep.SetPoint1WorldPosition(P1) |
| 83 | + lineWidgetRep.SetPoint2WorldPosition(P2) |
| 84 | + lineWidget.SetInteractor(renderWindowInteractor) |
| 85 | + |
| 86 | + streamTracer = vtk.vtkStreamTracer() |
| 87 | + streamTracer.SetInputConnection(tunnelReader.GetOutputPort()) |
| 88 | + streamTracer.SetSourceConnection(lineSeed.GetOutputPort()) |
| 89 | + streamTracer.SetIntegrationDirectionToForward() |
| 90 | + streamTracer.SetIntegratorTypeToRungeKutta45() |
| 91 | + streamTracer.SetMaximumPropagation(3) |
| 92 | + streamTracer.SetIntegrationStepUnit(2) |
| 93 | + streamTracer.SetInitialIntegrationStep(0.2) |
| 94 | + streamTracer.SetMinimumIntegrationStep(0.01) |
| 95 | + streamTracer.SetMaximumIntegrationStep(0.5) |
| 96 | + streamTracer.SetMaximumError(0.000001) |
| 97 | + streamTracer.SetMaximumNumberOfSteps(2000) |
| 98 | + streamTracer.SetTerminalSpeed(0.00000000001) |
| 99 | + |
| 100 | + tubeFilter = vtk.vtkTubeFilter() |
| 101 | + tubeFilter.SetInputConnection(streamTracer.GetOutputPort()) |
| 102 | + tubeFilter.SetRadius(0.01) |
| 103 | + tubeFilter.SetNumberOfSides(6) |
| 104 | + tubeFilter.CappingOn() |
| 105 | + tubeFilter.Update() |
| 106 | + |
| 107 | + bike_mapper = vtk.vtkPolyDataMapper() |
| 108 | + bike_actor = vtk.vtkActor() |
| 109 | + bike_mapper.SetInputConnection(bikeReader.GetOutputPort()) |
| 110 | + bike_actor.SetMapper(bike_mapper) |
| 111 | + renderer.AddActor(bike_actor) |
| 112 | + |
| 113 | + stream_mapper = vtk.vtkPolyDataMapper() |
| 114 | + stream_actor = vtk.vtkActor() |
| 115 | + stream_mapper.SetInputConnection(tubeFilter.GetOutputPort()) |
| 116 | + stream_actor.SetMapper(stream_mapper) |
| 117 | + renderer.AddActor(stream_actor) |
| 118 | + |
| 119 | + lut = vtk.vtkLookupTable() |
| 120 | + lut.SetHueRange(0.7, 0) |
| 121 | + lut.SetSaturationRange(1.0, 0) |
| 122 | + lut.SetValueRange(0.5, 1.0) |
| 123 | + |
| 124 | + stream_mapper.SetLookupTable(lut) |
| 125 | + stream_mapper.SetColorModeToMapScalars() |
| 126 | + stream_mapper.SetScalarModeToUsePointData() |
| 127 | + stream_mapper.SetArrayName("k") |
| 128 | + stream_mapper.SetScalarRange(K_RANGE) |
| 129 | + |
| 130 | + renderWindow.Render() |
| 131 | + renderer.ResetCamera() |
| 132 | + renderer.SetBackground(0.4, 0.4, 0.4) |
| 133 | + |
| 134 | + lineWidget.On() |
| 135 | + |
| 136 | + return renderWindow, lineSeed, lineWidget, bike_actor |
| 137 | + |
| 138 | + |
| 139 | +# ----------------------------------------------------------------------------- |
| 140 | +# Trame app |
| 141 | +# ----------------------------------------------------------------------------- |
| 142 | +class App(TrameApp): |
| 143 | + def __init__(self, server=None): |
| 144 | + super().__init__(server) |
| 145 | + |
| 146 | + # VTK setup |
| 147 | + self.rw, self.seed, self.widget, self.bike_actor = create_vtk_pipeline() |
| 148 | + |
| 149 | + # GUI setup |
| 150 | + self._build_ui() |
| 151 | + |
| 152 | + # Initial state |
| 153 | + self.state.update(INITIAL_STATE) |
| 154 | + |
| 155 | + @change("bike_opacity") |
| 156 | + def _on_opacity(self, bike_opacity, **_): |
| 157 | + self.bike_actor.property.opacity = bike_opacity |
| 158 | + self.ctrl.view_update() |
| 159 | + |
| 160 | + @change("line_widget") |
| 161 | + def _on_widget_update(self, line_widget, **_): |
| 162 | + if line_widget is None: |
| 163 | + return |
| 164 | + |
| 165 | + p1 = line_widget.get("p1") |
| 166 | + p2 = line_widget.get("p2") |
| 167 | + |
| 168 | + self.seed.SetPoint1(p1) |
| 169 | + self.seed.SetPoint2(p2) |
| 170 | + |
| 171 | + if line_widget.get("widget_update", False): |
| 172 | + self.widget.representation.point1_world_position = p1 |
| 173 | + self.widget.representation.point2_world_position = p2 |
| 174 | + |
| 175 | + self.ctrl.view_update() |
| 176 | + |
| 177 | + def _build_ui(self): |
| 178 | + with SinglePageWithDrawerLayout(self.server, full_height=True) as layout: |
| 179 | + self.ui = layout # for jupyter integration |
| 180 | + |
| 181 | + # Toolbar |
| 182 | + with layout.toolbar as toolbar: |
| 183 | + toolbar.density = "compact" |
| 184 | + layout.title.set_text("Bike CFD") |
| 185 | + v3.VSpacer() |
| 186 | + v3.VSlider( |
| 187 | + v_model=("bike_opacity", 1), |
| 188 | + min=0, |
| 189 | + max=1, |
| 190 | + step=0.05, |
| 191 | + density="compact", |
| 192 | + hide_details=True, |
| 193 | + ) |
| 194 | + v3.VBtn(icon="mdi-crop-free", click=self.ctrl.view_reset_camera) |
| 195 | + |
| 196 | + # Drawer |
| 197 | + with layout.drawer: |
| 198 | + tw.LineSeed( |
| 199 | + image=to_url(IMAGE.path), |
| 200 | + point_1=("line_widget.p1",), |
| 201 | + point_2=("line_widget.p2",), |
| 202 | + bounds=("[-0.399, 1.80, -1.12, 1.11, -0.43, 1.79]",), |
| 203 | + update_seed="line_widget = { ...$event, widget_update: 1 }", |
| 204 | + n_sliders=2, |
| 205 | + ) |
| 206 | + |
| 207 | + # Content |
| 208 | + with layout.content: |
| 209 | + with vtklocal.LocalView(self.rw, throttle_rate=20) as view: |
| 210 | + self.ctrl.view_update = view.update_throttle |
| 211 | + self.ctrl.view_reset_camera = view.reset_camera |
| 212 | + |
| 213 | + # Bind state to 3D widget interaction event |
| 214 | + widget_id = view.register_vtk_object(self.widget) |
| 215 | + view.listeners = ( |
| 216 | + "wasm_listeners", |
| 217 | + { |
| 218 | + widget_id: { |
| 219 | + "InteractionEvent": { |
| 220 | + "line_widget": { |
| 221 | + "p1": ( |
| 222 | + widget_id, |
| 223 | + "WidgetRepresentation", |
| 224 | + "Point1WorldPosition", |
| 225 | + ), |
| 226 | + "p2": ( |
| 227 | + widget_id, |
| 228 | + "WidgetRepresentation", |
| 229 | + "Point2WorldPosition", |
| 230 | + ), |
| 231 | + } |
| 232 | + }, |
| 233 | + }, |
| 234 | + }, |
| 235 | + ) |
| 236 | + |
| 237 | + |
| 238 | +def main(): |
| 239 | + app = App() |
| 240 | + app.server.start() |
| 241 | + |
| 242 | + |
| 243 | +if __name__ == "__main__": |
| 244 | + main() |
0 commit comments