use brep_render::engine_state::EngineState;
pub mod ecad;
pub type EngineFactory = Box<dyn Fn() -> EngineState>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Access {
Editable,
ReadOnly { reason: String },
}
impl Access {
pub fn read_only(reason: impl Into<String>) -> Self {
Access::ReadOnly { reason: reason.into() }
}
pub fn is_editable(&self) -> bool {
matches!(self, Access::Editable)
}
}
pub const EMPTY_DOCUMENT: &str = r#"{"expressions":"","configurator":{},"features":[]}"#;
pub struct Document {
pub engine: EngineState,
name: Option<String>,
plm_save_access: Option<Access>,
saved_signature: Option<String>,
dirty_marker: bool,
marker_key: Option<(u64, u64)>,
dirty_checks: u64,
id: u64,
pub ecad: crate::workbench::ecad::Editors,
pub ecad_blocks: [ecad::BlockSync; 4],
pub ecad_parts_revision: Option<u64>,
pub ecad_parts_reported: Vec<String>,
pub ecad_occurrences: Option<crate::panels::ecad_parts::OccurrenceMark>,
pub ecad_outdated: Vec<String>,
pub ecad_update_requested: bool,
refused_seen: u64,
}
fn signature(engine: &EngineState) -> String {
signature_without(engine, None)
}
fn signature_without(engine: &EngineState, without: Option<&str>) -> String {
let mut document: serde_json::Value =
serde_json::from_str(&engine.history.request_json()).unwrap_or_else(|_| serde_json::json!({}));
if let Some(object) = document.as_object_mut() {
if let Some(key) = without {
object.remove(key);
}
if !engine.metadata.is_empty() {
object.insert("metadata".into(), engine.metadata.to_json());
}
}
document.to_string()
}
fn ecad_blocks() -> [ecad::BlockSync; 4] {
use ecad::Block;
[Block::Diagram, Block::Pcb, Block::Symbol, Block::Pads].map(ecad::BlockSync::new)
}
impl Document {
pub fn new(engine: EngineState) -> Self {
let saved_signature = Some(signature(&engine));
Self {
engine,
name: None,
plm_save_access: None,
saved_signature,
dirty_marker: false,
marker_key: None,
dirty_checks: 0,
id: next_document_id(),
ecad: crate::workbench::ecad::Editors::new(),
ecad_blocks: ecad_blocks(),
ecad_parts_revision: None,
ecad_parts_reported: Vec::new(),
ecad_occurrences: None,
ecad_outdated: Vec::new(),
ecad_update_requested: false,
refused_seen: 0,
}
}
pub fn recovered(engine: EngineState, name: Option<String>) -> Self {
Self {
engine,
name,
plm_save_access: None,
saved_signature: None,
dirty_marker: true,
marker_key: None,
dirty_checks: 0,
id: next_document_id(),
ecad: crate::workbench::ecad::Editors::new(),
ecad_blocks: ecad_blocks(),
ecad_parts_revision: None,
ecad_parts_reported: Vec::new(),
ecad_occurrences: None,
ecad_outdated: Vec::new(),
ecad_update_requested: false,
refused_seen: 0,
}
}
pub fn access(&self) -> Access {
match self.engine.history.locked() {
None => Access::Editable,
Some(reason) => Access::read_only(reason),
}
}
pub fn set_access(&mut self, access: Access) {
match access {
Access::Editable => self.engine.history.unlock(),
Access::ReadOnly { reason } => self.engine.history.lock(reason),
}
}
pub fn save_access(&self) -> Access {
self.plm_save_access.clone().unwrap_or_else(|| self.access())
}
pub(crate) fn has_plm_save_access(&self) -> bool { self.plm_save_access.is_some() }
pub fn set_plm_save_access(&mut self, access: Access) {
self.plm_save_access = Some(access);
self.engine.history.unlock();
}
pub fn take_refused(&mut self) -> Option<String> {
let refused = self.engine.history.refused_user_edits();
if refused == self.refused_seen {
return None;
}
self.refused_seen = refused;
let reason = self.engine.history.locked()?;
Some(format!("This document is read-only: {reason}."))
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn set_name(&mut self, name: Option<String>) {
if self.name != name { self.plm_save_access = None; }
self.name = name;
}
pub fn title(&self) -> String {
match &self.name {
Some(name) => crate::panels::plm_host::revision_of(name)
.map(|(part, revision)| format!("{part}/{revision}"))
.unwrap_or_else(|| crate::store::model_display_name(name)),
None => "untitled".to_string(),
}
}
pub fn mark_clean(&mut self) {
self.saved_signature = Some(signature(&self.engine));
self.dirty_marker = false;
self.marker_key = Some(self.edit_key());
}
pub(crate) fn save_signature(&self) -> String { signature(&self.engine) }
pub(crate) fn save_signature_without(&self, block: &str) -> String {
signature_without(&self.engine, Some(block))
}
pub(crate) fn mark_saved_signature(&mut self, saved: String) {
self.saved_signature = Some(saved);
self.marker_key = None;
self.refresh_dirty_marker();
}
pub fn is_dirty(&self) -> bool {
match &self.saved_signature {
Some(saved) => *saved != signature(&self.engine),
None => true,
}
}
pub fn edit_key(&self) -> (u64, u64) {
(self.engine.applied_generation(), self.engine.history_revision())
}
pub fn refresh_dirty_marker(&mut self) {
let key = self.edit_key();
let previous = self.marker_key.replace(key);
if previous == Some(key) {
return;
}
let run_moved = previous.map_or(true, |(generation, _)| generation != key.0);
if self.dirty_marker && !run_moved {
return;
}
self.dirty_checks += 1;
self.dirty_marker = self.is_dirty();
}
pub fn absorb_open_write(&mut self, before: &serde_json::Value, after: &serde_json::Value, key: (u64, u64)) {
let rebased = self.saved_signature.as_deref().and_then(|saved| {
let mut base: serde_json::Value = serde_json::from_str(saved).ok()?;
let (base_map, before, after) = (base.as_object_mut()?, before.as_object()?, after.as_object()?);
for name in before.keys().chain(after.keys()) {
let (was, is) = (before.get(name), after.get(name));
if was == is || base_map.get(name) != was {
continue;
}
match is {
Some(value) => base_map.insert(name.clone(), value.clone()),
None => base_map.remove(name),
};
}
Some(base.to_string())
});
if let Some(rebased) = rebased {
self.saved_signature = Some(rebased);
}
if self.marker_key == Some(key) {
self.marker_key = Some(self.edit_key());
}
}
pub fn dirty_checks(&self) -> u64 {
self.dirty_checks
}
pub fn dirty_marker(&self) -> bool {
self.dirty_marker
}
pub fn id(&self) -> u64 {
self.id
}
}
pub struct Documents {
open: Vec<Document>,
active: usize,
new_engine: EngineFactory,
}
impl Documents {
pub fn new(new_engine: EngineFactory) -> Self {
let first = Document::new(new_engine());
Self {
open: vec![first],
active: 0,
new_engine,
}
}
pub fn wrap_engine_factory(&mut self, wrap: impl Fn(EngineState) -> EngineState + 'static) {
let old = std::mem::replace(&mut self.new_engine, Box::new(EngineState::new));
self.new_engine = Box::new(move || wrap(old()));
}
pub fn spawn_engine(&self) -> EngineState {
let mut engine = (self.new_engine)();
carry_settings(&self.engine().settings_json(), &mut engine, true);
let (width, height) = self.viewport_size();
engine.resize(width, height);
engine
}
fn viewport_size(&self) -> (f64, f64) {
let camera = &self.engine().camera;
(camera.width, camera.height)
}
pub fn engine(&self) -> &EngineState {
&self.open[self.active].engine
}
pub fn engine_mut(&mut self) -> &mut EngineState {
&mut self.open[self.active].engine
}
pub fn active(&self) -> &Document {
&self.open[self.active]
}
pub fn active_mut(&mut self) -> &mut Document {
&mut self.open[self.active]
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn active_id(&self) -> u64 {
self.open[self.active].id
}
pub fn len(&self) -> usize {
self.open.len()
}
pub fn get(&self, index: usize) -> Option<&Document> {
self.open.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Document> {
self.open.get_mut(index)
}
pub fn iter(&self) -> std::slice::Iter<'_, Document> {
self.open.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Document> {
self.open.iter_mut()
}
pub fn pump_all(&mut self) -> bool {
use brep_render::brep_kernel::parts_library_revision;
let mut work_in_flight = false;
for index in 0..self.open.len() {
let background = index != self.active;
let doc = &self.open[index];
let own_library = background
&& (doc.engine.run_pending()
|| doc.engine.mesh_imports_pending()
|| doc.engine.history.ports_follow_pending());
if own_library {
install_library_of(doc);
}
let revision = parts_library_revision();
let doc = &mut self.open[index];
doc.engine.pump();
work_in_flight |= doc.engine.run_pending()
|| doc.engine.queries_pending()
|| doc.engine.mesh_imports_pending()
|| doc.engine.step_probes_pending()
|| doc.engine.topology_pending()
|| doc.engine.sheet_lines_pending();
if own_library || (background && parts_library_revision() != revision) {
self.install_active_library();
}
}
work_in_flight
}
pub fn index_of(&self, name: &str) -> Option<usize> {
self.open
.iter()
.position(|doc| doc.name.as_deref() == Some(name))
}
pub fn focus_named(&mut self, name: &str) -> bool {
match self.index_of(name) {
Some(index) => {
self.activate(index);
true
}
None => false,
}
}
pub fn open_document(&mut self, doc: Document) -> usize {
self.open.push(doc);
let index = self.open.len() - 1;
self.activate(index);
index
}
pub fn activate(&mut self, index: usize) {
if index >= self.open.len() || index == self.active {
return;
}
let settings = self.open[self.active].engine.settings_json();
self.active = index;
carry_settings(&settings, &mut self.open[index].engine, false);
self.install_active_library();
}
fn install_active_library(&self) {
install_library_of(&self.open[self.active]);
}
pub fn close(&mut self, index: usize) {
if index >= self.open.len() {
return;
}
let settings = self.open[self.active].engine.settings_json();
let viewport = self.viewport_size();
let closed_active = index == self.active;
self.open.remove(index);
let refilled = self.open.is_empty();
if refilled {
self.open.push(Document::new((self.new_engine)()));
self.active = 0;
self.open[0].engine.resize(viewport.0, viewport.1);
} else if closed_active {
self.active = index.min(self.open.len() - 1);
} else if index < self.active {
self.active -= 1;
}
if closed_active {
let active = self.active;
carry_settings(&settings, &mut self.open[active].engine, refilled);
self.install_active_library();
}
}
pub fn refresh_dirty_markers(&mut self) {
for doc in &mut self.open {
doc.refresh_dirty_marker();
}
}
}
fn install_library_of(doc: &Document) {
let library = serde_json::from_value(doc.engine.history.parts_library().clone()).unwrap_or_default();
brep_render::brep_kernel::install_parts_library(&library);
}
fn carry_settings(settings_json: &str, target: &mut EngineState, with_workbench: bool) {
let Ok(mut settings) = serde_json::from_str::<serde_json::Value>(settings_json) else {
return;
};
if !with_workbench {
if let Some(object) = settings.as_object_mut() {
object.remove("workbench");
}
}
let _ = target.apply_settings_json(&settings.to_string());
}
fn next_document_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}