mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
refactor(tools): move builder.rs items to builder/{cache,live_paint,lookup,map}.rs
This commit is contained in:
+17
-1165
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
||||
use super::lookup::{drawn_outlines, faces_of, shape_builder_shapes, shapes_for};
|
||||
use super::map::{BuilderMap, sorted_roots};
|
||||
use super::*;
|
||||
|
||||
/// What a [`RegionCache`] map was built for: the document's address and revision, the selected ids
|
||||
/// and whether filled open paths count as closed.
|
||||
pub(super) type CacheKey = (usize, (u64, u64), Vec<NodeId>, bool);
|
||||
|
||||
/// Hovering builds the map of inputs with more segments than this on another thread
|
||||
/// ([`RegionCache::ready`]); smaller maps are built on the calling thread.
|
||||
const BACKGROUND_SEGMENTS: usize = 128;
|
||||
|
||||
/// A map being built on another thread: the key it is for, the drawn outlines of its inputs, and
|
||||
/// the channel it arrives on.
|
||||
struct Pending {
|
||||
key: CacheKey,
|
||||
outlines: Vec<PathData>,
|
||||
done: Receiver<BuilderMap>,
|
||||
}
|
||||
|
||||
/// The planar map of the current selection, cached by document identity, revision, selection and
|
||||
/// whether filled open paths count as closed.
|
||||
#[derive(Default)]
|
||||
pub(super) struct RegionCache {
|
||||
pub(super) key: Option<CacheKey>,
|
||||
/// The map, which the Shape Builder's merge command takes too ([`Tool::shape_builder_map`]).
|
||||
pub(super) map: Arc<BuilderMap>,
|
||||
/// Live Paint faces (every path an edge, filled or not) rather than Shape Builder regions.
|
||||
live: bool,
|
||||
/// Gap Detection distance; changing tool options invalidates the cache.
|
||||
pub(super) gap: f64,
|
||||
/// The outlines of the Shape Builder's inputs as they are drawn (see [`BuilderMap::stroke_at`]).
|
||||
pub(super) outlines: Vec<PathData>,
|
||||
/// The map being built on another thread, if any.
|
||||
pending: Option<Pending>,
|
||||
/// The key of the last map whose build on another thread failed.
|
||||
failed: Option<CacheKey>,
|
||||
/// How many times the map has been built.
|
||||
#[cfg(test)]
|
||||
pub(super) builds: usize,
|
||||
/// How many of those builds ran on another thread.
|
||||
#[cfg(test)]
|
||||
pub(super) background: usize,
|
||||
}
|
||||
|
||||
/// The map of `shapes`: Live Paint faces with `live`, else the Shape Builder map with its edges,
|
||||
/// closing gaps up to `gap`.
|
||||
fn build_map(shapes: Vec<po::Shape>, live: bool, gap: f64) -> BuilderMap {
|
||||
if live {
|
||||
let regions = if shapes.is_empty() { vec![] } else { faces_of(po::live_paint(&shapes).0) };
|
||||
BuilderMap { regions, ..Default::default() }
|
||||
} else {
|
||||
BuilderMap::of_shapes(shapes, true, gap)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the map of `shapes` ([`build_map`]) on a new thread; None when no thread starts.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spawn_build(shapes: Vec<po::Shape>, live: bool, gap: f64) -> Option<Receiver<BuilderMap>> {
|
||||
let (tx, done) = std::sync::mpsc::channel();
|
||||
let spawned = std::thread::Builder::new().name("vectorcraft-region-map".into()).spawn(move || {
|
||||
// The tool may have dropped the receiver meanwhile (another tool, another selection).
|
||||
let _ = tx.send(build_map(shapes, live, gap));
|
||||
});
|
||||
spawned.is_ok().then_some(done)
|
||||
}
|
||||
|
||||
/// The web build has no threads.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn spawn_build(_shapes: Vec<po::Shape>, _live: bool, _gap: f64) -> Option<Receiver<BuilderMap>> {
|
||||
None
|
||||
}
|
||||
|
||||
impl RegionCache {
|
||||
pub(super) fn live_paint() -> Self {
|
||||
Self { live: true, ..Default::default() }
|
||||
}
|
||||
|
||||
fn key_for(cx: &ToolContext, open_filled_as_closed: bool) -> CacheKey {
|
||||
(cx.doc as *const Document as usize, cx.revision, cx.selection.objects.clone(), open_filled_as_closed)
|
||||
}
|
||||
|
||||
/// The inputs of the selection's map, back → front, and their drawn outlines (Shape Builder
|
||||
/// only).
|
||||
fn inputs(&self, cx: &ToolContext, open_filled_as_closed: bool) -> (Vec<po::Shape>, Vec<PathData>) {
|
||||
let roots = sorted_roots(cx.doc, &cx.selection.objects);
|
||||
if self.live {
|
||||
(shapes_for(cx.doc, &roots).0, vec![])
|
||||
} else {
|
||||
let (shapes, leaves) = shape_builder_shapes(cx.doc, &roots, open_filled_as_closed);
|
||||
let outlines = drawn_outlines(cx.doc, &leaves);
|
||||
(shapes, outlines)
|
||||
}
|
||||
}
|
||||
|
||||
fn install(&mut self, key: CacheKey, map: BuilderMap, outlines: Vec<PathData>) {
|
||||
self.map = Arc::new(map);
|
||||
self.outlines = outlines;
|
||||
self.key = Some(key);
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.builds += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Forgets the map and any map being built (the tool options changed).
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.key = None;
|
||||
self.pending = None;
|
||||
self.failed = None;
|
||||
}
|
||||
|
||||
/// Is a map being built on another thread?
|
||||
pub(super) fn waiting(&self) -> bool {
|
||||
self.pending.is_some()
|
||||
}
|
||||
|
||||
/// The map, built again only when the key changes, with filled open paths closed when
|
||||
/// `open_filled_as_closed` (Shape Builder only). A Shape Builder map has the outline edges too,
|
||||
/// so hovering and erasing use the same map. Waits for a map being built on another thread for
|
||||
/// the same key.
|
||||
pub(super) fn get(&mut self, cx: &ToolContext, open_filled_as_closed: bool) -> &BuilderMap {
|
||||
let key = Self::key_for(cx, open_filled_as_closed);
|
||||
if self.key.as_ref() != Some(&key) {
|
||||
let built = self.pending.take().filter(|p| p.key == key).and_then(|p| Some((p.done.recv().ok()?, p.outlines)));
|
||||
let (map, outlines) = match built {
|
||||
Some(b) => {
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.background += 1;
|
||||
}
|
||||
b
|
||||
}
|
||||
None => {
|
||||
let (shapes, outlines) = self.inputs(cx, open_filled_as_closed);
|
||||
(build_map(shapes, self.live, self.gap), outlines)
|
||||
}
|
||||
};
|
||||
self.install(key, map, outlines);
|
||||
}
|
||||
&self.map
|
||||
}
|
||||
|
||||
/// Takes the map being built on another thread once it has arrived, or with `wait` once it
|
||||
/// arrives, as the map for the key it was built for.
|
||||
pub(super) fn settle(&mut self, wait: bool) {
|
||||
let Some(p) = self.pending.take() else { return };
|
||||
let got = if wait { p.done.recv().map_err(|_| TryRecvError::Disconnected) } else { p.done.try_recv() };
|
||||
match got {
|
||||
Ok(map) => {
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.background += 1;
|
||||
}
|
||||
self.install(p.key, map, p.outlines);
|
||||
}
|
||||
// The build failed (a bug in the planar map): hovering does not build it again, and a
|
||||
// gesture builds it on the calling thread, which reports the failure.
|
||||
Err(TryRecvError::Disconnected) => self.failed = Some(p.key),
|
||||
Err(TryRecvError::Empty) => self.pending = Some(p),
|
||||
}
|
||||
}
|
||||
|
||||
/// The map as [`Self::get`] gives it, without waiting: when the key changes and the inputs have
|
||||
/// more than [`BACKGROUND_SEGMENTS`] segments, the map is built on another thread, and this is
|
||||
/// None until it arrives ([`Self::waiting`] holds meanwhile). A map being built for an earlier
|
||||
/// key is let finish before the next one starts.
|
||||
pub(super) fn ready(&mut self, cx: &ToolContext, open_filled_as_closed: bool) -> Option<&BuilderMap> {
|
||||
self.settle(false);
|
||||
let key = Self::key_for(cx, open_filled_as_closed);
|
||||
if self.key.as_ref() == Some(&key) {
|
||||
return Some(&self.map);
|
||||
}
|
||||
if self.pending.is_some() || self.failed.as_ref() == Some(&key) {
|
||||
return None;
|
||||
}
|
||||
let (shapes, outlines) = self.inputs(cx, open_filled_as_closed);
|
||||
let segments: usize = shapes.iter().map(|s| s.path.subpaths.iter().map(|sp| sp.segment_count()).sum::<usize>()).sum();
|
||||
if segments > BACKGROUND_SEGMENTS && cfg!(not(target_arch = "wasm32")) {
|
||||
if let Some(done) = spawn_build(shapes, self.live, self.gap) {
|
||||
self.pending = Some(Pending { key, outlines, done });
|
||||
return None;
|
||||
}
|
||||
// No thread started: build here.
|
||||
return Some(self.get(cx, open_filled_as_closed));
|
||||
}
|
||||
let map = build_map(shapes, self.live, self.gap);
|
||||
self.install(key, map, outlines);
|
||||
Some(&self.map)
|
||||
}
|
||||
|
||||
pub(super) fn at(&mut self, cx: &ToolContext, p: Point) -> Option<usize> {
|
||||
self.get(cx, true).region_at(p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
use super::cache::RegionCache;
|
||||
use super::lookup::{is_live_paint, live_paint_at, visible_edge_near, visible_face_at};
|
||||
use super::map::{HIGHLIGHT_RED, sorted_roots};
|
||||
use super::*;
|
||||
|
||||
// ---------- Live Paint Bucket ----------
|
||||
|
||||
/// What the Live Paint tools highlight under the cursor.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(super) enum LpHover {
|
||||
#[default]
|
||||
None,
|
||||
/// A face (or edge) of an existing Live Paint group.
|
||||
Part(BezPath),
|
||||
/// A region of the (not yet live) selection.
|
||||
Region(usize),
|
||||
}
|
||||
|
||||
/// The Live Paint group and part the Live Paint tools hover and click at `p`. Inside a visible face
|
||||
/// of a visible Live Paint group ([`live_paint_at`]) it is that group with its visible face under
|
||||
/// `p`, or with `edge` its visible edge nearest `p` within `tol` (None when there is none). Outside
|
||||
/// every visible face, with `edge`, it is the visible edge nearest `p` within `tol` among the
|
||||
/// visible Live Paint groups (the front-most group's on a tie). None when no group is found.
|
||||
fn lp_part_at(doc: &Document, p: Point, tol: f64, edge: bool) -> Option<(NodeId, Option<&Arc<Node>>)> {
|
||||
if let Some(g) = live_paint_at(doc, p).and_then(|id| doc.node(id)) {
|
||||
return Some((g.id, if edge { visible_edge_near(g, p, tol) } else { visible_face_at(g, p) }));
|
||||
}
|
||||
if !edge {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<(f64, NodeId, &Arc<Node>)> = None;
|
||||
doc.walk(|n| {
|
||||
if is_live_paint(n)
|
||||
&& let Some(e) = visible_edge_near(n, p, tol)
|
||||
&& let Some((.., d)) = e.path_data().and_then(|pd| pd.nearest(p))
|
||||
&& best.is_none_or(|b| d <= b.0)
|
||||
&& doc.is_visible(n.id)
|
||||
{
|
||||
best = Some((d, n.id, e));
|
||||
}
|
||||
});
|
||||
best.map(|(_, g, e)| (g, Some(e)))
|
||||
}
|
||||
|
||||
/// What the Live Paint tools highlight at `p`: the part of a Live Paint group ([`lp_part_at`]), else
|
||||
/// with `cache` the region of the selection under `p`. A large selection's regions are found on
|
||||
/// another thread ([`RegionCache::ready`]), and until they are, no region is highlighted.
|
||||
fn lp_hover(cx: &ToolContext, cache: Option<&mut RegionCache>, p: Point, edge: bool) -> LpHover {
|
||||
if let Some((_, part)) = lp_part_at(cx.doc, p, cx.tol(4.0), edge) {
|
||||
return part.and_then(|n| n.path_data()).map(|pd| LpHover::Part(pd.to_bezpath())).unwrap_or_default();
|
||||
}
|
||||
let sel_is_live = cx.selection.objects.iter().any(|id| cx.doc.node(*id).is_some_and(is_live_paint));
|
||||
if !sel_is_live && let Some(i) = cache.and_then(|c| c.ready(cx, true)).and_then(|m| m.region_at(p)) {
|
||||
return LpHover::Region(i);
|
||||
}
|
||||
LpHover::None
|
||||
}
|
||||
|
||||
/// The highlight of `h`, `width` screen pixels wide in `color`.
|
||||
fn lp_overlays(h: &LpHover, cache: &RegionCache, color: [u8; 3], width: f32) -> Vec<Overlay> {
|
||||
match h {
|
||||
LpHover::None => vec![],
|
||||
LpHover::Part(bp) => vec![Overlay::Path { path: bp.clone(), color, width, dashed: false }],
|
||||
LpHover::Region(i) => {
|
||||
cache.map.regions.get(*i).map(|(_, bp, _)| vec![Overlay::Path { path: bp.clone(), color, width, dashed: false }]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Live Paint Bucket: click fills the face under the cursor with the current fill; Shift-click
|
||||
/// paints the nearest edge with the current stroke. Clicking selected ordinary paths makes them a
|
||||
/// Live Paint group first.
|
||||
pub struct LivePaintBucketTool {
|
||||
pub(super) cache: RegionCache,
|
||||
pub(super) hover: LpHover,
|
||||
/// Where the pointer last moved, and whether Shift was held.
|
||||
at: Option<(Point, bool)>,
|
||||
}
|
||||
|
||||
impl Default for LivePaintBucketTool {
|
||||
fn default() -> Self {
|
||||
Self { cache: RegionCache::live_paint(), hover: LpHover::None, at: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for LivePaintBucketTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"livePaintBucket"
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.at = Some((p, ev.mods.shift));
|
||||
self.hover = lp_hover(cx, Some(&mut self.cache), p, ev.mods.shift);
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.hover = LpHover::None;
|
||||
let point = json!([p.x, p.y]);
|
||||
if ev.mods.shift {
|
||||
return match lp_part_at(cx.doc, p, cx.tol(4.0), true) {
|
||||
Some((g, Some(_))) => {
|
||||
vec![Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0) }))]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
}
|
||||
if let Some(g) = live_paint_at(cx.doc, p) {
|
||||
return vec![Action::Exec("livePaint.fill".into(), json!({ "group": g.0, "point": point }))];
|
||||
}
|
||||
if self.cache.at(cx, p).is_some() {
|
||||
let roots = sorted_roots(cx.doc, &cx.selection.objects);
|
||||
return vec![Action::Exec("livePaint.fill".into(), json!({ "ids": json_ids(&roots), "point": point }))];
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
lp_overlays(&self.hover, &self.cache, HIGHLIGHT_RED, 4.0)
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::LivePaintBucket
|
||||
}
|
||||
|
||||
fn waiting(&self) -> bool {
|
||||
self.cache.waiting()
|
||||
}
|
||||
|
||||
fn poll(&mut self, cx: &ToolContext, wait: bool) {
|
||||
if !self.cache.waiting() {
|
||||
return;
|
||||
}
|
||||
// As the Shape Builder's: the map that arrives may be for an earlier revision. Over a part
|
||||
// of a Live Paint group the hover takes no map.
|
||||
loop {
|
||||
self.cache.settle(wait);
|
||||
if let Some((p, shift)) = self.at {
|
||||
self.hover = lp_hover(cx, Some(&mut self.cache), p, shift);
|
||||
}
|
||||
if !wait || !self.cache.waiting() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Live Paint Selection ----------
|
||||
|
||||
/// The Live Paint Selection tool's highlight width (screen pixels) by default.
|
||||
pub const LP_SELECT_WIDTH: f64 = 4.0;
|
||||
|
||||
/// The least and greatest highlight width (screen pixels) the Live Paint Selection tool takes.
|
||||
pub const LP_SELECT_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
|
||||
|
||||
/// Live Paint Selection: click selects a face (Shift adds); Alt-click selects an edge. Its Tool
|
||||
/// Options (double-click the tool) are `selectFills` and `selectStrokes` (both on), the highlight
|
||||
/// `color` (`#rrggbb`, red by default) and its `width` ([`LP_SELECT_WIDTH`] pixels). With Select
|
||||
/// Strokes off, Alt-click selects the face under the pointer as a click does; with Select Fills
|
||||
/// off, a click selects the edge nearest the pointer as Alt-click does; with both off, a click
|
||||
/// selects no face or edge.
|
||||
pub struct LivePaintSelectionTool {
|
||||
pub(super) cache: RegionCache,
|
||||
pub(super) hover: LpHover,
|
||||
fills: bool,
|
||||
strokes: bool,
|
||||
color: [u8; 3],
|
||||
width: f64,
|
||||
}
|
||||
|
||||
impl Default for LivePaintSelectionTool {
|
||||
fn default() -> Self {
|
||||
Self { cache: RegionCache::live_paint(), hover: LpHover::None, fills: true, strokes: true, color: HIGHLIGHT_RED, width: LP_SELECT_WIDTH }
|
||||
}
|
||||
}
|
||||
|
||||
impl LivePaintSelectionTool {
|
||||
/// What a click picks, Alt held or not: Some(true) an edge, Some(false) a face, None nothing.
|
||||
fn picks_edge(&self, alt: bool) -> Option<bool> {
|
||||
match (self.fills, self.strokes) {
|
||||
(true, true) => Some(alt),
|
||||
(true, false) => Some(false),
|
||||
(false, true) => Some(true),
|
||||
(false, false) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A highlight color option: `#rrggbb` or a layer color's name (any case).
|
||||
pub(super) fn lp_color(v: &Value) -> Option<[u8; 3]> {
|
||||
let s = v.as_str()?.trim();
|
||||
if let Some((_, c)) = vectorcraft_doc::LAYER_COLORS.iter().find(|(name, _)| name.eq_ignore_ascii_case(s)) {
|
||||
return Some(*c);
|
||||
}
|
||||
let [r, g, b, _] = vectorcraft_color::Color::from_hex(s)?.to_rgba8(1.0);
|
||||
Some([r, g, b])
|
||||
}
|
||||
|
||||
impl Tool for LivePaintSelectionTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"livePaintSelection"
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
let edge = self.picks_edge(ev.mods.alt);
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
// Only the parts of Live Paint groups highlight, so no map of the selection is built.
|
||||
self.hover = match edge.map(|e| lp_hover(cx, None, p, e)) {
|
||||
Some(h @ LpHover::Part(_)) => h,
|
||||
_ => LpHover::None,
|
||||
};
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
let hit = edge.and_then(|e| lp_part_at(cx.doc, p, cx.tol(4.0), e)).and_then(|(_, part)| part).map(|n| n.id);
|
||||
match (hit, ev.mods.shift) {
|
||||
(Some(id), true) => vec![Action::Exec("select.toggle".into(), json!({ "id": id.0 }))],
|
||||
(Some(id), false) => vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))],
|
||||
(None, true) => vec![],
|
||||
(None, false) => vec![Action::Exec("select.none".into(), json!({}))],
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32)
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Arrow
|
||||
}
|
||||
|
||||
fn options(&self) -> Value {
|
||||
let [r, g, b] = self.color;
|
||||
json!({
|
||||
"selectFills": self.fills,
|
||||
"selectStrokes": self.strokes,
|
||||
"color": format!("#{r:02x}{g:02x}{b:02x}"),
|
||||
"width": self.width,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_option(&mut self, key: &str, v: &Value) {
|
||||
match key {
|
||||
"selectFills" => self.fills = v.as_bool().unwrap_or(self.fills),
|
||||
"selectStrokes" => self.strokes = v.as_bool().unwrap_or(self.strokes),
|
||||
"color" => self.color = lp_color(v).unwrap_or(self.color),
|
||||
"width" => {
|
||||
let (min, max) = LP_SELECT_WIDTH_RANGE;
|
||||
self.width = v.as_f64().filter(|w| w.is_finite()).map_or(self.width, |w| w.clamp(min, max));
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
self.hover = LpHover::None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
use super::*;
|
||||
|
||||
/// Names starting with this mark a Live Paint group, together with its sources group ([`SOURCES_NAME`]).
|
||||
pub const LIVE_PAINT_PREFIX: &str = "Live Paint";
|
||||
|
||||
/// Default name of a new Live Paint group.
|
||||
pub const LIVE_PAINT_NAME: &str = "Live Paint Group";
|
||||
|
||||
/// The hidden child group holding the source paths.
|
||||
pub const SOURCES_NAME: &str = "Live Paint Sources";
|
||||
|
||||
pub const FACE_NAME: &str = "Face";
|
||||
|
||||
pub const EDGE_NAME: &str = "Edge";
|
||||
|
||||
/// Is `n` a Live Paint group: an unclipped group whose name starts with [`LIVE_PAINT_PREFIX`] and
|
||||
/// that holds a group named [`SOURCES_NAME`]? A plain group with such a name is not one.
|
||||
pub fn is_live_paint(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Group { clip: false, .. })
|
||||
&& n.name.as_deref().is_some_and(|s| s.starts_with(LIVE_PAINT_PREFIX))
|
||||
&& sources(n).is_some_and(|s| matches!(s.kind, NodeKind::Group { .. }))
|
||||
}
|
||||
|
||||
/// Whether Live Paint can take `n` as it is: a Live Paint group, a path or compound path, or a
|
||||
/// group of those. Type, images, symbols, blends, meshes and the rest have to be converted to
|
||||
/// paths first (<https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html>);
|
||||
/// Make and Merge refuse them rather than hide them among the group's sources.
|
||||
pub fn live_paint_takes(n: &Node) -> bool {
|
||||
is_live_paint(n)
|
||||
|| match &n.kind {
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => true,
|
||||
NodeKind::Group { children, .. } => children.iter().all(|c| live_paint_takes(c)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn named(n: &Node, name: &str) -> bool {
|
||||
n.name.as_deref() == Some(name)
|
||||
}
|
||||
|
||||
/// Face children of a Live Paint group (paint order).
|
||||
pub fn faces(group: &Node) -> Vec<&Arc<Node>> {
|
||||
group.children().map(|c| c.iter().filter(|n| named(n, FACE_NAME)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Edge children of a Live Paint group.
|
||||
pub fn edges(group: &Node) -> Vec<&Arc<Node>> {
|
||||
group.children().map(|c| c.iter().filter(|n| named(n, EDGE_NAME)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The hidden sources group of a Live Paint group.
|
||||
pub fn sources(group: &Node) -> Option<&Arc<Node>> {
|
||||
group.children()?.iter().find(|n| named(n, SOURCES_NAME))
|
||||
}
|
||||
|
||||
/// Does the path (filled) contain `p`?
|
||||
pub fn path_contains(path: &PathData, rule: FillRule, p: Point) -> bool {
|
||||
if path.bounds().is_none_or(|b| !b.inflate(1e-6, 1e-6).contains(p)) {
|
||||
return false;
|
||||
}
|
||||
let w = path.to_bezpath().winding(p);
|
||||
match rule {
|
||||
FillRule::EvenOdd => w % 2 != 0,
|
||||
_ => w != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The face of `group` under `p` (front-most).
|
||||
pub fn face_at(group: &Node, p: Point) -> Option<&Arc<Node>> {
|
||||
front_face(faces(group).into_iter(), p)
|
||||
}
|
||||
|
||||
/// The face of `group` under `p` (front-most) among its faces that are not hidden.
|
||||
pub fn visible_face_at(group: &Node, p: Point) -> Option<&Arc<Node>> {
|
||||
front_face(faces(group).into_iter().filter(|f| f.visible), p)
|
||||
}
|
||||
|
||||
/// The face of `faces` (paint order) under `p` (front-most).
|
||||
fn front_face<'a>(faces: impl DoubleEndedIterator<Item = &'a Arc<Node>>, p: Point) -> Option<&'a Arc<Node>> {
|
||||
faces.rev().find(|f| f.path_data().is_some_and(|pd| path_contains(pd, FillRule::NonZero, p)))
|
||||
}
|
||||
|
||||
/// The edge of `group` nearest to `p` within `tol`.
|
||||
pub fn edge_near(group: &Node, p: Point, tol: f64) -> Option<&Arc<Node>> {
|
||||
nearest_edge(edges(group), p, tol)
|
||||
}
|
||||
|
||||
/// The edge of `group` nearest to `p` within `tol` among its edges that are not hidden.
|
||||
pub fn visible_edge_near(group: &Node, p: Point, tol: f64) -> Option<&Arc<Node>> {
|
||||
nearest_edge(edges(group).into_iter().filter(|e| e.visible), p, tol)
|
||||
}
|
||||
|
||||
/// The edge of `edges` nearest to `p` within `tol`.
|
||||
fn nearest_edge<'a>(edges: impl IntoIterator<Item = &'a Arc<Node>>, p: Point, tol: f64) -> Option<&'a Arc<Node>> {
|
||||
let mut best: Option<(f64, &Arc<Node>)> = None;
|
||||
for e in edges {
|
||||
if let Some((.., d)) = e.path_data().and_then(|pd| pd.nearest(p))
|
||||
&& d <= tol
|
||||
&& best.is_none_or(|b| d < b.0)
|
||||
{
|
||||
best = Some((d, e));
|
||||
}
|
||||
}
|
||||
best.map(|b| b.1)
|
||||
}
|
||||
|
||||
/// The visible Live Paint group with a face under `p` that is not hidden (front-most in paint order).
|
||||
pub fn live_paint_at(doc: &Document, p: Point) -> Option<NodeId> {
|
||||
let mut hit = None;
|
||||
doc.walk(|n| {
|
||||
if is_live_paint(n) && doc.is_visible(n.id) && visible_face_at(n, p).is_some() {
|
||||
hit = Some(n.id);
|
||||
}
|
||||
});
|
||||
hit
|
||||
}
|
||||
|
||||
/// Paths, compound paths (as one shape) of a subtree in paint order; guides, clipping paths and
|
||||
/// hidden objects are skipped. Live Paint groups contribute their faces and edges.
|
||||
pub fn leaf_shapes<'a>(n: &'a Node, out: &mut Vec<&'a Node>) {
|
||||
collect_leaves(n, false, out);
|
||||
}
|
||||
|
||||
/// [`leaf_shapes`]; with `filled_faces`, a Live Paint group contributes only its filled faces and
|
||||
/// its stroked edges.
|
||||
fn collect_leaves<'a>(n: &'a Node, filled_faces: bool, out: &mut Vec<&'a Node>) {
|
||||
if !n.visible {
|
||||
return;
|
||||
}
|
||||
if n.extras().shaper.is_some() {
|
||||
for c in n.children().into_iter().flatten().skip(1) {
|
||||
collect_leaves(c, filled_faces, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. } => {}
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => out.push(n),
|
||||
NodeKind::Group { .. } if filled_faces && is_live_paint(n) => {
|
||||
out.extend(faces(n).into_iter().filter(|f| f.visible && !f.appearance.fill_paint().is_none()).map(|f| &**f));
|
||||
out.extend(edges(n).into_iter().filter(|e| e.visible && !e.appearance.stroke_paint().is_none()).map(|e| &**e));
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
||||
for c in children {
|
||||
collect_leaves(c, filled_faces, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filled outline of a path or compound path node.
|
||||
pub fn node_outline(n: &Node) -> Option<(PathData, FillRule)> {
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
let subs = children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect();
|
||||
Some((PathData::new(subs), *rule))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How far (pt) a piece of the planar map may lie from `outline`, the outline it follows, to be
|
||||
/// matched to it or cut out of it: a ten-thousandth of the outline's larger side, at least 0.05.
|
||||
/// The map's pieces follow the outlines to within a few hundredths of a point.
|
||||
pub fn outline_snap(outline: &PathData) -> f64 {
|
||||
outline.bounds().map_or(0.05, |b| (b.width().max(b.height()) / 2000.0).max(0.25) * 0.2)
|
||||
}
|
||||
|
||||
/// The outlines of `leaves` as they are drawn ([`node_outline`]), in their order, for
|
||||
/// [`BuilderMap::stroke_at`]. A leaf without an outline, and one inside a Live Paint group or a
|
||||
/// Shaper Group (which make their own paths), gets an empty outline, so no stroke of it splits.
|
||||
pub fn drawn_outlines(doc: &Document, leaves: &[&Node]) -> Vec<PathData> {
|
||||
let in_live_group = |id: NodeId| {
|
||||
let mut up = doc.parent_of(id);
|
||||
while let Some(p) = up {
|
||||
if doc.node(p).is_some_and(|n| is_live_paint(n) || n.extras().shaper.is_some()) {
|
||||
return true;
|
||||
}
|
||||
up = doc.parent_of(p);
|
||||
}
|
||||
false
|
||||
};
|
||||
leaves.iter().map(|l| if in_live_group(l.id) { None } else { node_outline(l) }.map_or_else(PathData::default, |o| o.0)).collect()
|
||||
}
|
||||
|
||||
/// Pathfinder shapes for the given roots (back → front by the order given), keyed by leaf index,
|
||||
/// with the leaves they came from.
|
||||
pub fn shapes_for<'a>(doc: &'a Document, roots: &[NodeId]) -> (Vec<po::Shape>, Vec<&'a Node>) {
|
||||
shapes_of(doc, roots, false)
|
||||
}
|
||||
|
||||
/// [`shapes_for`]; with `filled_faces`, a Live Paint group contributes only its filled faces and
|
||||
/// its stroked edges.
|
||||
fn shapes_of<'a>(doc: &'a Document, roots: &[NodeId], filled_faces: bool) -> (Vec<po::Shape>, Vec<&'a Node>) {
|
||||
let mut lv = vec![];
|
||||
for id in roots {
|
||||
if let Some(n) = doc.node(*id) {
|
||||
collect_leaves(n, filled_faces, &mut lv);
|
||||
}
|
||||
}
|
||||
let mut shapes = vec![];
|
||||
let mut used = vec![];
|
||||
for l in lv {
|
||||
if let Some((p, r)) = node_outline(l)
|
||||
&& !p.is_empty()
|
||||
&& p.subpaths.iter().all(|s| s.anchors.iter().all(|a| a.p.x.is_finite() && a.p.y.is_finite()))
|
||||
{
|
||||
shapes.push(po::Shape::new(p, r, used.len() as u64));
|
||||
used.push(l);
|
||||
}
|
||||
}
|
||||
(shapes, used)
|
||||
}
|
||||
|
||||
/// Shape Builder inputs: [`shapes_for`], with the open subpaths of filled paths closed where that
|
||||
/// encloses an area (the fill bounds a region, as if an invisible edge closed it) when
|
||||
/// `open_filled_as_closed`. Unfilled open paths and straight lines stay open and cut the regions
|
||||
/// they cross, and so do filled open paths without `open_filled_as_closed`. A Live Paint group
|
||||
/// contributes its filled faces as shapes and its stroked edges as open lines; its unfilled faces
|
||||
/// and its edges without a stroke are left out.
|
||||
pub fn shape_builder_shapes<'a>(doc: &'a Document, roots: &[NodeId], open_filled_as_closed: bool) -> (Vec<po::Shape>, Vec<&'a Node>) {
|
||||
let (mut shapes, leaves) = shapes_of(doc, roots, true);
|
||||
for (s, l) in shapes.iter_mut().zip(&leaves) {
|
||||
if named(l, EDGE_NAME) && doc.parent_of(l.id).and_then(|p| doc.node(p)).is_some_and(is_live_paint) {
|
||||
// A closed edge opens at its first anchor and keeps its closing segment.
|
||||
let start = BTreeSet::from([0]);
|
||||
s.path.subpaths = s.path.subpaths.iter().flat_map(|sp| sp.cut_at(&start)).collect();
|
||||
continue;
|
||||
}
|
||||
if !open_filled_as_closed || l.appearance.fill_paint().is_none() {
|
||||
continue;
|
||||
}
|
||||
for sp in s.path.subpaths.iter_mut().filter(|sp| !sp.closed) {
|
||||
sp.closed = po::encloses_area(sp);
|
||||
}
|
||||
}
|
||||
(shapes, leaves)
|
||||
}
|
||||
|
||||
/// A planar-map face with its outline and bounds, ready for hit tests.
|
||||
pub type Face = (po::Region, BezPath, Rect);
|
||||
|
||||
pub(super) fn faces_of(regions: Vec<po::Region>) -> Vec<Face> {
|
||||
regions
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
let bp = r.path.to_bezpath();
|
||||
let b = r.path.bounds()?;
|
||||
Some((r, bp, b))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The Shape Builder maps built on this thread ([`maps_built`]).
|
||||
pub(super) static MAPS_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
use super::lookup::{Face, MAPS_BUILT, faces_of, outline_snap, shape_builder_shapes};
|
||||
use super::*;
|
||||
|
||||
/// Shape Builder hover hatch colour (neutral grey) and Live Paint highlight red.
|
||||
pub const HATCH: [u8; 3] = [0x80, 0x80, 0x80];
|
||||
|
||||
pub const HIGHLIGHT_RED: [u8; 3] = [0xff, 0x2a, 0x2a];
|
||||
|
||||
/// How many Shape Builder maps ([`BuilderMap::of_shapes`]) this thread has built, for tests that
|
||||
/// count them.
|
||||
pub fn maps_built() -> usize {
|
||||
MAPS_BUILT.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// The Shape Builder's planar map of a selection.
|
||||
#[derive(Default)]
|
||||
pub struct BuilderMap {
|
||||
/// The inputs, back → front (see [`po::shape_builder`]).
|
||||
pub shapes: Vec<po::Shape>,
|
||||
pub regions: Vec<Face>,
|
||||
/// Pieces of the open paths that cut regions (see [`BuilderMap::new`]), keyed by input index,
|
||||
/// with their bounds.
|
||||
pub lines: Vec<(po::Shape, Rect)>,
|
||||
/// Pieces of the other inputs' outlines (cut wherever paths meet), keyed by input index, with
|
||||
/// their bounds: the edges erasing deletes. Only in a map made `with_edges`.
|
||||
pub edges: Vec<(po::Shape, Rect)>,
|
||||
}
|
||||
|
||||
/// What a Shape Builder gesture touches at a point.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Hit {
|
||||
Region(usize),
|
||||
Line(usize),
|
||||
Edge(usize),
|
||||
}
|
||||
|
||||
/// The piece of `pieces` nearest `p` within `tol`, with its distance.
|
||||
fn piece_near(pieces: &[(po::Shape, Rect)], p: Point, tol: f64) -> Option<(f64, usize)> {
|
||||
let mut best: Option<(f64, usize)> = None;
|
||||
for (i, (l, b)) in pieces.iter().enumerate() {
|
||||
if b.inflate(tol, tol).contains(p)
|
||||
&& let Some((.., d)) = l.path.nearest(p)
|
||||
&& d <= tol
|
||||
&& best.is_none_or(|b| d < b.0)
|
||||
{
|
||||
best = Some((d, i));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
impl BuilderMap {
|
||||
/// The map of `roots` (sorted back → front), with the leaves the inputs came from; the outline
|
||||
/// edges only `with_edges`. With `open_filled_as_closed` (Consider Open Filled Paths as
|
||||
/// Closed), a filled open path bounds a region as if an invisible edge closed it; without it,
|
||||
/// the path cuts like a line.
|
||||
pub fn new<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool, open_filled_as_closed: bool) -> (Self, Vec<&'a Node>) {
|
||||
Self::with_gap(doc, roots, with_edges, open_filled_as_closed, 0.0)
|
||||
}
|
||||
|
||||
/// The same arrangement with Gap Detection, using the shared Live Paint gap finder.
|
||||
pub fn with_gap<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool, open_filled_as_closed: bool, gap: f64) -> (Self, Vec<&'a Node>) {
|
||||
let (shapes, leaves) = shape_builder_shapes(doc, roots, open_filled_as_closed);
|
||||
(Self::of_shapes(shapes, with_edges, gap), leaves)
|
||||
}
|
||||
|
||||
/// The map of `shapes` (the inputs, back → front, as [`shape_builder_shapes`] gives them), with
|
||||
/// the outline edges only `with_edges`, closing gaps up to `gap`.
|
||||
pub fn of_shapes(shapes: Vec<po::Shape>, with_edges: bool, gap: f64) -> Self {
|
||||
if shapes.is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
MAPS_BUILT.with(|n| n.set(n.get() + 1));
|
||||
let arr = po::shape_builder_with_gap(&shapes, with_edges, gap);
|
||||
let bounded = |pieces: Vec<po::Shape>| {
|
||||
pieces
|
||||
.into_iter()
|
||||
.filter_map(|l| {
|
||||
let b = l.path.bounds()?;
|
||||
Some((l, b))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Self { shapes, regions: faces_of(arr.regions), lines: bounded(arr.lines), edges: bounded(arr.edges) }
|
||||
}
|
||||
|
||||
/// The region containing `p`.
|
||||
pub fn region_at(&self, p: Point) -> Option<usize> {
|
||||
self.regions.iter().position(|(_, bp, b)| b.contains(p) && bp.winding(p) != 0)
|
||||
}
|
||||
|
||||
/// The regions a rectangular marquee from `from` (where the drag started) to `to` touches: each
|
||||
/// region whose outline crosses the rectangle or has an anchor inside it, and the region the
|
||||
/// whole rectangle lies in. Of those, the region under `from` comes first, then the one under
|
||||
/// `to`, then the others in map order. A marquee of no size touches the region under `from`.
|
||||
pub fn regions_in_marquee(&self, from: Point, to: Point) -> Vec<usize> {
|
||||
let r = Rect::from_points(from, to);
|
||||
if r.width() <= 0.0 && r.height() <= 0.0 {
|
||||
return self.region_at(from).into_iter().collect();
|
||||
}
|
||||
let c = r.center();
|
||||
let touches = |(reg, bp, b): &Face| (b.contains(c) && bp.winding(c) != 0) || vectorcraft_geom::hit::intersects_rect(®.path, r);
|
||||
let mut out: Vec<usize> = self.regions.iter().enumerate().filter(|(_, f)| touches(f)).map(|(i, _)| i).collect();
|
||||
for p in [to, from] {
|
||||
if let Some(k) = self.region_at(p).and_then(|i| out.iter().position(|&j| j == i)) {
|
||||
let i = out.remove(k);
|
||||
out.insert(0, i);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// What a gesture at `p` touches: with `piece_tol` (erasing), the line piece or edge that
|
||||
/// close, else the region under it.
|
||||
pub fn hit(&self, p: Point, piece_tol: Option<f64>) -> Option<Hit> {
|
||||
self.piece_hit(p, piece_tol).or_else(|| self.region_at(p).map(Hit::Region))
|
||||
}
|
||||
|
||||
/// The line piece or edge within `piece_tol` of `p` that [`Self::hit`] gives (None without
|
||||
/// `piece_tol`).
|
||||
fn piece_hit(&self, p: Point, piece_tol: Option<f64>) -> Option<Hit> {
|
||||
piece_tol.and_then(|t| match (piece_near(&self.lines, p, t), piece_near(&self.edges, p, t)) {
|
||||
(Some(l), Some(e)) if e.0 < l.0 => Some(Hit::Edge(e.1)),
|
||||
(Some(l), _) => Some(Hit::Line(l.1)),
|
||||
(None, e) => e.map(|e| Hit::Edge(e.1)),
|
||||
})
|
||||
}
|
||||
|
||||
/// The stroke a click at `p` splits with In Merge Mode, Clicking Stroke Splits the Path: the
|
||||
/// line piece or edge nearest `p` within `tol` (as [`BuilderMap::hit`] finds it), from the
|
||||
/// anchor or crossing point before `p` to the one after it on the path it lies on. `outlines`
|
||||
/// are the inputs' outlines as they are drawn (a filled open path without the edge that closes
|
||||
/// it in the map). Returns the input's index and the run, going the way the path goes; None
|
||||
/// when no piece is that close, when the run is not on the outline as drawn, when it would be a
|
||||
/// whole closed subpath, when it is too short to cut out (see [`po::cut_out`]), or when it is
|
||||
/// all of the path (a lone line) or leaves no more of it than [`outline_snap`], which leaves
|
||||
/// nothing to split off.
|
||||
pub fn stroke_at(&self, outlines: &[PathData], p: Point, tol: f64) -> Option<(usize, SubPath)> {
|
||||
use kurbo::ParamCurve as _;
|
||||
let piece = match self.hit(p, Some(tol))? {
|
||||
Hit::Line(i) => self.lines.get(i),
|
||||
Hit::Edge(i) => self.edges.get(i),
|
||||
Hit::Region(_) => None,
|
||||
}?;
|
||||
let i = piece.0.key as usize;
|
||||
let drawn = outlines.get(i)?;
|
||||
let stops: Vec<Point> = drawn.anchors().map(|(.., a)| a.p).collect();
|
||||
let snap = outline_snap(drawn);
|
||||
let mut run = po::run_around(piece.0.path.subpaths.first()?, &stops, p, snap)?;
|
||||
let n = run.segment_count();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
let mid = run.segment(n / 2).eval(0.5);
|
||||
drawn.nearest(mid).filter(|n| n.4 <= snap)?;
|
||||
// A run that cannot be cut out, or that is all of the path (such as a lone line) or all
|
||||
// but a speck of it, leaves nothing to split off.
|
||||
if !po::cut_out(drawn, &run, false, snap).is_some_and(|rest| rest.length() > snap) {
|
||||
return None;
|
||||
}
|
||||
// Reverse the run when the path's parameter goes down along it.
|
||||
let seg = run.segment(n / 2);
|
||||
if let (Some(a), Some(b)) = (drawn.nearest(seg.eval(0.25)), drawn.nearest(seg.eval(0.75)))
|
||||
&& a.0 == b.0
|
||||
&& (b.1 as f64 + b.2) < (a.1 as f64 + a.2)
|
||||
{
|
||||
run.reverse();
|
||||
}
|
||||
Some((i, run))
|
||||
}
|
||||
|
||||
/// What a Shape Builder gesture at `p` touches: [`Self::hit`], except that while erasing, a
|
||||
/// point in a region that is no more than `2 * piece_tol` across there (see
|
||||
/// [`Self::narrow_at`]) touches the region and not the line piece or edge beside it.
|
||||
pub fn builder_hit(&self, p: Point, piece_tol: Option<f64>) -> Option<Hit> {
|
||||
match (piece_tol, self.piece_hit(p, piece_tol)) {
|
||||
(Some(tol), Some(piece)) => Some(match self.region_at(p) {
|
||||
Some(r) if self.narrow_at(r, p, tol) => Hit::Region(r),
|
||||
_ => piece,
|
||||
}),
|
||||
(_, piece) => piece.or_else(|| self.region_at(p).map(Hit::Region)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is region `r` no more than `2 * tol` across at `p`, measured from a point of its outline
|
||||
/// within `tol` of `p`, through `p`, to its outline on the other side? Every point on that line
|
||||
/// is then within `tol` of the outline. The points tried are the point of the outline nearest
|
||||
/// `p` and each point of a segment where the segment is square to the line to `p`, so a
|
||||
/// point near the end of a thin bar measures across the bar from its sides as well as along it
|
||||
/// from the end, and splitting a segment in two does not change the answer. False when `p` is
|
||||
/// on the outline or more than `tol` from it.
|
||||
fn narrow_at(&self, r: usize, p: Point, tol: f64) -> bool {
|
||||
use kurbo::{ParamCurve as _, ParamCurveNearest as _};
|
||||
let Some((_, bp, _)) = self.regions.get(r) else { return false };
|
||||
// Each segment with the parameter, point and distance of its point nearest `p`.
|
||||
let near: Vec<(kurbo::PathSeg, f64, Point, f64)> = bp
|
||||
.segments()
|
||||
.map(|seg| {
|
||||
let n = seg.nearest(p, 1e-6);
|
||||
(seg, n.t, seg.eval(n.t), n.distance_sq.sqrt())
|
||||
})
|
||||
.collect();
|
||||
if !near.iter().all(|&(.., d)| d.is_finite() && d > 1e-9) {
|
||||
return false;
|
||||
}
|
||||
let closest = near.iter().map(|&(.., d)| d).fold(f64::INFINITY, f64::min);
|
||||
// A line from `p` no longer than `2 * tol` only meets segments that close to `p`.
|
||||
let reach: Vec<kurbo::PathSeg> = near.iter().filter(|&&(.., d)| d <= 2.0 * tol).map(|&(seg, ..)| seg).collect();
|
||||
// A segment's end is its nearest point when the segment turns away from `p` or stops
|
||||
// short of the point square to it; there, only the nearest point of all the outline and
|
||||
// an end where the segment is square to the line to `p` count.
|
||||
let square = |seg: kurbo::PathSeg, t: f64, n: Point| {
|
||||
use kurbo::ParamCurveDeriv as _;
|
||||
let dir = match seg {
|
||||
kurbo::PathSeg::Line(l) => l.p1 - l.p0,
|
||||
kurbo::PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(),
|
||||
kurbo::PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(),
|
||||
};
|
||||
let v = p - n;
|
||||
dir.hypot() > 1e-12 && dir.dot(v).abs() <= 1e-6 * dir.hypot() * v.hypot()
|
||||
};
|
||||
let from =
|
||||
|&&(seg, t, n, d): &&(kurbo::PathSeg, f64, Point, f64)| d <= tol && (d == closest || (t > 1e-9 && t < 1.0 - 1e-9) || square(seg, t, n));
|
||||
near.iter().filter(from).any(|&(_, _, n, d)| {
|
||||
let line = kurbo::Line::new(p, p + (p - n) * ((2.0 * tol - d) / d));
|
||||
reach.iter().any(|seg| seg.intersect_line(line).iter().any(|x| x.line_t > 1e-9 && x.line_t <= 1.0))
|
||||
})
|
||||
}
|
||||
|
||||
/// What a gesture along the polyline `pts` touches, in order: [`Self::hit`] at each vertex and
|
||||
/// at the points [`Self::probes`] picks between them, so it touches every region it crosses,
|
||||
/// however narrow.
|
||||
pub fn hits_along(&self, pts: &[Point], piece_tol: Option<f64>) -> Touched {
|
||||
self.along(pts, piece_tol, Self::hit)
|
||||
}
|
||||
|
||||
/// What a Shape Builder gesture along the polyline `pts` touches: [`Self::hits_along`] with
|
||||
/// [`Self::builder_hit`], so an erasing gesture also touches each region it goes into where the
|
||||
/// region is no more than `2 * piece_tol` across.
|
||||
pub fn builder_hits_along(&self, pts: &[Point], piece_tol: Option<f64>) -> Touched {
|
||||
self.along(pts, piece_tol, Self::builder_hit)
|
||||
}
|
||||
|
||||
fn along(&self, pts: &[Point], piece_tol: Option<f64>, hit: fn(&Self, Point, Option<f64>) -> Option<Hit>) -> Touched {
|
||||
let mut touched = Touched::default();
|
||||
let mut last: Option<Point> = None;
|
||||
for &p in pts {
|
||||
if let Some(a) = last {
|
||||
for q in self.probes(a, p, piece_tol) {
|
||||
touched.add(hit(self, q, piece_tol));
|
||||
}
|
||||
}
|
||||
touched.add(hit(self, p, piece_tol));
|
||||
last = Some(p);
|
||||
}
|
||||
touched
|
||||
}
|
||||
|
||||
/// The points strictly between `a` and `b`, in order, that [`Self::hits_along`] tests: the
|
||||
/// middle of each stretch between two places where the segment crosses a region's outline, and
|
||||
/// with `piece_tol` (erasing) those crossings, the places where it crosses a line piece or an
|
||||
/// edge, and points `piece_tol / 2` apart (at most 4,000 per segment), so the line pieces and
|
||||
/// edges the segment crosses or passes close to are touched too.
|
||||
fn probes(&self, a: Point, b: Point, piece_tol: Option<f64>) -> Vec<Point> {
|
||||
let len = a.distance(b);
|
||||
if !(len.is_finite() && len > 0.0) {
|
||||
return vec![];
|
||||
}
|
||||
let line = kurbo::Line::new(a, b);
|
||||
let lb = Rect::from_points(a, b);
|
||||
let mut cross: Vec<f64> = vec![];
|
||||
for (_, bp, rb) in &self.regions {
|
||||
if rb.x0 > lb.x1 || rb.x1 < lb.x0 || rb.y0 > lb.y1 || rb.y1 < lb.y0 {
|
||||
continue;
|
||||
}
|
||||
for seg in bp.segments() {
|
||||
cross.extend(seg.intersect_line(line).iter().map(|x| x.line_t));
|
||||
}
|
||||
}
|
||||
cross.retain(|t| t.is_finite() && *t > 0.0 && *t < 1.0);
|
||||
cross.sort_by(f64::total_cmp);
|
||||
// Crossings closer than this along the segment are one place.
|
||||
let gap = 1e-9 / len;
|
||||
let mut ts: Vec<f64> = vec![];
|
||||
let mut from = 0.0;
|
||||
for &t in cross.iter().chain([1.0].iter()) {
|
||||
if t - from > gap {
|
||||
ts.push((from + t) / 2.0);
|
||||
from = t;
|
||||
}
|
||||
}
|
||||
if let Some(tol) = piece_tol {
|
||||
ts.extend(cross.iter().copied());
|
||||
for (piece, pb) in self.lines.iter().chain(&self.edges) {
|
||||
if pb.x0 > lb.x1 || pb.x1 < lb.x0 || pb.y0 > lb.y1 || pb.y1 < lb.y0 {
|
||||
continue;
|
||||
}
|
||||
for seg in piece.path.to_bezpath().segments() {
|
||||
ts.extend(seg.intersect_line(line).iter().map(|x| x.line_t).filter(|t| t.is_finite() && *t > 0.0 && *t < 1.0));
|
||||
}
|
||||
}
|
||||
let n = ((len / (tol / 2.0).max(1e-3)).ceil() as usize).min(4000);
|
||||
ts.extend((1..n).map(|k| k as f64 / n as f64));
|
||||
ts.sort_by(f64::total_cmp);
|
||||
}
|
||||
ts.into_iter().map(|t| a.lerp(b, t)).collect()
|
||||
}
|
||||
|
||||
/// The regions just either side of the middle of `piece` (None: outside every region).
|
||||
pub fn sides(&self, piece: &po::Shape) -> [Option<usize>; 2] {
|
||||
let Some(sp) = piece.path.subpaths.first() else { return [None; 2] };
|
||||
let n = sp.segment_count();
|
||||
if n == 0 {
|
||||
return [None; 2];
|
||||
}
|
||||
let c = sp.segment(n / 2);
|
||||
use kurbo::{ParamCurve as _, ParamCurveDeriv as _};
|
||||
let (m, d) = (c.eval(0.5), c.deriv().eval(0.5).to_vec2());
|
||||
let len = d.hypot();
|
||||
if !(len.is_finite() && len > 0.0) {
|
||||
return [None; 2];
|
||||
}
|
||||
// A little to each side: inside the faces next to it, which share it exactly.
|
||||
let off = d.turn_90() * (0.05f64.min(0.05 * c.p0.distance(c.p3)).max(1e-6) / len);
|
||||
[m + off, m - off].map(|q| self.region_at(q))
|
||||
}
|
||||
|
||||
/// Does line piece `i` run between two different regions of `regions` (an edge that merging or
|
||||
/// deleting them removes)? A piece with the same region on both sides, such as the end of a
|
||||
/// line inside a shape, bounds nothing and stays.
|
||||
pub fn line_between(&self, i: usize, regions: &[usize]) -> bool {
|
||||
self.lines.get(i).is_some_and(|(l, _)| matches!(self.sides(l), [Some(a), Some(b)] if a != b && regions.contains(&a) && regions.contains(&b)))
|
||||
}
|
||||
|
||||
/// The edges `touched` deletes: those that bound none of its regions (deleting a region takes
|
||||
/// its edges with it, so an erase drag that crosses an edge into a region deletes the region).
|
||||
pub fn erased_edges(&self, touched: &Touched) -> Vec<usize> {
|
||||
let bounds_one = |(e, _): &(po::Shape, Rect)| self.sides(e).iter().any(|r| r.is_some_and(|r| touched.regions.contains(&r)));
|
||||
touched.edges.iter().copied().filter(|&k| self.edges.get(k).is_some_and(|e| !bounds_one(e))).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The regions, line pieces and edges a Shape Builder gesture has touched, in the order touched.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Touched {
|
||||
pub regions: Vec<usize>,
|
||||
pub lines: Vec<usize>,
|
||||
pub edges: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Touched {
|
||||
pub fn add(&mut self, hit: Option<Hit>) {
|
||||
let (list, i) = match hit {
|
||||
Some(Hit::Region(i)) => (&mut self.regions, i),
|
||||
Some(Hit::Line(i)) => (&mut self.lines, i),
|
||||
Some(Hit::Edge(i)) => (&mut self.edges, i),
|
||||
None => return,
|
||||
};
|
||||
if !list.contains(&i) {
|
||||
list.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds what `other` touched, in its order.
|
||||
pub fn extend(&mut self, other: &Touched) {
|
||||
let regions = other.regions.iter().map(|&i| Hit::Region(i));
|
||||
for hit in regions.chain(other.lines.iter().map(|&i| Hit::Line(i))).chain(other.edges.iter().map(|&i| Hit::Edge(i))) {
|
||||
self.add(Some(hit));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.regions.is_empty() && self.lines.is_empty() && self.edges.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected roots sorted back → front in document order.
|
||||
pub fn sorted_roots(doc: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
let mut v: Vec<(Vec<usize>, NodeId)> = ids.iter().filter_map(|id| doc.index_path(*id).map(|p| (p, *id))).collect();
|
||||
v.sort();
|
||||
v.dedup_by(|a, b| a.1 == b.1);
|
||||
v.into_iter().map(|(_, id)| id).collect()
|
||||
}
|
||||
|
||||
/// Sample points along a polyline every `step` units (always including the vertices).
|
||||
pub fn sample_polyline(pts: &[Point], step: f64) -> Vec<Point> {
|
||||
let mut out = vec![];
|
||||
let step = step.max(1e-3);
|
||||
for (i, &p) in pts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
let a = pts[i - 1];
|
||||
let n = ((a.distance(p) / step).ceil() as usize).min(4000);
|
||||
for k in 1..n {
|
||||
out.push(a.lerp(p, k as f64 / n as f64));
|
||||
}
|
||||
}
|
||||
out.push(p);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Diagonal hatch lines filling `path` (the Shape Builder's grey mesh highlight), `gap` apart.
|
||||
pub fn hatch(path: &BezPath, gap: f64) -> BezPath {
|
||||
let mut out = BezPath::new();
|
||||
let b = path.bounding_box();
|
||||
if b.width() <= 0.0 || b.height() <= 0.0 {
|
||||
return out;
|
||||
}
|
||||
// Rotate by -45° so the hatch lines become horizontal scanlines.
|
||||
let s = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let rot = |p: Point| Point::new((p.x + p.y) * s, (p.y - p.x) * s);
|
||||
let unrot = |p: Point| Point::new((p.x - p.y) * s, (p.x + p.y) * s);
|
||||
let mut edges: Vec<(Point, Point)> = vec![];
|
||||
let mut first = Point::ZERO;
|
||||
let mut last = Point::ZERO;
|
||||
kurbo::flatten(path.iter(), gap.max(1e-3) * 0.1, |el| match el {
|
||||
kurbo::PathEl::MoveTo(p) => {
|
||||
if last != first {
|
||||
edges.push((rot(last), rot(first)));
|
||||
}
|
||||
first = p;
|
||||
last = p;
|
||||
}
|
||||
kurbo::PathEl::LineTo(p) => {
|
||||
edges.push((rot(last), rot(p)));
|
||||
last = p;
|
||||
}
|
||||
kurbo::PathEl::ClosePath => {
|
||||
if last != first {
|
||||
edges.push((rot(last), rot(first)));
|
||||
}
|
||||
last = first;
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
if last != first {
|
||||
edges.push((rot(last), rot(first)));
|
||||
}
|
||||
let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for (a, c) in &edges {
|
||||
y0 = y0.min(a.y.min(c.y));
|
||||
y1 = y1.max(a.y.max(c.y));
|
||||
}
|
||||
let gap = gap.max((y1 - y0) / 400.0).max(1e-6);
|
||||
let mut y = (y0 / gap).floor() * gap + gap * 0.5;
|
||||
let mut xs: Vec<f64> = vec![];
|
||||
while y < y1 {
|
||||
xs.clear();
|
||||
for (a, c) in &edges {
|
||||
if (a.y <= y) != (c.y <= y) {
|
||||
xs.push(a.x + (y - a.y) / (c.y - a.y) * (c.x - a.x));
|
||||
}
|
||||
}
|
||||
xs.sort_by(f64::total_cmp);
|
||||
for pair in xs.as_chunks::<2>().0 {
|
||||
out.move_to(unrot(Point::new(pair[0], y)));
|
||||
out.line_to(unrot(Point::new(pair[1], y)));
|
||||
}
|
||||
y += gap;
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user