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

eX-Mech / pymech / 3806400180

pending completion
3806400180

Pull #84

github

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

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

2376 of 3125 relevant lines covered (76.03%)

0.76 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

57
        # data to be drawn
58
        self.vertex_data = np.array([])
×
59
        self.colour_data = np.array([])
×
60
        self.num_vertices = 0
×
61
        self.buildMesh(mesh)
×
62
        self.vertex_buffer = 0
×
63
        self.colour_buffer = 0
×
64

65
        # view parameters
66
        # relative margins to display around the mesh in the default view
67
        self.margins = 0.05
×
68
        # when zooming in by one step, the field of view is multiplied by this value
69
        self.zoom_factor = 0.95
×
70
        # sets self.mesh_limits
71
        self.setLimits(mesh)
×
72
        # current limits
73
        self.limits = self.mesh_limits.copy()
×
74
        self.target_limits = self.mesh_limits.copy()
×
75

76
        # motion state
77
        self.moving = False
×
78
        self.motion_origin = (0.0, 0.0)
×
79

80
        # and a status bar
81
        # self.CreateStatusBar()
82
        # self.SetStatusText("initialised")
83

84
    # Canvas Proxy Methods
85

86
    def GetGLExtents(self):
×
87
        """Get the extents of the OpenGL canvas."""
88
        return self.canvas.GetClientSize()
×
89

90
    def SwapBuffers(self):
×
91
        """Swap the OpenGL buffers."""
92
        self.canvas.SwapBuffers()
×
93

94
    def OnExit(self, event):
×
95
        """Close the frame, terminating the application."""
96
        self.Close(True)
×
97

98
    # wxPython Window Handlers
99

100
    def processEraseBackgroundEvent(self, event):
×
101
        """Process the erase background event."""
102
        pass  # Do nothing, to avoid flashing on MSWin
×
103

104
    def processSizeEvent(self, event):
×
105
        """Process the resize event."""
106
        if self.context:
×
107
            # Make sure the frame is shown before calling SetCurrent.
108
            self.Show()
×
109
            self.canvas.SetCurrent(self.context)
×
110

111
            # update the view limits
112
            self.updateLimits()
×
113
            self.canvas.Refresh(False)
×
114
        event.Skip()
×
115

116
    def processPaintEvent(self, event):
×
117
        """Process the drawing event."""
118
        self.canvas.SetCurrent(self.context)
×
119

120
        # This is a 'perfect' time to initialize OpenGL ... only if we need to
121
        if not self.GLinitialized:
×
122
            self.OnInitGL()
×
123
            self.GLinitialized = True
×
124

125
        self.OnDraw()
×
126
        event.Skip()
×
127

128
    def processMouseWheelEvent(self, event):
×
129
        """
130
        Processes mouse wheel events.
131
        Zooms when the vertical mouse wheel is used.
132
        """
133

134
        # vertical or horizontal wheel axis
135
        axis = event.GetWheelAxis()
×
136
        # position of the pointer in pixels in the window frame
137
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
138
        increment = event.GetWheelRotation() / event.GetWheelDelta()
×
139
        if axis == wx.MOUSE_WHEEL_VERTICAL:
×
140
            self.zoom(xcp, ycp, increment)
×
141

142
    def pixelsToMeshUnits(self, xcp, ycp):
×
143
        """
144
        Given a position in pixels in the window, returns the
145
        corresponding position in mesh units.
146
        """
147

148
        xmin, xmax, ymin, ymax = self.limits
×
149
        size = self.GetGLExtents()
×
150
        # xcp is 0 on the left, ycp is zero on the top
151
        xc = xmin + (xmax - xmin) * xcp / size.width
×
152
        yc = ymax - (ymax - ymin) * ycp / size.height
×
153
        return (xc, yc)
×
154

155
    def zoom(self, xcp, ycp, increment):
×
156
        """
157
        Zooms around the given centre proportionally to the increment.
158
        Zooms in if `increment` is positive, out if it is negative.
159
        The centre (xcp, ycp) is given in pixels, not in mesh units.
160
        """
161

162
        factor = self.zoom_factor**increment
×
163
        xmin, xmax, ymin, ymax = self.limits
×
164
        xmin_t, xmax_t, ymin_t, ymax_t = self.target_limits
×
165
        # get the centre location in mesh units
166
        xc, yc = self.pixelsToMeshUnits(xcp, ycp)
×
167
        # update the limits
168
        xmin = xc + factor * (xmin - xc)
×
169
        xmax = xc + factor * (xmax - xc)
×
170
        ymin = yc + factor * (ymin - yc)
×
171
        ymax = yc + factor * (ymax - yc)
×
172
        xmin_t = xc + factor * (xmin_t - xc)
×
173
        xmax_t = xc + factor * (xmax_t - xc)
×
174
        ymin_t = yc + factor * (ymin_t - yc)
×
175
        ymax_t = yc + factor * (ymax_t - yc)
×
176
        self.limits = [xmin, xmax, ymin, ymax]
×
177
        self.target_limits = [xmin_t, xmax_t, ymin_t, ymax_t]
×
178
        self.updateLimits()
×
179
        self.OnDraw()
×
180

181
    def processLeftDownEvent(self, event):
×
182
        """
183
        Process actions that should be done when there is a left click
184
        """
185

186
        # enable view motion mode
187
        self.moving = True
×
188
        # store the position of the click to track how much we're moving the view
189
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
190
        self.motion_origin = self.pixelsToMeshUnits(xcp, ycp)
×
191

192
    def processLeftUpEvent(self, event):
×
193
        """
194
        Process actions that should be done when the left mouse button is released
195
        """
196

197
        # stop dragging the view
198
        self.moving = False
×
199

200
    def processMiddleDownEvent(self, event):
×
201
        """
202
        Process actions that should be done when there is a middle click
203
        """
204
        # enable view motion mode
205
        print("middle down")
×
206
        self.moving = True
×
207
        # store the position of the click to track how much we're moving the view
208
        xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
209
        self.motion_origin = self.pixelsToMeshUnits(xcp, ycp)
×
210

211
    def processMiddleUpEvent(self, event):
×
212
        """
213
        Process actions that should be done when the middle mouse button is released
214
        """
215
        print("middle up")
×
216
        # stop dragging the view
217
        self.moving = False
×
218

219
    def processMotionEvent(self, event):
×
220
        if self.moving:
×
221
            xcp, ycp = event.GetLogicalPosition(self.dc).Get()
×
222
            xc, yc = self.pixelsToMeshUnits(xcp, ycp)
×
223
            dx = xc - self.motion_origin[0]
×
224
            dy = yc - self.motion_origin[1]
×
225
            self.target_limits[0] -= dx
×
226
            self.target_limits[1] -= dx
×
227
            self.target_limits[2] -= dy
×
228
            self.target_limits[3] -= dy
×
229
            self.updateLimits()
×
230
            self.OnDraw()
×
231

232
    # GLFrame OpenGL Event Handlers
233

234
    def OnInitGL(self):
×
235
        """Initialize OpenGL for use in the window."""
236
        self.createBuffers()
×
237
        gl.glClearColor(1, 1, 1, 1)
×
238

239
    def updateLimits(self):
×
240
        """Update the view limits based on the dimensions of the window."""
241

242
        size = self.GetGLExtents()
×
243
        width = size.width
×
244
        height = size.height
×
245
        xmin = self.target_limits[0]
×
246
        xmax = self.target_limits[1]
×
247
        ymin = self.target_limits[2]
×
248
        ymax = self.target_limits[3]
×
249
        # check whether the view is limited by width or height, and scale accordingly
250
        lx = xmax - xmin
×
251
        ly = ymax - ymin
×
252
        if lx / width > ly / height:
×
253
            y0 = 0.5 * (ymin + ymax)
×
254
            dy = height / width * lx / 2
×
255
            ymin = y0 - dy
×
256
            ymax = y0 + dy
×
257
        else:
258
            x0 = 0.5 * (xmin + xmax)
×
259
            dx = width / height * ly / 2
×
260
            xmin = x0 - dx
×
261
            xmax = x0 + dx
×
262
        self.limits = [xmin, xmax, ymin, ymax]
×
263
        gl.glViewport(0, 0, width, height)
×
264
        gl.glMatrixMode(gl.GL_PROJECTION)
×
265
        gl.glLoadIdentity()
×
266
        gl.glOrtho(xmin, xmax, ymin, ymax, -1, 1)
×
267

268
        gl.glMatrixMode(gl.GL_MODELVIEW)
×
269
        gl.glLoadIdentity()
×
270

271
    def createBuffers(self):
×
272
        # new vertex buffer
273
        self.vertex_buffer = gl.glGenBuffers(1)
×
274
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
275
        # send the vertex data to the buffer
276
        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.vertex_data, gl.GL_STATIC_DRAW)
×
277
        # unbind the buffer
278
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
279

280
    def OnDraw(self, *args, **kwargs):
×
281
        "Draw the window."
282

283
        # initialise
284
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)
×
285
        gl.glClear(gl.GL_DEPTH_BUFFER_BIT)
×
286
        gl.glClearColor(1, 1, 1, 1)
×
287
        gl.glEnable(gl.GL_LINE_SMOOTH)
×
288
        gl.glLineWidth(1.0)
×
289
        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
×
290
        gl.glColor(0, 0, 0)
×
291
        # load buffers
292
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
293
        gl.glVertexPointer(3, gl.GL_DOUBLE, 0, None)
×
294
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
295
        # draw the mesh
296
        gl.glDrawArrays(gl.GL_LINES, 0, self.num_vertices)
×
297
        # finalise
298
        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
×
299

300
        self.SwapBuffers()
×
301

302
    def buildMesh(self, mesh):
×
303
        """
304
        Builds the edges to be drawn based on the mesh representation.
305
        """
306

307
        # gives the indices of the vertices of an element in a position array
308
        def vertex_indices(iface):
×
309
            if iface == 0:
×
310
                return (0, 0)
×
311
            elif iface == 1:
×
312
                return (0, -1)
×
313
            elif iface == 2:
×
314
                return (-1, -1)
×
315
            else:
316
                return (-1, 0)
×
317

318
        current_point = 0
×
319
        first_point = 0
×
320
        vertices = []
×
321
        edges = []
×
322
        for el in mesh.elem:
×
323
            first_point = current_point
×
324
            for iface in range(4):
×
325
                j0, i0 = vertex_indices(iface)
×
326
                if el.ccurv[iface] == "":
×
327
                    vertices.append(
×
328
                        (
329
                            el.pos[0, 0, j0, i0],
330
                            el.pos[1, 0, j0, i0],
331
                            0.0,
332
                        )
333
                    )
334
                    if iface < 3:
×
335
                        next_point = current_point + 1
×
336
                    else:
337
                        next_point = first_point
×
338
                    edges.append((current_point, next_point))
×
339
                    current_point += 1
×
340
                elif el.ccurv[iface] == "m":
×
341
                    # we should draw a parabola passing through the current vertex, the midpoint, and the next vertex.
342
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
343
                    xm, ym = el.curv[iface][0:2]
×
344
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
345
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
346
                    # quadratic Lagrange interpolation between points
347
                    for ipt in range(self.curve_points):
×
348
                        # tp varies between 0 and 1
349
                        tp = ipt / self.curve_points
×
350
                        xp = (
×
351
                            x0 * 2 * (tp - 0.5) * (tp - 1)
352
                            - xm * 4 * tp * (tp - 1)
353
                            + x1 * 2 * tp * (tp - 0.5)
354
                        )
355
                        yp = (
×
356
                            y0 * 2 * (tp - 0.5) * (tp - 1)
357
                            - ym * 4 * tp * (tp - 1)
358
                            + y1 * 2 * tp * (tp - 0.5)
359
                        )
360
                        vertices.append((xp, yp, 0))
×
361
                        if iface == 3 and ipt == self.curve_points - 1:
×
362
                            next_point = first_point
×
363
                        else:
364
                            next_point = current_point + 1
×
365
                        edges.append((current_point, next_point))
×
366
                        current_point += 1
×
367
                elif el.ccurv[iface] == "C":
×
368
                    # draw a circle of given radius passing through the next vertex and the current one
369
                    # first, find the distance between the midpoint of the segment ((x0, y0), (x1, y1)) and the center (xc, yc) of the circle
370
                    radius = el.curv[iface][
×
371
                        0
372
                    ]  # this can be positive or negative depending on direction
373
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
374
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
375
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
376
                    # length of the segment
377
                    ls2 = (x1 - x0) ** 2 + (y1 - y0) ** 2
×
378
                    try:
×
379
                        dist = radius * sqrt(1 - ls2 / (4 * radius**2))
×
380
                    except ValueError:
×
381
                        raise ValueError("the radius of the curved edge is too small")
382
                    # midpoint of the edge
383
                    xm = 0.5 * (x0 + x1)
×
384
                    ym = 0.5 * (y0 + y1)
×
385
                    # outward normal direction
386
                    ls = sqrt(ls2)
×
387
                    nx = (y1 - y0) / ls
×
388
                    ny = -(x1 - x0) / ls
×
389
                    # position of the centre
390
                    xc = xm - nx * dist
×
391
                    yc = ym - ny * dist
×
392
                    # now find the range of arguments spanned by the circle arc
393
                    # argument to the centre of the edge
394
                    theta0 = atan2(ym - yc, xm - xc)
×
395
                    dtheta = asin(ls / (2 * radius))
×
396
                    theta_min = theta0 - dtheta
×
397
                    # Now, add the points
398
                    for itheta in range(self.curve_points):
×
399
                        theta = theta_min + 2 * dtheta * itheta / self.curve_points
×
400
                        xp = xc + abs(radius) * cos(theta)
×
401
                        yp = yc + abs(radius) * sin(theta)
×
402
                        vertices.append((xp, yp, 0))
×
403
                        if iface == 3 and itheta == self.curve_points - 1:
×
404
                            next_point = first_point
×
405
                        else:
406
                            next_point = current_point + 1
×
407
                        edges.append((current_point, next_point))
×
408
                        current_point += 1
×
409

410
        # put everything into a buffer that OpenGL can read
411
        self.num_vertices = 2 * len(edges)
×
412
        self.colour_data = np.array([0 for _ in range(4 * self.num_vertices)])
×
413
        vertex_data = []
×
414
        for edge in edges:
×
415
            for vertex in edge:
×
416
                x, y, z = vertices[vertex]
×
417
                vertex_data.append(x)
×
418
                vertex_data.append(y)
×
419
                vertex_data.append(z)
×
420
        self.vertex_data = np.array(vertex_data)
×
421

422
    def setLimits(self, mesh):
×
423
        """
424
        set view limits to the size of the mesh with some margin
425
        """
426
        xmin, xmax = mesh.lims.pos[0]
×
427
        ymin, ymax = mesh.lims.pos[1]
×
428
        lx = xmax - xmin
×
429
        ly = ymax - ymin
×
430
        self.mesh_limits = [
×
431
            xmin - self.margins * lx,
432
            xmax + self.margins * lx,
433
            ymin - self.margins * ly,
434
            ymax + self.margins * ly,
435
        ]
436

437

438
def plot2D(mesh):
×
439
    # make a new app & frame
440
    app = wx.App()
×
441
    frame = MeshFrame(mesh, None, -1, title="pymech")
×
442

443
    frame.Show()
×
444

445
    # Start the event loop.
446
    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