mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Brushes: each Pattern Brush Options tile can be set to a pattern swatch or None
A menu under each of the five tiles in Pattern Brush Options lists None,
Original and the document's pattern swatches; the corner menus keep the
four automatic corners. A swatch choice copies the swatch's whole art and
its tile frame into the brush, so the brush keeps drawing after the swatch
is deleted, and the menu then shows that art as Original. Side cannot be
None: OK shows "You must choose a side pattern." and keeps the dialog open.
brush.options takes side_tile, outer_corner_tile, inner_corner_tile,
start_tile and end_tile (a pattern name, none, original, the corner auto
ids, or {"pattern": name} for a swatch named like one of those words), and
rejects side_tile "none" and unknown names before editing.
This commit is contained in:
@@ -134,7 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Brush Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, width_mode: null (fixed)|pressure, width_max 1..1000% (pressure: width at the lightest pen pressure to width_max at the heaviest, half way on a stroke drawn without pressure), scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across, overlap. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across, outer_corner and inner_corner: corner tile art or null, outer_source and inner_source: what that kind of corner draws without its art: none|autoCentered|autoBetween|autoSliced|autoOverlap (a tile made from the side tile), or null (the other kind of corner's art). scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour; without a key: the solid color the art's fills and strokes use most, black when they use none)}; colorization and the art brush's scale change only the fields given}, newName? (trimmed; blank: the name is kept; a new bristle brush name has at most 31 characters, and a longer name the brush has is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, width_mode: null (fixed)|pressure, width_max 1..1000% (pressure: width at the lightest pen pressure to width_max at the heaviest, half way on a stroke drawn without pressure), scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across, overlap. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across, outer_corner and inner_corner: corner tile art or null, outer_source and inner_source: what that kind of corner draws without its art: none|autoCentered|autoBetween|autoSliced|autoOverlap (a tile made from the side tile), or null (the other kind of corner's art); side_tile, outer_corner_tile, inner_corner_tile, start_tile, end_tile: a document pattern name, original, or none (side refuses none); corner tiles also take autoCentered|autoBetween|autoSliced|autoOverlap. Each tile also accepts {pattern: name} to select a swatch whose name is a reserved choice. scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour; without a key: the solid color the art's fills and strokes use most, black when they use none)}; colorization and the art brush's scale change only the fields given}, newName? (trimmed; blank: the name is kept; a new bristle brush name has at most 31 characters, and a longer name the brush has is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
has_doc,
|
||||
brush_options
|
||||
),
|
||||
|
||||
@@ -390,7 +390,77 @@ pub(super) fn brush_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = lib.iter().position(|b| b.name == name).ok_or_else(|| bad(C, format!("no brush named `{name}`")))?;
|
||||
let mut v = serde_json::to_value(&lib[i]).unwrap_or_default();
|
||||
if let Some(params) = p.get("params") {
|
||||
let original = v.clone();
|
||||
merge(&mut v, params);
|
||||
if lib[i].kind.type_id() == "pattern" {
|
||||
let tiles = [
|
||||
("side_tile", "side"),
|
||||
("outer_corner_tile", "outer_corner"),
|
||||
("inner_corner_tile", "inner_corner"),
|
||||
("start_tile", "start"),
|
||||
("end_tile", "end"),
|
||||
];
|
||||
// Resolve all Original choices against the brush before any tile can clear the art
|
||||
// another corner borrows. Keep the matching frame with each copied tile.
|
||||
let originals: Vec<_> = tiles
|
||||
.iter()
|
||||
.map(|(_, art)| {
|
||||
let from = if art.ends_with("corner")
|
||||
&& original.get(art).is_none_or(Value::is_null)
|
||||
&& original.get(if *art == "outer_corner" { "outer_source" } else { "inner_source" }).is_none_or(Value::is_null)
|
||||
{
|
||||
if *art == "outer_corner" { "inner_corner" } else { "outer_corner" }
|
||||
} else {
|
||||
*art
|
||||
};
|
||||
(original.get(from).cloned().unwrap_or(Value::Null), original.get(format!("{from}_frame")).cloned().unwrap_or(Value::Null))
|
||||
})
|
||||
.collect();
|
||||
for ((tile, art), (original_art, original_frame)) in tiles.into_iter().zip(originals) {
|
||||
let frame = format!("{art}_frame");
|
||||
let swatch = format!("{art}_swatch");
|
||||
let Some(choice) = params.get(tile) else {
|
||||
if params.get(art).is_some() {
|
||||
if params.get(frame.as_str()).is_none() {
|
||||
v[frame.as_str()] = Value::Null;
|
||||
}
|
||||
if params.get(swatch.as_str()).is_none() {
|
||||
v[swatch.as_str()] = Value::Null;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let (choice, swatch_only) = if let Some(name) = choice.as_object() {
|
||||
(name.get("pattern").and_then(Value::as_str).ok_or_else(|| bad(C, "invalid pattern tile choice"))?, true)
|
||||
} else {
|
||||
(choice.as_str().ok_or_else(|| bad(C, "invalid pattern tile choice"))?, false)
|
||||
};
|
||||
if art == "side" && choice == "none" && !swatch_only {
|
||||
return Err(bad(C, "You must choose a side pattern."));
|
||||
}
|
||||
if choice == "original" && !swatch_only {
|
||||
v[art] = original_art;
|
||||
v[frame.as_str()] = original_frame;
|
||||
v[swatch.as_str()] = Value::Null;
|
||||
} else if !swatch_only && (choice == "none" || (art.ends_with("corner") && brush::CornerSource::parse(choice).is_some())) {
|
||||
v[art] = Value::Null;
|
||||
v[frame.as_str()] = Value::Null;
|
||||
v[swatch.as_str()] = Value::Null;
|
||||
if art.ends_with("corner") {
|
||||
v[if art == "outer_corner" { "outer_source" } else { "inner_source" }] = json!(choice);
|
||||
}
|
||||
} else {
|
||||
let pattern = s.doc()?.doc.pattern(choice).ok_or_else(|| bad(C, format!("no pattern named `{choice}`")))?;
|
||||
let copy = Node::new(NodeId(0), NodeKind::Group { children: pattern.art.clone(), clip: false });
|
||||
v[art] = serde_json::to_value(copy).unwrap_or_default();
|
||||
v[frame.as_str()] = serde_json::to_value(pattern.tile).unwrap_or_default();
|
||||
v[swatch.as_str()] = json!(choice);
|
||||
if art.ends_with("corner") {
|
||||
v[if art == "outer_corner" { "outer_source" } else { "inner_source" }] = Value::Null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let new_name = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty() && *n != name).map(str::to_string);
|
||||
if let Some(nn) = &new_name {
|
||||
|
||||
Reference in New Issue
Block a user