mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M8.2: tool options last across tool switches and launches
Each tool keeps its options (a table in vectorcraft_tools::settings) when another
tool is chosen, across document switches, and in the saved preferences
(Prefs::tool_settings). The 7 Liquify tools share one set of Global Brush
Dimensions; the Symbolism tools share their brush. tool.setOption takes
{values} and a `tool` (another tool's options are stored for later), returns
the options, and works headless (MCP host command).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2c02502f43
commit
08108f693f
11 files changed
+346
-13
No files matched your search
@@ -476,6 +476,12 @@ pub struct Prefs {
|
||||
/// the Properties panel and the Control bar (their size fields pass `proportional` to
|
||||
/// `object.setBounds`).
|
||||
pub constrain_proportions: bool,
|
||||
/// The tools' persistent options by store (a tool id, or a store a family shares: `liquify`
|
||||
/// holds the Liquify tools' Global Brush Dimensions), see [`vectorcraft_tools::settings`]:
|
||||
/// kept across tool switches and saved with the preferences. Not a Preferences dialog field:
|
||||
/// resetting the preferences keeps them; `tool.setOption` edits them.
|
||||
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub tool_settings: std::collections::BTreeMap<String, serde_json::Map<String, Value>>,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
@@ -609,6 +615,7 @@ impl Default for Prefs {
|
||||
web_export_settings: None,
|
||||
print_presets: vec![],
|
||||
constrain_proportions: false,
|
||||
tool_settings: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -756,7 +763,8 @@ impl Session {
|
||||
if self.batch_stash.is_none() {
|
||||
let _ = self.cancel_interaction();
|
||||
}
|
||||
self.tool = vectorcraft_tools::create(self.tool.id());
|
||||
self.keep_tool_settings();
|
||||
self.tool = self.make_tool(self.tool.id());
|
||||
}
|
||||
pub fn set_active(&mut self, index: usize) -> bool {
|
||||
if index < self.docs.len() {
|
||||
@@ -1302,6 +1310,8 @@ mod tests_tileedge;
|
||||
#[cfg(test)]
|
||||
mod tests_tints;
|
||||
#[cfg(test)]
|
||||
mod tests_toolsettings;
|
||||
#[cfg(test)]
|
||||
mod tests_transparencygrid;
|
||||
#[cfg(test)]
|
||||
mod tests_units;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Tool options that persist across tool switches, documents and launches (`Prefs::tool_settings`).
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, tool: &str) {
|
||||
s.select_tool(tool, ViewInfo::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_survive_a_tool_switch() {
|
||||
let mut s = session();
|
||||
select(&mut s, "mirrorCut");
|
||||
s.set_tool_option("axis", &json!("horizontal"));
|
||||
s.set_tool_option("keep", &json!("bottom"));
|
||||
select(&mut s, "selection");
|
||||
select(&mut s, "mirrorCut");
|
||||
assert_eq!(s.tool_options(), json!({"axis": "horizontal", "keep": "bottom"}));
|
||||
// Interaction state isn't kept: the Rotate tool's reference point starts afresh.
|
||||
select(&mut s, "rotate");
|
||||
s.set_tool_option("origin", &json!([10, 20]));
|
||||
select(&mut s, "selection");
|
||||
select(&mut s, "rotate");
|
||||
assert_eq!(s.tool_options(), json!({"origin": null}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn liquify_tools_share_the_global_brush_but_not_their_own_options() {
|
||||
let mut s = session();
|
||||
select(&mut s, "warp");
|
||||
s.set_tool_option("width", &json!(60));
|
||||
s.set_tool_option("intensity", &json!(80));
|
||||
s.set_tool_option("detail", &json!(7));
|
||||
select(&mut s, "twirl");
|
||||
let o = s.tool_options();
|
||||
assert_eq!((o["width"].as_f64(), o["intensity"].as_f64(), o["detail"].as_f64()), (Some(60.0), Some(0.8), Some(2.0)));
|
||||
s.set_tool_option("rate", &json!(-90));
|
||||
select(&mut s, "warp");
|
||||
assert_eq!(s.tool_options()["detail"], json!(7.0));
|
||||
select(&mut s, "twirl");
|
||||
assert_eq!(s.tool_options()["rate"], json!(-90.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_tools_options_are_stored_and_shared_ones_reach_the_active_tool() {
|
||||
let mut s = session();
|
||||
select(&mut s, "warp");
|
||||
let o = s.set_tool_option_cmd(&json!({"tool": "pucker", "values": {"width": 40, "height": 30, "detail": 5}})).unwrap();
|
||||
assert_eq!((o["tool"].as_str(), o["detail"].as_f64()), (Some("pucker"), Some(5.0)));
|
||||
assert_eq!((s.tool_id(), s.tool_options()["width"].as_f64(), s.tool_options()["detail"].as_f64()), ("warp", Some(40.0), Some(2.0)));
|
||||
assert_eq!(s.tool_options_of("pucker")["height"], json!(30.0));
|
||||
select(&mut s, "pucker");
|
||||
assert_eq!(s.tool_options()["detail"], json!(5.0));
|
||||
// `{}` reads the options; an unknown tool is an error.
|
||||
assert_eq!(s.set_tool_option_cmd(&json!({})).unwrap(), s.tool_options());
|
||||
assert!(s.set_tool_option_cmd(&json!({"tool": "nope", "key": "width", "value": 1})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gesture_keeps_what_it_changed_and_a_document_switch_keeps_options() {
|
||||
let mut s = session();
|
||||
select(&mut s, "spiral");
|
||||
s.set_tool_option("segments", &json!(14));
|
||||
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||||
assert_eq!(s.tool_id(), "spiral");
|
||||
assert_eq!(s.tool_options()["segments"], json!(14));
|
||||
s.set_active(0);
|
||||
assert_eq!(s.tool_options()["segments"], json!(14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_saved_with_the_preferences() {
|
||||
let mut s = session();
|
||||
select(&mut s, "polygon");
|
||||
s.set_tool_option("sides", &json!(9));
|
||||
select(&mut s, "selection");
|
||||
let saved = serde_json::to_string(&s.prefs).unwrap();
|
||||
assert!(saved.contains("toolSettings"), "{saved}");
|
||||
// Next launch.
|
||||
let mut t = session();
|
||||
t.apply_prefs(serde_json::from_str(&saved).unwrap());
|
||||
select(&mut t, "polygon");
|
||||
assert_eq!(t.tool_options()["sides"], json!(9));
|
||||
// Older preference files have none; none are written until a tool keeps something.
|
||||
assert!(Prefs::default().to_json().get("toolSettings").is_none());
|
||||
let old: Prefs = serde_json::from_value(json!({"keyboardIncrement": 2})).unwrap();
|
||||
assert!(old.tool_settings.is_empty());
|
||||
// Resetting the preferences keeps them.
|
||||
t.execute("prefs.reset", &json!({})).unwrap();
|
||||
assert_eq!(t.prefs.tool_settings["polygon"]["sides"], json!(9));
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Hosting the active tool: pointer/key events → actions → commands.
|
||||
|
||||
use serde_json::Value;
|
||||
use serde_json::{Map, Value};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::{Action, Cursor, Mods, Overlay, PointerEvent, ToolContext, ToolKey};
|
||||
use vectorcraft_tools::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, settings};
|
||||
|
||||
use crate::{EngineError, Result, Session};
|
||||
use crate::{EngineError, Prefs, Result, Session};
|
||||
|
||||
/// View state the tools need from the frontend.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -56,10 +56,50 @@ impl Session {
|
||||
}
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
self.apply_actions(acts)?;
|
||||
self.tool = vectorcraft_tools::create(id);
|
||||
self.keep_tool_settings();
|
||||
self.tool = self.make_tool(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A fresh `id` tool with the options it keeps ([`settings`]): its last values, from this
|
||||
/// session or a saved one.
|
||||
pub(crate) fn make_tool(&self, id: &str) -> Box<dyn Tool> {
|
||||
let mut t = vectorcraft_tools::create(id);
|
||||
Self::restore_tool_settings(&self.prefs, t.as_mut());
|
||||
t
|
||||
}
|
||||
|
||||
/// Give `t` the persistent options stored for it in `prefs`.
|
||||
fn restore_tool_settings(prefs: &Prefs, t: &mut dyn Tool) {
|
||||
for (store, keys) in settings::stores(t.id()) {
|
||||
let Some(saved) = prefs.tool_settings.get(store) else { continue };
|
||||
for k in keys {
|
||||
if let Some(v) = saved.get(*k) {
|
||||
t.set_option(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the persistent options of `t` in `prefs` (so they are saved with them too).
|
||||
fn store_tool_settings(prefs: &mut Prefs, t: &dyn Tool) {
|
||||
let opts = t.options();
|
||||
for (store, keys) in settings::stores(t.id()) {
|
||||
let saved = prefs.tool_settings.entry(store.to_string()).or_default();
|
||||
for k in keys {
|
||||
if let Some(v) = opts.get(*k) {
|
||||
saved.insert(k.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the active tool's persistent options for the next time it, or a tool sharing them, is
|
||||
/// made.
|
||||
pub(crate) fn keep_tool_settings(&mut self) {
|
||||
Self::store_tool_settings(&mut self.prefs, self.tool.as_ref());
|
||||
}
|
||||
|
||||
pub(crate) fn with_tool_cx<R>(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn vectorcraft_tools::Tool, &ToolContext) -> R) -> R
|
||||
where
|
||||
R: Default,
|
||||
@@ -98,7 +138,7 @@ impl Session {
|
||||
Err(msg) => {
|
||||
// A bug in the tool: start it afresh and drop its half-done drag.
|
||||
let id = self.tool.id();
|
||||
self.tool = vectorcraft_tools::create(id);
|
||||
self.tool = self.make_tool(id);
|
||||
let _ = self.cancel_interaction();
|
||||
self.tool_panic = Some(EngineError::Internal { cmd: format!("tool `{id}`"), msg });
|
||||
R::default()
|
||||
@@ -116,6 +156,10 @@ impl Session {
|
||||
self.last_view = view;
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
self.take_tool_panic()?;
|
||||
// A gesture may change options (Alt-drag sizes a Liquify brush): keep them.
|
||||
if ev.kind == PointerKind::Up {
|
||||
self.keep_tool_settings();
|
||||
}
|
||||
self.apply_actions(acts)
|
||||
}
|
||||
|
||||
@@ -162,6 +206,47 @@ impl Session {
|
||||
}
|
||||
pub fn set_tool_option(&mut self, key: &str, v: &Value) {
|
||||
self.tool.set_option(key, v);
|
||||
self.keep_tool_settings();
|
||||
}
|
||||
|
||||
/// The options of tool `id`: the active tool's, or those it would have if chosen now.
|
||||
pub fn tool_options_of(&self, id: &str) -> Value {
|
||||
if id == self.tool.id() { self.tool.options() } else { self.make_tool(id).options() }
|
||||
}
|
||||
|
||||
/// Set options of tool `id` (default: the active tool). Another tool's persistent options are
|
||||
/// stored for when it is chosen, and the active tool takes those it shares with it (the
|
||||
/// Liquify tools' Global Brush Dimensions). Returns the tool's options.
|
||||
pub fn set_tool_options(&mut self, id: Option<&str>, values: &Map<String, Value>) -> Value {
|
||||
let active = id.is_none_or(|id| id == self.tool.id());
|
||||
let mut other = (!active).then(|| self.make_tool(id.unwrap_or_default()));
|
||||
let t = other.as_deref_mut().unwrap_or(self.tool.as_mut());
|
||||
for (k, v) in values {
|
||||
t.set_option(k, v);
|
||||
}
|
||||
Self::store_tool_settings(&mut self.prefs, t);
|
||||
let out = t.options();
|
||||
if other.is_some() {
|
||||
Self::restore_tool_settings(&self.prefs, self.tool.as_mut());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `tool.setOption {tool?, key?, value?, values?}` (a command of the desktop app and of the
|
||||
/// headless host): set one option (`key`, `value`) or several (`values`) of `tool` (default:
|
||||
/// the active tool) → its options.
|
||||
pub fn set_tool_option_cmd(&mut self, p: &Value) -> std::result::Result<Value, String> {
|
||||
let tool = p.get("tool").and_then(Value::as_str);
|
||||
if let Some(t) = tool
|
||||
&& vectorcraft_tools::tool_info(t).is_none()
|
||||
{
|
||||
return Err(format!("unknown tool `{t}`"));
|
||||
}
|
||||
let mut values = p.get("values").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
if let Some(k) = p.get("key").and_then(Value::as_str) {
|
||||
values.insert(k.to_string(), p.get("value").cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
Ok(self.set_tool_options(tool, &values))
|
||||
}
|
||||
|
||||
/// Let the active tool finish work a command from outside it ended ([`vectorcraft_tools::Tool::after_command`]):
|
||||
|
||||
@@ -38,6 +38,11 @@ const HOST_COMMANDS: &[(&str, &str, &str)] = &[
|
||||
"{folder?, zip?, artboards? | range? | fullDocument? | assets?, includeBleed?, subfolders?, preset?, formats?, settings?, prefix?} = document.exportForScreens (no folder → the files, or one zip, as dataBase64)",
|
||||
),
|
||||
("tool.select", "Select Tool", "{tool} e.g. selection, directSelection, pen, rectangle, ellipse, polygon, star, lineSegment"),
|
||||
(
|
||||
"tool.setOption",
|
||||
"Tool Option",
|
||||
"{key, value} | {values: {key: value…}}, tool?: id (default: the active tool) → the tool's options (`{}` reads them); kept options last across tool switches",
|
||||
),
|
||||
];
|
||||
|
||||
fn s<'a>(p: &'a Value, k: &str) -> Option<&'a str> {
|
||||
@@ -113,6 +118,7 @@ impl Headless {
|
||||
"file.export" => self.export(params),
|
||||
"file.exportForScreens" => self.session.execute("document.exportForScreens", params).map_err(|e| e.to_string()),
|
||||
"tool.select" => self.select_tool(params),
|
||||
"tool.setOption" => self.session.set_tool_option_cmd(params),
|
||||
_ => self.session.execute(id, params).map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//! The Liquify tools through MCP: their options (`tool.setOption`, kept across tool switches) and
|
||||
//! brush strokes with `pointer_gesture`.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Headless, call_tool};
|
||||
|
||||
fn ok(h: &mut Headless, name: &str, args: Value) -> Value {
|
||||
let r = call_tool(h, name, &args);
|
||||
assert!(!r.is_error, "{name} {args}: {r:?}");
|
||||
serde_json::from_str(r.content[0]["text"].as_str().unwrap()).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
fn command(h: &mut Headless, command: &str, params: Value) -> Value {
|
||||
ok(h, "run_command", json!({"command": command, "params": params}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_options_are_set_headless_and_kept_across_tool_switches() {
|
||||
let mut h = Headless::with_document();
|
||||
command(&mut h, "tool.select", json!({"tool": "warp"}));
|
||||
let o = command(&mut h, "tool.setOption", json!({"values": {"width": 64, "detail": 6}}));
|
||||
assert_eq!((o["width"].as_f64(), o["detail"].as_f64()), (Some(64.0), Some(6.0)));
|
||||
command(&mut h, "tool.select", json!({"tool": "bloat"}));
|
||||
let o = command(&mut h, "tool.setOption", json!({}));
|
||||
assert_eq!((o["tool"].as_str(), o["width"].as_f64(), o["detail"].as_f64()), (Some("bloat"), Some(64.0), Some(2.0)));
|
||||
command(&mut h, "tool.select", json!({"tool": "warp"}));
|
||||
assert_eq!(command(&mut h, "tool.setOption", json!({}))["detail"], json!(6.0));
|
||||
}
|
||||
@@ -157,6 +157,16 @@ impl LiquifyParams {
|
||||
v
|
||||
}
|
||||
|
||||
/// Every option, whichever tool these are (the tool's options: [`Self::to_json`] gives only
|
||||
/// the parameters its kind uses).
|
||||
pub fn options_json(&self) -> Value {
|
||||
let mut v = self.to_json();
|
||||
for (k, x) in [("rate", self.rate), ("complexity", self.complexity), ("horizontal", self.horizontal), ("vertical", self.vertical)] {
|
||||
v[k] = json!(x);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn radii(&self) -> (f64, f64) {
|
||||
(self.width / 2.0, self.height / 2.0)
|
||||
}
|
||||
@@ -528,7 +538,7 @@ impl Tool for LiquifyTool {
|
||||
Cursor::Crosshair
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
self.params.to_json()
|
||||
self.params.options_json()
|
||||
}
|
||||
fn set_option(&mut self, key: &str, value: &Value) {
|
||||
let mut v = self.params.to_json();
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod pen;
|
||||
pub mod place;
|
||||
pub mod printtiling;
|
||||
pub mod select;
|
||||
pub mod settings;
|
||||
pub mod shape;
|
||||
pub mod slice;
|
||||
pub mod symbolism;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Tool options that persist: a tool keeps them when another tool is chosen and they are saved
|
||||
//! with the preferences, as the reference app keeps each tool's options. Options that are the
|
||||
//! state of an interaction (pins, a reference point, the text being edited, the files a place
|
||||
//! cursor holds) aren't listed: they start afresh.
|
||||
//!
|
||||
//! Each row names the tools it covers, the store their values live in and the option keys. A row
|
||||
//! with a shared store gives every tool it lists one set of values: the Liquify tools share their
|
||||
//! Global Brush Dimensions, the Symbolism tools their brush. Otherwise each tool has its own store,
|
||||
//! named after its id.
|
||||
|
||||
/// (tools, the store they share (None: each tool's own), option keys).
|
||||
type Row = (&'static [&'static str], Option<&'static str>, &'static [&'static str]);
|
||||
|
||||
/// The Liquify tools.
|
||||
pub const LIQUIFY: &[&str] = &["warp", "twirl", "pucker", "bloat", "scallop", "crystallize", "wrinkle"];
|
||||
|
||||
const SYMBOLISM: &[&str] =
|
||||
&["symbolSprayer", "symbolShifter", "symbolScruncher", "symbolSizer", "symbolSpinner", "symbolStainer", "symbolScreener", "symbolStyler"];
|
||||
|
||||
const ROWS: &[Row] = &[
|
||||
// Global Brush Dimensions.
|
||||
(LIQUIFY, Some("liquify"), &["width", "height", "angle", "intensity"]),
|
||||
(LIQUIFY, None, &["detail", "simplify", "rate", "complexity", "horizontal", "vertical"]),
|
||||
(SYMBOLISM, Some("symbolism"), &["diameter", "intensity", "density"]),
|
||||
(&["mirrorCut"], None, &["axis", "keep"]),
|
||||
(&["puppetWarp"], None, &["showMesh", "expand"]),
|
||||
(&["arc"], None, &["closed"]),
|
||||
(&["spiral"], None, &["decay", "segments", "clockwise"]),
|
||||
(&["rectangularGrid"], None, &["rows", "columns"]),
|
||||
(&["polarGrid"], None, &["concentric", "radial"]),
|
||||
(&["pencil", "paintbrush"], None, &["fidelity", "fill", "editWithin", "closeWithin"]),
|
||||
(&["smooth"], None, &["fidelity"]),
|
||||
(&["blobBrush", "eraser"], None, &["size"]),
|
||||
(&["polygon"], None, &["sides"]),
|
||||
(&["star"], None, &["points"]),
|
||||
(&["freeTransform"], None, &["constrain"]),
|
||||
(&["artboard"], None, &["moveArt"]),
|
||||
];
|
||||
|
||||
/// The stores `tool`'s persistent options live in, each with the option keys it holds.
|
||||
pub fn stores(tool: &str) -> impl Iterator<Item = (&'static str, &'static [&'static str])> + '_ {
|
||||
ROWS.iter().filter_map(move |(tools, shared, keys)| {
|
||||
let id = tools.iter().find(|t| **t == tool)?;
|
||||
Some((shared.unwrap_or(id), *keys))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_listed_option_is_one_the_tool_reports() {
|
||||
for (tools, _, keys) in ROWS {
|
||||
for id in *tools {
|
||||
assert!(crate::tool_info(id).is_some(), "{id} is a tool");
|
||||
let opts = crate::create(id).options();
|
||||
for k in *keys {
|
||||
assert!(opts.get(*k).is_some(), "{id} reports `{k}` in {opts}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn liquify_tools_share_the_global_brush() {
|
||||
let warp: Vec<_> = stores("warp").collect();
|
||||
let twirl: Vec<_> = stores("twirl").collect();
|
||||
assert_eq!(warp[0], twirl[0]);
|
||||
assert_eq!(warp[0].0, "liquify");
|
||||
assert_eq!((warp[1].0, twirl[1].0), ("warp", "twirl"));
|
||||
assert_eq!(stores("selection").count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -170,7 +170,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("window.workspace.list", "List Workspaces", "", "{}"),
|
||||
("window.newWindow", "New Window", "", "{}"),
|
||||
("tool.select", "Select Tool", "", "{tool: id} (see tools)"),
|
||||
("tool.setOption", "Tool Option", "", "{key, value}"),
|
||||
(
|
||||
"tool.setOption",
|
||||
"Tool Option",
|
||||
"",
|
||||
"{key, value} | {values: {key: value…}}, tool?: id (default: the active tool) → the tool's options (`{}` reads them). The options a tool keeps (Liquify brush and tool options, Mirror & Cut, Puppet Warp, drawing tools…) last across tool switches and are saved with the preferences; another tool's are stored for when it is chosen",
|
||||
),
|
||||
(
|
||||
"effect.dialog",
|
||||
"Effect…",
|
||||
@@ -786,11 +791,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
Some(t) => Err(format!("unknown tool `{t}`")),
|
||||
None => Err("missing `tool`".into()),
|
||||
},
|
||||
"tool.setOption" => {
|
||||
let k = s("key").unwrap_or_default();
|
||||
app.session.set_tool_option(&k, p.get("value").unwrap_or(&Value::Null));
|
||||
Ok(app.session.tool_options())
|
||||
}
|
||||
"tool.setOption" => app.session.set_tool_option_cmd(p),
|
||||
"effect.dialog" => crate::dialogs::open_effect_dialog(app, p),
|
||||
"ui.recolorDialog" => crate::dialogs::recolor::open(app, p),
|
||||
"ui.paramDialog" => {
|
||||
|
||||
@@ -444,3 +444,8 @@ Colour adjustment effects (Effect → Color Adjustments) open the `effect` dialo
|
||||
Halftone… (`engine.execute {command: "ui.menuDialog", params: {command: "object.vectorHalftone"}}`) opens the
|
||||
`vectorHalftone` dialog (fields `shape`, `frequency`, `angle`, `mode`, `color`, `invert`, `clip`, `keepOriginal`,
|
||||
`preview`): it previews live and `ui.dialog.confirm` keeps the halftone as one undo step.
|
||||
|
||||
Tool options last: `tool.setOption {key, value}` or `{values: {…}}` (and `tool` to set another tool's) sets them, and
|
||||
the options a tool keeps (Liquify brush and tool options, Mirror & Cut, Puppet Warp, Symbolism, drawing tools) stay
|
||||
when you switch tools or documents and are saved with the preferences. The Liquify tools share their Global Brush
|
||||
Dimensions (`width`, `height`, `angle`, `intensity`).
|
||||
+14
@@ -1729,3 +1729,17 @@ font that became available redraws in it, without editing the document.
|
||||
{"name":"run_command","arguments":{"command":"text.fontList","params":{"family":"Source Serif 4"}}}
|
||||
{"name":"run_command","arguments":{"command":"text.rescanFonts","params":{}}}
|
||||
```
|
||||
|
||||
## Tool options
|
||||
|
||||
`tool.setOption` reads and sets a tool's options, headless too: `{key, value}` or `{values: {…}}` sets them on the
|
||||
active tool, `tool` names another one, and `{}` just reads them; the result is the tool's options. The options a
|
||||
tool keeps last across tool switches (and documents) and are saved with the preferences (`toolSettings` in
|
||||
`prefs.get`'s full object), as the reference app keeps them: the Liquify tools' brush and tool options, Mirror & Cut's
|
||||
axis and side, Puppet Warp's mesh, the Symbolism brush, the line, grid, pencil, brush and eraser tools' options,
|
||||
polygon sides and star points. The Liquify tools share one set of Global Brush Dimensions (width, height, angle,
|
||||
intensity), and the Symbolism tools one brush. Interaction state (pins, a reference point) starts afresh.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"tool.setOption","params":{"tool":"twirl","values":{"width":60,"rate":90}}}}
|
||||
```
|
||||
Reference in new issue
Block a user