forked from 1b-agent/opencadstudio-upstream
feat(python): text and dimension styles (doc.text_styles, doc.dim_styles)
Extends TableOperation with text style create/modify, dimension style create/modify (DimStyle fields as a validated JSON object), rename, delete and set-current. Reuses OCS's style in-use scan and reference-following rename (four style_ops methods widened to pub(super)); a renamed text style also updates dimension styles that name it. Real-IPC audit with DWG/DXF persistence and undo, IPC and Python unit tests, docs. Four cadcodec findings are pinned by canaries. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
b7b6e2cef5
commit
aaea15e4ef
11 files changed
+894
-8
No files matched your search
+1
-1
@@ -35,7 +35,7 @@ mod record_api;
|
||||
pub(crate) mod settings;
|
||||
mod shortcuts;
|
||||
mod startup;
|
||||
mod style_ops;
|
||||
pub(crate) mod style_ops;
|
||||
mod text_inline;
|
||||
mod tolerance_dialog;
|
||||
mod update;
|
||||
|
||||
+425
-3
@@ -1245,9 +1245,170 @@ impl<'a> HostSession<'a> {
|
||||
self.app.set_current_layer_name(self.tab, &stored)?;
|
||||
Ok(handle)
|
||||
}
|
||||
other => self.style_operation(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text and dimension style operations. They act on the active document
|
||||
/// because the shared style machinery (in-use scan, rename, ribbon sync) is
|
||||
/// keyed by the active tab.
|
||||
fn style_operation(&mut self, operation: ocs_plugin_api::host::TableOperation) -> Result<Handle, String> {
|
||||
use crate::app::style_ops::StyleKind;
|
||||
use ocs_plugin_api::host::{TableOperation, TableStyleKind};
|
||||
if self.tab != self.app.active_tab {
|
||||
return Err("style operations act on the active document; switch to it first".to_owned());
|
||||
}
|
||||
let kind_of = |kind: TableStyleKind| match kind {
|
||||
TableStyleKind::Text => StyleKind::Text,
|
||||
TableStyleKind::Dim => StyleKind::Dim,
|
||||
};
|
||||
let stored = |doc: &CadDocument, kind: TableStyleKind, name: &str| -> Result<(String, Handle), String> {
|
||||
let found = match kind {
|
||||
TableStyleKind::Text => doc.text_styles.get(name).map(|s| (s.name.clone(), s.handle)),
|
||||
TableStyleKind::Dim => doc.dim_styles.get(name).map(|s| (s.name.clone(), s.handle)),
|
||||
};
|
||||
found.ok_or_else(|| format!("{} style {name:?} does not exist", match kind {
|
||||
TableStyleKind::Text => "text",
|
||||
TableStyleKind::Dim => "dimension",
|
||||
}))
|
||||
};
|
||||
match operation {
|
||||
TableOperation::TextStyleCreate { config } => {
|
||||
let name = validated_symbol_name(&config.name, "a text style name")?;
|
||||
if self.app.style_exists(StyleKind::Text, &name) {
|
||||
return Err(format!("text style {name:?} already exists"));
|
||||
}
|
||||
let mut style = acadrust::tables::TextStyle::new(name.clone());
|
||||
apply_text_style_config(&mut style, &config)?;
|
||||
self.push_undo("Create text style");
|
||||
let handle = self.document_mut().allocate_handle();
|
||||
style.handle = handle;
|
||||
self.document_mut().text_styles.add(style).map_err(|e| format!("the text style could not be created: {e}"))?;
|
||||
self.finish_style_change(StyleKind::Text);
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::TextStyleModify { config } => {
|
||||
let (name, handle) = stored(self.document(), TableStyleKind::Text, config.name.trim())?;
|
||||
let mut style = self.document().text_styles.get(&name).cloned().expect("checked above");
|
||||
let before = style.clone();
|
||||
if config == (ocs_plugin_api::host::TextStyleConfig { name: config.name.clone(), ..Default::default() }) {
|
||||
return Err("no text style properties to change".to_owned());
|
||||
}
|
||||
apply_text_style_config(&mut style, &config)?;
|
||||
if style == before {
|
||||
return Ok(handle);
|
||||
}
|
||||
self.push_undo("Modify text style");
|
||||
if let Some(slot) = self.document_mut().text_styles.get_mut(&name) {
|
||||
*slot = style;
|
||||
}
|
||||
self.finish_style_change(StyleKind::Text);
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::DimStyleCreate { name, copy_from, properties } => {
|
||||
let name = validated_symbol_name(&name, "a dimension style name")?;
|
||||
if self.app.style_exists(StyleKind::Dim, &name) {
|
||||
return Err(format!("dimension style {name:?} already exists"));
|
||||
}
|
||||
let base = match ©_from {
|
||||
Some(source) => {
|
||||
let (source, _) = stored(self.document(), TableStyleKind::Dim, source.trim())?;
|
||||
self.document().dim_styles.get(&source).cloned().expect("checked above")
|
||||
}
|
||||
None => acadrust::tables::DimStyle::new(name.clone()),
|
||||
};
|
||||
let mut style = apply_dim_style_json(self.document(), &base, &properties)?;
|
||||
self.push_undo("Create dimension style");
|
||||
let handle = self.document_mut().allocate_handle();
|
||||
style.handle = handle;
|
||||
style.name = name;
|
||||
self.document_mut().dim_styles.add(style).map_err(|e| format!("the dimension style could not be created: {e}"))?;
|
||||
self.finish_style_change(StyleKind::Dim);
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::DimStyleModify { name, properties } => {
|
||||
let (name, handle) = stored(self.document(), TableStyleKind::Dim, name.trim())?;
|
||||
let base = self.document().dim_styles.get(&name).cloned().expect("checked above");
|
||||
let style = apply_dim_style_json(self.document(), &base, &properties)?;
|
||||
if style == base {
|
||||
return Ok(handle);
|
||||
}
|
||||
self.push_undo("Modify dimension style");
|
||||
if let Some(slot) = self.document_mut().dim_styles.get_mut(&name) {
|
||||
*slot = style;
|
||||
}
|
||||
self.finish_style_change(StyleKind::Dim);
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::StyleRename { kind, from, to } => {
|
||||
let (from, handle) = stored(self.document(), kind, from.trim())?;
|
||||
if from.eq_ignore_ascii_case("Standard") {
|
||||
return Err("the Standard style cannot be renamed".to_owned());
|
||||
}
|
||||
let to = validated_symbol_name(&to, "a style name")?;
|
||||
if from.eq_ignore_ascii_case(&to) {
|
||||
return Err("the new style name matches the current one (a case-only change is not supported)".to_owned());
|
||||
}
|
||||
if self.app.style_exists(kind_of(kind), &to) {
|
||||
return Err(format!("style {to:?} already exists"));
|
||||
}
|
||||
self.push_undo("Rename style");
|
||||
self.app.rename_style_storage(kind_of(kind), &from, &to);
|
||||
if kind == TableStyleKind::Text {
|
||||
// A dimension style names its text style; follow the rename.
|
||||
for dim in self.document_mut().dim_styles.iter_mut() {
|
||||
if dim.dimtxsty.eq_ignore_ascii_case(&from) {
|
||||
dim.dimtxsty = to.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_style_change(kind_of(kind));
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::StyleDelete { kind, name } => {
|
||||
let (name, handle) = stored(self.document(), kind, name.trim())?;
|
||||
if name.eq_ignore_ascii_case("Standard") {
|
||||
return Err("the Standard style cannot be deleted".to_owned());
|
||||
}
|
||||
if self.app.style_in_use(kind_of(kind), &name) {
|
||||
return Err(format!("style {name:?} is current or still in use"));
|
||||
}
|
||||
self.push_undo("Delete style");
|
||||
if !self.app.remove_style_storage(kind_of(kind), &name) {
|
||||
return Err("the style could not be deleted".to_owned());
|
||||
}
|
||||
self.finish_style_change(kind_of(kind));
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::StyleSetCurrent { kind, name } => {
|
||||
let (name, handle) = stored(self.document(), kind, name.trim())?;
|
||||
let header = &mut self.document_mut().header;
|
||||
match kind {
|
||||
TableStyleKind::Text => {
|
||||
header.current_text_style_handle = handle;
|
||||
header.current_text_style_name = name.clone();
|
||||
self.app.ribbon.active_text_style = name;
|
||||
}
|
||||
TableStyleKind::Dim => {
|
||||
header.current_dimstyle_handle = handle;
|
||||
header.current_dimstyle_name = name.clone();
|
||||
self.app.ribbon.active_dim_style = name;
|
||||
}
|
||||
}
|
||||
self.app.tabs[self.tab].dirty = true;
|
||||
Ok(handle)
|
||||
}
|
||||
_ => Err("unsupported table operation".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_style_change(&mut self, kind: crate::app::style_ops::StyleKind) {
|
||||
self.app.tabs[self.tab].dirty = true;
|
||||
self.app.after_style_change(kind);
|
||||
self.app.tabs[self.tab].scene.bump_geometry();
|
||||
self.publish_document_view();
|
||||
}
|
||||
|
||||
fn finish_layer_change(&mut self, names: &[String]) {
|
||||
self.app.tabs[self.tab].dirty = true;
|
||||
self.app.tabs[self.tab].scene.invalidate_layer_dependencies(names);
|
||||
@@ -1320,19 +1481,125 @@ impl<'a> HostSession<'a> {
|
||||
|
||||
/// A layer name AutoCAD would accept: 1-255 characters, none of `<>/\":;?*|=\``.
|
||||
fn validated_layer_name(raw: &str) -> Result<String, String> {
|
||||
validated_symbol_name(raw, "a layer name")
|
||||
}
|
||||
|
||||
/// A symbol-table name AutoCAD would accept (layers, styles): 1-255 characters,
|
||||
/// none of `<>/\":;?*|=` or a backquote, and no control characters.
|
||||
fn validated_symbol_name(raw: &str, what: &str) -> Result<String, String> {
|
||||
let name = raw.trim();
|
||||
if name.is_empty() {
|
||||
return Err("a layer name cannot be empty".to_owned());
|
||||
return Err(format!("{what} cannot be empty"));
|
||||
}
|
||||
if name.chars().count() > 255 {
|
||||
return Err("a layer name is limited to 255 characters".to_owned());
|
||||
return Err(format!("{what} is limited to 255 characters"));
|
||||
}
|
||||
if let Some(bad) = name.chars().find(|c| "<>/\\\":;?*|=`".contains(*c) || c.is_control()) {
|
||||
return Err(format!("a layer name cannot contain {bad:?}"));
|
||||
return Err(format!("{what} cannot contain {bad:?}"));
|
||||
}
|
||||
Ok(name.to_owned())
|
||||
}
|
||||
|
||||
fn apply_text_style_config(
|
||||
style: &mut acadrust::tables::TextStyle,
|
||||
config: &ocs_plugin_api::host::TextStyleConfig,
|
||||
) -> Result<(), String> {
|
||||
let text = |what: &str, value: &str| {
|
||||
if value.chars().any(|c| c.is_control()) || value.chars().count() > 255 {
|
||||
Err(format!("{what} must be text of at most 255 characters"))
|
||||
} else {
|
||||
Ok(value.trim().to_owned())
|
||||
}
|
||||
};
|
||||
if let Some(height) = config.height {
|
||||
if !height.is_finite() || height < 0.0 {
|
||||
return Err("text style height must be finite and not negative (0 = variable)".to_owned());
|
||||
}
|
||||
style.height = height;
|
||||
}
|
||||
if let Some(width) = config.width_factor {
|
||||
if !width.is_finite() || width <= 0.0 || width > 100.0 {
|
||||
return Err("text style width factor must be greater than 0 and at most 100".to_owned());
|
||||
}
|
||||
style.width_factor = width;
|
||||
}
|
||||
if let Some(angle) = config.oblique_angle {
|
||||
if !angle.is_finite() || angle.abs() > 85f64.to_radians() + 1e-9 {
|
||||
return Err("text style oblique angle must be within 85 degrees either way".to_owned());
|
||||
}
|
||||
style.oblique_angle = angle;
|
||||
}
|
||||
if let Some(font) = &config.font_file {
|
||||
style.font_file = text("font file", font)?;
|
||||
}
|
||||
if let Some(font) = &config.big_font_file {
|
||||
style.big_font_file = text("big font file", font)?;
|
||||
}
|
||||
if let Some(font) = &config.true_type_font {
|
||||
style.true_type_font = text("TrueType font", font)?;
|
||||
}
|
||||
if let Some(value) = config.backward {
|
||||
style.flags.backward = value;
|
||||
}
|
||||
if let Some(value) = config.upside_down {
|
||||
style.flags.upside_down = value;
|
||||
}
|
||||
if let Some(value) = config.vertical {
|
||||
style.is_vertical = value;
|
||||
}
|
||||
if let Some(value) = config.annotative {
|
||||
style.annotative = value;
|
||||
}
|
||||
if style.font_file.is_empty() && style.true_type_font.is_empty() {
|
||||
return Err("a text style needs a font file or a TrueType font".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a JSON object of DimStyle fields on top of `base`. Handles and xref
|
||||
/// fields belong to the host and the name is set separately, so those keys are
|
||||
/// refused; unknown keys and wrongly typed values are refused too. `dimtxsty`
|
||||
/// must name an existing text style, whose handle is linked here.
|
||||
fn apply_dim_style_json(
|
||||
doc: &CadDocument,
|
||||
base: &acadrust::tables::DimStyle,
|
||||
properties: &str,
|
||||
) -> Result<acadrust::tables::DimStyle, String> {
|
||||
let patch: serde_json::Value = serde_json::from_str(properties)
|
||||
.map_err(|e| format!("dimension style properties are not valid JSON: {e}"))?;
|
||||
let serde_json::Value::Object(patch) = patch else {
|
||||
return Err("dimension style properties must be an object".to_owned());
|
||||
};
|
||||
let mut value = serde_json::to_value(base).map_err(|e| e.to_string())?;
|
||||
let object = value.as_object_mut().ok_or("dimension style did not serialize to an object")?;
|
||||
for (key, new) in patch {
|
||||
if key == "handle" || key == "name" || key.starts_with("xref_") || key.ends_with("_handle") {
|
||||
return Err(format!("dimension style property {key:?} is managed by the host"));
|
||||
}
|
||||
if !object.contains_key(&key) {
|
||||
return Err(format!("unknown dimension style property {key:?}"));
|
||||
}
|
||||
object.insert(key, new);
|
||||
}
|
||||
let mut style: acadrust::tables::DimStyle = serde_json::from_value(value)
|
||||
.map_err(|e| format!("invalid dimension style value: {e}"))?;
|
||||
if !(style.dimscale.is_finite() && style.dimscale > 0.0) {
|
||||
return Err("dimscale must be greater than zero".to_owned());
|
||||
}
|
||||
if !(style.dimtxt.is_finite() && style.dimtxt > 0.0) {
|
||||
return Err("dimtxt (text height) must be greater than zero".to_owned());
|
||||
}
|
||||
if !(style.dimasz.is_finite() && style.dimasz >= 0.0) {
|
||||
return Err("dimasz (arrow size) cannot be negative".to_owned());
|
||||
}
|
||||
let Some(text_style) = doc.text_styles.get(&style.dimtxsty) else {
|
||||
return Err(format!("dimtxsty {:?} is not a text style in this drawing", style.dimtxsty));
|
||||
};
|
||||
style.dimtxsty = text_style.name.clone();
|
||||
style.dimtxsty_handle = text_style.handle;
|
||||
Ok(style)
|
||||
}
|
||||
|
||||
/// The stored spelling of `name` in the drawing's linetype table, loading the
|
||||
/// standard linetypes first when it is not there yet. `None` when the drawing
|
||||
/// cannot supply it: a layer must not reference a linetype that does not exist.
|
||||
@@ -6392,6 +6659,161 @@ L.delete('Structure', erase_objects=True)
|
||||
assert!(document.entities().any(|e| matches!(e, EntityType::Line(_))), "undo restored the line");
|
||||
}
|
||||
|
||||
/// Text and dimension styles through Python over the real runner: create,
|
||||
/// modify, copy, rename (references follow), delete and make current, with
|
||||
/// refusals that change nothing, DWG/DXF persistence and one-step undo.
|
||||
#[test]
|
||||
fn audit_python_text_and_dim_styles_over_real_ipc() {
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
||||
return;
|
||||
};
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let process = ocs_plugin_api::process::PluginProcess::spawn(
|
||||
std::path::Path::new(&plugin_path), &mut host, crate::plugin::v4_support::notification_handler(),
|
||||
).unwrap();
|
||||
let dir = std::env::temp_dir();
|
||||
let run = |host: &mut HostSession<'_>, tag: &str, body: &str| {
|
||||
let script = dir.join(format!("ocs_styles_{tag}_{}.py", std::process::id()));
|
||||
std::fs::write(&script, format!(concat!(
|
||||
"doc = ocs.active_document\nS = doc.text_styles\nD = doc.dim_styles\nL = doc.layers\n",
|
||||
"refused, accepted = [], []\n",
|
||||
"def check(tag, fn):\n",
|
||||
" try:\n fn()\n except (RuntimeError, TypeError, ValueError):\n refused.append(tag)\n",
|
||||
" else:\n accepted.append(tag)\n",
|
||||
"try:\n{body}\n",
|
||||
"except Exception as error:\n",
|
||||
" accepted.append('SCRIPTERROR_' + ''.join(c if c.isalnum() else '_' for c in str(error))[:120])\n",
|
||||
"L.create('REPORT ' + str(len(refused)) + ' ~ ' + ' '.join(accepted))\n",
|
||||
), body = body.lines().map(|line| format!(" {line}")).collect::<Vec<_>>().join("\n"))).unwrap();
|
||||
assert!(process.dispatch(host, &format!("PY_RUN {}", script.display()), &mut |_| {}).unwrap());
|
||||
let _ = std::fs::remove_file(&script);
|
||||
};
|
||||
let report = |document: &CadDocument| -> (usize, Vec<String>) {
|
||||
let name = document.layers.iter().map(|l| l.name.clone()).filter(|n| n.starts_with("REPORT")).last()
|
||||
.expect("script report layer");
|
||||
let (refused, accepted) = name["REPORT".len()..].split_once('~').unwrap();
|
||||
(refused.trim().parse().unwrap(), accepted.split_whitespace().map(str::to_owned).collect())
|
||||
};
|
||||
|
||||
run(&mut host, "build", r#"
|
||||
S.create('Title', height=5, width_factor=0.8, oblique=15, font='romans', big_font='bigfont', backward=True, annotative=True)
|
||||
S.create('Notes', font='arial.ttf')
|
||||
S.modify('Title', height=6, upside_down=True)
|
||||
D.create('Metric', dimscale=2, dimtxt=3.5, dimasz=2.5, dimtxsty='Title')
|
||||
D.create('Metric2', copy_from='Metric', dimtxt=4)
|
||||
D.modify('Metric', dimscale=3)
|
||||
S.rename('Notes', 'Remarks')
|
||||
S.rename('Title', 'Heading')
|
||||
check('t_dup', lambda: S.create('heading'))
|
||||
check('t_badname', lambda: S.create('a|b'))
|
||||
check('t_height', lambda: S.create('T1', height=-1))
|
||||
check('t_width0', lambda: S.create('T2', width_factor=0))
|
||||
check('t_oblique', lambda: S.create('T3', oblique=89))
|
||||
check('t_nofont', lambda: S.create('T4', font=''))
|
||||
check('t_unknown', lambda: S.create('T5', fnt='x'))
|
||||
check('t_modmissing', lambda: S.modify('Nope', height=1))
|
||||
check('t_modnone', lambda: S.modify('Heading'))
|
||||
check('t_renameStandard', lambda: S.rename('Standard', 'Std2'))
|
||||
check('t_rename_case', lambda: S.rename('Heading', 'HEADING'))
|
||||
check('t_rename_clash', lambda: S.rename('Remarks', 'heading'))
|
||||
check('t_delStandard', lambda: S.delete('Standard'))
|
||||
check('t_delInUse', lambda: S.delete('Heading'))
|
||||
check('t_current_missing', lambda: S.set_current('Nope'))
|
||||
check('d_dup', lambda: D.create('metric'))
|
||||
check('d_unknown', lambda: D.create('D1', dimbogus=1))
|
||||
check('d_handle', lambda: D.create('D2', dimtxsty_handle=5))
|
||||
check('d_name', lambda: D.modify('Metric', name='X'))
|
||||
check('d_type', lambda: D.create('D3', dimscale='big'))
|
||||
check('d_scale0', lambda: D.create('D4', dimscale=0))
|
||||
check('d_txt', lambda: D.create('D5', dimtxt=-1))
|
||||
check('d_txsty', lambda: D.create('D6', dimtxsty='Nope'))
|
||||
check('d_copy_missing', lambda: D.create('D7', copy_from='Nope'))
|
||||
check('d_delStandard', lambda: D.delete('Standard'))
|
||||
check('d_modmissing', lambda: D.modify('Nope', dimscale=2))
|
||||
"#);
|
||||
let (refused, accepted) = report(host.document());
|
||||
assert!(accepted.is_empty(), "accepted invalid requests: {accepted:?}");
|
||||
assert_eq!(refused, 26);
|
||||
|
||||
let check_state = |label: &str, document: &CadDocument| {
|
||||
assert!(document.text_styles.get("Title").is_none() && document.text_styles.get("Notes").is_none(), "{label}: old names gone");
|
||||
let title = document.text_styles.get("Heading").unwrap_or_else(|| panic!("{label}: Heading missing"));
|
||||
assert_eq!(title.height, 6.0, "{label}");
|
||||
assert!((title.width_factor - 0.8).abs() < 1e-9, "{label}");
|
||||
assert!((title.oblique_angle - 15f64.to_radians()).abs() < 1e-6, "{label}: oblique {}", title.oblique_angle);
|
||||
assert_eq!(title.font_file.to_lowercase(), "romans", "{label}");
|
||||
assert_eq!(title.big_font_file.to_lowercase(), "bigfont", "{label}");
|
||||
if label == "DXF" {
|
||||
// BLOCKER (cadcodec): the DXF STYLE writer hard-codes group 71 to 0, so the
|
||||
// backward and upside-down generation flags are lost on a DXF save.
|
||||
assert!(!title.flags.backward && !title.flags.upside_down, "{label}: cadcodec now keeps the flags; flip this canary");
|
||||
} else {
|
||||
assert!(title.flags.backward && title.flags.upside_down, "{label}");
|
||||
}
|
||||
let remarks = document.text_styles.get("Remarks").unwrap_or_else(|| panic!("{label}: Remarks missing"));
|
||||
assert_eq!(remarks.font_file.to_lowercase(), "arial.ttf", "{label}: {remarks:?}");
|
||||
let metric = document.dim_styles.get("Metric").unwrap_or_else(|| panic!("{label}: Metric missing"));
|
||||
assert_eq!((metric.dimscale, metric.dimtxt, metric.dimasz), (3.0, 3.5, 2.5), "{label}");
|
||||
if label == "DXF" {
|
||||
// BLOCKER (cadcodec): the DXF DIMSTYLE reader keeps only the text-style handle
|
||||
// (group 340) and never resolves `dimtxsty` from it, so the name reopens as
|
||||
// "Standard". The handle survives and identifies the right style.
|
||||
assert_eq!(metric.dimtxsty, "Standard", "{label}: cadcodec now resolves the name; flip this canary");
|
||||
assert_eq!(metric.dimtxsty_handle, title.handle, "{label}: text style link by handle");
|
||||
} else {
|
||||
assert_eq!(metric.dimtxsty, "Heading", "{label}: dimtxsty followed the rename");
|
||||
}
|
||||
let copy = document.dim_styles.get("Metric2").unwrap_or_else(|| panic!("{label}: Metric2 missing"));
|
||||
assert_eq!((copy.dimscale, copy.dimtxt), (2.0, 4.0), "{label}: copied before Metric changed");
|
||||
};
|
||||
check_state("live", host.document());
|
||||
let live_title = host.document().text_styles.get("Heading").unwrap().clone();
|
||||
assert!(live_title.annotative, "annotative is kept in the live style");
|
||||
assert_eq!(host.document().dim_styles.get("Metric").unwrap().dimtxsty_handle, live_title.handle, "dim style links the text style handle");
|
||||
let dwg = crate::io::load_bytes("styles.dwg", acadrust::DwgWriter::write_to_vec(host.document()).unwrap()).unwrap();
|
||||
let dxf = crate::io::load_bytes("styles.dxf", acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap()).unwrap();
|
||||
check_state("DWG", &dwg);
|
||||
check_state("DXF", &dxf);
|
||||
|
||||
run(&mut host, "destroy", r#"
|
||||
S.set_current('Remarks')
|
||||
D.set_current('Metric2')
|
||||
check('t_delcurrent', lambda: S.delete('Remarks'))
|
||||
check('d_delcurrent', lambda: D.delete('Metric2'))
|
||||
check('d_delInUseByCurrentOnly', lambda: D.delete('Metric2'))
|
||||
S.set_current('Standard')
|
||||
D.set_current('Standard')
|
||||
S.delete('Remarks')
|
||||
D.delete('Metric2')
|
||||
D.delete('Metric')
|
||||
S.delete('Heading')
|
||||
"#);
|
||||
let (refused, accepted) = report(host.document());
|
||||
assert!(accepted.is_empty(), "{accepted:?}");
|
||||
assert_eq!(refused, 3);
|
||||
let document = host.document();
|
||||
for name in ["Heading", "Remarks"] {
|
||||
assert!(document.text_styles.get(name).is_none(), "{name} deleted");
|
||||
}
|
||||
for name in ["Metric", "Metric2"] {
|
||||
assert!(document.dim_styles.get(name).is_none(), "{name} deleted");
|
||||
}
|
||||
assert_eq!(document.header.current_text_style_name, "Standard");
|
||||
assert_eq!(document.header.current_dimstyle_name, "Standard");
|
||||
|
||||
// U: the final report layer and the four deletes are five steps; undoing
|
||||
// them all brings the styles back.
|
||||
drop(process);
|
||||
drop(host);
|
||||
app.finish_pending_history(0);
|
||||
app.undo_steps(5);
|
||||
let document = &app.tabs[0].scene.document;
|
||||
assert!(document.text_styles.get("Heading").is_some() && document.text_styles.get("Remarks").is_some(), "undo restored the text styles");
|
||||
assert!(document.dim_styles.get("Metric").is_some() && document.dim_styles.get("Metric2").is_some(), "undo restored the dimension styles");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_python_raster_image_definition_linkage_over_real_ipc() {
|
||||
use acadrust::objects::ObjectType;
|
||||
|
||||
@@ -271,7 +271,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
fn style_exists(&self, kind: StyleKind, name: &str) -> bool {
|
||||
pub(super) fn style_exists(&self, kind: StyleKind, name: &str) -> bool {
|
||||
self.style_names(kind)
|
||||
.iter()
|
||||
.any(|n| n.eq_ignore_ascii_case(name))
|
||||
@@ -308,7 +308,7 @@ impl OpenCADStudio {
|
||||
|
||||
/// Refresh anything that mirrors the style list / current style after a
|
||||
/// mutation (ribbon dropdowns, geometry that depends on the style).
|
||||
fn after_style_change(&mut self, kind: StyleKind) {
|
||||
pub(super) fn after_style_change(&mut self, kind: StyleKind) {
|
||||
if kind.in_ribbon() {
|
||||
self.sync_ribbon_styles();
|
||||
}
|
||||
@@ -405,7 +405,7 @@ impl OpenCADStudio {
|
||||
false
|
||||
}
|
||||
|
||||
fn remove_style_storage(&mut self, kind: StyleKind, name: &str) -> bool {
|
||||
pub(super) fn remove_style_storage(&mut self, kind: StyleKind, name: &str) -> bool {
|
||||
let doc = &mut self.tabs[self.active_tab].scene.document;
|
||||
match kind {
|
||||
StyleKind::Text => doc.text_styles.remove(name).is_some(),
|
||||
@@ -541,7 +541,7 @@ impl OpenCADStudio {
|
||||
|
||||
/// Rename `old`→`new` in the backing store, re-keying table entries and
|
||||
/// rewriting name-based references + current-style pointers.
|
||||
fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) {
|
||||
pub(super) fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) {
|
||||
let i = self.active_tab;
|
||||
match kind {
|
||||
StyleKind::Text => {
|
||||
|
||||
Reference in new issue
Block a user