use super::*;
#[derive(Clone)]
pub struct SketchDrag {
pub(super) points: Vec<(serde_json::Value, f64, f64, bool)>,
pub(super) anchor: (f64, f64),
}
pub struct SketchEdit {
pub session: crate::sketch::SketchSession,
pub feature_id: String,
prev_rollback: usize,
prev_camera: crate::view::ViewCamera,
is_new: bool,
pub(super) drag: Option<SketchDrag>,
pub(super) pending: Vec<serde_json::Value>,
pub(super) hover_uv: Option<(f64, f64)>,
pub(super) undo_stack: Vec<SketchSnapshot>,
pub(super) redo_stack: Vec<SketchSnapshot>,
pub(super) dim_drag_snapshotted: bool,
pub(super) external_refs: Vec<crate::sketch::ExternalRef>,
pub(super) handdraw_stroke: Vec<(f64, f64)>,
}
#[derive(Clone)]
pub(super) struct SketchSnapshot {
pub(super) doc: crate::sketch::SketchDoc,
pub(super) dim_offsets: serde_json::Map<String, serde_json::Value>,
pub(super) selection: Vec<serde_json::Value>,
pub(super) external_refs: Vec<crate::sketch::ExternalRef>,
}
impl SketchEdit {
pub(super) fn snapshot(&self) -> SketchSnapshot {
SketchSnapshot {
doc: self.session.doc.clone(),
dim_offsets: self.session.dim_offsets.clone(),
selection: self.session.selection.clone(),
external_refs: self.external_refs.clone(),
}
}
pub(super) fn record_undo(&mut self) {
let snap = self.snapshot();
self.undo_stack.push(snap);
self.redo_stack.clear();
}
}
fn plane_frame_from_kernel(frame: &brep_kernel::Frame) -> crate::sketch::PlaneFrame {
crate::sketch::PlaneFrame {
origin: [frame.origin.x, frame.origin.y, frame.origin.z],
x_axis: [frame.x_axis.x, frame.x_axis.y, frame.x_axis.z],
y_axis: [frame.y_axis.x, frame.y_axis.y, frame.y_axis.z],
z_axis: [frame.z_axis.x, frame.z_axis.y, frame.z_axis.z],
}
}
impl EngineState {
pub fn sketch_mode(&self) -> bool {
self.sketch_edit.is_some()
}
pub fn sketch_edit_session(&self) -> Option<&crate::sketch::SketchSession> {
self.sketch_edit.as_ref().map(|edit| &edit.session)
}
pub fn sketch_edit_feature_id(&self) -> Option<&str> {
self.sketch_edit.as_ref().map(|edit| edit.feature_id.as_str())
}
pub fn sketch_camera_locked(&self) -> bool {
self.sketch_camera_locked
}
pub fn toggle_sketch_camera_lock(&mut self) {
if !self.sketch_mode() {
return;
}
self.sketch_camera_locked = !self.sketch_camera_locked;
if self.sketch_camera_locked {
if let Some(plane) = self.sketch_edit_session().map(|s| s.plane) {
self.face_camera_to_plane(&plane);
self.dirty = true;
}
}
}
fn face_camera_to_plane(&mut self, plane: &crate::sketch::PlaneFrame) {
let dir = [
-(plane.z_axis[0] as f32),
-(plane.z_axis[1] as f32),
-(plane.z_axis[2] as f32),
];
let up = [
plane.y_axis[0] as f32,
plane.y_axis[1] as f32,
plane.y_axis[2] as f32,
];
self.apply_look_direction(dir, up);
}
pub fn enter_sketch_mode(&mut self, feature_id: &str) -> Result<String, String> {
let index = self
.history
.index_of(feature_id)
.ok_or_else(|| format!("no feature with id '{feature_id}'"))?;
if self.history.feature_type(index).as_deref() != Some("S") {
return Err(format!("feature '{feature_id}' is not a sketch (type \"S\")"));
}
let resolved_frame = self
.construction_frames
.iter()
.find(|(name, _)| name == feature_id)
.map(|(_, frame)| plane_frame_from_kernel(frame));
let prev_camera = self.camera.clone();
let prev_rollback = self.history.rollback();
self.history.set_rollback(index.saturating_sub(1));
self.rerun_history();
let persistent = self.history.feature_persistent_data(index);
let plane = resolved_frame
.or_else(|| {
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 = serde_json::from_value(doc_value)
.map_err(|error| format!("sketch doc parse: {error}"))?;
let mut session = crate::sketch::SketchSession::new(doc, plane)?;
session.colors = self.settings.sketch_colors();
if let Some(offsets) = persistent
.as_ref()
.and_then(|p| p.get("dimOffsets"))
.and_then(serde_json::Value::as_object)
{
session.dim_offsets = offsets.clone();
}
let external_refs: Vec<crate::sketch::ExternalRef> = persistent
.as_ref()
.and_then(|p| p.get("externalRefs"))
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
self.clear_selection();
self.sketch_camera_locked = true;
self.face_camera_to_plane(&plane);
self.set_sketch_overlay(&session);
let diagnostics =
serde_json::to_string(&session.diagnostics).unwrap_or_else(|_| "{}".to_string());
self.sketch_edit = Some(SketchEdit {
session,
feature_id: feature_id.to_string(),
prev_rollback,
prev_camera,
is_new: false,
drag: None,
pending: Vec::new(),
hover_uv: None,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
dim_drag_snapshotted: false,
external_refs,
handdraw_stroke: Vec::new(),
});
let reprojected = self.sketch_reproject_external_refs();
let diagnostics = if reprojected {
self.sketch_edit
.as_ref()
.map(|edit| {
serde_json::to_string(&edit.session.diagnostics)
.unwrap_or_else(|_| "{}".to_string())
})
.unwrap_or(diagnostics)
} else {
diagnostics
};
self.refresh_committed_sketches();
self.dirty = true;
Ok(diagnostics)
}
pub fn exit_sketch_mode(&mut self, commit: bool) -> String {
let Some(edit) = self.sketch_edit.take() else {
return "{}".to_string();
};
if commit {
if let Some(index) = self.history.index_of(&edit.feature_id) {
let sketch_value = serde_json::to_value(&edit.session.doc).unwrap_or_else(|_| {
serde_json::json!({ "points": [], "geometries": [], "constraints": [] })
});
self.history
.set_feature_persistent_field(index, "sketch", sketch_value);
self.history.set_feature_persistent_field(
index,
"dimOffsets",
serde_json::Value::Object(edit.session.dim_offsets.clone()),
);
let refs_value = serde_json::to_value(&edit.external_refs)
.unwrap_or_else(|_| serde_json::Value::Array(Vec::new()));
self.history
.set_feature_persistent_field(index, "externalRefs", refs_value);
}
} else if edit.is_new {
if let Some(index) = self.history.index_of(&edit.feature_id) {
self.history.remove_feature(index);
}
}
self.history.set_rollback(edit.prev_rollback);
self.clear_sketch_overlay();
self.camera = edit.prev_camera;
self.dirty = true;
self.rerun_history()
}
pub fn new_sketch(&mut self, plane: &str) -> Result<String, String> {
let frame = match plane {
"XY" => crate::sketch::PlaneFrame::xy(),
"XZ" => crate::sketch::PlaneFrame::xz(),
"YZ" => crate::sketch::PlaneFrame::yz(),
other => {
return Err(format!("unknown base plane '{other}' (expected XY|XZ|YZ)"));
}
};
let id = self
.history
.next_feature_id(&crate::features::feature_short_name("S"));
let feature = serde_json::json!({
"type": "S",
"inputParams": { "id": id, "sketchPlane": plane },
"persistentData": {
"basis": {
"origin": frame.origin,
"x": frame.x_axis,
"y": frame.y_axis,
"z": frame.z_axis,
},
"sketch": { "points": [], "geometries": [], "constraints": [] }
}
});
self.history.push_feature(feature);
let diagnostics = self.enter_sketch_mode(&id)?;
if let Some(edit) = self.sketch_edit.as_mut() {
edit.is_new = true;
}
Ok(diagnostics)
}
}