Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
148ef1870a | ||
|
|
7f17d980d4 |
No files matched your search
+1
-1
@@ -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). MCP `screenshot` of a connected app renders the artboard asked for (#1004). |
|
||||
| 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 |
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<Value> {
|
||||
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),
|
||||
|
||||
@@ -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::<Value>(&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::<Value>(&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() {
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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<Value, String> {
|
||||
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});
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
//! - `ui.dialog.set {field, value}` / `ui.dialog.confirm` / `ui.dialog.cancel`
|
||||
//! - `ui.resize {width, height}`, `ui.focus`, `ui.screenshot {path?, data?}` (`data: true`: the PNG
|
||||
//! comes back as `pngBase64` as well)
|
||||
//! - `ui.render {path?, scale?, data?}`: render the active artboard headlessly (PNG; without a path,
|
||||
//! or with `data: true`, as `pngBase64`)
|
||||
//! - `ui.render {path?, scale?, artboard?, data?}`: render an artboard headlessly (`artboard`: its
|
||||
//! 0-based index, default the first; PNG; without a path, or with `data: true`, as `pngBase64`)
|
||||
//! - `app.open {path}` (any readable format) / `app.save {path?, svg?: {…SVG options}}` / `app.quit`
|
||||
//! (`file.close`, `file.closeAll` and `app.quit` first open a `saveChanges` dialog for each
|
||||
//! modified document: `ui.dialog.confirm` saves, set `discard: true` then confirm to discard)
|
||||
@@ -321,7 +321,9 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
"ui.render" => {
|
||||
let Some(st) = app.session.active() else { return err("no document") };
|
||||
let doc = st.doc.clone();
|
||||
let Some(r) = doc.artboards.first().map(|a| a.rect) else { return err("no artboard") };
|
||||
// The artboard asked for (0-based, as headless), else the first.
|
||||
let index = p.get("artboard").and_then(Value::as_u64).map_or(Some(0), |i| usize::try_from(i).ok());
|
||||
let Some(r) = index.and_then(|i| doc.artboards.get(i)).map(|a| a.rect) else { return err("no such artboard") };
|
||||
let scale = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0);
|
||||
if let Err(e) = vectorcraft_render::raster_size(r, scale) {
|
||||
return err(e);
|
||||
@@ -390,3 +392,42 @@ pub fn save_screenshot(app: &mut VectorcraftApp, image: &egui::ColorImage, path:
|
||||
None => json!({"ok": false, "error": "no writer configured"}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::ControlRequest;
|
||||
use crate::{Services, VectorcraftApp};
|
||||
|
||||
/// #1004: `ui.render` (the MCP `screenshot` of a connected app) renders the artboard asked
|
||||
/// for, at the scale asked for; the first without one.
|
||||
#[test]
|
||||
fn render_takes_the_artboard_asked_for() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default()).with_control(rx);
|
||||
let mut request = |method: &str, params: Value| {
|
||||
let (req, reply) = ControlRequest::new(method, params);
|
||||
tx.send(req).unwrap();
|
||||
app.drain_control(&egui::Context::default());
|
||||
reply.try_recv().unwrap()
|
||||
};
|
||||
let new = json!({"width": 64, "height": 64, "units": "Points", "artboards": 2});
|
||||
request("engine.execute", json!({"command": "file.new", "params": new}));
|
||||
let board = json!({"index": 1, "x": 100, "y": 0, "width": 96, "height": 48});
|
||||
let r = request("engine.execute", json!({"command": "artboard.setProps", "params": board}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
for (params, size) in [
|
||||
(json!({}), [64, 64]),
|
||||
(json!({"artboard": 0}), [64, 64]),
|
||||
(json!({"artboard": 1}), [96, 48]),
|
||||
(json!({"artboard": 1, "scale": 0.5}), [48, 24]),
|
||||
] {
|
||||
let r = request("ui.render", params.clone());
|
||||
let got = [&r["result"]["width"], &r["result"]["height"]].map(Value::as_u64);
|
||||
assert_eq!(got, size.map(Some), "{params}: {r}");
|
||||
}
|
||||
assert!(request("ui.render", json!({"artboard": 2}))["error"].is_string());
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ the two can still win that race.
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?}` | |
|
||||
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog |
|
||||
| `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
|
||||
| `ui.render` | `{path?, scale?, data?}` | render the artboard headlessly (PNG): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
| `ui.resize` / `ui.focus` | | |
|
||||
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
|
||||
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
|
||||
|
||||
Reference in new issue
Block a user