fix(dimension): baked *D block matches live render (gaps, arrows, leaders, text)
Brings the save-time *D-block bake into parity with the live render so a dimension no longer changes when the file is saved and reopened (the reload draws from the block). Per a bake-vs-live audit: - text is now the live render's own Text/MText entity (alignment, MText codes, text style, alignment point) — fixes every label shifting ~half a glyph on reload - extension lines carry the DIMEXO start gap + DIMEXE overshoot, and slant by ext_line_rotation (DIMEDIT Oblique) - terminators follow the style: oblique tick when DIMTSZ>0, else a closed filled SOLID arrow (was a generic open stroke) - angular dims bake the swept arc + tangent terminators, flush extension lines (matching live exactly) - radius/diameter bake the leader leg; diameter runs edge-to-edge through the centre; centre mark honours negative DIMCEN (centre lines) - per-element colours/lineweights (DIMCLRD/E/T, DIMLWD/E), DIMSE/DIMSD suppression, DIMDLE tick overshoot - consistent arc sweep at exactly 180 degrees Refs #181. Remaining (custom DIMBLK arrow shapes, DIMFXLON, arrow-outside fit, annotative viewport scale) tracked separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
73eabc3aaa
commit
58da76ff70
2 changed files with 322 additions and 133 deletions
|
|
@ -2803,31 +2803,35 @@ fn perp_sign_default() -> f64 {
|
|||
1.0
|
||||
}
|
||||
|
||||
/// Text placement (position, height, rotation) the live renderer uses for
|
||||
/// `dim`, resolving its dimension style from `document`. Shared with the
|
||||
/// dimension-block baking on save so a baked `*D` block places its measurement
|
||||
/// text exactly where OCS draws it on screen — otherwise the text jumps when
|
||||
/// the file is saved and reopened (the reload renders from the baked block).
|
||||
/// `anno_scale` is the annotative scale (1.0 for a plain model-space bake).
|
||||
pub(crate) fn baked_dimension_text(
|
||||
/// The measurement-text entity (Text or MText) for a baked `*D` block, built
|
||||
/// through the SAME `dimension_text_entity` the live renderer uses — so the
|
||||
/// baked text matches the on-screen value, position, height, rotation,
|
||||
/// alignment, text style and MText handling, and nothing shifts when the file
|
||||
/// is saved and reopened (the reload renders from the block). Returns `None`
|
||||
/// when the text is suppressed (`user_text` is a single space). `anno_scale` is
|
||||
/// the annotative scale (1.0 for a plain model-space bake).
|
||||
pub(crate) fn baked_dimension_text_entity(
|
||||
dim: &Dimension,
|
||||
document: &CadDocument,
|
||||
anno_scale: f64,
|
||||
) -> Option<(String, Vector3, f64, f64)> {
|
||||
) -> Option<EntityType> {
|
||||
let style_name = dim.base().style_name.as_str();
|
||||
let style = document.dim_styles.iter().find(|s| {
|
||||
s.name.eq_ignore_ascii_case(style_name)
|
||||
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
// None => the dimension's text is suppressed (user_text " "): bake no Text.
|
||||
let value = dimension_text_value(dim, style)?;
|
||||
let dim_scale = style
|
||||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { anno_scale })
|
||||
.unwrap_or(1.0);
|
||||
let dim_txt = style.map(|s| s.dimtxt * dim_scale).unwrap_or(2.5 * dim_scale);
|
||||
let pos = dimension_text_pos_f64(dim, style, dim_txt, dim_scale);
|
||||
let rot = dimension_text_rotation(dim, style);
|
||||
Some((value, pos, dim_txt, rot))
|
||||
let mut ent = dimension_text_entity(dim, dim_txt, style, document, dim_scale)?;
|
||||
// For a non-default-aligned Text, pin the DXF alignment point (group 11) to
|
||||
// the insertion point so other CAD programs anchor the centred text where
|
||||
// OCS does, not at the world origin.
|
||||
if let EntityType::Text(t) = &mut ent {
|
||||
t.alignment_point = Some(t.insertion_point);
|
||||
}
|
||||
Some(ent)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -334,47 +334,69 @@ fn dim_seg(a: Vector3, b: Vector3, common: &acadrust::entities::EntityCommon) ->
|
|||
})
|
||||
}
|
||||
|
||||
/// Open (two-stroke) arrowhead with its tip at `tip`, strokes pointing back
|
||||
/// along the unit vector `(dx,dy)` (from the tip toward the dimension line),
|
||||
/// sized `size`. Simple, valid in any reader, and far better than a bare line.
|
||||
fn dim_arrowhead(
|
||||
/// A dimension-line terminator at `tip`, body extending back along the unit
|
||||
/// vector `(dx,dy)` (toward the dim line). When DIMTSZ>0 it's an oblique 45°
|
||||
/// tick; otherwise a closed *filled* arrowhead (DXF SOLID) of length DIMASZ —
|
||||
/// matching the style so a baked block keeps the original arrow type rather
|
||||
/// than a generic open stroke.
|
||||
fn dim_terminator(
|
||||
tip: Vector3,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
size: f64,
|
||||
dimasz: f64,
|
||||
dimtsz: f64,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
let ang = dy.atan2(dx);
|
||||
let wing = 18f64.to_radians();
|
||||
let mk = |a: f64| Vector3::new(tip.x + size * a.cos(), tip.y + size * a.sin(), tip.z);
|
||||
vec![
|
||||
dim_seg(tip, mk(ang + wing), common),
|
||||
dim_seg(tip, mk(ang - wing), common),
|
||||
]
|
||||
if dimtsz > 1e-9 {
|
||||
// 45° oblique tick centred on the tip (architectural style).
|
||||
let c = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let (tx, ty) = ((dx - dy) * c, (dx + dy) * c);
|
||||
return vec![dim_seg(
|
||||
Vector3::new(tip.x - tx * dimtsz, tip.y - ty * dimtsz, tip.z),
|
||||
Vector3::new(tip.x + tx * dimtsz, tip.y + ty * dimtsz, tip.z),
|
||||
common,
|
||||
)];
|
||||
}
|
||||
// Closed filled arrowhead: a triangle from the tip to a base DIMASZ back
|
||||
// along the dim line, half-width ~DIMASZ/6 (the standard arrow proportion).
|
||||
let (bx, by) = (tip.x + dx * dimasz, tip.y + dy * dimasz);
|
||||
let hw = dimasz / 6.0;
|
||||
let (px, py) = (-dy, dx);
|
||||
let c1 = Vector3::new(bx + px * hw, by + py * hw, tip.z);
|
||||
let c2 = Vector3::new(bx - px * hw, by - py * hw, tip.z);
|
||||
let mut sol = acadrust::entities::Solid::triangle(tip, c1, c2);
|
||||
sol.common = common.clone();
|
||||
sol.common.handle = Handle::NULL;
|
||||
vec![EntityType::Solid(sol)]
|
||||
}
|
||||
|
||||
/// Center-mark cross at `center`, arm length |DIMCEN|. Empty when DIMCEN == 0.
|
||||
fn dim_center_mark(
|
||||
center: Vector3,
|
||||
dimcen: f64,
|
||||
radius: f64,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
let s = dimcen.abs();
|
||||
if s < 1e-9 {
|
||||
let mag = dimcen.abs();
|
||||
if mag < 1e-9 {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
dim_seg(
|
||||
Vector3::new(center.x - s, center.y, center.z),
|
||||
Vector3::new(center.x + s, center.y, center.z),
|
||||
common,
|
||||
),
|
||||
dim_seg(
|
||||
Vector3::new(center.x, center.y - s, center.z),
|
||||
Vector3::new(center.x, center.y + s, center.z),
|
||||
common,
|
||||
),
|
||||
]
|
||||
let v = |x: f64, y: f64| Vector3::new(x, y, center.z);
|
||||
// Central "+" mark.
|
||||
let mut out = vec![
|
||||
dim_seg(v(center.x - mag, center.y), v(center.x + mag, center.y), common),
|
||||
dim_seg(v(center.x, center.y - mag), v(center.x, center.y + mag), common),
|
||||
];
|
||||
// Negative DIMCEN adds centre LINES: four radial strokes spanning the edge.
|
||||
if dimcen < 0.0 && radius > mag + 1e-6 {
|
||||
let inner = (radius - mag).max(0.0);
|
||||
let outer = radius + mag;
|
||||
out.push(dim_seg(v(center.x + inner, center.y), v(center.x + outer, center.y), common));
|
||||
out.push(dim_seg(v(center.x - inner, center.y), v(center.x - outer, center.y), common));
|
||||
out.push(dim_seg(v(center.x, center.y + inner), v(center.x, center.y + outer), common));
|
||||
out.push(dim_seg(v(center.x, center.y - inner), v(center.x, center.y - outer), common));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Arc from angle `a1` to `a2` (radians, swept the short way) at `radius` about
|
||||
|
|
@ -391,7 +413,9 @@ fn dim_arc_segs(
|
|||
while sweep > PI {
|
||||
sweep -= 2.0 * PI;
|
||||
}
|
||||
while sweep < -PI {
|
||||
// Half-open at -PI so an exact 180° span sweeps the same way as the
|
||||
// terminator code and the live render (CCW), not the opposite semicircle.
|
||||
while sweep <= -PI {
|
||||
sweep += 2.0 * PI;
|
||||
}
|
||||
let steps = 12usize.max((sweep.abs() / (PI / 36.0)).ceil() as usize);
|
||||
|
|
@ -406,8 +430,32 @@ fn dim_arc_segs(
|
|||
out
|
||||
}
|
||||
|
||||
/// (DIMASZ, DIMCEN), DIMSCALE-applied, resolved from the dimension's style.
|
||||
fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> (f64, f64) {
|
||||
/// Resolved, DIMSCALE-applied dimension metrics for baking.
|
||||
struct DimMetrics {
|
||||
dimasz: f64,
|
||||
dimcen: f64,
|
||||
dimexo: f64,
|
||||
dimexe: f64,
|
||||
dimtsz: f64,
|
||||
dimdle: f64,
|
||||
dimse1: bool,
|
||||
dimse2: bool,
|
||||
dimsd1: bool,
|
||||
dimsd2: bool,
|
||||
dimclrd: i16,
|
||||
dimclre: i16,
|
||||
dimclrt: i16,
|
||||
dimlwd: i16,
|
||||
dimlwe: i16,
|
||||
}
|
||||
|
||||
/// Metrics from the dim's style, mirroring what the live renderer applies, so a
|
||||
/// baked block reproduces the same gaps, arrow type, suppression, colours and
|
||||
/// lineweights: DIMASZ (arrow), DIMCEN (centre mark), DIMEXO/DIMEXE (extension
|
||||
/// gap/overshoot), DIMTSZ (oblique tick; >0 = ticks not arrows), DIMDLE (dim
|
||||
/// line overshoot past ticks), DIMSE1/2 + DIMSD1/2 (extension / dim-line
|
||||
/// suppression), DIMCLRD/E/T (colours) and DIMLWD/E (lineweights).
|
||||
fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
|
||||
let name = dim.base().style_name.as_str();
|
||||
let style = doc.dim_styles.iter().find(|s| {
|
||||
s.name.eq_ignore_ascii_case(name)
|
||||
|
|
@ -416,55 +464,123 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> (f64, f64) {
|
|||
let scale = style
|
||||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 })
|
||||
.unwrap_or(1.0);
|
||||
let dimasz = style.map(|s| s.dimasz * scale).unwrap_or(0.18 * scale).max(1e-6);
|
||||
let dimcen = style.map(|s| s.dimcen * scale).unwrap_or(0.09 * scale);
|
||||
(dimasz, dimcen)
|
||||
DimMetrics {
|
||||
dimasz: style.map(|s| s.dimasz * scale).unwrap_or(0.18 * scale).max(1e-6),
|
||||
dimcen: style.map(|s| s.dimcen * scale).unwrap_or(0.09 * scale),
|
||||
dimexo: style.map(|s| s.dimexo * scale).unwrap_or(0.0),
|
||||
dimexe: style.map(|s| s.dimexe * scale).unwrap_or(0.0),
|
||||
dimtsz: style.map(|s| s.dimtsz * scale).unwrap_or(0.0),
|
||||
dimdle: style.map(|s| s.dimdle * scale).unwrap_or(0.0),
|
||||
dimse1: style.map(|s| s.dimse1).unwrap_or(false),
|
||||
dimse2: style.map(|s| s.dimse2).unwrap_or(false),
|
||||
dimsd1: style.map(|s| s.dimsd1).unwrap_or(false),
|
||||
dimsd2: style.map(|s| s.dimsd2).unwrap_or(false),
|
||||
dimclrd: style.map(|s| s.dimclrd).unwrap_or(0),
|
||||
dimclre: style.map(|s| s.dimclre).unwrap_or(0),
|
||||
dimclrt: style.map(|s| s.dimclrt).unwrap_or(0),
|
||||
dimlwd: style.map(|s| s.dimlwd).unwrap_or(-2),
|
||||
dimlwe: style.map(|s| s.dimlwe).unwrap_or(-2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Baked geometry for an angular dimension: an extension line along each ray
|
||||
/// out to the dimension arc, plus the swept arc itself (which is what makes it
|
||||
/// read as an angle rather than two crossing lines — #181 / DIM-022).
|
||||
/// A baked sub-entity common with a per-element dimension colour / lineweight.
|
||||
/// A DIMCLR* of 0 (ByBlock) or 256 (ByLayer) keeps the dimension's own colour
|
||||
/// so the block inherits it; a specific ACI overrides. DIMLW* < 0 (ByBlock /
|
||||
/// ByLayer) keeps the dimension's lineweight.
|
||||
fn dim_common(base: &acadrust::entities::EntityCommon, clr: i16, lw: i16) -> acadrust::entities::EntityCommon {
|
||||
let mut c = base.clone();
|
||||
c.handle = Handle::NULL;
|
||||
if clr != 0 && clr != 256 {
|
||||
c.color = acadrust::types::Color::from_index(clr);
|
||||
}
|
||||
if lw >= 0 {
|
||||
c.line_weight = acadrust::types::LineWeight::from_value(lw);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Baked geometry for an angular dimension, matching the live render exactly:
|
||||
/// an extension line from each measured ray point flush out to the dimension
|
||||
/// arc, the swept arc, and a terminator at each arc end. (The live angular path
|
||||
/// applies neither DIMEXO/DIMEXE nor DIMSE suppression, so the bake mustn't
|
||||
/// either, or the dim would shift on reload. Adding those to both is tracked as
|
||||
/// a separate live-render enhancement.) #181 / DIM-022.
|
||||
fn angular_block_segs(
|
||||
vertex: Vector3,
|
||||
p1: Vector3,
|
||||
p2: Vector3,
|
||||
arc_loc: Vector3,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
met: &DimMetrics,
|
||||
ext_c: &acadrust::entities::EntityCommon,
|
||||
dim_c: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
use std::f64::consts::PI;
|
||||
let a1 = (p1.y - vertex.y).atan2(p1.x - vertex.x);
|
||||
let a2 = (p2.y - vertex.y).atan2(p2.x - vertex.x);
|
||||
let radius = ((arc_loc.x - vertex.x).powi(2) + (arc_loc.y - vertex.y).powi(2)).sqrt();
|
||||
let mut out = Vec::new();
|
||||
if radius < 1e-9 {
|
||||
out.push(dim_seg(p1, vertex, common));
|
||||
out.push(dim_seg(p2, vertex, common));
|
||||
out.push(dim_seg(p1, vertex, ext_c));
|
||||
out.push(dim_seg(p2, vertex, ext_c));
|
||||
return out;
|
||||
}
|
||||
let e1 = Vector3::new(
|
||||
vertex.x + a1.cos() * radius,
|
||||
vertex.y + a1.sin() * radius,
|
||||
vertex.z,
|
||||
);
|
||||
let e2 = Vector3::new(
|
||||
vertex.x + a2.cos() * radius,
|
||||
vertex.y + a2.sin() * radius,
|
||||
vertex.z,
|
||||
);
|
||||
// Extension lines run from each measured ray point out to the arc.
|
||||
out.push(dim_seg(p1, e1, common));
|
||||
out.push(dim_seg(p2, e2, common));
|
||||
out.extend(dim_arc_segs(vertex, radius, a1, a2, common));
|
||||
// Extension lines run flush from each measured ray point out to the arc.
|
||||
let e1 = Vector3::new(vertex.x + a1.cos() * radius, vertex.y + a1.sin() * radius, vertex.z);
|
||||
let e2 = Vector3::new(vertex.x + a2.cos() * radius, vertex.y + a2.sin() * radius, vertex.z);
|
||||
out.push(dim_seg(p1, e1, ext_c));
|
||||
out.push(dim_seg(p2, e2, ext_c));
|
||||
out.extend(dim_arc_segs(vertex, radius, a1, a2, dim_c));
|
||||
// Terminators tangent to the arc at each end, pointing along the sweep.
|
||||
let mut sweep = a2 - a1;
|
||||
while sweep > PI {
|
||||
sweep -= 2.0 * PI;
|
||||
}
|
||||
while sweep <= -PI {
|
||||
sweep += 2.0 * PI;
|
||||
}
|
||||
let sgn = if sweep >= 0.0 { 1.0 } else { -1.0 };
|
||||
out.extend(dim_terminator(e1, -a1.sin() * sgn, a1.cos() * sgn, met.dimasz, met.dimtsz, dim_c));
|
||||
out.extend(dim_terminator(e2, a2.sin() * sgn, -a2.cos() * sgn, met.dimasz, met.dimtsz, dim_c));
|
||||
out
|
||||
}
|
||||
|
||||
/// The text anchor for a radial leader: the saved text middle point when set,
|
||||
/// else the midpoint of `a` and `b` — mirroring the live `dimension_text_position`.
|
||||
fn dim_text_anchor(
|
||||
base: &acadrust::entities::dimension::DimensionBase,
|
||||
a: Vector3,
|
||||
b: Vector3,
|
||||
) -> Vector3 {
|
||||
let p = base.text_middle_point;
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-8 {
|
||||
p
|
||||
} else {
|
||||
Vector3::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5, (a.z + b.z) * 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalise `(dx,dy)`, falling back to `(fx,fy)` when it is ~zero length.
|
||||
fn norm2(dx: f64, dy: f64, fx: f64, fy: f64) -> (f64, f64) {
|
||||
let m = (dx * dx + dy * dy).sqrt();
|
||||
if m > 1e-9 {
|
||||
(dx / m, dy / m)
|
||||
} else {
|
||||
(fx, fy)
|
||||
}
|
||||
}
|
||||
|
||||
fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
use acadrust::entities::Text;
|
||||
|
||||
let base = dim.base();
|
||||
let common = base.common.clone();
|
||||
let met = dim_metrics(dim, doc);
|
||||
// Per-element commons so a baked block keeps the style's colours/lineweights:
|
||||
// extension lines (DIMCLRE/DIMLWE), dimension line + arrows + centre mark
|
||||
// (DIMCLRD/DIMLWD), text (DIMCLRT).
|
||||
let ext_c = dim_common(&base.common, met.dimclre, met.dimlwe);
|
||||
let dim_c = dim_common(&base.common, met.dimclrd, met.dimlwd);
|
||||
let mut result: Vec<EntityType> = Vec::new();
|
||||
|
||||
// Helper: make a line segment
|
||||
// Helper: make a line segment with a given common.
|
||||
let make_seg = |a: &Vector3, b: &Vector3, common: &EntityCommon| -> EntityType {
|
||||
let mut c = common.clone();
|
||||
c.handle = Handle::NULL;
|
||||
|
|
@ -478,35 +594,55 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
|||
|
||||
let v3 = |x: f64, y: f64, z: f64| Vector3::new(x, y, z);
|
||||
|
||||
// Dimension line for a linear/aligned dim, between d1 and d2 with the unit
|
||||
// direction (ux,uy): DIMDLE overshoot when ticks are used, omitted when both
|
||||
// halves are suppressed. Terminators are always placed at the origins.
|
||||
let push_dim_line = |result: &mut Vec<EntityType>, d1: Vector3, d2: Vector3, ux: f64, uy: f64| {
|
||||
if !(met.dimsd1 && met.dimsd2) {
|
||||
let dle = if met.dimtsz > 1e-9 { met.dimdle } else { 0.0 };
|
||||
let a = v3(d1.x - ux * dle, d1.y - uy * dle, d1.z);
|
||||
let b = v3(d2.x + ux * dle, d2.y + uy * dle, d2.z);
|
||||
result.push(make_seg(&a, &b, &dim_c));
|
||||
}
|
||||
result.extend(dim_terminator(d1, ux, uy, met.dimasz, met.dimtsz, &dim_c));
|
||||
result.extend(dim_terminator(d2, -ux, -uy, met.dimasz, met.dimtsz, &dim_c));
|
||||
};
|
||||
|
||||
match dim {
|
||||
Dimension::Aligned(d) => {
|
||||
let fx = d.first_point.x;
|
||||
let fy = d.first_point.y;
|
||||
let sx = d.second_point.x;
|
||||
let sy = d.second_point.y;
|
||||
let dx_s = sx - fx;
|
||||
let dy_s = sy - fy;
|
||||
let len = (dx_s * dx_s + dy_s * dy_s).sqrt().max(1e-12);
|
||||
let axis_angle = dy_s.atan2(dx_s);
|
||||
let axis_angle = (sy - fy).atan2(sx - fx);
|
||||
let perp_x = -(axis_angle.sin());
|
||||
let perp_y = axis_angle.cos();
|
||||
let offset =
|
||||
(d.definition_point.x - fx) * perp_x + (d.definition_point.y - fy) * perp_y;
|
||||
let d1 = v3(fx + perp_x * offset, fy + perp_y * offset, d.first_point.z);
|
||||
let d2 = v3(sx + perp_x * offset, sy + perp_y * offset, d.second_point.z);
|
||||
result.push(make_seg(&d.first_point, &d1, &common));
|
||||
result.push(make_seg(&d.second_point, &d2, &common));
|
||||
result.push(make_seg(&d1, &d2, &common));
|
||||
let (asz, _) = dim_metrics(dim, doc);
|
||||
let s = if offset >= 0.0 { 1.0 } else { -1.0 };
|
||||
// DIMEDIT "Oblique": extension lines slant by ext_line_rotation (the
|
||||
// dim line itself stays on the unrotated perp). DIM-EXT-OBLIQUE.
|
||||
let (es, ec) = d.ext_line_rotation.sin_cos();
|
||||
let edx = perp_x * ec - perp_y * es;
|
||||
let edy = perp_x * es + perp_y * ec;
|
||||
let e1a = v3(fx + edx * (s * met.dimexo), fy + edy * (s * met.dimexo), d.first_point.z);
|
||||
let e1b = v3(d1.x + edx * (s * met.dimexe), d1.y + edy * (s * met.dimexe), d1.z);
|
||||
let e2a = v3(sx + edx * (s * met.dimexo), sy + edy * (s * met.dimexo), d.second_point.z);
|
||||
let e2b = v3(d2.x + edx * (s * met.dimexe), d2.y + edy * (s * met.dimexe), d2.z);
|
||||
if !met.dimse1 {
|
||||
result.push(make_seg(&e1a, &e1b, &ext_c));
|
||||
}
|
||||
if !met.dimse2 {
|
||||
result.push(make_seg(&e2a, &e2b, &ext_c));
|
||||
}
|
||||
let ml = ((d2.x - d1.x).powi(2) + (d2.y - d1.y).powi(2)).sqrt().max(1e-12);
|
||||
let (ux, uy) = ((d2.x - d1.x) / ml, (d2.y - d1.y) / ml);
|
||||
result.extend(dim_arrowhead(d1, ux, uy, asz, &common));
|
||||
result.extend(dim_arrowhead(d2, -ux, -uy, asz, &common));
|
||||
let _ = len;
|
||||
push_dim_line(&mut result, d1, d2, ux, uy);
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
// `rotation` is the dimension-line angle, already in radians (the
|
||||
// same convention the live renderer uses) — do not convert again.
|
||||
// `rotation` is the dimension-line angle, already in radians.
|
||||
let angle = d.rotation;
|
||||
let perp_x = -(angle.sin());
|
||||
let perp_y = angle.cos();
|
||||
|
|
@ -514,59 +650,85 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
|||
let fy = d.first_point.y;
|
||||
let sx = d.second_point.x;
|
||||
let sy = d.second_point.y;
|
||||
// The dimension line passes through `definition_point` at `rotation`.
|
||||
// Project each extension origin onto that line *independently*: a
|
||||
// point's landing offset is (def - point)·perp. A single shared
|
||||
// offset only lands both origins on the line when they are level;
|
||||
// for sloped origins (e.g. a DIMCONTINUE chain over non-level
|
||||
// points) it tilts the dimension line — issue #181.
|
||||
// Project each extension origin onto the dim line independently — a
|
||||
// shared offset tilts the line over non-level origins (#181).
|
||||
let dperp = d.definition_point.x * perp_x + d.definition_point.y * perp_y;
|
||||
let off1 = dperp - (fx * perp_x + fy * perp_y);
|
||||
let off2 = dperp - (sx * perp_x + sy * perp_y);
|
||||
let d1 = v3(fx + perp_x * off1, fy + perp_y * off1, d.first_point.z);
|
||||
let d2 = v3(sx + perp_x * off2, sy + perp_y * off2, d.second_point.z);
|
||||
result.push(make_seg(&d.first_point, &d1, &common));
|
||||
result.push(make_seg(&d.second_point, &d2, &common));
|
||||
result.push(make_seg(&d1, &d2, &common));
|
||||
let (asz, _) = dim_metrics(dim, doc);
|
||||
// Extension lines: DIMEXO start gap from the point, DIMEXE overshoot
|
||||
// past the dim line, in the perp direction toward it. #181 / DIM-023.
|
||||
let s1 = if off1 >= 0.0 { 1.0 } else { -1.0 };
|
||||
let s2 = if off2 >= 0.0 { 1.0 } else { -1.0 };
|
||||
// DIMEDIT "Oblique": extension lines slant by ext_line_rotation.
|
||||
let (es, ec) = d.ext_line_rotation.sin_cos();
|
||||
let edx = perp_x * ec - perp_y * es;
|
||||
let edy = perp_x * es + perp_y * ec;
|
||||
let e1a = v3(fx + edx * (s1 * met.dimexo), fy + edy * (s1 * met.dimexo), d.first_point.z);
|
||||
let e1b = v3(d1.x + edx * (s1 * met.dimexe), d1.y + edy * (s1 * met.dimexe), d1.z);
|
||||
let e2a = v3(sx + edx * (s2 * met.dimexo), sy + edy * (s2 * met.dimexo), d.second_point.z);
|
||||
let e2b = v3(d2.x + edx * (s2 * met.dimexe), d2.y + edy * (s2 * met.dimexe), d2.z);
|
||||
if !met.dimse1 {
|
||||
result.push(make_seg(&e1a, &e1b, &ext_c));
|
||||
}
|
||||
if !met.dimse2 {
|
||||
result.push(make_seg(&e2a, &e2b, &ext_c));
|
||||
}
|
||||
let ml = ((d2.x - d1.x).powi(2) + (d2.y - d1.y).powi(2)).sqrt().max(1e-12);
|
||||
let (ux, uy) = ((d2.x - d1.x) / ml, (d2.y - d1.y) / ml);
|
||||
result.extend(dim_arrowhead(d1, ux, uy, asz, &common));
|
||||
result.extend(dim_arrowhead(d2, -ux, -uy, asz, &common));
|
||||
push_dim_line(&mut result, d1, d2, ux, uy);
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
// center -> point on circle, arrowhead at the point toward centre,
|
||||
// plus the centre mark.
|
||||
let (center, point) = (d.angle_vertex, d.definition_point);
|
||||
result.push(make_seg(¢er, &point, &common));
|
||||
let (asz, cen) = dim_metrics(dim, doc);
|
||||
let m = ((center.x - point.x).powi(2) + (center.y - point.y).powi(2))
|
||||
result.push(make_seg(¢er, &point, &dim_c));
|
||||
let len = ((center.x - point.x).powi(2) + (center.y - point.y).powi(2))
|
||||
.sqrt()
|
||||
.max(1e-12);
|
||||
result.extend(dim_arrowhead(
|
||||
result.extend(dim_terminator(
|
||||
point,
|
||||
(center.x - point.x) / m,
|
||||
(center.y - point.y) / m,
|
||||
asz,
|
||||
&common,
|
||||
(center.x - point.x) / len,
|
||||
(center.y - point.y) / len,
|
||||
met.dimasz,
|
||||
met.dimtsz,
|
||||
&dim_c,
|
||||
));
|
||||
result.extend(dim_center_mark(center, cen, &common));
|
||||
// Leader from the arrow tip toward the text — to the text anchor when
|
||||
// leader_length is 0, else that far along the text direction (matches
|
||||
// the live radius render). DIM-RAD-LEADER.
|
||||
let anchor = dim_text_anchor(base, center, point);
|
||||
let ld = norm2(anchor.x - point.x, anchor.y - point.y, 1.0, 0.0);
|
||||
let leader = if d.leader_length.abs() > 1e-9 {
|
||||
v3(point.x + ld.0 * d.leader_length, point.y + ld.1 * d.leader_length, point.z)
|
||||
} else {
|
||||
anchor
|
||||
};
|
||||
result.push(make_seg(&point, &leader, &dim_c));
|
||||
result.extend(dim_center_mark(center, met.dimcen, len, &dim_c));
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
// Full diameter line through the centre (far edge -> near edge),
|
||||
// inward arrows at both edges, plus the centre mark. angle_vertex is
|
||||
// the centre and definition_point the point on the circle.
|
||||
// Full diameter through the centre (far edge -> near edge), inward
|
||||
// terminators at both edges, plus the centre mark.
|
||||
let (center, edge) = (d.angle_vertex, d.definition_point);
|
||||
let far = v3(2.0 * center.x - edge.x, 2.0 * center.y - edge.y, edge.z);
|
||||
result.push(make_seg(&far, &edge, &common));
|
||||
let (asz, cen) = dim_metrics(dim, doc);
|
||||
let m = ((edge.x - far.x).powi(2) + (edge.y - far.y).powi(2))
|
||||
result.push(make_seg(&far, &edge, &dim_c));
|
||||
let len = ((edge.x - far.x).powi(2) + (edge.y - far.y).powi(2))
|
||||
.sqrt()
|
||||
.max(1e-12);
|
||||
let (ux, uy) = ((edge.x - far.x) / m, (edge.y - far.y) / m);
|
||||
result.extend(dim_arrowhead(edge, -ux, -uy, asz, &common));
|
||||
result.extend(dim_arrowhead(far, ux, uy, asz, &common));
|
||||
result.extend(dim_center_mark(center, cen, &common));
|
||||
let (ux, uy) = ((edge.x - far.x) / len, (edge.y - far.y) / len);
|
||||
result.extend(dim_terminator(edge, -ux, -uy, met.dimasz, met.dimtsz, &dim_c));
|
||||
result.extend(dim_terminator(far, ux, uy, met.dimasz, met.dimtsz, &dim_c));
|
||||
// Optional leader past the near edge toward the text. DIM-DIA-LEADER.
|
||||
if d.leader_length.abs() > 1e-9 {
|
||||
let anchor = dim_text_anchor(base, center, edge);
|
||||
let ld = norm2(anchor.x - edge.x, anchor.y - edge.y, ux, uy);
|
||||
result.push(make_seg(
|
||||
&edge,
|
||||
&v3(edge.x + ld.0 * d.leader_length, edge.y + ld.1 * d.leader_length, edge.z),
|
||||
&dim_c,
|
||||
));
|
||||
}
|
||||
result.extend(dim_center_mark(center, met.dimcen, len * 0.5, &dim_c));
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
result.extend(angular_block_segs(
|
||||
|
|
@ -574,7 +736,9 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
|||
d.first_point,
|
||||
d.second_point,
|
||||
d.dimension_arc,
|
||||
&common,
|
||||
&met,
|
||||
&ext_c,
|
||||
&dim_c,
|
||||
));
|
||||
}
|
||||
Dimension::Angular3Pt(d) => {
|
||||
|
|
@ -583,30 +747,27 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
|||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
&common,
|
||||
&met,
|
||||
&ext_c,
|
||||
&dim_c,
|
||||
));
|
||||
}
|
||||
Dimension::Ordinate(d) => {
|
||||
result.push(make_seg(&d.feature_location, &d.definition_point, &common));
|
||||
result.push(make_seg(&d.definition_point, &d.leader_endpoint, &common));
|
||||
result.push(make_seg(&d.feature_location, &d.definition_point, &dim_c));
|
||||
result.push(make_seg(&d.definition_point, &d.leader_endpoint, &dim_c));
|
||||
}
|
||||
}
|
||||
|
||||
// Measurement text: value, position, height and rotation are all taken
|
||||
// from the SAME live-render path (style-resolved formatting incl.
|
||||
// DIMDEC/DIMLFAC/DIMPOST/units/`<>` and DIMTAD/DIMGAP placement), so the
|
||||
// baked block matches the on-screen dimension and nothing changes when the
|
||||
// file is saved and reopened — the reload renders from this block. A `None`
|
||||
// means the text is suppressed (user_text " "), so bake no Text. #181.
|
||||
if let Some((value, text_pos, text_h, text_rot)) =
|
||||
crate::entities::dimension::baked_dimension_text(dim, doc, 1.0)
|
||||
{
|
||||
let mut text = Text::with_value(value, text_pos)
|
||||
.with_height(text_h.max(0.1))
|
||||
.with_rotation(text_rot);
|
||||
text.common = common.clone();
|
||||
text.common.handle = Handle::NULL;
|
||||
result.push(EntityType::Text(text));
|
||||
// Measurement text is the live render's own Text/MText entity (value,
|
||||
// position, height, rotation, alignment, text style, MText handling all
|
||||
// shared), so the baked block matches the on-screen dimension and the label
|
||||
// doesn't shift when the file is saved and reopened. `None` = suppressed.
|
||||
if let Some(mut tent) = crate::entities::dimension::baked_dimension_text_entity(dim, doc, 1.0) {
|
||||
// Apply the text colour / lineweight (DIMCLRT) onto the entity.
|
||||
let tc = dim_common(&base.common, met.dimclrt, -2);
|
||||
tent.common_mut().color = tc.color;
|
||||
tent.common_mut().line_weight = tc.line_weight;
|
||||
result.push(tent);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -856,6 +1017,30 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// A linear dimension bakes closed FILLED arrowheads (SOLID entities), not
|
||||
// generic open strokes — so a saved+reloaded dim keeps the style's arrow
|
||||
// type. (Default style: DIMTSZ=0 -> arrows, two of them.)
|
||||
#[test]
|
||||
fn linear_dim_bakes_filled_arrowheads() {
|
||||
let mut doc = CadDocument::new();
|
||||
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||
let handle = doc
|
||||
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
|
||||
.unwrap();
|
||||
bake_dimension_blocks(&mut doc);
|
||||
let name = match doc.get_entity(handle) {
|
||||
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
|
||||
_ => panic!("dimension missing"),
|
||||
};
|
||||
let rec = doc.block_records.get(&name).expect("block");
|
||||
let solids = doc
|
||||
.entities()
|
||||
.filter(|e| matches!(e, EntityType::Solid(s) if s.common.owner_handle == rec.handle))
|
||||
.count();
|
||||
assert_eq!(solids, 2, "expected 2 filled (SOLID) arrowheads, got {solids}");
|
||||
}
|
||||
|
||||
// An angular dimension must bake its swept ARC (not just two rays), else a
|
||||
// saved+reloaded angular dim collapses to a V. The block should carry the
|
||||
// two extension lines plus many arc chords.
|
||||
|
|
|
|||
Loading…
Reference in a new issue