fix(renderer): match leader and dash styles

Read canonical ACAD/DSTYLE overrides for leader color and arrow size. Scale block-local linetype patterns through INSERT transforms.

Closes #436
This commit is contained in:
Hakan Seven 2026-07-24 19:04:57 +03:00
commit ceddb287f5
3 changed files with 131 additions and 35 deletions

View file

@ -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<Vec<acadrust::xdata::XDataValue>> = 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<Vec<(i16, acadrust::xdata::XDataValue)>> = 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;
}
}

View file

@ -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<Handle> {
})
}
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<XDataValue>) {
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<XData
if let Some(v) = value {
kept.push((code, v));
}
let values = if kept.is_empty() {
None
} else {
let mut vals = vec![XDataValue::ControlString("{".to_string())];
for (c, v) in kept {
vals.push(XDataValue::Integer16(c));
vals.push(v);
}
vals.push(XDataValue::ControlString("}".to_string()));
Some(vals)
};
crate::scene::view::dispatch::set_entity_xdata(doc, handle, "ACAD_DSTYLE", values);
write_pairs(doc, handle, kept);
}

View file

@ -1306,6 +1306,49 @@ fn resolve_wire_color(lw: &LocalWire, ctx: &ExpandCtx) -> [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<Vector3> = 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.