diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 1b337df50..7bebaf5f8 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -910,10 +910,10 @@ impl OpenCADStudio { // style crosses Dimension / Leader / Tolerance. let src_clone = self.tabs[i].scene.document.get_entity(src).cloned(); let transparency = src_clone.as_ref().map(|e| e.common().transparency.clone()); - // Dimension-style OVERRIDES ride the ACAD_DSTYLE xdata record; - // matching replicates the source's record (or clears the - // destination's when the source has none). - let dstyle_xdata: Option> = src_clone + // Dimension-style overrides ride the ACAD record, identified + // by a leading DSTYLE string. Matching replicates that payload + // (or clears the destination when the source has none). + let dstyle_xdata: Option> = src_clone .as_ref() .filter(|e| { matches!( @@ -921,8 +921,7 @@ impl OpenCADStudio { acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_) ) }) - .and_then(|e| e.common().extended_data.get_record("ACAD_DSTYLE")) - .map(|r| r.values.clone()); + .map(|e| crate::entities::dim_override::pairs(&e.common().extended_data)); if let Some((layer, color, linetype, lt_scale, lw)) = props { self.push_undo_snapshot(i, "MATCHPROP"); @@ -944,9 +943,9 @@ impl OpenCADStudio { match_special_props(se, e); } } - // Dim-style overrides (ACAD_DSTYLE) follow the style - // for dimension / leader destinations — through - // set_entity_xdata so no stale raw record survives. + // Dim-style overrides follow the style for dimension / + // leader destinations — through set_entity_xdata so no + // stale raw record survives. if dstyle_xdata.is_some() || matches!( self.tabs[i].scene.document.get_entity(*h), @@ -969,11 +968,10 @@ impl OpenCADStudio { | acadrust::EntityType::Leader(_) ) ) { - crate::scene::view::dispatch::set_entity_xdata( + crate::entities::dim_override::replace( &mut self.tabs[i].scene.document, *h, - "ACAD_DSTYLE", - dstyle_xdata.clone(), + dstyle_xdata.clone().unwrap_or_default(), ); } } @@ -2931,4 +2929,3 @@ fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityTyp dm.property_override_flags = sm.property_override_flags; } } - diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 0b5d66be2..9e2102eab 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -1,10 +1,11 @@ //! Per-object dimension-variable overrides. //! //! A leader (or dimension) that departs from its dimension style stores the -//! changed variables in the standard `ACAD_DSTYLE` XDATA record as a list of -//! (dimvar group code, value) pairs wrapped in `{ }` control strings. Both the -//! renderer and the properties panel prefer an override over the style default, -//! so editing one of these rows writes here and the change round-trips to file. +//! changed variables in the standard `ACAD` XDATA record, identified by a +//! leading `DSTYLE` string, as a list of (dimvar group code, value) pairs wrapped +//! in `{ }` control strings. Both the renderer and the properties panel prefer +//! an override over the style default, so editing one of these rows writes here +//! and the change round-trips to file. use acadrust::types::Color; use acadrust::xdata::{ExtendedData, XDataValue}; @@ -19,13 +20,24 @@ pub const DIMGAP: i16 = 147; // text offset / gap (real) pub const DIMLWD: i16 = 371; // dim line lineweight (int16) pub const DIMLDRBLK: i16 = 341; // leader arrow block (handle) -/// Every (code, value) override present in the `ACAD_DSTYLE` record. +/// Every (code, value) override present in the `ACAD`/`DSTYLE` record. pub fn pairs(xd: &ExtendedData) -> Vec<(i16, XDataValue)> { - let Some(rec) = xd.get_record("ACAD_DSTYLE") else { + let values = xd + .get_record("ACAD") + .and_then(|rec| match rec.values.first() { + Some(XDataValue::String(name)) if name == "DSTYLE" => Some(&rec.values[1..]), + _ => None, + }) + // Retain compatibility with records produced by older OCS versions. + .or_else(|| { + xd.get_record("ACAD_DSTYLE") + .map(|rec| rec.values.as_slice()) + }); + let Some(values) = values else { return Vec::new(); }; let mut out = Vec::new(); - let mut it = rec.values.iter(); + let mut it = values.iter(); // The record is a flat stream: a 1070 code marker followed by its typed // value, bracketed by 1002 "{" / "}" control strings (which are skipped). while let Some(v) = it.next() { @@ -78,9 +90,55 @@ pub fn handle(xd: &ExtendedData, code: i16) -> Option { }) } +fn write_pairs(doc: &mut CadDocument, handle: Handle, pairs: Vec<(i16, XDataValue)>) { + let Some(entity) = doc.get_entity(handle) else { + return; + }; + let use_canonical_record = entity + .common() + .extended_data + .get_record("ACAD") + .map(|rec| { + matches!( + rec.values.first(), + Some(XDataValue::String(name)) if name == "DSTYLE" + ) + }) + .unwrap_or(true); + + let mut values = if pairs.is_empty() { + None + } else { + let mut vals = vec![XDataValue::ControlString("{".to_string())]; + for (code, value) in pairs { + vals.push(XDataValue::Integer16(code)); + vals.push(value); + } + vals.push(XDataValue::ControlString("}".to_string())); + Some(vals) + }; + + if use_canonical_record { + if let Some(vals) = &mut values { + vals.insert(0, XDataValue::String("DSTYLE".to_string())); + } + crate::scene::view::dispatch::set_entity_xdata(doc, handle, "ACAD_DSTYLE", None); + crate::scene::view::dispatch::set_entity_xdata(doc, handle, "ACAD", values); + } else { + // Preserve unrelated Autodesk XDATA already occupying the ACAD record. + crate::scene::view::dispatch::set_entity_xdata(doc, handle, "ACAD_DSTYLE", values); + } +} + +/// Replace every dimension-variable override on entity `handle`. +pub fn replace(doc: &mut CadDocument, handle: Handle, values: Vec<(i16, XDataValue)>) { + write_pairs(doc, handle, values); +} + /// Set — or, with `value: None`, clear — a single override on entity `handle`, /// leaving the other overrides in the record untouched. Clearing the last one -/// drops the whole `ACAD_DSTYLE` record. +/// drops the whole `ACAD`/`DSTYLE` record. Legacy `ACAD_DSTYLE` records written +/// by older OCS versions are migrated when the canonical `ACAD` slot is free. pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option) { let Some(entity) = doc.get_entity(handle) else { return; @@ -92,16 +150,5 @@ pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option [f32; 4] { } } +/// Effective linetype scale along this wire after an INSERT transform. +/// +/// A non-uniform INSERT has no single global scale. Weight each segment by its +/// local length, producing the exact factor for a line and a stable +/// path-weighted approximation for a polyline or tessellated curve. +fn transformed_wire_length_scale(lw: &LocalWire, xform: &Transform) -> f32 { + let mut local_length = 0.0_f64; + let mut transformed_length = 0.0_f64; + let mut previous: Option = None; + + for (index, point) in lw.points.iter().enumerate() { + if !point.iter().all(|v| v.is_finite()) { + previous = None; + continue; + } + let low = lw.points_low.get(index).copied().unwrap_or([0.0; 3]); + let current = Vector3::new( + point[0] as f64 + low[0] as f64, + point[1] as f64 + low[1] as f64, + point[2] as f64 + low[2] as f64, + ); + if let Some(prev) = previous { + let delta = current - prev; + let segment_length = (delta.x * delta.x + delta.y * delta.y + delta.z * delta.z).sqrt(); + if segment_length > 1e-12 { + let transformed = xform.matrix.transform_direction(delta); + local_length += segment_length; + transformed_length += (transformed.x * transformed.x + + transformed.y * transformed.y + + transformed.z * transformed.z) + .sqrt(); + } + } + previous = Some(current); + } + + if local_length > 1e-12 && transformed_length.is_finite() { + (transformed_length / local_length) as f32 + } else { + 1.0 + } +} + fn emit_wire( lw: &LocalWire, accum_xform: &Transform, @@ -1338,8 +1381,17 @@ fn emit_wire( } else { lw.line_weight_px }; - let final_pat_len = final_pat_len * ctx.pslt_factor; - let final_pat = final_pat.map(|v| v * ctx.pslt_factor); + + // Pattern distances are stored in block-local units. Scale them by the + // wire's actual path-length ratio so uniform and non-uniform INSERTs both + // stay dimensionally consistent with their transformed geometry. + let pattern_scale = if final_pat_len > 0.0 { + transformed_wire_length_scale(lw, accum_xform) + } else { + 1.0 + }; + let final_pat_len = final_pat_len * ctx.pslt_factor * pattern_scale; + let final_pat = final_pat.map(|v| v * ctx.pslt_factor * pattern_scale); // A wide polyline's band width is baked in block-local units; scale it by // the insert transform so the shader band matches the scaled geometry.