use super::*;
use brep_kernel::{
PmiAnnotation, PmiCamera, PmiDisplay, PmiGeometry, PmiProjection, PmiReport, PmiState,
PmiStatus, PmiView,
};
use crate::view::Projection;
#[derive(Debug, Clone)]
pub struct PmiModelingSnapshot {
pub camera_json: String,
pub hidden: Vec<String>,
pub wireframe: bool,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct PmiViewPatch {
pub name: Option<String>,
pub text_size_pt: Option<f64>,
pub wireframe: Option<bool>,
pub hidden: Option<Vec<String>>,
}
pub fn world_vertex_ref(solid: &str, position: [f64; 3]) -> String {
let trim = |v: f64| {
let rounded = (v * 1e9).round() / 1e9;
let trimmed = crate::formatting::compact_decimal(rounded, 9);
if trimmed == "-0" || trimmed.is_empty() { "0".to_string() } else { trimmed }
};
format!("{solid}@{},{},{}", trim(position[0]), trim(position[1]), trim(position[2]))
}
impl EngineState {
pub fn pmi_state(&self) -> PmiState {
self.history
.pmi_block()
.and_then(|block| serde_json::from_value(block.clone()).ok())
.unwrap_or_default()
}
pub fn pmi_report(&self) -> Option<&PmiReport> {
self.pmi_report.as_ref()
}
pub fn pmi_active_view(&self) -> Option<&str> {
self.pmi_active_view.as_deref()
}
pub fn pmi_open_annotation(&self) -> Option<&str> {
self.pmi_open_annotation.as_deref()
}
pub fn pmi_workbench_entered(&self) -> bool {
self.pmi_modeling.is_some()
}
pub fn pmi_state_json(&self) -> String {
let state = self.pmi_state();
serde_json::json!({
"views": state.views,
"idCounter": state.id_counter,
"activeView": self.pmi_active_view,
"openAnnotation": self.pmi_open_annotation,
"entered": self.pmi_modeling.is_some(),
"datums": state.datum_letters(),
"report": self.pmi_report,
})
.to_string()
}
fn write_pmi_state(&mut self, state: PmiState) -> String {
let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
self.history.set_pmi_block(block, None);
self.rerun_history()
}
fn snapshot_camera(&self) -> PmiCamera {
let camera = &self.camera;
PmiCamera {
eye: camera.eye,
target: camera.target,
up: camera.up,
projection: match camera.projection {
Projection::Orthographic { half_height } => PmiProjection::Orthographic { half_height },
Projection::Perspective { fov_y_deg } => PmiProjection::Perspective { fov_y_deg },
},
viewport: [camera.width, camera.height],
}
}
fn hidden_solid_names(&self) -> Vec<String> {
self.scene
.solids()
.iter()
.filter(|solid| !solid.visible)
.map(|solid| solid.name.clone())
.collect()
}
pub fn pmi_capture_view(&mut self, name: Option<&str>) -> String {
let mut state = self.pmi_state();
let id = state.next_id("VIEW");
let name = name
.map(str::trim)
.filter(|n| !n.is_empty())
.map(String::from)
.unwrap_or_else(|| format!("View {}", state.views.len() + 1));
let was_active = self.pmi_active_view.is_some();
if was_active {
self.pmi_deactivate_view();
}
self.pmi_remember_modeling();
state.views.push(PmiView {
id: id.clone(),
name,
camera: Some(self.snapshot_camera()),
display: PmiDisplay {
text_size_pt: 12.0,
wireframe: self.settings.wireframe,
hidden: self.hidden_solid_names(),
},
annotations: Vec::new(),
});
self.write_pmi_state(state);
let _ = self.pmi_activate_view(&id);
id
}
pub fn pmi_rename_view(&mut self, id: &str, name: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
let name = name.trim();
if name.is_empty() {
return Err("a view needs a name".into());
}
view.name = name.to_string();
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_delete_view(&mut self, id: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let before = state.views.len();
if self.pmi_active_view.as_deref() == Some(id) {
self.pmi_deactivate_view();
}
state.views.retain(|view| view.id != id);
if state.views.len() == before {
return Err(format!("no PMI view '{id}'"));
}
if let Some(open) = &self.pmi_open_annotation {
if state.find_annotation(open).is_none() {
self.pmi_open_annotation = None;
}
}
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let camera = self.snapshot_camera();
let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
view.camera = Some(camera);
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_update_view_visibility(&mut self, id: &str) -> Result<(), String> {
let hidden = self.hidden_solid_names();
self.pmi_set_view_display(id, &PmiViewPatch { hidden: Some(hidden), ..Default::default() })
}
pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
let mut state = self.pmi_state();
let view = state.find_view_mut(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
if let Some(name) = &patch.name {
let name = name.trim();
if name.is_empty() {
return Err("a view needs a name".into());
}
view.name = name.to_string();
}
if let Some(size) = patch.text_size_pt {
view.display.text_size_pt = brep_kernel::clamp_text_size(size);
}
if let Some(wireframe) = patch.wireframe {
view.display.wireframe = wireframe;
}
if let Some(hidden) = &patch.hidden {
view.display.hidden = hidden.clone();
}
let display = view.display.clone();
self.write_pmi_state(state);
if self.pmi_active_view.as_deref() == Some(id) {
self.apply_view_display(&display);
}
Ok(())
}
fn pmi_remember_modeling(&mut self) {
if self.pmi_active_view.is_none() || self.pmi_modeling.is_none() {
self.pmi_modeling = Some(PmiModelingSnapshot {
camera_json: self.camera_state_json(),
hidden: self.hidden_solid_names(),
wireframe: self.settings.wireframe,
});
}
}
pub fn pmi_enter_workbench(&mut self) {
self.pmi_remember_modeling();
}
pub fn pmi_leave_workbench(&mut self) {
self.pmi_deactivate_view();
self.pmi_modeling = None;
self.pmi_open_annotation = None;
self.refresh_pmi_overlay();
}
fn apply_view_display(&mut self, display: &PmiDisplay) {
let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
for name in names {
let visible = !display.hidden.contains(&name);
self.scene.set_visible(&name, visible);
}
if self.settings.wireframe != display.wireframe {
self.settings.wireframe = display.wireframe;
self.settings_generation = self.settings_generation.wrapping_add(1);
}
self.dirty = true;
}
pub fn pmi_activate_view(&mut self, id: &str) -> Result<(), String> {
let state = self.pmi_state();
let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?.clone();
if self.pmi_active_view.as_deref() != Some(id) {
let keep = self.pmi_active_view.is_some();
self.pmi_deactivate_view();
if !keep {
self.pmi_remember_modeling();
}
}
if self.pmi_modeling.is_none() {
self.pmi_remember_modeling();
}
self.pmi_active_view = Some(id.to_string());
if let Some(camera) = &view.camera {
let (kind, scale) = match camera.projection {
PmiProjection::Orthographic { half_height } => ("orthographic", half_height),
PmiProjection::Perspective { fov_y_deg } => ("perspective", fov_y_deg),
};
let json = serde_json::json!({
"kind": kind, "eye": camera.eye, "target": camera.target, "up": camera.up, "scale": scale,
})
.to_string();
let _ = self.apply_camera_state_json(&json);
self.controls_sync_after_camera_apply();
}
self.apply_view_display(&view.display);
self.pmi_apply_explode();
if let Some(open) = &self.pmi_open_annotation {
if !view.annotations.iter().any(|a| a.id() == open) {
self.pmi_open_annotation = None;
}
}
self.refresh_pmi_overlay();
Ok(())
}
pub fn pmi_deactivate_view(&mut self) {
if self.pmi_active_view.take().is_none() {
return;
}
self.pmi_restore_explode();
if let Some(snapshot) = self.pmi_modeling.clone() {
let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
for name in names {
let visible = !snapshot.hidden.contains(&name);
self.scene.set_visible(&name, visible);
}
if self.settings.wireframe != snapshot.wireframe {
self.settings.wireframe = snapshot.wireframe;
self.settings_generation = self.settings_generation.wrapping_add(1);
}
let _ = self.apply_camera_state_json(&snapshot.camera_json);
self.controls_sync_after_camera_apply();
}
self.pmi_open_annotation = None;
self.dirty = true;
self.refresh_pmi_overlay();
}
fn controls_sync_after_camera_apply(&mut self) {
self.dirty = true;
}
pub(crate) fn pmi_apply_explode(&mut self) {
self.pmi_restore_explode();
let Some(active) = self.pmi_active_view.clone() else {
return;
};
let Some(report) = self.pmi_report.as_ref() else {
return;
};
let Some(view) = report.view(&active) else {
return;
};
let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = view
.annotations
.iter()
.filter(|row| row.enabled && row.status == PmiStatus::Ok)
.filter_map(|row| match &row.geometry {
PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
Some((solids.clone(), *translate, *rotate_deg, *scale, *center))
}
_ => None,
})
.collect();
for (solids, translate, rotate_deg, scale, center) in poses {
for name in solids {
let Some(display) = self.scene.solid(&name) else { continue };
self.pmi_explode_originals
.entry(name.clone())
.or_insert_with(|| display.clone());
if let Some(display) = self.scene.solid_mut(&name) {
transform_display(display, center, translate, rotate_deg, scale);
}
}
}
if !self.pmi_explode_originals.is_empty() {
self.dirty = true;
}
}
pub(crate) fn pmi_restore_explode(&mut self) {
let originals = std::mem::take(&mut self.pmi_explode_originals);
for (name, original) in originals {
if let Some(display) = self.scene.solid_mut(&name) {
*display = original;
}
}
self.dirty = true;
}
pub(crate) fn pmi_after_apply(&mut self) {
self.pmi_restore_explode();
if let Some(active) = self.pmi_active_view.clone() {
let exists = self.pmi_state().find_view(&active).is_some();
if !exists {
self.pmi_active_view = None;
self.pmi_open_annotation = None;
} else {
self.pmi_apply_explode();
}
}
if let Some(open) = &self.pmi_open_annotation {
if self.pmi_state().find_annotation(open).is_none() {
self.pmi_open_annotation = None;
}
}
self.refresh_pmi_overlay();
}
pub fn pmi_add_annotation(
&mut self,
view_id: Option<&str>,
type_id: &str,
params_json: &str,
) -> Result<String, String> {
let def = brep_kernel::pmi_type(type_id).ok_or_else(|| format!("unknown PMI annotation type '{type_id}'"))?;
let mut state = self.pmi_state();
let view_id = view_id
.map(String::from)
.or_else(|| self.pmi_active_view.clone())
.ok_or_else(|| "no active PMI view — capture or activate a view first".to_string())?;
let id = state.next_id(def.short_name);
let mut params: serde_json::Value = serde_json::from_str(params_json).unwrap_or_else(|_| serde_json::json!({}));
if !params.is_object() {
params = serde_json::json!({});
}
let schema = (def.schema)();
if let (Some(fields), Some(object)) = (
schema.get("inputParamsSchema").and_then(serde_json::Value::as_object),
params.as_object_mut(),
) {
for (key, spec) in fields {
if !object.contains_key(key) {
if let Some(default) = spec.get("default_value") {
if !default.is_null() {
object.insert(key.clone(), default.clone());
}
}
}
}
object.insert("id".into(), serde_json::Value::String(id.clone()));
if type_id == "datum" {
let letter = object.get("letter").and_then(serde_json::Value::as_str).unwrap_or("").trim().to_string();
if letter.is_empty() {
if let Some(next) = state.next_datum_letter() {
object.insert("letter".into(), serde_json::Value::String(next));
}
}
}
}
let view = state.find_view_mut(&view_id).ok_or_else(|| format!("no PMI view '{view_id}'"))?;
view.annotations.push(PmiAnnotation {
kind: type_id.to_string(),
enabled: true,
params,
label_world: None,
});
self.pmi_open_annotation = Some(id.clone());
self.write_pmi_state(state);
Ok(id)
}
pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
self.pmi_update_annotation_no_rerun(id, params_json)?;
self.rerun_history();
Ok(())
}
pub(crate) fn pmi_update_annotation_no_rerun(&mut self, id: &str, params_json: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
let mut params: serde_json::Value =
serde_json::from_str(params_json).map_err(|error| format!("annotation params: {error}"))?;
if let Some(object) = params.as_object_mut() {
object.insert("id".into(), serde_json::Value::String(id.to_string()));
}
annotation.params = params;
let block = serde_json::to_value(&state).ok();
self.history.set_pmi_block(block, Some(&format!("pmi:params:{id}")));
Ok(())
}
pub fn pmi_remove_annotation(&mut self, id: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let Some((view_index, index)) = state.locate_annotation(id) else {
return Err(format!("no PMI annotation '{id}'"));
};
state.views[view_index].annotations.remove(index);
if self.pmi_open_annotation.as_deref() == Some(id) {
self.pmi_open_annotation = None;
}
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_set_annotation_enabled(&mut self, id: &str, enabled: bool) -> Result<(), String> {
let mut state = self.pmi_state();
let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
if annotation.enabled == enabled {
return Ok(());
}
annotation.enabled = enabled;
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_move_annotation(&mut self, id: &str, index: usize) -> Result<(), String> {
let mut state = self.pmi_state();
let Some((view_index, from)) = state.locate_annotation(id) else {
return Err(format!("no PMI annotation '{id}'"));
};
let annotations = &mut state.views[view_index].annotations;
let annotation = annotations.remove(from);
let to = index.min(annotations.len());
annotations.insert(to, annotation);
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_move_annotation_to_view(&mut self, id: &str, view_id: &str) -> Result<(), String> {
let mut state = self.pmi_state();
let Some((view_index, from)) = state.locate_annotation(id) else {
return Err(format!("no PMI annotation '{id}'"));
};
if state.find_view(view_id).is_none() {
return Err(format!("no PMI view '{view_id}'"));
}
let annotation = state.views[view_index].annotations.remove(from);
state.find_view_mut(view_id).expect("checked").annotations.push(annotation);
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_set_annotation_open(&mut self, id: Option<&str>) {
match id {
Some(id) => {
let state = self.pmi_state();
if let Some((view, _)) = state.find_annotation(id) {
let view_id = view.id.clone();
if self.pmi_active_view.as_deref() != Some(view_id.as_str()) {
let _ = self.pmi_activate_view(&view_id);
}
self.pmi_open_annotation = Some(id.to_string());
}
}
None => self.pmi_open_annotation = None,
}
self.refresh_pmi_overlay();
}
pub fn pmi_set_label_world(&mut self, id: &str, world: [f64; 3]) -> Result<(), String> {
let mut state = self.pmi_state();
let annotation = state.find_annotation_mut(id).ok_or_else(|| format!("no PMI annotation '{id}'"))?;
annotation.label_world = Some(world);
let block = serde_json::to_value(&state).ok();
self.history.set_pmi_block(block, Some(&format!("pmi:label:{id}")));
if let Some(report) = self.pmi_report.as_mut() {
if let Some(row) = report.annotation_mut(id) {
row.label_world = world;
if let PmiGeometry::Note { position } = &mut row.geometry {
*position = world;
}
}
}
self.refresh_pmi_overlay();
Ok(())
}
pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
let Some((current, plane)) = self
.pmi_report
.as_ref()
.and_then(|r| r.annotation(id))
.map(|r| (r.label_world, r.plane))
else {
return;
};
let ray = self.camera.pick_ray(x, y);
let plane = plane.unwrap_or_else(|| {
let (_, _, view) = self.camera.basis();
brep_kernel::PmiPlane {
origin: current,
normal: view,
x_axis: [0.0; 3],
}
});
let Some(world) = plane.hit(ray.origin, ray.dir) else {
return;
};
let _ = self.pmi_set_label_world(id, world);
}
pub fn pmi_label_drag_end(&mut self) {
self.history.break_coalescing();
}
pub fn pmi_hover(&mut self, id: &str) {
self.pmi_label_hover_active = true;
if self.pmi_hovered.as_deref() == Some(id) {
return;
}
let references: Vec<String> = self
.pmi_report
.as_ref()
.and_then(|report| report.annotation(id))
.map(|row| row.references.clone())
.unwrap_or_default();
let mut solids: Vec<String> = Vec::new();
let mut faces: Vec<String> = Vec::new();
let mut edges: Vec<String> = Vec::new();
for reference in &references {
if let Some(at) = reference.find('@') {
solids.push(reference[..at].to_string());
} else if self.scene_has_face(reference) {
faces.push(reference.clone());
} else if self.scene_has_edge(reference) {
edges.push(reference.clone());
} else if self.scene.solid(reference).is_some() {
solids.push(reference.clone());
} else {
let prefix = format!("{reference}:");
solids.extend(self.scene.solids().iter().filter(|s| s.name.starts_with(&prefix)).map(|s| s.name.clone()));
}
}
self.clear_hover();
self.emphasis.hovered_solids.extend(solids);
self.emphasis.hovered_faces.extend(faces);
self.emphasis.hovered_edges.extend(edges);
self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
self.pmi_hovered = Some(id.to_string());
self.dirty = true;
}
pub fn pmi_hover_end(&mut self) {
if self.pmi_hovered.take().is_some() {
self.clear_hover();
self.dirty = true;
}
}
pub fn take_pmi_label_hover(&mut self) -> bool {
std::mem::take(&mut self.pmi_label_hover_active)
}
pub fn pmi_label_clicked(&mut self, id: &str) {
self.pmi_set_annotation_open(Some(id));
}
pub fn begin_ref_select_for_pmi(
&mut self,
id: &str,
path: Vec<String>,
label: String,
filter: Vec<String>,
multiple: bool,
seed_names: Vec<String>,
) {
let restore_index = self.history.rollback();
self.selection_filter = SelectionFilter::from_ref_filter(&filter);
self.ref_select = Some(RefSelectState {
feature_id: id.to_string(),
path,
label,
filter,
multiple,
names: seed_names,
restore_index,
target: RefSelectTarget::Pmi,
});
self.sync_ref_select_emphasis();
}
pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
let state = self.pmi_state();
let Some((_, annotation)) = state.find_annotation(id) else {
self.push_notice(format!("unknown PMI annotation '{id}'"));
return;
};
let mut params = annotation.params.clone();
let value = if multiple {
serde_json::Value::Array(names.iter().cloned().map(serde_json::Value::String).collect())
} else {
serde_json::Value::String(names.first().cloned().unwrap_or_default())
};
super::selection_ux::set_json_at(&mut params, path, value);
if let Err(error) = self.pmi_update_annotation_no_rerun(id, ¶ms.to_string()) {
self.push_notice(format!("PMI update failed: {error}"));
}
}
pub(crate) fn pmi_merge_imported(&mut self, lifted: PmiState) {
let mut state = self.pmi_state();
for view in lifted.views {
let id = state.next_id("VIEW");
let mut annotations = Vec::with_capacity(view.annotations.len());
for mut annotation in view.annotations {
let prefix = brep_kernel::pmi_type(&annotation.kind).map(|def| def.short_name).unwrap_or("PMI");
let fresh = state.next_id(prefix);
if let Some(object) = annotation.params.as_object_mut() {
object.insert("id".into(), serde_json::Value::String(fresh));
}
annotations.push(annotation);
}
state.views.push(PmiView {
id,
name: view.name,
camera: view.camera,
display: view.display,
annotations,
});
}
let block = serde_json::to_value(&state).ok();
self.history.set_pmi_block_no_undo(block);
self.rerun_history();
}
}
fn transform_display(
display: &mut crate::scene::SolidDisplay,
center: [f64; 3],
translate: [f64; 3],
rotate_deg: [f64; 3],
scale: [f64; 3],
) {
let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_deg);
let pose = |p: [f64; 3]| -> [f64; 3] {
let local = [(p[0] - center[0]) * scale[0], (p[1] - center[1]) * scale[1], (p[2] - center[2]) * scale[2]];
let rotated = rotate(local);
[rotated[0] + center[0] + translate[0], rotated[1] + center[1] + translate[1], rotated[2] + center[2] + translate[2]]
};
let mut bbox = crate::camera::Aabb::empty();
for position in &mut display.mesh.positions {
let posed = pose([position[0] as f64, position[1] as f64, position[2] as f64]);
*position = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
bbox.expand(posed);
}
for normal in &mut display.mesh.normals {
let rotated = rotate([normal[0] as f64, normal[1] as f64, normal[2] as f64]);
*normal = [rotated[0] as f32, rotated[1] as f32, rotated[2] as f32];
}
for edge in &mut display.edges {
for point in &mut edge.polyline {
let posed = pose([point[0] as f64, point[1] as f64, point[2] as f64]);
*point = [posed[0] as f32, posed[1] as f32, posed[2] as f32];
bbox.expand(posed);
}
}
for vertex in &mut display.vertices {
vertex.position = pose(vertex.position);
bbox.expand(vertex.position);
}
if !bbox.is_empty() {
display.bbox = bbox;
}
}