158 lines
6.4 KiB
Rust
158 lines
6.4 KiB
Rust
//! Draw tab: pen and eraser tools, ink strokes, Ink to Shape.
|
|
|
|
use gridcraft_core::CellRef;
|
|
use gridcraft_model::*;
|
|
use serde_json::{Value as Json, json};
|
|
|
|
use super::*;
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!(noundo "draw.pen", "Pen", ["Draw", "Tools"], None, "{on?: bool, color?: hex, width?: 2}", has_doc, pen),
|
|
cmd!(noundo "draw.eraser", "Eraser", ["Draw", "Tools"], None, "{on?: bool}", has_doc, eraser),
|
|
cmd!(noundo "draw.lasso", "Lasso Select", ["Draw", "Tools"], None, "{} (selects ink by dragging; same as Select Objects)", has_doc, |s, _| {
|
|
s.draw_tool = String::new();
|
|
ok()
|
|
}),
|
|
cmd!("draw.stroke", "Ink Stroke", [], None, "{points: [[x,y],…] in sheet points, color?: hex, width?: 2}", has_doc, stroke),
|
|
cmd!(
|
|
"draw.inkToShape",
|
|
"Ink to Shape",
|
|
["Draw", "Convert"],
|
|
None,
|
|
"{id?} converts an ink stroke (default: the last) into a rectangle, ellipse, triangle or line",
|
|
has_doc,
|
|
ink_to_shape
|
|
),
|
|
]
|
|
}
|
|
|
|
fn pen(s: &mut Session, p: &Json) -> Result<Json> {
|
|
let on = bool_param(p, "on").unwrap_or(s.draw_tool != "pen");
|
|
s.draw_tool = if on { "pen".into() } else { String::new() };
|
|
if let Some(c) = str_param(p, "color") {
|
|
s.draw_color = c.to_string();
|
|
}
|
|
if let Some(w) = f64_param(p, "width") {
|
|
s.draw_width = w.clamp(0.5, 40.0) as f32;
|
|
}
|
|
Ok(json!({"tool": s.draw_tool, "color": s.draw_color, "width": s.draw_width}))
|
|
}
|
|
|
|
fn eraser(s: &mut Session, p: &Json) -> Result<Json> {
|
|
let on = bool_param(p, "on").unwrap_or(s.draw_tool != "eraser");
|
|
s.draw_tool = if on { "eraser".into() } else { String::new() };
|
|
Ok(json!({"tool": s.draw_tool}))
|
|
}
|
|
|
|
fn parse_points(t: &str) -> Vec<[f32; 2]> {
|
|
t.split(';').filter_map(|pt| pt.split_once(',').and_then(|(x, y)| Some([x.parse().ok()?, y.parse().ok()?]))).collect()
|
|
}
|
|
|
|
fn stroke(s: &mut Session, p: &Json) -> Result<Json> {
|
|
let pts: Vec<[f64; 2]> = p
|
|
.get("points")
|
|
.and_then(Json::as_array)
|
|
.map(|a| {
|
|
a.iter()
|
|
.filter_map(|q| Some([q.get(0)?.as_f64()?, q.get(1)?.as_f64()?]))
|
|
.filter(|q| q[0].is_finite() && q[1].is_finite())
|
|
.take(20_000)
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
if pts.len() < 2 {
|
|
return Err(bad("draw.stroke", "a stroke needs at least two points"));
|
|
}
|
|
let minx = pts.iter().map(|q| q[0]).fold(f64::INFINITY, f64::min).max(0.0);
|
|
let miny = pts.iter().map(|q| q[1]).fold(f64::INFINITY, f64::min).max(0.0);
|
|
let maxx = pts.iter().map(|q| q[0]).fold(f64::NEG_INFINITY, f64::max);
|
|
let maxy = pts.iter().map(|q| q[1]).fold(f64::NEG_INFINITY, f64::max);
|
|
let color =
|
|
str_param(p, "color").map(str::to_string).unwrap_or_else(|| if s.draw_color.is_empty() { "#1F5FC9".into() } else { s.draw_color.clone() });
|
|
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);
|
|
let d = s.doc()?;
|
|
let si = d.wb.active_sheet;
|
|
let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
|
|
let cell = CellRef::new(sh.row_at(miny), sh.col_at(minx));
|
|
let dx = (minx - sh.col_left(cell.col)) as f32;
|
|
let dy = (miny - sh.row_top(cell.row)) as f32;
|
|
let text = pts.iter().map(|q| format!("{:.1},{:.1}", q[0] - minx, q[1] - miny)).collect::<Vec<_>>().join(";");
|
|
let id = d.wb.next_object_id();
|
|
let shape = Shape {
|
|
id,
|
|
kind: ShapeKind::Ink,
|
|
anchor: Anchor {
|
|
cell,
|
|
dx,
|
|
dy,
|
|
width: (maxx - minx).max(1.0) as f32,
|
|
height: (maxy - miny).max(1.0) as f32,
|
|
// Ink is a freehand mark: pinned where it was drawn, it does not follow cells.
|
|
mode: gridcraft_model::AnchorMode::Absolute,
|
|
},
|
|
fill: Color::from_hex(&format!("#{:02X}0000", width.round().clamp(0.0, 255.0) as u8)).unwrap_or_default(),
|
|
line: Color::from_hex(&color).unwrap_or(Color::rgb(0x1F, 0x5F, 0xC9)),
|
|
text,
|
|
};
|
|
edit(s, |cx| {
|
|
cx.sheet_mut(si)?.shapes.push(shape);
|
|
Ok(json!({"shape": id}))
|
|
})
|
|
}
|
|
|
|
/// Ink width is stored in the red channel of `fill` (shapes have no width field).
|
|
pub fn ink_width(sh: &Shape) -> f32 {
|
|
match sh.fill {
|
|
Color::Rgb(v) => ((v >> 16) & 0xFF).max(1) as f32,
|
|
_ => 2.0,
|
|
}
|
|
}
|
|
|
|
pub fn ink_points(sh: &Shape) -> Vec<[f32; 2]> {
|
|
parse_points(&sh.text)
|
|
}
|
|
|
|
fn ink_to_shape(s: &mut Session, p: &Json) -> Result<Json> {
|
|
let si = s.doc()?.wb.active_sheet;
|
|
let id = u32_param(p, "id");
|
|
edit(s, |cx| {
|
|
let sh = cx.sheet_mut(si)?;
|
|
let idx = match id {
|
|
Some(id) => sh.shapes.iter().position(|x| x.id == id && x.kind == ShapeKind::Ink),
|
|
None => sh.shapes.iter().rposition(|x| x.kind == ShapeKind::Ink),
|
|
}
|
|
.ok_or_else(|| bad("draw.inkToShape", "no ink stroke to convert"))?;
|
|
let ink = sh.shapes.get_mut(idx).ok_or_else(|| bad("draw.inkToShape", "gone"))?;
|
|
let pts = parse_points(&ink.text);
|
|
let (w, h) = (ink.anchor.width.max(1.0), ink.anchor.height.max(1.0));
|
|
let closed = match (pts.first(), pts.last()) {
|
|
(Some(a), Some(b)) => ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt() < 0.25 * w.max(h),
|
|
_ => false,
|
|
};
|
|
let kind = if !closed {
|
|
ShapeKind::Line
|
|
} else {
|
|
// Corners hugging the bounding box mean a rectangle; one apex at the top a triangle.
|
|
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));
|
|
let corners = [near(0.0, 0.0), near(w, 0.0), near(0.0, h), near(w, h)].iter().filter(|b| **b).count();
|
|
let apex = near(w / 2.0, 0.0);
|
|
if corners >= 3 {
|
|
ShapeKind::Rectangle
|
|
} else if apex && near(0.0, h) && near(w, h) {
|
|
ShapeKind::Triangle
|
|
} else {
|
|
ShapeKind::Ellipse
|
|
}
|
|
};
|
|
let color = ink.line;
|
|
ink.kind = kind;
|
|
ink.fill = if kind == ShapeKind::Line { color } else { Color::Auto };
|
|
ink.text = String::new();
|
|
if kind == ShapeKind::Line && pts.first().zip(pts.last()).is_some_and(|(a, b)| a[1] > b[1]) {
|
|
// Lines are drawn top-left to bottom-right; keep the stroke direction simple.
|
|
}
|
|
Ok(json!({"kind": format!("{kind:?}")}))
|
|
})
|
|
}
|