mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-14 14:12:44 +00:00
fix(type): refuse area paths over 4096 anchors as too many anchor points
type.fillPath accepted up to 100,000 anchors, but each curved segment flattens to 16 edges against a 65,536-edge cap, so a big curved path failed with the misleading "the path encloses no area". The anchor limit now lives in photocraft_text::area as MAX_AREA_KNOTS (4096, the Type tool's existing click cap) and the edge cap is derived from it (4096 x 17), so every path within the limit fits even with every segment curved, and a bigger one gets "the path has too many anchor points". The Type tool's click cap now names the same constant, so a path it offers as an area target can always be filled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7040fdc233
commit
da1764082e
4 files changed
+43
-5
No files matched your search
@@ -13,8 +13,9 @@ use crate::{EngineError, Result, Session};
|
||||
|
||||
const CMD: &str = "type.fillPath";
|
||||
|
||||
/// Largest total anchor count of a path area type can fill (as for type on a path).
|
||||
const MAX_AREA_KNOTS: usize = 100_000;
|
||||
// Largest total anchor count of a path area type can fill. Within it every path fits the area
|
||||
// edge cap, so a big curved path gets "too many anchor points" rather than "encloses no area".
|
||||
use photocraft_text::area::MAX_AREA_KNOTS;
|
||||
|
||||
fn bad(m: &str) -> EngineError {
|
||||
EngineError::BadParams { cmd: CMD.into(), msg: m.into() }
|
||||
|
||||
@@ -166,3 +166,34 @@ fn area_type_saves_to_psd_as_paragraph_text_with_a_warning() {
|
||||
assert!(matches!(t.shape, TextShape::Box { width, height, .. } if (width - 200.0).abs() < 0.5 && (height - 150.0).abs() < 0.5), "{:?}", t.shape);
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_anchors_is_its_own_error() {
|
||||
// A ring of `n` smooth (curved) anchors around (200, 150).
|
||||
let ring = |n: usize| {
|
||||
let p = |a: f64, r: f64| photocraft_geom::Point::new(200.0 + r * a.cos(), 150.0 + r * a.sin());
|
||||
let step = std::f64::consts::TAU / n as f64;
|
||||
let knots = (0..n)
|
||||
.map(|i| {
|
||||
let a = i as f64 * step;
|
||||
let (anchor, d) = (p(a, 100.0), 100.0 * step / 3.0);
|
||||
let (tx, ty) = (-a.sin() * d, a.cos() * d);
|
||||
let at = |k: f64| photocraft_geom::Point::new(anchor.x + k * tx, anchor.y + k * ty);
|
||||
photocraft_doc::vector::Knot::smooth(anchor, at(-1.0), at(1.0))
|
||||
})
|
||||
.collect();
|
||||
Path::new(vec![Subpath { closed: true, knots, op: PathOp::Combine }])
|
||||
};
|
||||
let mut s = session(8);
|
||||
let id = text_layer(&mut s, WORDS);
|
||||
// Every anchor up to the limit fits, even with every segment curved.
|
||||
set_work_path(&mut s, ring(MAX_AREA_KNOTS));
|
||||
s.execute("type.fillPath", json!({})).unwrap();
|
||||
assert!(matches!(text(&s, id).shape, TextShape::Area { .. }));
|
||||
// One more is refused for what it is, not as a path that encloses nothing.
|
||||
let id = text_layer(&mut s, WORDS);
|
||||
set_work_path(&mut s, ring(MAX_AREA_KNOTS + 1));
|
||||
let e = s.execute("type.fillPath", json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("too many anchor points"), "{e}");
|
||||
assert_eq!(text(&s, id).shape, TextShape::Point);
|
||||
}
|
||||
@@ -12,8 +12,13 @@ use photocraft_geom::Point;
|
||||
|
||||
/// Pieces each curved segment is cut into (about 0.1 px off a 300 px bubble's curve).
|
||||
const CURVE_PIECES: usize = 16;
|
||||
/// Most anchors an area path can have: the most `type.fillPath` accepts and a Type tool click
|
||||
/// targets (`path_target` in ui-egui).
|
||||
pub const MAX_AREA_KNOTS: usize = 4096;
|
||||
/// Largest number of edges an area keeps, so a hostile path can't make every line scan millions.
|
||||
pub const MAX_AREA_EDGES: usize = 65_536;
|
||||
/// A closed subpath of `n` anchors gives at most `CURVE_PIECES * n + 1` edges, so every path
|
||||
/// within [`MAX_AREA_KNOTS`] fits, even with every segment curved.
|
||||
pub const MAX_AREA_EDGES: usize = MAX_AREA_KNOTS * (CURVE_PIECES + 1);
|
||||
/// Most shape components an area keeps: every row folds each one in, so a path of thousands of
|
||||
/// tiny shapes can't make each line cost thousands of interval merges.
|
||||
pub const MAX_AREA_COMPONENTS: usize = 4096;
|
||||
|
||||
@@ -342,8 +342,9 @@ pub(crate) fn path_target(app: &PhotocraftApp, x: f64, y: f64) -> Option<PathCli
|
||||
};
|
||||
// The canvas hover runs this every frame and `hit` samples every segment, so a path with more
|
||||
// anchors than this is not a click target (the click and the pointer agree: plain point
|
||||
// text). 4096 anchors is far beyond hand-drawn paths and shapes yet keeps a frame cheap.
|
||||
const MAX_TARGET_KNOTS: usize = 4096;
|
||||
// text). 4096 anchors is far beyond hand-drawn paths and shapes yet keeps a frame cheap, and
|
||||
// it is the most `type.fillPath` accepts, so every area click target can be filled.
|
||||
const MAX_TARGET_KNOTS: usize = photocraft_text::area::MAX_AREA_KNOTS;
|
||||
if path.subpaths.iter().map(|s| s.knots.len()).fold(0usize, usize::saturating_add) > MAX_TARGET_KNOTS {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user