mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Object > Pattern > Tile Edge Color... picks the colour of the tile edge
(a layer-colour preset or a custom colour), stored in the preference
patternTileEdgeColor and passed to the renderer. Pattern Options gains
Show Swatch Bounds: a dashed outline of the part of the tiling the swatch
repeats (the period of brick and hex tilings).
Commands: prefs.set {patternTileEdgeColor}, pattern.options
{showSwatchBounds}, ui.tileEdgeColor. Model: PatternDef.show_swatch_bounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
813 lines
30 KiB
Rust
813 lines
30 KiB
Rust
//! Pattern swatches (Object → Pattern, Pattern Options panel) and Repeat (Object → Repeat).
|
|
//!
|
|
//! **Pattern editing mode** works like isolation: `object.pattern.make` / `object.pattern.edit`
|
|
//! put a copy of the tile art into a temporary top layer (`Document::pattern_edit` records it),
|
|
//! which the renderer draws alone with dimmed copies around it; hit testing only reaches that
|
|
//! layer. `object.pattern.done` stores the layer's art as the pattern definition,
|
|
//! `object.pattern.cancel` restores the previous definition. Both remove the layer.
|
|
//!
|
|
//! **Repeat** objects are live nodes (`NodeKind::Repeat`) holding the source art and the
|
|
//! arrangement; `vectorcraft_doc::pattern` evaluates the instances.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_color::Paint;
|
|
use vectorcraft_doc::pattern::{self, Overlap, PATTERN_EDIT_LAYER, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType, pattern_paint};
|
|
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection};
|
|
use vectorcraft_geom::Rect;
|
|
|
|
use super::edit::selected_roots;
|
|
use super::*;
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
// ---------- Pattern ----------
|
|
cmd!(
|
|
"object.pattern.make",
|
|
"Make",
|
|
["Object", "Pattern"],
|
|
None,
|
|
"{ids?, name?, tileType?: grid|brickByRow|brickByColumn|hexByColumn|hexByRow, brickOffset?: 0..1 (0.5), width?, height?, edit?: bool (true)} make a pattern swatch from the selection and enter pattern editing mode → {name}",
|
|
has_selection,
|
|
pattern_make
|
|
),
|
|
cmd!(
|
|
"object.pattern.edit",
|
|
"Edit Pattern",
|
|
["Object", "Pattern"],
|
|
Some("Cmd+Shift+F8"),
|
|
"{name?} edit a pattern (default: the selected object's fill pattern) in pattern editing mode",
|
|
can_edit_pattern,
|
|
pattern_edit
|
|
),
|
|
cmd!(
|
|
"object.pattern.done",
|
|
"Done",
|
|
[],
|
|
None,
|
|
"{} leave pattern editing mode, saving the tile art into the pattern",
|
|
in_pattern_edit,
|
|
pattern_done
|
|
),
|
|
cmd!("object.pattern.cancel", "Cancel", [], None, "{} leave pattern editing mode, discarding changes", in_pattern_edit, pattern_cancel),
|
|
cmd!(
|
|
"object.pattern.saveCopy",
|
|
"Save a Copy",
|
|
[],
|
|
None,
|
|
"{name?} while editing, save the current tile as a new pattern swatch → {name}",
|
|
in_pattern_edit,
|
|
pattern_save_copy
|
|
),
|
|
cmd!(
|
|
"pattern.options",
|
|
"Pattern Options",
|
|
["Window", "Pattern Options"],
|
|
None,
|
|
"{name? (default: the pattern being edited), newName?, tileType?, brickOffset?, width?, height?, hSpacing?, vSpacing?, sizeTileToArt?: bool, overlap?: {h: left|right, v: top|bottom}, copies?: 3|5|7|9, dimCopies?: %, showTileEdge?: bool, showSwatchBounds?: bool (outline the part of the tiling the swatch repeats, dashed)}",
|
|
has_doc,
|
|
pattern_options
|
|
),
|
|
cmd!(
|
|
"pattern.delete",
|
|
"Delete Pattern",
|
|
[],
|
|
None,
|
|
"{name} delete a pattern swatch (objects painted with it lose that paint)",
|
|
has_doc,
|
|
pattern_delete
|
|
),
|
|
cmd!(query "pattern.list", "List Patterns", [], None, "{} → {patterns: [{name, tileType, width, height, ...}], editing?}", has_doc, pattern_list),
|
|
cmd!(
|
|
"pattern.transform",
|
|
"Transform Patterns",
|
|
[],
|
|
None,
|
|
"{ids?, matrix: [a,b,c,d,e,f]} transform only the pattern placement of the objects' pattern paints (Transform Patterns)",
|
|
has_selection_or_ids,
|
|
pattern_transform
|
|
),
|
|
// ---------- Repeat ----------
|
|
cmd!(
|
|
"object.repeat.radial",
|
|
"Radial",
|
|
["Object", "Repeat"],
|
|
None,
|
|
"{ids?, instances?: n (8), radius?: pt} radial repeat of the selection (or switch the selected repeat to radial) → {id}",
|
|
has_selection,
|
|
repeat_radial
|
|
),
|
|
cmd!(
|
|
"object.repeat.grid",
|
|
"Grid",
|
|
["Object", "Repeat"],
|
|
None,
|
|
"{ids?, hSpacing?: pt, vSpacing?: pt (¼ of the art size), rows?: n (3), cols?: n (3)} grid repeat of the selection → {id}",
|
|
has_selection,
|
|
repeat_grid
|
|
),
|
|
cmd!(
|
|
"object.repeat.mirror",
|
|
"Mirror",
|
|
["Object", "Repeat"],
|
|
None,
|
|
"{ids?, angle?: deg (90 = vertical axis), offset?: pt (10)} mirror repeat of the selection → {id}",
|
|
has_selection,
|
|
repeat_mirror
|
|
),
|
|
cmd!(
|
|
"object.repeat.release",
|
|
"Release",
|
|
["Object", "Repeat"],
|
|
None,
|
|
"{} release the selected repeats back to their source art → {ids}",
|
|
has_repeat,
|
|
repeat_release
|
|
),
|
|
cmd!(
|
|
"object.repeat.options",
|
|
"Options…",
|
|
["Object", "Repeat"],
|
|
None,
|
|
"{instances?, radius?, reverseOverlap?, startAngle?, endAngle?, center?: [x,y] | hSpacing?, vSpacing?, rows?, cols?, gridType?, flipRows?, flipCols? | angle?} set the options of the selected repeats",
|
|
has_repeat,
|
|
repeat_options
|
|
),
|
|
cmd!(
|
|
"object.repeat.expand",
|
|
"Expand Repeat",
|
|
[],
|
|
None,
|
|
"{} replace the selected repeats by groups of their instances → {ids}",
|
|
has_repeat,
|
|
repeat_expand
|
|
),
|
|
]
|
|
}
|
|
|
|
// ---------- predicates ----------
|
|
|
|
fn editing(s: &Session) -> Option<&PatternEdit> {
|
|
s.active()?.doc.pattern_edit.as_ref()
|
|
}
|
|
|
|
fn in_pattern_edit(s: &Session) -> std::result::Result<(), String> {
|
|
has_doc(s)?;
|
|
editing(s).map(|_| ()).ok_or_else(|| "not in pattern editing mode".into())
|
|
}
|
|
|
|
fn can_edit_pattern(s: &Session) -> std::result::Result<(), String> {
|
|
has_doc(s)?;
|
|
if editing(s).is_some() {
|
|
return Err("already editing a pattern".into());
|
|
}
|
|
if s.active().is_some_and(|st| st.doc.patterns.is_empty()) { Err("the document has no patterns".into()) } else { Ok(()) }
|
|
}
|
|
|
|
fn has_selection_or_ids(s: &Session) -> std::result::Result<(), String> {
|
|
has_doc(s)
|
|
}
|
|
|
|
fn selected_repeats(s: &Session) -> Vec<NodeId> {
|
|
let Some(st) = s.active() else { return vec![] };
|
|
let mut out = vec![];
|
|
for id in &st.selection.objects {
|
|
for a in st.doc.ancestry(*id).unwrap_or_default().into_iter().rev() {
|
|
if st.doc.node(a).is_some_and(pattern::is_repeat) {
|
|
if !out.contains(&a) {
|
|
out.push(a);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn has_repeat(s: &Session) -> std::result::Result<(), String> {
|
|
has_selection(s)?;
|
|
if selected_repeats(s).is_empty() { Err("select a repeat".into()) } else { Ok(()) }
|
|
}
|
|
|
|
// ---------- helpers ----------
|
|
|
|
fn ids_json(ids: &[NodeId]) -> Value {
|
|
json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })
|
|
}
|
|
|
|
fn roots(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
|
match ids_param(p, "ids") {
|
|
Some(ids) => {
|
|
let st = s.doc()?;
|
|
let mut sel = Selection::default();
|
|
sel.set(ids.iter().copied().filter(|id| st.doc.node(*id).is_some_and(|n| !n.is_layer())));
|
|
Ok(sel.in_paint_order(&st.doc))
|
|
}
|
|
None => selected_roots(s),
|
|
}
|
|
}
|
|
|
|
fn unique_pattern_name(d: &Document, base: &str) -> String {
|
|
if d.pattern(base).is_none() && d.swatch(base).is_none() {
|
|
return base.to_string();
|
|
}
|
|
(2..).map(|i| format!("{base} {i}")).find(|n| d.pattern(n).is_none() && d.swatch(n).is_none()).unwrap()
|
|
}
|
|
|
|
fn next_pattern_name(d: &Document) -> String {
|
|
(1..).map(|i| format!("New Pattern {i}")).find(|n| d.pattern(n).is_none() && d.swatch(n).is_none()).unwrap()
|
|
}
|
|
|
|
/// Add pattern `def` and its swatch.
|
|
pub(crate) fn add_pattern(d: &mut Document, def: PatternDef) {
|
|
d.swatches.push(vectorcraft_color::Swatch { name: def.name.clone(), paint: pattern_paint(&def.name), global: false, spot: false });
|
|
d.patterns.push(def);
|
|
}
|
|
|
|
/// Fresh-id deep copies of `nodes`.
|
|
fn copies(d: &mut Document, nodes: &[Arc<Node>]) -> Vec<Node> {
|
|
nodes.iter().map(|n| d.reid(n)).collect()
|
|
}
|
|
|
|
/// Enter pattern editing mode for `name`: a top layer with copies of the tile art.
|
|
fn enter_edit(d: &mut Document, sel: &mut Selection, name: &str, original: Option<PatternDef>) -> Result<NodeId> {
|
|
let art = d.pattern(name).map(|p| p.art.clone()).ok_or_else(|| EngineError::Other(format!("no pattern `{name}`")))?;
|
|
let layer = d.add_layer(Some(PATTERN_EDIT_LAYER));
|
|
let nodes = copies(d, &art);
|
|
let mut ids = vec![];
|
|
for n in nodes {
|
|
ids.push(n.id);
|
|
d.insert(Some(layer), usize::MAX, n)?;
|
|
}
|
|
d.pattern_edit = Some(PatternEdit { pattern: name.to_string(), layer, original });
|
|
sel.set(ids);
|
|
Ok(layer)
|
|
}
|
|
|
|
/// Leave editing mode: drop the layer, return the edit record.
|
|
fn leave_edit(d: &mut Document, sel: &mut Selection) -> Result<(PatternEdit, Vec<Arc<Node>>)> {
|
|
let pe = d.pattern_edit.take().ok_or_else(|| EngineError::Other("not in pattern editing mode".into()))?;
|
|
let art = match d.remove(pe.layer) {
|
|
Ok(l) => l.children().cloned().unwrap_or_default(),
|
|
Err(_) => vec![],
|
|
};
|
|
sel.clear();
|
|
Ok((pe, art))
|
|
}
|
|
|
|
fn after_edit_mode(s: &mut Session, layer: Option<NodeId>) -> Result<()> {
|
|
let st = s.doc_mut()?;
|
|
st.isolation = layer;
|
|
st.active_layer = layer.or_else(|| st.doc.default_layer());
|
|
st.revision += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn tile_type_param(p: &Value, cmd: &str, current: TileType) -> Result<TileType> {
|
|
let off = p.get("brickOffset").and_then(Value::as_f64).map(|o| if o > 1.0 { o / 100.0 } else { o });
|
|
match str_param(p, "tileType") {
|
|
Some(t) => TileType::parse(t, off).ok_or_else(|| bad(cmd, format!("unknown tileType `{t}` ({})", TileType::IDS.join("|")))),
|
|
None => Ok(match (current, off) {
|
|
(TileType::BrickByRow { .. }, Some(o)) => TileType::BrickByRow { offset: o },
|
|
(TileType::BrickByColumn { .. }, Some(o)) => TileType::BrickByColumn { offset: o },
|
|
(c, _) => c,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ---------- Pattern commands ----------
|
|
|
|
fn pattern_make(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "object.pattern.make";
|
|
if editing(s).is_some() {
|
|
return Err(bad(C, "already in pattern editing mode"));
|
|
}
|
|
let roots = roots(s, p)?;
|
|
if roots.is_empty() {
|
|
return Err(bad(C, "select the art for the pattern tile"));
|
|
}
|
|
let tt = tile_type_param(p, C, TileType::Grid)?;
|
|
let (w, h) = (p.get("width").and_then(Value::as_f64), p.get("height").and_then(Value::as_f64));
|
|
if w.is_some_and(|v| !(v.is_finite() && v > 0.0)) || h.is_some_and(|v| !(v.is_finite() && v > 0.0)) {
|
|
return Err(bad(C, "width/height must be > 0"));
|
|
}
|
|
let edit = bool_or(p, "edit", true);
|
|
let wanted = str_param(p, "name").map(str::to_string);
|
|
let name = s.edit("Make Pattern", |d, sel| {
|
|
let name = match &wanted {
|
|
Some(n) => unique_pattern_name(d, n),
|
|
None => next_pattern_name(d),
|
|
};
|
|
let src: Vec<Arc<Node>> = roots.iter().filter_map(|id| d.node(*id).cloned()).map(Arc::new).collect();
|
|
let art: Vec<Arc<Node>> = copies(d, &src).into_iter().map(Arc::new).collect();
|
|
let mut def = PatternDef::new(&name, art);
|
|
def.tile_type = tt;
|
|
let c = def.tile.center();
|
|
def.tile = Rect::from_center_size(c, (w.unwrap_or(def.tile.width()).max(1e-3), h.unwrap_or(def.tile.height()).max(1e-3)));
|
|
add_pattern(d, def);
|
|
if edit {
|
|
enter_edit(d, sel, &name, None)?;
|
|
}
|
|
Ok(name)
|
|
})?;
|
|
if edit {
|
|
let layer = editing(s).map(|e| e.layer);
|
|
after_edit_mode(s, layer)?;
|
|
}
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn pattern_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "object.pattern.edit";
|
|
let name = match str_param(p, "name") {
|
|
Some(n) => n.to_string(),
|
|
None => {
|
|
// The selected object's fill (or stroke) pattern.
|
|
let st = s.doc()?;
|
|
st.selection
|
|
.objects
|
|
.iter()
|
|
.filter_map(|id| st.doc.node(*id))
|
|
.find_map(|n| {
|
|
n.appearance.items.iter().find_map(|it| match it {
|
|
vectorcraft_doc::AppearanceItem::Fill(f) => pattern_name(&f.paint),
|
|
vectorcraft_doc::AppearanceItem::Stroke(sk) => pattern_name(&sk.paint),
|
|
})
|
|
})
|
|
.ok_or_else(|| bad(C, "give a pattern name or select an object painted with a pattern"))?
|
|
}
|
|
};
|
|
let orig = s.doc()?.doc.pattern(&name).cloned().ok_or_else(|| bad(C, format!("no pattern `{name}`")))?;
|
|
let layer = s.edit("Edit Pattern", |d, sel| enter_edit(d, sel, &name, Some(orig)))?;
|
|
after_edit_mode(s, Some(layer))?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn pattern_name(p: &Paint) -> Option<String> {
|
|
match p {
|
|
Paint::Pattern { pattern, .. } => Some(pattern.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn pattern_done(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let name = s.edit("Done Editing Pattern", |d, sel| {
|
|
let (pe, art) = leave_edit(d, sel)?;
|
|
if let Some(def) = d.pattern_mut(&pe.pattern) {
|
|
def.art = art;
|
|
if def.size_tile_to_art {
|
|
def.fit_tile_to_art();
|
|
}
|
|
}
|
|
Ok(pe.pattern)
|
|
})?;
|
|
after_edit_mode(s, None)?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn pattern_cancel(s: &mut Session, _: &Value) -> Result<Value> {
|
|
s.edit("Cancel Pattern Editing", |d, sel| {
|
|
let (pe, _) = leave_edit(d, sel)?;
|
|
match pe.original {
|
|
Some(orig) => {
|
|
if let Some(def) = d.pattern_mut(&pe.pattern) {
|
|
*def = orig;
|
|
}
|
|
}
|
|
None => {
|
|
d.patterns.retain(|p| p.name != pe.pattern);
|
|
d.swatches.retain(|sw| !matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == pe.pattern));
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
after_edit_mode(s, None)?;
|
|
ok()
|
|
}
|
|
|
|
fn pattern_save_copy(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let wanted = str_param(p, "name").map(str::to_string);
|
|
let name = s.edit("Save a Copy", |d, _| {
|
|
let pe = d.pattern_edit.clone().ok_or_else(|| EngineError::Other("not in pattern editing mode".into()))?;
|
|
let mut def = d.pattern(&pe.pattern).cloned().ok_or_else(|| EngineError::Other("pattern missing".into()))?;
|
|
let art: Vec<Arc<Node>> = d.node(pe.layer).and_then(|l| l.children().cloned()).unwrap_or_default();
|
|
let name = unique_pattern_name(d, wanted.as_deref().unwrap_or(&format!("{} copy", pe.pattern)));
|
|
def.name = name.clone();
|
|
def.art = copies(d, &art).into_iter().map(Arc::new).collect();
|
|
if def.size_tile_to_art {
|
|
def.fit_tile_to_art();
|
|
}
|
|
add_pattern(d, def);
|
|
Ok(name)
|
|
})?;
|
|
Ok(json!({ "name": name }))
|
|
}
|
|
|
|
fn pattern_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "pattern.options";
|
|
let name = match str_param(p, "name") {
|
|
Some(n) => n.to_string(),
|
|
None => editing(s).map(|e| e.pattern.clone()).ok_or_else(|| bad(C, "give `name` (or edit a pattern)"))?,
|
|
};
|
|
let cur = s.doc()?.doc.pattern(&name).cloned().ok_or_else(|| bad(C, format!("no pattern `{name}`")))?;
|
|
let tt = tile_type_param(p, C, cur.tile_type)?;
|
|
let num = |k: &str| p.get(k).and_then(Value::as_f64);
|
|
for k in ["width", "height"] {
|
|
if num(k).is_some_and(|v| !(v.is_finite() && v > 0.0)) {
|
|
return Err(bad(C, format!("{k} must be > 0")));
|
|
}
|
|
}
|
|
let new_name = str_param(p, "newName").map(str::to_string).filter(|n| !n.is_empty() && *n != name);
|
|
if let Some(n) = &new_name
|
|
&& s.doc()?.doc.pattern(n).is_some()
|
|
{
|
|
return Err(bad(C, format!("a pattern named `{n}` exists")));
|
|
}
|
|
let overlap = match p.get("overlap") {
|
|
Some(o) => Overlap {
|
|
right_in_front: str_param(o, "h").map_or(cur.overlap.right_in_front, |v| v == "right"),
|
|
bottom_in_front: str_param(o, "v").map_or(cur.overlap.bottom_in_front, |v| v == "bottom"),
|
|
},
|
|
None => cur.overlap,
|
|
};
|
|
let edit_layer = editing(s).filter(|e| e.pattern == name).map(|e| e.layer);
|
|
let out_name = new_name.clone().unwrap_or(name.clone());
|
|
s.edit("Pattern Options", |d, _| {
|
|
// While editing, the tile art is the edit layer's.
|
|
let live_art: Option<Vec<Arc<Node>>> = edit_layer.and_then(|l| d.node(l)).and_then(|l| l.children().cloned());
|
|
let def = d.pattern_mut(&name).ok_or_else(|| EngineError::Other("pattern missing".into()))?;
|
|
def.tile_type = tt;
|
|
def.overlap = overlap;
|
|
if let Some(v) = num("hSpacing") {
|
|
def.h_spacing = v;
|
|
}
|
|
if let Some(v) = num("vSpacing") {
|
|
def.v_spacing = v;
|
|
}
|
|
if let Some(v) = p.get("sizeTileToArt").and_then(Value::as_bool) {
|
|
def.size_tile_to_art = v;
|
|
}
|
|
if let Some(v) = num("copies") {
|
|
def.copies = (v as u32).clamp(1, 15) | 1;
|
|
}
|
|
if let Some(v) = num("dimCopies") {
|
|
def.dim_copies = v.clamp(0.0, 100.0) as f32;
|
|
}
|
|
if let Some(v) = p.get("showTileEdge").and_then(Value::as_bool) {
|
|
def.show_tile_edge = v;
|
|
}
|
|
if let Some(v) = p.get("showSwatchBounds").and_then(Value::as_bool) {
|
|
def.show_swatch_bounds = v;
|
|
}
|
|
if def.size_tile_to_art {
|
|
let saved = std::mem::take(&mut def.art);
|
|
def.art = live_art.clone().unwrap_or_else(|| saved.clone());
|
|
def.fit_tile_to_art();
|
|
def.art = saved;
|
|
} else if num("width").is_some() || num("height").is_some() {
|
|
let c = def.tile.center();
|
|
def.tile = Rect::from_center_size(c, (num("width").unwrap_or(def.tile.width()), num("height").unwrap_or(def.tile.height())));
|
|
}
|
|
if let Some(nn) = &new_name {
|
|
def.name = nn.clone();
|
|
rename_pattern_uses(d, &name, nn);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({ "name": out_name }))
|
|
}
|
|
|
|
/// Rename a pattern in swatches, paints and the edit record.
|
|
fn rename_pattern_uses(d: &mut Document, old: &str, new: &str) {
|
|
let fix = |p: &mut Paint| {
|
|
if let Paint::Pattern { pattern, .. } = p
|
|
&& pattern == old
|
|
{
|
|
*pattern = new.to_string();
|
|
}
|
|
};
|
|
for sw in d.swatches.iter_mut().chain(d.swatch_groups.iter_mut().flat_map(|g| g.swatches.iter_mut())) {
|
|
if sw.name == old {
|
|
sw.name = new.to_string();
|
|
}
|
|
fix(&mut sw.paint);
|
|
}
|
|
if let Some(pe) = &mut d.pattern_edit
|
|
&& pe.pattern == old
|
|
{
|
|
pe.pattern = new.to_string();
|
|
}
|
|
let ids = nodes_using(d, Some(old));
|
|
for id in ids {
|
|
if let Some(n) = d.node_mut(id) {
|
|
for it in n.appearance.items.iter_mut() {
|
|
match it {
|
|
vectorcraft_doc::AppearanceItem::Fill(f) => fix(&mut f.paint),
|
|
vectorcraft_doc::AppearanceItem::Stroke(s) => fix(&mut s.paint),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn nodes_using(d: &Document, name: Option<&str>) -> Vec<NodeId> {
|
|
let mut ids = vec![];
|
|
d.walk(|n| {
|
|
if pattern::uses_pattern(n, name) {
|
|
ids.push(n.id);
|
|
}
|
|
});
|
|
ids
|
|
}
|
|
|
|
fn pattern_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "pattern.delete";
|
|
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing name"))?.to_string();
|
|
if s.doc()?.doc.pattern(&name).is_none() {
|
|
return Err(bad(C, format!("no pattern `{name}`")));
|
|
}
|
|
if editing(s).is_some_and(|e| e.pattern == name) {
|
|
return Err(bad(C, "finish editing the pattern first"));
|
|
}
|
|
let n = s.edit("Delete Pattern", |d, _| {
|
|
d.patterns.retain(|p| p.name != name);
|
|
let is_it = |sw: &vectorcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == name);
|
|
d.swatches.retain(|sw| !is_it(sw));
|
|
for g in &mut d.swatch_groups {
|
|
g.swatches.retain(|sw| !is_it(sw));
|
|
}
|
|
let ids = nodes_using(d, Some(&name));
|
|
for id in &ids {
|
|
if let Some(n) = d.node_mut(*id) {
|
|
for it in n.appearance.items.iter_mut() {
|
|
let p = match it {
|
|
vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint,
|
|
vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint,
|
|
};
|
|
if matches!(p, Paint::Pattern { pattern, .. } if *pattern == name) {
|
|
*p = Paint::None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(ids.len())
|
|
})?;
|
|
Ok(json!({ "unpainted": n }))
|
|
}
|
|
|
|
fn pattern_list(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let st = s.doc()?;
|
|
let pats: Vec<Value> = st
|
|
.doc
|
|
.patterns
|
|
.iter()
|
|
.map(|p| {
|
|
json!({
|
|
"name": p.name,
|
|
"tileType": p.tile_type.id(),
|
|
"brickOffset": match p.tile_type { TileType::BrickByRow { offset } | TileType::BrickByColumn { offset } => offset, _ => 0.5 },
|
|
"tile": [p.tile.x0, p.tile.y0, p.tile.width(), p.tile.height()],
|
|
"width": p.tile.width(),
|
|
"height": p.tile.height(),
|
|
"hSpacing": p.h_spacing,
|
|
"vSpacing": p.v_spacing,
|
|
"sizeTileToArt": p.size_tile_to_art,
|
|
"overlap": {"h": if p.overlap.right_in_front { "right" } else { "left" }, "v": if p.overlap.bottom_in_front { "bottom" } else { "top" }},
|
|
"copies": p.copies,
|
|
"dimCopies": p.dim_copies,
|
|
"showTileEdge": p.show_tile_edge,
|
|
"showSwatchBounds": p.show_swatch_bounds,
|
|
"artCount": p.art.len(),
|
|
})
|
|
})
|
|
.collect();
|
|
let editing = st.doc.pattern_edit.as_ref().map(|e| json!({"name": e.pattern, "layer": e.layer.0}));
|
|
Ok(json!({ "patterns": pats, "editing": editing }))
|
|
}
|
|
|
|
fn pattern_transform(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "pattern.transform";
|
|
let m = matrix_param(p, "matrix").ok_or_else(|| bad(C, "missing `matrix` [a,b,c,d,e,f]"))?;
|
|
let ids = targets(s, p)?;
|
|
if ids.is_empty() {
|
|
return Err(bad(C, "nothing selected"));
|
|
}
|
|
let n = s.edit("Transform Patterns", |d, _| {
|
|
let mut all = vec![];
|
|
for id in &ids {
|
|
if let Some(n) = d.node(*id) {
|
|
n.walk(&mut |c| {
|
|
if pattern::uses_pattern(c, None) {
|
|
all.push(c.id);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
for id in &all {
|
|
if let Some(n) = d.node_mut(*id) {
|
|
pattern::transform_pattern_paints(n, m);
|
|
}
|
|
}
|
|
Ok(all.len())
|
|
})?;
|
|
Ok(json!({ "count": n }))
|
|
}
|
|
|
|
// ---------- Repeat ----------
|
|
|
|
/// Make a repeat from the selection, or switch the selected repeat's kind.
|
|
fn make_repeat(s: &mut Session, p: &Value, label: &str, make: impl Fn(Vec<Arc<Node>>) -> RepeatSpec) -> Result<Value> {
|
|
let existing = selected_repeats(s);
|
|
if ids_param(p, "ids").is_none()
|
|
&& let [r] = existing.as_slice()
|
|
{
|
|
let r = *r;
|
|
s.edit(label, |d, _| {
|
|
let n = d.node_mut(r).ok_or(EngineError::NoNode(r))?;
|
|
if let NodeKind::Repeat(spec) = &mut n.kind {
|
|
spec.kind = make(spec.source.clone()).kind;
|
|
}
|
|
Ok(())
|
|
})?;
|
|
return Ok(json!({ "id": r.0 }));
|
|
}
|
|
let roots = roots(s, p)?;
|
|
if roots.is_empty() {
|
|
return Err(EngineError::Other("select the art to repeat".into()));
|
|
}
|
|
let id = s.edit(label, |d, sel| {
|
|
let top = *roots.last().unwrap();
|
|
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
|
let nodes: Vec<Arc<Node>> = roots.iter().filter_map(|id| d.node(*id).cloned()).map(Arc::new).collect();
|
|
let id = d.alloc_id();
|
|
d.insert(par, idx + 1, Node::new(id, NodeKind::Repeat(make(nodes))))?;
|
|
for r in &roots {
|
|
d.remove(*r)?;
|
|
}
|
|
sel.set([id]);
|
|
Ok(id)
|
|
})?;
|
|
Ok(json!({ "id": id.0 }))
|
|
}
|
|
|
|
fn repeat_radial(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let n = f64_or(p, "instances", 8.0);
|
|
if !(1.0..=1000.0).contains(&n) {
|
|
return Err(bad("object.repeat.radial", "instances must be 1..1000"));
|
|
}
|
|
let radius = p.get("radius").and_then(Value::as_f64).filter(|r| r.is_finite() && *r >= 0.0);
|
|
make_repeat(s, p, "Radial Repeat", |src| RepeatSpec::radial(src, n as u32, radius))
|
|
}
|
|
|
|
fn repeat_grid(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (hs, vs) = (p.get("hSpacing").and_then(Value::as_f64), p.get("vSpacing").and_then(Value::as_f64));
|
|
let rows = p.get("rows").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32);
|
|
let cols = p.get("cols").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32);
|
|
make_repeat(s, p, "Grid Repeat", |src| {
|
|
let b = vectorcraft_doc::live::nodes_bounds(&src).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0));
|
|
let mut r = RepeatSpec::grid(src, hs.unwrap_or(b.width() / 4.0), vs.unwrap_or(b.height() / 4.0));
|
|
if let RepeatKind::Grid { rows: rr, cols: cc, .. } = &mut r.kind {
|
|
*rr = rows.unwrap_or(*rr);
|
|
*cc = cols.unwrap_or(*cc);
|
|
}
|
|
r
|
|
})
|
|
}
|
|
|
|
fn repeat_mirror(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let angle = f64_or(p, "angle", 90.0);
|
|
let offset = f64_or(p, "offset", 10.0);
|
|
make_repeat(s, p, "Mirror Repeat", |src| RepeatSpec::mirror(src, angle, offset))
|
|
}
|
|
|
|
fn repeat_release(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let reps = selected_repeats(s);
|
|
let ids = s.edit("Release Repeat", |d, sel| {
|
|
let mut out = vec![];
|
|
for r in &reps {
|
|
let Some(n) = d.node(*r).cloned() else { continue };
|
|
let (par, idx, _) = d.position(*r).ok_or(EngineError::NoNode(*r))?;
|
|
d.remove(*r)?;
|
|
for (k, c) in n.children().into_iter().flatten().enumerate() {
|
|
out.push(c.id);
|
|
d.insert(par, idx + k, (**c).clone())?;
|
|
}
|
|
}
|
|
sel.set(out.iter().copied());
|
|
Ok(out)
|
|
})?;
|
|
Ok(ids_json(&ids))
|
|
}
|
|
|
|
fn fix_ids(d: &mut Document, n: &mut Node) {
|
|
if n.id == NodeId(0) {
|
|
n.id = d.alloc_id();
|
|
}
|
|
if let Some(ch) = n.children_mut() {
|
|
for c in ch.iter_mut() {
|
|
fix_ids(d, Arc::make_mut(c));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn repeat_expand(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let reps = selected_repeats(s);
|
|
let ids = s.edit("Expand Repeat", |d, sel| {
|
|
let mut out = vec![];
|
|
for r in &reps {
|
|
let Some(n) = d.node(*r).cloned() else { continue };
|
|
let mut g = vectorcraft_doc::live::expanded_group(&n, None);
|
|
|
|
// The group keeps the repeat's id; every generated instance node gets a fresh one.
|
|
if let Some(ch) = g.children_mut() {
|
|
for c in ch.iter_mut() {
|
|
fix_ids(d, Arc::make_mut(c));
|
|
}
|
|
}
|
|
let (par, idx, _) = d.position(*r).ok_or(EngineError::NoNode(*r))?;
|
|
d.remove(*r)?;
|
|
d.insert(par, idx, g)?;
|
|
out.push(*r);
|
|
}
|
|
sel.set(out.iter().copied());
|
|
Ok(out)
|
|
})?;
|
|
Ok(ids_json(&ids))
|
|
}
|
|
|
|
fn repeat_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "object.repeat.options";
|
|
let reps = selected_repeats(s);
|
|
let num = |k: &str| p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite());
|
|
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
|
|
if num("instances").is_some_and(|n| !(1.0..=1000.0).contains(&n)) {
|
|
return Err(bad(C, "instances must be 1..1000"));
|
|
}
|
|
let grid_type = match str_param(p, "gridType") {
|
|
Some(t) => Some(TileType::parse(t, num("brickOffset")).ok_or_else(|| bad(C, format!("unknown gridType `{t}`")))?),
|
|
None => None,
|
|
};
|
|
let center = point_param(p, "center");
|
|
s.edit("Repeat Options", |d, _| {
|
|
for r in &reps {
|
|
let Some(NodeKind::Repeat(spec)) = d.node_mut(*r).map(|n| &mut n.kind) else { continue };
|
|
match &mut spec.kind {
|
|
RepeatKind::Radial { instances, radius, center: c, reverse_overlap, start_angle, end_angle } => {
|
|
if let Some(v) = num("instances") {
|
|
*instances = v as u32;
|
|
}
|
|
if let Some(v) = num("radius") {
|
|
*radius = v.max(0.0);
|
|
}
|
|
if let Some(v) = flag("reverseOverlap") {
|
|
*reverse_overlap = v;
|
|
}
|
|
if let Some(v) = num("startAngle") {
|
|
*start_angle = v;
|
|
}
|
|
if let Some(v) = num("endAngle") {
|
|
*end_angle = v;
|
|
}
|
|
if let Some(v) = center {
|
|
*c = v;
|
|
}
|
|
}
|
|
RepeatKind::Grid { h_spacing, v_spacing, rows, cols, grid_type: gt, flip_rows, flip_cols } => {
|
|
if let Some(v) = num("hSpacing") {
|
|
*h_spacing = v;
|
|
}
|
|
if let Some(v) = num("vSpacing") {
|
|
*v_spacing = v;
|
|
}
|
|
if let Some(v) = num("rows") {
|
|
*rows = (v as u32).clamp(1, 500);
|
|
}
|
|
if let Some(v) = num("cols") {
|
|
*cols = (v as u32).clamp(1, 500);
|
|
}
|
|
if let Some(t) = grid_type {
|
|
*gt = t;
|
|
}
|
|
if let Some(v) = flag("flipRows") {
|
|
*flip_rows = v;
|
|
}
|
|
if let Some(v) = flag("flipCols") {
|
|
*flip_cols = v;
|
|
}
|
|
}
|
|
RepeatKind::Mirror { angle, center: c } => {
|
|
if let Some(v) = num("angle") {
|
|
*angle = v;
|
|
}
|
|
if let Some(v) = center {
|
|
*c = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|