v0.16.0
Loading...
Searching...
No Matches
make_mesh_png.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4import argparse
5import html
6import json
7from glob import glob
8from pathlib import Path
9
10from matplotlib import colors as mcolors
11import matplotlib.pyplot as plt
12import numpy as np
13import pyvista as pv
14
15
16GLYPH_VECTOR_NAME = "__glyph_vectors"
17
18
20 files = []
21 for entry in inputs:
22 matches = sorted(glob(entry))
23 if matches:
24 files.extend(matches)
25 else:
26 files.append(entry)
27 return files
28
29
31 return [file for file in files if file.lower().endswith(".vtk")]
32
33
34def select_field_component(mesh, field_name, component):
35 if field_name in mesh.point_data:
36 data = mesh.point_data[field_name]
37 target = mesh.point_data
38 elif field_name in mesh.cell_data:
39 data = mesh.cell_data[field_name]
40 target = mesh.cell_data
41 else:
42 raise KeyError(f"Field '{field_name}' was not found in point or cell data.")
43
44 values = np.asarray(data)
45 if values.ndim == 1:
46 values = values.reshape(-1, 1)
47 else:
48 values = values.reshape(values.shape[0], -1)
49
50 component_index = component
51
52 if component_index < 0 or component_index >= values.shape[1]:
53 raise ValueError(
54 f"Field '{field_name}' has {values.shape[1]} component(s), "
55 f"so component {component} is out of range. "
56 f"Use 0..{values.shape[1] - 1}."
57 )
58
59 component_name = f"{field_name}[{component_index}]"
60 target[component_name] = values[:, component_index]
61 return component_name
62
63
65 return plt.get_cmap("turbo", 124)
66
67
68def get_scalar_name(mesh, args):
69 if not args.field:
70 return None
71 if args.field_component is not None:
72 return select_field_component(mesh, args.field, args.field_component)
73 return args.field
74
75
76def make_plotter(mesh, args, off_screen=True):
77 cmap = get_cmap()
78 plotter = pv.Plotter(notebook=False, off_screen=off_screen)
79
80 scalar_name = get_scalar_name(mesh, args)
81 if scalar_name:
82 plotter.add_mesh(
83 mesh,
84 scalars=scalar_name,
85 show_edges=args.show_edges,
86 smooth_shading=False,
87 cmap=cmap,
88 )
89 else:
90 plotter.add_mesh(
91 mesh,
92 show_edges=args.show_edges,
93 edge_color="white",
94 color="white",
95 )
96
97 glyph_source = make_glyph_source(mesh, args)
98 if glyph_source is not None:
99 glyphs = make_glyph_mesh(glyph_source, args)
100 plotter.add_mesh(
101 glyphs,
102 color=args.glyph_color,
103 smooth_shading=True,
104 )
105
106 if args.d2:
107 plotter.camera_position = args.d2
108 plotter.camera.zoom(args.zoom)
109 plotter.camera.roll += args.roll
110 plotter.camera.azimuth += args.azimuth
111 return plotter
112
113
115 try:
116 surface = mesh.extract_surface(algorithm="dataset_surface")
117 except TypeError:
118 surface = mesh.extract_surface()
119 return surface.triangulate()
120
121
123 values = np.asarray(data)
124 if values.ndim == 1:
125 return values.astype(float)
126
127 values = values.reshape(values.shape[0], -1).astype(float)
128 if values.shape[1] == 1:
129 return values[:, 0]
130 return np.linalg.norm(values, axis=1)
131
132
133def vector_values(data, field_name):
134 values = np.asarray(data)
135 if values.ndim == 1:
136 values = values.reshape(-1, 1)
137 else:
138 values = values.reshape(values.shape[0], -1)
139
140 if values.shape[1] < 2:
141 raise ValueError(
142 f"Glyph field '{field_name}' has {values.shape[1]} component(s). "
143 "Use a vector field with at least 2 components."
144 )
145
146 vectors = np.zeros((values.shape[0], 3), dtype=float)
147 vectors[:, : min(values.shape[1], 3)] = values[:, :3]
148 return vectors
149
150
151def make_glyph_source(mesh, args):
152 if not args.glyph_field:
153 return None
154
155 if args.glyph_field in mesh.point_data:
156 points = np.asarray(mesh.points, dtype=float)
157 vectors = vector_values(mesh.point_data[args.glyph_field], args.glyph_field)
158 elif args.glyph_field in mesh.cell_data:
159 centers = mesh.cell_centers()
160 points = np.asarray(centers.points, dtype=float)
161 vectors = vector_values(mesh.cell_data[args.glyph_field], args.glyph_field)
162 else:
163 raise KeyError(
164 f"Glyph field '{args.glyph_field}' was not found in point or cell data."
165 )
166
167 vector_norms = np.linalg.norm(vectors, axis=1)
168 valid = (
169 np.isfinite(points).all(axis=1)
170 & np.isfinite(vectors).all(axis=1)
171 & (vector_norms > 0)
172 )
173 points = points[valid]
174 vectors = vectors[valid]
175
176 if points.size == 0:
177 return None
178
179 stride = max(1, int(args.glyph_stride))
180 points = points[::stride]
181 vectors = vectors[::stride]
182
183 source = pv.PolyData(points)
184 source.point_data[GLYPH_VECTOR_NAME] = vectors
185 return source
186
187
188def make_glyph_mesh(glyph_source, args):
189 return glyph_source.glyph(
190 orient=GLYPH_VECTOR_NAME,
191 scale=GLYPH_VECTOR_NAME,
192 factor=args.glyph_magnitude,
193 geom=pv.Arrow(tip_resolution=12, shaft_resolution=12),
194 )
195
196
197def values_to_rgb(values):
198 values = np.asarray(values, dtype=float)
199 finite_values = values[np.isfinite(values)]
200 if finite_values.size:
201 value_min = float(np.min(finite_values))
202 value_max = float(np.max(finite_values))
203 else:
204 value_min = 0.0
205 value_max = 1.0
206
207 if value_max > value_min:
208 normalized = (values - value_min) / (value_max - value_min)
209 else:
210 normalized = np.full(values.shape, 0.5)
211
212 normalized = np.nan_to_num(normalized, nan=0.5, posinf=1.0, neginf=0.0)
213 colors = get_cmap()(np.clip(normalized, 0.0, 1.0))[:, :3]
214 return colors, value_min, value_max
215
216
217def make_edge_positions(triangle_points):
218 edges = triangle_points[:, ((0, 1), (1, 2), (2, 0)), :]
219 return edges.reshape(-1, 3)
220
221
222def make_glyph_surface_payload(mesh, args, center, radius):
223 source = make_glyph_source(mesh, args)
224 if source is None:
225 return {
226 "positions": [],
227 "normals": [],
228 "colors": [],
229 }
230
231 glyph_surface = extract_surface(make_glyph_mesh(source, args))
232 if glyph_surface.n_points == 0 or glyph_surface.n_cells == 0:
233 return {
234 "positions": [],
235 "normals": [],
236 "colors": [],
237 }
238
239 faces = np.asarray(glyph_surface.faces, dtype=np.int64).reshape(-1, 4)[:, 1:4]
240 points = np.asarray(glyph_surface.points, dtype=float)
241 triangle_points = points[faces]
242 face_normals = np.cross(
243 triangle_points[:, 1] - triangle_points[:, 0],
244 triangle_points[:, 2] - triangle_points[:, 0],
245 )
246 normal_lengths = np.linalg.norm(face_normals, axis=1)
247 valid_normals = normal_lengths > 0
248 face_normals[valid_normals] /= normal_lengths[valid_normals, None]
249 face_normals[~valid_normals] = (0.0, 0.0, 1.0)
250
251 positions = ((triangle_points.reshape(-1, 3) - center) / radius).astype(float)
252 normals = np.repeat(face_normals, 3, axis=0)
253 glyph_color = np.array(mcolors.to_rgb(args.glyph_color), dtype=float)
254 colors = np.tile(glyph_color, (positions.shape[0], 1))
255
256 return {
257 "positions": positions.ravel().round(8).tolist(),
258 "normals": normals.ravel().round(8).tolist(),
259 "colors": colors.ravel().round(8).tolist(),
260 }
261
262
263def make_widget_payload(file, mesh, args):
264 scalar_name = get_scalar_name(mesh, args)
265 surface = extract_surface(mesh)
266 faces = np.asarray(surface.faces, dtype=np.int64).reshape(-1, 4)[:, 1:4]
267 points = np.asarray(surface.points, dtype=float)
268
269 if points.size == 0 or faces.size == 0:
270 raise RuntimeError(f"File '{file}' does not contain surface triangles.")
271
272 triangle_points = points[faces]
273 face_normals = np.cross(
274 triangle_points[:, 1] - triangle_points[:, 0],
275 triangle_points[:, 2] - triangle_points[:, 0],
276 )
277 normal_lengths = np.linalg.norm(face_normals, axis=1)
278 valid_normals = normal_lengths > 0
279 face_normals[valid_normals] /= normal_lengths[valid_normals, None]
280 face_normals[~valid_normals] = (0.0, 0.0, 1.0)
281
282 if scalar_name:
283 if scalar_name in surface.point_data:
284 scalar_data = scalar_values(surface.point_data[scalar_name])
285 colors, scalar_min, scalar_max = values_to_rgb(scalar_data[faces].ravel())
286 elif scalar_name in surface.cell_data:
287 scalar_data = scalar_values(surface.cell_data[scalar_name])
288 colors, scalar_min, scalar_max = values_to_rgb(np.repeat(scalar_data, 3))
289 else:
290 raise KeyError(
291 f"Field '{scalar_name}' was not found on the extracted surface."
292 )
293 else:
294 colors = np.tile(np.array([[0.92, 0.92, 0.88]]), (faces.size, 1))
295 scalar_min = None
296 scalar_max = None
297
298 center = np.mean(points, axis=0)
299 radius = np.max(np.linalg.norm(points - center, axis=1))
300 if not np.isfinite(radius) or radius <= 0:
301 radius = 1.0
302
303 positions = ((triangle_points.reshape(-1, 3) - center) / radius).astype(float)
304 normals = np.repeat(face_normals, 3, axis=0)
305 edge_positions = ((make_edge_positions(triangle_points) - center) / radius).astype(
306 float
307 )
308 glyph_payload = make_glyph_surface_payload(mesh, args, center, radius)
309
310 return {
311 "file": Path(file).name,
312 "scalar": scalar_name,
313 "glyph": args.glyph_field or None,
314 "scalarMin": scalar_min,
315 "scalarMax": scalar_max,
316 "showEdges": bool(args.show_edges),
317 "zoom": float(args.zoom),
318 "d2": args.d2,
319 "roll": float(args.roll),
320 "azimuth": float(args.azimuth),
321 "positions": positions.ravel().round(8).tolist(),
322 "normals": normals.ravel().round(8).tolist(),
323 "colors": colors.ravel().round(8).tolist(),
324 "edges": edge_positions.ravel().round(8).tolist(),
325 "glyphPositions": glyph_payload["positions"],
326 "glyphNormals": glyph_payload["normals"],
327 "glyphColors": glyph_payload["colors"],
328 }
329
330
331def make_widget_html(payload):
332 payload_json = json.dumps(payload, separators=(",", ":")).replace("</", "<\\/")
333 title = html.escape(payload["file"])
334 return f"""<!doctype html>
335<html lang="en">
336<head>
337<meta charset="utf-8">
338<meta name="viewport" content="width=device-width, initial-scale=1">
339<title>{title}</title>
340<style>
341html, body {{
342 margin: 0;
343 width: 100%;
344 height: 100%;
345 overflow: hidden;
346 background: #f5f5f2;
347 font-family: Arial, sans-serif;
348}}
349#viewer {{
350 width: 100vw;
351 height: 100vh;
352 display: block;
353 cursor: grab;
354}}
355#viewer:active {{
356 cursor: grabbing;
357}}
358#toolbar {{
359 position: fixed;
360 top: 12px;
361 right: 12px;
362 display: flex;
363 gap: 6px;
364 align-items: center;
365}}
366button {{
367 min-width: 40px;
368 height: 32px;
369 border: 1px solid rgba(16, 24, 32, 0.2);
370 border-radius: 4px;
371 background: rgba(255, 255, 255, 0.86);
372 color: #111820;
373 font-size: 12px;
374 font-weight: 600;
375}}
376button:hover {{
377 background: #ffffff;
378}}
379#status {{
380 position: fixed;
381 left: 12px;
382 top: 12px;
383 max-width: min(520px, calc(100vw - 24px));
384 padding: 8px 10px;
385 border: 1px solid rgba(16, 24, 32, 0.18);
386 border-radius: 4px;
387 background: rgba(255, 255, 255, 0.88);
388 color: #111820;
389 font-size: 12px;
390 line-height: 1.4;
391 white-space: pre-wrap;
392}}
393#status.hidden {{
394 display: none;
395}}
396#colorbar {{
397 position: fixed;
398 left: 12px;
399 bottom: 12px;
400 display: none;
401 align-items: flex-end;
402 gap: 8px;
403 color: #111820;
404 font-size: 11px;
405 background: rgba(255, 255, 255, 0.78);
406 border: 1px solid rgba(16, 24, 32, 0.16);
407 border-radius: 4px;
408 padding: 8px;
409}}
410#gradient {{
411 width: 160px;
412 height: 12px;
413 background: linear-gradient(90deg, #30123b, #4662d7, #36a9e1, #1ae4b6, #72fe5c, #d7e219, #f89441, #d93806, #7a0403);
414 border: 1px solid rgba(16, 24, 32, 0.24);
415}}
416#legend-values {{
417 display: flex;
418 justify-content: space-between;
419 width: 160px;
420 margin-top: 4px;
421}}
422</style>
423</head>
424<body>
425<canvas id="viewer"></canvas>
426<div id="status">Loading mesh...</div>
427<div id="toolbar">
428 <button type="button" data-view="iso">Iso</button>
429 <button type="button" data-view="xy">XY</button>
430 <button type="button" data-view="xz">XZ</button>
431 <button type="button" data-view="yz">YZ</button>
432</div>
433<div id="colorbar">
434 <div>
435 <div id="gradient"></div>
436 <div id="legend-values"><span id="legend-min"></span><span id="legend-max"></span></div>
437 </div>
438</div>
439<script>
440const payload = {payload_json};
441const canvas = document.getElementById("viewer");
442const status = document.getElementById("status");
443window.addEventListener("error", (event) => showError(event.message));
444window.addEventListener("unhandledrejection", (event) => showError(String(event.reason)));
445const gl = canvas.getContext("webgl", {{ antialias: true }});
446if (!gl) {{
447 showError("WebGL is not available in this browser. Try opening this file in Firefox/Chrome instead of VS Code Simple Browser.");
448 throw new Error("WebGL is not available in this browser.");
449}}
450
451const positions = new Float32Array(payload.positions);
452const normals = new Float32Array(payload.normals);
453const colors = new Float32Array(payload.colors);
454const edges = new Float32Array(payload.edges);
455const glyphPositions = new Float32Array(payload.glyphPositions);
456const glyphNormals = new Float32Array(payload.glyphNormals);
457const glyphColors = new Float32Array(payload.glyphColors);
458let rotation = identity();
459let zoom = Number.isFinite(payload.zoom) ? payload.zoom : 1.2;
460let dragging = false;
461let lastX = 0;
462let lastY = 0;
463
464const meshProgram = makeProgram(`
465attribute vec3 aPosition;
466attribute vec3 aNormal;
467attribute vec3 aColor;
468uniform mat4 uModelView;
469uniform mat4 uProjection;
470varying vec3 vNormal;
471varying vec3 vColor;
472void main() {{
473 vec4 pos = uModelView * vec4(aPosition, 1.0);
474 vNormal = (uModelView * vec4(aNormal, 0.0)).xyz;
475 vColor = aColor;
476 gl_Position = uProjection * pos;
477}}`, `
478precision mediump float;
479varying vec3 vNormal;
480varying vec3 vColor;
481void main() {{
482 vec3 normal = normalize(vNormal);
483 vec3 light = normalize(vec3(0.35, 0.55, 0.9));
484 float diffuse = max(dot(normal, light), 0.0);
485 float shade = 0.34 + 0.66 * diffuse;
486 gl_FragColor = vec4(vColor * shade, 1.0);
487}}`);
488
489const lineProgram = makeProgram(`
490attribute vec3 aPosition;
491uniform mat4 uModelView;
492uniform mat4 uProjection;
493void main() {{
494 gl_Position = uProjection * uModelView * vec4(aPosition, 1.0);
495}}`, `
496precision mediump float;
497uniform vec4 uColor;
498void main() {{
499 gl_FragColor = uColor;
500}}`);
501
502const positionBuffer = bufferData(positions);
503const normalBuffer = bufferData(normals);
504const colorBuffer = bufferData(colors);
505const edgeBuffer = bufferData(edges);
506const glyphPositionBuffer = bufferData(glyphPositions);
507const glyphNormalBuffer = bufferData(glyphNormals);
508const glyphColorBuffer = bufferData(glyphColors);
509
510setInitialView();
511setupLegend();
512resize();
513requestAnimationFrame(draw);
514setTimeout(() => status.classList.add("hidden"), 1200);
515
516window.addEventListener("resize", resize);
517canvas.addEventListener("pointerdown", (event) => {{
518 dragging = true;
519 lastX = event.clientX;
520 lastY = event.clientY;
521 canvas.setPointerCapture(event.pointerId);
522}});
523canvas.addEventListener("pointermove", (event) => {{
524 if (!dragging) return;
525 const dx = event.clientX - lastX;
526 const dy = event.clientY - lastY;
527 lastX = event.clientX;
528 lastY = event.clientY;
529 rotation = multiply(rotateX(dy * 0.01), multiply(rotateY(dx * 0.01), rotation));
530 requestAnimationFrame(draw);
531}});
532canvas.addEventListener("pointerup", () => {{
533 dragging = false;
534}});
535canvas.addEventListener("wheel", (event) => {{
536 event.preventDefault();
537 zoom *= Math.exp(-event.deltaY * 0.001);
538 zoom = Math.min(Math.max(zoom, 0.15), 20.0);
539 requestAnimationFrame(draw);
540}}, {{ passive: false }});
541document.querySelectorAll("button[data-view]").forEach((button) => {{
542 button.addEventListener("click", () => {{
543 setView(button.dataset.view);
544 requestAnimationFrame(draw);
545 }});
546}});
547
548function setupLegend() {{
549 if (payload.scalar === null || payload.scalarMin === null || payload.scalarMax === null) return;
550 document.getElementById("colorbar").style.display = "flex";
551 document.getElementById("legend-min").textContent = formatValue(payload.scalarMin);
552 document.getElementById("legend-max").textContent = formatValue(payload.scalarMax);
553}}
554
555function showError(message) {{
556 status.classList.remove("hidden");
557 status.textContent = "Viewer error: " + message;
558}}
559
560function formatValue(value) {{
561 const absValue = Math.abs(value);
562 if ((absValue > 0 && absValue < 0.001) || absValue >= 10000) {{
563 return value.toExponential(2);
564 }}
565 return value.toPrecision(4);
566}}
567
568function resize() {{
569 const dpr = window.devicePixelRatio || 1;
570 const width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
571 const height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
572 if (canvas.width !== width || canvas.height !== height) {{
573 canvas.width = width;
574 canvas.height = height;
575 }}
576 gl.viewport(0, 0, canvas.width, canvas.height);
577 requestAnimationFrame(draw);
578}}
579
580function draw() {{
581 if (!gl) return;
582 gl.clearColor(0.96, 0.96, 0.94, 1.0);
583 gl.clearDepth(1.0);
584 gl.enable(gl.DEPTH_TEST);
585 gl.depthFunc(gl.LEQUAL);
586 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
587
588 const aspect = canvas.width / Math.max(1, canvas.height);
589 const projection = perspective(35 * Math.PI / 180, aspect, 0.01, 100.0);
590 const distance = 3.15 / Math.max(zoom, 0.001);
591 const modelView = multiply(translate(0, 0, -distance), rotation);
592
593 gl.useProgram(meshProgram.program);
594 setMat4(meshProgram, "uProjection", projection);
595 setMat4(meshProgram, "uModelView", modelView);
596 bindAttrib(meshProgram, "aPosition", positionBuffer, 3);
597 bindAttrib(meshProgram, "aNormal", normalBuffer, 3);
598 bindAttrib(meshProgram, "aColor", colorBuffer, 3);
599 gl.enable(gl.POLYGON_OFFSET_FILL);
600 gl.polygonOffset(1, 1);
601 gl.drawArrays(gl.TRIANGLES, 0, positions.length / 3);
602 gl.disable(gl.POLYGON_OFFSET_FILL);
603
604 if (payload.showEdges && edges.length > 0) {{
605 gl.useProgram(lineProgram.program);
606 setMat4(lineProgram, "uProjection", projection);
607 setMat4(lineProgram, "uModelView", modelView);
608 setVec4(lineProgram, "uColor", [0.08, 0.09, 0.1, 0.42]);
609 bindAttrib(lineProgram, "aPosition", edgeBuffer, 3);
610 gl.drawArrays(gl.LINES, 0, edges.length / 3);
611 }}
612
613 if (glyphPositions.length > 0) {{
614 gl.useProgram(meshProgram.program);
615 setMat4(meshProgram, "uProjection", projection);
616 setMat4(meshProgram, "uModelView", modelView);
617 bindAttrib(meshProgram, "aPosition", glyphPositionBuffer, 3);
618 bindAttrib(meshProgram, "aNormal", glyphNormalBuffer, 3);
619 bindAttrib(meshProgram, "aColor", glyphColorBuffer, 3);
620 gl.drawArrays(gl.TRIANGLES, 0, glyphPositions.length / 3);
621 }}
622}}
623
624function setView(view) {{
625 if (view === "xy") rotation = identity();
626 if (view === "xz") rotation = rotateX(-Math.PI / 2);
627 if (view === "yz") rotation = rotateY(Math.PI / 2);
628 if (view === "iso") rotation = multiply(rotateX(-0.75), rotateY(0.72));
629}}
630
631function setInitialView() {{
632 const view = String(payload.d2 || "iso").toLowerCase();
633 setView(["xy", "xz", "yz", "iso"].includes(view) ? view : "iso");
634 const azimuth = Number(payload.azimuth) || 0;
635 const roll = Number(payload.roll) || 0;
636 rotation = multiply(rotateY(azimuth * Math.PI / 180), rotation);
637 rotation = multiply(rotateZ(roll * Math.PI / 180), rotation);
638}}
639
640function makeProgram(vertexSource, fragmentSource) {{
641 const vertexShader = compileShader(gl.VERTEX_SHADER, vertexSource);
642 const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSource);
643 const program = gl.createProgram();
644 gl.attachShader(program, vertexShader);
645 gl.attachShader(program, fragmentShader);
646 gl.linkProgram(program);
647 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {{
648 throw new Error(gl.getProgramInfoLog(program));
649 }}
650 return {{ program, locations: new Map() }};
651}}
652
653function compileShader(type, source) {{
654 const shader = gl.createShader(type);
655 gl.shaderSource(shader, source);
656 gl.compileShader(shader);
657 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {{
658 throw new Error(gl.getShaderInfoLog(shader));
659 }}
660 return shader;
661}}
662
663function bufferData(data) {{
664 const buffer = gl.createBuffer();
665 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
666 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
667 return buffer;
668}}
669
670function bindAttrib(programInfo, name, buffer, size) {{
671 const location = attribLocation(programInfo, name);
672 gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
673 gl.enableVertexAttribArray(location);
674 gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
675}}
676
677function attribLocation(programInfo, name) {{
678 if (!programInfo.locations.has(name)) {{
679 programInfo.locations.set(name, gl.getAttribLocation(programInfo.program, name));
680 }}
681 return programInfo.locations.get(name);
682}}
683
684function uniformLocation(programInfo, name) {{
685 if (!programInfo.locations.has(name)) {{
686 programInfo.locations.set(name, gl.getUniformLocation(programInfo.program, name));
687 }}
688 return programInfo.locations.get(name);
689}}
690
691function setMat4(programInfo, name, matrix) {{
692 gl.uniformMatrix4fv(uniformLocation(programInfo, name), false, new Float32Array(matrix));
693}}
694
695function setVec4(programInfo, name, value) {{
696 gl.uniform4fv(uniformLocation(programInfo, name), new Float32Array(value));
697}}
698
699function identity() {{
700 return [
701 1, 0, 0, 0,
702 0, 1, 0, 0,
703 0, 0, 1, 0,
704 0, 0, 0, 1,
705 ];
706}}
707
708function translate(x, y, z) {{
709 return [
710 1, 0, 0, 0,
711 0, 1, 0, 0,
712 0, 0, 1, 0,
713 x, y, z, 1,
714 ];
715}}
716
717function rotateX(angle) {{
718 const c = Math.cos(angle);
719 const s = Math.sin(angle);
720 return [
721 1, 0, 0, 0,
722 0, c, s, 0,
723 0, -s, c, 0,
724 0, 0, 0, 1,
725 ];
726}}
727
728function rotateY(angle) {{
729 const c = Math.cos(angle);
730 const s = Math.sin(angle);
731 return [
732 c, 0, -s, 0,
733 0, 1, 0, 0,
734 s, 0, c, 0,
735 0, 0, 0, 1,
736 ];
737}}
738
739function rotateZ(angle) {{
740 const c = Math.cos(angle);
741 const s = Math.sin(angle);
742 return [
743 c, s, 0, 0,
744 -s, c, 0, 0,
745 0, 0, 1, 0,
746 0, 0, 0, 1,
747 ];
748}}
749
750function perspective(fovy, aspect, near, far) {{
751 const f = 1.0 / Math.tan(fovy / 2.0);
752 const nf = 1.0 / (near - far);
753 return [
754 f / aspect, 0, 0, 0,
755 0, f, 0, 0,
756 0, 0, (far + near) * nf, -1,
757 0, 0, 2 * far * near * nf, 0,
758 ];
759}}
760
761function multiply(a, b) {{
762 const out = new Array(16);
763 for (let col = 0; col < 4; col++) {{
764 for (let row = 0; row < 4; row++) {{
765 out[col * 4 + row] =
766 a[0 * 4 + row] * b[col * 4 + 0] +
767 a[1 * 4 + row] * b[col * 4 + 1] +
768 a[2 * 4 + row] * b[col * 4 + 2] +
769 a[3 * 4 + row] * b[col * 4 + 3];
770 }}
771 }}
772 return out;
773}}
774</script>
775</body>
776</html>
777"""
778
779
780def read_mesh(file, args):
781 mesh = pv.read(file)
782
783 if args.wrap_vector:
784 mesh = mesh.warp_by_vector(args.wrap_vector, factor=args.wrap_factor)
785 return mesh
786
787
788def make_png(file, mesh, args):
789 output_file = str(Path(file).with_suffix(".png"))
790 plotter = make_plotter(mesh, args)
791 try:
792 plotter.screenshot(output_file)
793 finally:
794 plotter.close()
795
796
797def make_widget(file, mesh, args):
798 output_file = str(Path(file).with_suffix(".html"))
799 payload = make_widget_payload(file, mesh, args)
800 Path(output_file).write_text(make_widget_html(payload), encoding="utf-8")
801
802
803def make_outputs(file, args):
804 mesh = read_mesh(file, args)
805 if not args.widget_only:
806 make_png(file, mesh, args)
807 if args.write_widget or args.widget_only:
808 make_widget(file, mesh, args)
809
810
812 try:
813 from pyvirtualdisplay import Display
814 except ImportError:
815 return None
816
817 try:
818 display = Display(backend="xvfb", visible=False, size=(800, 800))
819 display.start()
820 except Exception:
821 return None
822 return display
823
824
825if __name__ == "__main__":
826 parser = argparse.ArgumentParser(
827 description=(
828 "Convert multiple vtk files to png files or interactive HTML "
829 "widgets using PyVista."
830 )
831 )
832 parser.add_argument(
833 "files",
834 help="list of vtk files or glob masks",
835 nargs="+",
836 )
837 parser.add_argument("-d2", "--d2", dest="d2", default="")
838 parser.add_argument("-f", "--field", dest="field", default="", type=str)
839 parser.add_argument(
840 "--field-component",
841 dest="field_component",
842 default=None,
843 type=int,
844 help="0-based component index for vector/tensor fields",
845 )
846 parser.add_argument(
847 "-g",
848 "--glyph",
849 "--glyph-field",
850 dest="glyph_field",
851 default="",
852 type=str,
853 help="Vector field used to draw glyph arrows.",
854 )
855 parser.add_argument(
856 "--glyph-magnitude",
857 "--glyph-factor",
858 dest="glyph_magnitude",
859 default=1.0,
860 type=float,
861 help="Scale factor applied to glyph arrow lengths.",
862 )
863 parser.add_argument(
864 "--glyph-stride",
865 dest="glyph_stride",
866 default=1,
867 type=int,
868 help="Draw every Nth glyph vector.",
869 )
870 parser.add_argument(
871 "--glyph-color",
872 dest="glyph_color",
873 default="red",
874 type=str,
875 help="Glyph color for PNG output.",
876 )
877 parser.add_argument(
878 "-wv", "--wrap_vector", dest="wrap_vector", default="", type=str
879 )
880 parser.add_argument("--wrap-factor", dest="wrap_factor", default=1.0, type=float)
881 parser.add_argument("--zoom", dest="zoom", default=1.2, type=float)
882 parser.add_argument("--roll", dest="roll", default=0, type=float)
883 parser.add_argument("--azimuth", dest="azimuth", default=0, type=float)
884 parser.add_argument(
885 "--show-edges",
886 dest="show_edges",
887 action="store_true",
888 default=True,
889 help="Show mesh edges (default)",
890 )
891 parser.add_argument(
892 "--no-show-edges",
893 dest="show_edges",
894 action="store_false",
895 help="Hide mesh edges",
896 )
897 parser.add_argument(
898 "--widget",
899 "--html",
900 dest="write_widget",
901 action="store_true",
902 help="Also write an interactive HTML widget next to each PNG file.",
903 )
904 parser.add_argument(
905 "--widget-only",
906 dest="widget_only",
907 action="store_true",
908 help="Write only the interactive HTML widget, without a PNG screenshot.",
909 )
910 parser.add_argument("--debug", dest="debug", action="store_true")
911 args = parser.parse_args()
912
913 if args.debug:
914 print(args)
915
916 file_list = filter_vtk_files(expand_input_files(args.files))
917 if not file_list:
918 parser.error("No vtk files were found with the given names or glob masks.")
919
921 try:
922 for file_name in file_list:
923 make_outputs(file_name, args)
924 finally:
925 if display is not None:
926 display.stop()
make_png(file, mesh, args)
make_plotter(mesh, args, off_screen=True)
values_to_rgb(values)
make_edge_positions(triangle_points)
make_glyph_mesh(glyph_source, args)
make_glyph_surface_payload(mesh, args, center, radius)
make_widget_html(payload)
make_outputs(file, args)
make_glyph_source(mesh, args)
vector_values(data, field_name)
get_scalar_name(mesh, args)
select_field_component(mesh, field_name, component)
make_widget(file, mesh, args)
read_mesh(file, args)
expand_input_files(inputs)
filter_vtk_files(files)
make_widget_payload(file, mesh, args)