mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 14:41:19 +00:00
The History panel gets a New Document from State button at its bottom,
which runs the new edit.newDocumentFromState command. The command makes
a new document holding the art of a History panel row ({index?}, the
current state when omitted) and makes it the active document. An index
that names no state is an error and changes nothing.
The new document takes the next Untitled-N name, is marked unsaved, has
nothing selected, no steps to undo or redo and no modified date, and
leaves symbol, opacity-mask and pattern editing; the art drawn in a
pattern's tile is stored in the pattern, as Done stores it. Its links point at the files the
source finds, including files found in the source's folder away from the
links' paths. Those files are looked for only within the folders the
asking caller may read; for a caller without limits, within the folders
the caller that opened the source may read. The source
document, its steps and its saved state stay as they are; typing or a
drag in progress is first kept as a step, as Undo keeps it. While an IME
composes, the command waits, as Undo does.
Docs: docs/mcp.md, docs/target-app-parity.md.
874 lines
34 KiB
Rust
874 lines
34 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::{outermost, selected_roots};
|
||
use super::pathops::Beside;
|
||
use super::{
|
||
CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, f64_or, has_doc, has_selection, ids_param, matrix_param, ok, point_param,
|
||
str_param, targets,
|
||
};
|
||
|
||
pub fn specs() -> Vec<CommandSpec> {
|
||
vec![
|
||
// ---------- Pattern ----------
|
||
cmd!(
|
||
"object.pattern.make",
|
||
"Make",
|
||
["Object", "Pattern"],
|
||
None,
|
||
"{ids?, name? (trimmed; blank: New Pattern N), 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; refused while an opacity mask is edited → {name}",
|
||
can_make_pattern,
|
||
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; refused while an opacity mask is edited",
|
||
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? (trimmed; blank: <pattern> copy)} 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? (trimmed; blank: the name is kept), 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?|id?, 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())
|
||
}
|
||
|
||
/// Make: a selection, and no opacity mask or symbol is being edited
|
||
/// ([`super::maskedit::outside_mask_editing`], [`outside_symbol_editing`]), since the tile layer
|
||
/// would replace that mode's editing layer as the isolated layer.
|
||
fn can_make_pattern(s: &Session) -> std::result::Result<(), String> {
|
||
has_selection(s)?;
|
||
super::maskedit::outside_mask_editing(s)?;
|
||
outside_symbol_editing(s)
|
||
}
|
||
|
||
/// No symbol is being edited (symbol editing mode shows the symbol in isolation mode).
|
||
fn outside_symbol_editing(s: &Session) -> std::result::Result<(), String> {
|
||
if s.active().is_some_and(|st| st.doc.symbol_edit.is_some()) { Err("exit isolation mode first".into()) } else { Ok(()) }
|
||
}
|
||
|
||
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());
|
||
}
|
||
super::maskedit::outside_mask_editing(s)?;
|
||
outside_symbol_editing(s)?;
|
||
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(outermost(&st.doc, 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_name_taken(base) {
|
||
return base.to_string();
|
||
}
|
||
(2..).map(|i| format!("{base} {i}")).find(|n| d.pattern(n).is_none() && !d.swatch_name_taken(n)).unwrap_or_else(|| base.to_string())
|
||
}
|
||
|
||
fn next_pattern_name(d: &Document) -> String {
|
||
(1..).map(|i| format!("New Pattern {i}")).find(|n| d.pattern(n).is_none() && !d.swatch_name_taken(n)).unwrap_or_else(|| "New Pattern".to_string())
|
||
}
|
||
|
||
/// 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::trim).filter(|n| !n.is_empty()).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,
|
||
}
|
||
}
|
||
|
||
/// Save the edited tile art into the pattern and leave pattern editing mode (`object.pattern.done`).
|
||
/// `object.exitIsolation` runs it too while a pattern is edited.
|
||
pub(crate) fn pattern_done(s: &mut Session, _: &Value) -> Result<Value> {
|
||
if editing(s).is_some() {
|
||
s.end_text_edit();
|
||
}
|
||
let name = s.edit("Done Editing Pattern", finish_edit)?;
|
||
after_edit_mode(s, None)?;
|
||
Ok(json!({ "name": name }))
|
||
}
|
||
|
||
/// Leave pattern editing mode in `d`, storing the editing layer's art as the pattern's tile art
|
||
/// (resizing the tile to the art when the pattern does so) → the pattern's name.
|
||
pub(crate) fn finish_edit(d: &mut Document, sel: &mut Selection) -> Result<String> {
|
||
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)
|
||
}
|
||
|
||
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::trim).filter(|n| !n.is_empty()).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.trim())));
|
||
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::trim).filter(|n| !n.is_empty() && *n != name).map(str::to_string);
|
||
if let Some(n) = &new_name {
|
||
let d = &s.doc()?.doc;
|
||
if d.pattern(n).is_some() {
|
||
return Err(bad(C, format!("a pattern named `{n}` exists")));
|
||
}
|
||
if swatch_name_clash(d, &name, n) {
|
||
return Err(bad(C, format!("a swatch or color group 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 }))
|
||
}
|
||
|
||
/// Is `new` taken for pattern `old` by a swatch or a color group (they share one namespace,
|
||
/// [`Document::swatch_name_taken`])? It is free when the swatches named `new` are one or more
|
||
/// swatches of the pattern and none of the pattern's swatches is named `old` (those take the new
|
||
/// name in [`rename_pattern_uses`]).
|
||
fn swatch_name_clash(d: &Document, old: &str, new: &str) -> bool {
|
||
let own = |w: &vectorcraft_color::Swatch| pattern_name(&w.paint).is_some_and(|p| p == old);
|
||
let mut holders = d.swatches_iter().filter(|w| w.name == new).peekable();
|
||
let only_own = holders.peek().is_some() && holders.all(own);
|
||
d.swatch_name_taken(new) && (!only_own || d.swatches_iter().any(|w| w.name == old && own(w)))
|
||
}
|
||
|
||
/// Rename a pattern in swatches, paints and the edit record. Of the swatches named `old`, only the
|
||
/// pattern's own take the new name.
|
||
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 && pattern_name(&sw.paint).is_some_and(|p| p == 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 id = d.alloc_id();
|
||
d.wrap(&roots, |nodes| Node::new(id, NodeKind::Repeat(make(nodes))))?;
|
||
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![];
|
||
let mut released = Beside::replacing(d, &reps);
|
||
for r in &reps {
|
||
let Some(n) = d.node(*r).cloned() else { continue };
|
||
let art: Vec<Node> = n.children().into_iter().flatten().map(|c| (**c).clone()).collect();
|
||
out.extend(art.iter().map(|c| c.id));
|
||
released.put(d, *r, art)?;
|
||
}
|
||
released.finish(d)?;
|
||
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| {
|
||
for r in &reps {
|
||
expand_repeat(d, *r)?;
|
||
}
|
||
sel.set(reps.iter().copied());
|
||
Ok(reps.clone())
|
||
})?;
|
||
Ok(ids_json(&ids))
|
||
}
|
||
|
||
/// Repeat `r` replaced by a group of its instances, keeping its id and place; every generated
|
||
/// instance node gets a fresh id.
|
||
fn expand_repeat(d: &mut Document, r: NodeId) -> Result<()> {
|
||
let Some(n) = d.node(r).cloned() else { return Ok(()) };
|
||
let hooks = vectorcraft_doc::live::Hooks { symbols: Some(d), ..Default::default() };
|
||
let mut g = vectorcraft_doc::live::expanded_group_hooks(&n, hooks);
|
||
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)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// Object › Expand: every repeat in the subtrees of `roots` becomes a group of its instances
|
||
/// ([`expand_repeat`]), inner ones first so an outer repeat repeats plain art. Returns how many.
|
||
pub(crate) fn expand_repeats(d: &mut Document, roots: &[NodeId]) -> Result<usize> {
|
||
let mut reps = vec![];
|
||
for r in roots {
|
||
if let Some(n) = d.node(*r) {
|
||
n.walk(&mut |c| {
|
||
if pattern::is_repeat(c) {
|
||
reps.push(c.id);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
for r in reps.iter().rev() {
|
||
expand_repeat(d, *r)?;
|
||
}
|
||
Ok(reps.len())
|
||
}
|
||
|
||
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()
|
||
}
|