feat(scene): expand DimStyle integration to the remaining visual fields
Drives more of the dim render off the parsed DimStyle so files round-trip visually identical to AutoCAD instead of using hard-coded fallbacks: - text format dispatch via DIMLUNIT (Sci / Decimal / Engineering / Architectural / Fractional / Windows) + DIMFRAC fraction denominator - alternate units appended in brackets per DIMALT / DIMALTF / DIMALTD / DIMALTU / DIMALTRND / DIMAPOST / DIMALTZ - tolerance / limits text per DIMTOL / DIMLIM / DIMTP / DIMTM / DIMTDEC / DIMTZIN; "value ± t", "+tp / -tm", or stacked "high/low" - center mark for radius/diameter from DIMCEN (positive = "+", negative = "+" plus radial strokes) - DIMDLE dim-line overshoot at tick endpoints - DIMJUST horizontal text slide along the dim axis - DIMTVP perpendicular text offset multiplier when DIMTAD=0 - DIMFXL/DIMFXLON fixed extension-line length override - DIMTFILL/DIMTFILLCLR text background fill rectangle - DIMLTEX/DIMLTEX1/DIMLTEX2 linetype handles resolved per-wire; ext1 and ext2 split into separate wires when their patterns differ - DIMSOXD read; left as a no-op until autofit is added Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1b2415bde5
commit
9d0b406b31
1 changed files with 590 additions and 87 deletions
|
|
@ -482,23 +482,46 @@ pub fn tessellate_dimension(
|
|||
})
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let (dimasz_raw, dimexo, dimexe, dim_txt, dimtsz_raw, dimsah, dimse1, dimse2, dimsd1, dimsd2) =
|
||||
style
|
||||
.map(|s| {
|
||||
(
|
||||
s.dimasz * dim_scale,
|
||||
(s.dimexo * dim_scale) as f32,
|
||||
(s.dimexe * dim_scale) as f32,
|
||||
s.dimtxt * dim_scale,
|
||||
s.dimtsz * dim_scale,
|
||||
s.dimsah,
|
||||
s.dimse1,
|
||||
s.dimse2,
|
||||
s.dimsd1,
|
||||
s.dimsd2,
|
||||
)
|
||||
})
|
||||
.unwrap_or((0.18, 0.0, 0.0, 2.5, 0.0, false, false, false, false, false));
|
||||
let (
|
||||
dimasz_raw,
|
||||
dimexo,
|
||||
dimexe,
|
||||
dim_txt,
|
||||
dimtsz_raw,
|
||||
dimsah,
|
||||
dimse1,
|
||||
dimse2,
|
||||
dimsd1,
|
||||
dimsd2,
|
||||
dimdle,
|
||||
dimfxl,
|
||||
dimfxlon,
|
||||
dimsoxd,
|
||||
dimcen,
|
||||
) = style
|
||||
.map(|s| {
|
||||
(
|
||||
s.dimasz * dim_scale,
|
||||
(s.dimexo * dim_scale) as f32,
|
||||
(s.dimexe * dim_scale) as f32,
|
||||
s.dimtxt * dim_scale,
|
||||
s.dimtsz * dim_scale,
|
||||
s.dimsah,
|
||||
s.dimse1,
|
||||
s.dimse2,
|
||||
s.dimsd1,
|
||||
s.dimsd2,
|
||||
(s.dimdle * dim_scale) as f32,
|
||||
(s.dimfxl * dim_scale) as f32,
|
||||
s.dimfxlon,
|
||||
s.dimsoxd,
|
||||
(s.dimcen * dim_scale) as f32,
|
||||
)
|
||||
})
|
||||
.unwrap_or((
|
||||
0.18, 0.0, 0.0, 2.5, 0.0, false, false, false, false, false, 0.0, 1.0, false, false,
|
||||
0.09,
|
||||
));
|
||||
|
||||
// Arrow selection precedence:
|
||||
// 1. DIMTSZ>0 → oblique tick (overrides DIMBLK*).
|
||||
|
|
@ -534,8 +557,16 @@ pub fn tessellate_dimension(
|
|||
dim,
|
||||
&arrow1,
|
||||
&arrow2,
|
||||
dimexo,
|
||||
dimexe,
|
||||
DimLineParams {
|
||||
dimexo,
|
||||
dimexe,
|
||||
dimdle,
|
||||
dimfxl,
|
||||
dimfxlon,
|
||||
dimsoxd,
|
||||
dimcen,
|
||||
ticks: dimtsz_raw > 1e-9,
|
||||
},
|
||||
SuppressFlags {
|
||||
ext1: dimse1,
|
||||
ext2: dimse2,
|
||||
|
|
@ -583,26 +614,84 @@ pub fn tessellate_dimension(
|
|||
line_weight_px,
|
||||
);
|
||||
|
||||
// DIMLTEX (dim line) / DIMLTEX1 (ext1) / DIMLTEX2 (ext2) — linetype
|
||||
// handles → pattern. Looked up in document.line_types by handle.
|
||||
let lt_scale = document.header.linetype_scale as f32 * dim.base().common.linetype_scale as f32;
|
||||
let (dim_pat_len, dim_pat) = style
|
||||
.map(|s| resolve_pattern_by_handle(document, s.dimltex_handle, lt_scale))
|
||||
.unwrap_or((0.0, [0.0; 8]));
|
||||
let (ext1_pat_len, ext1_pat) = style
|
||||
.map(|s| resolve_pattern_by_handle(document, s.dimltex1_handle, lt_scale))
|
||||
.unwrap_or((0.0, [0.0; 8]));
|
||||
let (ext2_pat_len, ext2_pat) = style
|
||||
.map(|s| resolve_pattern_by_handle(document, s.dimltex2_handle, lt_scale))
|
||||
.unwrap_or((0.0, [0.0; 8]));
|
||||
|
||||
let mut wires = Vec::new();
|
||||
|
||||
if !geom.ext_lines.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: geom.ext_lines,
|
||||
color: ext_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: lw_ext,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
// If ext1 and ext2 have different linetypes, split into two wires so
|
||||
// each can carry its own pattern. Otherwise emit as a single wire.
|
||||
let split = ext1_pat_len != ext2_pat_len || ext1_pat != ext2_pat;
|
||||
if split {
|
||||
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
|
||||
if !ext1.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: ext1,
|
||||
color: ext_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: ext1_pat_len,
|
||||
pattern: ext1_pat,
|
||||
line_weight_px: lw_ext,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
if !ext2.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: ext2,
|
||||
color: ext_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: ext2_pat_len,
|
||||
pattern: ext2_pat,
|
||||
line_weight_px: lw_ext,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: geom.ext_lines,
|
||||
color: ext_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: ext1_pat_len,
|
||||
pattern: ext1_pat,
|
||||
line_weight_px: lw_ext,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
wires.push(WireModel {
|
||||
|
|
@ -611,8 +700,8 @@ pub fn tessellate_dimension(
|
|||
color: dim_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
pattern_length: dim_pat_len,
|
||||
pattern: dim_pat,
|
||||
line_weight_px: lw_dim,
|
||||
snap_pts,
|
||||
tangent_geoms: vec![],
|
||||
|
|
@ -623,6 +712,37 @@ pub fn tessellate_dimension(
|
|||
fill_tris: geom.arrow_fill,
|
||||
});
|
||||
|
||||
// DIMTFILL: 0=none, 1=drawing background (transparent → skip), 2=DIMTFILLCLR.
|
||||
if let Some(s) = style {
|
||||
if s.dimtfill == 2 {
|
||||
if let Some(rect) = text_fill_rect(dim, style, dim_txt, world_offset) {
|
||||
let fill_color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
let c = AcadColor::from_index(s.dimtfillclr);
|
||||
aci_to_rgba(&c)
|
||||
};
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: fill_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: rect,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(text) = dimension_text_entity(dim, dim_txt, style) {
|
||||
let mut wire = tessellate(
|
||||
document,
|
||||
|
|
@ -666,6 +786,96 @@ fn resolve_dim_lineweight_px(code: i16, fallback_px: f32) -> f32 {
|
|||
(mm * MM_TO_PX).max(1.0)
|
||||
}
|
||||
|
||||
/// Look up a linetype in the document's line_types table by handle and
|
||||
/// resolve it to a (pattern_length, pattern) pair compatible with WireModel.
|
||||
fn resolve_pattern_by_handle(
|
||||
doc: &CadDocument,
|
||||
handle: acadrust::types::Handle,
|
||||
scale: f32,
|
||||
) -> (f32, [f32; 8]) {
|
||||
if handle.is_null() {
|
||||
return (0.0, [0.0; 8]);
|
||||
}
|
||||
let name = doc
|
||||
.line_types
|
||||
.iter()
|
||||
.find(|lt| lt.handle == handle)
|
||||
.map(|lt| lt.name.clone());
|
||||
match name {
|
||||
Some(n) => crate::scene::render::resolve_pattern(&doc.line_types, &n, scale),
|
||||
None => (0.0, [0.0; 8]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the combined ext-lines point list (NaN-separated segment pairs)
|
||||
/// into "first" / "second" halves. `append_linear_dimension` writes ext1
|
||||
/// before ext2, so the first segment is ext1 and the second is ext2.
|
||||
fn split_ext_lines(points: &[[f32; 3]]) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
|
||||
let mut groups: Vec<Vec<[f32; 3]>> = Vec::new();
|
||||
let mut current: Vec<[f32; 3]> = Vec::new();
|
||||
for &p in points {
|
||||
if p[0].is_nan() {
|
||||
if !current.is_empty() {
|
||||
groups.push(std::mem::take(&mut current));
|
||||
}
|
||||
} else {
|
||||
current.push(p);
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
groups.push(current);
|
||||
}
|
||||
let mut iter = groups.into_iter();
|
||||
let first = iter.next().unwrap_or_default();
|
||||
let rest: Vec<[f32; 3]> = iter.flatten().collect();
|
||||
(first, rest)
|
||||
}
|
||||
|
||||
/// Build a rectangle of filled triangles sitting under the dim text, used
|
||||
/// when DIMTFILL = 2 (explicit fill colour). The rect width is estimated
|
||||
/// from the formatted text length × character-cell width; an absolutely
|
||||
/// correct box would need full text metrics from the font cache.
|
||||
fn text_fill_rect(
|
||||
dim: &Dimension,
|
||||
style: Option<&DimStyle>,
|
||||
text_height: f64,
|
||||
world_offset: [f64; 3],
|
||||
) -> Option<Vec<[f32; 3]>> {
|
||||
let value = dimension_text_value(dim, style)?;
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let pos = dimension_text_pos_f64(dim, style, text_height);
|
||||
let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0).max(0.0);
|
||||
// ~0.6 × text_height per character; matches average glyph aspect for
|
||||
// the bundled stick fonts. Inflate by 1 DIMGAP on each side.
|
||||
let approx_w =
|
||||
value.chars().count() as f64 * text_height * 0.6 + dimgap * 2.0;
|
||||
let approx_h = text_height + dimgap * 2.0;
|
||||
let rot = if dim.base().text_rotation.abs() > 1e-9 {
|
||||
dim.base().text_rotation
|
||||
} else {
|
||||
dimension_text_natural_rotation(dim)
|
||||
};
|
||||
let (sr, cr) = rot.sin_cos();
|
||||
let hx = approx_w * 0.5;
|
||||
let hy = approx_h * 0.5;
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let cx = (pos.x - ox) as f32;
|
||||
let cy = (pos.y - oy) as f32;
|
||||
let cz = (pos.z - oz) as f32;
|
||||
let corner = |dx: f64, dy: f64| -> [f32; 3] {
|
||||
let lx = dx * cr - dy * sr;
|
||||
let ly = dx * sr + dy * cr;
|
||||
[cx + lx as f32, cy + ly as f32, cz]
|
||||
};
|
||||
let p1 = corner(-hx, -hy);
|
||||
let p2 = corner(hx, -hy);
|
||||
let p3 = corner(hx, hy);
|
||||
let p4 = corner(-hx, hy);
|
||||
Some(vec![p1, p2, p3, p1, p3, p4])
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ArrowKind {
|
||||
None,
|
||||
|
|
@ -783,6 +993,18 @@ struct SuppressFlags {
|
|||
dim2: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct DimLineParams {
|
||||
dimexo: f32,
|
||||
dimexe: f32,
|
||||
dimdle: f32,
|
||||
dimfxl: f32,
|
||||
dimfxlon: bool,
|
||||
dimsoxd: bool,
|
||||
dimcen: f32,
|
||||
ticks: bool,
|
||||
}
|
||||
|
||||
struct DimGeom {
|
||||
ext_lines: Vec<[f32; 3]>,
|
||||
dim_lines: Vec<[f32; 3]>,
|
||||
|
|
@ -1720,8 +1942,7 @@ fn dimension_geometry(
|
|||
dim: &Dimension,
|
||||
arrow1: &ArrowKind,
|
||||
arrow2: &ArrowKind,
|
||||
dimexo: f32,
|
||||
dimexe: f32,
|
||||
params: DimLineParams,
|
||||
suppress: SuppressFlags,
|
||||
world_offset: [f64; 3],
|
||||
) -> DimGeom {
|
||||
|
|
@ -1734,7 +1955,7 @@ fn dimension_geometry(
|
|||
let def = lv(d.definition_point);
|
||||
let axis = normalized_or(second - first, Vec3::X);
|
||||
append_linear_dimension(
|
||||
&mut g, first, second, def, axis, arrow1, arrow2, dimexo, dimexe, suppress,
|
||||
&mut g, first, second, def, axis, arrow1, arrow2, params, suppress,
|
||||
);
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
|
|
@ -1750,8 +1971,7 @@ fn dimension_geometry(
|
|||
normalized_or(axis, Vec3::X),
|
||||
arrow1,
|
||||
arrow2,
|
||||
dimexo,
|
||||
dimexe,
|
||||
params,
|
||||
suppress,
|
||||
);
|
||||
}
|
||||
|
|
@ -1762,6 +1982,8 @@ fn dimension_geometry(
|
|||
add_segment(&mut g.dim_lines, center, point);
|
||||
add_segment(&mut g.dim_lines, point, text);
|
||||
append_arrow(&mut g, point, normalized_or(center - point, Vec3::X), arrow1);
|
||||
let radius = (point - center).length();
|
||||
append_center_mark(&mut g, center, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
let p1 = lv(d.angle_vertex);
|
||||
|
|
@ -1769,6 +1991,8 @@ fn dimension_geometry(
|
|||
add_segment(&mut g.dim_lines, p1, p2);
|
||||
append_arrow(&mut g, p1, normalized_or(p2 - p1, Vec3::X), arrow1);
|
||||
append_arrow(&mut g, p2, normalized_or(p1 - p2, Vec3::X), arrow2);
|
||||
let radius = (p2 - p1).length() * 0.5;
|
||||
append_center_mark(&mut g, (p1 + p2) * 0.5, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
append_angular_dimension(
|
||||
|
|
@ -1816,8 +2040,7 @@ fn append_linear_dimension(
|
|||
axis: Vec3,
|
||||
arrow1: &ArrowKind,
|
||||
arrow2: &ArrowKind,
|
||||
dimexo: f32,
|
||||
dimexe: f32,
|
||||
params: DimLineParams,
|
||||
suppress: SuppressFlags,
|
||||
) {
|
||||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
|
|
@ -1828,25 +2051,100 @@ fn append_linear_dimension(
|
|||
let d2 = second + perp * offset2;
|
||||
let sign1 = if offset1 >= 0.0 { 1.0_f32 } else { -1.0 };
|
||||
let sign2 = if offset2 >= 0.0 { 1.0_f32 } else { -1.0 };
|
||||
let ext1_start = first + perp * (sign1 * dimexo);
|
||||
let ext1_end = d1 + perp * (sign1 * dimexe);
|
||||
let ext2_start = second + perp * (sign2 * dimexo);
|
||||
let ext2_end = d2 + perp * (sign2 * dimexe);
|
||||
|
||||
// DIMFXLON / DIMFXL: fixed extension-line length from the dim line back
|
||||
// toward (but not past) the definition point. Otherwise grow from the
|
||||
// def point with DIMEXO gap, extending DIMEXE past the dim line.
|
||||
let (ext1_start, ext1_end, ext2_start, ext2_end) = if params.dimfxlon {
|
||||
let fxl = params.dimfxl.max(0.0);
|
||||
let s1 = d1 - perp * (sign1 * fxl);
|
||||
let e1 = d1 + perp * (sign1 * params.dimexe);
|
||||
let s2 = d2 - perp * (sign2 * fxl);
|
||||
let e2 = d2 + perp * (sign2 * params.dimexe);
|
||||
(s1, e1, s2, e2)
|
||||
} else {
|
||||
(
|
||||
first + perp * (sign1 * params.dimexo),
|
||||
d1 + perp * (sign1 * params.dimexe),
|
||||
second + perp * (sign2 * params.dimexo),
|
||||
d2 + perp * (sign2 * params.dimexe),
|
||||
)
|
||||
};
|
||||
if !suppress.ext1 {
|
||||
add_segment(&mut g.ext_lines, ext1_start, ext1_end);
|
||||
}
|
||||
if !suppress.ext2 {
|
||||
add_segment(&mut g.ext_lines, ext2_start, ext2_end);
|
||||
}
|
||||
|
||||
// DIMDLE: dim line overshoots the ext line by `dimdle` at each end,
|
||||
// but only when ticks are in use (DIMTSZ > 0). With arrowheads this
|
||||
// is ignored, matching AutoCAD.
|
||||
let dle = if params.ticks { params.dimdle } else { 0.0 };
|
||||
let dir_d1_to_d2 = normalized_or(d2 - d1, axis);
|
||||
let d1_out = d1 - dir_d1_to_d2 * dle;
|
||||
let d2_out = d2 + dir_d1_to_d2 * dle;
|
||||
// DIMSD1/DIMSD2: when *both* set, omit the dim line entirely. AutoCAD
|
||||
// splits at text otherwise — without that pivot info, leave as-is.
|
||||
let _ = params.dimsoxd; // DIMSOXD: only meaningful when text is auto-placed
|
||||
// outside the ext lines; we honour the saved
|
||||
// text_middle_point so this is a no-op for files.
|
||||
if !(suppress.dim1 && suppress.dim2) {
|
||||
add_segment(&mut g.dim_lines, d1, d2);
|
||||
add_segment(&mut g.dim_lines, d1_out, d2_out);
|
||||
}
|
||||
append_arrow(g, d1, normalized_or(d2 - d1, axis), arrow1);
|
||||
append_arrow(g, d2, normalized_or(d1 - d2, -axis), arrow2);
|
||||
}
|
||||
|
||||
/// Draw a center mark for radius/diameter dimensions.
|
||||
/// DIMCEN > 0 → small "+" of half-length |DIMCEN| at the centre.
|
||||
/// DIMCEN < 0 → small "+" *plus* four line segments extending from the
|
||||
/// circle (radius - |DIMCEN|) outward to (radius + |DIMCEN|).
|
||||
/// DIMCEN = 0 → no mark.
|
||||
fn append_center_mark(g: &mut DimGeom, center: Vec3, dimcen: f32, radius: f32) {
|
||||
let mag = dimcen.abs();
|
||||
if mag <= 1e-6 {
|
||||
return;
|
||||
}
|
||||
// Small "+" at the centre.
|
||||
let h = mag;
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x - h, center.y, center.z),
|
||||
Vec3::new(center.x + h, center.y, center.z),
|
||||
);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x, center.y - h, center.z),
|
||||
Vec3::new(center.x, center.y + h, center.z),
|
||||
);
|
||||
if dimcen < 0.0 && radius > mag + 1e-6 {
|
||||
let inner = (radius - mag).max(0.0);
|
||||
let outer = radius + mag;
|
||||
// Four short radial strokes spanning the circle edge.
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x + inner, center.y, center.z),
|
||||
Vec3::new(center.x + outer, center.y, center.z),
|
||||
);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x - inner, center.y, center.z),
|
||||
Vec3::new(center.x - outer, center.y, center.z),
|
||||
);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x, center.y + inner, center.z),
|
||||
Vec3::new(center.x, center.y + outer, center.z),
|
||||
);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
Vec3::new(center.x, center.y - inner, center.z),
|
||||
Vec3::new(center.x, center.y - outer, center.z),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_angular_dimension(
|
||||
g: &mut DimGeom,
|
||||
vertex: Vec3,
|
||||
|
|
@ -2134,40 +2432,113 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
|||
|
||||
fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
||||
let base = dim.base();
|
||||
let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_));
|
||||
|
||||
// Auto-generated body (the value AutoCAD would emit if the user did not
|
||||
// override it). Built first so user_text "<>" substitution can re-use it.
|
||||
let measured = match dim {
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => {
|
||||
format_angular_value(dim.measurement(), style)
|
||||
let primary = if is_angular {
|
||||
format_angular_value(dim.measurement(), style)
|
||||
} else {
|
||||
let v = format_linear_value(dim.measurement(), style);
|
||||
match dim {
|
||||
Dimension::Radius(_) => format!("R{}", v),
|
||||
Dimension::Diameter(_) => format!("Ø{}", v),
|
||||
_ => v,
|
||||
}
|
||||
Dimension::Radius(_) => {
|
||||
// R prefix is part of the conventional radial marker; DIMPOST wraps it.
|
||||
format!("R{}", format_linear_value(dim.measurement(), style))
|
||||
}
|
||||
Dimension::Diameter(_) => {
|
||||
format!("Ø{}", format_linear_value(dim.measurement(), style))
|
||||
}
|
||||
_ => format_linear_value(dim.measurement(), style),
|
||||
};
|
||||
|
||||
let wrapped = apply_dimpost(&measured, style);
|
||||
// Tolerances / limits — applied to the primary value before DIMPOST wraps.
|
||||
let primary = apply_tolerance(primary, dim.measurement(), style, is_angular);
|
||||
// DIMPOST: "<>" template (prefix/suffix wrap).
|
||||
let primary = apply_dimpost(&primary, style);
|
||||
|
||||
// Alternate units appended in brackets when DIMALT is on (linear only).
|
||||
let composed = if !is_angular {
|
||||
match alternate_units_text(dim.measurement(), style) {
|
||||
Some(alt) => format!("{} [{}]", primary, alt),
|
||||
None => primary,
|
||||
}
|
||||
} else {
|
||||
primary
|
||||
};
|
||||
|
||||
// Explicit user override (mtext-style "user_text") wins, but "<>" inside
|
||||
// it substitutes the measured value. " " (single space) suppresses text.
|
||||
if let Some(user_text) = &base.user_text {
|
||||
let trimmed = user_text;
|
||||
if trimmed.is_empty() {
|
||||
if user_text.is_empty() || user_text.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
if trimmed.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
return Some(trimmed.replace("<>", &measured));
|
||||
return Some(user_text.replace("<>", &composed));
|
||||
}
|
||||
if !base.text.trim().is_empty() {
|
||||
return Some(base.text.replace("<>", &measured));
|
||||
return Some(base.text.replace("<>", &composed));
|
||||
}
|
||||
Some(composed)
|
||||
}
|
||||
|
||||
/// Append tolerance text per DIMTOL / DIMLIM:
|
||||
/// DIMLIM true → replace value with "high/low" limits stacked
|
||||
/// DIMTOL true → "value +tp/-tm" (or "value±t" when tp==tm)
|
||||
fn apply_tolerance(
|
||||
value: String,
|
||||
measurement: f64,
|
||||
style: Option<&DimStyle>,
|
||||
is_angular: bool,
|
||||
) -> String {
|
||||
let Some(s) = style else { return value };
|
||||
let dimtdec = s.dimtdec.max(0) as usize;
|
||||
let dimtzin = s.dimtzin;
|
||||
let fmt = |v: f64| -> String {
|
||||
let raw = format!("{:.*}", dimtdec, v);
|
||||
apply_linear_zero_suppression(&raw, dimtzin)
|
||||
};
|
||||
if s.dimlim {
|
||||
// Replace value entirely with high/low limits.
|
||||
let high = measurement + s.dimtp;
|
||||
let low = measurement - s.dimtm;
|
||||
return format!("{}/{}", fmt(high), fmt(low));
|
||||
}
|
||||
if s.dimtol {
|
||||
let unit = if is_angular { "°" } else { "" };
|
||||
if (s.dimtp - s.dimtm).abs() < 1e-12 && s.dimtp.abs() > 1e-12 {
|
||||
return format!("{}±{}{}", value, fmt(s.dimtp), unit);
|
||||
}
|
||||
if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 {
|
||||
return format!(
|
||||
"{} +{}{} / -{}{}",
|
||||
value,
|
||||
fmt(s.dimtp),
|
||||
unit,
|
||||
fmt(s.dimtm),
|
||||
unit
|
||||
);
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Build the bracketed alternate-units suffix when DIMALT is enabled.
|
||||
fn alternate_units_text(measurement: f64, style: Option<&DimStyle>) -> Option<String> {
|
||||
let s = style?;
|
||||
if !s.dimalt {
|
||||
return None;
|
||||
}
|
||||
let mut v = measurement * s.dimaltf;
|
||||
if s.dimaltrnd > 1e-12 {
|
||||
v = (v / s.dimaltrnd).round() * s.dimaltrnd;
|
||||
}
|
||||
let dec = s.dimaltd.max(0) as usize;
|
||||
let raw = format_with_unit(v, s.dimaltu, dec, s.dimfrac);
|
||||
let suppressed = apply_linear_zero_suppression(&raw, s.dimaltz);
|
||||
let sep_swapped = swap_decimal_sep(&suppressed, s.dimdsep);
|
||||
// DIMAPOST wraps the alt value (same "<>" convention as DIMPOST).
|
||||
let wrapped = if s.dimapost.is_empty() {
|
||||
sep_swapped
|
||||
} else if s.dimapost.contains("<>") {
|
||||
s.dimapost.replace("<>", &sep_swapped)
|
||||
} else {
|
||||
format!("{}{}", sep_swapped, s.dimapost)
|
||||
};
|
||||
Some(wrapped)
|
||||
}
|
||||
|
||||
|
|
@ -2185,11 +2556,15 @@ fn apply_dimpost(value: &str, style: Option<&DimStyle>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP.
|
||||
/// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP, DIMLUNIT.
|
||||
fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String {
|
||||
let (dec, zin, lfac, rnd, dsep) = style
|
||||
.map(|s| (s.dimdec, s.dimzin, s.dimlfac, s.dimrnd, s.dimdsep))
|
||||
.unwrap_or((4, 8, 1.0, 0.0, 46));
|
||||
let (dec, zin, lfac, rnd, dsep, lunit, frac) = style
|
||||
.map(|s| {
|
||||
(
|
||||
s.dimdec, s.dimzin, s.dimlfac, s.dimrnd, s.dimdsep, s.dimlunit, s.dimfrac,
|
||||
)
|
||||
})
|
||||
.unwrap_or((4, 8, 1.0, 0.0, 46, 2, 0));
|
||||
|
||||
let lfac = if lfac.abs() < 1e-12 { 1.0 } else { lfac };
|
||||
let mut v = measurement * lfac;
|
||||
|
|
@ -2197,11 +2572,93 @@ fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String {
|
|||
v = (v / rnd).round() * rnd;
|
||||
}
|
||||
let dec = dec.max(0) as usize;
|
||||
let raw = format!("{:.*}", dec, v);
|
||||
let raw = format_with_unit(v, lunit, dec, frac);
|
||||
let suppressed = apply_linear_zero_suppression(&raw, zin);
|
||||
swap_decimal_sep(&suppressed, dsep)
|
||||
}
|
||||
|
||||
/// Dispatch on DIMLUNIT / DIMALTU.
|
||||
/// 1 = Scientific
|
||||
/// 2 = Decimal (default)
|
||||
/// 3 = Engineering (feet + decimal inches; 1 unit = 1 inch)
|
||||
/// 4 = Architectural (feet + fractional inches)
|
||||
/// 5 = Fractional (integer + fractional inches)
|
||||
/// 6 = Windows desktop → falls back to Decimal
|
||||
/// `dimfrac` controls denominator power for arch/fractional output (0/1/2);
|
||||
/// rendered inline as "n/d" (stacked glyphs require MText support).
|
||||
fn format_with_unit(value: f64, unit: i16, dec: usize, dimfrac: i16) -> String {
|
||||
match unit {
|
||||
1 => format!("{:.*e}", dec, value),
|
||||
3 => format_engineering(value, dec),
|
||||
4 => format_architectural(value, dimfrac),
|
||||
5 => format_fractional(value, dimfrac),
|
||||
_ => format!("{:.*}", dec, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_engineering(inches: f64, dec: usize) -> String {
|
||||
let sign = if inches < 0.0 { "-" } else { "" };
|
||||
let abs = inches.abs();
|
||||
let feet = (abs / 12.0).trunc();
|
||||
let rem_in = abs - feet * 12.0;
|
||||
format!("{}{:.0}'-{:.*}\"", sign, feet, dec, rem_in)
|
||||
}
|
||||
|
||||
fn format_architectural(inches: f64, dimfrac: i16) -> String {
|
||||
let sign = if inches < 0.0 { "-" } else { "" };
|
||||
let abs = inches.abs();
|
||||
let feet = (abs / 12.0).trunc();
|
||||
let rem_in_total = abs - feet * 12.0;
|
||||
let whole = rem_in_total.trunc();
|
||||
let frac = rem_in_total - whole;
|
||||
let frac_str = fraction_string(frac, dimfrac);
|
||||
if frac_str.is_empty() {
|
||||
format!("{}{:.0}'-{:.0}\"", sign, feet, whole)
|
||||
} else {
|
||||
format!("{}{:.0}'-{:.0} {}\"", sign, feet, whole, frac_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_fractional(value: f64, dimfrac: i16) -> String {
|
||||
let sign = if value < 0.0 { "-" } else { "" };
|
||||
let abs = value.abs();
|
||||
let whole = abs.trunc();
|
||||
let frac = abs - whole;
|
||||
let frac_str = fraction_string(frac, dimfrac);
|
||||
if frac_str.is_empty() {
|
||||
format!("{}{:.0}", sign, whole)
|
||||
} else if whole == 0.0 {
|
||||
format!("{}{}", sign, frac_str)
|
||||
} else {
|
||||
format!("{}{:.0} {}", sign, whole, frac_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn fraction_string(frac: f64, dimfrac: i16) -> String {
|
||||
// DIMFRAC denominator: AutoCAD encodes this on DIMSTYLE via DIMLUNIT
|
||||
// pairing — the value we accept is the *power-of-2* exponent (1..=6 ish).
|
||||
// Pick a sensible cap so the printed fraction stays readable.
|
||||
let exp = (dimfrac.clamp(0, 8) as u32).max(2) + 2; // 2..=10 → 4..=1024
|
||||
let denom = 1u64 << exp;
|
||||
let numer = (frac * denom as f64).round() as i64;
|
||||
if numer <= 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut n = numer as u64;
|
||||
let mut d = denom;
|
||||
while n % 2 == 0 && d % 2 == 0 {
|
||||
n /= 2;
|
||||
d /= 2;
|
||||
}
|
||||
if n == 0 {
|
||||
String::new()
|
||||
} else if d == 1 {
|
||||
format!("{}", n) // whole-number overflow back to caller
|
||||
} else {
|
||||
format!("{}/{}", n, d)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format an angular measurement (input in degrees as Dimension::measurement
|
||||
/// returns for angular variants) honouring DIMAUNIT, DIMADEC, DIMAZIN.
|
||||
fn format_angular_value(measurement_deg: f64, style: Option<&DimStyle>) -> String {
|
||||
|
|
@ -2395,21 +2852,30 @@ fn dimension_text_pos_f64(
|
|||
// 3=JIS. We honour 0 and 1; 2/3 fall back to "above".
|
||||
let dimtad = style.map(|s| s.dimtad).unwrap_or(1);
|
||||
let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0);
|
||||
if dimtad == 0 {
|
||||
return mid;
|
||||
}
|
||||
// Need a perpendicular vector inside the drawing plane for the offset.
|
||||
let (axis_x, axis_y, perp_sign) = match dim {
|
||||
// DIMJUST horizontal placement on the dim line (only meaningful for
|
||||
// linear/aligned dims). 0=centred, 1=near first ext, 2=near second ext,
|
||||
// 3=above first ext (perpendicular text), 4=above second ext.
|
||||
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
||||
// DIMTVP vertical-position multiplier (units of dimtxt). Only honoured
|
||||
// when DIMTAD == 0; offsets text perpendicular to the dim line.
|
||||
let dimtvp = style.map(|s| s.dimtvp).unwrap_or(0.0);
|
||||
|
||||
// Need axis + perp_sign (toward "above").
|
||||
let (axis_x, axis_y, perp_sign, p1, p2) = match dim {
|
||||
Dimension::Linear(d) => {
|
||||
let ax = d.rotation.cos();
|
||||
let ay = d.rotation.sin();
|
||||
// The sign-of-offset side (perp pointing FROM defpoints TOWARD dim line,
|
||||
// and past it = "above").
|
||||
let px = -ay;
|
||||
let py = ax;
|
||||
let off = (d.definition_point.x - d.first_point.x) * px
|
||||
+ (d.definition_point.y - d.first_point.y) * py;
|
||||
(ax, ay, if off >= 0.0 { 1.0 } else { -1.0 })
|
||||
(
|
||||
ax,
|
||||
ay,
|
||||
if off >= 0.0 { 1.0 } else { -1.0 },
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
)
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
let dx = d.second_point.x - d.first_point.x;
|
||||
|
|
@ -2421,18 +2887,55 @@ fn dimension_text_pos_f64(
|
|||
let py = ax;
|
||||
let off = (d.definition_point.x - d.first_point.x) * px
|
||||
+ (d.definition_point.y - d.first_point.y) * py;
|
||||
(ax, ay, if off >= 0.0 { 1.0 } else { -1.0 })
|
||||
(
|
||||
ax,
|
||||
ay,
|
||||
if off >= 0.0 { 1.0 } else { -1.0 },
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// Non-linear: only DIMTAD offset applies; no horizontal shift along axis.
|
||||
let off_perp = if dimtad == 0 {
|
||||
dimtvp * text_height
|
||||
} else {
|
||||
text_height * 0.5 + dimgap
|
||||
};
|
||||
return Vector3::new(mid.x, mid.y + off_perp * perp_sign_default(), mid.z);
|
||||
}
|
||||
_ => (1.0, 0.0, 1.0),
|
||||
};
|
||||
let offset = (text_height * 0.5 + dimgap) * perp_sign;
|
||||
|
||||
// Horizontal slide along the dim axis to honour DIMJUST. Slide endpoints
|
||||
// are the dim-line endpoints (projection of p1/p2 onto the dim line),
|
||||
// approximated here as the def-points themselves (we don't have axis-
|
||||
// projected d1/d2 here without more plumbing).
|
||||
let along_offset = match dimjust {
|
||||
1 => (p1.x - mid.x) * axis_x + (p1.y - mid.y) * axis_y,
|
||||
2 => (p2.x - mid.x) * axis_x + (p2.y - mid.y) * axis_y,
|
||||
3 => (p1.x - mid.x) * axis_x + (p1.y - mid.y) * axis_y,
|
||||
4 => (p2.x - mid.x) * axis_x + (p2.y - mid.y) * axis_y,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Perpendicular offset: DIMTAD 0 → DIMTVP * dimtxt, else above-line gap.
|
||||
let perp_offset = if dimtad == 0 {
|
||||
dimtvp * text_height * perp_sign
|
||||
} else {
|
||||
(text_height * 0.5 + dimgap) * perp_sign
|
||||
};
|
||||
|
||||
Vector3::new(
|
||||
mid.x + (-axis_y) * offset,
|
||||
mid.y + (axis_x) * offset,
|
||||
mid.x + axis_x * along_offset + (-axis_y) * perp_offset,
|
||||
mid.y + axis_y * along_offset + (axis_x) * perp_offset,
|
||||
mid.z,
|
||||
)
|
||||
}
|
||||
|
||||
fn perp_sign_default() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
||||
if v.length_squared() <= 1e-12 {
|
||||
fallback
|
||||
|
|
|
|||
Loading…
Reference in a new issue