Skip to content

Save diagrams as svg and pdf files - #328

Open
camUrban wants to merge 8 commits into
mainfrom
diagram_vector_output
Open

camUrban wants to merge 8 commits into
mainfrom
diagram_vector_output

Conversation

@camUrban

@camUrban camUrban commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Description

This PR lets every diagram method save its diagram as an svg or pdf file, chosen by the path's suffix, alongside the existing WebP output. The scene is recorded in a new private module, _vector_export.py, as the rendering helpers draw it, then exported through a parallel camera that matches the window's final view. Visibility is resolved in software: opaque fills are split and ordered by a BSP painter, and strokes are clipped against every nearer triangle and against each other, so the file shows what the window showed. Labels are read from VTK after every render, so dragged and edited labels survive, and are written as selectable text with their fonts embedded. The diagram methods also gain figure_size_in, font_size, text_color, and line_width, which size a vector diagram for a paper the way plot_results_versus_time sizes its figures.

All new parameters default to the current behavior, so existing calls are unaffected, and the WebP output is unchanged. output.draw is deliberately out of scope here and will adopt the same module in a follow-up PR, so the module's translucent polygon and screen polygon support, which only draw needs, is currently unused.

Motivation

Diagrams are mainly useful as figures in papers, slides, and documentation, where a WebP screenshot blurs when scaled and its labels can't be selected or searched. VTK's own vector export (GL2PS) rasterizes 3D geometry with the OpenGL2 backend, so it can't produce true vector output of these scenes. Recording the scene and resolving visibility in software gives sharp output at any size whose labels stay text. The figure parameters exist because a vector diagram is otherwise sized by the window, so its fonts and lines have no predictable physical size on a printed page.

Relevant Issues

Part of #266.

Changes

  • Added _vector_export.py with VectorCamera (a parallel camera that projects diagram axes to display coordinates, plus fit_to_page), VectorText, VectorLayer, and VectorScene. A layer records opaque quadrilaterals, polylines, convex occluders (the arrow tips), dots, and translucent polygons, and VectorScene.save writes the scene through Matplotlib with TrueType fonts in pdf files and embedded fonts in svg files.
  • Implemented the BSP painter (get_paint_order), adapted from csg.js, which uses explicit stacks and samples candidate splitters, so it orders crossing fills exactly and can't hit the recursion limit.
  • Implemented hidden-line removal (get_visible_intervals). Each stroke and each occluder outline hides the strokes behind it across its own width, so the nearer of two crossing strokes shows, matching VTK's depth test. Strokes that share an end are exempt, and triangles are looked up through a grid on screen so diagrams with tens of thousands of strokes stay fast.
  • Moved SVG font embedding from _output_plotting.py to _fonts.py as embed_fonts_in_svg, which avoids an import cycle. It now embeds every face the text uses, including the STIX faces Matplotlib writes math in, each subset to its glyphs. plot_results_versus_time uses the moved function.
  • Threaded an optional layer parameter through add_axes_and_points, _add_arrow_tips, add_airfoil, add_airfoil_lines, add_airfoils, add_panels, add_vortices, and add_steady_problem in _output_rendering.py. Each helper records what it draws as it adds the PyVista actors, and each collocation point's cross is recorded as four half arms so its strokes don't clip each other.
  • Updated show_diagram to read the camera, window size, and labels after every render (_get_diagram_export_state) and to export from the last reading when the path ends with ".svg" or ".pdf". Each label is drawn in its VTK background box, with math in STIX and other text in Liberation Mono.
  • Widened path on Airfoil.diagram, WingCrossSection.diagram, Wing.diagram, Airplane.diagram, SteadyProblem.diagram, the unsteady problems' diagram, and the three solvers' diagram methods to accept ".svg" and ".pdf".
  • Added figure_size_in, font_size, text_color, and line_width to the same methods, validated by _output_rendering.validate_diagram_figure_parameters:
    • figure_size_in opens the window at the page's aspect ratio and scales the view to fill the page, while text and line widths stay in points.
    • font_size sets every label's size, including math labels.
    • text_color colors the labels.
    • line_width sets the width of the panel edges, airfoil lines, and vortices, and scales the other lines with it.
    • None of them affect the window or WebP output.
  • Added tests/unit/test_vector_export.py and its fixtures, covering projection, page fitting, paint order (including a test that samples inside every fragment's footprint), stroke clipping and depth ties, line width scaling, and svg and pdf writing. Added tests/unit/test_fonts.py for the moved font embedding, and extended tests/unit/test_output_rendering.py for layer recording, export state reading, page fitting, label sizing and coloring, and parameter validation.
  • Added _vector_export.py to the architecture list in CLAUDE.md, and updated the _fonts.py entry.

Dependency Updates

None.

Change Magnitude

Moderate: Medium-sized change that adds or modifies a feature without large-scale impact.

Checklist (check each item when completed or not applicable)

  • I am familiar with the current contribution guidelines.
  • PR description links all relevant issues and follows this template.
  • My branch is based on main and is up to date with the upstream main branch.
  • All calculations use S.I. units.
  • Code is formatted with black (line length = 88).
  • Code is well documented with block comments where appropriate.
  • Any external code, algorithms, or equations used have been cited in comments or docstrings.
  • All new modules, classes, functions, and methods have docstrings in reStructuredText format, and are formatted using docformatter (--in-place --black). See the style guide for type hints and docstrings for more details.
  • All new classes, functions, and methods in the pterasoftware package use type hints. See the style guide for type hints and docstrings for more details.
  • If any major functionality was added or significantly changed, I have added or updated tests in the tests package.
  • Code locally passes all tests in the tests package.
  • This PR passes the ReadTheDocs build check (this runs automatically with the other workflows).
  • This PR passes the ascii-only, pre-commit-hooks, and zizmor GitHub actions.
  • This PR passes the lint job of the CI GitHub action.
  • This PR passes the test jobs of the CI GitHub action.

Assisted-by: Claude Opus 5.5

camUrban and others added 5 commits October 2, 2026 00:16
Lay the groundwork for saving draw and the diagrams as svg and pdf
files. The OpenGL2 backend rasterizes 3D geometry in GL2PS exports, so
resolve visibility in software instead: split and order the opaque
fills with a BSP painter, clip the strokes against every nearer
triangle, and write the result through Matplotlib, embedding the
vendored font in svg text as the results plots already do.

Keep the module unwired for now, so the diagrams and draw can adopt it
in their own commits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Let the vector export embed the vendored font without importing the
plotting module, which imports the rendering module. The rendering
module will import the vector export once the diagrams adopt it, so
leaving the function in place would close an import cycle. Keep it
beside the font it embeds, in a module that imports none of the three.

Name the font family through the fonts module in the moved SVG fixture,
so the fixture follows the font if it ever changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Give papers and slides diagrams that stay sharp at any size and keep
their labels as selectable text. Record each diagram as it is drawn and
export it through the core vector module, rather than rebuilding the
scene from VTK's actors, since recording keeps what each helper means
explicit.

Read the camera, the window size, and the labels after every render,
and export from the last reading, so the dragged and edited labels
survive even when closing the window destroys it. Take each label's box
from VTK and center its text in it, which places it as VTK does without
modeling VTK's padding.

Paint coincident strokes so the first one recorded shows, since VTK's
depth test keeps the first of two equally deep fragments. Embed every
font face an svg's text uses, so math labels carry their STIX faces
along with Liberation Mono.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fix diagrams that painted every panel edge over every vortex line,
which buried vorticity arrows that sit in front of the panels. Painting
strokes in the reverse of the order they were recorded was based on a
wrong reading of VTK: a line VTK shows on top wins on depth, and of two
equally deep lines VTK keeps the later one.

Let every stroke and every occluder outline hide the strokes behind it
across its own width, so the nearer of two crossing strokes shows
wherever they cross, and a stroke behind an arrow tip can no longer
paint over the tip's outline. Widen each hiding ribbon by half the
hidden stroke's width, so a clipped stroke's round end stops short of
the stroke in front. Exempt strokes that share an end, so a polyline's
segments don't clip each other where they meet, and record each
collocation point's cross as four half arms from the point for the same
reason.

Look the triangles up through a grid on screen, since the ribbons add
two triangles per stroke and a diagram can hold tens of thousands of
strokes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Let a diagram saved as an svg or pdf file be sized for a paper, the way
plot_results_versus_time's figures can be. The page takes the requested
size in inches, and the view framed in the window scales to fill it,
while the labels and lines keep absolute sizes in points, so a figure
printed at a column's width gets the font size and line weight the
publisher asks for.

Open the window at the page's aspect ratio, so the framing chosen on
screen is the framing saved. Leave the window and WebP output
untouched, since they are screenshots of what VTK draws.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@camUrban camUrban added the feature New feature or request label Oct 2, 2026
@camUrban camUrban self-assigned this Oct 2, 2026
@camUrban camUrban added this to the v5.2.0 milestone Oct 2, 2026
@camUrban
camUrban requested a balanced review from Copilot October 2, 2026 16:15
@camUrban camUrban added the maintenance Improvements or additions to documentation, testing, robustness, or tooling label Oct 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Format dispatch, overlapping-occluder visibility, and unconditional scene-recording issues remain unresolved.

Review effort: Balanced
Findings: 2 High severity · 2 Medium severity

Open (4)
What changed in this PR

Adds SVG/PDF diagram export with selectable embedded-font text, configurable figure styling, and software-resolved visibility, advancing issue #266.

Changes:

  • Records rendered diagram geometry into vector scenes with BSP and hidden-line processing.
  • Extends all diagram APIs with vector formats and figure styling parameters.
  • Moves SVG font embedding into _fonts.py and adds focused tests.
File Description
CLAUDE.md Documents vector export architecture.
pterasoftware/​_core.py Forwards new diagram options.
pterasoftware/​_fonts.py Embeds all used SVG font faces.
pterasoftware/​_output_plotting.py Uses shared font embedding.
pterasoftware/​_output_rendering.py Records scenes and dispatches exports.
pterasoftware/​_vector_export.py Implements vector projection and visibility.
pterasoftware/​geometry/​airfoil.py Adds vector airfoil diagrams.
pterasoftware/​geometry/​airplane.py Adds vector airplane diagrams.
pterasoftware/​geometry/​wing.py Adds vector wing diagrams.
pterasoftware/​geometry/​wing_cross_section.py Adds vector cross-section diagrams.
pterasoftware/​problems.py Adds vector problem diagrams.
pterasoftware/​steady_horseshoe_vortex_lattice_method.py Exports horseshoe-solver diagrams.
pterasoftware/​steady_ring_vortex_lattice_method.py Exports ring-solver diagrams.
pterasoftware/​unsteady_ring_vortex_lattice_method.py Exports unsteady-solver diagrams.
tests/​unit/​fixtures/​fonts_fixtures.py Adds SVG font fixtures.
tests/​unit/​fixtures/​output_plotting_fixtures.py Removes relocated SVG fixture.
tests/​unit/​fixtures/​vector_export_fixtures.py Adds vector geometry fixtures.
tests/​unit/​test_fonts.py Tests multi-face font embedding.
tests/​unit/​test_output_plotting.py Removes relocated embedding tests.
tests/​unit/​test_output_rendering.py Tests recording and export state.
tests/​unit/​test_vector_export.py Tests vector algorithms and output.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pterasoftware/_output_rendering.py Outdated
Comment thread pterasoftware/_vector_export.py Outdated
Comment thread pterasoftware/_output_rendering.py
if entries:
# Paint the farthest occluder first. Each occluder's fill is its first path,
# and the outlines of its faces follow it unfilled.
entries.sort(key=lambda entry: -entry[0])
camUrban and others added 2 commits October 2, 2026 12:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.78698% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.24%. Comparing base (aa23f51) to head (b6e5715).

Files with missing lines Patch % Lines
pterasoftware/_output_rendering.py 80.37% 21 Missing ⚠️
pterasoftware/_vector_export.py 95.54% 21 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #328      +/-   ##
==========================================
- Coverage   96.41%   96.24%   -0.17%     
==========================================
  Files          48       49       +1     
  Lines       10642    11281     +639     
==========================================
+ Hits        10260    10857     +597     
- Misses        382      424      +42     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request maintenance Improvements or additions to documentation, testing, robustness, or tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants