diff --git a/crates/doc/src/text/object.rs b/crates/doc/src/text/object.rs index 12bc7ec89..bb1fa0eb2 100644 --- a/crates/doc/src/text/object.rs +++ b/crates/doc/src/text/object.rs @@ -367,6 +367,24 @@ impl TextObject { pub fn move_path_anchors(&mut self, refs: &[(usize, usize)], d: Vec2) -> bool { matches!(self.kind, TextKind::OnPath { .. }) && self.reshape_local(|path, to_text| move_anchors(path, to_text, refs, d)) } + /// Move the out (`out`) or in direction handle of anchor `ai` of subpath `si` of type on a + /// path's path to `pos` in document space, as a path's anchor takes it + /// ([`vectorcraft_geom::Anchor::set_handle`]; `independent`: that side only, the anchor a + /// corner): a Direct Selection drag of a handle (#3565). The handle is set in document space, + /// so a smooth anchor keeps its other handle's length as drawn; the brackets keep their + /// fractions of the path. False, changing nothing, for other type, when the anchor doesn't + /// exist, a degenerate `xf` or a path that would not be finite. + pub fn set_path_handle(&mut self, (si, ai): (usize, usize), out: bool, pos: Point, independent: bool) -> bool { + let xf = self.xf; + matches!(self.kind, TextKind::OnPath { .. }) + && self.reshape_local(|path, to_text| { + let Some(a) = path.anchor_mut(si, ai) else { return false }; + let mut drawn = a.transform(xf); + drawn.set_handle(out, pos, independent); + *a = drawn.transform(to_text); + true + }) + } /// Edit a copy of the area frame with `f` (given the document → text space map) and keep it /// when `f` succeeds and the result is finite. fn reshape_area(&mut self, f: impl FnOnce(&mut PathData, Affine) -> bool) -> bool { diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index a4fe33762..2257b1a8a 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -115,6 +115,15 @@ pub fn specs() -> Vec { has_doc, reshape_path ), + cmd!( + "text.setPathHandle", + "Move Type Path Handle", + [], + None, + "{id, subpath?, anchor, which: \"in\"|\"out\", x, y, independent?: bool} move a direction handle of an anchor of type on a path's path to (x, y), as path.setHandle moves a path's (independent: that side only, the anchor becomes a corner); the start and end brackets keep their fractions of the path and the text reflows along it (what Direct Selection does dragging a handle of the path)", + has_doc, + set_path_handle + ), cmd!( "text.fitHeadline", "Fit Headline", @@ -1268,6 +1277,35 @@ fn reshape_path(s: &mut Session, p: &Value) -> Result { ok() } +fn set_path_handle(s: &mut Session, p: &Value) -> Result { + const C: &str = "text.setPathHandle"; + let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?; + let si = super::draw2::usize_opt(p, "subpath", C)?.unwrap_or(0); + let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing anchor"))?; + let out = str_param(p, "which") != Some("in"); + let pos = vectorcraft_geom::Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?); + let independent = switch_or(p, "independent", false, C)?; + let node = s.doc()?.doc.node(id); + let on_path = matches!(node.map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, vectorcraft_doc::TextKind::OnPath { .. })); + if !on_path || node.is_some_and(|n| n.extras().perspective.is_some()) { + return Err(bad(C, "not type on a path outside perspective")); + } + s.edit("Reshape Type Path", |doc, _| { + let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(id)) }; + let before = t.kind.clone(); + let within = |t: &TextObject| { + t.type_path().and_then(|p| p.bounds()).is_some_and(|b| [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.abs() <= crate::MAX_COORD)) + }; + if !(t.set_path_handle((si, ai), out, pos, independent) && within(t)) { + t.kind = before; + return Err(bad(C, "no such path anchor, or the path would reach past the canvas")); + } + refresh_bounds(t); + Ok(()) + })?; + ok() +} + #[cfg(test)] mod area_tests; diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs index 88ccb36da..571c9c346 100644 --- a/crates/engine/src/cmd/typecmd/area_tests.rs +++ b/crates/engine/src/cmd/typecmd/area_tests.rs @@ -2081,3 +2081,35 @@ fn fit_min_percent_for_several_frames_keeps_each_frames_fit() { assert_eq!(fit(&s, plain), vectorcraft_doc::AreaFit::None, "the plain frame doesn't start shrinking"); assert_eq!(fit(&s, shrink), vectorcraft_doc::AreaFit::ShrinkText { min_percent: 30.0 }); } + +/// #3565: `text.setPathHandle` moves a direction handle of type on a path's path, in document +/// space, as `path.setHandle` moves a path's: a smooth anchor's other handle turns with it at +/// its length, `independent` leaves it and makes the anchor a corner; one undo step. +#[test] +fn set_path_handle_reshapes_type_on_a_path() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap(); + let path = + s.execute("path.create", &json!({"anchors": [{"x": 100, "y": 300, "in": [50, 300], "out": [150, 300]}, {"x": 400, "y": 300}]})).unwrap(); + let text = s.execute("text.createInPath", &json!({"path": path["id"], "mode": "onPath", "text": "Along the curve"})).unwrap(); + let id = NodeId(text["id"].as_u64().unwrap()); + let first = |s: &Session| match &s.doc().unwrap().doc.node(id).unwrap().kind { + NodeKind::Text(t) => *t.type_path().unwrap().subpaths[0].anchors.first().unwrap(), + _ => panic!("not text"), + }; + let smooth = first(&s).kind == vectorcraft_geom::AnchorKind::Smooth; + s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 0, "which": "out", "x": 100, "y": 250})).unwrap(); + let a = first(&s); + assert!((a.h_out.x - 100.0).abs() < 1e-6 && (a.h_out.y - 250.0).abs() < 1e-6, "{a:?}"); + if smooth { + assert!((a.h_in.x - 100.0).abs() < 1e-6 && (a.h_in.y - 350.0).abs() < 1e-6, "smooth: the in handle turns with it at its length: {a:?}"); + } + s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 0, "which": "in", "x": 60, "y": 300, "independent": true})).unwrap(); + let b = first(&s); + assert_eq!(b.h_out, a.h_out, "independent: the other handle stays"); + assert_eq!(b.kind, vectorcraft_geom::AnchorKind::Corner); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(first(&s), a, "one undo step each"); + assert!(s.execute("text.setPathHandle", &json!({"id": id.0, "anchor": 9, "which": "out", "x": 0, "y": 0})).is_err(), "no such anchor"); + assert!(s.execute("text.setPathHandle", &json!({"id": path["id"], "anchor": 0, "which": "out", "x": 0, "y": 0})).is_err(), "not type"); +} diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 833f48225..1477cd5bb 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -557,7 +557,7 @@ fn hit_frame_edge(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Vec Vec<(NodeId, usize, usize, Anchor)> { let mut v = vec![]; for &id in &cx.selection.objects { - let Some(pd) = cx.doc.node(id).and_then(|n| n.path_data()) else { continue }; + let Some(pd) = cx.doc.node(id).and_then(handle_path) else { continue }; let shown = cx.selection.partial(id); v.extend(pd.anchors().filter(|(si, ai, _)| shown.is_none_or(|set| set.contains(&(*si, *ai)))).map(|(si, ai, a)| { let mut a = *a; @@ -579,6 +579,33 @@ fn handle_anchors(cx: &ToolContext) -> Vec<(NodeId, usize, usize, Anchor)> { v } +/// The path whose direction handles Direct Selection shows and drags on `n` once it is selected: +/// a path's, or type on a path's path (#3565), in document space. +fn handle_path(n: &Node) -> Option> { + match &n.kind { + NodeKind::Path { path, .. } => Some(Cow::Borrowed(path)), + NodeKind::Text(t) if is_path_type(n) => t.type_path().map(Cow::Owned), + _ => None, + } +} + +/// The direction handles of selected type on a path ([`handle_anchors`]), drawn by the tool: +/// the canvas draws only a path's own (#3565). +fn type_path_handles(cx: &ToolContext) -> Vec { + let mut out = vec![]; + for (id, _, _, a) in handle_anchors(cx) { + if !cx.doc.node(id).is_some_and(is_path_type) { + continue; + } + let color = cx.doc.layer_color(id); + for h in [a.h_in, a.h_out].into_iter().filter(|h| h.distance(a.p) > 1e-6) { + out.push(Overlay::Line { a: a.p, b: h, color, dashed: false }); + out.push(Overlay::Handle { p: h, color }); + } + } + out +} + /// The end of a shown direction handle ([`handle_anchors`]) nearest to `p` within `tol`, and /// nearer to it than the handle's anchor (a short handle leaves its anchor to be picked): (path, /// subpath, anchor, is_out). @@ -897,8 +924,9 @@ impl Tool for DirectSelectionTool { let (q, guides) = self.handle_snap.snap(cx, (id, si, ai), p, ev.mods.shift); self.snapped = snapped(q.to_vec2(), p.to_vec2(), ev.mods.shift, &guides); self.guides = guides; + let cmd = if cx.doc.node(id).is_some_and(is_path_type) { "text.setPathHandle" } else { "path.setHandle" }; vec![Action::Preview( - "path.setHandle".into(), + cmd.into(), json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out {"out"} else {"in"}, "x": q.x, "y": q.y, "independent": ev.mods.alt}), )] } @@ -1001,6 +1029,7 @@ impl Tool for DirectSelectionTool { out.push(Overlay::Path { path, color: cx.doc.layer_color(sp.instance), width: 1.0, dashed: false }); } out.extend(frame_overlays(cx, &self.frame_anchors(cx))); + out.extend(type_path_handles(cx)); let drag = if let State::Bracket(b) = &self.state { Some(b) } else { None }; out.extend(pathtype::overlays(cx, self.bracket_hover, drag)); } diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index 7948b4707..d7d4bb85b 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -2334,3 +2334,38 @@ fn a_press_on_a_clipping_path_in_a_dynamic_instance_picks_it() { outline.iter().find_map(|o| if let Overlay::Path { path, .. } = o { Some(vectorcraft_geom::kurbo::Shape::bounding_box(path)) } else { None }); assert_eq!(drawn, Some(Rect::new(310.0, 310.0, 320.0, 320.0))); } + +/// #3565: selected type on a path shows its path's direction handles, as a path selected whole +/// shows its own, and a press on one drags it with `text.setPathHandle` (Shift: 45° steps round +/// its anchor; Alt: that side only); none with Show handles when multiple anchors are selected off. +#[test] +fn type_on_a_path_shows_and_drags_its_paths_handles() { + let (mut d, text) = doc_with_path_type(); + let Some(vectorcraft_doc::NodeKind::Text(t)) = d.node_mut(text).map(|n| &mut n.kind) else { panic!("type") }; + let vectorcraft_doc::TextKind::OnPath { path, .. } = &mut t.kind else { panic!("on a path") }; + path.subpaths[0].anchors[0].h_out = Point::new(100.0, 200.0); + path.subpaths[0].anchors[1].h_in = Point::new(400.0, 200.0); + let p = paint(); + let mut s = Selection::default(); + s.add(text); + let c = cx(&d, &s, &p); + let handles = |c: &ToolContext| -> Vec { + DirectSelectionTool::new(false).overlays(c).iter().filter_map(|o| if let Overlay::Handle { p, .. } = o { Some(*p) } else { None }).collect() + }; + assert_eq!(handles(&c), vec![Point::new(100.0, 200.0), Point::new(400.0, 200.0)]); + let mut t = DirectSelectionTool::new(false); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 101.0, 201.0)), vec![Action::Begin("Reshape".into())]); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 210.0).with_mods(Mods { alt: true, ..Mods::default() })); + assert!( + matches!(&a[..], [Action::Preview(n, v)] if n == "text.setPathHandle" && v["id"] == text.0 && v["subpath"] == 0 && v["anchor"] == 0 && v["which"] == "out" && v["independent"] == true), + "{a:?}" + ); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 210.0)), vec![Action::Commit]); + let mut t = DirectSelectionTool::new(false); + t.pointer(&c, &PointerEvent::new(PointerKind::Down, 399.0, 201.0)); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 390.0, 120.0).with_mods(Mods { shift: true, ..Mods::default() })); + let Some(Action::Preview(n, v)) = a.first() else { panic!("{a:?}") }; + assert_eq!((n.as_str(), &v["anchor"], &v["which"]), ("text.setPathHandle", &json!(1), &json!("in"))); + assert!((v["x"].as_f64().unwrap() - 400.0).abs() < 1e-9, "Shift keeps it straight above its anchor: {v}"); + assert!(handles(&ToolContext { handles_multiple: false, ..cx(&d, &s, &p) }).is_empty()); +} diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index 9d530f4db..32941c5f7 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -428,8 +428,12 @@ impl HandleSnap { /// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`, /// and the guides showing why. pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec) { - let path = cx.doc.node(id).and_then(|n| n.path_data()); - if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) { + // A path's anchor, or one of type on a path's path (#3565), in document space. + let anchor = cx.doc.node(id).and_then(|n| match &n.kind { + vectorcraft_doc::NodeKind::Text(t) => t.type_path()?.subpaths.get(si)?.anchors.get(ai).copied(), + _ => n.path_data()?.subpaths.get(si)?.anchors.get(ai).copied(), + }); + if shift && let Some(a) = anchor { return (a.p + constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]); } if self.0.targets.is_none() { diff --git a/docs/mcp.md b/docs/mcp.md index a9b1f1cd8..77041cb28 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5902,6 +5902,9 @@ undo step. Object › Transform › Scale, the Scale tool and the Transform pane `text.reshapePath {id, anchors: [[subpath, anchor]…], dx, dy}` does the same for the path of type on a path (Direct Selection dragging one of its anchors): the start and end brackets keep their fractions of the path and the text reflows along it. +`text.setPathHandle {id, subpath?, anchor, which: "in"|"out", x, y, independent?}` moves a direction handle of one +of its anchors, as `path.setHandle` moves a path's (Direct Selection dragging a handle of the path, #3565), in one +undo step. ```json {"name":"run_command","arguments":{"command":"text.reshapeArea","params":{"id":42,"anchors":[[0,2]],"dx":40,"dy":60}}} diff --git a/docs/type-parity.md b/docs/type-parity.md index 8f3c8e7c2..aac66f77c 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -25,7 +25,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Area Type Options (rows, columns, inset, first baseline, Text Flow by rows or by columns, Auto Size, Shrink Text to Fit) | done | Text Flow fills the cells down each column first (By Columns, the default, as files from before it open) or across each row first (By Rows), in the dialog and `text.areaOptions {flow}` (#4978); the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) | | Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444); a thread holds area type and type on a path in any mix, as Illustrator links type in an area or along a path ([help](https://helpx.adobe.com/illustrator/using/manage-text-area.html)) | | Threading from the ports: selected area type and type on a path show an in port and an out port, empty, with an arrow on a side where the frame is threaded, or with a red "+" in the out port when the text doesn't fit; a click on either port loads the text; a click on a closed path, area type or type on a path threads into it, a click on empty canvas makes a frame of the same kind, shape and size there, a drag draws a rectangular frame; the frame placed comes after the clicked one from its out port and before it from its in port (the story then starts there); Escape or another tool cancels; a double-click on a threaded port breaks the thread there | partial | #2572: the Selection tool, `text.thread.add {from, to?, rect?, at?, before?}`, as the reference app's help describes ports and threading ([archived help, Threading text between objects](https://web.archive.org/web/20210119051048/https://helpx.adobe.com/illustrator/using/creating-text.html), [create text threads](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html)); area type's in port sits on the left side below the top-left corner, type on a path's just before its start bracket; Show Text Threads draws its lines between the same ports; with more than 1,000 such objects selected no ports are drawn. a double-click on a port where the frame is threaded breaks the thread there, the story flowing into the frames before the break and the frames after it left empty, still threaded with each other when two or more (`text.thread.break`, [archived help](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html)). Remaining: the ports only take clicks with the Selection tool, and the loaded cursor borrows the I-beam until it's drawn | -| Type on a Path effects and options; brackets | done | #429; Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show) | +| Type on a Path effects and options; brackets | done | #429; Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show); Direct Selection shows the anchors and direction handles of selected type on a path's path and drags both, the brackets keeping their fractions of the path and the text reflowing along it (#3980, #3565; `text.reshapePath`, `text.setPathHandle`), as the [Direct Selection page](https://helpx.adobe.com/illustrator/using/tool-techniques/direct-selection-tool.html) describes for a path | | Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing. Double-clicking a user character style's row opens its Character Style Options, and the dialog keeps a fixed width while its sections scroll when the window is shorter, so its buttons stay on screen; a paragraph style's row opens its Paragraph Style Options, the rename dialog the panel menu opens. Evidence: `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options`, `dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list` | | Copy or Cut selected characters, then paste as separate text art | done | A Type-range copy keeps the selected characters' font family, size and color when pasted on the canvas after leaving text editing, in the same document or another. On desktop the clipboard offers plain text to text consumers and rich art for canvas Paste; Paste Without Formatting while editing uses the destination text's style. `tests::clipboard::type_range_copy_stays_styled_when_pasted_as_art` (#5636) | | Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. | @@ -81,6 +81,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-10 | minor | Type on a path (#3565): Direct Selection shows the direction handles of selected type on a path's path, as of a path selected whole, and drags them (`text.setPathHandle`; Alt: one side, Shift: 45° steps); the text reflows along the new curve. Scores and estimates unchanged. | | 2026-11-10 | minor | Spelling in the canvas context menu (#4936): a right-click on type has Spelling › Auto Spell Check, checked while it is on; the Check Spelling and Edit Custom Dictionary dialogs remain. Scores and estimates unchanged. | | 2026-11-10 | minor | Spelling in the canvas context menu (#4936): a right-click on a word Auto Spell Check underlined lists its suggestions, Ignore All and Add to Dictionary; the Check Spelling and Edit Custom Dictionary dialogs remain. Scores and estimates unchanged. | | 2026-11-09 | minor | Auto Spell Check (#4936): Edit › Spelling › Auto Spell Check (`edit.autoSpellCheck`) underlines the unknown words of the visible type, without type with effects; the Check Spelling and Edit Custom Dictionary dialogs and the right-click Spelling menu remain. Scores and estimates unchanged. |