mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Draw Normal/Behind/Inside modes (Shift+D)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a2fd35cd32
commit
213b0927ee
5 files changed
+119
-4
No files matched your search
@@ -26,6 +26,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
path_create
|
||||
),
|
||||
cmd!("view.drawMode", "Drawing Mode", [], Some("Shift+D"), "{mode?: normal|behind|inside} (no param cycles; inside needs one selected path)", has_doc, draw_mode),
|
||||
cmd!(
|
||||
"text.create",
|
||||
"Create Text",
|
||||
@@ -46,18 +47,79 @@ pub(crate) fn add_art(s: &mut Session, label: &str, kind: NodeKind, name: Option
|
||||
|
||||
pub(crate) fn add_node(s: &mut Session, label: &str, kind: NodeKind, appearance: Appearance, name: Option<String>) -> Result<Value> {
|
||||
let parent = s.doc()?.insertion_parent();
|
||||
let mode = s.draw_mode;
|
||||
let inside = s.draw_inside;
|
||||
let behind_of = s.doc()?.selection.in_paint_order(&s.doc()?.doc).first().copied();
|
||||
let id = s.edit(label, |d, sel| {
|
||||
let id = d.alloc_id();
|
||||
let mut n = Node::new(id, kind);
|
||||
n.appearance = appearance;
|
||||
n.name = name;
|
||||
d.insert(parent, usize::MAX, n)?;
|
||||
match (mode, inside) {
|
||||
(crate::DrawMode::Inside, Some(target)) if d.node(target).is_some() => {
|
||||
// Wrap the target once in a clip group [mask copy, target, …new art].
|
||||
let group = match d.parent_of(target).and_then(|p| d.node(p).map(|pn| (p, pn.kind.clone()))) {
|
||||
Some((p, NodeKind::Group { clip: true, children })) if children.get(1).is_some_and(|c| c.id == target) => p,
|
||||
_ => {
|
||||
let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?;
|
||||
let mut mask = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?;
|
||||
mask.id = d.alloc_id();
|
||||
mask.appearance = Appearance::basic(drawcraft_color::Paint::None, drawcraft_color::Paint::None, 0.0);
|
||||
if let NodeKind::Path { clipping, .. } = &mut mask.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let gid = d.alloc_id();
|
||||
d.insert(par, idx + 1, Node::new(gid, NodeKind::Group { children: vec![std::sync::Arc::new(mask)], clip: true }))?;
|
||||
d.move_node(target, Some(gid), usize::MAX)?;
|
||||
gid
|
||||
}
|
||||
};
|
||||
d.insert(Some(group), usize::MAX, n)?;
|
||||
}
|
||||
(crate::DrawMode::Behind, _) => match behind_of.and_then(|b| d.position(b)) {
|
||||
Some((par, idx, _)) => {
|
||||
d.insert(par, idx, n)?;
|
||||
}
|
||||
None => {
|
||||
d.insert(parent, 0, n)?;
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
d.insert(parent, usize::MAX, n)?;
|
||||
}
|
||||
}
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
fn draw_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mode = match str_param(p, "mode") {
|
||||
Some("behind") => crate::DrawMode::Behind,
|
||||
Some("inside") => crate::DrawMode::Inside,
|
||||
Some("normal") => crate::DrawMode::Normal,
|
||||
None => match s.draw_mode {
|
||||
crate::DrawMode::Normal => crate::DrawMode::Behind,
|
||||
crate::DrawMode::Behind if s.doc()?.selection.len() == 1 => crate::DrawMode::Inside,
|
||||
_ => crate::DrawMode::Normal,
|
||||
},
|
||||
Some(o) => return Err(bad("view.drawMode", format!("unknown mode `{o}`"))),
|
||||
};
|
||||
if mode == crate::DrawMode::Inside {
|
||||
let st = s.doc()?;
|
||||
let target = st.selection.objects.first().copied().filter(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_))));
|
||||
match target {
|
||||
Some(t) if st.selection.len() == 1 => s.draw_inside = Some(t),
|
||||
_ => return Err(bad("view.drawMode", "Draw Inside needs exactly one selected path, compound path or text")),
|
||||
}
|
||||
} else {
|
||||
s.draw_inside = None;
|
||||
}
|
||||
s.draw_mode = mode;
|
||||
Ok(json!({ "mode": mode }))
|
||||
}
|
||||
|
||||
fn path_kind(path: PathData, live: Option<LiveShape>) -> NodeKind {
|
||||
NodeKind::Path { path, rule: FillRule::NonZero, live, clipping: false, guide: false }
|
||||
}
|
||||
|
||||
@@ -131,6 +131,16 @@ impl DocState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where new art goes (Illustrator's drawing modes, Shift+D cycles).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DrawMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Behind,
|
||||
Inside,
|
||||
}
|
||||
|
||||
/// Application preferences that affect commands.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Prefs {
|
||||
@@ -163,7 +173,13 @@ pub struct Session {
|
||||
pub journal: Vec<(String, Value)>,
|
||||
pub(crate) tool: Box<dyn Tool>,
|
||||
pub(crate) last_view: ViewInfo,
|
||||
/// Draw Normal / Behind / Inside (toolbar drawing modes).
|
||||
pub draw_mode: DrawMode,
|
||||
/// The path new art is drawn inside (Draw Inside).
|
||||
pub draw_inside: Option<NodeId>,
|
||||
untitled_counter: u32,
|
||||
/// Session-level state of the menu commands (saved selections, guide lock).
|
||||
pub(crate) menu: cmd::menucmds::MenuState,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -184,7 +200,10 @@ impl Session {
|
||||
journal: vec![],
|
||||
tool: drawcraft_tools::create("selection"),
|
||||
last_view: ViewInfo::default(),
|
||||
draw_mode: DrawMode::Normal,
|
||||
draw_inside: None,
|
||||
untitled_counter: 0,
|
||||
menu: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +377,10 @@ mod tests_draw2;
|
||||
#[cfg(test)]
|
||||
mod tests_file;
|
||||
#[cfg(test)]
|
||||
mod tests_menucmds;
|
||||
#[cfg(test)]
|
||||
mod tests_pathops;
|
||||
#[cfg(test)]
|
||||
mod tests_panelcmds;
|
||||
#[cfg(test)]
|
||||
mod tests_xform;
|
||||
@@ -87,3 +87,25 @@ fn pdf_roundtrip_through_engine() {
|
||||
s.execute("document.open", &json!({"name": "x.pdf", "dataBase64": b64})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node_count() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_behind_and_inside() {
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "behind"})).unwrap();
|
||||
let b = s.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
let order: Vec<u64> = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id.0).collect();
|
||||
assert_eq!(order, vec![b, a]);
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap();
|
||||
let c = s.execute("shape.ellipse", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let g = d.parent_of(NodeId(c)).unwrap();
|
||||
assert_eq!(d.node(g).unwrap().kind_label(), "Clip Group");
|
||||
assert_eq!(d.parent_of(NodeId(a)), Some(g));
|
||||
// A second shape goes into the same clip group.
|
||||
s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(g).unwrap().children().unwrap().len(), 4);
|
||||
s.execute("view.drawMode", &json!({"mode": "normal"})).unwrap();
|
||||
assert!(s.draw_inside.is_none());
|
||||
}
|
||||
@@ -187,6 +187,10 @@ pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context) {
|
||||
let _ = app.run("paint.none", json!({}));
|
||||
continue;
|
||||
}
|
||||
"Shift+D" => {
|
||||
let _ = app.run("view.drawMode", json!({}));
|
||||
continue;
|
||||
}
|
||||
"F" => {
|
||||
let _ = app.run("view.screenMode", json!({}));
|
||||
continue;
|
||||
|
||||
@@ -224,9 +224,13 @@ fn bottom_controls(app: &mut DrawcraftApp, ui: &mut Ui, t: &Tokens) {
|
||||
ui.vertical_centered(|ui| {
|
||||
let modes = ["dc-draw-normal", "dc-draw-behind", "dc-draw-inside"];
|
||||
let names = ["Draw Normal", "Draw Behind", "Draw Inside"];
|
||||
let m = app.ui.draw_mode as usize % 3;
|
||||
if widgets::icon_button(ui, modes[m], &format!("{} (Shift+D)", names[m]), false, 26.0).clicked() {
|
||||
app.ui.draw_mode = (app.ui.draw_mode + 1) % 3;
|
||||
let m = match app.session.draw_mode {
|
||||
drawcraft_engine::DrawMode::Normal => 0,
|
||||
drawcraft_engine::DrawMode::Behind => 1,
|
||||
drawcraft_engine::DrawMode::Inside => 2,
|
||||
};
|
||||
if widgets::icon_button(ui, modes[m], &format!("{} (Shift+D)", names[m]), m != 0, 26.0).clicked() {
|
||||
app.run("view.drawMode", json!({})).ok();
|
||||
}
|
||||
if widgets::icon_button(ui, "dc-screen-mode", "Change Screen Mode (F)", false, 26.0).clicked() {
|
||||
app.run("view.screenMode", json!({})).ok();
|
||||
|
||||
Reference in new issue
Block a user