Commit Graph
468 Commits
Author SHA1 Message Date
BFlat 9107dad274 Merge pull request #7194 from dotCooCoo/fix/item-3829
Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
2026-11-14 03:23:28 +07:00
BFlat cf6c666d25 Merge pull request #6980 from addibble/seam-round-joins
3D effects: every facet's seam has round joins
2026-11-14 03:21:49 +07:00
Drew Dibble 09d5522785 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/effects-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-13 12:29:58 -07:00
dotCooCoo 4d4d7ee1a7 Envelopes: opaque pixels of a warped image stay opaque where its pieces overlap
In SVG and PDF output, each piece of a warped image whose pieces can show
through was drawn through a mask of every later piece that reaches it. On a
later piece's antialiased edge the viewer multiplies the earlier piece's
coverage by the mask and draws the later piece over it, so an image with
both opaque and partly transparent pixels came out lighter at its opaque
pixels along the triangles' edges (alpha 184 of 255 in the new SVG test).

A later piece now goes into an earlier piece's mask only when a pixel of
either piece's image within 2 pixels of their overlap is less than opaque
(vectorcraft_effects::WarpSeams). Where two pieces overlap over opaque
pixels, the later one is drawn over the earlier one without a mask.
2026-11-13 10:36:16 -08:00
dotCooCoo b54f357ced Merge main into fix/raster-effects-skipped-transform-budget 2026-11-13 10:06:29 -08:00
BFlat 4f58b83c75 Merge pull request #7118 from dotCooCoo/fix/painted-effect-every-item
Effects: a later painted effect runs on every fill and stroke a piece returns
2026-11-14 00:49:38 +07:00
BFlat 3904a7396c Merge pull request #7120 from dotCooCoo/feat/revolve-graphics
3D and Materials: wrap graphics around the side of Revolve objects
2026-11-14 00:49:21 +07:00
Drew Dibble 0654aef75b Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	crates/effects/src/solids.rs
#	docs/target-app-parity.md
2026-11-13 10:18:23 -07:00
dotCooCoo 25a99d7c07 Effects: raster effects before a Transform effect past the budget stay with the art
A Transform effect whose result would pass the geometry budget (2 million
anchors or 8 million lines drawn) is left out and the art stays where it
was, but the raster effects listed before it still scaled and moved with
it: before a Transform effect that scales the art 1000 times, a Drop
Shadow's offset grew 1000 times and its blur went to the 1000 pt limit
around art that had not moved, and Expand kept those values.

The geometry pass now notes each Transform effect that moves the art once
and that it leaves out, in the Carry it already returns, and
Carry::hide_skipped hides those effects in the list the raster effects
are read from. The canvas (a path's own effects and its fills' and
strokes'), its bounds, bake (SVG and PDF export), shape art, Expand,
Expand Appearance and Outline Stroke use it, and a fill's Transform
effect inside a piece no longer scales the effects before it when the
budget leaves it out. The budget itself is unchanged.

A path drawn as its own art inside a group whose geometry effects run
after its members' own read its object effects whole. The reshaping pass
now notes a Transform effect that no path in the art took past the
budget, and the member's art hides it before its raster effects are read.

The bounds skip the extra geometry pass when no raster effect could
follow a Transform effect, and painted_reach runs it once for both
measurements.
2026-11-13 09:16:26 -08:00
dotCooCoo 0663c76be1 Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
An envelope or the Warp effect cuts an image into triangle pieces that
reach a little past their edges, so neighbors overlap and no seams show.
On an image with alpha, the overlap drew that alpha twice: a 50% image
came out about 75% opaque along every cell edge and diagonal.

- The pieces now form a knockout group, and each piece is marked as a
  warp piece (`warp_piece`) whose knockout shape is its whole triangle,
  so where pieces overlap only the top one draws.
- The canvas knocks each piece out by its triangle at full coverage.
- SVG and PDF draw the pieces of a partly transparent image (its pixels,
  opacity, color mask or colorize paint) through one mask each, of the
  later triangles that reach it; pixels on a triangle's antialiased edge
  come out slightly lighter there. Pieces of an opaque image are drawn
  one over another as before.
- SVG writes one copy of the image for pieces cut by opacity masks
  (Preserve Shape Using Transparency) too.
- A cell count past the generated-object budget makes an empty group
  instead of overflowing.

Docs: docs/target-app-parity.md.
2026-11-13 08:38:48 -08:00
BFlat d3bb35ff61 Merge pull request #7154 from dotCooCoo/fix/3d-walls-depth-sort
3D: fill walls and dashed or profiled strokes draw in depth order where they overlap
2026-11-13 23:31:09 +07:00
dotCooCoo 4b695aafb1 Merge main into fix/painted-effect-every-item 2026-11-13 08:12:58 -08:00
Drew Dibble 26af1c7aa4 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/target-app-parity.md
2026-11-13 09:09:32 -07:00
dotCooCoo 2361d2cb1b 3D: caps stay last when overlapping faces form a depth cycle
- When the faces that overlap form a cycle of depth orders, the order
  broke the cycle only after drawing every face with nothing to wait
  for, so a cap could be drawn before the walls and they painted over
  it. A face drawn last now keeps its place: no face moves across it.
- A face with a corner at or past the eye, which the perspective
  projection holds just in front of the eye, no longer gets a plane,
  so its depth is not measured on a plane its drawing does not match.
- docs/three-d.md states the masked member rule in Groups as the
  Fills and strokes section does, and docs/target-app-parity.md cites
  the Inflate test by its name.
2026-11-13 07:23:56 -08:00
dotCooCoo a0e0f09d79 3D and Materials: bound Revolve graphics' work and paint copies, and close the gap at the back
- Each contour of a graphic is cut only at the faces its own bounds reach,
  and the points cut at the faces count toward a limit of 64,000,000.
- The paint copies of a graphic's parts count toward a budget of their own,
  one copy for a part's fill and one for its seam stroke when it takes one.
- A part filled with a pattern, or whose gradient has a transparent stop or
  point, takes no seam stroke.
- Under several lights an opaque solid part also covers its seams, with its
  lit art clipped to a band of the seam stroke's width.
- A line of a graphic that reaches farther than any surface is refused, as a
  curve already was.
- A full turn places each segment by its own middle, so the cut at the back
  falls on the segment edge nearest half a turn from the middle.
- A group's members are measured for graphics only when it has some.
- docs/three-d.md: Revolve shows no Face control, and the new limits.
2026-11-13 07:06:38 -08:00
dotCooCoo 57d37a6dae 3D: fill walls and gapped strokes draw in depth order where they overlap
Extrude & Bevel, Extrude & Bevel (Classic) and Inflate sorted the fill
walls and the faces of a stroke that leaves gaps (dashed, profiled,
brushed) by one depth each, so a far stroke face could draw over a near
fill wall and the reverse. Two such strokes on one shape drew over each
other the same way, and Inflate drew a plain stroke as if it covered
the fill's edge, which an inflated fill can rise in front of.

- Each flat facet now carries its plane, so its depth can be measured
  at any point under it, with orthographic and perspective views.
- Where faces of one shape's fill and strokes overlap in the wrong
  order, the overlapping faces are ordered by their depths where they
  overlap. A fill wall and its covering stroke keep their order.
- A face of a stroke that leaves gaps that passes through a fill wall,
  a covering stroke's face or an earlier such stroke's face is cut
  along the line where their depths meet, keeping its curved outline,
  within the face limit and the paint allowance.
- Under Inflate, no stroke counts as covering the fill's edge.
- The faces go in one order when the fill and strokes are at full
  opacity with Normal blending. A member of a 3D group with an opacity
  mask or a blending mode of its own orders its faces inside it when
  its own paints qualify. A gradient with transparent stops keeps the
  one order with graphics on its cap, and so does Inflate in gradients
  under a colored light. A solid color under a colored light, drawn as
  one composite per surface, still draws each surface whole.
2026-11-13 06:45:13 -08:00
Drew Dibble 00c0e7a7fd Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/target-app-parity.md
2026-11-13 07:25:46 -07:00
dotCooCoo 1517c81262 3D and Materials: wrap graphics around the side of Revolve objects
The Materials tab and threeD.mapGraphic now map graphics onto
Revolve's swept side, unrolled flat: x runs around the axis (the
angle times the profile's distance from the axis at that point) and
y along the profile. A graphic keeps its size at every distance from
the axis, so it reaches farther around where the profile comes nearer
the axis. Each face of the side draws the part that falls on it: a
graphic narrows toward the silhouette, follows the profile along its
length, is shaded with the surface (several lights included) and is
hidden where the side faces away. A part that runs past the end of
the profile reaches the curved rim and stops there. The middle of the
side faces the viewer with the axis on either edge. On a compound
profile or a group the graphics go on the longest contour.

- three-d: with Revolve::map_surface set, side faces carry a
  SurfaceCell (corners on the page and on the unrolled side, whether
  the outside faces the viewer) and a closed profile off the axis
  takes an anchor where its side is cut; without it the faces are as
  before. revolve_surface_size gives the side's size for a new
  graphic's fit.
- effects: Revolve takes `graphics` (front only); surface_parts cuts
  and maps them per face; curves are cut into straight pieces one at
  a time, and commands refuse graphics past the point limits;
  switching between Extrude, Plane and Revolve keeps the front's
  graphics.
- The Materials tab hides Face for Revolve; params text, docs/mcp.md,
  docs/three-d.md and the progress docs describe the change.
- i18n: the complete-message catalogs translate the three new
  refusals in place of the planar-faces refusal they replace, and ja,
  es, zh-hans and zh-hant translate the Materials tab's label.
2026-11-13 05:06:03 -08:00
dotCooCoo 39f66f3870 Effects: a later painted effect runs on every fill and stroke a piece returns
When a painted live effect on a fill or stroke returned several fills or
strokes, the next effect in that stack ran only on the first of them and
the others took its geometry. A later live-effect plug-in, and a later
geometry effect on strokes, now runs once on each of them in paint order,
given the piece's fills and strokes and its own `_context.item`. Each
run's pieces replace only that fill or stroke; a run that fails, is
refused, is deferred or would pass the effects' budget leaves only its own
fill or stroke as it was. Fills and strokes that keep the same path stay
on one path, and a deferred run is listed at the source fill's or
stroke's place.

The results that are kept share the effects' budget, and each run's
result is kept or left out whole. They are first placed with all the
room the art leaves, and placed again with room kept for the piece's path
when a fill or stroke left on that path finds no room. A result on the
path before it costs nothing and is taken whatever room is left; a
built-in effect that finds no room is built again with all the room the
art leaves, and that result is taken only when it is on the path before
it. A plug-in result is held only while it is placed: one that does not
fit, or that moves onto the path before it, is let go at once, so the
results held at one time stay within the budget however many fills and
strokes the piece has. A result kept in the first placing is reused in
the second without running the plug-in again. A painted piece's share of
the budget is counted once at each later effect.
2026-11-13 04:44:11 -08:00
dotCooCoo e8c3dace32 Effects: a group's geometry effects reshape what its members draw
A group's or layer's geometry effects now run after its members' own:
the geometry effects of each visible path and compound path in it and
of their fills and strokes, and a nested group's geometry and
Pathfinder effects, are applied first, at any depth, and the group's
effects reshape the result. A symbol instance's geometry effects run
after those of its symbol's members in the same way. Before, the group
reshaped the members' outlines and their own effects ran afterwards:
a 100 pt square with its own 10 pt Offset Path in a group with a 50%
Transform drew 70 pt wide, and it now draws 60 pt wide. Existing
documents with such groups draw differently.

The canvas, export (including groups that keep their type), Expand
Appearance and the art that Pathfinder operations and compound shapes
take from such a group use the same order. The reply of
effect.expandAppearance names such a group and no longer names the
members whose effects the group's art already holds.

The renderer now drops the art it keeps for a group when a plug-in is
installed or removed and a member of the group has a plug-in effect.

Docs: docs/target-app-parity.md. Not yet: the own geometry effects of
images, symbol instances, paths with a linked opacity mask or a 3D
effect, and nested groups with a 3D effect in a group still run after
the group's.
2026-11-13 04:41:58 -08:00
dotCooCoo c30e553f74 3D: build each solid as its mesh is exported
Mesh export checked the object's solids together by building all of them
before the first mesh. It now builds and counts each solid as the loop
reaches it, so a mesh past the triangle budget stops the export before
the solids after it are built.
2026-11-12 23:03:32 -08:00
dotCooCoo 304fdfeb35 Merge main into fix/3d-face-budget-per-object
The i18n test that main added for 3D refusals calls validate_three_d_at with this branch's (Option<usize>, usize) position: effect 0 of appearance item 0 is (Some(0), 0).
2026-11-12 22:07:00 -08:00
dotCooCoo af7c5d0432 3D: check an object's solids after the geometry effects before the effect
Validation built the Extrude & Bevel, Inflate and Rotate solids from the
stored shape, while drawing, Export 3D Object and Expand as Wireframes
build them after the geometry effects before the 3D effect. A Transform
effect with copies before Extrude & Bevel could pass the check and then
draw the source art. Validation now reshapes the object, the fill or
stroke, or a group's members with those effects first.

validate_three_d_at and validate_three_d_at_in take the effect's index
for an effect on the object too, so an edit checks the effects before
that effect, and effect.apply checks a new effect after the others.
2026-11-12 21:47:53 -08:00
BFlat bae885e393 Merge pull request #6876 from dotCooCoo/feat/outline-object-image-alpha
Effects: Outline Object on an image's fill or stroke follows its opaque pixels
2026-11-13 12:27:50 +07:00
Drew Dibble d44e9e8301 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/target-app-parity.md
2026-11-12 22:00:28 -07:00
dotCooCoo 145ac9d30a 3D: an object's fill and stroke solids share one face limit
Extrude & Bevel (both kinds) and Inflate built every fill and stroke
solid of an object, each up to 163,840 faces, before any check, so one
effect on a path with many strokes or a group with many members could
build several times that. The solids are now built in order and their
faces counted together: once they pass 163,840 (VectorCraft's own
limit), no later solid is built, the object draws its source art, and
effect.apply, effect.setParams, Export 3D Object and Expand as
Wireframes report "The 3D surface is too complex; simplify the shape".
A stroke left out for its own limits is not counted.

Export 3D Object now builds Extrude & Bevel (Classic) meshes with the
Classic bevel options, so the exported solid matches the drawn one.
2026-11-12 20:40:25 -08:00
BFlat 62aee53c50 Merge pull request #7025 from dotCooCoo/fix/followup-6236
Tests: hidden layer children and edited freeform gradients under the Warp effects
2026-11-13 11:27:02 +07:00
BFlat be389b8b59 Merge pull request #7030 from dotCooCoo/fix/followup-6960
3D: mapped graphics keep paint order and hide handles on hidden fills
2026-11-13 11:26:46 +07:00
dotCooCoo ac76040014 Merge main into feat/outline-object-image-alpha 2026-11-12 20:16:33 -08:00
BFlat e899f3fdb4 Merge pull request #6881 from dotCooCoo/feat/image-color-mask
Image Tools: Color Mask hides chosen colors of an image without changing its pixels
2026-11-13 10:27:04 +07:00
Drew Dibble d73c92b74f Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/target-app-parity.md
2026-11-12 20:04:01 -07:00
dotCooCoo 3e9e58afb1 3D: mapped graphics keep paint order and hide handles on hidden fills
A solid now shares its mapped graphics only with the run of solids just
before it that have the same opacity and blending mode. Before, it
joined any earlier solid with the same settings, so with members A, B
and C where A and C match, A's graphic was drawn after C and over B.

The Materials tab shows no artboard handles for graphics on a hidden
fill or stroke, which draws no solid and no graphics.

docs/three-d.md now says that pattern tiles in a graphic keep their own
colors instead of taking the face's light.
2026-11-12 18:59:02 -08:00
dotCooCoo aa113a9ac2 Tests: hidden layer children and edited freeform gradients under the Warp effects
A Warp on a layer whose hidden circle keeps its shape past the budget
moves the layer's placed gradient from the layer's bounds to the warped
square's bounds, as the art and inside the art. Edited freeform points
on a path, on a group inside the art and on the art itself move from the
bounds before the warp to the bounds after it.
2026-11-12 18:44:25 -08:00
BFlat 23cae99a84 Merge pull request #7011 from dotCooCoo/fix/expand-symbol-item-effects
Effects: a symbol instance's fill or stroke effects reshape its art and expand
2026-11-13 09:30:26 +07:00
BFlat 9c007ec416 Merge pull request #7009 from dotCooCoo/fix/3d-clip-image-outside-shape
3D: an image inside a clip's box but outside its shape is left out
2026-11-13 09:29:50 +07:00
Drew Dibble 81675ae7de Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/target-app-parity.md
2026-11-12 19:22:23 -07:00
dotCooCoo df155042d4 Merge main into feat/image-color-mask 2026-11-12 17:40:13 -08:00
BFlat ae014cc896 Merge pull request #7002 from dotCooCoo/fix/3d-plain-fill-keeps-compound
3D: a fill or stroke without the effect keeps an even-odd path's holes
2026-11-13 08:29:49 +07:00
dotCooCoo e51685dc23 Effects: a symbol instance's fill or stroke effects reshape its art and expand
A geometry effect on a symbol instance's topmost fill now reshapes the
filled shapes of the symbol's art, and one on its topmost stroke reshapes
the stroked shapes, keeping their colors, strokes and other detail. Only
a visible topmost fill or stroke with an opacity above 0 counts. Clipping
paths, guides and opacity-mask art keep their shapes; a group's own fills
and strokes in the art take the effects; a nested instance passes them on
to its own art, except one whose symbol holds the outer symbol.

The canvas, Use Preview Bounds and the drawn bounds, SVG and PDF export
and Object > Expand Appearance use the same art. Expand Appearance is
enabled for such an instance and turns it into an editable group in one
undo step. Bounds measure the art with a dynamic symbol's overrides, as
the canvas draws it.

Existing documents with a geometry effect on a symbol instance's fill or
stroke draw differently: before, the effect drew nothing.
2026-11-12 17:15:04 -08:00
dotCooCoo c5f406ffab 3D: an image inside a clip's box but outside its shape is left out
A 3D effect left an image in a clipping group out only when the image's
box missed the clipping path's box. An image in the corner of a round
clip's box, a turned image whose box reaches a clip but whose frame does
not, an image in an even-odd hole, or one inside each of two nested clips
but outside the area they show together was refused with the Image Trace
message, although it draws nothing.

The image's frame, the pixel rectangle as its transform places it, is now
cut to every clipping path around it in turn, each with its own fill rule,
and the image is left out when nothing is left. An image the clips show
any part of, a strip narrower than a hundredth of a point included, is
still refused. One effect's art cuts image frames at most 1,024 times;
past that, or when a cut fails, the art is refused.
2026-11-12 17:07:13 -08:00
BFlat 23825d2943 Merge pull request #6993 from dotCooCoo/fix/revolve-plugin-item-context
Effects: a fill's 3D Revolve tells the plug-in effect before it which fill it sits on
2026-11-13 07:48:14 +07:00
dotCooCoo 9b65bf2708 3D: a fill or stroke without the effect keeps an even-odd path's holes
With a 3D effect on one fill or stroke, the object's other fills and
strokes were drawn as nonzero paths, so an even-odd compound path or
path filled its holes in them. Extrude & Bevel, Inflate, Rotate,
Revolve and their Classic kinds now draw those items with the source's
fill rule, through the group code's shape_node helper.
2026-11-12 16:28:54 -08:00
Drew Dibble d7d77dd0a5 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/effects-parity.md
#	docs/target-app-parity.md
2026-11-12 17:27:14 -07:00
dotCooCoo 3570efa0a9 Effects: a fill's 3D Revolve tells the plug-in effect before it which fill it sits on
The per-item Revolve loop passed a copy of each fill or stroke to the
geometry context, which finds the item by identity in the object's
appearance, so a plug-in effect before the Revolve was told it sat on
the object. Each copy now keeps its original item beside it.
2026-11-12 15:48:59 -08:00
Drew Dibble 3aede6aca2 Merge remote-tracking branch 'upstream/main' into seam-round-joins
# Conflicts:
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-12 16:30:37 -07:00
Drew Dibble 1032fb3979 Round seams: Classic Rotate and Revolve tests, Wireframe edge, docs
- a_faces_seam_stays_within_half_its_width_of_the_face checks Rotate
  (Classic)'s branches too: No Shading keeps its paint, a gradient keeps
  the gradient, and Diffuse shades its plane darker at some view.
- classic_revolves_seams_are_round checks each seam's reach as well as
  its join, and covers a gradient.
- The Wireframe edge takes seam_stroke (the same stroke it built by hand).
- Docs: which seams went from miter to round and which already were;
  the white-light difference from the 2026-10-17 comparison is expected,
  not measured; the 3D area rows record the change; the performance note
  says its measurement predates the Classic Rotate and Revolve paths.
2026-11-12 16:28:51 -07:00
dotCooCoo d5a02ee872 Merge main into feat/3d-graphics-planar 2026-11-12 15:19:09 -08:00
BFlat 1461bb2123 Merge pull request #6963 from dotCooCoo/fix/transform-effect-moves-member-raster-effects
Effects: a group's or object's Transform effect moves the raster effects inside it
2026-11-13 06:11:24 +07:00
dotCooCoo 505d11dae5 3D and Materials: straight graphic sides count once, and graphic handles stay with the drawn art
- Under a perspective, the shared 400,000-element limit of a 3D effect's graphics now counts
  what each path draws once mapped: a straight side stays one element, and only curves count
  as their 8 pieces. Two copies of a 30,000-sided polygon now both draw. The symbol check
  counts the same way, and without a perspective it counts each curve once.
- The Materials tab's handles on the artboard are hidden while a geometry effect (such as
  Transform) comes after the 3D effect of a selected object, since that effect draws the
  graphics away from the handles.
- A press on the canvas while its context menu is open only closes the menu, and no longer
  grabs a graphic's handle.
- A symbol instance inside a graphic's art shows its changes to a dynamic symbol, and a symbol
  with 9-slice guides is placed through them, as the canvas draws the instance. Paths that
  cross a guide are cut at it.
2026-11-12 15:02:34 -08:00
Drew Dibble 05fd55bde1 3D effects: every facet's seam has round joins
Each 3D facet is stroked with a narrow seam of its own paint (0.05 to
0.75 pt; Revolve's is always 0.75 pt) so neighboring facets meet without
an antialiasing hairline. In Extrude & Bevel, Inflate and Rotate under a
white light, and in every face of the 3D (Classic) effects (Extrude &
Bevel, Rotate and Revolve (Classic); shaded, No Shading, gradient and
pattern alike), that seam kept the default Miter Join with its limit
of 10. Revolve's seams and the colored-light seams
were already round. A facet seen almost edge-on is a sharp sliver, and
its mitered seam reached up to ten times its half-width past the facet's
tip: thorns of up to 3.75 pt past the solid, along the tops of type seen
tilted and at every edge-on bevel facet.

#2706 (multiple lights) deliberately kept white-light seam joins as they
were, so that legacy white-light output stayed byte-identical to main
(its comment "Preserve existing white-light paints, alpha composition and
seam joins exactly", and four PNGs compared byte for byte). That kept
the thorns too. This change reverses that one part on purpose: the
paints and alpha composition stay exactly as #2706 left them; only the
join changes.

- Outputs change, the document model doesn't. Exports draw the new
  joins (SVG writes stroke-linejoin="round", PDF and .ai draw round
  joins), and Expand Appearance writes round seams into the document.
  But nothing a document stores selects the join, and the format is
  unchanged, so there is no version gate: an old file drawn now simply
  has no thorns.
- Round joins. Projected facets rarely have right-angle corners (a
  square seen at an angle is a parallelogram with acute corners, and an
  exact 90° corner sits at a √2 limit), and round is what the seams of
  colored lights and Revolve already draw. The cost: where a facet has
  no neighbor (Rotate's plane, every silhouette corner) its convex
  corners are drawn rounded by up to 0.375 pt instead of sharp.
  Alternatives weighed and not taken: a Miter Join with a limit near √2
  at every seam, the composed clip included, which would bevel slivers
  but chamfer most projected corners as well; and seaming only the edges
  facets share, or clipping facets to their union as solid_surface
  does, which would keep every outer corner sharp but means a new
  geometry pass for every 3D effect. Round is the smallest change that
  matches the seams already drawn.
- A seam no longer reaches past its facet by more than half its width,
  so the art's visual bounds shrink by up to 3.375 pt where a thorn
  stood (10 × 0.375 less the 0.375 a round seam still reaches).
  White-light output is no longer byte-identical to #2706's comparison;
  docs/3d-lighting-performance.md says so.

Every seam stroke is made by lighting::seam_stroke. The colored clip in
composed_art already outlined its seam with round joins and is
unchanged. Tests: every seam of Extrude & Bevel, Extrude & Bevel (Classic, each
branch), Rotate (Classic), Inflate and Rotate, under white and colored
light, has round joins and reaches no more than half its width past its
face, at views that make a sliver; Revolve (Classic)'s seams are round
on each surface; a mitered seam fails them, and so does the legacy
white-light Inflate test, which now expects Round.
2026-11-12 15:47:18 -07:00