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,
})
})
.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 };
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;
}
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_reproject_external_refs(&mut self) -> bool {
let jobs: Vec<(String, String, Vec<[f64; 3]>)> = match self.sketch_edit.as_ref() {
Some(edit) if !edit.external_refs.is_empty() => edit
.external_refs
.iter()
.filter_map(|r| {
let poly = self.scene.edge_polyline_world(&r.edge_name)?;
let solid = self
.scene
.edge_solid_name(&r.edge_name)
.unwrap_or_default()
.to_string();
Some((r.edge_name.clone(), solid, poly))
})
.collect(),
_ => return false,
};
if jobs.is_empty() {
return false;
}
let Some(edit) = self.sketch_edit.as_mut() else {
return false;
};
let mut changed = false;
for (edge_name, solid_name, world_poly) in &jobs {
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 reproject");
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())
}
}