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 section: Option<Option<brep_kernel::feature_pipeline::pmi::PmiSection>>,
}
pub fn pmi_view_schema() -> serde_json::Value {
serde_json::json!({
"type": "pmiView",
"shortName": "VIEW",
"longName": "PMI view",
"inputParamsSchema": {
"name": { "type": "string", "label": "Name", "default_value": "View 1" },
"textSizePt": { "type": "number", "label": "Text size (pt)", "default_value": brep_kernel::PmiDisplay::default().text_size_pt },
"wireframe": { "type": "boolean", "label": "Wireframe", "default_value": false },
"sectionEnabled": { "type": "boolean", "label": "Section view", "default_value": false },
"sectionAxis": { "type": "options", "label": "Section normal", "options": ["View", "X", "Y", "Z", "Plane"], "default_value": "View" },
"sectionPlane": {
"type": "reference_selection", "label": "Section plane", "selectionFilter": ["FACE", "PLANE"], "multiple": false,
"minSelections": 0, "maxSelections": 1, "default_value": null,
"hint": "A planar face, datum plane or sketch plane to cut on (Section normal: Plane); the offset moves the cut along its normal"
},
"sectionOffset": { "type": "number", "label": "Section offset (mm)", "default_value": 0.0 },
"sectionFlip": { "type": "boolean", "label": "Reverse section", "default_value": false },
"halfAxis": {
"type": "options", "label": "Half section", "options": ["None", "View", "X", "Y", "Z"], "default_value": "None",
"hint": "A second plane through the section's origin: material is removed only where both planes remove it (a quarter cut away)"
},
"halfOffset": { "type": "number", "label": "Half section offset (mm)", "default_value": 0.0 },
"halfFlip": { "type": "boolean", "label": "Reverse half section", "default_value": false },
"sectionExclude": {
"type": "reference_selection", "label": "Not cut", "selectionFilter": ["SOLID", "COMPONENT"], "multiple": true,
"minSelections": 0, "maxSelections": 64, "default_value": [],
"hint": "Solids or components the section leaves whole, such as shafts and fasteners"
},
"updateCamera": { "type": "button", "label": "Update camera from the viewport" },
"hideSelected": { "type": "button", "label": "Hide the selected bodies in this view" }
}
})
}
fn section_planes_from(
params: &serde_json::Value,
prior: Option<&brep_kernel::feature_pipeline::pmi::PmiSection>,
) -> Result<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>, String> {
use brep_kernel::feature_pipeline::pmi::PmiSectionPlane;
let prior_planes = prior.map(|s| s.planes.clone()).unwrap_or_default();
let valid = |plane: &PmiSectionPlane| ["View", "X", "Y", "Z"].contains(&plane.axis.as_str()) && plane.offset.is_finite();
let mut planes = match params.get("sectionPlanes") {
Some(serde_json::Value::Null) => Vec::new(),
Some(list) => {
let planes: Vec<PmiSectionPlane> = serde_json::from_value(list.clone()).map_err(|e| format!("invalid section planes: {e}"))?;
if !planes.iter().all(valid) {
return Err("invalid section plane".into());
}
planes
}
None => prior_planes,
};
if let Some(axis) = params.get("halfAxis").and_then(serde_json::Value::as_str) {
if axis == "None" {
if !planes.is_empty() {
planes.remove(0);
}
} else {
let before = planes.first();
let plane = PmiSectionPlane {
axis: axis.to_string(),
offset: params.get("halfOffset").and_then(serde_json::Value::as_f64).or(before.map(|p| p.offset)).unwrap_or(0.),
flip: params.get("halfFlip").and_then(serde_json::Value::as_bool).or(before.map(|p| p.flip)).unwrap_or(false),
normal: before.filter(|p| p.axis == axis).and_then(|p| p.normal),
};
if !valid(&plane) {
return Err("invalid section plane".into());
}
match planes.first_mut() {
Some(first) => *first = plane,
None => planes.push(plane),
}
}
}
Ok(planes)
}
pub fn pmi_view_params(view: &brep_kernel::PmiView) -> serde_json::Value {
serde_json::json!({
"name": view.name,
"textSizePt": view.display.text_size_pt,
"wireframe": view.display.wireframe,
"sectionEnabled": view.display.section.is_some(),
"sectionAxis": view.display.section.as_ref().map(|s| s.axis.as_str()).unwrap_or("View"),
"sectionOffset": view.display.section.as_ref().map(|s| s.offset).unwrap_or(0.),
"sectionFlip": view.display.section.as_ref().is_some_and(|s| s.flip),
"sectionPlane": view.display.section.as_ref().and_then(|s| s.plane.as_deref()).unwrap_or(""),
"sectionExclude": view.display.section.as_ref().map(|s| s.exclude.clone()).unwrap_or_default(),
"halfAxis": view.display.section.as_ref().and_then(|s| s.planes.first()).map(|p| p.axis.as_str()).unwrap_or("None"),
"halfOffset": view.display.section.as_ref().and_then(|s| s.planes.first()).map(|p| p.offset).unwrap_or(0.),
"halfFlip": view.display.section.as_ref().and_then(|s| s.planes.first()).is_some_and(|p| p.flip),
})
}
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_state_shared(&self) -> std::rc::Rc<PmiState> {
let revision = self.history.revision();
if let Some((at, state)) = self.pmi_state_cache.borrow().as_ref() {
if *at == revision {
return state.clone();
}
}
let state = std::rc::Rc::new(self.pmi_state());
*self.pmi_state_cache.borrow_mut() = Some((revision, state.clone()));
state
}
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_open_view(&self) -> Option<&str> {
self.pmi_open_view.as_deref()
}
pub fn pmi_dialog_opens(&self) -> u64 {
self.pmi_dialog_opens
}
fn open_pmi_annotation(&mut self, id: String) {
self.pmi_open_view = None;
self.pmi_selected_view = None;
self.pmi_selected_annotation = Some(id.clone());
self.pmi_open_annotation = Some(id);
self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
}
fn open_pmi_view(&mut self, id: String) {
self.pmi_open_annotation = None;
if self.pmi_selected_annotation.take().is_some() {
self.refresh_pmi_overlay();
}
self.pmi_selected_view = Some(id.clone());
self.pmi_open_view = Some(id);
self.pmi_dialog_opens = self.pmi_dialog_opens.wrapping_add(1);
}
pub fn pmi_selected_annotation(&self) -> Option<&str> {
let id = self.pmi_selected_annotation.as_deref()?;
self.pmi_state_shared().find_annotation(id).is_some().then_some(id)
}
pub fn pmi_selected_view(&self) -> Option<&str> {
let id = self.pmi_selected_view.as_deref()?;
self.pmi_state_shared().find_view(id).is_some().then_some(id)
}
pub fn pmi_workbench_entered(&self) -> bool {
self.pmi_modeling.is_some()
}
pub fn pmi_state_json(&self) -> String {
self.pmi_state_value().to_string()
}
pub fn pmi_state_value(&self) -> serde_json::Value {
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,
"openView": self.pmi_open_view,
"selectedAnnotation": self.pmi_selected_annotation(),
"selectedView": self.pmi_selected_view(),
"entered": self.pmi_modeling.is_some(),
"datums": state.datum_letters(),
"report": self.pmi_report,
})
}
pub(super) 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()
}
pub(super) 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 && !solid.is_sketch)
.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::new(12.0, self.settings.wireframe, None),
annotations: Vec::new(),
});
state.set_view_baseline(&id, &self.hidden_solid_names());
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;
}
}
if self.pmi_open_view.as_deref() == Some(id) {
self.pmi_open_view = None;
}
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_update_view_camera(&mut self, id: &str) -> Result<(), String> {
if let Some(step) = self.instructions_step_of_view(id) {
let shown = self.instructions_shown_step().as_deref() == Some(step.as_str());
self.instructions_capture_camera(&step)?;
if shown {
self.instructions_show_step(&step)?;
}
return Ok(());
}
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}'"))?;
if let Some(section) = view.display.section.as_mut() {
section.freeze(view.camera.as_ref());
}
view.camera = Some(camera);
self.write_pmi_state(state);
Ok(())
}
pub fn pmi_hide_selected(&mut self, view_id: Option<&str>) -> Result<String, String> {
let mut targets = self.selected_occurrences();
for solid in &self.emphasis.selected_solids {
if !targets.iter().any(|target| brep_kernel::entry_names_solid(target, solid)) {
targets.push(solid.clone());
}
}
if targets.is_empty() {
return Err("select the solids or components to hide".into());
}
let params = serde_json::json!({ "targets": targets }).to_string();
self.pmi_add_annotation(view_id, "hide", ¶ms)
}
pub fn pmi_set_view_display(&mut self, id: &str, patch: &PmiViewPatch) -> Result<(), String> {
self.patch_pmi_view(id, patch, None)
}
pub fn pmi_update_view(&mut self, id: &str, params_json: &str) -> Result<(), String> {
let params: serde_json::Value =
serde_json::from_str(params_json).map_err(|error| format!("view params: {error}"))?;
let state = self.pmi_state();
let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
let name = params
.get("name")
.and_then(serde_json::Value::as_str)
.filter(|name| *name != view.name)
.map(String::from);
let text_size_pt = params
.get("textSizePt")
.and_then(serde_json::Value::as_f64)
.filter(|size| *size != view.display.text_size_pt);
let wireframe = params
.get("wireframe")
.and_then(serde_json::Value::as_bool)
.filter(|on| *on != view.display.wireframe);
let section = if params.get("sectionEnabled").is_some() {
let section = if params["sectionEnabled"].as_bool().unwrap_or(false) {
let axis = params["sectionAxis"].as_str().unwrap_or("View");
let offset = params["sectionOffset"].as_f64().unwrap_or(0.);
if !["View", "X", "Y", "Z", "Plane"].contains(&axis) || !offset.is_finite() { return Err("invalid section plane".into()); }
let prior = view.display.section.as_ref();
let mut section = brep_kernel::feature_pipeline::pmi::PmiSection {
axis: axis.into(),
offset,
flip: params["sectionFlip"].as_bool().unwrap_or(false),
origin: prior.and_then(|s| s.origin),
normal: prior.filter(|s| s.axis == axis).and_then(|s| s.normal),
plane: match params.get("sectionPlane") {
Some(serde_json::Value::String(name)) => Some(name.trim().to_string()).filter(|name| !name.is_empty()),
Some(serde_json::Value::Null) => None,
_ => prior.and_then(|s| s.plane.clone()),
},
exclude: match params.get("sectionExclude") {
Some(serde_json::Value::Array(names)) => names
.iter()
.filter_map(serde_json::Value::as_str)
.map(|name| name.trim().to_string())
.filter(|name| !name.is_empty())
.collect(),
Some(serde_json::Value::Null) => Vec::new(),
_ => prior.map(|s| s.exclude.clone()).unwrap_or_default(),
},
planes: section_planes_from(¶ms, prior)?,
};
section.freeze(view.camera.as_ref());
Some(section)
} else { None };
(section != view.display.section).then_some(section)
} else { None };
if name.is_none() && text_size_pt.is_none() && wireframe.is_none() && section.is_none() {
return Ok(());
}
let patch = PmiViewPatch { name, text_size_pt, wireframe, hidden: None, section };
self.patch_pmi_view(id, &patch, Some(&format!("pmi:view:{id}")))
}
#[allow(clippy::too_many_arguments)]
pub fn pmi_set_view_section(
&mut self,
id: &str,
enabled: bool,
axis: Option<&str>,
offset: Option<f64>,
flip: Option<bool>,
plane: Option<&str>,
exclude: Option<Vec<String>>,
planes: Option<Vec<brep_kernel::feature_pipeline::pmi::PmiSectionPlane>>,
) -> Result<(), String> {
let state = self.pmi_state();
let view = state.find_view(id).ok_or_else(|| format!("no PMI view '{id}'"))?;
let prior = view.display.section.as_ref();
let axis = axis.or_else(|| plane.filter(|plane| !plane.trim().is_empty()).map(|_| "Plane"));
let mut params = serde_json::json!({
"sectionEnabled": enabled,
"sectionAxis": axis.map(str::to_string).or_else(|| prior.map(|s| s.axis.clone())).unwrap_or_else(|| "View".into()),
"sectionOffset": offset.or_else(|| prior.map(|s| s.offset)).unwrap_or(0.),
"sectionFlip": flip.or_else(|| prior.map(|s| s.flip)).unwrap_or(false),
});
if let Some(plane) = plane {
params["sectionPlane"] = plane.into();
}
if let Some(exclude) = exclude {
params["sectionExclude"] = exclude.into();
}
if let Some(planes) = planes {
params["sectionPlanes"] = serde_json::to_value(planes).map_err(|e| e.to_string())?;
}
self.history.break_coalescing();
let result = self.pmi_update_view(id, ¶ms.to_string());
self.history.break_coalescing();
result
}
pub fn pmi_set_view_open(&mut self, id: Option<&str>) {
match id {
Some(id) => {
if self.pmi_state().find_view(id).is_none() {
return;
}
if self.pmi_active_view.as_deref() != Some(id) {
let _ = self.pmi_activate_view(id);
}
self.open_pmi_view(id.to_string());
}
None => self.pmi_open_view = None,
}
self.refresh_pmi_overlay();
}
fn patch_pmi_view(&mut self, id: &str, patch: &PmiViewPatch, coalesce_key: Option<&str>) -> 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(section) = &patch.section { view.display.section = section.clone(); }
if let Some(hidden) = &patch.hidden {
let solids = self.view_hideable_names();
state.set_view_hidden(id, hidden, &solids);
}
let view = state.find_view(id).expect("patched above").clone();
let block = if state.is_empty() { None } else { serde_json::to_value(&state).ok() };
self.history.set_pmi_block(block, coalesce_key);
self.rerun_history();
if self.pmi_active_view.as_deref() == Some(id) {
self.apply_view_display(&view);
}
Ok(())
}
fn view_hideable_names(&self) -> Vec<String> {
self.scene.solids().iter().filter(|solid| !solid.is_sketch).map(|solid| solid.name.clone()).collect()
}
pub(crate) fn pmi_record_visibility(&mut self, view_id: &str, names: &[String], visible: bool) -> bool {
let solids = self.view_hideable_names();
let names: Vec<String> = names.iter().filter(|name| solids.contains(name)).cloned().collect();
if names.is_empty() {
return false;
}
let mut state = self.pmi_state();
let changed = if visible {
state.show_in_view(view_id, &names, &solids)
} else {
state.hide_in_view(view_id, &names).is_some()
};
if !changed {
return false;
}
let Some(view) = state.find_view(view_id).cloned() else { return false };
self.write_pmi_state(state);
if self.pmi_active_view.as_deref() == Some(view_id) {
self.apply_view_display(&view);
self.refresh_pmi_overlay();
}
true
}
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.pmi_open_view = None;
self.refresh_pmi_overlay();
}
fn apply_view_display(&mut self, view: &PmiView) {
let display = &view.display;
let names: Vec<String> = self.scene.solids().iter().map(|s| s.name.clone()).collect();
for name in names {
let hidden = view.hides(&name);
if let Some(original) = self.pmi_explode_originals.get_mut(&name).filter(|original| original.visible == hidden) {
original.visible = !hidden;
self.pmi_unposed_visibility += 1;
}
if hidden {
self.pmi_section_hidden.retain(|hidden| *hidden != name);
self.scene.set_visible(&name, false);
} else if !self.pmi_section_hidden.contains(&name) {
self.scene.set_visible(&name, true);
}
}
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();
}
if self.pmi_active_view.as_deref() != Some(id) {
self.explode_playback = Default::default();
}
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);
self.pmi_apply_explode();
self.instructions_refresh_ghosts();
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.instructions_refresh_ghosts();
if self.pmi_state().find_annotation(&self.transform_armed_feature()).is_some() { self.disarm_transform(); }
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();
}
pub(super) fn controls_sync_after_camera_apply(&mut self) {
self.dirty = true;
}
pub(crate) fn pmi_apply_explode(&mut self) {
let Some(active) = self.pmi_active_view.clone() else {
self.pmi_restore_explode();
return;
};
let report = self.pmi_report.as_ref().and_then(|report| report.view(&active));
let state = self.pmi_state_shared();
let view = state.find_view(&active);
let planes = view.map(|v| v.section_planes_in(report)).unwrap_or_default();
let section = (!planes.is_empty()).then_some(());
let exclude = view.and_then(|v| v.display.section.clone());
let poses: Vec<(Vec<String>, [f64; 3], [f64; 3], [f64; 3], [f64; 3])> = report
.map(|view| {
view.annotations
.iter()
.filter(|row| row.enabled && row.status == PmiStatus::Ok)
.filter(|row| matches!(row.geometry, PmiGeometry::Explode { .. }))
.enumerate()
.filter_map(|(step, row)| match super::exploded_ops::explode_at(&row.geometry, self.explode_fraction(step)) {
PmiGeometry::Explode { solids, translate, rotate_deg, scale, center, .. } => {
Some((solids, translate, rotate_deg, scale, center))
}
_ => None,
})
.collect()
})
.unwrap_or_default();
let mut poses = poses;
poses.extend(self.instructions_insertion_poses());
if section.is_none() && self.pmi_section_hidden.is_empty() && !self.pmi_explode_originals.is_empty() {
let wanted: std::collections::HashSet<&String> = poses.iter().flat_map(|pose| pose.0.iter()).collect();
let stale: Vec<String> = self.pmi_explode_originals.keys().filter(|name| !wanted.contains(name)).cloned().collect();
for name in stale {
if let (Some(original), Some(display)) = (self.pmi_explode_originals.remove(&name), self.scene.solid_mut(&name)) {
*display = original;
}
}
for name in wanted {
match self.pmi_explode_originals.get(name) {
Some(original) => {
if let Some(display) = self.scene.solid_mut(name) {
reset_posed_geometry(display, original);
}
}
None => {
if let Some(display) = self.scene.solid(name) {
self.pmi_explode_originals.insert(name.clone(), display.clone());
}
}
}
}
} else {
self.pmi_restore_explode();
for (solids, ..) 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());
}
}
}
for (solids, translate, rotate_deg, scale, center) in poses {
for name in solids {
if let Some(display) = self.scene.solid_mut(&name) {
transform_display(display, center, translate, rotate_deg, scale);
}
}
}
if section.is_some() {
use super::pmi_section::{side_of_all, Side};
let names: Vec<(String, Side)> = self
.scene
.solids()
.iter()
.map(|s| (s.name.clone(), if exclude.as_ref().is_some_and(|cut| cut.excludes(&s.name)) { Side::Kept } else { side_of_all(s, &planes) }))
.collect();
for (name, side) in names {
match side {
Side::Kept => {}
Side::Removed => {
if let Some(display) = self.scene.solid(&name).filter(|display| display.visible) {
self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone());
self.scene.set_visible(&name, false);
self.pmi_section_hidden.push(name);
}
}
Side::Cut => {
if let Some(display) = self.scene.solid(&name) { self.pmi_explode_originals.entry(name.clone()).or_insert_with(|| display.clone()); }
if let Some(display) = self.scene.solid_mut(&name) { super::pmi_section::clip_display_all(display, &planes); }
}
}
}
}
if !self.pmi_explode_originals.is_empty() || !self.pmi_section_hidden.is_empty() {
self.dirty = true;
}
}
pub(crate) fn pmi_restore_explode(&mut self) {
for name in std::mem::take(&mut self.pmi_section_hidden) {
self.scene.set_visible(&name, true);
}
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_balloon_stale = None;
self.pmi_restore_explode();
if let Some(active) = self.pmi_active_view.clone() {
let view = self.pmi_state().find_view(&active).cloned();
match view {
None => {
self.pmi_active_view = None;
self.pmi_open_annotation = None;
}
Some(view) => {
self.apply_view_display(&view);
self.pmi_apply_explode();
}
}
}
self.instructions_refresh_ghosts();
if let Some(open) = &self.pmi_open_annotation {
if self.pmi_state().find_annotation(open).is_none() {
self.pmi_open_annotation = None;
}
}
if let Some(open) = &self.pmi_open_view {
if self.pmi_state().find_view(open).is_none() {
self.pmi_open_view = 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 {
plugin_replay: None,
persistent_data: serde_json::Value::Null,
kind: type_id.to_string(),
enabled: true,
params,
label_world: None,
});
self.open_pmi_annotation(id.clone());
self.write_pmi_state(state);
Ok(id)
}
pub fn pmi_update_annotation(&mut self, id: &str, params_json: &str) -> Result<(), String> {
if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
let params = serde_json::from_str(params_json).map_err(|e| format!("annotation params: {e}"))?;
return self.plugin_update_annotation(id, params).map(|_| ());
}
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}'"))?;
if annotation.kind.contains('/') {
return Err("plugin annotation edits require the worker transaction path".into());
}
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> {
if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
return self.plugin_delete_annotation(id).map(|_| ());
}
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> {
if self.pmi_state().find_annotation(id).is_some_and(|(_, a)| a.kind.contains('/')) {
return self.plugin_set_annotation_enabled(id, enabled).map(|_| ());
}
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.open_pmi_annotation(id.to_string());
}
}
None => {
if self.pmi_open_annotation.as_deref() == Some(self.transform_armed_feature().as_str()) { self.disarm_transform(); }
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.pmi_reproject_balloon_head(id);
self.refresh_pmi_overlay();
Ok(())
}
pub(crate) fn pmi_reproject_balloon_head(&mut self, id: &str) {
if let Some(missing) = self.pmi_balloon_head_now(id) {
self.pmi_balloon_stale = Some(id.to_string());
self.request_exact_solids(missing.iter().map(String::as_str));
self.pmi_refresh_stale_balloon();
}
}
pub(crate) fn pmi_refresh_stale_balloon(&mut self) {
let Some(id) = self.pmi_balloon_stale.take() else {
return;
};
if self.pmi_balloon_head_now(&id).is_some() {
self.pmi_balloon_stale = Some(id);
} else {
self.refresh_pmi_overlay();
}
}
pub(super) fn pmi_balloon_head_now(&mut self, id: &str) -> Option<Vec<String>> {
let (mut anchor, bubble) = self
.pmi_report
.as_ref()
.and_then(|report| report.annotation(id))
.and_then(|row| match &row.geometry {
PmiGeometry::Leader { balloon: true, anchor: Some(anchor), .. } => Some((anchor.clone(), row.label_world)),
_ => None,
})?;
anchor.camera = Some(self.snapshot_camera());
let mut missing = Vec::new();
let head = brep_kernel::pmi_balloon_head(&anchor, bubble, |name| self.scene.exact_solid(name), &mut missing);
if !missing.is_empty() {
return Some(missing);
}
if let Ok(Some(head)) = head {
if let Some(row) = self.pmi_report.as_mut().and_then(|report| report.annotation_mut(id)) {
if let PmiGeometry::Leader { targets, anchor: recipe, .. } = &mut row.geometry {
*recipe = Some(anchor);
if let Some(first) = targets.first_mut() {
*first = head;
}
}
}
}
None
}
pub fn pmi_label_drag_to(&mut self, id: &str, x: f64, y: f64) {
let playback = self.explode_playback_poses();
let Some((current, plane)) = self
.pmi_report
.as_ref()
.and_then(|r| r.annotation(id))
.map(|r| (playback.as_ref().map_or(r.label_world, |poses| poses.shown(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 world = match (&playback, self.pmi_report.as_ref().and_then(|r| r.annotation(id))) {
(Some(poses), Some(row)) => poses.full(row, world),
_ => world,
};
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_select_annotation(id);
}
pub fn pmi_label_double_clicked(&mut self, id: &str) {
self.pmi_set_annotation_open(Some(id));
}
pub fn pmi_select_annotation(&mut self, id: &str) -> bool {
let state = self.pmi_state();
let Some((view, _)) = state.find_annotation(id) else {
return false;
};
if self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
let _ = self.pmi_activate_view(&view.id);
}
self.pmi_selected_view = None;
self.pmi_selected_annotation = Some(id.to_string());
self.refresh_pmi_overlay();
true
}
pub fn pmi_select_view(&mut self, id: &str) -> bool {
if self.pmi_state().find_view(id).is_none() {
return false;
}
if self.pmi_selected_annotation.take().is_some() {
self.refresh_pmi_overlay();
}
self.pmi_selected_view = Some(id.to_string());
self.dirty = true;
true
}
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,
picks: 0,
claimed: false,
finish_requested: false,
});
self.sync_ref_select_emphasis();
}
fn explode_target_names(&self, id: &str, annotation: &PmiAnnotation) -> Vec<String> {
if let Some(field) = self.ref_select_field() {
if field.target == RefSelectTarget::Pmi && field.subject == id && field.path == ["targets"] {
return field.names;
}
}
annotation.references("targets")
}
fn scene_names_center(&self, names: &[String]) -> Option<[f64; 3]> {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
let mut any = false;
for name in names {
let prefix = format!("{name}:");
for solid in self.scene.solids() {
if solid.name == *name || solid.name.starts_with(&prefix) {
for axis in 0..3 {
low[axis] = low[axis].min(solid.bbox.min[axis]);
high[axis] = high[axis].max(solid.bbox.max[axis]);
}
any = true;
}
}
}
any.then(|| std::array::from_fn(|axis| (low[axis] + high[axis]) * 0.5))
}
pub(super) fn pmi_explode_live_center(&self, id: &str) -> Option<[f64; 3]> {
let state = self.pmi_state_shared();
let (view, annotation) = state.find_annotation(id)?;
if annotation.kind != "explode" || self.pmi_active_view.as_deref() != Some(view.id.as_str()) {
return None;
}
let names = self.explode_target_names(id, annotation);
self.scene_names_center(&names)
}
pub(crate) fn pmi_commit_refs(&mut self, id: &str, path: &[String], names: &[String], multiple: bool) {
if self.pmi_state().find_view(id).is_some() {
match path.first().map(String::as_str) {
Some("sectionExclude") => self.pmi_commit_section_exclude(id, names),
_ => self.pmi_commit_section_plane(id, names.first().map(String::as_str).unwrap_or("")),
}
self.history.break_coalescing();
return;
}
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}"));
}
self.history.break_coalescing();
}
fn pmi_commit_section_plane(&mut self, id: &str, name: &str) {
let mut state = self.pmi_state();
let Some(view) = state.find_view_mut(id) else { return };
let section = view.display.section.get_or_insert_with(|| brep_kernel::feature_pipeline::pmi::PmiSection {
axis: "Plane".into(),
offset: 0.,
flip: false,
origin: None,
normal: None,
plane: None,
exclude: Vec::new(),
planes: Vec::new(),
});
section.axis = "Plane".into();
section.plane = Some(name.trim().to_string()).filter(|name| !name.is_empty());
section.freeze(None);
let block = serde_json::to_value(&state).ok();
self.history.break_coalescing();
self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
}
fn pmi_commit_section_exclude(&mut self, id: &str, names: &[String]) {
let mut state = self.pmi_state();
let Some(section) = state.find_view_mut(id).and_then(|view| view.display.section.as_mut()) else { return };
section.exclude = names.iter().map(|name| name.trim().to_string()).filter(|name| !name.is_empty()).collect();
let block = serde_json::to_value(&state).ok();
self.history.break_coalescing();
self.history.set_pmi_block(block, Some(&format!("pmi:view:{id}")));
}
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 pose_matrix(center: [f64; 3], translate: [f64; 3], rotate: &dyn Fn([f64; 3]) -> [f64; 3], scale: [f64; 3]) -> [f32; 16] {
let columns = [rotate([1.0, 0.0, 0.0]), rotate([0.0, 1.0, 0.0]), rotate([0.0, 0.0, 1.0])];
let linear: [[f64; 3]; 3] = std::array::from_fn(|j| columns[j].map(|v| v * scale[j]));
let shifted: [f64; 3] = std::array::from_fn(|i| center[i] + translate[i] - (0..3).map(|j| linear[j][i] * center[j]).sum::<f64>());
let mut m = [0.0f32; 16];
for j in 0..3 {
for i in 0..3 {
m[j * 4 + i] = linear[j][i] as f32;
}
}
m[12] = shifted[0] as f32;
m[13] = shifted[1] as f32;
m[14] = shifted[2] as f32;
m[15] = 1.0;
m
}
use crate::view::mat4_mul_column_major as mat4_mul;
fn reset_posed_geometry(display: &mut crate::scene::SolidDisplay, original: &crate::scene::SolidDisplay) {
display.mesh.positions.clone_from(&original.mesh.positions);
display.mesh.normals.clone_from(&original.mesh.normals);
if display.edges.len() == original.edges.len() {
for (edge, from) in display.edges.iter_mut().zip(&original.edges) {
edge.polyline.clone_from(&from.polyline);
}
} else {
display.edges.clone_from(&original.edges);
}
display.vertices.clone_from(&original.vertices);
display.bbox = original.bbox;
display.revision = original.revision;
display.shape_revision = original.shape_revision;
display.pose = original.pose;
}
pub(super) fn transform_display(
display: &mut crate::scene::SolidDisplay,
center: [f64; 3],
translate: [f64; 3],
rotate_deg: [f64; 3],
scale: [f64; 3],
) {
let rotate_rad = rotate_deg.map(f64::to_radians);
let rotate = |v: [f64; 3]| super::rotate_euler_xyz_f64(v, rotate_rad);
let step = pose_matrix(center, translate, &rotate, scale);
display.pose = if display.shape_revision == 0 {
Some(step)
} else {
display.pose.map(|before| mat4_mul(&step, &before))
};
if display.shape_revision == 0 {
display.shape_revision = display.revision;
}
display.revision = crate::scene::next_revision();
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(std::array::from_fn(|i| normal[i] as f64 / scale[i]));
let length = rotated.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-20);
*normal = rotated.map(|v| (v / length) 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;
}
}