forked from 1b-agent/opencadstudio-upstream
fix(sheetset): wasm build, lock files and sheet set field refresh
- `.dst` read/write go through native-only helpers (a browser answers "Not available"); the folder pickers are web no-ops; time stamps use iced::time (web-safe). - Lock files are written only for drawings that are sheets of an open set, released when the last set closes, and a lock a crashed session left (recorded with its process id, same user and machine) is taken back. The `.dwl2` header is well-formed and the date line comes from the OS long date on every platform. The 1 Hz poll runs only while a set is open. - Refreshing sheet set fields never stores `####`: fields no open set resolves keep their value, and only the hosts that changed are redrawn (and their tab marked modified). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
078306d8fc
commit
76f4bb97dc
5 files changed
+219
-71
No files matched your search
@@ -4,8 +4,8 @@
|
||||
use crate::app::{Message, OpenCADStudio};
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand};
|
||||
use crate::ui::window::field_dialog::{nav_props,
|
||||
fields_of, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS,
|
||||
use crate::ui::window::field_dialog::{
|
||||
fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS,
|
||||
NAMED_TYPES,
|
||||
};
|
||||
use iced::Task;
|
||||
@@ -209,7 +209,11 @@ impl OpenCADStudio {
|
||||
state.ss_prop.clear();
|
||||
}
|
||||
FieldDialogMsg::SsNode(id) => {
|
||||
state.ss_prop = state.ss_sets.get(state.ss_set).and_then(|db| nav_props(db, &id).first().map(|p| p.0.clone())).unwrap_or_default();
|
||||
state.ss_prop = state
|
||||
.ss_sets
|
||||
.get(state.ss_set)
|
||||
.and_then(|db| nav_props(db, &id).first().map(|p| p.0.clone()))
|
||||
.unwrap_or_default();
|
||||
state.ss_node = id;
|
||||
}
|
||||
FieldDialogMsg::SsProp(p) => state.ss_prop = p,
|
||||
@@ -228,10 +232,14 @@ impl OpenCADStudio {
|
||||
);
|
||||
}
|
||||
FieldDialogMsg::SsPicked(None) => {}
|
||||
FieldDialogMsg::SsPicked(Some(path)) => match codec::sheet_set::SheetSetDatabase::read(&path.to_string_lossy()) {
|
||||
FieldDialogMsg::SsPicked(Some(path)) => match crate::app::commands::sheet_set::read_set(&path.to_string_lossy()) {
|
||||
Ok(db) => {
|
||||
let key = codec::sheet_set::path_key(&path.to_string_lossy());
|
||||
state.ss_set = match state.ss_sets.iter().position(|d| d.path.as_deref().is_some_and(|p| codec::sheet_set::path_key(p) == key)) {
|
||||
let open = state
|
||||
.ss_sets
|
||||
.iter()
|
||||
.position(|d| d.path.as_deref().is_some_and(|p| codec::sheet_set::path_key(p) == key));
|
||||
state.ss_set = match open {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
state.ss_sets.push(db);
|
||||
|
||||
+136
-58
@@ -13,7 +13,6 @@ use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
use codec::sheet_set::{self as ss, ComponentKind, LayoutReference, SheetSetData, SheetSetDatabase};
|
||||
use iced::Task;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The variables this family answers for.
|
||||
@@ -38,8 +37,8 @@ fn paper_layouts(doc: &codec::CadDocument) -> Vec<(String, String)> {
|
||||
|
||||
/// UTC now as the reference stamps `UpdateTime`: `yyyy/MM/dd HH:mm:ss.fff`.
|
||||
fn utc_stamp() -> String {
|
||||
let ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
let ms = iced::time::SystemTime::now()
|
||||
.duration_since(iced::time::SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0);
|
||||
let (days, rem) = (ms.div_euclid(86_400_000), ms.rem_euclid(86_400_000));
|
||||
@@ -66,55 +65,116 @@ fn folder_of(path: &str) -> String {
|
||||
Path::new(path).parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Template of a component for new sheets: its own `DefDwtLayout`, else
|
||||
/// the nearest parent's.
|
||||
/// The lock files' date: the long date of the user's locale, two spaces and
|
||||
/// the 24-hour time (`5 Ekim 2026 Pazartesi 09:26:39`).
|
||||
#[cfg(windows)]
|
||||
fn lock_stamp() -> String {
|
||||
use windows_sys::Win32::Globalization::{GetDateFormatEx, GetTimeFormatEx, DATE_LONGDATE};
|
||||
let wide = |s: &str| s.encode_utf16().chain(std::iter::once(0)).collect::<Vec<u16>>();
|
||||
let mut date = [0u16; 128];
|
||||
let mut time = [0u16; 64];
|
||||
let format = wide("HH:mm:ss");
|
||||
// SAFETY: the buffers outlive the calls and their lengths are passed.
|
||||
let (d, t) = unsafe {
|
||||
(
|
||||
GetDateFormatEx(std::ptr::null(), DATE_LONGDATE, std::ptr::null(), std::ptr::null(), date.as_mut_ptr(), date.len() as i32, std::ptr::null()),
|
||||
GetTimeFormatEx(std::ptr::null(), 0, std::ptr::null(), format.as_ptr(), time.as_mut_ptr(), time.len() as i32),
|
||||
)
|
||||
};
|
||||
let text = |b: &[u16], n: i32| String::from_utf16_lossy(&b[..(n.max(1) as usize - 1)]);
|
||||
format!("{} {}", text(&date, d), text(&time, t))
|
||||
/// `.dst` access: native only (a browser has no file paths to read or write).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn read_set(path: &str) -> Result<SheetSetDatabase, String> {
|
||||
SheetSetDatabase::read(path)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn lock_stamp() -> String {
|
||||
String::new()
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn read_set(_path: &str) -> Result<SheetSetDatabase, String> {
|
||||
Err(crate::t!("Not available").into_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn write_set(db: &mut SheetSetDatabase, path: &str) -> Result<(), String> {
|
||||
db.write(path)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn write_set(_db: &mut SheetSetDatabase, _path: &str) -> Result<(), String> {
|
||||
Err(crate::t!("Not available").into_owned())
|
||||
}
|
||||
|
||||
/// The user and machine names a lock file carries.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn lock_owner() -> (String, String) {
|
||||
let user = std::env::var("USERNAME").or_else(|_| std::env::var("USER")).unwrap_or_default();
|
||||
let machine = std::env::var("COMPUTERNAME")
|
||||
.or_else(|_| std::env::var("HOSTNAME"))
|
||||
.ok()
|
||||
.or_else(|| std::fs::read_to_string("/etc/hostname").ok().map(|h| h.trim().to_string()))
|
||||
.unwrap_or_default();
|
||||
(user, machine)
|
||||
}
|
||||
|
||||
/// The drawings whose lock files this application wrote, with the writing
|
||||
/// process id, kept in the config folder so a crashed session's locks can
|
||||
/// be told from someone else's and taken back.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn lock_record() -> Option<PathBuf> {
|
||||
Some(crate::config::config_dir()?.join("sheet_set_locks.txt"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn recorded_locks() -> Vec<(u32, PathBuf)> {
|
||||
let text = lock_record().and_then(|p| std::fs::read_to_string(p).ok()).unwrap_or_default();
|
||||
text.lines()
|
||||
.filter_map(|l| {
|
||||
let (pid, path) = l.split_once('\t')?;
|
||||
Some((pid.parse().ok()?, PathBuf::from(path)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn record_lock(path: &Path, held: bool) {
|
||||
let Some(record) = lock_record() else { return };
|
||||
let mut locks = recorded_locks();
|
||||
locks.retain(|(_, p)| p != path);
|
||||
if held {
|
||||
locks.push((std::process::id(), path.to_path_buf()));
|
||||
}
|
||||
let text: String = locks.iter().map(|(pid, p)| format!("{pid}\t{}\n", p.display())).collect();
|
||||
if let Some(dir) = record.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let _ = std::fs::write(record, text);
|
||||
}
|
||||
|
||||
/// Whether the `.dwl` next to `path` is one an earlier session of this
|
||||
/// application left behind (it crashed): recorded by another process, and
|
||||
/// still naming this user and machine.
|
||||
// ponytail: the recorded process is assumed gone (the app is single
|
||||
// instance); check it is alive if several instances ever share a profile.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn stale_own_lock(path: &Path) -> bool {
|
||||
let me = std::process::id();
|
||||
if !recorded_locks().iter().any(|(pid, p)| p == path && *pid != me) {
|
||||
return false;
|
||||
}
|
||||
let text = std::fs::read_to_string(path.with_extension("dwl")).unwrap_or_default();
|
||||
let mut lines = text.lines().map(str::trim);
|
||||
let (user, machine) = lock_owner();
|
||||
lines.next() == Some(user.as_str()) && lines.next() == Some(machine.as_str())
|
||||
}
|
||||
|
||||
/// `<drawing>.dwl` (user, machine, date lines) and `<drawing>.dwl2` (the same
|
||||
/// as XML), as the reference writes them for a drawing it has open.
|
||||
/// as XML), as the reference writes them for a drawing it has open. The date
|
||||
/// is the OS long date, two spaces and the 24-hour time.
|
||||
// ponytail: .dwl is written as UTF-8; the reference's encoding of non-ASCII
|
||||
// day names was not measured.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn write_lock(path: &Path) -> std::io::Result<()> {
|
||||
let user = std::env::var("USERNAME").or_else(|_| std::env::var("USER")).unwrap_or_default();
|
||||
let machine = std::env::var("COMPUTERNAME").unwrap_or_default();
|
||||
let stamp = lock_stamp();
|
||||
let (user, machine) = lock_owner();
|
||||
let stamp = crate::entities::field::long_date_time_now();
|
||||
std::fs::write(path.with_extension("dwl"), format!("{user}\n{machine} \n{stamp}"))?;
|
||||
std::fs::write(
|
||||
path.with_extension("dwl2"),
|
||||
format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\">\n<whprops>\n<username>{user}</username>\n<machinename>{machine} </machinename>\n<fullname></fullname>\n<datetime>{stamp}</datetime>\n</whprops>"
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<whprops>\n<username>{user}</username>\n\
|
||||
<machinename>{machine} </machinename>\n<fullname></fullname>\n<datetime>{stamp}</datetime>\n</whprops>"
|
||||
),
|
||||
)
|
||||
)?;
|
||||
record_lock(path, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn release_lock(path: &Path) {
|
||||
let _ = std::fs::remove_file(path.with_extension("dwl"));
|
||||
let _ = std::fs::remove_file(path.with_extension("dwl2"));
|
||||
record_lock(path, false);
|
||||
}
|
||||
|
||||
/// The insertion point of Place on Sheet (with the scale, picked from the
|
||||
@@ -306,6 +366,8 @@ fn rename_form(db: &SheetSetDatabase, set: usize, id: &str) -> Form {
|
||||
}
|
||||
}
|
||||
|
||||
/// Template of a component for new sheets: its own `DefDwtLayout`, else
|
||||
/// the nearest parent's.
|
||||
fn template_of(db: &SheetSetDatabase, component: &str) -> Option<LayoutReference> {
|
||||
let mut id = component.to_string();
|
||||
for _ in 0..32 {
|
||||
@@ -468,6 +530,7 @@ impl OpenCADStudio {
|
||||
/// Publish the open sets to the field engine and redraw every field.
|
||||
fn sheet_sets_changed(&mut self) {
|
||||
crate::entities::field::set_sheet_sets(self.sheet_set.sets.clone());
|
||||
self.sync_drawing_locks();
|
||||
self.refresh_locations();
|
||||
if self.sheet_set.settings.sheet_status >= 1 {
|
||||
self.refresh_sheet_status();
|
||||
@@ -475,7 +538,9 @@ impl OpenCADStudio {
|
||||
self.sheet_set.status.clear();
|
||||
}
|
||||
for tab in &mut self.tabs {
|
||||
refresh_sheet_fields(&mut tab.scene);
|
||||
if refresh_sheet_fields(&mut tab.scene) {
|
||||
tab.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +555,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
self.sheet_set.current = Some(k);
|
||||
} else {
|
||||
let db = SheetSetDatabase::read(&path.to_string_lossy())?;
|
||||
let db = read_set(&path.to_string_lossy())?;
|
||||
self.sheet_set.sets.push(db);
|
||||
self.sheet_set.current = Some(self.sheet_set.sets.len() - 1);
|
||||
}
|
||||
@@ -501,19 +566,31 @@ impl OpenCADStudio {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock files for the drawings open in tabs, as other instances expect
|
||||
/// them (the sheet status shows such a sheet as open); a drawing that
|
||||
/// already has someone else's lock file is left alone.
|
||||
/// Lock files for the sheets of the open sets that are open in tabs, as
|
||||
/// other instances expect them (the sheet status shows such a sheet as
|
||||
/// open). A drawing with someone else's lock file is left alone; a lock
|
||||
/// a crashed session of this application left is taken back. With no set
|
||||
/// open every lock is released.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn sync_drawing_locks(&mut self) {
|
||||
let open: HashSet<PathBuf> = self.tabs.iter().filter_map(|t| t.current_path.clone()).filter(|p| p.exists()).collect();
|
||||
let sets = &self.sheet_set.sets;
|
||||
let open: std::collections::HashSet<PathBuf> = self
|
||||
.tabs
|
||||
.iter()
|
||||
.filter_map(|t| t.current_path.clone())
|
||||
.filter(|p| {
|
||||
let drawing = p.to_string_lossy();
|
||||
sets.iter().any(|db| db.sheet_for(&drawing, None).is_some())
|
||||
})
|
||||
.filter(|p| p.exists())
|
||||
.collect();
|
||||
let gone: Vec<PathBuf> = self.sheet_set.locks.difference(&open).cloned().collect();
|
||||
for p in gone {
|
||||
release_lock(&p);
|
||||
self.sheet_set.locks.remove(&p);
|
||||
}
|
||||
for p in open {
|
||||
if self.sheet_set.locks.contains(&p) || p.with_extension("dwl").exists() {
|
||||
if self.sheet_set.locks.contains(&p) || (p.with_extension("dwl").exists() && !stale_own_lock(&p)) {
|
||||
continue;
|
||||
}
|
||||
if write_lock(&p).is_ok() {
|
||||
@@ -522,9 +599,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn sync_drawing_locks(&mut self) {}
|
||||
|
||||
/// Reload every open set whose `.dst` was changed by someone else.
|
||||
fn poll_sheet_sets(&mut self) {
|
||||
let mut changed = false;
|
||||
@@ -539,7 +613,7 @@ impl OpenCADStudio {
|
||||
if self.sheet_set.seen.insert(key, modified).is_none_or(|t| t == modified) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(fresh) = SheetSetDatabase::read(&path) {
|
||||
if let Ok(fresh) = read_set(&path) {
|
||||
if fresh.root != db.root {
|
||||
*db = fresh;
|
||||
changed = true;
|
||||
@@ -559,7 +633,7 @@ impl OpenCADStudio {
|
||||
let Some(path) = db.path.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = db.write(&path) {
|
||||
if let Err(e) = write_set(db, &path) {
|
||||
self.command_line.push_error(&format!("{path}: {e}"));
|
||||
}
|
||||
self.sheet_sets_changed();
|
||||
@@ -660,7 +734,7 @@ impl OpenCADStudio {
|
||||
self.sync_drawing_locks();
|
||||
self.poll_sheet_sets();
|
||||
// SSMSHEETSTATUS 2: the status is taken again every SSMPOLLTIME seconds.
|
||||
let every = std::time::Duration::from_secs(u64::from(self.sheet_set.settings.poll_time.max(20)));
|
||||
let every = iced::time::Duration::from_secs(u64::from(self.sheet_set.settings.poll_time.max(20)));
|
||||
if self.sheet_set.settings.sheet_status == 2 && self.sheet_set.status_at.is_none_or(|t| t.elapsed() >= every) {
|
||||
self.refresh_sheet_status();
|
||||
}
|
||||
@@ -789,6 +863,10 @@ impl OpenCADStudio {
|
||||
self.sheet_set.submenu = None;
|
||||
return self.view_menu(id, action);
|
||||
}
|
||||
// Browsers expose no folder picker.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
SheetSetMsg::AddLocation | SheetSetMsg::Browse(_) => {}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SheetSetMsg::AddLocation => {
|
||||
return Task::perform(
|
||||
async {
|
||||
@@ -816,7 +894,7 @@ impl OpenCADStudio {
|
||||
SheetSetMsg::OpenDrawing(path) => return self.update(Message::OpenRecent(PathBuf::from(path))),
|
||||
SheetSetMsg::Refresh => {
|
||||
if let Some(path) = self.sheet_set.db().and_then(|db| db.path.clone()) {
|
||||
match SheetSetDatabase::read(&path) {
|
||||
match read_set(&path) {
|
||||
Ok(db) => {
|
||||
if let Some(slot) = self.sheet_set.db_mut() {
|
||||
*slot = db;
|
||||
@@ -838,6 +916,7 @@ impl OpenCADStudio {
|
||||
SheetSetMsg::FolderPicked(field, Some(path)) => self.dialog_folder(field, path),
|
||||
SheetSetMsg::FolderPicked(_, None) => {}
|
||||
SheetSetMsg::TemplatePicked(Some(path)) => self.properties_template(path),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SheetSetMsg::Browse(field) => {
|
||||
return Task::perform(
|
||||
async {
|
||||
@@ -1138,8 +1217,8 @@ impl OpenCADStudio {
|
||||
pub(in crate::app) fn sheet_set_after_open(&mut self, i: usize) -> Task<Message> {
|
||||
let mut task = Task::none();
|
||||
// A sheet's fields show its current values once it is open.
|
||||
if !self.sheet_set.sets.is_empty() {
|
||||
refresh_sheet_fields(&mut self.tabs[i].scene);
|
||||
if !self.sheet_set.sets.is_empty() && refresh_sheet_fields(&mut self.tabs[i].scene) {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
let path = self.tabs[i].current_path.as_ref().map(|p| p.to_string_lossy().to_string());
|
||||
if let (Some(path), Some((want, layout))) = (path.as_ref(), self.sheet_set_pending_layout.as_ref()) {
|
||||
@@ -1925,7 +2004,7 @@ impl OpenCADStudio {
|
||||
/// SSMSHEETSTATUS: a sheet whose drawing is missing, or open (its `.dwl`
|
||||
/// lock file, or a tab of this application).
|
||||
pub(in crate::app) fn refresh_sheet_status(&mut self) {
|
||||
self.sheet_set.status_at = Some(std::time::Instant::now());
|
||||
self.sheet_set.status_at = Some(iced::time::Instant::now());
|
||||
self.sheet_set.status.clear();
|
||||
if self.sheet_set.settings.sheet_status == 0 {
|
||||
return;
|
||||
@@ -2105,7 +2184,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = db.write(&path) {
|
||||
if let Err(e) = write_set(&mut db, &path) {
|
||||
w.error = Some(e);
|
||||
self.sheet_set.dialog = Some(SsDialog::Wizard(w));
|
||||
return Task::none();
|
||||
@@ -2509,16 +2588,15 @@ fn write_properties(db: &mut SheetSetDatabase, p: &Properties) {
|
||||
}
|
||||
|
||||
/// Store fresh values for a drawing's sheet set fields (the fields keep them,
|
||||
/// as when the reference updates them) and redraw their hosts.
|
||||
fn refresh_sheet_fields(scene: &mut crate::scene::Scene) {
|
||||
crate::entities::field::refresh_sheet_set_fields(&mut scene.document);
|
||||
let changes: Vec<_> = scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| crate::entities::field::hosts_field(&scene.document, e))
|
||||
.map(|e| (e.common().handle, crate::scene::ChangeKind::Modified))
|
||||
/// as when the reference updates them) and redraw the hosts that changed;
|
||||
/// whether any did.
|
||||
fn refresh_sheet_fields(scene: &mut crate::scene::Scene) -> bool {
|
||||
let changes: Vec<_> = crate::entities::field::refresh_sheet_set_fields(&mut scene.document)
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
if !changes.is_empty() {
|
||||
scene.bump_entities(&changes);
|
||||
}
|
||||
!changes.is_empty()
|
||||
}
|
||||
+8
-5
@@ -2643,12 +2643,15 @@ impl OpenCADStudio {
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let plugin_drain = Subscription::none();
|
||||
// Open sheet sets reload when their .dst changes on disk.
|
||||
// While a sheet set is open: it reloads when its .dst changes on disk,
|
||||
// and its sheets' .dwl lock files follow the open tabs.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
// Open sheet sets reload when their .dst changes on disk; the
|
||||
// drawings' .dwl lock files follow the open tabs.
|
||||
let sheet_set_poll = iced::time::every(std::time::Duration::from_secs(1))
|
||||
.map(|_| Message::SheetSet(crate::ui::window::sheet_set::SheetSetMsg::Poll));
|
||||
let sheet_set_poll = if self.sheet_set.sets.is_empty() {
|
||||
Subscription::none()
|
||||
} else {
|
||||
iced::time::every(std::time::Duration::from_secs(1))
|
||||
.map(|_| Message::SheetSet(crate::ui::window::sheet_set::SheetSetMsg::Poll))
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let sheet_set_poll = Subscription::none();
|
||||
let hatch_pattern_keys = if self.tabs[self.active_tab]
|
||||
|
||||
+61
-2
@@ -387,10 +387,69 @@ pub fn attach_attribute_fields(document: &mut CadDocument, insert: Handle) -> Ve
|
||||
document.attach_attribute_fields(insert, &context)
|
||||
}
|
||||
|
||||
/// Store fresh values for the sheet set fields (after a sheet set changed).
|
||||
/// Store fresh values for the sheet set fields (after a sheet set changed)
|
||||
/// and return the hosts whose text changed. A field no open sheet set
|
||||
/// resolves (`####`: its set was closed or not found) keeps its stored value.
|
||||
pub fn refresh_sheet_set_fields(document: &mut CadDocument) -> Vec<Handle> {
|
||||
let context = OcsFieldContext(None);
|
||||
document.refresh_sheet_set_fields(&context)
|
||||
let unresolved: Vec<(Handle, String)> = document
|
||||
.fields
|
||||
.values()
|
||||
.filter(|f| f.evaluator.starts_with("AcSm"))
|
||||
.filter(|f| {
|
||||
let host = field_host(document, f.handle).unwrap_or(Handle::NULL);
|
||||
codec::fields::resolve_handle(document, f.handle, host, &context).is_none_or(|v| v == "####")
|
||||
})
|
||||
.map(|f| (f.handle, f.evaluator.clone()))
|
||||
.collect();
|
||||
// ponytail: the engine refreshes every sheet set field, so the unresolved
|
||||
// ones are hidden from it (evaluator cleared) for this one call; a
|
||||
// "skip unresolved" switch in the engine would replace this.
|
||||
for (h, _) in &unresolved {
|
||||
if let Some(f) = document.fields.get_mut(h) {
|
||||
f.evaluator.clear();
|
||||
}
|
||||
}
|
||||
let changed = document.refresh_sheet_set_fields(&context);
|
||||
for (h, evaluator) in unresolved {
|
||||
if let Some(f) = document.fields.get_mut(&h) {
|
||||
f.evaluator = evaluator;
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// The entity hosting field `field`: up its owner chain (fields, then the
|
||||
/// field dictionary and extension dictionary) to the first non-object.
|
||||
fn field_host(document: &CadDocument, field: Handle) -> Option<Handle> {
|
||||
let owner = |h: Handle| -> Option<Handle> {
|
||||
if let Some(f) = document.fields.get(&h) {
|
||||
return Some(f.owner);
|
||||
}
|
||||
match document.objects.get(&h)? {
|
||||
codec::objects::ObjectType::Dictionary(d) => Some(d.owner),
|
||||
codec::objects::ObjectType::Unknown { owner, .. } => Some(*owner),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let mut h = field;
|
||||
for _ in 0..12 {
|
||||
let o = owner(h)?;
|
||||
if owner(o).is_none() {
|
||||
return Some(o);
|
||||
}
|
||||
h = o;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Now as the OS long date, two spaces and the 24-hour time
|
||||
/// (`Monday, October 5, 2026 09:26:39`).
|
||||
pub fn long_date_time_now() -> String {
|
||||
let locale = os_date_locale();
|
||||
let picture = format!("{} HH:mm:ss", locale.long_date);
|
||||
let now = now_utc_julian() + utc_offset_days();
|
||||
codec::fields::format_dt_in(codec::fields::julian_parts(now), &picture, &locale)
|
||||
}
|
||||
|
||||
/// [`attach_attribute_fields`] with every field code passed through `map`.
|
||||
|
||||
@@ -67,7 +67,7 @@ pub struct SheetSetManager {
|
||||
pub seen: std::collections::HashMap<String, std::time::SystemTime>,
|
||||
/// Sheet status by sheet id (SSMSHEETSTATUS) and when it was taken.
|
||||
pub status: std::collections::HashMap<String, SheetStatus>,
|
||||
pub status_at: Option<std::time::Instant>,
|
||||
pub status_at: Option<iced::time::Instant>,
|
||||
/// Sheet Views: by category (else by sheet).
|
||||
pub by_category: bool,
|
||||
/// Model Views: location id, folder and its drawings.
|
||||
|
||||
Reference in new issue
Block a user