forked from 1b-agent/opencadstudio-upstream
feat(dimension): bake full arrow shapes, arrow-fit, fixed-length ext lines
Completes the bake↔live dimension parity from the audit: - terminators now reproduce the style's actual arrow block: dim_metrics resolves arrow1/arrow2 via arrow_from_block (DIMTSZ tick, DIMBLK / DIMBLK1+DIMBLK2 per DIMSAH, else closed-filled), and dim_terminator bakes each kind (None, filled/blank triangle, tick, open, dot, origin, box, datum) as Line + SOLID — no more every arrow degrading to a filled triangle - short dimensions flip their arrows outside with stubs (DIMSOXD honoured) when the arrowheads don't fit between the extension lines (DIM-ARROWS-OUTSIDE) - DIMFXLON fixed-length extension lines via ext_endpoints (DIM-FXL) Refs #181. Remaining (annotative viewport scale at non-1:1, the ~1deg angular arrowhead-tangent cosmetic, and DIMTFILL text-break gap) are niche/cosmetic and tracked separately. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
58da76ff70
commit
12f4264183
1 file changed
+205
-42
@@ -343,31 +343,115 @@ fn dim_terminator(
|
||||
tip: Vector3,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
dimasz: f64,
|
||||
dimtsz: f64,
|
||||
arrow: &crate::scene::convert::tessellate::ArrowKind,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
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;
|
||||
use crate::scene::convert::tessellate::ArrowKind as A;
|
||||
let (dx, dy) = norm2(dx, dy, 1.0, 0.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)]
|
||||
// Point at `along` (down the dim line from the tip) and `perp` (sideways).
|
||||
let pt = |along: f64, perp: f64| {
|
||||
Vector3::new(tip.x + dx * along + px * perp, tip.y + dy * along + py * perp, tip.z)
|
||||
};
|
||||
let mut out: Vec<EntityType> = Vec::new();
|
||||
let tri = |a: Vector3, b: Vector3, c: Vector3, out: &mut Vec<EntityType>| {
|
||||
let mut s = acadrust::entities::Solid::triangle(a, b, c);
|
||||
s.common = common.clone();
|
||||
s.common.handle = Handle::NULL;
|
||||
out.push(EntityType::Solid(s));
|
||||
};
|
||||
match *arrow {
|
||||
A::None => {}
|
||||
A::Triangle { size, filled, size_mul } => {
|
||||
let size = (size * size_mul) as f64;
|
||||
let hw = size / 6.0;
|
||||
let (l, r) = (pt(size, hw), pt(size, -hw));
|
||||
if filled {
|
||||
tri(tip, l, r, &mut out);
|
||||
} else {
|
||||
out.push(dim_seg(tip, l, common));
|
||||
out.push(dim_seg(l, r, common));
|
||||
out.push(dim_seg(r, tip, common));
|
||||
}
|
||||
}
|
||||
A::Tick { size } => {
|
||||
let s = size as f64;
|
||||
let (ox, oy) = (dx + px, dy + py);
|
||||
let m = (ox * ox + oy * oy).sqrt().max(1e-9);
|
||||
let (ox, oy) = (ox / m * s, oy / m * s);
|
||||
out.push(dim_seg(
|
||||
Vector3::new(tip.x - ox, tip.y - oy, tip.z),
|
||||
Vector3::new(tip.x + ox, tip.y + oy, tip.z),
|
||||
common,
|
||||
));
|
||||
}
|
||||
A::Open { size, half_angle } => {
|
||||
let size = size as f64;
|
||||
let hw = size * (half_angle as f64).tan();
|
||||
out.push(dim_seg(tip, pt(size, hw), common));
|
||||
out.push(dim_seg(tip, pt(size, -hw), common));
|
||||
}
|
||||
A::Dot { size, filled } => {
|
||||
let r = size as f64 * 0.5;
|
||||
terminator_circle(tip, r, filled, common, &mut out);
|
||||
}
|
||||
A::Origin { size } => {
|
||||
terminator_circle(tip, size as f64 * 0.25, true, common, &mut out);
|
||||
let half = size as f64 * 0.5;
|
||||
out.push(dim_seg(pt(0.0, -half), pt(0.0, half), common));
|
||||
}
|
||||
A::Box_ { size, filled } => {
|
||||
let h = size as f64 * 0.5;
|
||||
let (p1, p2, p3, p4) = (pt(-h, -h), pt(h, -h), pt(h, h), pt(-h, h));
|
||||
out.push(dim_seg(p1, p2, common));
|
||||
out.push(dim_seg(p2, p3, common));
|
||||
out.push(dim_seg(p3, p4, common));
|
||||
out.push(dim_seg(p4, p1, common));
|
||||
if filled {
|
||||
tri(p1, p2, p3, &mut out);
|
||||
tri(p1, p3, p4, &mut out);
|
||||
}
|
||||
}
|
||||
A::Datum { size, filled } => {
|
||||
let half = size as f64 * 0.5;
|
||||
let (ba, bb, apex) = (pt(0.0, half), pt(0.0, -half), pt(size as f64, 0.0));
|
||||
out.push(dim_seg(ba, apex, common));
|
||||
out.push(dim_seg(apex, bb, common));
|
||||
out.push(dim_seg(bb, ba, common));
|
||||
if filled {
|
||||
tri(ba, apex, bb, &mut out);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A dot terminator: a 16-gon ring outline, optionally filled with a triangle fan.
|
||||
fn terminator_circle(
|
||||
center: Vector3,
|
||||
r: f64,
|
||||
filled: bool,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
out: &mut Vec<EntityType>,
|
||||
) {
|
||||
const N: usize = 16;
|
||||
let ring: Vec<Vector3> = (0..=N)
|
||||
.map(|i| {
|
||||
let a = i as f64 * std::f64::consts::TAU / N as f64;
|
||||
Vector3::new(center.x + a.cos() * r, center.y + a.sin() * r, center.z)
|
||||
})
|
||||
.collect();
|
||||
for w in ring.windows(2) {
|
||||
out.push(dim_seg(w[0], w[1], common));
|
||||
}
|
||||
if filled {
|
||||
for i in 0..N {
|
||||
let mut s = acadrust::entities::Solid::triangle(center, ring[i], ring[i + 1]);
|
||||
s.common = common.clone();
|
||||
s.common.handle = Handle::NULL;
|
||||
out.push(EntityType::Solid(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Center-mark cross at `center`, arm length |DIMCEN|. Empty when DIMCEN == 0.
|
||||
@@ -438,15 +522,23 @@ struct DimMetrics {
|
||||
dimexe: f64,
|
||||
dimtsz: f64,
|
||||
dimdle: f64,
|
||||
dimfxl: f64,
|
||||
dimfxlon: bool,
|
||||
dimse1: bool,
|
||||
dimse2: bool,
|
||||
dimsd1: bool,
|
||||
dimsd2: bool,
|
||||
dimsoxd: bool,
|
||||
dimclrd: i16,
|
||||
dimclre: i16,
|
||||
dimclrt: i16,
|
||||
dimlwd: i16,
|
||||
dimlwe: i16,
|
||||
/// Resolved terminator shapes for the first / second end (DIMTSZ tick,
|
||||
/// DIMBLK/DIMBLK1/DIMBLK2 per DIMSAH, else closed-filled), so the bake
|
||||
/// reproduces the style's actual arrow type.
|
||||
arrow1: crate::scene::convert::tessellate::ArrowKind,
|
||||
arrow2: crate::scene::convert::tessellate::ArrowKind,
|
||||
}
|
||||
|
||||
/// Metrics from the dim's style, mirroring what the live renderer applies, so a
|
||||
@@ -464,22 +556,48 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
|
||||
let scale = style
|
||||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 })
|
||||
.unwrap_or(1.0);
|
||||
use crate::scene::convert::tessellate::{arrow_from_block, ArrowKind};
|
||||
let dimasz = style.map(|s| s.dimasz * scale).unwrap_or(0.18 * scale).max(1e-6);
|
||||
let dimtsz = style.map(|s| s.dimtsz * scale).unwrap_or(0.0);
|
||||
let asz = dimasz as f32;
|
||||
// Resolve the terminator shapes exactly like the live render: ticks when
|
||||
// DIMTSZ>0, otherwise the DIMBLK / DIMBLK1+DIMBLK2 (per DIMSAH) arrow blocks,
|
||||
// else closed-filled.
|
||||
let (arrow1, arrow2) = if dimtsz > 1e-9 {
|
||||
let t = ArrowKind::Tick { size: dimtsz as f32 };
|
||||
(t, t)
|
||||
} else if let Some(s) = style {
|
||||
if s.dimsah {
|
||||
(arrow_from_block(doc, s.dimblk1, asz), arrow_from_block(doc, s.dimblk2, asz))
|
||||
} else {
|
||||
let a = arrow_from_block(doc, s.dimblk, asz);
|
||||
(a, a)
|
||||
}
|
||||
} else {
|
||||
let a = ArrowKind::Triangle { size: asz, filled: true, size_mul: 1.0 };
|
||||
(a, a)
|
||||
};
|
||||
DimMetrics {
|
||||
dimasz: style.map(|s| s.dimasz * scale).unwrap_or(0.18 * scale).max(1e-6),
|
||||
dimasz,
|
||||
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),
|
||||
dimtsz,
|
||||
dimdle: style.map(|s| s.dimdle * scale).unwrap_or(0.0),
|
||||
dimfxl: style.map(|s| s.dimfxl * scale).unwrap_or(1.0),
|
||||
dimfxlon: style.map(|s| s.dimfxlon).unwrap_or(false),
|
||||
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),
|
||||
dimsoxd: style.map(|s| s.dimsoxd).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),
|
||||
arrow1,
|
||||
arrow2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,8 +657,8 @@ fn angular_block_segs(
|
||||
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.extend(dim_terminator(e1, -a1.sin() * sgn, a1.cos() * sgn, &met.arrow1, dim_c));
|
||||
out.extend(dim_terminator(e2, a2.sin() * sgn, -a2.cos() * sgn, &met.arrow2, dim_c));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -559,6 +677,40 @@ fn dim_text_anchor(
|
||||
}
|
||||
}
|
||||
|
||||
/// The (start, end) of a linear/aligned extension line. `origin` is the
|
||||
/// measured point, `land` its landing on the dim line, `sign` the perpendicular
|
||||
/// direction, `(edx,edy)` the (possibly oblique) extension direction. Normally
|
||||
/// the line runs from `origin + DIMEXO` to `land + DIMEXE`; with DIMFXLON it is
|
||||
/// a fixed length DIMFXL back from the dim line instead. DIM-FXL.
|
||||
fn ext_endpoints(
|
||||
origin: Vector3,
|
||||
land: Vector3,
|
||||
sign: f64,
|
||||
edx: f64,
|
||||
edy: f64,
|
||||
met: &DimMetrics,
|
||||
) -> (Vector3, Vector3) {
|
||||
let end = Vector3::new(
|
||||
land.x + edx * (sign * met.dimexe),
|
||||
land.y + edy * (sign * met.dimexe),
|
||||
land.z,
|
||||
);
|
||||
let start = if met.dimfxlon {
|
||||
Vector3::new(
|
||||
land.x - edx * (sign * met.dimfxl),
|
||||
land.y - edy * (sign * met.dimfxl),
|
||||
land.z,
|
||||
)
|
||||
} else {
|
||||
Vector3::new(
|
||||
origin.x + edx * (sign * met.dimexo),
|
||||
origin.y + edy * (sign * met.dimexo),
|
||||
origin.z,
|
||||
)
|
||||
};
|
||||
(start, end)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@@ -596,16 +748,32 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
|
||||
// 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.
|
||||
// halves are suppressed. When the arrows don't fit between the extension
|
||||
// lines (gap < 2·DIMASZ, arrows only) they flip to the outside with short
|
||||
// stubs unless DIMSOXD — matching the live fit logic. DIM-ARROWS-OUTSIDE.
|
||||
let push_dim_line = |result: &mut Vec<EntityType>, d1: Vector3, d2: Vector3, ux: f64, uy: f64| {
|
||||
let ticks = met.dimtsz > 1e-9;
|
||||
if !(met.dimsd1 && met.dimsd2) {
|
||||
let dle = if met.dimtsz > 1e-9 { met.dimdle } else { 0.0 };
|
||||
let dle = if ticks { 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));
|
||||
let gap = ((d2.x - d1.x).powi(2) + (d2.y - d1.y).powi(2)).sqrt();
|
||||
let outside = !ticks && met.dimasz > 1e-6 && gap < 2.0 * met.dimasz;
|
||||
if outside {
|
||||
// Arrow bodies extend outward (tips still at the origins).
|
||||
result.extend(dim_terminator(d1, -ux, -uy, &met.arrow1, &dim_c));
|
||||
result.extend(dim_terminator(d2, ux, uy, &met.arrow2, &dim_c));
|
||||
if !met.dimsoxd {
|
||||
let stub = 2.0 * met.dimasz;
|
||||
result.push(make_seg(&v3(d1.x - ux * stub, d1.y - uy * stub, d1.z), &d1, &dim_c));
|
||||
result.push(make_seg(&d2, &v3(d2.x + ux * stub, d2.y + uy * stub, d2.z), &dim_c));
|
||||
}
|
||||
} else {
|
||||
result.extend(dim_terminator(d1, ux, uy, &met.arrow1, &dim_c));
|
||||
result.extend(dim_terminator(d2, -ux, -uy, &met.arrow2, &dim_c));
|
||||
}
|
||||
};
|
||||
|
||||
match dim {
|
||||
@@ -627,10 +795,8 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
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);
|
||||
let (e1a, e1b) = ext_endpoints(d.first_point, d1, s, edx, edy, &met);
|
||||
let (e2a, e2b) = ext_endpoints(d.second_point, d2, s, edx, edy, &met);
|
||||
if !met.dimse1 {
|
||||
result.push(make_seg(&e1a, &e1b, &ext_c));
|
||||
}
|
||||
@@ -665,10 +831,8 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
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);
|
||||
let (e1a, e1b) = ext_endpoints(d.first_point, d1, s1, edx, edy, &met);
|
||||
let (e2a, e2b) = ext_endpoints(d.second_point, d2, s2, edx, edy, &met);
|
||||
if !met.dimse1 {
|
||||
result.push(make_seg(&e1a, &e1b, &ext_c));
|
||||
}
|
||||
@@ -689,8 +853,7 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
point,
|
||||
(center.x - point.x) / len,
|
||||
(center.y - point.y) / len,
|
||||
met.dimasz,
|
||||
met.dimtsz,
|
||||
&met.arrow1,
|
||||
&dim_c,
|
||||
));
|
||||
// Leader from the arrow tip toward the text — to the text anchor when
|
||||
@@ -716,8 +879,8 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
.sqrt()
|
||||
.max(1e-12);
|
||||
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));
|
||||
result.extend(dim_terminator(edge, -ux, -uy, &met.arrow1, &dim_c));
|
||||
result.extend(dim_terminator(far, ux, uy, &met.arrow2, &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);
|
||||
|
||||
Reference in new issue
Block a user