diff --git a/ROADMAP.md b/ROADMAP.md index 0e054ab5..fca8ef63 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| -| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). | +| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | | 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections | diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 0df32a97..7c0b41c7 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -45,7 +45,7 @@ pub fn specs() -> Vec { "Area / Path Type", [], None, - "{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, fit?: none|autoHeight|shrinkText, fitMinPercent? (area: as text.areaOptions; default autoHeight when the autoSizeAreaType preference is on), placeholder?: bool (placeholder text instead, as text.create), leadingModel?, charAlign? (as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id}", + "{path: id, mode: \"area\"|\"onPath\", text?: \"\", vertical?: bool = false, at?: [x, y] (on-path start: nearest point), size?, font?, style?, color? (as text.create), fit?: none|autoHeight|shrinkText, fitMinPercent? (area: as text.areaOptions; default autoHeight when the autoSizeAreaType preference is on), placeholder?: bool (placeholder text instead, as text.create), leadingModel?, charAlign? (as text.create)} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id}", has_doc, create_in_path ), @@ -485,16 +485,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result { if !on_path && !path.is_closed() && path.bounds().is_none_or(|b| b.width() < 1.0 || b.height() < 1.0) { return Err(bad(C, "area type needs a path that encloses an area")); } - let mut style = CharStyle::default(); - if let Some(v) = p.get("size").and_then(Value::as_f64) { - style.size = v.clamp(0.1, 1296.0); - } - if let Some(f) = str_param(p, "font") { - style.font_family = f.to_string(); - } - if !s.paint.fill.is_none() && s.paint.fill != Paint::solid(vectorcraft_color::Color::WHITE) { - style.fill = s.paint.fill.clone(); - } + let style = super::create::new_type_style(s, p); let text = str_param(p, "text").unwrap_or("").to_string(); let start = match point_param(p, "at") { Some(at) if on_path => vectorcraft_geom::ArcPath::new(path).fraction_at(at).unwrap_or(0.0), diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 25639ebd..17b3773a 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -312,6 +312,22 @@ fn inspect_document_reports_the_paint_of_type() { assert_eq!((t["fill"].as_str(), t["stroke"].as_str(), t["strokeWidth"].as_f64()), (Some("#0000ff"), Some("#00ff00"), Some(3.0)), "{t}"); } +/// #1003: type made on or in a path takes add_text's `color` too, over the current fill. +#[test] +fn add_text_on_or_in_a_path_takes_its_color() { + for mode in ["onPath", "area"] { + let mut s = server(); + let r = call(&mut s, 2, "draw_shape", json!({"shape": "rectangle", "x": 40, "y": 40, "width": 180, "height": 60, "fill": "#0000ff"})); + let path = serde_json::from_str::(&text_of(&r)).unwrap()["id"].clone(); + let r = call(&mut s, 3, "add_text", json!({"text": "Hello", "path": path, "mode": mode, "color": "#ff0000"})); + assert_eq!(r["isError"], false, "{r}"); + let id = serde_json::from_str::(&text_of(&r)).unwrap()["id"].clone(); + let v: Value = serde_json::from_str(&text_of(&call(&mut s, 4, "inspect_document", json!({})))).unwrap(); + let t = v["layers"][0]["children"].as_array().unwrap().iter().find(|c| c["id"] == id).cloned().unwrap(); + assert_eq!(t["fill"].as_str(), Some("#ff0000"), "{mode}: {t}"); + } +} + /// save_file says it saves in the document's own format or the one the path's extension picks. #[test] fn save_file_describes_the_formats_it_writes() { diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index b84358ce..9ae42a4a 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -574,7 +574,7 @@ fn select_ids(b: &mut dyn Backend, a: &Args) -> Result<(), String> { fn add_text(b: &mut dyn Backend, a: &Args) -> Result { let text = a.get("text").and_then(Value::as_str).ok_or("missing string argument `text`")?; let mut style = json!({}); - for k in ["size", "font"] { + for k in ["size", "font", "color"] { if let Some(v) = a.get(k) { style[k] = v.clone(); } @@ -592,9 +592,6 @@ fn add_text(b: &mut dyn Backend, a: &Args) -> Result { for (k, v) in style.as_object().into_iter().flatten() { p[k.as_str()] = v.clone(); } - if let Some(c) = a.get("color") { - p["color"] = c.clone(); - } if let (Some(w), Some(h)) = (a.get("width"), a.get("height")) { p["area"] = json!({"width": w, "height": h}); }