use super::*;
impl EngineState {
pub fn sketch_dimension_labels_json(&self) -> String {
let world_per_pixel = self.camera.world_per_pixel();
let labels = match self.sketch_edit.as_ref() {
Some(edit) => edit.session.dimension_labels(world_per_pixel),
None => return "[]".to_string(),
};
let out: Vec<serde_json::Value> = labels
.into_iter()
.map(|l| {
serde_json::json!({
"id": l.id,
"text": l.text,
"world": l.world,
"value": l.value,
"valueExpr": l.value_expr,
"mode": l.mode,
"conflicting": l.conflicting,
})
})
.collect();
serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
}
pub fn sketch_dimension_value_json(&self, constraint_id: &serde_json::Value) -> String {
use crate::sketch::doc::id_key;
let Some(edit) = self.sketch_edit.as_ref() else {
return "{}".to_string();
};
let key = id_key(constraint_id);
let Some(c) = edit
.session
.doc
.constraints
.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return "{}".to_string();
};
let is_diameter = c.ctype() == Some("⟺")
&& c.raw.get("displayStyle").and_then(serde_json::Value::as_str) == Some("diameter");
let mode = dimension_mode(c);
let stored = c
.raw
.get("value")
.and_then(serde_json::Value::as_f64)
.filter(|v| v.is_finite());
let display_value = stored.map(|v| if is_diameter { v * 2.0 } else { v });
let value_expr = c
.raw
.get("valueExpr")
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string);
serde_json::json!({
"value": display_value,
"valueExpr": value_expr,
"mode": mode,
})
.to_string()
}
pub fn sketch_set_dimension_value(
&mut self,
constraint_id: &serde_json::Value,
input: &str,
) -> bool {
use crate::sketch::doc::id_key;
let expressions = self.history.expressions();
let configurator = self.history.configurator();
let key = id_key(constraint_id);
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let is_diameter = {
let Some(c) = edit
.session
.doc
.constraints
.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return false;
};
c.ctype() == Some("⟺")
&& c.raw.get("displayStyle").and_then(serde_json::Value::as_str)
== Some("diameter")
};
let trimmed = input.trim();
if trimmed.is_empty() {
return false;
}
let is_plain_number = is_plain_number_literal(trimmed);
let displayed: f64 = if is_plain_number {
match trimmed.parse::<f64>() {
Ok(n) if n.is_finite() => n,
_ => return false,
}
} else {
match brep_kernel::eval_expression(&expressions, &configurator, trimmed) {
Ok(n) if n.is_finite() => n,
_ => return false,
}
};
let solver_value = if is_diameter { displayed * 0.5 } else { displayed };
let constraint = edit.session.doc.constraints.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
.expect("constraint was located above");
let controls_radius = constraint.ctype() == Some("⟺")
&& edit.session.doc.geometries.iter().any(|g| {
matches!(g.geom_type.as_str(), "circle" | "arc")
&& g.points.len() >= 2
&& constraint.points().len() == 2
&& ((constraint.points()[0] == g.points[0] && constraint.points()[1] == g.points[1])
|| (constraint.points()[0] == g.points[1] && constraint.points()[1] == g.points[0]))
});
if (controls_radius || matches!(dimension_mode(constraint), "radius" | "diameter"))
&& solver_value <= 1e-5 {
return false;
}
edit.record_undo();
{
let Some(c) = edit
.session
.doc
.constraints
.iter_mut()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return false;
};
if is_plain_number {
c.raw.remove("valueExpr");
c.raw.remove("valueExprMode");
} else {
c.raw.insert(
"valueExpr".to_string(),
serde_json::Value::String(trimmed.to_string()),
);
if is_diameter {
c.raw.insert(
"valueExprMode".to_string(),
serde_json::Value::String("diameter".to_string()),
);
} else {
c.raw.remove("valueExprMode");
}
}
c.raw.insert(
"value".to_string(),
serde_json::Value::from(solver_value),
);
c.raw.insert(
"valueNeedsSetup".to_string(),
serde_json::Value::Bool(false),
);
}
self.resolve_active_sketch("set-dimension-value");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub fn sketch_dimension_drag_to(
&mut self,
constraint_id: &serde_json::Value,
x: f64,
y: f64,
) {
use crate::sketch::doc::id_key;
let Some((u, v)) = self.sketch_uv_at(x, y) else {
return;
};
let key = id_key(constraint_id);
let Some(edit) = self.sketch_edit.as_mut() else {
return;
};
let anchor = {
let Some(c) = edit
.session
.doc
.constraints
.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return;
};
crate::sketch::dimensions::dimension_anchor_uv(c, &edit.session.doc)
};
let Some(anchor) = anchor else {
return;
};
if !edit.dim_drag_snapshotted {
edit.record_undo();
edit.dim_drag_snapshotted = true;
}
edit.session.dim_offsets.insert(
key,
serde_json::json!({ "du": u - anchor[0], "dv": v - anchor[1] }),
);
self.refresh_sketch_overlay();
self.dirty = true;
}
}
pub(super) fn is_plain_number_literal(s: &str) -> bool {
let s = s.trim();
if s.is_empty() {
return false;
}
let bytes = s.as_bytes();
let mut i = 0;
if bytes[i] == b'+' || bytes[i] == b'-' {
i += 1;
}
let mut digits_before = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
digits_before += 1;
}
let mut digits_after = 0;
let mut had_dot = false;
if i < bytes.len() && bytes[i] == b'.' {
had_dot = true;
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
digits_after += 1;
}
}
if had_dot && digits_after == 0 {
return false;
}
if digits_before == 0 && digits_after == 0 {
return false;
}
if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
i += 1;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let mut exp_digits = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
exp_digits += 1;
}
if exp_digits == 0 {
return false;
}
}
i == bytes.len()
}
fn dimension_mode(c: &crate::sketch::SketchConstraint) -> &'static str {
match c.ctype() {
Some("∠") => "angle",
Some("⟺") => match c.raw.get("displayStyle").and_then(serde_json::Value::as_str) {
Some("diameter") => "diameter",
Some("radius") => "radius",
_ => "distance",
},
_ => "distance",
}
}
impl EngineState {
pub fn sketch_undo(&mut self) -> bool {
self.sketch_undo_redo(true)
}
pub fn sketch_redo(&mut self) -> bool {
self.sketch_undo_redo(false)
}
pub fn sketch_can_undo(&self) -> bool {
self.sketch_edit
.as_ref()
.map_or(false, |edit| !edit.undo_stack.is_empty())
}
pub fn sketch_can_redo(&self) -> bool {
self.sketch_edit
.as_ref()
.map_or(false, |edit| !edit.redo_stack.is_empty())
}
pub fn sketch_dimension_drag_end(&mut self) {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.dim_drag_snapshotted = false;
}
}
fn sketch_undo_redo(&mut self, undo: bool) -> bool {
let acted = {
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let popped = if undo {
edit.undo_stack.pop()
} else {
edit.redo_stack.pop()
};
let Some(snapshot) = popped else {
return false;
};
let current = edit.snapshot();
if undo {
edit.redo_stack.push(current);
} else {
edit.undo_stack.push(current);
}
edit.session.doc = snapshot.doc;
edit.session.dim_offsets = snapshot.dim_offsets;
edit.session.selection = snapshot.selection;
edit.external_refs = snapshot.external_refs;
edit.pending.clear();
edit.hover_uv = None;
edit.drag = None;
edit.handdraw_stroke.clear();
edit.dim_drag_snapshotted = false;
edit.session.hovered = None;
self.resolve_active_sketch("undo/redo");
true
};
if acted {
self.refresh_sketch_overlay();
self.dirty = true;
}
acted
}
pub fn sketch_diagnostics_dump_json(&self, feature_id: &str) -> String {
if let Some(edit) = self.sketch_edit.as_ref() {
if edit.feature_id == feature_id {
return sketch_dump_value(feature_id, &edit.session).to_string();
}
}
let Some(index) = self.history.index_of(feature_id) else {
return serde_json::json!({ "error": format!("no feature '{feature_id}'") })
.to_string();
};
if self.history.feature_type(index).as_deref() != Some("S") {
return serde_json::json!({ "error": format!("'{feature_id}' is not a sketch") })
.to_string();
}
let persistent = self.history.feature_persistent_data(index);
let plane = persistent
.as_ref()
.and_then(|p| p.get("basis"))
.map(crate::sketch::PlaneFrame::from_basis_json)
.unwrap_or_else(crate::sketch::PlaneFrame::xy);
let doc_value = persistent
.as_ref()
.and_then(|p| p.get("sketch"))
.cloned()
.unwrap_or_else(|| {
serde_json::json!({ "points": [], "geometries": [], "constraints": [] })
});
let doc: crate::sketch::SketchDoc = match serde_json::from_value(doc_value) {
Ok(doc) => doc,
Err(error) => {
return serde_json::json!({ "error": format!("sketch doc parse: {error}") })
.to_string();
}
};
match crate::sketch::SketchSession::new(doc, plane) {
Ok(session) => sketch_dump_value(feature_id, &session).to_string(),
Err(error) => serde_json::json!({ "error": error }).to_string(),
}
}
}
fn sketch_dump_value(
feature_id: &str,
session: &crate::sketch::SketchSession,
) -> serde_json::Value {
serde_json::json!({
"featureId": feature_id,
"sketch": serde_json::to_value(&session.doc).unwrap_or(serde_json::Value::Null),
"diagnostics": serde_json::to_value(&session.diagnostics)
.unwrap_or(serde_json::Value::Null),
"dimOffsets": serde_json::Value::Object(session.dim_offsets.clone()),
"pointCount": session.doc.points.len(),
"geometryCount": session.doc.geometries.len(),
"constraintCount": session.doc.constraints.len(),
})
}
impl EngineState {
pub fn sketch_trim_at(&mut self, x: f64, y: f64) -> bool {
let Some((u, v)) = self.sketch_uv_at(x, y) else {
return false;
};
self.sketch_trim_uv(u, v)
}
pub fn sketch_trim_uv(&mut self, u: f64, v: f64) -> bool {
let radius = self.sketch_pick_radius();
let point_id = match self.sketch_edit.as_ref() {
Some(edit) => edit.session.pick_entity(u, v, radius).and_then(|entity_ref| {
(entity_ref.get("kind").and_then(|k| k.as_str()) == Some("point"))
.then(|| entity_ref.get("id").cloned())
.flatten()
}),
None => return false,
};
if let Some(point_id) = point_id {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.session.clear_selection();
edit.session
.toggle_selection(serde_json::json!({ "kind": "point", "id": point_id }));
}
return self.sketch_delete_selection();
}
let geo_id = match self.sketch_edit.as_ref() {
Some(edit) => crate::sketch::trim::pick_geometry_id(&edit.session.doc, u, v, radius),
None => return false,
};
let Some(geo_id) = geo_id else {
return false;
};
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
edit.record_undo();
let changed = crate::sketch::trim::trim_geometry(&mut edit.session.doc, &geo_id, u, v);
if !changed {
edit.undo_stack.pop();
return false;
}
edit.session.clear_selection();
edit.session.set_hover(None);
self.resolve_active_sketch("trim");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
}
impl EngineState {
fn sketch_top_edge_name(&self, x: f64, y: f64) -> Option<String> {
let candidates: serde_json::Value = serde_json::from_str(&self.pick_json(x, y)).ok()?;
candidates
.as_array()?
.iter()
.find(|c| c.get("kind").and_then(|k| k.as_str()) == Some("EDGE"))
.and_then(|c| c.get("name").and_then(|n| n.as_str()))
.filter(|name| !name.is_empty())
.map(str::to_string)
}
pub fn sketch_pick_edge_at(&mut self, x: f64, y: f64) -> bool {
if self.sketch_edit.is_none() {
return false;
}
if let Some(context) = self.context_edge_at(x, y) {
let tolerance = 1e-6 * context.depth.abs().max(1.0) + 1e-4;
let own_in_front = pick::pick_unoccluded(&self.scene, &self.context_scene, &self.camera, x, y, &self.pick_options())
.into_iter()
.find(|candidate| candidate.kind == pick::PickKind::Edge)
.is_some_and(|own| own.depth <= context.depth + tolerance);
if !own_in_front {
return self.sketch_copy_context_edge(&context);
}
}
let Some(edge_name) = self.sketch_top_edge_name(x, y) else {
return false;
};
self.sketch_link_edge(&edge_name)
}
pub fn sketch_link_edge(&mut self, edge_name: &str) -> bool {
let Some(world_poly) = self.scene.edge_polyline_world(edge_name) else {
return false;
};
let solid_name = self
.scene
.edge_solid_name(edge_name)
.unwrap_or_default()
.to_string();
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
edit.record_undo();
let changed = crate::sketch::external_ref::link_or_update(
&mut edit.session.doc,
&mut edit.external_refs,
edge_name,
&solid_name,
&world_poly,
&edit.session.plane,
);
if !changed {
edit.undo_stack.pop();
return false;
}
self.resolve_active_sketch("pickEdges");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub(super) fn sketch_sync_external_refs(&mut self, listed: &[String], links_listed: bool) -> bool {
let Some(edit) = self.sketch_edit.as_ref() else {
return false;
};
let mut names: Vec<String> = listed.to_vec();
for r in &edit.external_refs {
if !names.contains(&r.edge_name) {
names.push(r.edge_name.clone());
}
}
let mut jobs: Vec<(String, String, Vec<[f64; 3]>)> = Vec::new();
let mut unlinked: Vec<String> = Vec::new();
for name in &names {
match self.scene.edge_polyline_world(name) {
Some(poly) => {
let solid = self.scene.edge_solid_name(name).unwrap_or_default().to_string();
jobs.push((name.clone(), solid, poly));
}
None => {
if listed.contains(name) && !edit.external_refs.iter().any(|r| &r.edge_name == name) {
unlinked.push(name.clone());
}
}
}
}
let released: Vec<crate::sketch::ExternalRef> = if links_listed {
edit.external_refs
.iter()
.filter(|r| !listed.contains(&r.edge_name))
.cloned()
.collect()
} else {
Vec::new()
};
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
edit.listed_unlinked = unlinked;
let mut changed = false;
for r in &released {
crate::sketch::external_ref::release_ref(&mut edit.session.doc, r);
edit.external_refs.retain(|live| live.edge_name != r.edge_name);
changed = true;
}
for (edge_name, solid_name, world_poly) in &jobs {
if released.iter().any(|r| &r.edge_name == edge_name) {
continue;
}
changed |= crate::sketch::external_ref::link_or_update(
&mut edit.session.doc,
&mut edit.external_refs,
edge_name,
solid_name,
world_poly,
&edit.session.plane,
);
}
if changed {
self.resolve_active_sketch("external-ref sync");
self.refresh_sketch_overlay();
self.dirty = true;
}
changed
}
pub fn sketch_external_ref_count(&self) -> usize {
self.sketch_edit
.as_ref()
.map_or(0, |edit| edit.external_refs.len())
}
}
impl EngineState {
pub fn sketch_handdraw_begin(&mut self, x: f64, y: f64) {
if let Some((u, v)) = self.sketch_uv_at(x, y) {
self.sketch_handdraw_begin_uv(u, v);
}
}
pub fn sketch_handdraw_move(&mut self, x: f64, y: f64) {
if let Some((u, v)) = self.sketch_uv_at(x, y) {
self.sketch_handdraw_move_uv(u, v);
}
}
pub fn sketch_handdraw_begin_uv(&mut self, u: f64, v: f64) {
if let Some(edit) = self.sketch_edit.as_mut() {
edit.record_undo();
edit.handdraw_stroke.clear();
edit.handdraw_stroke.push((u, v));
} else {
return;
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_handdraw_move_uv(&mut self, u: f64, v: f64) {
let min_step = 2.0 * self.camera.world_per_pixel();
if let Some(edit) = self.sketch_edit.as_mut() {
let Some(&last) = edit.handdraw_stroke.last() else {
return; };
if (u - last.0).hypot(v - last.1) < min_step {
return;
}
edit.handdraw_stroke.push((u, v));
} else {
return;
}
self.refresh_sketch_overlay();
self.dirty = true;
}
pub fn sketch_handdraw_end(&mut self) -> bool {
let radius = self.sketch_pick_radius();
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
if edit.handdraw_stroke.is_empty() {
return false; }
let stroke = std::mem::take(&mut edit.handdraw_stroke);
if stroke.len() < 3 || crate::sketch::handdraw::stroke_extent(&stroke) < radius {
edit.undo_stack.pop();
self.refresh_sketch_overlay();
self.dirty = true;
return false;
}
let shape = crate::sketch::handdraw::recognize(&stroke);
crate::sketch::handdraw::emit_shape(&mut edit.session.doc, &shape, radius);
self.resolve_active_sketch("handdraw");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
pub fn sketch_handdraw_len(&self) -> usize {
self.sketch_edit
.as_ref()
.map_or(0, |edit| edit.handdraw_stroke.len())
}
}
impl EngineState {
pub fn sketch_selected_angle_constraint(&self) -> Option<serde_json::Value> {
use crate::sketch::doc::id_key;
let edit = self.sketch_edit.as_ref()?;
if edit.session.selection.len() != 1 {
return None;
}
let r = &edit.session.selection[0];
if r.get("kind").and_then(serde_json::Value::as_str) != Some("constraint") {
return None;
}
let id = r.get("id")?;
let key = id_key(id);
let c = edit
.session
.doc
.constraints
.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))?;
(c.ctype() == Some("∠") && c.points().len() >= 4).then(|| id.clone())
}
pub fn sketch_cycle_angle_measure(&mut self) -> bool {
use crate::sketch::doc::id_key;
let Some(id) = self.sketch_selected_angle_constraint() else {
return false;
};
let key = id_key(&id);
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let measured = {
let doc = &edit.session.doc;
let Some(c) = doc
.constraints
.iter()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return false;
};
let pts = c.points();
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 dir = |a: (f64, f64), b: (f64, f64)| (b.1 - a.1).atan2(b.0 - a.0).to_degrees();
(dir(p0, p1) - dir(p2, p3)).rem_euclid(360.0)
};
edit.record_undo();
let doc = &mut edit.session.doc;
let Some(c) = doc
.constraints
.iter_mut()
.find(|c| c.raw.get("id").map(id_key) == Some(key.clone()))
else {
return false;
};
let pts: Vec<serde_json::Value> = c.points().to_vec();
let value = c
.raw
.get("value")
.and_then(serde_json::Value::as_f64)
.filter(|v| v.is_finite())
.map(|v| v.rem_euclid(360.0))
.unwrap_or_else(|| (measured * 1e4).round() / 1e4);
let reflex = value > 180.0;
let (order, pivot): (Vec<serde_json::Value>, f64) = if reflex {
(vec![pts[2].clone(), pts[3].clone(), pts[0].clone(), pts[1].clone()], 360.0)
} else {
(vec![pts[3].clone(), pts[2].clone(), pts[0].clone(), pts[1].clone()], 180.0)
};
let next = ((pivot - value).rem_euclid(360.0) * 1e4).round() / 1e4;
c.raw.insert("points".to_string(), serde_json::Value::Array(order));
c.raw.insert("value".to_string(), serde_json::json!(next));
c.raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(false));
if let Some(expr) = c
.raw
.get("valueExpr")
.and_then(serde_json::Value::as_str)
.filter(|s| !s.trim().is_empty())
.map(str::to_string)
{
c.raw.insert(
"valueExpr".to_string(),
serde_json::Value::String(format!("{pivot} - ({expr})")),
);
}
c.raw.remove("previousPointValues");
c.raw.remove("_previousSolveValue");
c.raw.remove("status");
c.raw.remove("error");
self.resolve_active_sketch("angle-measure");
self.refresh_sketch_overlay();
self.dirty = true;
true
}
}