Commit Graph
16 Commits
Author SHA1 Message Date
dotCooCoo 6c84c92907 3D: a beveled cap and its walls draw their shared edge as the same curves
A cap inset by a bevel fitted each run of sampled edges along one curve
as a whole, while the wall next to it fitted each sampled edge by itself.
The two outlines lay up to 0.25 pt apart on an 800 pt circle, which left
a gap or an overlap between the cap and the wall.

An inset outline is now fitted one sampled edge at a time in the cap as
well, so both faces draw the same curves. Caps that are not inset keep
one cubic for each curve of the art.

Test: solid::tests::a_beveled_cap_and_its_walls_share_their_curves.
Docs: docs/three-d.md, docs/target-app-parity.md.
2026-11-10 14:36:28 -08:00
dotCooCoo f259c2372a 3D: expanded solids keep the art's curves in caps and the walls' cap edges
Extrude & Bevel (both kinds) and Rotate wrote every face as straight
segments between sample points, so Expand Appearance and the exports
gave a corner at every sample. Sampling now records the curve each
polygon edge follows, and a facet carries an outline of cubics rebuilt
from it: exact without perspective, within 0.001 pt with it, and within
the sampling tolerance for bevel insets. Caps, Rotate's face and the
wall and bevel edges that meet a cap keep curves; interior bevel edges,
edges a twist cuts and edges past 1,048,576 curves stay straight.
Inflate's faces stay polygons.

Docs: docs/three-d.md, docs/target-app-parity.md.
2026-11-10 13:43:05 -08:00
dotCooCoo a900653c9c 3D: Inflate limits the work of filling its grid
Inflate's grid clipped every ring of the outline in each cell the
outline crosses and tested every edge for each cell's center, so its
work grew with the outline's points times the cells: a comb of 4,000
teeth (16,002 points) took 1.1 to 1.3 s and one of 16,000 teeth 8.8 to
9.1 s. Inflate now counts that work (the edge index's entries, the edges
its queries look at and the ring points its cells clip) and refuses the
shape with "The 3D surface is too complex; simplify the shape" once it
passes 268,435,456, VectorCraft's own value (about 0.65 s). The index
takes at most 16,777,216 entries (64 MiB). Each row's inside test reads
the sorted crossings of its line, and a cell clips only the rings whose
bounds meet it, so a 65,536-point circle inflates in 0.66 s instead of
1.0 to 1.2 s.

The effects test of a stroke past the limits uses 40,000 corners, so its
outline passes the 65,536 points a solid takes.

Docs: docs/three-d.md, docs/mcp.md.
2026-11-10 10:43:24 -08:00
dotCooCoo 6484e72dff 3D: Extrude & Bevel and Inflate build detailed shapes they refused as too complex
Each Extrude & Bevel and Inflate solid now takes up to 65,536 outline
points (was 2,048) and 163,840 faces (was 32,768), VectorCraft's own
limits, as Revolve's. Before any point is sampled, the outline's lines
and curves are counted with each curve's tolerance pieces, and the faces
the walls, bevels and caps (Extrude) or the sides (Inflate) need. When
they do not fit, every curve takes fewer pieces, down to 4; a shape that
does not fit at 4 is refused with the existing surface error before any
face is built. The subpath count is capped at the point limit too. The
error texts are unchanged.

A closed path of 2,000 curves now extrudes with a bevel and inflates,
and paths of 5,000 and 10,000 curves build at the default settings.

Docs: docs/three-d.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-10 10:14:22 -08:00
dotCooCoo a7fc513a0c Merge main into feat/3d-classic-extrude-bevel 2026-11-08 15:34:06 -08:00
dotCooCoo b9d999fe26 Asset Export: the 3D mesh triangle limit bounds memory across objects
The 2,000,000-triangle limit on a 3D object's meshes is now checked as
each mesh is built, so an object past it is refused before the rest of
its meshes are built:

- Revolve counts each fill's and stroke's mesh as it builds it, instead
  of building every one first.
- Inflate still solves every shape's membrane at once (the eye depends
  on the highest puff), but builds each solid only when its mesh is
  reached (inflate_meshes), so a refused object leaves the later
  solids unbuilt.
- All the objects of one 3D asset share one budget
  (three_d_meshes_of), so assets.export3D refuses an asset whose
  objects pass the limit together.
2026-11-09 03:08:23 +07:00
Drew Dibble bc87fd1b7e 3D effects: merged contours keep their parts, and edge cases are consistent
The boolean's answer went through orient(), which judges each ring's
side from its first point. The boolean often starts a contour where it
touches another, so a triangle whose tip rests on a square's edge was
dropped, and an even-odd hole touching its outline was filled. The
boolean already runs holes against their outlines, so its rings are now
only turned as a whole.

Other fixes:
- A bowtie drawn as one contour has no area of its own. It was dropped
  before the meet check and built nothing, although it fills both lobes.
  Sampling now keeps rings with no area, and orient() drops them after
  the check. Flat rings, every point on one line, fill nothing and are
  still dropped, so a stray flat contour doesn't send separate art
  through the boolean.
- An open subpath was closed along its end handles when the profile went
  through the boolean, but straight otherwise, as the renderer fills it.
  It is now closed straight before the boolean too.
- An empty union was taken as a refusal. Contours that cancel (a square
  drawn twice under even-odd) built a square. They now build nothing.
- Contours meet within twice the boolean's own sweep tolerance,
  computed by its rule from every anchor and handle of the path it is
  given. pathops snaps anchors onto nearby heights before the sweep
  joins edges within that tolerance, so contours up to about 1.4 times
  it apart can come out as one; within twice it, every contour it
  might join goes through it, even far from the origin, and the few it
  leaves apart cost a boolean that builds the same shape.
- A one-anchor subpath draws nothing, so it is skipped, as the renderer
  and the boolean skip it.
- A profile the boolean refuses reuses the rings already sampled.

Tests:
- touching parts and holes;
- an open subpath with handles, alone and beside crossing bars;
- a flat contour beside separate squares;
- contours joined far from the origin, including by handles alone,
  and contours a few millionths apart near it kept apart;
- triangles snapped together and joined at 1.4 times the tolerance,
  the same triangles left apart and kept whole, and the factor pinned
  between 1.9 and 2.2 times, with the worst case just past twice the
  tolerance left apart by pathops;
- a 24-bar grid: too complex for Extrude and Inflate, whole under Rotate;
- the merged rings turned as a whole;
- a one-anchor loop;
- the bowtie and the cancelling squares under all three effects;
- a 3,001-point star that the boolean refuses (TooComplex), built as
  drawn.

Docs: the pathops README's Limits section now says only the try_
functions refuse, lists OpenPath and the caught panic, and describes
unite_all's answers, and which calls take a precision. three-d.md and effects-parity.md (the 3D row, its
history and status line) describe the merge and its limits.
2026-11-09 02:24:47 +07:00
Drew Dibble 62a01689f0 3D effects: resolve a profile only when its contours meet
The boolean that merges crossing contours cost about as much as the rest
of a plain extrusion, and it ran on every profile. Most art has no
contours that meet, so the profile is now sampled first (its point limit
checked there, before any boolean) and resolved only when two edges
cross, touch, run along each other or fold back, in one contour or
across two. Edges are swept by their left ends, within a pair budget past
which the boolean decides. Touching counts: squares sharing an edge
inflate as one puff, not two with a seam.

On a release build, Extrude of 120 separate contours takes 0.31 ms
instead of 4.18 ms; profiles that do cross cost what they did.

Tests: the meeting cases, squares sharing an edge, the fallback for a
profile the boolean refuses, Inflate's extent and area for the crossing
k, and the coincident circles' area. The hairline test now holds a
crossing pair so that its profile still goes through the boolean.

Docs: the merge is described once for all three effects, linking to a new
Limits section in the pathops README instead of repeating its limits,
and the point limit notes that a merged outline can have more points.
2026-11-09 02:24:47 +07:00
Drew Dibble e1108210f8 3D effects: keep narrow contours when resolving overlaps
try_normalize refits its result at Pathfinder's default precision and
drops every contour thinner than 0.01 pt on average as a sliver. A
hairline beside a larger shape is art, not debris, and resolved() only
fell back to the input when nothing at all was left, so the hairline lost
its cap, walls and surface.

Resolve with try_unite_all_with at a precision far below any art, which
still drops the debris a boolean leaves where curves coincide: a circle
drawn twice, exactly or 1e-7 pt off, stays one outline.
2026-11-09 02:24:47 +07:00
Drew Dibble 9bd8b69613 3D effects: resolve overlapping contours before building a solid
Extrude & Bevel, Inflate and Rotate broke on shapes whose contours cross.
Type often draws a glyph that way: Roboto Slab's "k" has its arm and leg
as one contour that crosses both the stem and the leg's foot serif.

rings() decided each contour's role (outline, hole, or no edge at all
under nonzero) from the contour's first point only. A contour that starts
inside another was dropped or flipped into a hole, cutting part of the
glyph away. A contour that starts outside was kept whole, so its walls and
bevels ran through the overlap and the overlapping parts drew as separate
boxes.

The profile now goes through vectorcraft_pathops::try_normalize first, so
rings() only ever sees simple contours that do not cross. A path the
boolean refuses is used as it was.
2026-11-09 02:24:47 +07:00
dotCooCoo b568977af4 Asset Export: 3D objects export as USDA, USDZ, glTF or OBJ meshes
Squashed from #6168 onto main.
2026-11-09 00:29:05 +07:00
dotCooCoo 5a4c3e12a5 3D (Classic): Extrude & Bevel (Classic) extrudes art with Wireframe, No Shading, Diffuse and Plastic surfaces
Effect > 3D and Materials > 3D (Classic) > Extrude & Bevel (Classic)... applies
`threeD.classicExtrude`, a live effect built on the Extrude & Bevel solid. It
takes Extrude Depth, Cap, a bevel with Height and Bevel Extent In or Out, the
Position presets, rotation and Perspective, and draws the faces with the
Wireframe, No Shading, Diffuse or Plastic surface: one to ten lights, Ambient
Light, Highlight Intensity and Size, Blend Steps, a Black, Custom or no Shading
Color, and Draw Hidden Faces. Invalid parameters are refused with an error.
2026-11-07 15:02:39 -08:00
Dylan M. Taylor 5cb58c0507 3D effects: every ring pair counts toward the containment budget; no zero-sized inflate cells (#5413)
* fix(three-d): charge every ring pair; refuse zero-sized inflate cells

* fix(three-d): clippy-friendly cell check

* address review: reject subnormal inflate cells
2026-11-03 23:21:52 -05:00
Dylan M. Taylor dec8b90e3a refactor(text): clarify line adjustment inputs 2026-10-27 17:47:12 +00:00
aiongg b1ca05d990 refactor(three-d): move solid.rs items to solid/{build,inflate,membrane,rings}.rs 2026-10-26 21:17:08 +00:00
aiongg 37f2376899 refactor(three-d): move solid.rs test files to solid/ 2026-10-26 18:33:15 +00:00