Merge pull request #1481 from AyberkA0/fix/3d-curve-sampling
3D and Materials: curves follow a tolerance, and Rotate takes large outlines
This commit is contained in:
commit
214312f883
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
use vectorcraft_geom::{BezPath, ParamCurve, PathData, Point, Rect};
|
use vectorcraft_geom::{BezPath, ParamCurve, PathData, Point, Rect};
|
||||||
|
|
||||||
use crate::{MAX_FACES, MAX_PROFILE_POINTS, Revolve, V3};
|
use crate::{MAX_FACES, Revolve, V3};
|
||||||
|
|
||||||
/// The view shared by every 3D effect: rotation, perspective and one directional light.
|
/// The view shared by every 3D effect: rotation, perspective and one directional light.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
|
@ -253,14 +253,32 @@ impl Inflate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Outline points Extrude & Bevel and Inflate sample at most: every point costs walls, bevel
|
||||||
|
/// bands and distance tests.
|
||||||
|
const MAX_SOLID_POINTS: usize = 2048;
|
||||||
|
/// Outline points Rotate samples at most: it only turns the outline, so an outlined paragraph fits.
|
||||||
|
const MAX_PLANE_POINTS: usize = 65536;
|
||||||
|
|
||||||
|
/// How many straight pieces follow `c` within `tolerance` (Wang's bound on a cubic's distance from
|
||||||
|
/// its chords), 1 to 32.
|
||||||
|
fn pieces(c: vectorcraft_geom::CubicBez, tolerance: f64) -> u32 {
|
||||||
|
let d = |a: Point, b: Point, c: Point| (a.to_vec2() - 2.0 * b.to_vec2() + c.to_vec2()).hypot();
|
||||||
|
let bend = d(c.p0, c.p1, c.p2).max(d(c.p1, c.p2, c.p3));
|
||||||
|
let n = (0.75 * bend / tolerance).sqrt().ceil();
|
||||||
|
if n.is_finite() { (n as u32).clamp(1, 32) } else { 32 }
|
||||||
|
}
|
||||||
|
|
||||||
/// The rings of a shape: closed polygons with the filled side on their left, (-dy, dx): outer
|
/// The rings of a shape: closed polygons with the filled side on their left, (-dy, dx): outer
|
||||||
/// outlines run one way and holes the other, so nonzero winding fills them as the art is.
|
/// outlines run one way and holes the other, so nonzero winding fills them as the art is. Curves
|
||||||
fn rings(path: &PathData) -> Result<Vec<Vec<Point>>, String> {
|
/// are followed within a tolerance of 1/2000 of `size` (at least 0.05 pt), so small curves take
|
||||||
|
/// few points and large ones more; more than `limit` points in all is refused.
|
||||||
|
fn rings(path: &PathData, size: f64, limit: usize) -> Result<Vec<Vec<Point>>, String> {
|
||||||
|
let tolerance = (size / 2000.0).max(0.05);
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
for sp in &path.subpaths {
|
for sp in &path.subpaths {
|
||||||
let segments = if sp.closed { sp.anchors.len() } else { sp.anchors.len().saturating_sub(1) };
|
let segments = if sp.closed { sp.anchors.len() } else { sp.anchors.len().saturating_sub(1) };
|
||||||
if segments > MAX_PROFILE_POINTS {
|
if segments > limit {
|
||||||
return Err("The shape is too complex for a 3D effect".into());
|
return Err("The shape is too complex for a 3D effect".into());
|
||||||
}
|
}
|
||||||
let mut ring: Vec<Point> = vec![];
|
let mut ring: Vec<Point> = vec![];
|
||||||
|
|
@ -269,14 +287,14 @@ fn rings(path: &PathData) -> Result<Vec<Vec<Point>>, String> {
|
||||||
}
|
}
|
||||||
for i in 0..segments {
|
for i in 0..segments {
|
||||||
let curve = sp.segment(i);
|
let curve = sp.segment(i);
|
||||||
let samples: u32 = if sp.segment_is_line(i) { 1 } else { 16 };
|
let samples = if sp.segment_is_line(i) { 1 } else { pieces(curve, tolerance) };
|
||||||
|
count += samples as usize;
|
||||||
|
if count > limit {
|
||||||
|
return Err("The shape is too complex for a 3D effect".into());
|
||||||
|
}
|
||||||
for k in 1..=samples {
|
for k in 1..=samples {
|
||||||
ring.push(curve.eval(f64::from(k) / f64::from(samples)));
|
ring.push(curve.eval(f64::from(k) / f64::from(samples)));
|
||||||
}
|
}
|
||||||
count += samples as usize;
|
|
||||||
if count > MAX_PROFILE_POINTS {
|
|
||||||
return Err("The shape is too complex for a 3D effect".into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ring.iter().any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs() > 1e8 || p.y.abs() > 1e8) {
|
if ring.iter().any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs() > 1e8 || p.y.abs() > 1e8) {
|
||||||
return Err("The shape contains invalid coordinates".into());
|
return Err("The shape contains invalid coordinates".into());
|
||||||
|
|
@ -585,11 +603,11 @@ fn steps(o: &Extrude) -> Vec<Step> {
|
||||||
pub fn extrude(path: &PathData, bounds: Rect, options: Extrude, view: View) -> Result<Vec<Facet>, String> {
|
pub fn extrude(path: &PathData, bounds: Rect, options: Extrude, view: View) -> Result<Vec<Facet>, String> {
|
||||||
check_bounds(bounds)?;
|
check_bounds(bounds)?;
|
||||||
let o = options.sanitized();
|
let o = options.sanitized();
|
||||||
let rings = rings(path)?;
|
|
||||||
// No depth: the flat shape, seen from either side.
|
// No depth: the flat shape, seen from either side.
|
||||||
if o.depth <= 0.0 {
|
if o.depth <= 0.0 {
|
||||||
return plane(path, bounds, view);
|
return plane(path, bounds, view);
|
||||||
}
|
}
|
||||||
|
let rings = rings(path, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||||
let camera = Camera::new(view, bounds, o.depth);
|
let camera = Camera::new(view, bounds, o.depth);
|
||||||
let mut b = Builder { camera: &camera, closed: o.cap, facets: vec![] };
|
let mut b = Builder { camera: &camera, closed: o.cap, facets: vec![] };
|
||||||
let steps = steps(&o);
|
let steps = steps(&o);
|
||||||
|
|
@ -625,7 +643,7 @@ pub fn extrude(path: &PathData, bounds: Rect, options: Extrude, view: View) -> R
|
||||||
/// shape's own, so that every part of one object turns about the same point).
|
/// shape's own, so that every part of one object turns about the same point).
|
||||||
pub fn plane(path: &PathData, bounds: Rect, view: View) -> Result<Vec<Facet>, String> {
|
pub fn plane(path: &PathData, bounds: Rect, view: View) -> Result<Vec<Facet>, String> {
|
||||||
check_bounds(bounds)?;
|
check_bounds(bounds)?;
|
||||||
let rings = rings(path)?;
|
let rings = rings(path, bounds.width().max(bounds.height()), MAX_PLANE_POINTS)?;
|
||||||
let camera = Camera::new(view, bounds, 0.0);
|
let camera = Camera::new(view, bounds, 0.0);
|
||||||
let mut b = Builder { camera: &camera, closed: false, facets: vec![] };
|
let mut b = Builder { camera: &camera, closed: false, facets: vec![] };
|
||||||
let contours: Vec<Vec<V3>> = rings.iter().map(|r| r.iter().map(|p| camera.place(*p, 0.0)).collect()).collect();
|
let contours: Vec<Vec<V3>> = rings.iter().map(|r| r.iter().map(|p| camera.place(*p, 0.0)).collect()).collect();
|
||||||
|
|
@ -633,18 +651,142 @@ pub fn plane(path: &PathData, bounds: Rect, view: View) -> Result<Vec<Facet>, St
|
||||||
Ok(b.finish())
|
Ok(b.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The distance from `p` to the nearest edge of `rings`.
|
/// The distance from `p` to the segment `a`–`b`.
|
||||||
|
fn segment_distance(p: Point, a: Point, b: Point) -> f64 {
|
||||||
|
let (dx, dy) = (b.x - a.x, b.y - a.y);
|
||||||
|
let l2 = dx * dx + dy * dy;
|
||||||
|
let t = if l2 < 1e-18 { 0.0 } else { (((p.x - a.x) * dx + (p.y - a.y) * dy) / l2).clamp(0.0, 1.0) };
|
||||||
|
p.distance(Point::new(a.x + dx * t, a.y + dy * t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The distance from `p` to the nearest edge of `rings`, edge by edge.
|
||||||
|
#[cfg(test)]
|
||||||
fn edge_distance(rings: &[Vec<Point>], p: Point) -> f64 {
|
fn edge_distance(rings: &[Vec<Point>], p: Point) -> f64 {
|
||||||
let mut best = f64::INFINITY;
|
rings
|
||||||
for r in rings {
|
.iter()
|
||||||
for (a, b) in r.iter().zip(r.iter().cycle().skip(1)).take(r.len()) {
|
.flat_map(|r| r.iter().zip(r.iter().cycle().skip(1)).take(r.len()))
|
||||||
let (dx, dy) = (b.x - a.x, b.y - a.y);
|
.map(|(a, b)| segment_distance(p, *a, *b))
|
||||||
let l2 = dx * dx + dy * dy;
|
.fold(f64::INFINITY, f64::min)
|
||||||
let t = if l2 < 1e-18 { 0.0 } else { (((p.x - a.x) * dx + (p.y - a.y) * dy) / l2).clamp(0.0, 1.0) };
|
}
|
||||||
best = best.min(p.distance(Point::new(a.x + dx * t, a.y + dy * t)));
|
|
||||||
|
/// 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.
|
||||||
|
struct Edges {
|
||||||
|
edges: Vec<(Point, Point)>,
|
||||||
|
origin: Point,
|
||||||
|
size: f64,
|
||||||
|
n: usize,
|
||||||
|
buckets: Vec<Vec<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edges {
|
||||||
|
fn new(rings: &[Vec<Point>]) -> Self {
|
||||||
|
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();
|
||||||
|
let bounds = edges.iter().fold(None, |b: Option<Rect>, (a, c)| Some(b.map_or(Rect::from_points(*a, *c), |b| b.union_pt(*a).union_pt(*c))));
|
||||||
|
let bounds = bounds.unwrap_or(Rect::ZERO);
|
||||||
|
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));
|
||||||
|
for j in j0..=j1 {
|
||||||
|
for i in i0..=i1 {
|
||||||
|
buckets[j * N + i].push(k as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { edges, origin: bounds.origin(), size, n: N, buckets }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bucket row or column of coordinate `v`, from the grid's origin `o` on that axis.
|
||||||
|
fn cell(&self, v: f64, o: f64) -> isize {
|
||||||
|
(((v - o) / self.size).floor() as isize).clamp(0, self.n as isize - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is `p` within `tolerance` of an edge? Only the buckets `p` could share an edge with are
|
||||||
|
/// looked at.
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
best
|
|
||||||
|
/// 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)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does any edge pass through `r` (its bounding box, at least, overlapping it)?
|
||||||
|
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]
|
||||||
|
.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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The distance from `p` to the nearest edge: the buckets are searched in growing squares
|
||||||
|
/// around `p`'s, until no unsearched bucket could hold a nearer edge.
|
||||||
|
fn distance(&self, p: Point) -> f64 {
|
||||||
|
let (fx, fy) = ((p.x - self.origin.x) / self.size, (p.y - self.origin.y) / self.size);
|
||||||
|
if !fx.is_finite() || !fy.is_finite() {
|
||||||
|
return f64::INFINITY;
|
||||||
|
}
|
||||||
|
let n = self.n as isize;
|
||||||
|
let (ci, cj) = ((fx.floor() as isize).clamp(0, n - 1), (fy.floor() as isize).clamp(0, n - 1));
|
||||||
|
// How far `p` is outside the grid: the nearest point of the grid to it lies in `p`'s bucket.
|
||||||
|
let outside = (fx.clamp(0.0, n as f64) - fx).hypot(fy.clamp(0.0, n as f64) - fy) * self.size;
|
||||||
|
let mut best = f64::INFINITY;
|
||||||
|
for r in 0..n {
|
||||||
|
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] {
|
||||||
|
if let Some((a, b)) = self.edges.get(*k as usize) {
|
||||||
|
best = best.min(segment_distance(p, *a, *b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Buckets r + 1 away are at least r buckets from any point of `p`'s own, and that
|
||||||
|
// far across the grid from a `p` outside it (at right angles to its way in, at worst).
|
||||||
|
if best <= outside.hypot(r as f64 * self.size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is `p` filled by `rings` (nonzero winding, as they are oriented)?
|
/// Is `p` filled by `rings` (nonzero winding, as they are oriented)?
|
||||||
|
|
@ -695,36 +837,36 @@ struct Membrane {
|
||||||
|
|
||||||
impl Membrane {
|
impl Membrane {
|
||||||
fn solve(rings: &[Vec<Point>], origin: Point, step: f64, nx: usize, ny: usize) -> Self {
|
fn solve(rings: &[Vec<Point>], origin: Point, step: f64, nx: usize, ny: usize) -> Self {
|
||||||
|
let edges = Edges::new(rings);
|
||||||
let node = |i: usize, j: usize| Point::new(origin.x + i as f64 * step, origin.y + j as f64 * step);
|
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;
|
let idx = |i: usize, j: usize| j * nx + i;
|
||||||
// A node on the outline itself is held at zero with the outside.
|
// A node on the outline itself is held at zero with the outside.
|
||||||
let inside: Vec<bool> = (0..nx * ny)
|
let inside: Vec<bool> = (0..nx * ny)
|
||||||
.map(|k| {
|
.map(|k| {
|
||||||
let p = node(k % nx, k / nx);
|
let p = node(k % nx, k / nx);
|
||||||
filled(rings, p) && edge_distance(rings, p) > 1e-6 * step
|
filled(rings, p) && !edges.near(p, 1e-6 * step)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// The nearest crossing of the outline from `p` along (dx, dy), within one 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 crossing = |p: Point, dx: f64, dy: f64| -> f64 {
|
||||||
let mut best = step;
|
let mut best = step;
|
||||||
for r in rings {
|
let reach = Rect::from_points(p, Point::new(p.x + dx * step, p.y + dy * step));
|
||||||
for (a, b) in r.iter().zip(r.iter().cycle().skip(1)).take(r.len()) {
|
edges.each(reach, |a, b| {
|
||||||
let t = if dy == 0.0 {
|
let t = if dy == 0.0 {
|
||||||
if (a.y > p.y) == (b.y > p.y) {
|
if (a.y > p.y) == (b.y > p.y) {
|
||||||
continue;
|
return;
|
||||||
}
|
|
||||||
(a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) - p.x) * dx
|
|
||||||
} else {
|
|
||||||
if (a.x > p.x) == (b.x > p.x) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
(a.y + (p.x - a.x) / (b.x - a.x) * (b.y - a.y) - p.y) * dy
|
|
||||||
};
|
|
||||||
if t > 1e-9 * step && t < best {
|
|
||||||
best = t;
|
|
||||||
}
|
}
|
||||||
|
(a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) - p.x) * dx
|
||||||
|
} else {
|
||||||
|
if (a.x > p.x) == (b.x > p.x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(a.y + (p.x - a.x) / (b.x - a.x) * (b.y - a.y) - p.y) * dy
|
||||||
|
};
|
||||||
|
if t > 1e-9 * step && t < best {
|
||||||
|
best = t;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
best
|
best
|
||||||
};
|
};
|
||||||
// Each inside node's four neighbours: (weight, neighbour inside or None at the outline).
|
// Each inside node's four neighbours: (weight, neighbour inside or None at the outline).
|
||||||
|
|
@ -781,14 +923,14 @@ impl Membrane {
|
||||||
if inside[k] {
|
if inside[k] {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d_out = edge_distance(rings, node(i, j));
|
let d_out = edges.distance(node(i, j));
|
||||||
let mut slopes = vec![];
|
let mut slopes = vec![];
|
||||||
for (di, dj) in [(1isize, 0isize), (-1, 0), (0, 1), (0, -1)] {
|
for (di, dj) in [(1isize, 0isize), (-1, 0), (0, 1), (0, -1)] {
|
||||||
let (ni, nj) = (i as isize + di, j as isize + dj);
|
let (ni, nj) = (i as isize + di, j as isize + dj);
|
||||||
if ni < 0 || nj < 0 || ni as usize >= nx || nj as usize >= ny || !inside[idx(ni as usize, nj as usize)] {
|
if ni < 0 || nj < 0 || ni as usize >= nx || nj as usize >= ny || !inside[idx(ni as usize, nj as usize)] {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d_in = edge_distance(rings, node(ni as usize, nj as usize)).max(1e-9);
|
let d_in = edges.distance(node(ni as usize, nj as usize)).max(1e-9);
|
||||||
slopes.push(phi[idx(ni as usize, nj as usize)] / d_in);
|
slopes.push(phi[idx(ni as usize, nj as usize)] / d_in);
|
||||||
}
|
}
|
||||||
if !slopes.is_empty() {
|
if !slopes.is_empty() {
|
||||||
|
|
@ -825,14 +967,14 @@ fn material(outline: &PathData, cell: Rect) -> Option<Vec<Vec<Point>>> {
|
||||||
let part =
|
let part =
|
||||||
vectorcraft_pathops::try_boolean(&r, rule, outline, rule, vectorcraft_pathops::BoolOp::Intersect, vectorcraft_pathops::DEFAULT_PRECISION)
|
vectorcraft_pathops::try_boolean(&r, rule, outline, rule, vectorcraft_pathops::BoolOp::Intersect, vectorcraft_pathops::DEFAULT_PRECISION)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
rings(&part).ok()
|
rings(&part, cell.width(), MAX_PLANE_POINTS).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inflate: the shape puffed out like a cushion, its edge kept where the art's outline is.
|
/// Inflate: the shape puffed out like a cushion, its edge kept where the art's outline is.
|
||||||
pub fn inflate(path: &PathData, bounds: Rect, options: Inflate, view: View) -> Result<Vec<Facet>, String> {
|
pub fn inflate(path: &PathData, bounds: Rect, options: Inflate, view: View) -> Result<Vec<Facet>, String> {
|
||||||
check_bounds(bounds)?;
|
check_bounds(bounds)?;
|
||||||
let o = options.sanitized();
|
let o = options.sanitized();
|
||||||
let rings = rings(path)?;
|
let rings = rings(path, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||||
if rings.is_empty() {
|
if rings.is_empty() {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
}
|
}
|
||||||
|
|
@ -844,11 +986,12 @@ pub fn inflate(path: &PathData, bounds: Rect, options: Inflate, view: View) -> R
|
||||||
let cell = size / CELLS as f64;
|
let cell = size / CELLS as f64;
|
||||||
let (nx, ny) = (((bounds.width() / cell).ceil() as usize).max(1), ((bounds.height() / cell).ceil() as usize).max(1));
|
let (nx, ny) = (((bounds.width() / cell).ceil() as usize).max(1), ((bounds.height() / cell).ceil() as usize).max(1));
|
||||||
let m = Membrane::solve(&rings, bounds.origin(), cell, nx + 1, ny + 1);
|
let m = Membrane::solve(&rings, bounds.origin(), cell, nx + 1, ny + 1);
|
||||||
|
let edges = Edges::new(&rings);
|
||||||
let outline = PathData::new(rings.iter().map(|r| vectorcraft_geom::SubPath::polyline(r, true)).collect());
|
let outline = PathData::new(rings.iter().map(|r| vectorcraft_geom::SubPath::polyline(r, true)).collect());
|
||||||
// A circle of radius r puffs into a half sphere of radius r at 50 % (its membrane peaks at r²/4).
|
// 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;
|
let height = 2.0 * m.peak.sqrt() * o.volume / 50.0;
|
||||||
let lift = |p: Point| {
|
let lift = |p: Point| {
|
||||||
if edge_distance(&rings, p) < 1e-9 || m.peak <= 0.0 {
|
if m.peak <= 0.0 || edges.near(p, 1e-9) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
height * (m.at(p).max(0.0) / m.peak).sqrt()
|
height * (m.at(p).max(0.0) / m.peak).sqrt()
|
||||||
|
|
@ -868,6 +1011,11 @@ pub fn inflate(path: &PathData, bounds: Rect, options: Inflate, view: View) -> R
|
||||||
let base = |i: usize, j: usize| Rect::from_points(at(i * FINE, j * FINE), at((i + 1) * FINE, (j + 1) * FINE));
|
let base = |i: usize, j: usize| Rect::from_points(at(i * FINE, j * FINE), at((i + 1) * FINE, (j + 1) * FINE));
|
||||||
// The shape's part of a rectangle, as rings.
|
// The shape's part of a rectangle, as rings.
|
||||||
let cut = |r: Rect| -> Vec<Vec<Point>> {
|
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![] };
|
||||||
|
}
|
||||||
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().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() {
|
if pieces.iter().map(|p| signed_area(p)).sum::<f64>() <= 1e-9 * r.area() {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
|
||||||
|
|
@ -252,10 +252,10 @@ fn the_inflate_membrane_matches_known_solutions() {
|
||||||
let mut c = BezPath::new();
|
let mut c = BezPath::new();
|
||||||
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 50.0), 0.01));
|
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 (p, b) = shape(&c);
|
||||||
let disc = Membrane::solve(&rings(&p).unwrap(), b.origin(), b.width() / 48.0, 49, 49);
|
let disc = Membrane::solve(&rings(&p, 100.0, MAX_SOLID_POINTS).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);
|
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 (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||||
let sq = Membrane::solve(&rings(&p).unwrap(), b.origin(), 100.0 / 48.0, 49, 49);
|
let sq = Membrane::solve(&rings(&p, 100.0, MAX_SOLID_POINTS).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);
|
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
|
// 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.
|
// (the diagonals). The second difference along a diagonal stays small and even.
|
||||||
|
|
@ -287,3 +287,78 @@ fn inflate_cells_across_a_hole_edge_are_cut_to_the_shape() {
|
||||||
let flat = inflate(&p, r, Inflate::default(), front()).unwrap();
|
let flat = inflate(&p, r, Inflate::default(), front()).unwrap();
|
||||||
assert!(close(area(&flat), 220.0 * 220.0 - 100.0 * 90.0, 1e-3), "{}", area(&flat));
|
assert!(close(area(&flat), 220.0 * 220.0 - 100.0 * 90.0, 1e-3), "{}", area(&flat));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A row of `count` small bumps, each a curve, closed along the bottom: like an outlined word,
|
||||||
|
/// many short curves.
|
||||||
|
fn bumps(count: usize) -> (PathData, Rect) {
|
||||||
|
let mut b = BezPath::new();
|
||||||
|
b.move_to((0.0, 20.0));
|
||||||
|
for i in 0..count {
|
||||||
|
let x = i as f64 * 4.0;
|
||||||
|
b.curve_to((x + 1.0, 14.0), (x + 3.0, 14.0), (x + 4.0, 20.0));
|
||||||
|
}
|
||||||
|
b.line_to((count as f64 * 4.0, 30.0));
|
||||||
|
b.line_to((0.0, 30.0));
|
||||||
|
b.close_path();
|
||||||
|
shape(&b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Curves are followed within a tolerance rather than by sixteen points each: small curves take a
|
||||||
|
/// few points, so shapes of many curves (outlined type) fit; Rotate, which only turns the outline,
|
||||||
|
/// takes far more.
|
||||||
|
#[test]
|
||||||
|
fn small_curves_take_few_points_and_rotate_takes_large_outlines() {
|
||||||
|
// 300 curves were 4,800 points at sixteen each, over the old limit of 1,024.
|
||||||
|
let (p, r) = bumps(300);
|
||||||
|
assert!(extrude(&p, r, Extrude::default(), front()).is_ok());
|
||||||
|
assert!(inflate(&p, r, Inflate { volume: 20.0, ..Inflate::default() }, front()).is_ok());
|
||||||
|
assert!(plane(&p, r, front()).is_ok());
|
||||||
|
// 4,000 curves: too many for a solid, not for Rotate.
|
||||||
|
let (p, r) = bumps(4000);
|
||||||
|
assert!(extrude(&p, r, Extrude::default(), front()).unwrap_err().contains("too complex"));
|
||||||
|
assert_eq!(plane(&p, r, front()).unwrap().len(), 1);
|
||||||
|
// Each piece stays within the tolerance of the curve: a circle's chords sag at most 0.05 pt
|
||||||
|
// here (1/2000 of its 100 pt size is less).
|
||||||
|
let mut c = BezPath::new();
|
||||||
|
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((50.0, 50.0), 50.0), 0.01));
|
||||||
|
let (p, r) = shape(&c);
|
||||||
|
let ring = &rings(&p, 100.0, MAX_SOLID_POINTS).unwrap()[0];
|
||||||
|
for (a, b) in ring.iter().zip(ring.iter().cycle().skip(1)) {
|
||||||
|
let mid = a.midpoint(*b);
|
||||||
|
assert!(50.0 - mid.distance(Point::new(50.0, 50.0)) <= 0.05 + 1e-3, "{a:?} {b:?}");
|
||||||
|
}
|
||||||
|
assert!(r.width() > 0.0);
|
||||||
|
// A small bump takes a few points; a large curve more than sixteen where it needs them.
|
||||||
|
let (p, r) = bumps(300);
|
||||||
|
let small = rings(&p, r.width().max(r.height()), MAX_SOLID_POINTS).unwrap()[0].len();
|
||||||
|
assert!(small <= 300 * 4 + 3, "{small} points for 300 bumps");
|
||||||
|
assert!(ring.len() > 4 * 16, "a 100 pt circle within 0.05 pt: {}", ring.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bucketed nearest-edge search agrees with checking every edge, inside, outside and far
|
||||||
|
/// beyond the shape.
|
||||||
|
#[test]
|
||||||
|
fn the_edge_index_finds_the_nearest_edge() {
|
||||||
|
let (p, r) = bumps(200);
|
||||||
|
let rings = rings(&p, r.width(), MAX_SOLID_POINTS).unwrap();
|
||||||
|
let edges = Edges::new(&rings);
|
||||||
|
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);
|
||||||
|
let (fast, slow) = (edges.distance(q), edge_distance(&rings, q));
|
||||||
|
assert!((fast - slow).abs() < 1e-9, "{q:?}: {fast} against {slow}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a point outside the grid, an edge further along the grid's side can be nearer than one
|
||||||
|
/// straight ahead: the search doesn't stop before it has looked there.
|
||||||
|
#[test]
|
||||||
|
fn the_edge_index_looks_along_the_side_from_outside() {
|
||||||
|
let ring = |b: BezPath| rings(&shape(&b).0, 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 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}");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,5 +88,8 @@ out, perspective). No Adobe software, assets, presets or bevel shapes were used.
|
||||||
front. Concave solids seen at steep angles can overlap wrongly; open (hollow) solids show both sides.
|
front. Concave solids seen at steep angles can overlap wrongly; open (hollow) solids show both sides.
|
||||||
- Shading is flat per facet: an inflated rim seen almost edge-on shows faint banding where the facets get finer.
|
- Shading is flat per facet: an inflated rim seen almost edge-on shows faint banding where the facets get finer.
|
||||||
- Paths and compound paths only: convert type, images and groups to outlines first. One 3D effect per object.
|
- Paths and compound paths only: convert type, images and groups to outlines first. One 3D effect per object.
|
||||||
|
- Curves are followed within 1/2000 of the shape's size (at least 0.05 pt), so a small curve takes a few points
|
||||||
|
and a large one up to 32. Extrude & Bevel and Inflate take up to 2,048 outline points, Rotate (which only turns
|
||||||
|
the outline) 65,536.
|
||||||
- **3D (Classic)** (the track cube, surface types, blend steps, map art), Turntable and 3D export are separate,
|
- **3D (Classic)** (the track cube, surface types, blend steps, map art), Turntable and 3D export are separate,
|
||||||
unbuilt items.
|
unbuilt items.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue