use super::*;
use super::sketch_panel::{sketch_constraint_signature, sketch_perpendicular_should_swap};
impl EngineState {
pub(super) fn refresh_sketch_overlay(&mut self) {
let world_per_pixel = self.camera.world_per_pixel();
let (json, preview, leaders, glyphs) = match self.sketch_edit.as_ref() {
Some(edit) => (
edit.session.overlay_json_with_state(world_per_pixel),
edit.session.preview_overlay_json(
world_per_pixel,
&edit.pending,
edit.hover_uv,
&edit.handdraw_stroke,
),
edit.session.dim_leaders_overlay_json_with_state(world_per_pixel),
edit.session
.constraint_glyphs_overlay_json_with_state(world_per_pixel),
),
None => return,
};
let _ = self.set_overlay_json(&json);
let _ = self.set_overlay_json(&preview);
let _ = self.set_overlay_json(&leaders);
let _ = self.set_overlay_json(&glyphs);
}
pub fn sketch_uv_at(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let edit = self.sketch_edit.as_ref()?;
let ray = self.camera.pick_ray(x, y);
crate::sketch::ray_plane_uv(&edit.session.plane, ray.origin, ray.dir)
}
pub(super) fn sketch_pick_radius(&self) -> f64 {
f64::from(crate::sketch::tessellate::POINT_SIZE_PX) * 1.5 * self.camera.world_per_pixel()
}
fn sketch_entity_at(&self, x: f64, y: f64) -> Option<serde_json::Value> {
let (u, v) = self.sketch_uv_at(x, y)?;
let radius = self.sketch_pick_radius();
let wpp = self.camera.world_per_pixel();
self.sketch_edit.as_ref().and_then(|edit| {
edit.session
.pick_entity(u, v, radius)
.or_else(|| edit.session.pick_constraint(u, v, radius, wpp))
})
}
pub(super) fn set_sketch_hover(&mut self, new_hover: Option<serde_json::Value>) -> bool {
let changed = match self.sketch_edit.as_ref() {
Some(edit) => {
!crate::sketch::entity_ref_eq(edit.session.hovered.as_ref(), new_hover.as_ref())
}
None => false,
};
if changed {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.session.set_hover(new_hover);
}
self.refresh_sketch_overlay();
self.dirty = true;
}
changed
}
pub fn sketch_hover_at(&mut self, x: f64, y: f64) -> bool {
if self.sketch_edit.is_none() {
return false;
}
let uv = self.sketch_uv_at(x, y);
let preview_live = match self.sketch_edit.as_mut() {
Some(edit) => {
edit.hover_uv = uv;
edit.session.tool.is_some() && !edit.pending.is_empty()
}
None => false,
};
let new_hover = self.sketch_entity_at(x, y);
let changed = self.set_sketch_hover(new_hover);
if preview_live && !changed {
self.refresh_sketch_overlay();
self.dirty = true;
}
changed
}
pub fn sketch_clear_hover(&mut self) -> bool {
self.set_sketch_hover(None)
}
pub fn sketch_click_at(&mut self, x: f64, y: f64, additive: bool) {
let hit = self.sketch_entity_at(x, y);
let Some(edit) = self.sketch_edit.as_mut() else {
return;
};
match hit {
None => edit.session.clear_selection(),
Some(entity_ref) => {
if additive {
edit.session.toggle_selection(entity_ref);
} else {
edit.session.clear_selection();
edit.session.toggle_selection(entity_ref);
}
}
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_drag_begin(&mut self, x: f64, y: f64) -> bool {
let Some((u, v)) = self.sketch_uv_at(x, y) else {
return false;
};
let radius = self.sketch_pick_radius();
let points = self.sketch_edit.as_ref().and_then(|edit| {
match edit.session.hovered.as_ref() {
Some(entity_ref) => edit.session.drag_points_from_ref(entity_ref),
None => edit
.session
.pick_draggable_point(u, v, radius)
.and_then(|(id, fixed)| {
edit.session
.doc
.point(&id)
.map(|p| vec![(id, p.x, p.y, fixed)])
}),
}
});
let Some(points) = points else {
return false;
};
if let Some(edit) = self.sketch_edit.as_mut() {
edit.record_undo();
edit.drag = Some(SketchDrag { points, anchor: (u, v) });
}
true
}
pub fn sketch_drag_to(&mut self, x: f64, y: f64) {
let Some((u, v)) = self.sketch_uv_at(x, y) else {
return;
};
let Some(edit) = self.sketch_edit.as_mut() else {
return;
};
let Some(drag) = edit.drag.clone() else {
return;
};
let (du, dv) = (u - drag.anchor.0, v - drag.anchor.1);
let session = &mut edit.session;
for (id, ox, oy, _) in &drag.points {
if let Some(p) = session.doc.point_mut(id) {
p.x = *ox + du;
p.y = *oy + dv;
p.fixed = true;
}
}
match session.resolve() {
Ok(()) => {
for (id, _, _, orig_fixed) in &drag.points {
if let Some(p) = session.doc.point_mut(id) {
p.fixed = *orig_fixed;
}
}
}
Err(_) => {
for (id, ox, oy, orig_fixed) in &drag.points {
if let Some(p) = session.doc.point_mut(id) {
p.x = *ox;
p.y = *oy;
p.fixed = *orig_fixed;
}
}
}
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_drag_end(&mut self) {
let had_grab = self
.sketch_edit
.as_ref()
.map_or(false, |edit| edit.drag.is_some());
if !had_grab {
return;
}
let drop_tol = self.sketch_pick_radius();
if let Some(edit) = self.sketch_edit.as_mut() {
let dragged_single = edit.drag.as_ref().and_then(|drag| {
(drag.points.len() == 1).then(|| drag.points[0].0.clone())
});
edit.drag = None;
if let Some(point_id) = dragged_single {
crate::sketch::infer::infer_drop_constraint(
&mut edit.session.doc,
&point_id,
drop_tol,
);
}
let _ = edit.session.resolve();
if edit
.undo_stack
.last()
.map_or(false, |snap| snap.doc == edit.session.doc)
{
edit.undo_stack.pop();
}
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_selection_count(&self) -> usize {
self.sketch_edit
.as_ref()
.map_or(0, |edit| edit.session.selection.len())
}
pub fn sketch_selected_constraint_count(&self) -> usize {
self.sketch_edit.as_ref().map_or(0, |edit| {
edit.session
.selection
.iter()
.filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("constraint"))
.count()
})
}
}
impl EngineState {
pub fn sketch_set_tool(&mut self, tool: Option<&str>) {
let normalized = normalize_sketch_tool(tool);
if let Some(edit) = self.sketch_edit.as_mut() {
edit.session.tool = normalized;
edit.pending.clear();
edit.hover_uv = None;
edit.handdraw_stroke.clear();
} else {
return;
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_active_tool(&self) -> Option<&str> {
self.sketch_edit
.as_ref()
.and_then(|edit| edit.session.tool.as_deref())
}
pub fn sketch_pending_len(&self) -> usize {
self.sketch_edit.as_ref().map_or(0, |edit| edit.pending.len())
}
pub fn sketch_tool_click_at(&mut self, x: f64, y: f64) {
if self.sketch_active_tool() == Some("pickEdges") {
self.sketch_pick_edge_at(x, y);
return;
}
let Some((u, v)) = self.sketch_uv_at(x, y) else {
return;
};
self.sketch_tool_place_uv(u, v);
}
pub fn sketch_tool_place_uv(&mut self, u: f64, v: f64) {
let radius = self.sketch_pick_radius();
let tool = match self.sketch_edit.as_ref() {
Some(edit) => match edit.session.tool.clone() {
Some(tool) => tool,
None => return,
},
None => return,
};
if tool == "trim" {
self.sketch_trim_uv(u, v);
return;
}
if tool == "pickEdges" {
return;
}
if tool == "handdraw" {
return;
}
let Some(edit) = self.sketch_edit.as_mut() else {
return;
};
edit.record_undo();
let doc = &mut edit.session.doc;
match tool.as_str() {
"point" => {
doc.snap_or_add_point(u, v, radius);
edit.pending.clear();
}
"line" => {
if edit.pending.is_empty() {
let a = doc.snap_or_add_point(u, v, radius);
edit.pending.push(a);
} else {
let start = edit.pending.last().cloned().expect("pending non-empty");
let end = doc.snap_or_add_point(u, v, radius);
push_sketch_geometry(doc, "line", vec![start, end.clone()]);
edit.pending = vec![end];
}
}
"rect" => {
if edit.pending.is_empty() {
let a = doc.snap_or_add_point(u, v, radius);
edit.pending.push(a);
} else {
let a_id = edit.pending[0].clone();
let Some((ax, ay)) = doc.point(&a_id).map(|p| (p.x, p.y)) else {
edit.pending.clear();
return;
};
let (bx, by) = (u, v);
let b1 = doc.snap_or_add_point(bx, ay, radius);
let b2 = doc.snap_or_add_point(bx, by, radius);
let b3 = doc.snap_or_add_point(ax, by, radius);
push_sketch_geometry(doc, "line", vec![a_id.clone(), b1.clone()]);
push_sketch_geometry(doc, "line", vec![b1.clone(), b2.clone()]);
push_sketch_geometry(doc, "line", vec![b2.clone(), b3.clone()]);
push_sketch_geometry(doc, "line", vec![b3.clone(), a_id.clone()]);
push_rect_perpendicular_constraints(doc, [a_id, b1, b2, b3]);
edit.pending.clear();
}
}
"circle" => {
if edit.pending.is_empty() {
let c = doc.snap_or_add_point(u, v, radius);
edit.pending.push(c);
} else {
let center = edit.pending[0].clone();
let r = doc.snap_or_add_point(u, v, radius);
push_sketch_geometry(doc, "circle", vec![center, r]);
edit.pending.clear();
}
}
"arc" => {
if edit.pending.len() < 2 {
let p = doc.snap_or_add_point(u, v, radius);
edit.pending.push(p);
} else {
let center = edit.pending[0].clone();
let start = edit.pending[1].clone();
let end = doc.snap_or_add_point(u, v, radius);
push_sketch_geometry(doc, "arc", vec![center, start, end]);
edit.pending.clear();
}
}
"bezier" => {
if edit.pending.len() < 3 {
let p = doc.snap_or_add_point(u, v, radius);
edit.pending.push(p);
} else {
let p0 = edit.pending[0].clone();
let p1 = edit.pending[1].clone();
let p2 = edit.pending[2].clone();
let p3 = doc.snap_or_add_point(u, v, radius);
push_sketch_geometry(
doc,
"bezier",
vec![p0.clone(), p1.clone(), p2.clone(), p3.clone()],
);
push_sketch_construction_line(doc, vec![p0, p1]);
push_sketch_construction_line(doc, vec![p3, p2]);
edit.pending.clear();
}
}
_ => return,
}
self.resolve_active_sketch("draw-tool");
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_tool_cancel(&mut self) {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.pending.clear();
} else {
return;
}
self.refresh_sketch_overlay();
self.dirty = true;
}
}
fn normalize_sketch_tool(tool: Option<&str>) -> Option<String> {
match tool {
None | Some("select") | Some("") => None,
Some(t) => Some(t.to_string()),
}
}
fn push_sketch_geometry(
doc: &mut crate::sketch::SketchDoc,
geom_type: &str,
points: Vec<serde_json::Value>,
) {
let id = doc.next_geometry_id();
let mut extra = serde_json::Map::new();
extra.insert("construction".to_string(), serde_json::Value::Bool(false));
doc.geometries.push(crate::sketch::SketchGeometry {
id,
geom_type: geom_type.to_string(),
points,
extra,
});
}
fn push_sketch_construction_line(
doc: &mut crate::sketch::SketchDoc,
points: Vec<serde_json::Value>,
) {
let id = doc.next_geometry_id();
let mut extra = serde_json::Map::new();
extra.insert("construction".to_string(), serde_json::Value::Bool(true));
doc.geometries.push(crate::sketch::SketchGeometry {
id,
geom_type: "line".to_string(),
points,
extra,
});
}
fn push_rect_perpendicular_constraints(
doc: &mut crate::sketch::SketchDoc,
corners: [serde_json::Value; 4],
) {
use crate::sketch::doc::id_key;
let [a, b1, b2, b3] = corners;
let keys = [id_key(&a), id_key(&b1), id_key(&b2), id_key(&b3)];
for i in 0..keys.len() {
for j in (i + 1)..keys.len() {
if keys[i] == keys[j] {
return; }
}
}
let pairs = [
[a.clone(), b1.clone(), b1.clone(), b2.clone()],
[b1.clone(), b2.clone(), b2.clone(), b3.clone()],
[b2.clone(), b3.clone(), b3.clone(), a.clone()],
];
for pair in pairs {
let mut pts = pair.to_vec();
if sketch_perpendicular_should_swap(doc, &pts) {
pts.swap(0, 1);
}
push_geometric_constraint(doc, "⟂", pts);
}
}
fn push_geometric_constraint(
doc: &mut crate::sketch::SketchDoc,
ctype: &str,
points: Vec<serde_json::Value>,
) {
let sig = sketch_constraint_signature(ctype, &points);
let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
Some(t) => sketch_constraint_signature(t, c.points()) == sig,
None => false,
});
if duplicate {
return;
}
let id = doc.next_constraint_id();
let mut raw = serde_json::Map::new();
raw.insert("id".to_string(), id);
raw.insert("type".to_string(), serde_json::Value::String(ctype.to_string()));
raw.insert("points".to_string(), serde_json::Value::Array(points));
raw.insert("labelX".to_string(), serde_json::Value::from(0));
raw.insert("labelY".to_string(), serde_json::Value::from(0));
raw.insert(
"displayStyle".to_string(),
serde_json::Value::String(String::new()),
);
raw.insert("value".to_string(), serde_json::Value::Null);
raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
doc.constraints.push(crate::sketch::SketchConstraint { raw });
}
impl EngineState {
pub fn sketch_delete_selection(&mut self) -> bool {
use crate::sketch::doc::id_key;
use std::collections::HashSet;
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
if edit.session.selection.is_empty() {
return false;
}
edit.record_undo();
let mut sel_geo: HashSet<String> = HashSet::new();
let mut sel_pt: HashSet<String> = HashSet::new();
let mut sel_constraint: HashSet<String> = HashSet::new();
for r in &edit.session.selection {
match (r.get("kind").and_then(|v| v.as_str()), r.get("id")) {
(Some("geometry"), Some(id)) => {
sel_geo.insert(id_key(id));
}
(Some("point"), Some(id)) => {
sel_pt.insert(id_key(id));
}
(Some("constraint"), Some(id)) => {
sel_constraint.insert(id_key(id));
}
_ => {}
}
}
let doc = &mut edit.session.doc;
doc.geometries.retain(|g| {
if sel_geo.contains(&id_key(&g.id)) {
return false;
}
!g.points.iter().any(|pid| sel_pt.contains(&id_key(pid)))
});
doc.points.retain(|p| !sel_pt.contains(&id_key(&p.id)));
let referenced: HashSet<String> = doc
.geometries
.iter()
.flat_map(|g| g.points.iter().map(id_key))
.collect();
let mut removed_pts = sel_pt;
doc.points.retain(|p| {
let key = id_key(&p.id);
if referenced.contains(&key) {
true
} else {
removed_pts.insert(key);
false
}
});
doc.constraints.retain(|c| {
if let Some(id) = c.raw.get("id") {
if sel_constraint.contains(&id_key(id)) {
return false;
}
}
!c.points().iter().any(|pid| removed_pts.contains(&id_key(pid)))
});
edit.session.clear_selection();
edit.session.set_hover(None);
self.resolve_active_sketch("delete");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
}