ravenrs_sheet/engine/cmd/
draw.rs1use crate::base::CellRef;
4use crate::model::{Anchor, Color, Shape, ShapeKind};
5use serde_json::{Value as Json, json};
6
7use super::{CommandSpec, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, ok, str_param, u32_param};
8
9pub fn specs() -> Vec<CommandSpec> {
10 vec![
11 cmd!(noundo "draw.pen", "Pen", ["Draw", "Tools"], None, "{on?: bool, color?: hex, width?: 2}", has_doc, pen),
12 cmd!(noundo "draw.eraser", "Eraser", ["Draw", "Tools"], None, "{on?: bool}", has_doc, eraser),
13 cmd!(noundo "draw.lasso", "Lasso Select", ["Draw", "Tools"], None, "{} (selects ink by dragging; same as Select Objects)", has_doc, |s, _| {
14 s.draw_tool = String::new();
15 ok()
16 }),
17 cmd!("draw.stroke", "Ink Stroke", [], None, "{points: [[x,y],…] in sheet points, color?: hex, width?: 2}", has_doc, stroke),
18 cmd!(
19 "draw.inkToShape",
20 "Ink to Shape",
21 ["Draw", "Convert"],
22 None,
23 "{id?} converts an ink stroke (default: the last) into a rectangle, ellipse, triangle or line",
24 has_doc,
25 ink_to_shape
26 ),
27 ]
28}
29
30fn pen(s: &mut Session, p: &Json) -> Result<Json> {
31 let on = bool_param(p, "on").unwrap_or(s.draw_tool != "pen");
32 s.draw_tool = if on { "pen".into() } else { String::new() };
33 if let Some(c) = str_param(p, "color") {
34 s.draw_color = c.to_string();
35 }
36 if let Some(w) = f64_param(p, "width") {
37 s.draw_width = w.clamp(0.5, 40.0) as f32;
38 }
39 Ok(json!({"tool": s.draw_tool, "color": s.draw_color, "width": s.draw_width}))
40}
41
42fn eraser(s: &mut Session, p: &Json) -> Result<Json> {
43 let on = bool_param(p, "on").unwrap_or(s.draw_tool != "eraser");
44 s.draw_tool = if on { "eraser".into() } else { String::new() };
45 Ok(json!({"tool": s.draw_tool}))
46}
47
48fn parse_points(t: &str) -> Vec<[f32; 2]> {
49 t.split(';').filter_map(|pt| pt.split_once(',').and_then(|(x, y)| Some([x.parse().ok()?, y.parse().ok()?]))).collect()
50}
51
52fn stroke(s: &mut Session, p: &Json) -> Result<Json> {
53 let pts: Vec<[f64; 2]> = p
54 .get("points")
55 .and_then(Json::as_array)
56 .map(|a| {
57 a.iter()
58 .filter_map(|q| Some([q.get(0)?.as_f64()?, q.get(1)?.as_f64()?]))
59 .filter(|q| q[0].is_finite() && q[1].is_finite())
60 .take(20_000)
61 .collect()
62 })
63 .unwrap_or_default();
64 if pts.len() < 2 {
65 return Err(bad("draw.stroke", "a stroke needs at least two points"));
66 }
67 let minx = pts.iter().map(|q| q[0]).fold(f64::INFINITY, f64::min).max(0.0);
68 let miny = pts.iter().map(|q| q[1]).fold(f64::INFINITY, f64::min).max(0.0);
69 let maxx = pts.iter().map(|q| q[0]).fold(f64::NEG_INFINITY, f64::max);
70 let maxy = pts.iter().map(|q| q[1]).fold(f64::NEG_INFINITY, f64::max);
71 let color =
72 str_param(p, "color").map(str::to_string).unwrap_or_else(|| if s.draw_color.is_empty() { "#1F5FC9".into() } else { s.draw_color.clone() });
73 let width = f64_param(p, "width").map(|w| w as f32).unwrap_or(if s.draw_width <= 0.0 { 2.0 } else { s.draw_width }).clamp(0.5, 40.0);
74 let d = s.doc()?;
75 let si = d.wb.active_sheet();
76 let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
77 let cell = CellRef::new(sh.row_at(miny), sh.col_at(minx));
78 let dx = (minx - sh.col_left(cell.col)) as f32;
79 let dy = (miny - sh.row_top(cell.row)) as f32;
80 let text = pts.iter().map(|q| format!("{:.1},{:.1}", q[0] - minx, q[1] - miny)).collect::<Vec<_>>().join(";");
81 let id = d.wb.next_object_id();
82 let shape = Shape {
83 math: Default::default(),
84 id,
85 kind: ShapeKind::Ink,
86 anchor: Anchor {
87 cell,
88 dx,
89 dy,
90 width: (maxx - minx).max(1.0) as f32,
91 height: (maxy - miny).max(1.0) as f32,
92 mode: crate::model::AnchorMode::Absolute,
94 },
95 fill: Color::from_hex(&format!("#{:02X}0000", width.round().clamp(0.0, 255.0) as u8)).unwrap_or_default(),
96 line: Color::from_hex(&color).unwrap_or(Color::rgb(0x1F, 0x5F, 0xC9)),
97 text,
98 format: Default::default(),
99 };
100 edit(s, |cx| {
101 cx.sheet_mut(si)?.shapes.push(shape);
102 Ok(json!({"shape": id}))
103 })
104}
105
106pub fn ink_width(sh: &Shape) -> f32 {
108 match sh.fill {
109 Color::Rgb(v) => ((v >> 16) & 0xFF).max(1) as f32,
110 _ => 2.0,
111 }
112}
113
114pub fn ink_points(sh: &Shape) -> Vec<[f32; 2]> {
115 parse_points(&sh.text)
116}
117
118fn ink_to_shape(s: &mut Session, p: &Json) -> Result<Json> {
119 let si = s.doc()?.wb.active_sheet();
120 let id = u32_param(p, "id");
121 edit(s, |cx| {
122 let sh = cx.sheet_mut(si)?;
123 let idx = match id {
124 Some(id) => sh.shapes.iter().position(|x| x.id == id && x.kind == ShapeKind::Ink),
125 None => sh.shapes.iter().rposition(|x| x.kind == ShapeKind::Ink),
126 }
127 .ok_or_else(|| bad("draw.inkToShape", "no ink stroke to convert"))?;
128 let ink = sh.shapes.get_mut(idx).ok_or_else(|| bad("draw.inkToShape", "gone"))?;
129 let pts = parse_points(&ink.text);
130 let (w, h) = (ink.anchor.width.max(1.0), ink.anchor.height.max(1.0));
131 let closed = match (pts.first(), pts.last()) {
132 (Some(a), Some(b)) => ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt() < 0.25 * w.max(h),
133 _ => false,
134 };
135 let kind = if !closed {
136 ShapeKind::Line
137 } else {
138 let near = |cx: f32, cy: f32| pts.iter().any(|q| ((q[0] - cx).powi(2) + (q[1] - cy).powi(2)).sqrt() < 0.15 * w.min(h).max(8.0));
140 let corners = [near(0.0, 0.0), near(w, 0.0), near(0.0, h), near(w, h)].iter().filter(|b| **b).count();
141 let apex = near(w / 2.0, 0.0);
142 if corners >= 3 {
143 ShapeKind::Rectangle
144 } else if apex && near(0.0, h) && near(w, h) {
145 ShapeKind::Triangle
146 } else {
147 ShapeKind::Ellipse
148 }
149 };
150 let color = ink.line;
151 ink.kind = kind;
152 ink.fill = if kind == ShapeKind::Line { color } else { Color::Auto };
153 ink.text = String::new();
154 if kind == ShapeKind::Line && pts.first().zip(pts.last()).is_some_and(|(a, b)| a[1] > b[1]) {
155 }
157 Ok(json!({"kind": format!("{kind:?}")}))
158 })
159}