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

eX-Mech / pymech / 3803211177

pending completion
3803211177

Pull #84

github

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

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

2376 of 3091 relevant lines covered (76.87%)

0.77 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
import time
×
7

8

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

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

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

38
        # Set the event handlers.
39
        self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, self.processEraseBackgroundEvent)
×
40
        self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent)
×
41
        self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent)
×
42
        self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.processMouseWheelEvent)
×
43

44
        # create a menu bar
45
        # self.makeMenuBar()
46

47
        # mesh display properties
48
        self.curve_points = 12
×
49

50
        # data to be drawn
51
        self.vertex_data = np.array([])
×
52
        self.colour_data = np.array([])
×
53
        self.num_vertices = 0
×
54
        self.buildMesh(mesh)
×
55
        self.vertex_buffer = 0
×
56
        self.colour_buffer = 0
×
57

58
        # view parameters
59
        # relative margins to display around the mesh in the default view
60
        self.margins = 0.05
×
61
        # when zooming in by one step, the field of view is multiplied by this value
62
        self.zoom_factor = 0.95
×
63
        # sets self.mesh_limits
64
        self.setLimits(mesh)
×
65
        # current limits
66
        self.limits = self.mesh_limits.copy()
×
67
        self.target_limits = self.mesh_limits.copy()
×
68

69
        # and a status bar
70
        # self.CreateStatusBar()
71
        # self.SetStatusText("initialised")
72

73
    # Canvas Proxy Methods
74

75
    def GetGLExtents(self):
×
76
        """Get the extents of the OpenGL canvas."""
77
        return self.canvas.GetClientSize()
×
78

79
    def SwapBuffers(self):
×
80
        """Swap the OpenGL buffers."""
81
        self.canvas.SwapBuffers()
×
82

83
    def OnExit(self, event):
×
84
        """Close the frame, terminating the application."""
85
        self.Close(True)
×
86

87
    # wxPython Window Handlers
88

89
    def processEraseBackgroundEvent(self, event):
×
90
        """Process the erase background event."""
91
        pass  # Do nothing, to avoid flashing on MSWin
×
92

93
    def processSizeEvent(self, event):
×
94
        """Process the resize event."""
95
        if self.context:
×
96
            # Make sure the frame is shown before calling SetCurrent.
97
            self.Show()
×
98
            self.canvas.SetCurrent(self.context)
×
99

100
            size = self.GetGLExtents()
×
101
            self.updateLimits(size.width, size.height)
×
102
            self.canvas.Refresh(False)
×
103
        event.Skip()
×
104

105
    def processPaintEvent(self, event):
×
106
        """Process the drawing event."""
107
        self.canvas.SetCurrent(self.context)
×
108

109
        # This is a 'perfect' time to initialize OpenGL ... only if we need to
110
        if not self.GLinitialized:
×
111
            self.OnInitGL()
×
112
            self.GLinitialized = True
×
113

114
        self.OnDraw()
×
115
        event.Skip()
×
116

117
    def processMouseWheelEvent(self, event):
×
118
        """
119
        Processes mouse wheel events.
120
        Zooms when the vertical mouse wheel is used.
121
        """
122

123
        # vertical or horizontal wheel axis
124
        axis = event.GetWheelAxis()
×
125
        # logical context for the mouse position
126
        dc = wx.ClientDC(self)
×
127
        # position of the pointer in pixels in the window frame
128
        xcp, ycp = event.GetLogicalPosition(dc).Get()
×
129
        increment = event.GetWheelRotation() / event.GetWheelDelta()
×
130
        if axis == wx.MOUSE_WHEEL_VERTICAL:
×
131
            self.zoom(xcp, ycp, increment)
×
132

133
    def zoom(self, xcp, ycp, increment):
×
134
        """
135
        Zooms around the given centre proportionally to the increment.
136
        Zooms in if `increment` is positive, out if it is negative.
137
        The centre (xcp, ycp) is given in pixels, not in mesh units.
138
        """
139

140
        factor = self.zoom_factor ** increment
×
141
        xmin, xmax, ymin, ymax = self.limits
×
142
        xmin_t, xmax_t, ymin_t, ymax_t = self.target_limits
×
143
        size = self.GetGLExtents()
×
144
        # get the centre location in mesh units
145
        # xcp is 0 on the left, ycp is zero on the top
146
        xc = xmin + (xmax - xmin) * xcp / size.width
×
147
        yc = ymax - (ymax - ymin) * ycp / size.height
×
148
        # update the limits
149
        xmin = xc + factor * (xmin - xc)
×
150
        xmax = xc + factor * (xmax - xc)
×
151
        ymin = yc + factor * (ymin - yc)
×
152
        ymax = yc + factor * (ymax - yc)
×
153
        xmin_t = xc + factor * (xmin_t - xc)
×
154
        xmax_t = xc + factor * (xmax_t - xc)
×
155
        ymin_t = yc + factor * (ymin_t - yc)
×
156
        ymax_t = yc + factor * (ymax_t - yc)
×
157
        self.limits = [xmin, xmax, ymin, ymax]
×
158
        self.target_limits = [xmin_t, xmax_t, ymin_t, ymax_t]
×
159
        size = self.GetGLExtents()
×
160
        self.updateLimits(size.width, size.height)
×
161
        self.OnDraw()
×
162

163
    # GLFrame OpenGL Event Handlers
164

165
    def OnInitGL(self):
×
166
        """Initialize OpenGL for use in the window."""
167
        self.createBuffers()
×
168
        gl.glClearColor(1, 1, 1, 1)
×
169

170
    def updateLimits(self, width, height):
×
171
        """Update the view limits based on the dimensions of the window."""
172

173
        xmin = self.target_limits[0]
×
174
        xmax = self.target_limits[1]
×
175
        ymin = self.target_limits[2]
×
176
        ymax = self.target_limits[3]
×
177
        # check whether the view is limited by width or height, and scale accordingly
178
        lx = xmax - xmin
×
179
        ly = ymax - ymin
×
180
        if lx / width > ly / height:
×
181
            y0 = 0.5 * (ymin + ymax)
×
182
            dy = height / width * lx / 2
×
183
            ymin = y0 - dy
×
184
            ymax = y0 + dy
×
185
        else:
186
            x0 = 0.5 * (xmin + xmax)
×
187
            dx = width / height * ly / 2
×
188
            xmin = x0 - dx
×
189
            xmax = x0 + dx
×
190
        self.limits = [xmin, xmax, ymin, ymax]
×
191
        gl.glViewport(0, 0, width, height)
×
192
        gl.glMatrixMode(gl.GL_PROJECTION)
×
193
        gl.glLoadIdentity()
×
194
        gl.glOrtho(xmin, xmax, ymin, ymax, -1, 1)
×
195

196
        gl.glMatrixMode(gl.GL_MODELVIEW)
×
197
        gl.glLoadIdentity()
×
198

199
    def createBuffers(self):
×
200
        # new vertex buffer
201
        self.vertex_buffer = gl.glGenBuffers(1)
×
202
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
203
        # send the vertex data to the buffer
204
        gl.glBufferData(gl.GL_ARRAY_BUFFER, self.vertex_data, gl.GL_STATIC_DRAW)
×
205
        # unbind the buffer
206
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
207

208
    def OnDraw(self, *args, **kwargs):
×
209
        "Draw the window."
210

211
        t1 = time.perf_counter()
×
212
        # initialise
213
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)
×
214
        gl.glClear(gl.GL_DEPTH_BUFFER_BIT)
×
215
        gl.glClearColor(1, 1, 1, 1)
×
216
        gl.glEnable(gl.GL_LINE_SMOOTH)
×
217
        gl.glLineWidth(1.0)
×
218
        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
×
219
        gl.glColor(0, 0, 0)
×
220
        # load buffers
221
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_buffer)
×
222
        gl.glVertexPointer(3, gl.GL_DOUBLE, 0, None)
×
223
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
×
224
        # draw the mesh
225
        gl.glDrawArrays(gl.GL_LINES, 0, self.num_vertices)
×
226
        # finalise
227
        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
×
228
        t2 = time.perf_counter()
×
229

230
        self.SwapBuffers()
×
231
        print(f"time: draw {t2 - t1:.6e}")
×
232

233
    def buildMesh(self, mesh):
×
234
        """
235
        Builds the edges to be drawn based on the mesh representation.
236
        """
237

238
        # gives the indices of the vertices of an element in a position array
239
        def vertex_indices(iface):
×
240
            if iface == 0:
×
241
                return (0, 0)
×
242
            elif iface == 1:
×
243
                return (0, -1)
×
244
            elif iface == 2:
×
245
                return (-1, -1)
×
246
            else:
247
                return (-1, 0)
×
248

249
        current_point = 0
×
250
        first_point = 0
×
251
        vertices = []
×
252
        edges = []
×
253
        for el in mesh.elem:
×
254
            first_point = current_point
×
255
            for iface in range(4):
×
256
                j0, i0 = vertex_indices(iface)
×
257
                if el.ccurv[iface] == "":
×
258
                    vertices.append(
×
259
                        (
260
                            el.pos[0, 0, j0, i0],
261
                            el.pos[1, 0, j0, i0],
262
                            0.0,
263
                        )
264
                    )
265
                    if iface < 3:
×
266
                        next_point = current_point + 1
×
267
                    else:
268
                        next_point = first_point
×
269
                    edges.append((current_point, next_point))
×
270
                    current_point += 1
×
271
                elif el.ccurv[iface] == "m":
×
272
                    # we should draw a parabola passing through the current vertex, the midpoint, and the next vertex.
273
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
274
                    xm, ym = el.curv[iface][0:2]
×
275
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
276
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
277
                    # quadratic Lagrange interpolation between points
278
                    for ipt in range(self.curve_points):
×
279
                        # tp varies between 0 and 1
280
                        tp = ipt / self.curve_points
×
281
                        xp = (
×
282
                            x0 * 2 * (tp - 0.5) * (tp - 1)
283
                            - xm * 4 * tp * (tp - 1)
284
                            + x1 * 2 * tp * (tp - 0.5)
285
                        )
286
                        yp = (
×
287
                            y0 * 2 * (tp - 0.5) * (tp - 1)
288
                            - ym * 4 * tp * (tp - 1)
289
                            + y1 * 2 * tp * (tp - 0.5)
290
                        )
291
                        vertices.append((xp, yp, 0))
×
292
                        if iface == 3 and ipt == self.curve_points - 1:
×
293
                            next_point = first_point
×
294
                        else:
295
                            next_point = current_point + 1
×
296
                        edges.append((current_point, next_point))
×
297
                        current_point += 1
×
298
                elif el.ccurv[iface] == "C":
×
299
                    # draw a circle of given radius passing through the next vertex and the current one
300
                    # first, find the distance between the midpoint of the segment ((x0, y0), (x1, y1)) and the center (xc, yc) of the circle
301
                    radius = el.curv[iface][
×
302
                        0
303
                    ]  # this can be positive or negative depending on direction
304
                    x0, y0 = el.pos[0:2, 0, j0, i0]
×
305
                    j1, i1 = vertex_indices((iface + 1) % 4)
×
306
                    x1, y1 = el.pos[0:2, 0, j1, i1]
×
307
                    # length of the segment
308
                    ls2 = (x1 - x0) ** 2 + (y1 - y0) ** 2
×
309
                    try:
×
310
                        dist = radius * sqrt(1 - ls2 / (4 * radius**2))
×
311
                    except ValueError:
×
312
                        raise ValueError("the radius of the curved edge is too small")
313
                    # midpoint of the edge
314
                    xm = 0.5 * (x0 + x1)
×
315
                    ym = 0.5 * (y0 + y1)
×
316
                    # outward normal direction
317
                    ls = sqrt(ls2)
×
318
                    nx = (y1 - y0) / ls
×
319
                    ny = -(x1 - x0) / ls
×
320
                    # position of the centre
321
                    xc = xm - nx * dist
×
322
                    yc = ym - ny * dist
×
323
                    # now find the range of arguments spanned by the circle arc
324
                    # argument to the centre of the edge
325
                    theta0 = atan2(ym - yc, xm - xc)
×
326
                    dtheta = asin(ls / (2 * radius))
×
327
                    theta_min = theta0 - dtheta
×
328
                    # Now, add the points
329
                    for itheta in range(self.curve_points):
×
330
                        theta = theta_min + 2 * dtheta * itheta / self.curve_points
×
331
                        xp = xc + abs(radius) * cos(theta)
×
332
                        yp = yc + abs(radius) * sin(theta)
×
333
                        vertices.append((xp, yp, 0))
×
334
                        if iface == 3 and itheta == self.curve_points - 1:
×
335
                            next_point = first_point
×
336
                        else:
337
                            next_point = current_point + 1
×
338
                        edges.append((current_point, next_point))
×
339
                        current_point += 1
×
340

341
        # put everything into a buffer that OpenGL can read
342
        self.num_vertices = 2 * len(edges)
×
343
        self.colour_data = np.array([0 for _ in range(4 * self.num_vertices)])
×
344
        vertex_data = []
×
345
        for edge in edges:
×
346
            for vertex in edge:
×
347
                x, y, z = vertices[vertex]
×
348
                vertex_data.append(x)
×
349
                vertex_data.append(y)
×
350
                vertex_data.append(z)
×
351
        self.vertex_data = np.array(vertex_data)
×
352

353
    def setLimits(self, mesh):
×
354
        """
355
        set view limits to the size of the mesh with some margin
356
        """
357
        xmin, xmax = mesh.lims.pos[0]
×
358
        ymin, ymax = mesh.lims.pos[1]
×
359
        lx = xmax - xmin
×
360
        ly = ymax - ymin
×
361
        self.mesh_limits = [
×
362
            xmin - self.margins * lx,
363
            xmax + self.margins * lx,
364
            ymin - self.margins * ly,
365
            ymax + self.margins * ly,
366
        ]
367

368

369
def plot2D(mesh):
×
370
    # make a new app & frame
371
    app = wx.App()
×
372
    frame = MeshFrame(mesh, None, -1, title="pymech")
×
373

374
    frame.Show()
×
375

376
    # Start the event loop.
377
    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