forked from 1b-agent/opencadstudio-upstream
fix(dimension): bake↔live consistency, edits, baseline/continue, grips
Batch 1-3 of the dimension audit (19 issues). The dominant class was the baked *D block diverging from the live render: a reloaded dimension renders from that block, so on save+reopen it silently changed. Bake now matches the live render: - text value/format via the shared style-resolved path (DIMDEC/DIMLFAC/ DIMPOST/units/<>), correct height + rotation ladder, suppression - angular dims bake their swept arc (no longer collapse to a V) - diameter draws edge-to-edge through the centre (live + bake), centre mark - radius/linear/aligned bake arrowheads - group-10 (base.definition_point) synced from the edited geometry so an edited dim no longer shifts on reload Editing / properties: - aligned no longer corrupts ext_line_rotation when a coordinate is edited - DIMTEDIT pins the moved text (text_user_positioned) - angle properties shown/edited in degrees - non-linear text respects the user-positioned gate Commands / geometry: - DIMBASELINE stacks outward (not toward the points) and its preview no longer tilts over non-level points - DIMCONTINUE/DIMBASELINE inherit the base dim's text rotation (UCS chains) - angular extension lines reach the arc (no gap) - text grip sits on the text for auto-placed dims instead of the origin Refs #181. (acadrust DXF round-trip + QDIM/DIMBREAK/DIMJOGLINE/DIMSPACE/ DIMEDIT to follow.) Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
b691854e7a
commit
3c5aa4453a
7 files changed
+591
-131
No files matched your search
+10
-8
@@ -75,9 +75,10 @@ impl OpenCADStudio {
|
||||
|
||||
"DIMCONTINUE" | "DCO" => {
|
||||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||||
let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene)
|
||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||||
find_last_linear_dim(&self.tabs[i].scene)
|
||||
{
|
||||
DimContinueCommand::from_base(p1, p2, dp, rot)
|
||||
DimContinueCommand::from_base(p1, p2, dp, rot, trot)
|
||||
} else {
|
||||
DimContinueCommand::new()
|
||||
};
|
||||
@@ -87,7 +88,8 @@ impl OpenCADStudio {
|
||||
|
||||
"DIMBASELINE" | "DBA" => {
|
||||
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
||||
let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene)
|
||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||||
find_last_linear_dim(&self.tabs[i].scene)
|
||||
{
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let dimdli = doc
|
||||
@@ -99,7 +101,7 @@ impl OpenCADStudio {
|
||||
})
|
||||
.map(|s| s.dimdli as f32)
|
||||
.unwrap_or(1.5);
|
||||
DimBaselineCommand::from_base(p1, p2, dp, rot, dimdli)
|
||||
DimBaselineCommand::from_base(p1, p2, dp, rot, trot, dimdli)
|
||||
} else {
|
||||
DimBaselineCommand::new()
|
||||
};
|
||||
@@ -502,10 +504,10 @@ impl OpenCADStudio {
|
||||
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
||||
fn find_last_linear_dim(
|
||||
scene: &crate::scene::Scene,
|
||||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64)> {
|
||||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> {
|
||||
use acadrust::entities::Dimension;
|
||||
let mut best_handle: u64 = 0;
|
||||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64)> = None;
|
||||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None;
|
||||
|
||||
for entity in scene.document.entities() {
|
||||
if let acadrust::EntityType::Dimension(dim) = entity {
|
||||
@@ -530,7 +532,7 @@ fn find_last_linear_dim(
|
||||
d.base.definition_point.y as f32,
|
||||
d.base.definition_point.z as f32,
|
||||
);
|
||||
Some((p1, p2, dp, d.rotation))
|
||||
Some((p1, p2, dp, d.rotation, d.base.text_rotation))
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
let p1 = glam::Vec3::new(
|
||||
@@ -551,7 +553,7 @@ fn find_last_linear_dim(
|
||||
let dx = (d.second_point.x - d.first_point.x) as f32;
|
||||
let dy = (d.second_point.y - d.first_point.y) as f32;
|
||||
let rot = dy.atan2(dx) as f64;
|
||||
Some((p1, p2, dp, rot))
|
||||
Some((p1, p2, dp, rot, d.base.text_rotation))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
+126
-59
@@ -34,11 +34,15 @@ fn base_props(base: &DimensionBase) -> Vec<crate::scene::model::object::Property
|
||||
edit("Text X", "text_x", base.text_middle_point.x),
|
||||
edit("Text Y", "text_y", base.text_middle_point.y),
|
||||
edit("Text Z", "text_z", base.text_middle_point.z),
|
||||
edit("Text Rotation", "text_rotation", base.text_rotation),
|
||||
edit(
|
||||
"Horizontal Dir",
|
||||
"Text Rotation (deg)",
|
||||
"text_rotation",
|
||||
base.text_rotation.to_degrees(),
|
||||
),
|
||||
edit(
|
||||
"Horizontal Dir (deg)",
|
||||
"horizontal_direction",
|
||||
base.horizontal_direction,
|
||||
base.horizontal_direction.to_degrees(),
|
||||
),
|
||||
edit(
|
||||
"Line Spacing",
|
||||
@@ -63,9 +67,9 @@ fn properties(dim: &Dimension) -> PropSection {
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"Ext Rotation (deg)",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
d.ext_line_rotation.to_degrees(),
|
||||
));
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
@@ -74,11 +78,11 @@ fn properties(dim: &Dimension) -> PropSection {
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit("Rotation", "rotation", d.rotation));
|
||||
props.push(edit("Rotation (deg)", "rotation", d.rotation.to_degrees()));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"Ext Rotation (deg)",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
d.ext_line_rotation.to_degrees(),
|
||||
));
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
@@ -217,8 +221,8 @@ fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
|
||||
base.text_user_positioned = true;
|
||||
assign_f64(value, &mut base.text_middle_point.z)
|
||||
}
|
||||
"text_rotation" => assign_f64(value, &mut base.text_rotation),
|
||||
"horizontal_direction" => assign_f64(value, &mut base.horizontal_direction),
|
||||
"text_rotation" => assign_deg(value, &mut base.text_rotation),
|
||||
"horizontal_direction" => assign_deg(value, &mut base.horizontal_direction),
|
||||
"line_spacing_factor" => assign_f64(value, &mut base.line_spacing_factor),
|
||||
_ => false,
|
||||
}
|
||||
@@ -232,6 +236,17 @@ fn assign_f64(value: &str, target: &mut f64) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Parse a value entered in DEGREES and store it as radians. Dimension angle
|
||||
/// fields are kept in radians internally but shown/edited in degrees, matching
|
||||
/// arc.rs / text.rs so the properties panel reads consistently.
|
||||
fn assign_deg(value: &str, target: &mut f64) -> bool {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return false;
|
||||
};
|
||||
*target = v.to_radians();
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_geom_prop(dim: &mut Dimension, field: &str, value: &str) {
|
||||
if apply_base_prop(dim.base_mut(), field, value) {
|
||||
return;
|
||||
@@ -256,7 +271,11 @@ fn apply_linear_fields_aligned(d: &mut DimensionAligned, field: &str, value: &st
|
||||
field,
|
||||
value,
|
||||
);
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
// Only the oblique field touches ext_line_rotation — otherwise editing a
|
||||
// coordinate (e.g. First X) would corrupt the dimension's obliquing. #181.
|
||||
if field == "ext_line_rotation" {
|
||||
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str) {
|
||||
@@ -269,10 +288,10 @@ fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str)
|
||||
);
|
||||
match field {
|
||||
"rotation" => {
|
||||
let _ = assign_f64(value, &mut d.rotation);
|
||||
let _ = assign_deg(value, &mut d.rotation);
|
||||
}
|
||||
"ext_line_rotation" => {
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -604,7 +623,17 @@ fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) {
|
||||
|
||||
impl Grippable for Dimension {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
let text = dv3(&self.base().text_middle_point);
|
||||
// Auto-placed dimensions carry a zero text_middle_point sentinel; put
|
||||
// the text grip at the style-default placement (default metrics) instead
|
||||
// of the world origin, so it stays on the visible text and grabbable.
|
||||
let text = {
|
||||
let p = self.base().text_middle_point;
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
||||
dv3(&p)
|
||||
} else {
|
||||
dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0))
|
||||
}
|
||||
};
|
||||
match self {
|
||||
Dimension::Linear(d) => vec![
|
||||
square_grip(0, dv3(&d.first_point)),
|
||||
@@ -1557,23 +1586,27 @@ fn dimension_geometry(
|
||||
append_center_mark(&mut g, center, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
let p1 = lv(d.angle_vertex);
|
||||
let p2 = lv(d.definition_point);
|
||||
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);
|
||||
// DIMETER leader: continue past p2 toward the text.
|
||||
// angle_vertex is the circle centre and definition_point a point on
|
||||
// the circle. The diameter line runs edge-to-edge THROUGH the centre
|
||||
// (far edge → near edge), with arrows pointing inward at each edge.
|
||||
let center = lv(d.angle_vertex);
|
||||
let edge = lv(d.definition_point);
|
||||
let far = center * 2.0 - edge;
|
||||
add_segment(&mut g.dim_lines, far, edge);
|
||||
append_arrow(&mut g, edge, normalized_or(far - edge, Vec3::X), arrow1);
|
||||
append_arrow(&mut g, far, normalized_or(edge - far, Vec3::X), arrow2);
|
||||
// Diameter leader: continue past the near edge toward the text.
|
||||
if d.leader_length.abs() > 1e-9 {
|
||||
let text = dimension_text_position(dim);
|
||||
let leader_dir = normalized_or(text - p2, p2 - p1);
|
||||
let leader_dir = normalized_or(text - edge, edge - far);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
p2,
|
||||
p2 + leader_dir * (d.leader_length as f32),
|
||||
edge,
|
||||
edge + leader_dir * (d.leader_length as f32),
|
||||
);
|
||||
}
|
||||
let radius = (p2 - p1).length() * 0.5;
|
||||
append_center_mark(&mut g, (p1 + p2) * 0.5, params.dimcen, radius);
|
||||
let radius = (edge - center).length();
|
||||
append_center_mark(&mut g, center, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
append_angular_dimension(
|
||||
@@ -1783,13 +1816,18 @@ fn append_angular_dimension(
|
||||
arrow1: &ArrowKind,
|
||||
arrow2: &ArrowKind,
|
||||
) {
|
||||
add_segment(&mut g.ext_lines, vertex, first);
|
||||
add_segment(&mut g.ext_lines, vertex, second);
|
||||
|
||||
let radius = vertex.distance(arc_point);
|
||||
if radius <= 1e-6 {
|
||||
add_segment(&mut g.ext_lines, vertex, first);
|
||||
add_segment(&mut g.ext_lines, vertex, second);
|
||||
return;
|
||||
}
|
||||
// Extension lines run from each measured point out to the dimension arc so
|
||||
// there is no gap between a ray and its arc endpoint. (#181 / DIM-027)
|
||||
let dir1 = normalized_or(first - vertex, Vec3::X);
|
||||
let dir2 = normalized_or(second - vertex, Vec3::X);
|
||||
add_segment(&mut g.ext_lines, first, vertex + dir1 * radius);
|
||||
add_segment(&mut g.ext_lines, second, vertex + dir2 * radius);
|
||||
|
||||
let start = (first.y - vertex.y).atan2(first.x - vertex.x);
|
||||
let mut end = (second.y - vertex.y).atan2(second.x - vertex.x);
|
||||
@@ -1936,29 +1974,7 @@ fn dimension_text_entity(
|
||||
let pos_f64 = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||||
let base = dim.base();
|
||||
|
||||
// DIMTIH/DIMTOH: when set, text is forced horizontal (rotation = 0)
|
||||
// regardless of the dim line angle. Honour explicit base.text_rotation
|
||||
// first, then horizontal_direction override, then DIMTIH/DIMTOH.
|
||||
let dimtih = style.map(|s| s.dimtih).unwrap_or(false);
|
||||
let dimtoh = style.map(|s| s.dimtoh).unwrap_or(false);
|
||||
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
||||
let rotation = if base.text_rotation.abs() > 1e-9 {
|
||||
base.text_rotation
|
||||
} else if base.horizontal_direction.abs() > 1e-9 {
|
||||
// horizontal_direction is the in-plane reading direction the writer
|
||||
// baked in (used for vertical / oblique dims).
|
||||
base.horizontal_direction
|
||||
} else if dimtih || dimtoh {
|
||||
0.0
|
||||
} else {
|
||||
let mut r = dimension_text_natural_rotation(dim);
|
||||
// DIMJUST 3/4: text reads along the extension lines, i.e. rotated 90°
|
||||
// from the dimension line.
|
||||
if dimjust == 3 || dimjust == 4 {
|
||||
r += std::f64::consts::FRAC_PI_2;
|
||||
}
|
||||
r
|
||||
};
|
||||
let rotation = dimension_text_rotation(dim, style);
|
||||
|
||||
// Text style resolution priority:
|
||||
// 1. DIMTXSTY by handle (most reliable; survives rename)
|
||||
@@ -2056,6 +2072,31 @@ fn attachment_to_text_align(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reading rotation for a dimension's measurement text, resolving the style
|
||||
/// flags the same way the live renderer does: explicit text_rotation, then
|
||||
/// horizontal_direction, then DIMTIH/DIMTOH force-horizontal, else the natural
|
||||
/// dim-line angle (+90° for DIMJUST 3/4). Shared by the live text entity and
|
||||
/// the bake so a reloaded dimension's text reads at the same angle. (#181)
|
||||
fn dimension_text_rotation(dim: &Dimension, style: Option<&DimStyle>) -> f64 {
|
||||
let base = dim.base();
|
||||
let dimtih = style.map(|s| s.dimtih).unwrap_or(false);
|
||||
let dimtoh = style.map(|s| s.dimtoh).unwrap_or(false);
|
||||
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
||||
if base.text_rotation.abs() > 1e-9 {
|
||||
base.text_rotation
|
||||
} else if base.horizontal_direction.abs() > 1e-9 {
|
||||
base.horizontal_direction
|
||||
} else if dimtih || dimtoh {
|
||||
0.0
|
||||
} else {
|
||||
let mut r = dimension_text_natural_rotation(dim);
|
||||
if dimjust == 3 || dimjust == 4 {
|
||||
r += std::f64::consts::FRAC_PI_2;
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
||||
let angle = match dim {
|
||||
Dimension::Linear(d) => d.rotation,
|
||||
@@ -2077,7 +2118,7 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
||||
pub(crate) fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
||||
let (main, tol) = dimension_text_parts(dim, style)?;
|
||||
// Tolerance is appended inline for callers (e.g. fill rect width) that
|
||||
// don't render a separate tolerance entity. The visual pipeline that
|
||||
@@ -2731,13 +2772,12 @@ fn dimension_text_pos_f64(
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// Non-linear (radius / diameter / angular / ordinate): keep the
|
||||
// saved text point when present for fidelity, else lift the natural
|
||||
// mid point straight up by the style offset.
|
||||
let p = base.text_middle_point;
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
||||
return p;
|
||||
}
|
||||
// Non-linear (radius / diameter / angular / ordinate): lift the
|
||||
// natural mid point straight up by the style offset. A user-dragged
|
||||
// text point is already returned by the `use_saved` gate above, so
|
||||
// we must NOT short-circuit on a merely-nonzero text_middle_point
|
||||
// here — that would ignore the style placement for auto-placed dims
|
||||
// and make a re-style a no-op. (#181)
|
||||
let mid = match dim {
|
||||
Dimension::Radius(d) => Vector3::new(
|
||||
(d.angle_vertex.x + d.definition_point.x) * 0.5,
|
||||
@@ -2763,6 +2803,33 @@ 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(
|
||||
dim: &Dimension,
|
||||
document: &CadDocument,
|
||||
anno_scale: f64,
|
||||
) -> Option<(String, Vector3, f64, f64)> {
|
||||
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))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dimtad_tests {
|
||||
use super::text_on_dim_line;
|
||||
|
||||
@@ -34,6 +34,9 @@ pub struct DimBaselineCommand {
|
||||
base_p1: Vec3,
|
||||
/// Direction along the measurement direction (0.0 = horizontal, PI/2 = vertical).
|
||||
rotation: f64,
|
||||
/// Text reading rotation inherited from the base dim (keeps a UCS-aligned
|
||||
/// chain's text consistent with the originating DIMLINEAR). 0 = natural.
|
||||
text_rotation: f64,
|
||||
/// Unit vector perpendicular to the dimension axis, pointing toward the dim line side.
|
||||
perp: Vec3,
|
||||
/// Perpendicular distance of the NEXT dimension line from the extension-line axis.
|
||||
@@ -50,6 +53,7 @@ impl DimBaselineCommand {
|
||||
Self {
|
||||
base_p1: Vec3::ZERO,
|
||||
rotation: 0.0,
|
||||
text_rotation: 0.0,
|
||||
perp: Vec3::Y,
|
||||
next_offset: DEFAULT_DIMDLI,
|
||||
dimdli: DEFAULT_DIMDLI,
|
||||
@@ -69,6 +73,7 @@ impl DimBaselineCommand {
|
||||
_p2: Vec3,
|
||||
definition_point: Vec3,
|
||||
rotation: f64,
|
||||
text_rotation: f64,
|
||||
dimdli: f32,
|
||||
) -> Self {
|
||||
// Measurement axis = the base dim's rotation angle (any angle, incl. a
|
||||
@@ -79,13 +84,19 @@ impl DimBaselineCommand {
|
||||
let dimdli = if dimdli.abs() < 1e-6 {
|
||||
DEFAULT_DIMDLI
|
||||
} else {
|
||||
dimdli
|
||||
dimdli.abs()
|
||||
};
|
||||
// Next baseline dim goes one DIMDLI further from the baseline.
|
||||
// Stack each baseline outward — in the SIGN direction of the base dim's
|
||||
// own offset. When the base dim sits on the -perp side (below / left of
|
||||
// the points) a positive increment would march the stack back toward and
|
||||
// across the points; the signed increment keeps it moving away. (#181)
|
||||
let dir = if base_offset >= 0.0 { 1.0 } else { -1.0 };
|
||||
let dimdli = dimdli * dir;
|
||||
let next_offset = base_offset + dimdli;
|
||||
Self {
|
||||
base_p1: p1,
|
||||
rotation,
|
||||
text_rotation,
|
||||
perp,
|
||||
next_offset,
|
||||
dimdli,
|
||||
@@ -94,6 +105,19 @@ impl DimBaselineCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl DimBaselineCommand {
|
||||
/// The two dimension-line endpoints for the new baseline at `p2`: the fixed
|
||||
/// base origin and `p2` each projected INDEPENDENTLY onto the dim line (at
|
||||
/// the current stacking offset). Projecting both keeps the line straight
|
||||
/// even when the two origins aren't level — a shared offset tilts it. (#181)
|
||||
fn dim_line_pts(&self, p2: Vec3) -> (Vec3, Vec3) {
|
||||
let target = self.base_p1.dot(self.perp) + self.next_offset;
|
||||
let d1 = self.base_p1 + self.perp * (target - self.base_p1.dot(self.perp));
|
||||
let d2 = p2 + self.perp * (target - p2.dot(self.perp));
|
||||
(d1, d2)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimBaselineCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMBASELINE"
|
||||
@@ -117,9 +141,11 @@ impl CadCommand for DimBaselineCommand {
|
||||
// Build a new linear dimension.
|
||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
||||
dim.rotation = self.rotation;
|
||||
if self.text_rotation.abs() > 1e-9 {
|
||||
dim.base.text_rotation = self.text_rotation;
|
||||
}
|
||||
|
||||
let dim_line_pt = p1 + self.perp * self.next_offset;
|
||||
let dim_line_pt2 = p2 + self.perp * self.next_offset;
|
||||
let (dim_line_pt, dim_line_pt2) = self.dim_line_pts(p2);
|
||||
dim.definition_point = v3(dim_line_pt);
|
||||
dim.base.definition_point = v3(dim_line_pt);
|
||||
dim.base.text_middle_point = v3((dim_line_pt + dim_line_pt2) * 0.5);
|
||||
@@ -141,8 +167,7 @@ impl CadCommand for DimBaselineCommand {
|
||||
return None;
|
||||
}
|
||||
let p1 = self.base_p1;
|
||||
let dim_line_pt = p1 + self.perp * self.next_offset;
|
||||
let dim_line_pt2 = pt + self.perp * self.next_offset;
|
||||
let (dim_line_pt, dim_line_pt2) = self.dim_line_pts(pt);
|
||||
Some(WireModel {
|
||||
name: "dimbase_preview".into(),
|
||||
points: vec![
|
||||
|
||||
@@ -31,6 +31,9 @@ pub struct DimContinueCommand {
|
||||
chain_p1: Vec3,
|
||||
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
|
||||
rotation: f64,
|
||||
/// Text reading rotation inherited from the base dim so a UCS-aligned chain
|
||||
/// keeps its text consistent with the originating DIMLINEAR. 0 = natural.
|
||||
text_rotation: f64,
|
||||
/// Absolute perpendicular coordinate (dot with `perp`) of the base
|
||||
/// dimension line. Each continued dim line is projected onto this exact
|
||||
/// coordinate so the whole chain stays collinear — even when the extension
|
||||
@@ -49,6 +52,7 @@ impl DimContinueCommand {
|
||||
Self {
|
||||
chain_p1: Vec3::ZERO,
|
||||
rotation: 0.0,
|
||||
text_rotation: 0.0,
|
||||
dim_line_perp: 0.0,
|
||||
perp: Vec3::Y,
|
||||
ready: false,
|
||||
@@ -64,7 +68,13 @@ impl DimContinueCommand {
|
||||
/// `definition_point` — where the dim line was placed; its perpendicular
|
||||
/// coordinate is the line the whole chain stays collinear with.
|
||||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
||||
pub fn from_base(_p1: Vec3, p2: Vec3, definition_point: Vec3, rotation: f64) -> Self {
|
||||
pub fn from_base(
|
||||
_p1: Vec3,
|
||||
p2: Vec3,
|
||||
definition_point: Vec3,
|
||||
rotation: f64,
|
||||
text_rotation: f64,
|
||||
) -> Self {
|
||||
// Axis unit vector along the measurement direction — the base dim's
|
||||
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
|
||||
let axis = Vec3::new(rotation.cos() as f32, rotation.sin() as f32, 0.0);
|
||||
@@ -74,6 +84,7 @@ impl DimContinueCommand {
|
||||
Self {
|
||||
chain_p1: p2,
|
||||
rotation,
|
||||
text_rotation,
|
||||
dim_line_perp,
|
||||
perp,
|
||||
ready: true,
|
||||
@@ -104,6 +115,9 @@ impl CadCommand for DimContinueCommand {
|
||||
// Build a new linear dimension.
|
||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
||||
dim.rotation = self.rotation;
|
||||
if self.text_rotation.abs() > 1e-9 {
|
||||
dim.base.text_rotation = self.text_rotation;
|
||||
}
|
||||
|
||||
// Dim line stays collinear with the base: project both extension
|
||||
// origins onto the base dim line's absolute perpendicular coordinate.
|
||||
|
||||
@@ -78,6 +78,9 @@ impl CadCommand for DimTeditCommand {
|
||||
acadrust::types::Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
||||
d.base_mut().text_middle_point = new_pt;
|
||||
d.base_mut().insertion_point = new_pt;
|
||||
// Pin the text to this location, else the renderer recomputes
|
||||
// the style-default placement and the move is discarded.
|
||||
d.base_mut().text_user_positioned = true;
|
||||
}
|
||||
return CmdResult::ReplaceEntity(h, vec![ent]);
|
||||
}
|
||||
|
||||
@@ -151,28 +151,28 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_dimension_preview(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec<Vec3> {
|
||||
/// Project the two extension origins onto the dimension line, which passes
|
||||
/// through `def` along `axis`. Each origin is projected *independently*: a
|
||||
/// single shared offset only lands both on the line when they are level, and
|
||||
/// tilts the dimension line when they are not (e.g. measuring across sloped
|
||||
/// points). See #181.
|
||||
fn dim_line_endpoints(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> (Vec3, Vec3) {
|
||||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
let offset = (def - first).dot(perp);
|
||||
let d1 = first + perp * offset;
|
||||
let d2 = second + perp * offset;
|
||||
vec![
|
||||
first,
|
||||
d1,
|
||||
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
|
||||
second,
|
||||
d2,
|
||||
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
|
||||
d1,
|
||||
d2,
|
||||
]
|
||||
let dperp = def.dot(perp);
|
||||
let d1 = first + perp * (dperp - first.dot(perp));
|
||||
let d2 = second + perp * (dperp - second.dot(perp));
|
||||
(d1, d2)
|
||||
}
|
||||
|
||||
fn linear_dimension_preview(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec<Vec3> {
|
||||
let (d1, d2) = dim_line_endpoints(first, second, def, axis);
|
||||
let nan = Vec3::new(f32::NAN, f32::NAN, f32::NAN);
|
||||
vec![first, d1, nan, second, d2, nan, d1, d2]
|
||||
}
|
||||
|
||||
fn linear_text_pos(first: Vec3, second: Vec3, def: Vec3, axis: Vec3) -> Vec3 {
|
||||
let (d1, d2) = dim_line_endpoints(first, second, def, axis);
|
||||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
let offset = (def - first).dot(perp);
|
||||
let d1 = first + perp * offset;
|
||||
let d2 = second + perp * offset;
|
||||
(d1 + d2) * 0.5 + perp * 0.15
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
|
||||
.map(normalize_insert_entity)
|
||||
.collect(),
|
||||
EntityType::MLine(ml) => explode_mline(ml),
|
||||
EntityType::Dimension(dim) => explode_dimension(dim),
|
||||
EntityType::Dimension(dim) => explode_dimension(dim, document),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -322,7 +322,142 @@ fn explode_mline(ml: &MLine) -> Vec<EntityType> {
|
||||
// ── Dimension explode ──────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a Dimension entity into Lines (geometry) + Text (label).
|
||||
fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
||||
/// A NULL-handle line segment for a baked dimension block.
|
||||
fn dim_seg(a: Vector3, b: Vector3, common: &acadrust::entities::EntityCommon) -> EntityType {
|
||||
let mut c = common.clone();
|
||||
c.handle = Handle::NULL;
|
||||
EntityType::Line(LineEnt {
|
||||
common: c,
|
||||
start: a,
|
||||
end: b,
|
||||
..LineEnt::new()
|
||||
})
|
||||
}
|
||||
|
||||
/// 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(
|
||||
tip: Vector3,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
size: 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),
|
||||
]
|
||||
}
|
||||
|
||||
/// Center-mark cross at `center`, arm length |DIMCEN|. Empty when DIMCEN == 0.
|
||||
fn dim_center_mark(
|
||||
center: Vector3,
|
||||
dimcen: f64,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
let s = dimcen.abs();
|
||||
if s < 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,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Arc from angle `a1` to `a2` (radians, swept the short way) at `radius` about
|
||||
/// `center`, approximated by straight chords (world f64).
|
||||
fn dim_arc_segs(
|
||||
center: Vector3,
|
||||
radius: f64,
|
||||
a1: f64,
|
||||
a2: f64,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
use std::f64::consts::PI;
|
||||
let mut sweep = a2 - a1;
|
||||
while sweep > PI {
|
||||
sweep -= 2.0 * PI;
|
||||
}
|
||||
while sweep < -PI {
|
||||
sweep += 2.0 * PI;
|
||||
}
|
||||
let steps = 12usize.max((sweep.abs() / (PI / 36.0)).ceil() as usize);
|
||||
let pt = |a: f64| Vector3::new(center.x + radius * a.cos(), center.y + radius * a.sin(), center.z);
|
||||
let mut out = Vec::new();
|
||||
let mut prev = pt(a1);
|
||||
for i in 1..=steps {
|
||||
let cur = pt(a1 + sweep * (i as f64 / steps as f64));
|
||||
out.push(dim_seg(prev, cur, common));
|
||||
prev = cur;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// (DIMASZ, DIMCEN), DIMSCALE-applied, resolved from the dimension's style.
|
||||
fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> (f64, f64) {
|
||||
let name = dim.base().style_name.as_str();
|
||||
let style = doc.dim_styles.iter().find(|s| {
|
||||
s.name.eq_ignore_ascii_case(name)
|
||||
|| (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
fn angular_block_segs(
|
||||
vertex: Vector3,
|
||||
p1: Vector3,
|
||||
p2: Vector3,
|
||||
arc_loc: Vector3,
|
||||
common: &acadrust::entities::EntityCommon,
|
||||
) -> Vec<EntityType> {
|
||||
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));
|
||||
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));
|
||||
out
|
||||
}
|
||||
|
||||
fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
use acadrust::entities::Text;
|
||||
|
||||
let base = dim.base();
|
||||
@@ -362,37 +497,94 @@ fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
||||
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 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;
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
let angle = d.rotation.to_radians();
|
||||
// `rotation` is the dimension-line angle, already in radians (the
|
||||
// same convention the live renderer uses) — do not convert again.
|
||||
let angle = d.rotation;
|
||||
let perp_x = -(angle.sin());
|
||||
let perp_y = angle.cos();
|
||||
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 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);
|
||||
// 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.
|
||||
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);
|
||||
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));
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
result.push(make_seg(&d.angle_vertex, &d.definition_point, &common));
|
||||
// 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))
|
||||
.sqrt()
|
||||
.max(1e-12);
|
||||
result.extend(dim_arrowhead(
|
||||
point,
|
||||
(center.x - point.x) / m,
|
||||
(center.y - point.y) / m,
|
||||
asz,
|
||||
&common,
|
||||
));
|
||||
result.extend(dim_center_mark(center, cen, &common));
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
result.push(make_seg(&d.angle_vertex, &d.definition_point, &common));
|
||||
// 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.
|
||||
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))
|
||||
.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));
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
result.push(make_seg(&d.first_point, &d.angle_vertex, &common));
|
||||
result.push(make_seg(&d.second_point, &d.angle_vertex, &common));
|
||||
result.extend(angular_block_segs(
|
||||
d.angle_vertex,
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.dimension_arc,
|
||||
&common,
|
||||
));
|
||||
}
|
||||
Dimension::Angular3Pt(d) => {
|
||||
result.push(make_seg(&d.first_point, &d.angle_vertex, &common));
|
||||
result.push(make_seg(&d.second_point, &d.angle_vertex, &common));
|
||||
result.extend(angular_block_segs(
|
||||
d.angle_vertex,
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
&common,
|
||||
));
|
||||
}
|
||||
Dimension::Ordinate(d) => {
|
||||
result.push(make_seg(&d.feature_location, &d.definition_point, &common));
|
||||
@@ -400,38 +592,53 @@ fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
||||
}
|
||||
}
|
||||
|
||||
// Text entity for the dimension label
|
||||
let text_val = if let Some(u) = &base.user_text {
|
||||
if !u.trim().is_empty() {
|
||||
u.clone()
|
||||
} else {
|
||||
format!("{:.4}", dim.measurement())
|
||||
}
|
||||
} else if !base.text.trim().is_empty() {
|
||||
base.text.clone()
|
||||
} else {
|
||||
match dim {
|
||||
Dimension::Radius(_) => format!("R{:.4}", dim.measurement()),
|
||||
Dimension::Diameter(_) => format!("Ø{:.4}", dim.measurement()),
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => {
|
||||
format!("{:.2}°", dim.measurement())
|
||||
}
|
||||
_ => format!("{:.4}", dim.measurement()),
|
||||
}
|
||||
};
|
||||
|
||||
let mut text = Text::with_value(text_val, base.text_middle_point.clone())
|
||||
.with_height(base.line_spacing_factor.abs().max(0.1))
|
||||
.with_rotation(base.text_rotation);
|
||||
text.common = common.clone();
|
||||
text.common.handle = Handle::NULL;
|
||||
result.push(EntityType::Text(text));
|
||||
// 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));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ── Dimension block baking (DWG/DXF interop) ────────────────────────────────
|
||||
|
||||
/// Mirror each dimension's authoritative geometric definition point into
|
||||
/// `base.definition_point`, which is the field the DWG/DXF writer emits as
|
||||
/// group 10. Edits (grips, properties, transforms) update the per-type struct
|
||||
/// field but not `base`, so without this the saved group 10 goes stale and the
|
||||
/// dimension's line / leader / origin jumps on reload (#181). Angular-2-line
|
||||
/// keeps a distinct base point (the second line's point) and is left alone.
|
||||
fn sync_dimension_base_points(doc: &mut CadDocument) {
|
||||
for e in doc.entities_mut() {
|
||||
if let EntityType::Dimension(d) = e {
|
||||
let def = match d {
|
||||
Dimension::Linear(x) => Some(x.definition_point),
|
||||
Dimension::Aligned(x) => Some(x.definition_point),
|
||||
Dimension::Radius(x) => Some(x.definition_point),
|
||||
Dimension::Diameter(x) => Some(x.definition_point),
|
||||
Dimension::Ordinate(x) => Some(x.definition_point),
|
||||
Dimension::Angular3Pt(x) => Some(x.definition_point),
|
||||
Dimension::Angular2Ln(_) => None,
|
||||
};
|
||||
if let Some(p) = def {
|
||||
d.base_mut().definition_point = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Smallest free `*D<n>` anonymous block name in `doc`.
|
||||
fn next_dimension_block_name(doc: &CadDocument) -> String {
|
||||
let mut n = 0u64;
|
||||
@@ -462,6 +669,10 @@ fn next_dimension_block_name(doc: &CadDocument) -> String {
|
||||
/// DWG, or copied via the `*D`-cloning copy path) are left untouched so their
|
||||
/// original graphics are preserved.
|
||||
pub fn bake_dimension_blocks(doc: &mut CadDocument) {
|
||||
// Keep group-10 (base.definition_point) in step with the per-type geometry
|
||||
// before writing — see sync_dimension_base_points.
|
||||
sync_dimension_base_points(doc);
|
||||
|
||||
// Handles of dimensions whose block reference is missing or dangling.
|
||||
let pending: Vec<Handle> = doc
|
||||
.entities()
|
||||
@@ -483,7 +694,7 @@ pub fn bake_dimension_blocks(doc: &mut CadDocument) {
|
||||
Some(EntityType::Dimension(d)) => d.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
let subs = explode_dimension(&dim);
|
||||
let subs = explode_dimension(&dim, doc);
|
||||
if subs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -600,4 +811,142 @@ mod tests {
|
||||
"a dimension that already owns a block must not be re-baked"
|
||||
);
|
||||
}
|
||||
|
||||
// Collect the line segments baked into the dimension's `*D` block.
|
||||
fn baked_segments(doc: &CadDocument, block_name: &str) -> Vec<(Vector3, Vector3)> {
|
||||
let rec = doc.block_records.get(block_name).expect("block record");
|
||||
doc.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Line(l) if l.common.owner_handle == rec.handle => {
|
||||
Some((l.start, l.end))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// A horizontal (rotation = 0) linear dimension whose two measured points sit
|
||||
// at *different* heights must still bake a level dimension line — both
|
||||
// extension origins project onto the same line. Regression test for #181,
|
||||
// where a shared offset tilted the dimension line.
|
||||
#[test]
|
||||
fn linear_dim_line_stays_level_over_sloped_points() {
|
||||
let mut doc = CadDocument::new();
|
||||
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 5.0, 0.0));
|
||||
d.rotation = 0.0;
|
||||
d.definition_point = Vector3::new(0.0, 8.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"),
|
||||
};
|
||||
// The dimension line is the segment spanning both x extents; it must be
|
||||
// horizontal at the definition-point level (y = 8).
|
||||
let dim_line = baked_segments(&doc, &name)
|
||||
.into_iter()
|
||||
.find(|(a, b)| (a.x - 0.0).abs() < 1e-6 && (b.x - 10.0).abs() < 1e-6)
|
||||
.expect("dimension line segment");
|
||||
assert!(
|
||||
(dim_line.0.y - 8.0).abs() < 1e-6 && (dim_line.1.y - 8.0).abs() < 1e-6,
|
||||
"dimension line must be level at y=8, got {:?}",
|
||||
dim_line
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
#[test]
|
||||
fn angular_dim_bakes_an_arc() {
|
||||
use acadrust::entities::DimensionAngular3Pt;
|
||||
let mut doc = CadDocument::new();
|
||||
let mut d = DimensionAngular3Pt::new(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(10.0, 0.0, 0.0),
|
||||
Vector3::new(0.0, 10.0, 0.0),
|
||||
);
|
||||
d.definition_point = Vector3::new(5.0, 5.0, 0.0); // arc location
|
||||
let handle = doc
|
||||
.add_entity(EntityType::Dimension(Dimension::Angular3Pt(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 lines = doc
|
||||
.entities()
|
||||
.filter(|e| matches!(e, EntityType::Line(l) if l.common.owner_handle == rec.handle))
|
||||
.count();
|
||||
assert!(lines > 5, "angular bake must include arc chords, got {lines} lines");
|
||||
}
|
||||
|
||||
// A diameter dimension bakes a line edge-to-edge THROUGH the centre, not a
|
||||
// radius-length line. The two extreme endpoints must be equidistant from the
|
||||
// centre (angle_vertex) and the centre must lie between them.
|
||||
#[test]
|
||||
fn diameter_dim_bakes_through_center() {
|
||||
use acadrust::entities::DimensionDiameter;
|
||||
let mut doc = CadDocument::new();
|
||||
let center = Vector3::new(3.0, 4.0, 0.0);
|
||||
let edge = Vector3::new(8.0, 4.0, 0.0); // radius 5 along +x
|
||||
let mut d = DimensionDiameter::new(center, edge);
|
||||
d.base.text_middle_point = Vector3::new(3.0, 9.0, 0.0);
|
||||
let handle = doc
|
||||
.add_entity(EntityType::Dimension(Dimension::Diameter(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"),
|
||||
};
|
||||
// The longest baked segment is the diameter line; its endpoints span the
|
||||
// full diameter (length ~= 2*radius = 10) centred on `center`.
|
||||
let diam = baked_segments(&doc, &name)
|
||||
.into_iter()
|
||||
.find(|(a, b)| {
|
||||
let len = ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
|
||||
(len - 10.0).abs() < 1e-6
|
||||
})
|
||||
.expect("diameter line spanning 2*radius");
|
||||
let mid_x = (diam.0.x + diam.1.x) * 0.5;
|
||||
let mid_y = (diam.0.y + diam.1.y) * 0.5;
|
||||
assert!(
|
||||
(mid_x - center.x).abs() < 1e-6 && (mid_y - center.y).abs() < 1e-6,
|
||||
"diameter line must be centred on the circle centre, mid=({mid_x},{mid_y})"
|
||||
);
|
||||
}
|
||||
|
||||
// A rotated linear dimension uses `rotation` directly (radians). Before the
|
||||
// fix `to_radians()` shrank a 90° dim to ~1.57°, baking a nearly-horizontal
|
||||
// line instead of a vertical one.
|
||||
#[test]
|
||||
fn rotated_linear_dim_bakes_at_its_angle() {
|
||||
let mut doc = CadDocument::new();
|
||||
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 10.0, 0.0));
|
||||
d.rotation = std::f64::consts::FRAC_PI_2; // 90°, vertical dimension line
|
||||
d.definition_point = Vector3::new(8.0, 0.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"),
|
||||
};
|
||||
// Dimension line spans both y extents and must be vertical at x = 8.
|
||||
let dim_line = baked_segments(&doc, &name)
|
||||
.into_iter()
|
||||
.find(|(a, b)| (a.y - 0.0).abs() < 1e-6 && (b.y - 10.0).abs() < 1e-6)
|
||||
.expect("dimension line segment");
|
||||
assert!(
|
||||
(dim_line.0.x - 8.0).abs() < 1e-6 && (dim_line.1.x - 8.0).abs() < 1e-6,
|
||||
"dimension line must be vertical at x=8, got {:?}",
|
||||
dim_line
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user