use super::*;
#[derive(Clone, Debug, PartialEq)]
pub struct SketchEntityRow {
pub kind: &'static str,
pub id: serde_json::Value,
pub label: String,
pub selected: bool,
pub construction: bool,
pub conflicting: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NoticeSeverity {
#[default]
Error,
Warning,
Info,
}
impl EngineState {
pub fn push_notice_as(&mut self, severity: NoticeSeverity, message: impl Into<String>) {
if severity == NoticeSeverity::Error {
return self.push_notice(message);
}
let message = message.into();
#[cfg(not(target_arch = "wasm32"))]
eprintln!("{message}");
self.graded_notices.push((severity, message));
if self.graded_notices.len() > 8 {
let overflow = self.graded_notices.len() - 8;
self.graded_notices.drain(0..overflow);
}
}
pub fn take_graded_notices(&mut self) -> Vec<(NoticeSeverity, String)> {
let errors = std::mem::take(&mut self.notices).into_iter().map(|text| (NoticeSeverity::Error, text));
errors.chain(std::mem::take(&mut self.graded_notices)).collect()
}
pub fn push_notice(&mut self, message: impl Into<String>) {
let message = message.into();
#[cfg(not(target_arch = "wasm32"))]
eprintln!("{message}");
self.notices.push(message);
if self.notices.len() > 8 {
let overflow = self.notices.len() - 8;
self.notices.drain(0..overflow);
}
}
pub fn take_notices(&mut self) -> Vec<String> {
self.take_graded_notices().into_iter().map(|(_, text)| text).collect()
}
pub(super) fn resolve_active_sketch(&mut self, context: &str) {
let error = self
.sketch_edit
.as_mut()
.and_then(|edit| edit.session.resolve().err());
if let Some(error) = error {
self.push_notice(format!("Sketch solve failed ({context}): {error}"));
}
}
fn sketch_ref_selected(session: &crate::sketch::SketchSession, kind: &str, id: &serde_json::Value) -> bool {
use crate::sketch::doc::id_key;
let key = id_key(id);
session.selection.items.iter().any(|r| {
r.get("kind").and_then(serde_json::Value::as_str) == Some(kind)
&& r.get("id").map(id_key).as_deref() == Some(key.as_str())
})
}
pub fn sketch_point_rows(&self) -> Vec<SketchEntityRow> {
use crate::sketch::doc::id_key;
let Some(edit) = self.sketch_edit.as_ref() else {
return Vec::new();
};
let session = &edit.session;
session
.doc
.points
.iter()
.map(|p| {
let grounded = session.doc.constraints.iter().any(|c| {
c.ctype() == Some("⏚")
&& c.points().first().map(id_key).as_deref() == Some(id_key(&p.id).as_str())
});
let mut marks = String::new();
if p.external_reference {
marks.push_str(" \u{26D3}"); }
if p.construction {
marks.push_str(" ◐");
}
if grounded {
marks.push_str(" ⏚");
}
SketchEntityRow {
kind: "point",
id: p.id.clone(),
label: if crate::sketch::external_ref::is_origin_id(&p.id) {
format!("Origin ({:.1}, {:.1}){marks}", p.x, p.y)
} else {
format!("P{} ({:.1}, {:.1}){marks}", id_key(&p.id), p.x, p.y)
},
selected: Self::sketch_ref_selected(session, "point", &p.id),
construction: p.construction,
conflicting: false,
}
})
.collect()
}
pub fn sketch_geometry_rows(&self) -> Vec<SketchEntityRow> {
use crate::sketch::doc::id_key;
let Some(edit) = self.sketch_edit.as_ref() else {
return Vec::new();
};
let session = &edit.session;
session
.doc
.geometries
.iter()
.map(|g| {
let pts = g
.points
.iter()
.map(id_key)
.collect::<Vec<_>>()
.join(",");
let construction = g.construction();
let mark = if construction { " ◐" } else { "" };
let note = g.extra.get("note").and_then(serde_json::Value::as_str).map(|n| format!(" \u{2014} {n}")).unwrap_or_default();
SketchEntityRow {
kind: "geometry",
id: g.id.clone(),
label: if g.geom_type == "text" {
format!("Text: {}", g.extra.get("text").and_then(|v|v.as_str()).unwrap_or(""))
} else { format!("{}:{}{mark} [{pts}]{note}", g.geom_type, id_key(&g.id)) },
selected: Self::sketch_ref_selected(session, "geometry", &g.id),
construction,
conflicting: false,
}
})
.collect()
}
pub fn sketch_constraint_rows(&self) -> Vec<SketchEntityRow> {
use crate::sketch::doc::id_key;
let Some(edit) = self.sketch_edit.as_ref() else {
return Vec::new();
};
let session = &edit.session;
session
.doc
.constraints
.iter()
.filter_map(|c| {
let id = c.raw.get("id")?.clone();
let ctype = c.ctype().unwrap_or("?");
let value = c
.raw
.get("value")
.and_then(serde_json::Value::as_f64)
.map(|v| format!(" {v:.3}"))
.unwrap_or_default();
let pts = c
.points()
.iter()
.map(id_key)
.collect::<Vec<_>>()
.join(",");
Some(SketchEntityRow {
kind: "constraint",
id: id.clone(),
label: format!("{} {ctype}{value} [{pts}]", id_key(&id)),
selected: Self::sketch_ref_selected(session, "constraint", &id),
construction: false,
conflicting: session.diagnostics.constraint_conflicting(&id),
})
})
.collect()
}
pub fn sketch_select_entity(&mut self, kind: &str, id: serde_json::Value, additive: bool) {
let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
let entity_ref = serde_json::json!({ "kind": kind, "id": id });
let Some(edit) = self.sketch_edit.as_mut() else {
return;
};
if !additive && !toggles {
edit.session.clear_selection();
}
edit.session.toggle_selection(entity_ref);
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_delete_entity(&mut self, kind: &str, id: serde_json::Value) -> bool {
use crate::sketch::doc::id_key;
let target = serde_json::json!({ "kind": kind, "id": id });
let same = |r: &serde_json::Value| {
r.get("kind") == target.get("kind")
&& r.get("id").map(id_key) == target.get("id").map(id_key)
};
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let kept: Vec<serde_json::Value> =
edit.session.selection.items.iter().filter(|r| !same(r)).cloned().collect();
edit.session.clear_selection();
edit.session.toggle_selection(target);
let deleted = self.sketch_delete_selection();
if let Some(edit) = self.sketch_edit.as_mut() {
let doc = &edit.session.doc;
let alive = |r: &serde_json::Value| {
let Some(id) = r.get("id") else { return false };
match r.get("kind").and_then(|k| k.as_str()) {
Some("geometry") => doc.geometry(id).is_some(),
Some("point") => doc.point(id).is_some(),
Some("constraint") => doc
.constraints
.iter()
.any(|c| c.raw.get("id").map(id_key) == Some(id_key(id))),
_ => false,
}
};
let restore: Vec<serde_json::Value> = kept.into_iter().filter(|r| alive(r)).collect();
if !deleted {
edit.session.clear_selection();
}
for r in restore {
edit.session.toggle_selection(r);
}
}
self.refresh_sketch_overlay();
self.dirty = true;
deleted
}
pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
if self.sketch_edit.is_none() {
return;
}
let entity_ref = serde_json::json!({ "kind": kind, "id": id });
self.set_sketch_hover(Some(entity_ref));
self.sketch_list_hover_active = true;
}
pub fn take_sketch_list_hover(&mut self) -> bool {
std::mem::take(&mut self.sketch_list_hover_active)
}
pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
self.sketch_edit
.as_ref()
.map(|edit| edit.session.solver_settings.clone())
}
pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.session.solver_settings = settings;
} else {
return;
}
self.resolve_active_sketch("solver settings");
self.refresh_sketch_overlay();
self.dirty = true;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SketchConstraintAction {
pub symbol: String,
pub label: String,
pub dimensional: bool,
}
impl EngineState {
pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
match self.sketch_edit.as_ref() {
Some(edit) => sketch_palette_for(&edit.session),
None => Vec::new(),
}
}
pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
let added = match self.sketch_edit.as_mut() {
Some(edit) => {
edit.record_undo();
sketch_build_and_add_constraint(&mut edit.session, symbol)
}
None => return false,
};
if added {
self.resolve_active_sketch("add-constraint");
self.refresh_sketch_overlay();
self.dirty = true;
} else if let Some(edit) = self.sketch_edit.as_mut() {
edit.undo_stack.pop();
}
added
}
pub fn sketch_toggle_ground(&mut self) -> bool {
use crate::sketch::doc::id_key;
use std::collections::HashSet;
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let sel_ids: Vec<serde_json::Value> = edit
.session
.selection.items
.iter()
.filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
.filter_map(|r| r.get("id").cloned())
.filter(|id| !crate::sketch::external_ref::is_origin_id(id))
.collect();
if sel_ids.is_empty() {
return false;
}
edit.record_undo();
let doc = &mut edit.session.doc;
let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
doc.constraints.iter().any(|c| {
c.ctype() == Some("⏚")
&& c.points().first().map(id_key) == Some(id_key(id))
})
};
let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
if all_grounded {
let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
doc.constraints.retain(|c| {
if c.ctype() != Some("⏚") {
return true;
}
match c.points().first() {
Some(p) => !sel_keys.contains(&id_key(p)),
None => true,
}
});
for id in &sel_ids {
if let Some(p) = doc.point_mut(id) {
p.fixed = false;
}
}
} else {
for id in &sel_ids {
if has_ground(doc, id) {
continue;
}
let cid = doc.next_constraint_id();
let mut raw = serde_json::Map::new();
raw.insert("id".to_string(), cid);
raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
raw.insert(
"points".to_string(),
serde_json::Value::Array(vec![id.clone()]),
);
doc.constraints.push(crate::sketch::SketchConstraint { raw });
if let Some(p) = doc.point_mut(id) {
p.fixed = true;
}
}
}
self.resolve_active_sketch("toggle-ground");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub fn sketch_toggle_construction(&mut self) -> bool {
if self.sketch_selection_has_text_frame() { return false; }
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let mut pt_ids: Vec<serde_json::Value> = Vec::new();
let mut geo_ids: Vec<serde_json::Value> = Vec::new();
for r in &edit.session.selection.items {
match r.get("kind").and_then(|v| v.as_str()) {
Some("point") => {
if let Some(id) = r.get("id").filter(|id| !crate::sketch::external_ref::is_origin_id(id)) {
pt_ids.push(id.clone());
}
}
Some("geometry") => {
if let Some(id) = r.get("id") {
geo_ids.push(id.clone());
}
}
_ => {}
}
}
if pt_ids.is_empty() && geo_ids.is_empty() {
return false;
}
edit.record_undo();
let doc = &mut edit.session.doc;
let all_construction = pt_ids
.iter()
.all(|id| doc.point(id).map_or(false, |p| p.construction))
&& geo_ids
.iter()
.all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
let next = !all_construction;
for id in &pt_ids {
if let Some(p) = doc.point_mut(id) {
p.construction = next;
}
}
for id in &geo_ids {
if let Some(g) = doc.geometry_mut(id) {
g.extra
.insert("construction".to_string(), serde_json::Value::Bool(next));
}
}
self.resolve_active_sketch("toggle-construction");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub fn sketch_cleanup_unused_points(&mut self) -> bool {
use crate::sketch::doc::id_key;
use std::collections::HashSet;
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
edit.record_undo();
let doc = &mut edit.session.doc;
let mut used: HashSet<String> = HashSet::new();
for g in &doc.geometries {
for pid in &g.points {
used.insert(id_key(pid));
}
}
for c in &doc.constraints {
for pid in c.points() {
used.insert(id_key(pid));
}
}
let before = doc.points.len();
doc.points.retain(|p| used.contains(&id_key(&p.id)));
let removed = doc.points.len() != before;
if !removed {
edit.undo_stack.pop();
return false;
}
self.resolve_active_sketch("cleanup");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub fn sketch_constraint_count(&self) -> usize {
self.sketch_edit
.as_ref()
.map_or(0, |edit| edit.session.doc.constraints.len())
}
pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
use crate::sketch::doc::id_key;
let edit = self.sketch_edit.as_ref()?;
let doc = &edit.session.doc;
let sel: Vec<&serde_json::Value> = edit
.session
.selection.items
.iter()
.filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
.filter_map(|r| r.get("id"))
.collect();
if sel.is_empty() {
return None;
}
let all = sel.iter().all(|id| {
doc.constraints.iter().any(|c| {
c.ctype() == Some("⏚")
&& c.points().first().map(id_key) == Some(id_key(id))
})
});
Some(all)
}
pub fn sketch_selection_all_construction(&self) -> Option<bool> {
if self.sketch_selection_has_text_frame() { return None; }
let edit = self.sketch_edit.as_ref()?;
let doc = &edit.session.doc;
let mut any = false;
let mut all = true;
for r in &edit.session.selection.items {
match r.get("kind").and_then(|v| v.as_str()) {
Some("point") => {
any = true;
if let Some(id) = r.get("id") {
if !doc.point(id).map_or(false, |p| p.construction) {
all = false;
}
}
}
Some("geometry") => {
any = true;
if let Some(id) = r.get("id") {
if !doc.geometry(id).map_or(false, |g| g.construction()) {
all = false;
}
}
}
_ => {}
}
}
if !any {
return None;
}
Some(all)
}
}
fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
use crate::sketch::doc::id_key;
use std::collections::HashSet;
let doc = &session.doc;
let mut sel_points: Vec<serde_json::Value> = Vec::new();
let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
for r in &session.selection.items {
match r.get("kind").and_then(|v| v.as_str()) {
Some("point") => {
if let Some(id) = r.get("id") {
sel_points.push(id.clone());
}
}
Some("geometry") => {
if let Some(id) = r.get("id") {
if let Some(g) = doc.geometry(id) {
geos.push(g);
}
}
}
_ => {}
}
}
if geos.iter().any(|g| g.geom_type == "text") { return Vec::new(); }
let mut point_set: HashSet<String> = HashSet::new();
for id in &sel_points {
point_set.insert(id_key(id));
}
for g in &geos {
let pts: &[serde_json::Value] = if g.geom_type == "arc" {
&g.points[..g.points.len().min(2)]
} else {
&g.points
};
for pid in pts {
point_set.insert(id_key(pid));
}
}
let point_count = point_set.len();
let selected_point_ids_len = sel_points.len();
let is_radial =
|g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
let mut out: Vec<SketchConstraintAction> = Vec::new();
let mut push = |symbol: &str, label: &str, dimensional: bool| {
out.push(SketchConstraintAction {
symbol: symbol.to_string(),
label: label.to_string(),
dimensional,
});
};
if geos.len() == 1 && is_radial(geos[0]) {
push("R", "Radius", true);
push("⌀", "Diameter", true);
return out;
}
if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
push("∥", "Parallel", false);
push("⟂", "Perpendicular", false);
push("∠", "Angle", true);
push("⇌", "Equal distance", false);
push("⋰", "Collinear", false);
push("⏛", "Point on line", false);
if geos
.iter()
.any(|g| crate::sketch::spline::is_handle_guide(doc, g))
{
push("⌒", "Tangent", false);
}
if crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
push("ϰ", "Curvature (G2)", false);
}
return out;
}
if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
push("⊜", "Equal radius", false);
push("◎", "Concentric", false);
push("⌒", "Tangent", false);
return out;
}
if geos.len() > 2 && sel_points.is_empty() {
if geos.iter().all(|g| g.geom_type == "line") {
push("⇌", "Equal distance", false);
return out;
}
if geos.iter().all(|g| is_radial(g)) {
push("⊜", "Equal radius", false);
return out;
}
}
if geos.len() == 2
&& ((geos[0].geom_type == "line" && is_radial(geos[1]))
|| (geos[1].geom_type == "line" && is_radial(geos[0])))
{
push("⌒", "Tangent", false);
if geos
.iter()
.any(|g| crate::sketch::spline::is_end_handle_guide(doc, g))
{
push("ϰ", "Curvature (G2)", false);
}
return out;
}
if sketch_foot_tangent_pair(doc, &geos).is_some() {
push("∿", "Tangent (span point)", false);
return out;
}
if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
push("⟂", "Perpendicular", false);
push("⋈", "Symmetric about line", false);
}
if point_count == 1 {
push("⏚", "Ground (fix point)", false);
}
if point_count == 2 {
push("━", "Horizontal", false);
push("│", "Vertical", false);
push("≡", "Coincident", false);
push("⟺", "Distance", true);
}
if point_count == 3 {
push("⋯", "Midpoint", false);
push("⏛", "Point on line", false);
push("↥", "Line to point distance", true);
push("∠", "Angle", true);
if selected_point_ids_len >= 3 {
push("⋰", "Collinear", false);
}
}
if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
push("⋰", "Collinear", false);
}
out
}
fn sketch_build_tangent_points(
doc: &crate::sketch::SketchDoc,
geos: &[&crate::sketch::SketchGeometry],
) -> Option<Vec<serde_json::Value>> {
if geos.len() != 2 {
return None;
}
let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
if geos.iter().all(|g| g.geom_type == "line") {
let (a, b) = (geos[0], geos[1]);
if a.points.len() < 2 || b.points.len() < 2 {
return None;
}
if !crate::sketch::spline::is_handle_guide(doc, a)
&& !crate::sketch::spline::is_handle_guide(doc, b)
{
return None;
}
return Some(vec![
a.points[0].clone(),
a.points[1].clone(),
b.points[0].clone(),
b.points[1].clone(),
]);
}
let line = geos.iter().find(|g| g.geom_type == "line");
let circ = geos.iter().find(|g| is_radial(g));
if let (Some(line), Some(circ)) = (line, circ) {
if line.points.len() < 2 || circ.points.len() < 2 {
return None;
}
return Some(vec![
line.points[0].clone(),
line.points[1].clone(),
circ.points[0].clone(),
circ.points[1].clone(),
]);
}
if geos.iter().all(|g| is_radial(g)) {
let (a, b) = (geos[0], geos[1]);
if a.points.len() < 2 || b.points.len() < 2 {
return None;
}
return Some(vec![
a.points[0].clone(),
a.points[1].clone(),
b.points[0].clone(),
b.points[1].clone(),
]);
}
None
}
fn sketch_foot_tangent_pair<'a>(
doc: &crate::sketch::SketchDoc,
geos: &[&'a crate::sketch::SketchGeometry],
) -> Option<(&'a crate::sketch::SketchGeometry, &'a crate::sketch::SketchGeometry)> {
if geos.len() != 2 {
return None;
}
let is_spline = |g: &crate::sketch::SketchGeometry| {
crate::sketch::spline::is_spline_type(&g.geom_type)
};
let is_radial =
|g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
let spline = *geos.iter().find(|g| is_spline(g))?;
let target = *geos.iter().find(|g| !is_spline(g))?;
if spline.points.len() < 4 || target.points.len() < 2 {
return None;
}
let target_is_usable = if target.geom_type == "line" {
!crate::sketch::spline::is_handle_guide(doc, target)
} else {
is_radial(target)
};
target_is_usable.then_some((spline, target))
}
fn sketch_build_foot_tangent_points(
doc: &crate::sketch::SketchDoc,
geos: &[&crate::sketch::SketchGeometry],
) -> Option<Vec<serde_json::Value>> {
let (spline, target) = sketch_foot_tangent_pair(doc, geos)?;
Some(vec![
target.points[0].clone(),
target.points[1].clone(),
spline.points[0].clone(),
spline.points[1].clone(),
])
}
fn sketch_build_curvature_points(
doc: &crate::sketch::SketchDoc,
geos: &[&crate::sketch::SketchGeometry],
) -> Option<Vec<serde_json::Value>> {
if geos.len() != 2 {
return None;
}
let is_radial =
|g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
if geos.iter().all(|g| g.geom_type == "line") {
if !crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
return None;
}
let (a, b) = (geos[0], geos[1]);
return Some(vec![
a.points[0].clone(),
a.points[1].clone(),
b.points[0].clone(),
b.points[1].clone(),
]);
}
let guide = geos
.iter()
.find(|g| crate::sketch::spline::is_end_handle_guide(doc, g))?;
let circ = geos.iter().find(|g| is_radial(g))?;
if circ.points.len() < 2 {
return None;
}
Some(vec![
guide.points[0].clone(),
guide.points[1].clone(),
circ.points[0].clone(),
circ.points[1].clone(),
])
}
pub(super) fn sketch_perpendicular_should_swap(
doc: &crate::sketch::SketchDoc,
pts: &[serde_json::Value],
) -> bool {
let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
let (Some(p0), Some(p1), Some(p2), Some(p3)) =
(coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
else {
return false;
};
let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
(deg + 360.0) % 360.0
};
let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
let line1_a = fold(angle(p0, p1));
let line1_b = fold(angle(p1, p0));
let line2 = fold(angle(p2, p3));
let diff_a = line1_a - line2;
let diff_b = line1_b - line2;
(90.0 - diff_a).abs() > (90.0 - diff_b).abs()
}
pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
use crate::sketch::doc::id_key;
let mut keys: Vec<String> = points.iter().map(id_key).collect();
keys.sort();
format!("{ctype}|{}", keys.join(","))
}
fn sketch_build_and_add_constraint(
session: &mut crate::sketch::SketchSession,
symbol: &str,
) -> bool {
struct Built {
store_type: String,
display_style: &'static str,
lists: Vec<Vec<serde_json::Value>>,
}
fn sketch_equal_to_first(t: &str, geos: &[&crate::sketch::SketchGeometry]) -> Built {
let first = geos[0];
Built {
store_type: t.to_string(),
display_style: "",
lists: geos[1..]
.iter()
.map(|g| {
vec![
first.points[0].clone(),
first.points[1].clone(),
g.points[0].clone(),
g.points[1].clone(),
]
})
.collect(),
}
}
let built: Option<Built> = {
let doc = &session.doc;
let mut selected: Vec<serde_json::Value> = Vec::new();
let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
let mut point_items: Vec<serde_json::Value> = Vec::new();
let mut geometry_type: Option<String> = None;
let mut first_kind: Option<String> = None;
let mut has_geometry = false;
for (i, r) in session.selection.items.iter().enumerate() {
let kind = r.get("kind").and_then(|v| v.as_str());
let Some(id) = r.get("id") else { continue };
if i == 0 {
first_kind = kind.map(|k| k.to_string());
}
match kind {
Some("point") => {
if let Some(p) = doc.point(id) {
selected.push(p.id.clone());
point_items.push(p.id.clone());
}
}
Some("geometry") => {
if let Some(g) = doc.geometry(id) {
for pid in &g.points {
if doc.point(pid).is_some() {
selected.push(pid.clone());
}
}
if g.geom_type == "arc" {
selected.pop();
}
geometry_type = Some(g.geom_type.clone());
geo_items.push(g);
has_geometry = true;
}
}
_ => {}
}
}
if selected.is_empty() {
None
} else {
let radial = |g: &crate::sketch::SketchGeometry| {
g.geom_type == "arc" || g.geom_type == "circle"
};
let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
store_type: t.to_string(),
display_style: ds,
lists: vec![list],
};
match symbol {
"◎" => {
if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
Some(simple(
"◎",
vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
"",
))
} else {
None
}
}
"⊜" => {
if geo_items.len() >= 2
&& geo_items.iter().all(|g| radial(g) && g.points.len() >= 2)
{
Some(sketch_equal_to_first("⊜", &geo_items))
} else {
None
}
}
"⇌" if geo_items.len() >= 2
&& point_items.is_empty()
&& geo_items
.iter()
.all(|g| g.geom_type == "line" && g.points.len() >= 2) =>
{
Some(sketch_equal_to_first("⇌", &geo_items))
}
"⌒" => sketch_build_tangent_points(doc, &geo_items).map(|pts| simple("⌒", pts, "")),
"ϰ" => {
sketch_build_curvature_points(doc, &geo_items).map(|pts| simple("ϰ", pts, ""))
}
"∿" => sketch_build_foot_tangent_points(doc, &geo_items)
.map(|pts| simple("∿", pts, "")),
"⋰" => {
let ids = if geo_items.len() >= 2
&& geo_items.iter().all(|g| g.geom_type == "line")
{
let mut v = Vec::new();
for g in &geo_items {
v.push(g.points[0].clone());
v.push(g.points[1].clone());
}
Some(v)
} else if point_items.len() >= 3 {
Some(point_items.clone())
} else {
None
};
ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
}
"⋈" => geo_items
.iter()
.find(|g| g.geom_type == "line")
.filter(|line| line.points.len() >= 2 && point_items.len() == 2)
.map(|line| {
simple(
"⋈",
vec![
line.points[0].clone(),
line.points[1].clone(),
point_items[0].clone(),
point_items[1].clone(),
],
"",
)
}),
"R" | "⌀" => {
if selected.len() == 2 {
let ds = if symbol == "⌀" { "diameter" } else { "radius" };
Some(Built {
store_type: "⟺".to_string(),
display_style: ds,
lists: vec![selected.clone()],
})
} else {
None
}
}
_ => {
match selected.len() {
1 => match symbol {
"⏚" => Some(simple("⏚", selected.clone(), "")),
_ => None,
},
2 => match symbol {
"━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
"⟺" => {
let ds = if matches!(
geometry_type.as_deref(),
Some("arc") | Some("circle")
) {
"radius"
} else {
""
};
Some(Built {
store_type: "⟺".to_string(),
display_style: ds,
lists: vec![selected.clone()],
})
}
_ => None,
},
3 => match symbol {
"⏛" => Some(simple("⏛", selected.clone(), "")),
"⋯" => {
let mut pts = selected.clone();
if has_geometry && first_kind.as_deref() == Some("point") {
pts.reverse();
}
Some(simple("⋯", pts, ""))
}
"↥" => {
if geo_items.len() == 1 && point_items.len() == 1 {
let line = geo_items[0];
if line.geom_type == "line" && line.points.len() >= 2 {
Some(simple(
"↥",
vec![
line.points[0].clone(),
line.points[1].clone(),
point_items[0].clone(),
],
"",
))
} else {
None
}
} else {
Some(simple("↥", selected.clone(), ""))
}
}
"⇌" => Some(simple("⇌", selected.clone(), "")),
_ => None,
},
4 | 5 => match symbol {
"⏛" => {
if geo_items.len() == 2
&& geo_items.iter().all(|g| g.geom_type == "line")
&& geo_items[0].points.len() >= 2
&& geo_items[1].points.len() >= 2
{
let (g0, g1) = (geo_items[0], geo_items[1]);
Some(Built {
store_type: "⏛".to_string(),
display_style: "",
lists: vec![
vec![
g0.points[0].clone(),
g0.points[1].clone(),
g1.points[0].clone(),
],
vec![
g0.points[0].clone(),
g0.points[1].clone(),
g1.points[1].clone(),
],
],
})
} else {
None
}
}
"⟂" => {
if selected.len() != 4 {
None
} else {
let mut pts = selected.clone();
if sketch_perpendicular_should_swap(doc, &pts) {
pts.swap(0, 1);
}
Some(simple("⟂", pts, ""))
}
}
"∥" => Some(simple("∥", selected.clone(), "")),
"∠" => Some(simple("∠", selected.clone(), "")),
"⇌" => Some(simple("⇌", selected.clone(), "")),
_ => None,
},
_ => None,
}
}
}
}
};
let Some(built) = built else {
return false;
};
let doc = &mut session.doc;
let mut added_any = false;
for pts in &built.lists {
let sig = sketch_constraint_signature(&built.store_type, pts);
let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
Some(t) => sketch_constraint_signature(t, c.points()) == sig,
None => false,
});
if duplicate {
continue;
}
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(built.store_type.clone()),
);
raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
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(built.display_style.to_string()),
);
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 });
added_any = true;
}
added_any
}
impl EngineState {
fn sketch_selection_has_text_frame(&self) -> bool {
let Some(edit) = &self.sketch_edit else { return false; };
edit.session.selection.items.iter().any(|r| {
let id = &r["id"];
if r["kind"] == "geometry" {
edit.session.doc.geometry(id).is_some_and(|g| g.geom_type == "text" || g.extra.contains_key("textFrame"))
} else {
edit.session.doc.geometries.iter().any(|g| g.geom_type == "text" && g.points.contains(id))
}
})
}
pub fn sketch_selected_text(&self) -> Option<(serde_json::Value, String, String)> {
let session = &self.sketch_edit.as_ref()?.session;
if session.selection.items.len() != 1 { return None; }
let r = &session.selection.items[0];
if r["kind"] != "geometry" { return None; }
let g = session.doc.geometry(&r["id"])?;
if g.geom_type != "text" { return None; }
Some((g.id.clone(), g.extra["text"].as_str()?.into(), g.extra["font"].as_str()?.into()))
}
pub fn sketch_edit_text(&mut self, id: &serde_json::Value, text: &str, font: &str) -> Result<(), String> {
let contours = crate::sketch::text::outlines(text,font)?;
let edit = self.sketch_edit.as_mut().ok_or("No active sketch")?;
let g = edit.session.doc.geometry(id).filter(|g| g.geom_type == "text").ok_or("Text object no longer exists")?;
if g.extra["text"] == text && g.extra["font"] == font { return Ok(()); }
edit.record_undo();
let g = edit.session.doc.geometry_mut(id).unwrap();
g.extra.insert("text".into(), serde_json::json!(text));
g.extra.insert("font".into(), serde_json::json!(font));
g.extra.insert("textContours".into(), contours);
self.refresh_sketch_overlay();
self.dirty = true;
Ok(())
}
}