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);
self.sketch_overlay_wpp = if world_per_pixel > 0.0 {
world_per_pixel
} else {
f64::MIN_POSITIVE
};
}
pub(super) fn ensure_sketch_overlay_current(&mut self) {
if !self.sketch_mode() {
self.sketch_overlay_wpp = 0.0;
return;
}
let wpp = self.camera.world_per_pixel();
if super::overlay_wpp_stale(self.sketch_overlay_wpp, wpp) {
self.refresh_sketch_overlay();
}
}
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 toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
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 || toggles {
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" => {
let refined = edit.pending.is_empty() && spline_insert_anchor(doc, u, v, radius);
if !refined {
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 spline_insert_anchor(doc: &mut crate::sketch::SketchDoc, u: f64, v: f64, radius: f64) -> bool {
let Some(added) = crate::sketch::spline::insert_anchor(doc, u, v, radius) else {
return false;
};
push_sketch_construction_line(doc, vec![added.anchor.clone(), added.before]);
push_sketch_construction_line(doc, vec![added.anchor, added.after]);
true
}
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 });
}
const AUTO_HV_ANGLE_TOL_DEG: f64 = 3.0;
const AUTO_COINCIDENT_FRAC: f64 = 0.01;
const AUTO_COINCIDENT_ABS: f64 = 1e-4;
pub(super) fn auto_constrain_doc(doc: &mut crate::sketch::SketchDoc) -> usize {
use crate::sketch::doc::id_key;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
let before = doc.constraints.len();
let hv_sin_tol = AUTO_HV_ANGLE_TOL_DEG.to_radians().sin();
let mut hv: Vec<(&str, Vec<Value>)> = Vec::new();
for g in &doc.geometries {
if g.geom_type != "line" || g.points.len() < 2 {
continue;
}
let (Some(a), Some(b)) = (doc.point(&g.points[0]), doc.point(&g.points[1])) else {
continue;
};
if a.fixed && b.fixed {
continue;
}
let (dx, dy) = (b.x - a.x, b.y - a.y);
let len = dx.hypot(dy);
if len < 1e-9 {
continue;
}
let mut keys: Vec<String> = g.points[..2].iter().map(id_key).collect();
keys.sort();
let already_hv = doc.constraints.iter().any(|c| {
if !matches!(c.ctype(), Some("━") | Some("│")) {
return false;
}
let mut k: Vec<String> = c.points().iter().map(id_key).collect();
k.sort();
k == keys
});
if already_hv {
continue;
}
let (ux, uy) = (dx / len, dy / len);
if uy.abs() <= hv_sin_tol {
hv.push(("━", vec![g.points[0].clone(), g.points[1].clone()]));
} else if ux.abs() <= hv_sin_tol {
hv.push(("│", vec![g.points[0].clone(), g.points[1].clone()]));
}
}
for (ct, pts) in hv {
push_geometric_constraint(doc, ct, pts);
}
let n = doc.points.len();
if n >= 2 {
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]]; x = parent[x];
}
x
}
fn union(parent: &mut [usize], a: usize, b: usize) {
let (ra, rb) = (find(parent, a), find(parent, b));
if ra != rb {
parent[ra] = rb;
}
}
let index: HashMap<String, usize> = doc
.points
.iter()
.enumerate()
.map(|(i, p)| (id_key(&p.id), i))
.collect();
let mut parent: Vec<usize> = (0..n).collect();
for c in &doc.constraints {
if c.ctype() == Some("≡") {
let pts = c.points();
if let (Some(p0), Some(p1)) = (pts.first(), pts.get(1)) {
if let (Some(&i), Some(&j)) = (index.get(&id_key(p0)), index.get(&id_key(p1))) {
union(&mut parent, i, j);
}
}
}
}
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
for p in &doc.points {
lo[0] = lo[0].min(p.x);
lo[1] = lo[1].min(p.y);
hi[0] = hi[0].max(p.x);
hi[1] = hi[1].max(p.y);
}
let extent = ((hi[0] - lo[0]).powi(2) + (hi[1] - lo[1]).powi(2)).sqrt();
let tol = (extent * AUTO_COINCIDENT_FRAC).max(AUTO_COINCIDENT_ABS);
let geo_sets: Vec<HashSet<String>> = doc
.geometries
.iter()
.map(|g| g.points.iter().map(id_key).collect())
.collect();
let mut coincidents: Vec<Vec<Value>> = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let (pi, pj) = (&doc.points[i], &doc.points[j]);
if pi.fixed && pj.fixed {
continue; }
let d = ((pi.x - pj.x).powi(2) + (pi.y - pj.y).powi(2)).sqrt();
if d > tol {
continue;
}
if find(&mut parent, i) == find(&mut parent, j) {
continue; }
let (ki, kj) = (id_key(&pi.id), id_key(&pj.id));
if geo_sets.iter().any(|s| s.contains(&ki) && s.contains(&kj)) {
continue; }
union(&mut parent, i, j);
coincidents.push(vec![pi.id.clone(), pj.id.clone()]);
}
}
for pts in coincidents {
push_geometric_constraint(doc, "≡", pts);
}
}
doc.constraints.len().saturating_sub(before)
}
impl EngineState {
pub fn sketch_auto_constrain(&mut self) -> usize {
let Some(edit) = self.sketch_edit.as_mut() else {
return 0;
};
edit.record_undo();
let added = auto_constrain_doc(&mut edit.session.doc);
if added == 0 {
edit.undo_stack.pop();
return 0;
}
self.resolve_active_sketch("auto-constrain");
self.refresh_sketch_overlay();
self.dirty = true;
added
}
}
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)))
});
crate::sketch::external_ref::prune_dead_refs(&edit.session.doc, &mut edit.external_refs);
edit.session.clear_selection();
edit.session.set_hover(None);
self.resolve_active_sketch("delete");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
}