feat(app): surface round-trip HeaderVariables via DWGPROPS + USERI/USERR

The "round-trip metadata" fields (timestamps, GUIDs, file identity,
user-script scalars) were parsed and re-emitted on save but never
exposed anywhere, so users couldn't confirm what the file actually
carried. Add two command-line commands that read / edit them:

- DWGPROPS / DWGPROP dumps the header to the command-line output:
  create / update / total-edit / user-elapsed timestamps,
  fingerprint_guid, version_guid, code_page, menu_name, hyperlink_base,
  project_name, stylesheet, required_versions, measurement (with a
  human label), proxy_graphics, tree_depth, plus the five USERI / USERR
  slots and the user_timer flag.
- USERI <1-5> <int> / USERR <1-5> <real> writes one of the five
  document-scoped scalar slots and marks the tab dirty so the value
  rides the next save. Lets users park drawing-scoped numbers in the
  same place AutoCAD scripts have always used them, even though we
  don't ship a LISP / DIESEL runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-20 05:48:07 +03:00
1 parent b3e117a969
commit 4cca6363ef
1 file changed
+175
+175
View File
@@ -2348,6 +2348,181 @@ impl H7CAD {
self.command_line.push_info("Opening Patreon page...");
}
// ── DWGPROPS — print round-trip-only HeaderVariables ─────────
// No UI dialog for these yet; the command surfaces them so
// users can confirm the values that the parser populated and
// the writer will round-trip on save.
"DWGPROPS" | "DWGPROP" => {
let i = self.active_tab;
let h = &self.tabs[i].scene.document.header;
let path_label = self.tabs[i]
.current_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(unsaved)".to_string());
self.command_line
.push_output(&format!("Drawing: {}", path_label));
self.command_line.push_output(&format!(
" Created (Julian): {:.6}",
h.create_date_julian
));
self.command_line.push_output(&format!(
" Updated (Julian): {:.6}",
h.update_date_julian
));
self.command_line.push_output(&format!(
" Total edit time: {:.4}",
h.total_editing_time
));
self.command_line.push_output(&format!(
" User elapsed: {:.4}",
h.user_elapsed_time
));
self.command_line.push_output(&format!(
" Last saved by: {}",
if h.last_saved_by.is_empty() {
"(unknown)"
} else {
&h.last_saved_by
}
));
self.command_line.push_output(&format!(
" Fingerprint GUID: {}",
if h.fingerprint_guid.is_empty() {
"(none)"
} else {
&h.fingerprint_guid
}
));
self.command_line.push_output(&format!(
" Version GUID: {}",
if h.version_guid.is_empty() {
"(none)"
} else {
&h.version_guid
}
));
self.command_line
.push_output(&format!(" Code page: {}", h.code_page));
self.command_line.push_output(&format!(
" Menu name: {}",
if h.menu_name.is_empty() {
"(none)"
} else {
&h.menu_name
}
));
self.command_line.push_output(&format!(
" Hyperlink base: {}",
if h.hyperlink_base.is_empty() {
"(none)"
} else {
&h.hyperlink_base
}
));
self.command_line.push_output(&format!(
" Project name: {}",
if h.project_name.is_empty() {
"(none)"
} else {
&h.project_name
}
));
self.command_line.push_output(&format!(
" Stylesheet: {}",
if h.stylesheet.is_empty() {
"(none)"
} else {
&h.stylesheet
}
));
self.command_line.push_output(&format!(
" Required versions: {:#018x}",
h.required_versions
));
self.command_line.push_output(&format!(
" Measurement: {} ({})",
h.measurement,
if h.measurement == 1 { "Metric" } else { "Imperial" }
));
self.command_line.push_output(&format!(
" Proxy graphics: {}",
h.proxy_graphics
));
self.command_line
.push_output(&format!(" Tree depth: {}", h.tree_depth));
self.command_line.push_output(&format!(
" User vars (int): {} {} {} {} {}",
h.user_int1, h.user_int2, h.user_int3, h.user_int4, h.user_int5
));
self.command_line.push_output(&format!(
" User vars (real): {:.6} {:.6} {:.6} {:.6} {:.6}",
h.user_real1, h.user_real2, h.user_real3, h.user_real4, h.user_real5
));
self.command_line.push_output(&format!(
" User timer: {}",
if h.user_timer { "On" } else { "Off" }
));
}
// Edit a USERI1..USERI5 / USERR1..USERR5 slot. Lets the user
// store drawing-scoped scalars (and save them through round-trip)
// even though we don't have a LISP / DIESEL runtime yet.
// USERI 1 42 → header.user_int1 = 42
// USERR 3 1.5e-3 → header.user_real3 = 0.0015
cmd if cmd.starts_with("USERI") || cmd.starts_with("USERR") => {
let is_real = cmd.starts_with("USERR");
let rest = if is_real {
cmd.trim_start_matches("USERR").trim()
} else {
cmd.trim_start_matches("USERI").trim()
};
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
let slot: Option<usize> = parts.first().and_then(|s| s.parse().ok());
let value = parts.get(1).copied().unwrap_or("").trim();
let i = self.active_tab;
let h = &mut self.tabs[i].scene.document.header;
match (slot, value, is_real) {
(Some(n @ 1..=5), v, true) => {
if let Ok(val) = v.parse::<f64>() {
match n {
1 => h.user_real1 = val,
2 => h.user_real2 = val,
3 => h.user_real3 = val,
4 => h.user_real4 = val,
_ => h.user_real5 = val,
}
self.tabs[i].dirty = true;
self.command_line
.push_output(&format!("USERR{n} = {val}"));
} else {
self.command_line
.push_info("Usage: USERR <1-5> <real>");
}
}
(Some(n @ 1..=5), v, false) => {
if let Ok(val) = v.parse::<i16>() {
match n {
1 => h.user_int1 = val,
2 => h.user_int2 = val,
3 => h.user_int3 = val,
4 => h.user_int4 = val,
_ => h.user_int5 = val,
}
self.tabs[i].dirty = true;
self.command_line
.push_output(&format!("USERI{n} = {val}"));
} else {
self.command_line
.push_info("Usage: USERI <1-5> <integer>");
}
}
_ => self
.command_line
.push_info("Usage: USERI <1-5> <int> | USERR <1-5> <real>"),
}
}
"REPORT" => {
let _ = open::that("https://github.com/HakanSeven12/H7CAD/issues/new");
self.command_line.push_info("Opening GitHub issue page...");