mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Use automatic vector smooth shading without a separate control. Enable Cap by default and keep continuous rim curves and bounded seam joins. Integrate the shared dialog with the other three 3D effects.
376 lines
16 KiB
Rust
376 lines
16 KiB
Rust
//! Vector visibility in the evaluator's paint order. Convex projected faces are cut by the
|
|
//! nearer faces covering them, irrespective of their winding or which side faces the camera.
|
|
//! This preserves interiors visible through open ends and partial sweeps.
|
|
|
|
use vectorcraft_geom::{PathData, Point, Rect, SubPath};
|
|
|
|
use crate::{Face, MAX_FACES, ShadeGradient};
|
|
#[path = "visibility_parts.rs"]
|
|
mod parts;
|
|
|
|
const GRID: usize = 32;
|
|
const MAX_GRID_REFS: usize = 2_000_000;
|
|
const MAX_WORK: usize = 16_000_000;
|
|
// Curved boundaries may begin with several convex pieces. Their total work and
|
|
// anchors still share the global limits used for ordinary projected faces.
|
|
const MAX_FRAGMENTS: usize = 1024;
|
|
const MAX_OUTPUT_POINTS: usize = 262_144;
|
|
|
|
/// The visible portions of one source face, with its original shading and paint order.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct VisibleFace {
|
|
pub path: PathData,
|
|
pub brightness: f64,
|
|
pub depth: f64,
|
|
pub gradient: Option<ShadeGradient>,
|
|
}
|
|
|
|
struct Polygon {
|
|
points: Vec<Point>,
|
|
bounds: Rect,
|
|
}
|
|
|
|
fn bounds(points: &[Point]) -> Rect {
|
|
let Some(first) = points.first() else { return Rect::ZERO };
|
|
points.iter().fold(Rect::from_points(*first, *first), |b, p| b.union_pt(*p))
|
|
}
|
|
|
|
fn area(points: &[Point]) -> f64 {
|
|
let Some(origin) = points.first() else { return 0.0 };
|
|
points.iter().zip(points.iter().cycle().skip(1)).take(points.len()).map(|(a, b)| (*a - *origin).cross(*b - *origin)).sum::<f64>() / 2.0
|
|
}
|
|
|
|
fn clean(mut points: Vec<Point>, epsilon: f64) -> Vec<Point> {
|
|
points.dedup_by(|a, b| a.distance(*b) <= epsilon);
|
|
if points.len() > 1 && points.first().zip(points.last()).is_some_and(|(a, b)| a.distance(*b) <= epsilon) {
|
|
points.pop();
|
|
}
|
|
if points.len() < 3 || area(&points).abs() <= epsilon * epsilon {
|
|
return vec![];
|
|
}
|
|
if area(&points) < 0.0 {
|
|
points.reverse();
|
|
}
|
|
points
|
|
}
|
|
|
|
fn overlaps(a: Rect, b: Rect) -> bool {
|
|
a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1
|
|
}
|
|
|
|
/// Split a convex polygon at an occluder's edge. Boundary vertices belong to both halves,
|
|
/// so independently clipped neighbours share precisely the same intersection positions.
|
|
fn split(points: &[Point], a: Point, b: Point, epsilon: f64) -> (Vec<Point>, Vec<Point>) {
|
|
let edge = b - a;
|
|
let mut inside = vec![];
|
|
let mut outside = vec![];
|
|
for (p, q) in points.iter().zip(points.iter().cycle().skip(1)).take(points.len()) {
|
|
let dp = edge.cross(*p - a);
|
|
let dq = edge.cross(*q - a);
|
|
if dp >= 0.0 {
|
|
inside.push(*p);
|
|
}
|
|
if dp <= 0.0 {
|
|
outside.push(*p);
|
|
}
|
|
if (dp > 0.0 && dq < 0.0) || (dp < 0.0 && dq > 0.0) {
|
|
let intersection = p.lerp(*q, dp / (dp - dq));
|
|
inside.push(intersection);
|
|
outside.push(intersection);
|
|
}
|
|
}
|
|
(clean(inside, epsilon), clean(outside, epsilon))
|
|
}
|
|
|
|
fn subtract(points: Vec<Point>, cover: &Polygon, epsilon: f64, work: &mut usize) -> Result<Vec<Vec<Point>>, String> {
|
|
if !overlaps(bounds(&points), cover.bounds) {
|
|
return Ok(vec![points]);
|
|
}
|
|
// Bounding boxes of neighbouring curved pieces overlap even when the pieces
|
|
// only share an edge. Reject separated convex polygons before cutting them,
|
|
// so irrelevant covers cannot subdivide a face into hundreds of fragments.
|
|
let mut separated = false;
|
|
'edges: for (polygon, other) in [(&cover.points, &points), (&points, &cover.points)] {
|
|
for (a, b) in polygon.iter().zip(polygon.iter().cycle().skip(1)).take(polygon.len()) {
|
|
*work += other.len();
|
|
if *work > MAX_WORK {
|
|
return Err("Revolve visibility exceeds its geometry work limit".into());
|
|
}
|
|
if other.iter().all(|p| (*b - *a).cross(*p - *a) <= epsilon * b.distance(*a)) {
|
|
separated = true;
|
|
break 'edges;
|
|
}
|
|
}
|
|
}
|
|
if separated {
|
|
return Ok(vec![points]);
|
|
}
|
|
if cover
|
|
.points
|
|
.iter()
|
|
.zip(cover.points.iter().cycle().skip(1))
|
|
.take(cover.points.len())
|
|
.all(|(a, b)| points.iter().all(|p| (*b - *a).cross(*p - *a) >= -epsilon * b.distance(*a)))
|
|
{
|
|
return Ok(vec![]);
|
|
}
|
|
let mut remaining = points;
|
|
let mut visible = vec![];
|
|
for (a, b) in cover.points.iter().zip(cover.points.iter().cycle().skip(1)).take(cover.points.len()) {
|
|
*work += remaining.len();
|
|
if *work > MAX_WORK {
|
|
return Err("Revolve visibility exceeds its geometry work limit".into());
|
|
}
|
|
let (inside, outside) = split(&remaining, *a, *b, epsilon);
|
|
if !outside.is_empty() {
|
|
visible.push(outside);
|
|
}
|
|
remaining = inside;
|
|
if remaining.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
Ok(visible)
|
|
}
|
|
|
|
struct Index {
|
|
cells: Vec<Vec<usize>>,
|
|
bounds: Rect,
|
|
refs: usize,
|
|
}
|
|
|
|
impl Index {
|
|
fn tiles(&self, b: Rect) -> impl Iterator<Item = usize> {
|
|
let cell = |v: f64, lo: f64, size: f64| (((v - lo) / size * GRID as f64).floor() as usize).min(GRID - 1);
|
|
let x0 = cell(b.x0, self.bounds.x0, self.bounds.width());
|
|
let x1 = cell(b.x1, self.bounds.x0, self.bounds.width());
|
|
let y0 = cell(b.y0, self.bounds.y0, self.bounds.height());
|
|
let y1 = cell(b.y1, self.bounds.y0, self.bounds.height());
|
|
(y0..=y1).flat_map(move |y| (x0..=x1).map(move |x| y * GRID + x))
|
|
}
|
|
|
|
fn insert(&mut self, b: Rect, face: usize) -> Result<(), String> {
|
|
let tiles: Vec<_> = self.tiles(b).collect();
|
|
self.refs += tiles.len();
|
|
if self.refs > MAX_GRID_REFS {
|
|
return Err("Revolve visibility exceeds its spatial index limit".into());
|
|
}
|
|
for tile in tiles {
|
|
if let Some(cell) = self.cells.get_mut(tile) {
|
|
cell.push(face);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Remove covered portions of opaque faces in back-to-front paint order. This follows the
|
|
/// live evaluator's painter visibility, including its approximation for intersecting surfaces.
|
|
/// Resource limits return an error; callers can retain the original complete surface safely.
|
|
pub fn visible_faces(faces: &[Face]) -> Result<Vec<VisibleFace>, String> {
|
|
if faces.len() > MAX_FACES {
|
|
return Err("Too many Revolve faces for visibility evaluation".into());
|
|
}
|
|
let count = |n: usize, len: usize| n.checked_add(len).filter(|&n| n <= MAX_OUTPUT_POINTS).ok_or("Revolve visibility has too many input anchors");
|
|
faces.iter().map(|f| f.points.len()).try_fold(0, count)?;
|
|
faces.iter().filter_map(|f| f.visibility_pieces.as_ref()).flatten().map(Vec::len).try_fold(0, count)?;
|
|
faces.iter().filter_map(|f| f.outline.as_ref()).flat_map(|p| &p.subpaths).map(|p| p.anchors.len()).try_fold(0, count)?;
|
|
if faces.iter().any(|f| {
|
|
f.points.len() > 1026
|
|
|| !f.brightness.is_finite()
|
|
|| !f.depth.is_finite()
|
|
|| f.gradient.is_some_and(|g| [g.start.x, g.start.y, g.end.x, g.end.y, g.brightness[0], g.brightness[1]].iter().any(|v| !v.is_finite()))
|
|
|| f.points.iter().any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs().max(p.y.abs()) > 1e8)
|
|
|| f.visibility_pieces.as_ref().is_some_and(|pieces| {
|
|
pieces.iter().any(|p| p.len() > 1026 || p.iter().any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs().max(p.y.abs()) > 1e8))
|
|
})
|
|
|| f.outline.as_ref().is_some_and(|p| {
|
|
p.subpaths
|
|
.iter()
|
|
.flat_map(|s| &s.anchors)
|
|
.any(|a| [a.p, a.h_in, a.h_out].iter().any(|p| !p.x.is_finite() || !p.y.is_finite() || p.x.abs().max(p.y.abs()) > 1e8))
|
|
})
|
|
}) {
|
|
return Err("Revolve visibility requires finite projected faces".into());
|
|
}
|
|
let all = faces.iter().flat_map(|f| &f.points).copied().collect::<Vec<_>>();
|
|
let total = bounds(&all);
|
|
if total.width() <= 0.0 || total.height() <= 0.0 {
|
|
return Ok(vec![]);
|
|
}
|
|
let epsilon = (total.width().max(total.height()) * 1e-10).clamp(1e-8, 1e-3);
|
|
let mut work = 0;
|
|
let polygons: Vec<Vec<Polygon>> = faces
|
|
.iter()
|
|
.map(|f| Ok(parts::pieces(f, epsilon, &mut work)?.into_iter().map(|points| Polygon { bounds: bounds(&points), points }).collect()))
|
|
.collect::<Result<_, String>>()?;
|
|
let covers: Vec<_> = polygons.iter().flatten().collect();
|
|
let mut index = Index { cells: vec![vec![]; GRID * GRID], bounds: total, refs: 0 };
|
|
let mut seen = vec![usize::MAX; covers.len()];
|
|
let (mut out, mut output_points) = (vec![], 0);
|
|
let mut cover_end = covers.len();
|
|
for (source, (face, pieces)) in faces.iter().zip(&polygons).enumerate().rev() {
|
|
let cover_start = cover_end.saturating_sub(pieces.len());
|
|
let source_covers = cover_start..cover_end;
|
|
cover_end = cover_start;
|
|
if pieces.is_empty() {
|
|
continue;
|
|
}
|
|
let source_bounds = bounds(&face.points);
|
|
let mut fragments: Vec<_> = pieces.iter().map(|p| p.points.clone()).collect();
|
|
let mut clipped = false;
|
|
'covers: for tile in index.tiles(source_bounds) {
|
|
for &candidate in index.cells.get(tile).into_iter().flatten() {
|
|
work += 1;
|
|
if work > MAX_WORK {
|
|
return Err("Revolve visibility exceeds its geometry work limit".into());
|
|
}
|
|
if let Some(mark) = seen.get_mut(candidate)
|
|
&& *mark != source
|
|
{
|
|
*mark = source;
|
|
if let Some(cover) = covers.get(candidate)
|
|
&& overlaps(source_bounds, cover.bounds)
|
|
{
|
|
let mut remaining = vec![];
|
|
for fragment in &fragments {
|
|
remaining.extend(subtract(fragment.clone(), cover, epsilon, &mut work)?);
|
|
if remaining.len() > MAX_FRAGMENTS {
|
|
return Err("Revolve visibility creates too many fragments".into());
|
|
}
|
|
}
|
|
clipped |= remaining != fragments;
|
|
fragments = remaining;
|
|
if fragments.is_empty() {
|
|
break 'covers;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if fragments.is_empty() {
|
|
continue;
|
|
}
|
|
output_points += fragments.iter().map(Vec::len).sum::<usize>();
|
|
if output_points > MAX_OUTPUT_POINTS {
|
|
return Err("Revolve visibility creates too many vector anchors".into());
|
|
}
|
|
let path = if !clipped { face.path() } else { PathData::new(fragments.into_iter().map(|p| SubPath::polyline(&p, true)).collect()) };
|
|
out.push(VisibleFace { path, brightness: face.brightness, depth: face.depth, gradient: face.gradient });
|
|
// A completely covered face need not occupy the grid. Its coverage already belongs
|
|
// to nearer faces; this keeps repeated overlapping surfaces bounded in practice.
|
|
for candidate in source_covers {
|
|
if let Some(cover) = covers.get(candidate) {
|
|
index.insert(cover.bounds, candidate)?;
|
|
}
|
|
}
|
|
}
|
|
out.reverse();
|
|
Ok(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{Edge, Revolve, revolve};
|
|
use vectorcraft_geom::{BezPath, Shape};
|
|
|
|
fn face(rect: Rect, brightness: f64) -> Face {
|
|
Face {
|
|
points: vec![Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1)],
|
|
depth: brightness,
|
|
brightness,
|
|
gradient: None,
|
|
outline: None,
|
|
cap: false,
|
|
visibility_pieces: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn covered_faces_disappear_and_partial_faces_are_trimmed_in_paint_order() {
|
|
let faces =
|
|
vec![face(Rect::new(2.0, 2.0, 8.0, 8.0), 0.1), face(Rect::new(0.0, 0.0, 10.0, 10.0), 0.2), face(Rect::new(5.0, 0.0, 15.0, 10.0), 0.8)];
|
|
let visible = visible_faces(&faces).unwrap();
|
|
assert_eq!(visible.len(), 2);
|
|
assert_eq!(visible[0].brightness, 0.2);
|
|
assert_eq!(visible[0].path.bounds(), Some(Rect::new(0.0, 0.0, 5.0, 10.0)));
|
|
assert!((visible[0].path.to_bezpath().area() - 50.0).abs() < 1e-8);
|
|
assert!((visible[1].path.to_bezpath().area() - 100.0).abs() < 1e-8);
|
|
// Winding and equal depth do not determine visibility; actual paint order does.
|
|
let mut flipped = faces.clone();
|
|
for f in &mut flipped {
|
|
f.points.reverse();
|
|
f.depth = 0.0;
|
|
}
|
|
let other = visible_faces(&flipped).unwrap();
|
|
assert_eq!(visible.iter().map(|f| f.brightness).collect::<Vec<_>>(), other.iter().map(|f| f.brightness).collect::<Vec<_>>());
|
|
assert_eq!(visible[0].path.bounds(), other[0].path.bounds());
|
|
}
|
|
|
|
#[test]
|
|
fn an_interior_surface_remains_visible_through_an_opening() {
|
|
let faces = vec![
|
|
face(Rect::new(0.0, 0.0, 10.0, 10.0), 0.1),
|
|
face(Rect::new(0.0, 0.0, 10.0, 3.0), 0.2),
|
|
face(Rect::new(0.0, 7.0, 10.0, 10.0), 0.3),
|
|
face(Rect::new(0.0, 3.0, 3.0, 7.0), 0.4),
|
|
face(Rect::new(7.0, 3.0, 10.0, 7.0), 0.5),
|
|
];
|
|
let visible = visible_faces(&faces).unwrap();
|
|
assert_eq!(visible[0].brightness, 0.1);
|
|
assert_eq!(visible[0].path.bounds(), Some(Rect::new(3.0, 3.0, 7.0, 7.0)));
|
|
assert_eq!(visible[0].path.to_bezpath().winding(Point::new(5.0, 5.0)), 1);
|
|
assert_eq!(visible[0].path.to_bezpath().winding(Point::new(1.0, 1.0)), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn revolved_open_profiles_match_visible_paint_for_both_axis_sides_and_perspective() {
|
|
let mut bp = BezPath::new();
|
|
bp.move_to((40.0, 10.0));
|
|
bp.curve_to((75.0, 20.0), (75.0, 80.0), (40.0, 90.0));
|
|
let path = PathData::from_bezpath(&bp);
|
|
for edge in [Edge::Left, Edge::Right] {
|
|
for angle in [155.0, 360.0] {
|
|
let options = Revolve {
|
|
edge,
|
|
angle,
|
|
offset: 8.0,
|
|
segments: 32,
|
|
rotation_x: -32.0,
|
|
rotation_y: 24.0,
|
|
rotation_z: 19.0,
|
|
perspective: crate::perspective_from_strength(0.7),
|
|
..Default::default()
|
|
};
|
|
let faces = revolve(&path, path.bounds().unwrap(), options).unwrap();
|
|
let visible = visible_faces(&faces).unwrap();
|
|
assert!(visible.len() <= faces.len());
|
|
let all: Vec<_> = faces.iter().map(|f| (f.path().to_bezpath(), f.brightness)).collect();
|
|
let cut: Vec<_> = visible.iter().map(|f| (f.path.to_bezpath(), f.brightness)).collect();
|
|
let b = bounds(&faces.iter().flat_map(|f| &f.points).copied().collect::<Vec<_>>());
|
|
for x in 0..48 {
|
|
for y in 0..48 {
|
|
let p = Point::new(b.x0 + b.width() * (x as f64 + 0.337) / 48.0, b.y0 + b.height() * (y as f64 + 0.413) / 48.0);
|
|
let expected = all.iter().rev().find(|(bp, _)| bp.winding(p) != 0).map(|(_, value)| value);
|
|
let actual = cut.iter().rev().find(|(bp, _)| bp.winding(p) != 0).map(|(_, value)| value);
|
|
assert_eq!(actual, expected, "{edge:?}, {angle}, {p:?}");
|
|
assert!(cut.iter().filter(|(bp, _)| bp.winding(p) != 0).count() <= 1, "expanded surfaces do not cover hidden regions");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn visibility_is_bounded_for_repeated_faces_and_rejects_invalid_input() {
|
|
let faces = vec![face(Rect::new(0.0, 0.0, 10.0, 10.0), 0.5); MAX_FACES];
|
|
assert_eq!(visible_faces(&faces).unwrap().len(), 1);
|
|
assert!(visible_faces(&vec![faces[0].clone(); MAX_FACES + 1]).is_err());
|
|
let mut bad = faces[0].clone();
|
|
bad.points[0].x = f64::NAN;
|
|
assert!(visible_faces(&[bad]).is_err());
|
|
assert!(visible_faces(&[]).unwrap().is_empty());
|
|
}
|
|
}
|