mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 21:51:36 +00:00
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.
This commit is contained in:
@@ -205,12 +205,12 @@ fn a_stroked_open_path_builds_a_ribbon() {
|
||||
/// still draws.
|
||||
#[test]
|
||||
fn a_stroke_past_the_limits_is_left_out() {
|
||||
// 1,500 corners: within the 2,048 points of the fill's solid, but not the stroke's outline.
|
||||
let points: Vec<Point> = (0..1500)
|
||||
// 40,000 corners: within the 65,536 points of the fill's solid, but not the stroke's outline.
|
||||
let points: Vec<Point> = (0..40_000)
|
||||
.map(|k| {
|
||||
let (s, c) = (k as f64 * std::f64::consts::TAU / 1500.0).sin_cos();
|
||||
let r = if k % 2 == 0 { 100.0 } else { 96.0 };
|
||||
Point::new(200.0 + r * c, 200.0 + r * s)
|
||||
let (s, c) = (k as f64 * std::f64::consts::TAU / 40_000.0).sin_cos();
|
||||
let r = if k % 2 == 0 { 10_000.0 } else { 9_999.0 };
|
||||
Point::new(20_000.0 + r * c, 20_000.0 + r * s)
|
||||
})
|
||||
.collect();
|
||||
let path = PathData::single(vectorcraft_geom::SubPath::polyline(&points, true));
|
||||
|
||||
@@ -23,11 +23,13 @@ use build::{Camera, cuts, edge_distance, newell, section};
|
||||
pub(crate) use build::{Face3, extrude_faces, plane_faces};
|
||||
pub use build::{extrude, plane};
|
||||
#[cfg(test)]
|
||||
use inflate::GRID_WORK;
|
||||
#[cfg(test)]
|
||||
pub(crate) use inflate::INFLATED;
|
||||
pub(crate) use inflate::inflate_faces;
|
||||
pub use inflate::{inflate, inflate_together};
|
||||
#[cfg(test)]
|
||||
use membrane::{Edges, Membrane, cell_triangles};
|
||||
use membrane::{Edges, GRID_BUDGET, MAX_EDGE_ENTRIES, MAX_GRID_WORK, Membrane, cell_triangles};
|
||||
use rings::signed_area;
|
||||
#[cfg(test)]
|
||||
use rings::{MAX_SOLID_POINTS, rings};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::build::{Builder, Built, Camera, Order, Turn, check_bounds, cuts, fit, newell, outward};
|
||||
use super::membrane::{Edges, MAX_MATERIAL_SEGMENTS, Membrane, cell_triangles, clip, filled, material};
|
||||
use super::membrane::{Edges, MAX_MATERIAL_SEGMENTS, Membrane, cell_triangles, clip, crossings, filled, material};
|
||||
use super::rings::{MAX_SOLID_POINTS, TOO_COMPLEX_SURFACE, curve_pieces, signed_area, solid_rings};
|
||||
use super::{Facet, FillRule, Inflate, MAX_FACES, MAX_SOLID_FACES, PathData, Point, Rect, V3, Vec2, View};
|
||||
|
||||
@@ -43,8 +43,11 @@ fn puff(path: &PathData, rule: FillRule, bounds: Rect, o: &Inflate) -> Result<Op
|
||||
}
|
||||
let side = |len: f64| ((len / cell).ceil() as usize).clamp(1, CELLS + 1);
|
||||
let (nx, ny) = (side(bounds.width()), side(bounds.height()));
|
||||
let edges = Edges::new(&rings);
|
||||
let edges = Edges::new(&rings)?;
|
||||
let m = Membrane::solve(&rings, &edges, bounds.origin(), cell, nx + 1, ny + 1);
|
||||
if edges.spent() {
|
||||
return Err(TOO_COMPLEX_SURFACE.into());
|
||||
}
|
||||
// A circle of radius r puffs into a half sphere of radius r at 50 % (its membrane peaks at r²/4).
|
||||
let height = 2.0 * m.peak.sqrt() * o.volume / 50.0;
|
||||
Ok(Some(Puff { rings, edges, cell, nx, ny, m, height }))
|
||||
@@ -87,6 +90,8 @@ pub(crate) fn inflate_faces(
|
||||
thread_local! {
|
||||
/// How many inflated solids this thread has built, for the tests.
|
||||
pub(crate) static INFLATED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// The grid work of the last solid this thread inflated in full, for the tests.
|
||||
pub(super) static GRID_WORK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The facets of one inflated shape, for a camera that allows for a puff `reach` high (or the
|
||||
@@ -151,14 +156,41 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo
|
||||
// builds the topology of every outline ring near its cell, so a self-crossing outline cost its
|
||||
// crossings again in every cell it spans. Past it, cells take their clipped pieces.
|
||||
let material_left = std::cell::Cell::new(MAX_MATERIAL_SEGMENTS);
|
||||
// The shape's part of a rectangle, as rings.
|
||||
// Every ring's bounds, and the crossings of the last few rows the cells' centres lie on (a
|
||||
// row of cells has at most seven, at its three levels).
|
||||
let ring_bounds: Vec<Option<Rect>> =
|
||||
rings.iter().map(|r| Some(r.iter().skip(1).fold(Rect::from_points(*r.first()?, *r.first()?), |b, p| b.union_pt(*p)))).collect();
|
||||
let points = rings.iter().map(Vec::len).fold(0usize, usize::saturating_add);
|
||||
let rows: std::cell::RefCell<Vec<(u64, Vec<f64>)>> = std::cell::RefCell::new(vec![]);
|
||||
let filled_at = |p: Point| -> bool {
|
||||
let mut rows = rows.borrow_mut();
|
||||
if let Some((_, xs)) = rows.iter().find(|(y, _)| *y == p.y.to_bits()) {
|
||||
return filled(xs, p.x);
|
||||
}
|
||||
let xs = if edges.charge(points) { crossings(&rings, p.y) } else { vec![] };
|
||||
let inside = filled(&xs, p.x);
|
||||
if rows.len() >= 8 {
|
||||
rows.remove(0);
|
||||
}
|
||||
rows.push((p.y.to_bits(), xs));
|
||||
inside
|
||||
};
|
||||
// The shape's part of a rectangle, as rings: only the rings whose bounds meet it are clipped,
|
||||
// each point counted as work.
|
||||
let cut = |r: Rect| -> Vec<Vec<Point>> {
|
||||
// A cell no edge crosses is wholly inside the shape or wholly outside it.
|
||||
if !edges.touch(r) {
|
||||
let corners = vec![Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)];
|
||||
return if filled(&rings, r.center()) { vec![corners] } else { vec![] };
|
||||
return if filled_at(r.center()) { vec![corners] } else { vec![] };
|
||||
}
|
||||
let pieces: Vec<Vec<Point>> = rings.iter().map(|ring| clip(ring, r)).filter(|c| !c.is_empty()).collect();
|
||||
let pieces: Vec<Vec<Point>> = rings
|
||||
.iter()
|
||||
.zip(&ring_bounds)
|
||||
.filter(|(_, b)| b.is_some_and(|b| b.x0 <= r.x1 && r.x0 <= b.x1 && b.y0 <= r.y1 && r.y0 <= b.y1))
|
||||
.filter(|(ring, _)| edges.charge(ring.len()))
|
||||
.map(|(ring, _)| clip(ring, r))
|
||||
.filter(|c| !c.is_empty())
|
||||
.collect();
|
||||
if pieces.iter().map(|p| signed_area(p)).sum::<f64>() <= 1e-9 * r.area() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -193,6 +225,9 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if edges.spent() {
|
||||
return Err(TOO_COMPLEX_SURFACE.into());
|
||||
}
|
||||
let level =
|
||||
|i: isize, j: isize| -> u32 { if i < 0 || j < 0 || i as usize >= nx || j as usize >= ny { 0 } else { levels[j as usize * nx + i as usize] } };
|
||||
for j in 0..ny {
|
||||
@@ -209,6 +244,9 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo
|
||||
for sx in (bx0..bx1).step_by(step) {
|
||||
let r = Rect::from_points(at(sx, sy), at(sx + step, sy + step));
|
||||
let pieces = cut(r);
|
||||
if edges.spent() {
|
||||
return Err(TOO_COMPLEX_SURFACE.into());
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -338,5 +376,10 @@ fn inflated(p: Puff, bounds: Rect, o: Inflate, view: View, reach: f64, mesh: boo
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
GRID_WORK.with(|w| w.set(edges.work()));
|
||||
if edges.spent() {
|
||||
return Err(TOO_COMPLEX_SURFACE.into());
|
||||
}
|
||||
Ok(b.finish())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use super::build::segment_distance;
|
||||
use super::rings::{MAX_PLANE_POINTS, contains, left, rings, signed_area};
|
||||
use super::rings::{MAX_PLANE_POINTS, TOO_COMPLEX_SURFACE, left, rings, signed_area};
|
||||
use super::{FillRule, PathData, Point, Rect, Vec2};
|
||||
|
||||
/// Most entries the edge index of one inflated shape holds: one for each bucket an edge's bounds
|
||||
/// cover (64 MiB).
|
||||
pub(super) const MAX_EDGE_ENTRIES: usize = 1 << 24;
|
||||
|
||||
/// Most work the grid of one inflated shape takes: the index's entries, the edges its queries
|
||||
/// look at, the crossings of the rows it fills and the ring points its cells clip, together.
|
||||
pub(super) const MAX_GRID_WORK: usize = 1 << 28;
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// The grid work the next edge indexes of this thread may take, for the tests.
|
||||
pub(super) static GRID_BUDGET: Cell<usize> = const { Cell::new(MAX_GRID_WORK) };
|
||||
}
|
||||
|
||||
/// The grid work a new edge index may take.
|
||||
fn grid_budget() -> usize {
|
||||
#[cfg(test)]
|
||||
return GRID_BUDGET.with(Cell::get);
|
||||
#[cfg(not(test))]
|
||||
MAX_GRID_WORK
|
||||
}
|
||||
|
||||
/// The edges of `rings` filed in a grid of square buckets, so the nearest edge to a point is
|
||||
/// found among the buckets around it rather than among all the edges.
|
||||
/// found among the buckets around it rather than among all the edges. It also counts the work
|
||||
/// done with it (see [`MAX_GRID_WORK`]): once that is spent, its buckets read as empty, and the
|
||||
/// caller refuses the shape.
|
||||
pub(super) struct Edges {
|
||||
edges: Vec<(Point, Point)>,
|
||||
normals: Vec<(Vec2, Vec2)>,
|
||||
@@ -11,10 +37,14 @@ pub(super) struct Edges {
|
||||
size: f64,
|
||||
n: usize,
|
||||
buckets: Vec<Vec<u32>>,
|
||||
work: Cell<usize>,
|
||||
budget: usize,
|
||||
}
|
||||
|
||||
impl Edges {
|
||||
pub(super) fn new(rings: &[Vec<Point>]) -> Self {
|
||||
/// The index of `rings`' edges; an error when it would hold more than [`MAX_EDGE_ENTRIES`]
|
||||
/// entries, counted before any is filed.
|
||||
pub(super) fn new(rings: &[Vec<Point>]) -> Result<Self, String> {
|
||||
const N: usize = 64;
|
||||
let edges: Vec<(Point, Point)> =
|
||||
rings.iter().flat_map(|r| r.iter().zip(r.iter().cycle().skip(1)).take(r.len()).map(|(a, b)| (*a, *b))).collect();
|
||||
@@ -37,16 +67,48 @@ impl Edges {
|
||||
let size = (bounds.width().max(bounds.height()) / N as f64).max(1e-9);
|
||||
let mut buckets = vec![vec![]; N * N];
|
||||
let index = |v: f64, o: f64| (((v - o) / size).floor().max(0.0) as usize).min(N - 1);
|
||||
for (k, (a, b)) in edges.iter().enumerate() {
|
||||
let (i0, i1) = (index(a.x.min(b.x), bounds.x0), index(a.x.max(b.x), bounds.x0));
|
||||
let (j0, j1) = (index(a.y.min(b.y), bounds.y0), index(a.y.max(b.y), bounds.y0));
|
||||
let spans = |(a, b): &(Point, Point)| {
|
||||
((index(a.x.min(b.x), bounds.x0), index(a.x.max(b.x), bounds.x0)), (index(a.y.min(b.y), bounds.y0), index(a.y.max(b.y), bounds.y0)))
|
||||
};
|
||||
let entries = edges.iter().map(spans).fold(0usize, |sum, ((i0, i1), (j0, j1))| sum.saturating_add((i1 - i0 + 1) * (j1 - j0 + 1)));
|
||||
if entries > MAX_EDGE_ENTRIES {
|
||||
return Err(TOO_COMPLEX_SURFACE.into());
|
||||
}
|
||||
for (k, e) in edges.iter().enumerate() {
|
||||
let ((i0, i1), (j0, j1)) = spans(e);
|
||||
for j in j0..=j1 {
|
||||
for i in i0..=i1 {
|
||||
buckets[j * N + i].push(k as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { edges, normals, origin: bounds.origin(), size, n: N, buckets }
|
||||
Ok(Self { edges, normals, origin: bounds.origin(), size, n: N, buckets, work: Cell::new(entries), budget: grid_budget() })
|
||||
}
|
||||
|
||||
/// Has the work done with the index gone past its budget?
|
||||
pub(super) fn spent(&self) -> bool {
|
||||
self.work.get() > self.budget
|
||||
}
|
||||
|
||||
/// The work done with the index so far.
|
||||
#[cfg(test)]
|
||||
pub(super) fn work(&self) -> usize {
|
||||
self.work.get()
|
||||
}
|
||||
|
||||
/// Count `amount` more work; `false` once the budget is spent, when the work is not to be done.
|
||||
pub(super) fn charge(&self, amount: usize) -> bool {
|
||||
if self.spent() {
|
||||
return false;
|
||||
}
|
||||
self.work.set(self.work.get().saturating_add(amount));
|
||||
!self.spent()
|
||||
}
|
||||
|
||||
/// The edges filed in bucket (`i`, `j`), counted as work; none once the budget is spent.
|
||||
fn bucket(&self, i: isize, j: isize) -> &[u32] {
|
||||
let bucket = self.buckets.get((j * self.n as isize + i) as usize).map_or(&[][..], Vec::as_slice);
|
||||
if self.charge(bucket.len()) { bucket } else { &[] }
|
||||
}
|
||||
|
||||
/// The bucket row or column of coordinate `v`, from the grid's origin `o` on that axis.
|
||||
@@ -58,15 +120,11 @@ impl Edges {
|
||||
/// looked at.
|
||||
pub(super) fn near(&self, p: Point, tolerance: f64) -> bool {
|
||||
self.touch(Rect::new(p.x - tolerance, p.y - tolerance, p.x + tolerance, p.y + tolerance)) && {
|
||||
let n = self.n as isize;
|
||||
let (i0, i1) = (self.cell(p.x - tolerance, self.origin.x), self.cell(p.x + tolerance, self.origin.x));
|
||||
let (j0, j1) = (self.cell(p.y - tolerance, self.origin.y), self.cell(p.y + tolerance, self.origin.y));
|
||||
(j0..=j1).any(|j| {
|
||||
(i0..=i1).any(|i| {
|
||||
self.buckets[(j * n + i) as usize]
|
||||
.iter()
|
||||
.filter_map(|k| self.edges.get(*k as usize))
|
||||
.any(|(a, b)| segment_distance(p, *a, *b) <= tolerance)
|
||||
self.bucket(i, j).iter().filter_map(|k| self.edges.get(*k as usize)).any(|(a, b)| segment_distance(p, *a, *b) <= tolerance)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -75,10 +133,9 @@ impl Edges {
|
||||
/// Call `f` with each edge whose bounding box overlaps `r` (an edge in several of the
|
||||
/// buckets `r` covers comes more than once).
|
||||
fn each(&self, r: Rect, mut f: impl FnMut(Point, Point)) {
|
||||
let n = self.n as isize;
|
||||
for j in self.cell(r.y0, self.origin.y)..=self.cell(r.y1, self.origin.y) {
|
||||
for i in self.cell(r.x0, self.origin.x)..=self.cell(r.x1, self.origin.x) {
|
||||
for (a, b) in self.buckets[(j * n + i) as usize].iter().filter_map(|k| self.edges.get(*k as usize)) {
|
||||
for (a, b) in self.bucket(i, j).iter().filter_map(|k| self.edges.get(*k as usize)) {
|
||||
if a.x.min(b.x) <= r.x1 && a.x.max(b.x) >= r.x0 && a.y.min(b.y) <= r.y1 && a.y.max(b.y) >= r.y0 {
|
||||
f(*a, *b);
|
||||
}
|
||||
@@ -89,12 +146,11 @@ impl Edges {
|
||||
|
||||
/// Does any edge pass through `r` (its bounding box, at least, overlapping it)?
|
||||
pub(super) fn touch(&self, r: Rect) -> bool {
|
||||
let n = self.n as isize;
|
||||
let (i0, i1) = (self.cell(r.x0, self.origin.x), self.cell(r.x1, self.origin.x));
|
||||
let (j0, j1) = (self.cell(r.y0, self.origin.y), self.cell(r.y1, self.origin.y));
|
||||
(j0..=j1).any(|j| {
|
||||
(i0..=i1).any(|i| {
|
||||
self.buckets[(j * n + i) as usize]
|
||||
self.bucket(i, j)
|
||||
.iter()
|
||||
.filter_map(|k| self.edges.get(*k as usize))
|
||||
.any(|(a, b)| a.x.min(b.x) <= r.x1 && a.x.max(b.x) >= r.x0 && a.y.min(b.y) <= r.y1 && a.y.max(b.y) >= r.y0)
|
||||
@@ -122,12 +178,15 @@ impl Edges {
|
||||
let mut best = f64::INFINITY;
|
||||
let mut nearest = (p, Vec2::ZERO);
|
||||
for r in 0..n {
|
||||
if self.spent() {
|
||||
break;
|
||||
}
|
||||
for j in (cj - r)..=(cj + r) {
|
||||
for i in (ci - r)..=(ci + r) {
|
||||
if (j - cj).abs() != r && (i - ci).abs() != r || i < 0 || j < 0 || i >= n || j >= n {
|
||||
continue;
|
||||
}
|
||||
for k in &self.buckets[(j * n + i) as usize] {
|
||||
for k in self.bucket(i, j) {
|
||||
if let Some((a, b)) = self.edges.get(*k as usize) {
|
||||
let d = *b - *a;
|
||||
let t = if d.hypot2() < 1e-18 { 0.0 } else { ((p - *a).dot(d) / d.hypot2()).clamp(0.0, 1.0) };
|
||||
@@ -152,9 +211,24 @@ impl Edges {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `p` filled by `rings` (nonzero winding, as they are oriented)?
|
||||
pub(super) fn filled(rings: &[Vec<Point>], p: Point) -> bool {
|
||||
rings.iter().filter(|r| contains(r, p)).count() % 2 == 1
|
||||
/// Where the edges of `rings` cross the line through `y`, in order: the points of the line
|
||||
/// filled by `rings` (nonzero winding, as they are oriented) are those with an odd number of
|
||||
/// crossings to their right ([`filled`]). An edge crosses where one end is above the line and
|
||||
/// the other is not, as [`super::rings::contains`] counts it.
|
||||
pub(super) fn crossings(rings: &[Vec<Point>], y: f64) -> Vec<f64> {
|
||||
let mut xs: Vec<f64> = rings
|
||||
.iter()
|
||||
.flat_map(|r| r.iter().zip(r.iter().cycle().skip(1)).take(r.len()))
|
||||
.filter(|(a, b)| (a.y > y) != (b.y > y))
|
||||
.map(|(a, b)| a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x))
|
||||
.collect();
|
||||
xs.sort_by(f64::total_cmp);
|
||||
xs
|
||||
}
|
||||
|
||||
/// Is `x` filled on the line whose [`crossings`] are `xs`?
|
||||
pub(super) fn filled(xs: &[f64], x: f64) -> bool {
|
||||
(xs.len() - xs.partition_point(|v| *v <= x)) % 2 == 1
|
||||
}
|
||||
|
||||
/// `ring` cut to the rectangle `cell` (Sutherland–Hodgman: the rectangle is convex, the ring
|
||||
@@ -203,13 +277,17 @@ impl Membrane {
|
||||
pub(super) fn solve(rings: &[Vec<Point>], edges: &Edges, origin: Point, step: f64, nx: usize, ny: usize) -> Self {
|
||||
let node = |i: usize, j: usize| Point::new(origin.x + i as f64 * step, origin.y + j as f64 * step);
|
||||
let idx = |i: usize, j: usize| j * nx + i;
|
||||
// A node on the outline itself is held at zero with the outside.
|
||||
let inside: Vec<bool> = (0..nx * ny)
|
||||
.map(|k| {
|
||||
let p = node(k % nx, k / nx);
|
||||
filled(rings, p) && !edges.near(p, 1e-6 * step)
|
||||
})
|
||||
.collect();
|
||||
// A node on the outline itself is held at zero with the outside. Each row of nodes is
|
||||
// tested against the crossings of its line.
|
||||
let points = rings.iter().map(Vec::len).fold(0usize, usize::saturating_add);
|
||||
let mut inside = Vec::with_capacity(nx * ny);
|
||||
for j in 0..ny {
|
||||
let xs = if edges.charge(points) { crossings(rings, node(0, j).y) } else { vec![] };
|
||||
inside.extend((0..nx).map(|i| {
|
||||
let p = node(i, j);
|
||||
filled(&xs, p.x) && !edges.near(p, 1e-6 * step)
|
||||
}));
|
||||
}
|
||||
// The nearest crossing of the outline from `p` along (dx, dy), within one step.
|
||||
let crossing = |p: Point, dx: f64, dy: f64| -> f64 {
|
||||
let mut best = step;
|
||||
|
||||
@@ -324,11 +324,11 @@ fn the_inflate_membrane_matches_known_solutions() {
|
||||
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 50.0), 0.01));
|
||||
let (p, b) = shape(&c);
|
||||
let r = rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap();
|
||||
let disc = Membrane::solve(&r, &Edges::new(&r), b.origin(), b.width() / 48.0, 49, 49);
|
||||
let disc = Membrane::solve(&r, &Edges::new(&r).unwrap(), b.origin(), b.width() / 48.0, 49, 49);
|
||||
assert!(close(disc.peak, 50.0 * 50.0 / 4.0, 625.0 * 0.02), "{}", disc.peak);
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let r = rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap();
|
||||
let sq = Membrane::solve(&r, &Edges::new(&r), b.origin(), 100.0 / 48.0, 49, 49);
|
||||
let sq = Membrane::solve(&r, &Edges::new(&r).unwrap(), b.origin(), 100.0 / 48.0, 49, 49);
|
||||
assert!(close(sq.peak, 0.07367 * 100.0 * 100.0, 736.7 * 0.02), "{}", sq.peak);
|
||||
// Smooth across the middle: no crease where a distance-to-the-edge height would have one
|
||||
// (the diagonals). The second difference along a diagonal stays small and even.
|
||||
@@ -426,7 +426,7 @@ fn twisted_pieces_stay_within_the_facet_limit() {
|
||||
fn the_edge_index_finds_the_nearest_edge() {
|
||||
let (p, r) = bumps(200);
|
||||
let rings = rings(&p, FillRule::EvenOdd, r.width(), MAX_SOLID_POINTS).unwrap();
|
||||
let edges = Edges::new(&rings);
|
||||
let edges = Edges::new(&rings).unwrap();
|
||||
for k in 0..400 {
|
||||
let t = f64::from(k);
|
||||
let q = Point::new(-300.0 + (t * 37.0) % 1400.0, -50.0 + (t * 13.0) % 130.0);
|
||||
@@ -442,7 +442,7 @@ fn the_edge_index_looks_along_the_side_from_outside() {
|
||||
let ring = |b: BezPath| rings(&shape(&b).0, FillRule::EvenOdd, 64.0, MAX_SOLID_POINTS).unwrap().remove(0);
|
||||
// Straight ahead of the point, a little further up its side, and the far corner (a 64 pt grid).
|
||||
let rings = vec![ring(square(2.5, 0.0, 1.0)), ring(square(0.0, 4.2, 0.8)), ring(square(63.0, 63.0, 1.0))];
|
||||
let edges = Edges::new(&rings);
|
||||
let edges = Edges::new(&rings).unwrap();
|
||||
let q = Point::new(-10.0, 0.5);
|
||||
let (fast, slow) = (edges.distance(q), edge_distance(&rings, q));
|
||||
assert!((fast - slow).abs() < 1e-9, "{fast} against {slow}");
|
||||
|
||||
@@ -139,3 +139,59 @@ fn many_empty_contours_and_invalid_handles_are_refused_before_sampling() {
|
||||
assert_eq!(rings::curve_pieces(&bad, 10.0, 1, 2), Err("The shape contains invalid coordinates".to_string()));
|
||||
assert_eq!(inflate(&bad, FillRule::NonZero, b, Inflate::default(), turned()), Err("The shape contains invalid coordinates".to_string()));
|
||||
}
|
||||
|
||||
/// A comb of `teeth` teeth, 100 wide and 100 tall, on a strip along its foot: its outline
|
||||
/// crosses every cell of the grid Inflate fills.
|
||||
fn comb(teeth: usize) -> PathData {
|
||||
let w = 100.0 / teeth as f64;
|
||||
let mut points: Vec<Point> = (0..teeth)
|
||||
.flat_map(|k| {
|
||||
let x = k as f64 * w;
|
||||
[Point::new(x, 0.0), Point::new(x, 100.0), Point::new(x + w / 2.0, 100.0), Point::new(x + w / 2.0, 0.0)]
|
||||
})
|
||||
.collect();
|
||||
points.extend([Point::new(100.0, -1.0), Point::new(0.0, -1.0)]);
|
||||
PathData::new(vec![SubPath::polyline(&points, true)])
|
||||
}
|
||||
|
||||
/// Inflate `path` with the grid work its edge index may take set to `budget`.
|
||||
fn inflated_within(path: &PathData, budget: usize) -> Result<Vec<Facet>, String> {
|
||||
GRID_BUDGET.with(|b| b.set(budget));
|
||||
let out = inflate(path, FillRule::NonZero, path.bounds().unwrap(), Inflate::default(), turned());
|
||||
GRID_BUDGET.with(|b| b.set(MAX_GRID_WORK));
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inflate_at_its_grid_work_budget_builds_and_one_past_it_is_refused() {
|
||||
let path = comb(200);
|
||||
assert!(built(&inflated_within(&path, MAX_GRID_WORK).unwrap()));
|
||||
let work = GRID_WORK.with(std::cell::Cell::get);
|
||||
assert!(work > 0);
|
||||
assert!(built(&inflated_within(&path, work).unwrap()));
|
||||
assert_eq!(GRID_WORK.with(std::cell::Cell::get), work, "the same work at its budget");
|
||||
assert_eq!(inflated_within(&path, work - 1), Err(SURFACE.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_edge_index_at_its_entry_limit_is_built_and_one_past_it_is_refused() {
|
||||
// A diagonal across the whole grid of 64 by 64 buckets is filed in every bucket, each way.
|
||||
let diagonal = vec![Point::new(0.0, 0.0), Point::new(64.0, 64.0)];
|
||||
assert_eq!(MAX_EDGE_ENTRIES, 2048 * 2 * 64 * 64);
|
||||
let mut rings = vec![diagonal; 2048];
|
||||
assert!(Edges::new(&rings).is_ok_and(|e| e.work() == MAX_EDGE_ENTRIES));
|
||||
// A small triangle in one bucket: three entries more.
|
||||
rings.push(vec![Point::new(0.0, 0.0), Point::new(0.5, 0.0), Point::new(0.0, 0.5)]);
|
||||
assert_eq!(Edges::new(&rings).err(), Some(SURFACE.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_comb_that_crosses_every_cell_is_refused_once_its_grid_work_is_spent() {
|
||||
// Two thousand curves take a small share of the budget.
|
||||
let curves = petals(2000, 500.0, 4.0);
|
||||
assert!(built(&inflate(&curves, FillRule::NonZero, curves.bounds().unwrap(), Inflate::default(), turned()).unwrap()));
|
||||
assert!(GRID_WORK.with(std::cell::Cell::get) < MAX_GRID_WORK / 16);
|
||||
// 16,002 points, each clipped in every cell its ring meets.
|
||||
let path = comb(4000);
|
||||
assert_eq!(inflate(&path, FillRule::NonZero, path.bounds().unwrap(), Inflate::default(), turned()), Err(SURFACE.to_string()));
|
||||
}
|
||||
|
||||
+3
-1
@@ -2581,7 +2581,9 @@ the scope and what differs from the public documentation.
|
||||
`threeD.extrude` and `threeD.inflate` take up to 65,536 outline points and build up to 163,840 faces for each solid
|
||||
(VectorCraft's own limits); with more faces than that, each curve takes fewer pieces, down to 4. A shape past the
|
||||
points returns "The shape is too complex for a 3D effect", and one whose faces do not fit at 4 pieces a curve returns
|
||||
"The 3D surface is too complex; simplify the shape". `threeD.rotate` takes up to 65,536 points.
|
||||
"The 3D surface is too complex; simplify the shape". `threeD.inflate` also returns the surface error when its outline
|
||||
has so many points across so many of its grid's cells that filling the grid would pass its work limit. `threeD.rotate`
|
||||
takes up to 65,536 points.
|
||||
|
||||
## Extrude & Bevel (Classic)
|
||||
|
||||
|
||||
@@ -245,6 +245,10 @@ out, perspective). No Adobe software, assets, presets or bevel shapes were used.
|
||||
builds them, against the same 163,840. The point limit is counted on the shape as drawn and, when its contours
|
||||
meet, again on the merged outline, which can have more points (bars crossing in a grid trace every hole between
|
||||
them); past it, the shape is too complex for the effect.
|
||||
- Inflate also limits the work of filling its grid, which grows with the outline's points times the grid cells its
|
||||
outline crosses. An outline of tens of thousands of points that crosses nearly every cell (a comb of 4,000 teeth
|
||||
across the shape) is refused because the 3D surface is too complex; a closed path of 65,536 points round a
|
||||
circle builds.
|
||||
- **3D (Classic)**: Extrude & Bevel (Classic) is built, with its surface types and blend steps. Revolve (Classic),
|
||||
Rotate (Classic), the track cube, map art, Turntable and 3D export are separate, unbuilt items.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user