Skip to content

Save diagrams as svg and pdf files - #328

Closed
camUrban wants to merge 30 commits into
mainfrom
diagram_vector_output
Closed

camUrban wants to merge 30 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 cut wherever they pass through a fill or swap depth order with another stroke they overlap, and painted among the fills in depth order, each before the fills that hide it and after everything else it overlaps, with the dots painted among them the same way, 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.

The export also keeps its files small: runs of fills and strokes that share a style are written as single compound paths, segments shared by neighboring polylines are drawn once, and an svg's path coordinates are rounded to hundredths of a point. Some programs that import an svg ignore its embedded fonts and draw its text in other fonts, which misplaces the parts of math labels, so the diagram methods also gain selectable_text. Setting it to False draws an svg's labels as glyph outlines, which look the same in every program but can't be selected or searched.

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.
  • Resolved convex occluders in the fills' BSP. VectorLayer._get_fills_and_strokes turns each occluder's faces that face the camera into fill triangles, and its silhouette and outlined faces into strokes owned by those triangles, so occluders are ordered exactly against the other fills and against each other, however they interpenetrate. Occluders must now be closed, since their faces that face away are dropped.
  • Painted the fills and strokes in one depth order (_order_items):
    • _cut_at_pierces cuts each stroke where its centerline passes through a fill it doesn't outline.
    • _get_overlap_gaps compares each stroke piece with each fill it overlaps on screen, averaged over the region where they overlap, so the piece is painted before the fills it is behind, which cover the part of its width they hide, and after the rest. Pieces of different strokes are ordered by their depths where they come closest (_get_stroke_constraints), and overlapping fills keep the BSP's order.
    • Pieces caught in a cycle of constraints are split for up to _SPLIT_ROUNDS rounds, and _break_cycles breaks any cycle that survives, within each strongly connected component, by dropping the lightest constraint of each cycle in turn (_get_greedy_drops).
    • _cut_at_depth_swaps also cuts two strokes that overlap on screen wherever they swap which of them is nearer, except strokes that share an end, so strokes lying along each other on screen are each painted over the other only where they are nearer. Pairs of pieces are compared by their depth difference averaged over the stretch where they overlap (_get_average_gaps), rather than where they come closest.
  • Painted the dots among the fills and strokes in depth order, rather than over everything in a pass of their own. Each dot is ordered as a stroke of zero length as wide as the dot and drawn as a filled circle, so a dot hidden in the window behind a panel, an arrow tip, or a nearer line is hidden in the file too. Each layer now draws in two passes.
  • Removed the seams between abutting fills. _merge_abutting_fills paints abutting fills of one color together, as one compound path, wherever that keeps the order possible, and _get_underlaps reaches each fill under the abutting fills painted after it, with a strip of its color along their shared edge. Fills with almost no area on screen, such as faces seen edge on, are dropped.
  • Kept the ordering fast enough for large diagrams: every overlap test is narrowed by a grid on screen (_get_overlapping_boxes), the overlap-region test and the grid lookup are compiled with Numba, the fills' constraints are found once and each piece's are cached until it is split, and each search for a cycle skips the items an earlier search finished.
  • Reduced the size of the exported files:
    • VectorLayer.draw writes each run of consecutive opaque fills of the same color, and of consecutive opaque strokes of the same color and width, as one compound path in a PathCollection, rather than one svg element per piece. Each fill piece is wound counterclockwise so the nonzero fill rule never cancels overlapping pieces, translucent strokes stay separate, and Matplotlib's path simplification is turned off for the strokes so no segment is dropped.
    • VectorLayer.add_polylines skips a segment that repeats one it already added in the same color and width, such as an edge shared by neighboring panels or ring vortices.
    • VectorScene.save rounds the coordinates in an svg's path data to _SVG_DECIMAL_PLACES (2) decimal places with _round_svg_path_data, since Matplotlib always writes six.
  • 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", in any case. Each label is drawn in its VTK background box, with math in STIX and other text in Liberation Mono. VectorScene.save also checks the suffix without regard to case.
  • 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, at least 1 pixel on each side, 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 selectable_text (default True) to the same methods, passed through show_diagram to VectorScene.save. When it is False, an svg is written with svg.fonttype set to "path" and without embedded fonts, so every label, math included, is drawn as glyph outlines. It has no effect on pdf or WebP output.
  • Added "fontTools" to autodoc_mock_imports in docs/website/conf.py, so the website build doesn't need it installed.
  • 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), the painting of strokes behind, in front of, and through fills and occluders, the depth order of crossing and coincident strokes, the cut where two strokes swap depth order, the painting of dots in depth order, the culling of occluders' faces that face away, line width scaling, the drawing of shared polyline edges once, and svg and pdf writing, including an svg whose text is drawn as outlines. 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
Comment thread pterasoftware/_vector_export.py Outdated
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 90.14085% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.26%. Comparing base (0a9c36f) to head (e6544f2).

Files with missing lines Patch % Lines
pterasoftware/_output_rendering.py 80.37% 21 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #328      +/-   ##
==========================================
- Coverage   96.41%   95.26%   -1.15%     
==========================================
  Files          48       49       +1     
  Lines       10642    12040    +1398     
==========================================
+ Hits        10260    11470    +1210     
- Misses        382      570     +188     

☔ 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.

camUrban and others added 14 commits October 4, 2026 16:21
Write each run of consecutive opaque fills or strokes that share a
color and width as one compound path instead of one path per piece.
Matplotlib gives every collection item its own SVG element with a full
inline style, which made the files for large diagrams many times bigger
than they need to be. Wind every fill piece counterclockwise so that
the nonzero fill rule never cancels overlapping pieces, keep
translucent strokes separate so their overlaps still compound, and
disable Matplotlib's path simplification so that no segment is dropped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Neighboring closed polylines, such as the outlines of adjacent panels
and ring vortices, share their edges, so many of a diagram's strokes
were exact repeats that inflated the vector exports and added needless
work to hidden-line clipping. Skip a segment only when its color and
width also match, so a coincident stroke in a different style still
paints over the earlier one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Matplotlib writes every path coordinate to six decimal places, which
makes up most of a large diagram's svg. Round them to hundredths of a
point, far finer than any display or print can show, to shrink the
files without changing what they show.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Some programs that import an svg ignore its embedded fonts and draw its
text in other fonts, which misplaces the parts of any math. Let a
diagram's svg draw its labels as glyph outlines instead, which look the
same everywhere at the cost of selectable and searchable text. Keep
selectable text as the default, and leave pdfs alone, since their
embedded TrueType fonts are read reliably.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Order convex occluders exactly against the layer's fills and against
each other, instead of painting them whole by the mean depth of their
points. Fix the misordering of occluders that interpenetrate, overlap
cyclically, or overlap in depth, and stop fills and occluders in one
layer from painting over each other regardless of depth. Convert each
occluder at draw time into fill triangles and outline strokes owned by
those triangles, so the existing BSP and hidden-line removal handle
them, and the outlines are hidden by whatever is nearer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Order the visible pieces of strokes by depth instead of clipping each
stroke against the strokes in front of it. Stop the clipping from
opening gaps where strokes cross, and stop the round ends of clipped
strokes from showing over the strokes in front of them. Pull a stroke's
line back by its radius where a fill's boundary cuts it, so its round
end stops at the boundary rather than reaching over the fill. Split
pieces caught in cycles of depth constraints, and drop the weakest
constraint in any cycle that survives.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Clipping each stroke against the fills along its centerline hides or
shows its whole width at once, so a stroke near a fill's edge or partly
behind it is drawn wrong. Instead, cut each stroke only where it passes
through a fill, and paint each piece before the fills it is behind on
average over the region where they overlap on screen, so those fills
cover the part of its width they hide, and after the rest.

Sort the fills and the pieces together, keeping the BSP's order between
overlapping fills and the depth order between overlapping pieces, and
narrow every pairwise overlap test with a grid on screen. Draw them in
one collection, which leaves each layer three passes, and remove the
stroke clipping that no longer has a caller.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the weakest remaining constraint one at a time while the sort
is blocked drops constraints that aren't in any cycle, and rescans
every constraint for each drop, which made exporting a small unsteady
solver diagram take tens of seconds. Instead, split the constraints
into strongly connected components and, for each, drop the set of
constraints with the smallest total weight whose removal breaks every
cycle, found exactly as an integer program whose cycles are added as
they are found.

Raise an error rather than fall back to a greedy choice if the solver
fails, so a failed solve is never silently replaced by a worse order.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Comparing each piece's bounding box with every later piece's grows
quadratically, and finding where each overlapping pair comes closest
one call at a time spends nearly all its time on numpy overhead for
scalars, which made exporting a diagram with thousands of strokes take
seconds. Instead, find the candidate pairs on the grid that the other
overlap tests use, and find where every pair comes closest at once,
with arrays.

The arithmetic differs from the old version's in the last bits, which
can flip which of two equally close points a pair is compared at, but
every test scene still renders identically.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The test runs for every pair of a stroke piece and a fill that overlap
on screen, and clipped the fill against each of the many edges of the
piece's footprint, one numpy scalar operation at a time, which made
exporting even a small unsteady solver diagram take minutes. Instead,
clip the footprint against the fill's few edges, which gives the same
region, and do the geometry in plain floats, which avoids numpy's
overhead on arrays of a few corners.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every split round compared every pair of overlapping fills and every
stroke piece with every fill it overlaps, although the fills never
change and most pieces aren't split, so much of the export time went to
repeating work. Instead, find the fills' constraints with each other
once, and keep each piece's constraints with the fills until it is
split. Keep the constraints in the same order as before, so the exact
cycle breaking sees the same input.

Also test whether two fills overlap in plain floats, since numpy's
overhead on arrays of a few corners far outweighed the arithmetic.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Breaking the cycles gathered each strongly connected component's items
and constraints by scanning all of them once per component, so its cost
grew with the number of components times the number of constraints,
which took seconds on a small unsteady solver diagram. Instead, group
them by component in one pass over each, keeping their order within
each component so the exact solve sees the same input.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A closed convex polyhedron's faces that face the camera cover its whole
footprint on screen, in front of the rest, so its other faces are
always hidden, yet they were split, ordered, compared with every stroke
they overlap, and written to the file. Instead, turn only the faces
that face the camera into fills, which cuts the time to export a small
unsteady solver diagram by about a fifth and its svg by about a
quarter, and require the polyhedron to be closed.

The faces dropped had sat beneath the joins between the faces kept, in
the same color, so faint seams now show at those joins until abutting
fills of one color are painted together.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
camUrban and others added 4 commits October 8, 2026 00:52
Exporting a mid-size unsteady solver diagram took nearly a minute, much
of it in Python loops over tiny arrays: the overlap-region depth test,
called once per pair of a stroke piece and a fill, and the grid lookup
that finds which boxes overlap. Compile both with Numba instead, the
depth test as one call over every pair in a round, and the grid as
sorted cell keys rather than a dict of lists. Let the depth test use
fastmath, since a last-bit difference there only moves a stroke's place
in the paint order among nearly equal choices.

Also skip the integer program when the cycles first found in a
component share no droppable constraint, since dropping each one's
cheapest is then already exact.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The docs build mocks numpy, so computing the stroke caps' angles from
np.pi when the export module loads raised a TypeError, which broke
importing pterasoftware for autodoc and failed the build on its
warnings. Take pi from math instead, which gives identical angles.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Paint abutting fills of the same color as one compound path wherever no
chain of constraints through another item runs between them, so no
anti-aliased seam shows along their shared edges. This removes most of
the seams that culling the occluders' back faces introduced.

Drop fills with almost no area on screen, such as faces seen edge on.
They can't hide anything, so they carry no constraints, and their
outlines painted stray lines of their color wherever merging moved them
in the order.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Paint a strip of each fill's color along every edge it shares with a
fill painted in a later run, so the later fill's anti-aliased edge
blends with the earlier fill rather than with whatever is behind both.
This removes nearly all of the seams that merging leaves between fills
of different runs, without changing what is visible.

Cover both sides of each shared edge, so the strip has no anti-aliased
edge of its own there, and keep it inset from the later fill's other
edges, where it would otherwise show past them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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.

🟡 Changes recommended

Vector output has unresolved depth-ordering mismatches and an invalid zero-sized viewport edge case.

3 open findings
4 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread pterasoftware/_vector_export.py Outdated
Comment thread pterasoftware/_output_rendering.py
Comment thread pterasoftware/_vector_export.py Outdated
camUrban and others added 3 commits October 8, 2026 01:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Two strokes lying along each other on screen were ordered by one depth
comparison where they come closest, so one was painted over the other
along their whole overlap, though each is nearer along part of it. Cut
both strokes where their depth order swaps within the overlap, except
strokes that share an end, and compare each pair of pieces by its depth
difference averaged over the stretch where they overlap, since the cut
pieces meet at equal depths where they come closest.

Also build the cycle constraints for the integer program as a sparse
matrix, and let each search for a cycle skip the items an earlier
search in the same round finished, which leaves the cycles found
unchanged but avoids repeating the work the extra pieces would multiply.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The dots were painted in a pass of their own over everything else, so a
dot hidden in the window behind a panel, an arrow tip, or a nearer line
showed on top in the exported file. Order each dot among the fills and
strokes as a stroke of zero length as wide as the dot, whose footprint
is the circle it paints, and draw it as a filled circle, since
Matplotlib's own renderer draws nothing for a zero-length stroke.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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.

🟡 Changes recommended

Fixed-interval sampling can miss narrow stroke depth swaps, and the public vector-export path lacks integration coverage.

2 open findings
3 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +1408 to +1410
for sample_id in range(num_samples):
fraction = _SWAP_SAMPLE_FRACTIONS[sample_id]
x = starts_display[first_id, 0] + fraction * (
Comment on lines +2369 to +2371
# If saving an svg or pdf file, read the state its export needs at the end of every
# render, keeping only the latest.
is_vector = save and path.suffix.lower() in (".svg", ".pdf")
@camUrban

camUrban commented Oct 8, 2026

Copy link
Copy Markdown
Owner Author

Closing this without merging. After working through it, the feature doesn't justify what it would add to the codebase.

Producing true vector output means resolving visibility in software, since VTK's own vector export rasterizes 3D geometry. Getting that close to what the window shows took a BSP painter for the fills, a depth ordering of every stroke and dot among them, cuts where strokes pass through fills or swap depth with each other, and cycle breaking for orderings that can't be satisfied. That brought the change to roughly 6,000 lines, most of it intricate geometry that would need ongoing maintenance.

Even then, it remains a tradeoff between accuracy, robustness, and speed rather than a solved problem. Each accuracy fix uncovered new edge cases, and the more exact methods made exports of large unsteady solver diagrams slow enough to be impractical. That's a hard balance to keep stable given this project's focus is the solvers, not the rendering, so the raster output will stay the supported diagram format for now.

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