Merge pull request #1301: Add associative viewport dimensions

Refresh associations after complete SpaceMouse camera updates and recognize imported paper-space owners without Layout objects. Use kernel containment, intersection, perpendicular and spline tangent solving; pin cadkernel 7ce27f2 and add regressions.
This commit is contained in:
Hakan Seven committed 2026-09-16 09:56:33 +03:00
commit d10a0cf798
35 files changed
+7077 -85

No files matched your search

+59
View File
@@ -235,6 +235,65 @@ pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option<XData
write_pairs(doc, handle, kept);
}
/// Set — or, with `value: None`, clear — a single override on an entity that
/// is **not yet in the document** (a command's pending commit), leaving the
/// other overrides intact.
///
/// The document-backed [`set`] additionally registers the `ACAD` APPID; that
/// APPID is present in every drawing OCS writes, and committing the entity
/// goes through the normal add path, so an uncommitted write needs no
/// document. Keeping the override on the entity means it is part of the single
/// committed object and rides the ordinary undo/redo history.
pub fn set_on_entity(entity: &mut EntityType, code: i16, value: Option<XDataValue>) {
let common = entity.common_mut();
let mut kept: Vec<(i16, XDataValue)> = pairs(&common.extended_data)
.into_iter()
.filter(|(c, _)| *c != code)
.collect();
if let Some(v) = value {
kept.push((code, v));
}
let legacy = common.extended_data.get_record("ACAD_DSTYLE").is_some()
&& common.extended_data.get_record("ACAD").is_some_and(
|rec| !matches!(rec.values.first(), Some(XDataValue::String(n)) if n == "DSTYLE"),
);
let app = if legacy { "ACAD_DSTYLE" } else { "ACAD" };
let values = if kept.is_empty() {
None
} else {
let mut vals = Vec::new();
if !legacy {
vals.push(XDataValue::String("DSTYLE".to_string()));
}
vals.push(XDataValue::ControlString("{".to_string()));
for (code, value) in kept {
vals.push(XDataValue::Integer16(code));
vals.push(value);
}
vals.push(XDataValue::ControlString("}".to_string()));
Some(vals)
};
let mut rebuilt = ExtendedData::new();
for record in common.extended_data.records() {
if record.application_name != app && !(!legacy && record.application_name == "ACAD_DSTYLE")
{
rebuilt.add_record(record.clone());
}
}
if let Some(vals) = values {
let mut record = acadrust::xdata::ExtendedDataRecord::new(app);
for v in vals {
record.add_value(v);
}
rebuilt.add_record(record);
}
rebuilt.raw_dwg_eed = common.extended_data.raw_dwg_eed.clone();
common.extended_data = rebuilt;
}
pub fn property_real_code(field: &str) -> Option<i16> {
Some(match field {
"dim_arrow_size" => DIMASZ,
+68 -3
View File
@@ -1613,9 +1613,56 @@ pub(crate) fn resolved_dimension_style(
style.dimtxsty = record.name.clone();
}
}
// Negative DIMLFAC applies only in paper space; preserve its sign on export.
if style.dimlfac < 0.0 {
style.dimlfac = crate::scene::viewport_ref::MeasurementScale::user_lfac_for_space(
style.dimlfac,
dimension_in_paper_space(dimension, document),
);
}
style
}
/// Whether the dimension's owner is paper space, even without a Layout object.
pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocument) -> bool {
let owner = dimension.base().common.owner_handle;
if !owner.is_valid() {
return false;
}
document.objects.values().any(|object| {
matches!(object,
acadrust::objects::ObjectType::Layout(layout)
if layout.name != "Model" && layout.block_record == owner
)
}) || document.block_records.iter().any(|block| {
block.handle == owner && block.name.to_ascii_uppercase().starts_with("*PAPER_SPACE")
})
}
/// The value a dimension's text reports for its measurement: the raw
/// geometric measurement times the resolved linear factor. Angular dimensions
/// never scale.
///
/// `actual_measurement` (DXF group 42) stays **raw** — that is what the format
/// specifies and what export writes — so this is the one helper UI surfaces
/// use when they want the number the user reads.
pub(crate) fn displayed_measurement(dimension: &Dimension, style: Option<&DimStyle>) -> f64 {
let raw = dimension.measurement();
if matches!(
dimension,
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
) {
return raw;
}
let lfac = style.map(|s| s.dimlfac).unwrap_or(1.0);
if lfac.abs() < 1e-12 {
raw
} else {
raw * lfac
}
}
/// Build the linear-dimension property groups from the assigned style plus
/// any entity-level DSTYLE overrides. Conditional rows remain visible but are
/// read-only while their controlling option is disabled.
@@ -2097,7 +2144,13 @@ pub fn style_sections(
property(
t!("Measurement").as_ref(),
"measurement",
PropValue::ReadOnly(format!("{:.4}", dimension.measurement())),
// The value the dimension text reports (DIMLFAC applied,
// including viewport compensation) — not the raw paper
// distance, which would disagree with what is drawn.
PropValue::ReadOnly(format!(
"{:.4}",
displayed_measurement(dimension, Some(s))
)),
),
property(
t!("Text override").as_ref(),
@@ -2196,7 +2249,12 @@ pub fn style_sections(
number(
t!("Dim scale linear").as_ref(),
"dim_scale_linear",
real(ov::DIMLFAC, s.dimlfac),
// The *raw* signed DIMLFAC: a negative value is the
// paper-space / viewport-compensation convention, and
// hiding its sign would make the row uneditable in
// practice. `s.dimlfac` is sign-resolved, so fall back to
// the unresolved source style instead.
real(ov::DIMLFAC, style.dimlfac),
true,
),
number(
@@ -6403,7 +6461,14 @@ fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String {
})
.unwrap_or((4, 8, 1.0, 0.0, 46, 2, 0, 1.0, ""));
let lfac = if lfac.abs() < 1e-12 { 1.0 } else { lfac };
// `resolved_dimension_style` has already applied the DXF sign convention,
// so a negative value here means the caller passed an unresolved style;
// fall back to 1.0 rather than reporting a negative length.
let lfac = if lfac.abs() < 1e-12 || lfac < 0.0 {
1.0
} else {
lfac
};
let scaled = measurement * lfac;
let use_sub_units = zin & 4 != 0 && scaled.abs() < 1.0 && sub_factor.abs() > 1e-12;
// For values below one unit, DIMMZF replaces DIMLFAC; applying it after