• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eX-Mech / pymech / 3840794544

pending completion
3840794544

Pull #84

github

GitHub
Merge cccd9551d into ccf4f61e9
Pull Request #84: Enh/meshplot

276 of 276 new or added lines in 1 file covered. (100.0%)

2376 of 3151 relevant lines covered (75.4%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pymech/meshplot.py
1
import wx
×
2
from wx import glcanvas
×
3
import OpenGL.GL as gl
×
4
import numpy as np
×
5
from math import sqrt, atan2, asin, cos, sin
×
6

7

8
class MeshFrame(wx.Frame):
×
9
    """
10
    A frame to display meshes
11
    """
12

13
    def __init__(
×
14
        self,
15
        mesh,
16
        parent,
17
        id,
18
        title,
19
        pos=wx.DefaultPosition,
20
        size=wx.DefaultSize,
21
        style=wx.DEFAULT_FRAME_STYLE,
22
        name="frame",
23
    ):
24
        super(MeshFrame, self).__init__(parent, id, title, pos, size, style, name)
×
25
        self.GLinitialized = False
×
26
        attribList = (
×
27
            glcanvas.WX_GL_RGBA,  # RGBA
28
            glcanvas.WX_GL_DOUBLEBUFFER,  # Double Buffered
29
            glcanvas.WX_GL_DEPTH_SIZE,
30
            24,
31
        )  # 24 bit
32

33
        # Create the canvas
34
        self.canvas = glcanvas.GLCanvas(self, attribList=attribList)
×
35
        self.context = glcanvas.GLContext(self.canvas)
×
36

37
        # Set the event handlers.
38
        self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent)
×
39
        self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent)
×
40
        self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent)
×
41
        self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.processMouseWheelEvent)
×
42
        self.canvas.Bind(wx.EVT_LEFT_DOWN, self.processLeftDownEvent)
×
43
        self.canvas.Bind(wx.EVT_LEFT_UP, self.processLeftUpEvent)
×
44
        self.canvas.Bind(wx.EVT_MOTION, self.processMotionEvent)
×
45
        self.canvas.Bind(wx.EVT_MIDDLE_UP, self.processMiddleUpEvent)
×
46
        self.canvas.Bind(wx.EVT_MIDDLE_DOWN, self.processMiddleDownEvent)
×
47

48
        # wx context
49
        self.dc = wx.ClientDC(self)
×
50

51
        # create a menu bar
52
        # self.makeMenuBar()
53

54
        # mesh display properties
55
        self.curve_points = 12
×
56
        # colours of the edges depending on their boundary conditions
57
        self.bc_colours = {
×
58
            "": (0.0, 0.0, 0.0, 1.0),
59
            "E": (0.0, 0.0, 0.0, 1.0),
60
            "W": (0.0, 0.0, 0.8, 1.0),
61
            "v": (0.3, 0.3, 1.0, 1.0),
62
            "O": (0.8, 0.0, 0.0, 1.0),
63
            "o": (1.0, 0.2, 0.2, 1.0),
64
            "ON": (0.8, 0.4, 0.0, 1.0),
65
            "on": (1.0, 0.6, 0.0, 1.0),
66
            "T": (0.0, 0.8, 0.0, 1.0),
67
            "t": (0.3, 1.0, 0.3, 1.0),
68
            "I": (0.95, 0.1, 0.6, 1.0),
69
        }
70

71
        # data to be drawn
72
        self.vertex_data = np.array([])
×
73
        self.colour_data = np.array([])
×
74
        self.edge_bcs = []
×
75
        self.num_vertices = 0
×
76
        self.buildMesh(mesh)
×
77
        self.colour_edges(0)
×
78
        self.vertex_buffer = 0
×
79
        self.colour_buffer = 0
×
80

81
        # view parameters
82
        # relative margins to display around the mesh in the default view
83
        self.margins = 0.05
×
84
        # when zooming in by one step, the field of view is multiplied by this value
85
        self.zoom_factor = 0.95
×
86
        # sets self.mesh_limits
87
        self.setLimits(mesh)
×
88
        # current limits
89
        self.limits = self.mesh_limits.copy()
×
90
        self.target_limits = self.mesh_limits.copy()
×
91

92
        # motion state
93
        self.moving = False
×
94
        self.motion_origin = (0.0, 0.0)
×
95

96
        # and a status bar
97
        # self.CreateStatusBar()
98
        # self.SetStatusText("initialised")
99

100
    # Canvas Proxy Methods
101

102
    def GetGLExtents(self):
×
103
        """Get the extents of the OpenGL canvas."""
104
        return self.canvas.GetClientSize()
×
105

106
    def SwapBuffers(self):
×
107
        """Swap the OpenGL buffers."""
108
        self.canvas.SwapBuffers()
×
109

110
    def OnExit(self, event):
×
111
        """Close the frame, terminating the application."""
112
        self.Close(True)
×
113

114
    # wxPython Window Handlers
115

116
    def processEraseBackgroundEvent(self, event):
×
117
        """Process the erase background event."""
118
        pass  # Do nothing, to avoid flashing on MSWin
×
119

120
    def processSizeEvent(self, event):
×
121
        """Process the resize event."""
122
        if self.context:
×
123
            # Make sure the frame is shown before calling SetCurrent.
124
            self.Show()
×
125
            self.canvas.SetCurrent(self.context)
×
126

127
            # update the view limits
128
            self.updateLimits()
×
129
            self.canvas.Refresh(False)
×
130
        event.Skip()
×
131

132
    def processPaintEvent(self, event):
×
133
        """Process the drawing event."""
134
        self.canvas.SetCurrent(self.context)
×
135

136
        # This is a 'perfect' time to initialize OpenGL ... only if we need to
137
        if not self.GLinitialized:
×
138
            self.OnInitGL()
×
139
            self.GLinitialized = True
×
140

141
        self.OnDraw()
×
142
        event.Skip()
×
143

144
    def processMouseWheelEvent(self, event):
×
145
        """
146
        Processes mouse wheel events.
147
        Zooms when the vertical mouse wheel is used.
148
        """
149

150
        # vertical or horizontal wheel axis
151
        axis = event.GetWheelAxis()
×
152
        # position of the pointer in pixels in the window frame
153
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
154
        increment = event.GetWheelRotation() / event.GetWheelDelta()
×
155
        if axis == wx.MOUSE_WHEEL_VERTICAL:
×
156
            self.zoom(xcp, ycp, increment)
×
157

158
    def pixelsToMeshUnits(self, xcp, ycp):
×
159
        """
160
        Given a position in pixels in the window, returns the
161
        corresponding position in mesh units.
162
        """
163

164
        xmin, xmax, ymin, ymax = self.limits
×
165
        size = self.GetGLExtents()
×
166
        # xcp is 0 on the left, ycp is zero on the top
167
        xc = xmin + (xmax - xmin) * xcp / size.width
×
168
        yc = ymax - (ymax - ymin) * ycp / size.height
×
169
        return (xc, yc)
×
170

171
    def zoom(self, xcp, ycp, increment):
×
172
        """
173
        Zooms around the given centre proportionally to the increment.
174
        Zooms in if `increment` is positive, out if it is negative.
175
        The centre (xcp, ycp) is given in pixels, not in mesh units.
176
        """
177

178
        factor = self.zoom_factor**increment
×
179
        xmin, xmax, ymin, ymax = self.limits
×
180
        xmin_t, xmax_t, ymin_t, ymax_t = self.target_limits
×
181
        # get the centre location in mesh units
182
        xc, yc = self.pixelsToMeshUnits(xcp, ycp)
×
183
        # update the limits
184
        xmin = xc + factor * (xmin - xc)
×
185
        xmax = xc + factor * (xmax - xc)
×
186
        ymin = yc + factor * (ymin - yc)
×
187
        ymax = yc + factor * (ymax - yc)
×
188
        xmin_t = xc + factor * (xmin_t - xc)
×
189
        xmax_t = xc + factor * (xmax_t - xc)
×
190
        ymin_t = yc + factor * (ymin_t - yc)
×
191
        ymax_t = yc + factor * (ymax_t - yc)
×
192
        self.limits = [xmin, xmax, ymin, ymax]
×
193
        self.target_limits = [xmin_t, xmax_t, ymin_t, ymax_t]
×
194
        self.updateLimits()
×
195
        self.OnDraw()
×
196

197
    def processLeftDownEvent(self, event):
×
198
        """
199
        Process actions that should be done when there is a left click
200
        """
201

202
        # enable view motion mode
203
        self.moving = True
×
204
        # store the position of the click to track how much we're moving the view
205
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
206
        self.motion_origin = self.pixelsToMeshUnits(xcp, ycp)
×
207

208
    def processLeftUpEvent(self, event):
×
209
        """
210
        Process actions that should be done when the left mouse button is released
211
        """
212

213
        # stop dragging the view
214
        self.moving = False
×
215

216
    def processMiddleDownEvent(self, event):
×
217
        """
218
        Process actions that should be done when there is a middle click
219
        """
220

221
        # enable view motion mode
222
        self.moving = True
×
223
        # store the position of the click to track how much we're moving the view
224
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
225
        self.motion_origin = self.pixelsToMeshUnits(xcp, ycp)
×
226

227
    def processMiddleUpEvent(self, event):
×
228
        """
229
        Process actions that should be done when the middle mouse button is released
230
        """
231

232
        # stop dragging the view
233
        self.moving = False
×
234

235
    def processMotionEvent(self, event):
×
236
        if self.moving:
×
237
            xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
238
            xc, yc = self.pixelsToMeshUnits(xcp, ycp)
×
239
            dx = xc - self.motion_origin[0]
×
240
            dy = yc - self.motion_origin[1]
×
241
            self.target_limits[0] -= dx
×
242
            self.target_limits[1] -= dx
×
243
            self.target_limits[2] -= dy
×
244
            self.target_limits[3] -= dy
×
245
            self.updateLimits()
×
246
            self.OnDraw()
×
247

248
    # GLFrame OpenGL Event Handlers
249

250
    def OnInitGL(self):
×
251
        """Initialize OpenGL for use in the window."""
252
        self.createBuffers()
×
253
        gl.glClearColor(1, 1, 1, 1)
×
254

255
    def updateLimits(self):
×
256
        """Update the view limits based on the dimensions of the window."""
257

258
        size = self.GetGLExtents()
×
259
        width = size.width
×
260
        height = size.height
×
261
        xmin = self.target_limits[0]
×
262
        xmax = self.target_limits[1]
×
263
        ymin = self.target_limits[2]
×
264
        ymax = self.target_limits[3]
×
265
        # check whether the view is limited by width or height, and scale accordingly
266
        lx = xmax - xmin
×
267
        ly = ymax - ymin
×
268
        if lx / width > ly / height:
×
269
            y0 = 0.5 * (ymin + ymax)
×
270
            dy = height / width * lx / 2
×
271
            ymin = y0 - dy
×
272
            ymax = y0 + dy
×
273
        else:
274
            x0 = 0.5 * (xmin + xmax)
×
275
            dx = width / height * ly / 2
×
276
            xmin = x0 - dx
×
277
            xmax = x0 + dx
×
278
        self.limits = [xmin, xmax, ymin, ymax]
×
279
        gl.glViewport(0, 0, width, height)
×
280
        gl.glMatrixMode(gl.GL_PROJECTION)
×
281
        gl.glLoadIdentity()
×
282
        gl.glOrtho(xmin, xmax, ymin, ymax, -1, 1)
×
283

284
        gl.glMatrixMode(gl.GL_MODELVIEW)
×
285
        gl.glLoadIdentity()
×
286

287
    def createBuffers(self):
×
288
        # new vertex and colour buffers
289
        self.vertex_buffer = gl.glGenBuffers(1)
×
290
        self.colour_buffer = gl.glGenBuffers(1)
×
291
        # send the vertex data to the buffer
292
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
293
        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.vertex_data, gl.GL_STATIC_DRAW)
×
294
        # same for colour
295
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.colour_buffer)
×
296
        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.colour_data, gl.GL_STATIC_DRAW)
×
297
        # unbind the buffer
298
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
299

300
    def OnDraw(self, *args, **kwargs):
×
301
        "Draw the window."
302

303
        # initialise
304
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)
×
305
        gl.glClear(gl.GL_DEPTH_BUFFER_BIT)
×
306
        gl.glClearColor(1, 1, 1, 1)
×
307
        gl.glEnable(gl.GL_LINE_SMOOTH)
×
308
        gl.glLineWidth(1.0)
×
309
        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
×
310
        gl.glEnableClientState(gl.GL_COLOR_ARRAY)
×
311
        # load buffers
312
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
313
        gl.glVertexPointer(3, gl.GL_DOUBLE, 0, None)
×
314
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.colour_buffer)
×
315
        gl.glColorPointer(4, gl.GL_FLOAT, 0, None)
×
316
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
317
        # draw the mesh
318
        gl.glDrawArrays(gl.GL_LINES, 0, self.num_vertices)
×
319
        # finalise
320
        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
×
321
        gl.glDisableClientState(gl.GL_COLOR_ARRAY)
×
322

323
        self.SwapBuffers()
×
324

325
    def buildMesh(self, mesh):
×
326
        """
327
        Builds the edges to be drawn based on the mesh representation.
328
        """
329

330
        # gives the indices of the vertices of an element in a position array
331
        def vertex_indices(iface):
×
332
            if iface == 0:
×
333
                return (0, 0)
×
334
            elif iface == 1:
×
335
                return (0, -1)
×
336
            elif iface == 2:
×
337
                return (-1, -1)
×
338
            else:
339
                return (-1, 0)
×
340

341
        current_point = 0
×
342
        first_point = 0
×
343
        vertices = []
×
344
        edges = []
×
345
        for el in mesh.elem:
×
346
            first_point = current_point
×
347
            for iface in range(4):
×
348
                current_bcs = []
×
349
                for ibc in range(mesh.nbc):
×
350
                    current_bcs.append(el.bcs[ibc, iface][0])
×
351
                j0, i0 = vertex_indices(iface)
×
352
                if el.ccurv[iface] == "":
×
353
                    vertices.append(
×
354
                        (
355
                            el.pos[0, 0, j0, i0],
356
                            el.pos[1, 0, j0, i0],
357
                            0.0,
358
                        )
359
                    )
360
                    if iface < 3:
×
361
                        next_point = current_point + 1
×
362
                    else:
363
                        next_point = first_point
×
364
                    edges.append((current_point, next_point))
×
365
                    self.edge_bcs.append(current_bcs)
×
366
                    current_point += 1
×
367
                elif el.ccurv[iface] == "m":
×
368
                    # we should draw a parabola passing through the current vertex, the midpoint, and the next vertex.
369
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
370
                    xm, ym = el.curv[iface][0:2]
×
371
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
372
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
373
                    # quadratic Lagrange interpolation between points
374
                    for ipt in range(self.curve_points):
×
375
                        # tp varies between 0 and 1
376
                        tp = ipt / self.curve_points
×
377
                        xp = (
×
378
                            x0 * 2 * (tp - 0.5) * (tp - 1)
379
                            - xm * 4 * tp * (tp - 1)
380
                            + x1 * 2 * tp * (tp - 0.5)
381
                        )
382
                        yp = (
×
383
                            y0 * 2 * (tp - 0.5) * (tp - 1)
384
                            - ym * 4 * tp * (tp - 1)
385
                            + y1 * 2 * tp * (tp - 0.5)
386
                        )
387
                        vertices.append((xp, yp, 0))
×
388
                        if iface == 3 and ipt == self.curve_points - 1:
×
389
                            next_point = first_point
×
390
                        else:
391
                            next_point = current_point + 1
×
392
                        edges.append((current_point, next_point))
×
393
                        self.edge_bcs.append(current_bcs)
×
394
                        current_point += 1
×
395
                elif el.ccurv[iface] == "C":
×
396
                    # draw a circle of given radius passing through the next vertex and the current one
397
                    # first, find the distance between the midpoint of the segment ((x0, y0), (x1, y1)) and the center (xc, yc) of the circle
398
                    radius = el.curv[iface][
×
399
                        0
400
                    ]  # this can be positive or negative depending on direction
401
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
402
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
403
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
404
                    # length of the segment
405
                    ls2 = (x1 - x0) ** 2 + (y1 - y0) ** 2
×
406
                    try:
×
407
                        dist = radius * sqrt(1 - ls2 / (4 * radius**2))
×
408
                    except ValueError:
×
409
                        raise ValueError("the radius of the curved edge is too small")
410
                    # midpoint of the edge
411
                    xm = 0.5 * (x0 + x1)
×
412
                    ym = 0.5 * (y0 + y1)
×
413
                    # outward normal direction
414
                    ls = sqrt(ls2)
×
415
                    nx = (y1 - y0) / ls
×
416
                    ny = -(x1 - x0) / ls
×
417
                    # position of the centre
418
                    xc = xm - nx * dist
×
419
                    yc = ym - ny * dist
×
420
                    # now find the range of arguments spanned by the circle arc
421
                    # argument to the centre of the edge
422
                    theta0 = atan2(ym - yc, xm - xc)
×
423
                    dtheta = asin(ls / (2 * radius))
×
424
                    theta_min = theta0 - dtheta
×
425
                    # Now, add the points
426
                    for itheta in range(self.curve_points):
×
427
                        theta = theta_min + 2 * dtheta * itheta / self.curve_points
×
428
                        xp = xc + abs(radius) * cos(theta)
×
429
                        yp = yc + abs(radius) * sin(theta)
×
430
                        vertices.append((xp, yp, 0))
×
431
                        if iface == 3 and itheta == self.curve_points - 1:
×
432
                            next_point = first_point
×
433
                        else:
434
                            next_point = current_point + 1
×
435
                        edges.append((current_point, next_point))
×
436
                        self.edge_bcs.append(current_bcs)
×
437
                        current_point += 1
×
438

439
        # put the vertex data into a buffer that OpenGL can read
440
        self.num_vertices = 2 * len(edges)
×
441
        vertex_data = []
×
442
        for edge in edges:
×
443
            for vertex in edge:
×
444
                x, y, z = vertices[vertex]
×
445
                vertex_data.append(x)
×
446
                vertex_data.append(y)
×
447
                vertex_data.append(z)
×
448
        self.vertex_data = np.array(vertex_data)
×
449

450
    def colour_edges(self, ibc):
×
451
        colour_data = []
×
452
        for bcs in self.edge_bcs:
×
453
            try:
×
454
                bc = bcs[ibc]
×
455
            except IndexError:
×
456
                raise ValueError(f"no boundary condition defined for field {ibc}")
457
            try:
×
458
                cr, cg, cb, ca = self.bc_colours[bc]
×
459
            except KeyError:
460
                # should probably log an error here too
461
                cr, cg, cb, ca = (0.0, 0.0, 0.0, 1.0)
462
                print(f"unknow BC type: {bc}")
463
            # We need to add a colour for each vertex of the edge!
464
            # They are the same colour, so we pu the same data twice
465
            for _ in range(2):
×
466
                colour_data.append(cr)
×
467
                colour_data.append(cg)
×
468
                colour_data.append(cb)
×
469
                colour_data.append(ca)
×
470
        self.colour_data = np.array(colour_data, dtype=np.float32)
×
471

472
    def setLimits(self, mesh):
×
473
        """
474
        set view limits to the size of the mesh with some margin
475
        """
476
        xmin, xmax = mesh.lims.pos[0]
×
477
        ymin, ymax = mesh.lims.pos[1]
×
478
        lx = xmax - xmin
×
479
        ly = ymax - ymin
×
480
        self.mesh_limits = [
×
481
            xmin - self.margins * lx,
482
            xmax + self.margins * lx,
483
            ymin - self.margins * ly,
484
            ymax + self.margins * ly,
485
        ]
486

487

488
def plot2D(mesh):
×
489
    # make a new app & frame
490
    app = wx.App()
×
491
    frame = MeshFrame(mesh, None, -1, title="pymech")
×
492

493
    frame.Show()
×
494

495
    # Start the event loop.
496
    app.MainLoop()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc