mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Integration: a hidden Work Path is no area type target (#6230)
After merging main, the Type tool's path target fell back to the Work Path over its whole inside, while main's #6551 only draws an unselected Work Path for path tools. Ctrl-click Load as Selection (or deselecting the Work Path) and a Type tool click inside it started area type in a path nobody could see. Now the Work Path's inside counts only while it is selected in Paths (drawn), its outline still takes type on a path with main's 6 px band, and a missed shape path falls back to the Work Path as on main. an_unselected_work_path_is_not_an_area_target fails without this. Co-authored-by: SillySilk <31193727+SillySilk@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
SillySilk
Claude Opus 5.5
parent
a7b833caf6
commit
f895f844e7
@@ -405,45 +405,55 @@ fn arc_length_to(sub: &photocraft_doc::Subpath, seg: usize, t: f64) -> f64 {
|
||||
whole + segs.get(seg).map_or(0.0, |s| length(s, t.clamp(0.0, 1.0)))
|
||||
}
|
||||
|
||||
/// The path a Type tool click at document `(x, y)` would use: the active shape layer's path,
|
||||
/// else the work path (the path Photoshop shows), within 4 screen px of its outline or inside it.
|
||||
/// None while View › Show › Target Path (or Extras) hides the path, as the canvas then draws none.
|
||||
/// The path a Type tool click at document `(x, y)` would use: on the outline (within 6 screen px)
|
||||
/// of the active shape layer's path, else of the work path, type on that path; inside a closed
|
||||
/// one, area type. The work path's inside counts only while the canvas draws it (selected in the
|
||||
/// Paths panel, #6551): a hidden Work Path left after Load as Selection must not turn a plain
|
||||
/// click into area type. None while View › Show › Target Path (or Extras) hides paths.
|
||||
pub(crate) fn path_target(app: &PhotocraftApp, x: f64, y: f64) -> Option<PathClick> {
|
||||
use photocraft_vector::edit::{Hit, hit};
|
||||
if !app.ui.view.shows(app.ui.view.show.target_path) {
|
||||
return None;
|
||||
}
|
||||
let (shape, path) = match crate::vector_ui::active_shape_path(app) {
|
||||
Some((id, p)) => (Some(id), p),
|
||||
None => (None, app.session.active()?.doc.work_path.clone()?),
|
||||
};
|
||||
let shape_path = crate::vector_ui::active_shape_path(app);
|
||||
let work = app.session.active().and_then(|s| s.doc.work_path.clone());
|
||||
let work_shown = app.ui.selected_path.as_deref() == Some("work");
|
||||
// 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, 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;
|
||||
}
|
||||
let tol = 4.0 / app.point_zoom().max(0.01) as f64;
|
||||
let small = |path: &photocraft_doc::vector::Path| path.subpaths.iter().map(|s| s.knots.len()).fold(0usize, usize::saturating_add) <= MAX_TARGET_KNOTS;
|
||||
let tol = 6.0 / app.point_zoom().max(0.01) as f64;
|
||||
// Only the horizontal Type tool starts type on a path: vertical type on a path is not laid
|
||||
// out yet.
|
||||
let vertical = app.ui.tool.is_vertical_type();
|
||||
let on_path = |sp: usize, k: usize, t: f64| {
|
||||
// Type on a path needs a segment to run along (`type.attachToPath` rejects one anchor).
|
||||
let sub = path.subpaths.get(sp).filter(|s| !vertical && s.knots.len() >= 2)?;
|
||||
Some(PathClick::OnPath { shape, subpath: sp, start: arc_length_to(sub, k, t) })
|
||||
let outline = |path: &photocraft_doc::vector::Path, shape: Option<u64>| {
|
||||
let on_path = |sp: usize, k: usize, t: f64| {
|
||||
// Type on a path needs a segment to run along (`type.attachToPath` rejects one anchor).
|
||||
let sub = path.subpaths.get(sp).filter(|s| !vertical && s.knots.len() >= 2)?;
|
||||
Some(PathClick::OnPath { shape, subpath: sp, start: arc_length_to(sub, k, t) })
|
||||
};
|
||||
match hit(path, Point::new(x, y), tol, &[]) {
|
||||
Some(Hit::Segment([sp, k], t)) => on_path(sp, k, t),
|
||||
Some(Hit::Anchor([sp, k])) => on_path(sp, k, 0.0),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let outline = match hit(&path, Point::new(x, y), tol, &[]) {
|
||||
Some(Hit::Segment([sp, k], t)) => on_path(sp, k, t),
|
||||
Some(Hit::Anchor([sp, k])) => on_path(sp, k, 0.0),
|
||||
_ => None,
|
||||
};
|
||||
if outline.is_some() {
|
||||
return outline;
|
||||
let inside = |path: &photocraft_doc::vector::Path| photocraft_text::area::AreaShape::new(path, false).is_some_and(|a| a.contains(x as f32, y as f32));
|
||||
if let Some((id, p)) = shape_path.as_ref().filter(|(_, p)| small(p)) {
|
||||
if let Some(c) = outline(p, Some(*id)) {
|
||||
return Some(c);
|
||||
}
|
||||
if inside(p) {
|
||||
return Some(PathClick::Area { shape: Some(*id) });
|
||||
}
|
||||
}
|
||||
let area = photocraft_text::area::AreaShape::new(&path, false)?;
|
||||
area.contains(x as f32, y as f32).then_some(PathClick::Area { shape })
|
||||
let w = work.as_ref().filter(|w| small(w))?;
|
||||
if let Some(c) = outline(w, None) {
|
||||
return Some(c);
|
||||
}
|
||||
(work_shown && inside(w)).then_some(PathClick::Area { shape: None })
|
||||
}
|
||||
|
||||
/// What the Type tool's pointer over document `(x, y)` shows (#218): the path click a press there
|
||||
|
||||
@@ -2913,6 +2913,7 @@ fn paths_with_too_many_anchors_are_not_hover_targets() {
|
||||
let mut app = new_app();
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).work_path =
|
||||
Some(photocraft_doc::Path::new(vec![photocraft_doc::Subpath::polygon(&ring(4000))]));
|
||||
app.ui.selected_path = Some("work".into());
|
||||
assert!(matches!(path_target(&app, 500.0, 400.0), Some(PathClick::Area { shape: None })));
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).work_path =
|
||||
Some(photocraft_doc::Path::new(vec![photocraft_doc::Subpath::polygon(&ring(5000))]));
|
||||
@@ -2925,6 +2926,7 @@ fn hidden_target_path_is_not_a_click_target() {
|
||||
let mut app = new_app();
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).work_path =
|
||||
Some(photocraft_doc::Path::new(vec![photocraft_doc::Subpath::polygon(&[(300.0, 300.0), (700.0, 300.0), (700.0, 500.0), (300.0, 500.0)])]));
|
||||
app.ui.selected_path = Some("work".into());
|
||||
assert!(matches!(path_target(&app, 500.0, 400.0), Some(PathClick::Area { shape: None })));
|
||||
app.ui.view.show.target_path = false;
|
||||
assert!(path_target(&app, 500.0, 400.0).is_none(), "Target Path off");
|
||||
@@ -2933,6 +2935,25 @@ fn hidden_target_path_is_not_a_click_target() {
|
||||
assert!(path_target(&app, 500.0, 400.0).is_none(), "Extras off");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unselected_work_path_is_not_an_area_target() {
|
||||
// The canvas draws a Work Path only while it is selected in Paths (or a path tool is active,
|
||||
// #6551). One left unselected, as after Ctrl-click Load as Selection, is invisible with the
|
||||
// Type tool, so a click inside it is plain point text; its outline still takes type on a
|
||||
// path, as before area type (#5180).
|
||||
let mut app = new_app();
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).work_path =
|
||||
Some(photocraft_doc::Path::new(vec![photocraft_doc::Subpath::polygon(&[(300.0, 300.0), (700.0, 300.0), (700.0, 500.0), (300.0, 500.0)])]));
|
||||
app.ui.selected_path = None;
|
||||
assert!(path_target(&app, 500.0, 400.0).is_none(), "inside a hidden work path");
|
||||
assert!(matches!(path_target(&app, 500.0, 301.0), Some(PathClick::OnPath { shape: None, .. })));
|
||||
click_at(&mut app, 500.0, 400.0);
|
||||
let id = LayerId(app.ui.text_edit.as_ref().expect("editing").layer);
|
||||
assert_eq!(text_layer(&app.session.active().unwrap().doc, id).unwrap().shape, photocraft_doc::text::TextShape::Point);
|
||||
app.ui.selected_path = Some("work".into());
|
||||
assert!(matches!(path_target(&app, 500.0, 400.0), Some(PathClick::Area { shape: None })));
|
||||
}
|
||||
|
||||
/// Area type is paragraph text too: the justify buttons work on it (#7713 greys them on point text).
|
||||
#[test]
|
||||
fn justify_buttons_work_on_area_type() {
|
||||
|
||||
Reference in New Issue
Block a user