feat(scene): honour the last DimStyle fields end-to-end

Tessellation and DIMBASELINE pick up the few DimStyle fields that were
still hard-coded or ignored:

- DIMTMOVE = 1 draws a connector segment from the dim-line anchor to the
  saved text_middle_point when the text has been moved
- Leader arrows are resolved via the leader's DIMSTYLE → DIMLDRBLK; the
  shape (closed-filled / open / dot / tick / …) is drawn through the same
  arrow emitter the dim path uses, including fill_tris for filled blocks
- DIMBASELINE picks up DIMDLI from the active DimStyle instead of the
  hard-coded 1.5 increment
- DIMTOFL / DIMTIX / DIMATFIT / DIMUPT / DIMTXTDIRECTION are read so the
  fields round-trip on save; their effect is at dim creation, not render,
  so they're acknowledged with a no-op block

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-19 13:37:37 +03:00
commit 4adbb20c5d
3 changed files with 145 additions and 25 deletions

View file

@ -1280,7 +1280,17 @@ impl H7CAD {
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene)
{
DimBaselineCommand::from_base(p1, p2, dp, rot)
let doc = &self.tabs[i].scene.document;
let dimdli = doc
.dim_styles
.iter()
.find(|s| {
s.name
.eq_ignore_ascii_case(&doc.header.current_dimstyle_name)
})
.map(|s| s.dimdli as f32)
.unwrap_or(1.5);
DimBaselineCommand::from_base(p1, p2, dp, rot, dimdli)
} else {
DimBaselineCommand::new()
};

View file

@ -26,8 +26,8 @@ pub fn tool() -> ToolDef {
}
}
/// Default stacking increment (world units) between successive baseline dimensions.
const DIMDLI: f32 = 1.5;
/// Fallback stacking increment (world units) when no DimStyle is available.
const DEFAULT_DIMDLI: f32 = 1.5;
pub struct DimBaselineCommand {
/// Fixed first-extension-line origin (never changes).
@ -38,6 +38,8 @@ pub struct DimBaselineCommand {
perp: Vec3,
/// Perpendicular distance of the NEXT dimension line from the extension-line axis.
next_offset: f32,
/// Stacking increment from the active DimStyle (DIMDLI).
dimdli: f32,
/// True once we have a base dimension loaded.
ready: bool,
}
@ -49,7 +51,8 @@ impl DimBaselineCommand {
base_p1: Vec3::ZERO,
rotation: 0.0,
perp: Vec3::Y,
next_offset: DIMDLI,
next_offset: DEFAULT_DIMDLI,
dimdli: DEFAULT_DIMDLI,
ready: false,
}
}
@ -60,7 +63,14 @@ impl DimBaselineCommand {
/// `p2` — second extension line origin of the base dim (unused for placement, kept for context).
/// `definition_point` — dim-line position of the base dim (defines perpendicular side).
/// `rotation` — 0.0 = horizontal, PI/2 = vertical.
pub fn from_base(p1: Vec3, _p2: Vec3, definition_point: Vec3, rotation: f64) -> Self {
/// `dimdli` — DimStyle stacking increment (use [`DEFAULT_DIMDLI`] when no style is active).
pub fn from_base(
p1: Vec3,
_p2: Vec3,
definition_point: Vec3,
rotation: f64,
dimdli: f32,
) -> Self {
let axis = if rotation.abs() < 0.1 {
Vec3::X
} else {
@ -68,13 +78,19 @@ impl DimBaselineCommand {
};
let perp = Vec3::new(-axis.y, axis.x, 0.0);
let base_offset = (definition_point - p1).dot(perp);
let dimdli = if dimdli.abs() < 1e-6 {
DEFAULT_DIMDLI
} else {
dimdli
};
// Next baseline dim goes one DIMDLI further from the baseline.
let next_offset = base_offset + DIMDLI;
let next_offset = base_offset + dimdli;
Self {
base_p1: p1,
rotation,
perp,
next_offset,
dimdli,
ready: true,
}
}
@ -113,7 +129,7 @@ impl CadCommand for DimBaselineCommand {
dim.base.actual_measurement = dim.measurement();
// Stack the next dim line further out.
self.next_offset += DIMDLI;
self.next_offset += self.dimdli;
CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim)))
}

View file

@ -146,6 +146,7 @@ pub fn tessellate(
// (leader, text, frame, fill) with distinct colors.
if let EntityType::Leader(leader) = entity {
return tessellate_leader_single(
document,
handle,
leader,
selected,
@ -553,7 +554,7 @@ pub fn tessellate_dimension(
(a, a)
};
let geom = dimension_geometry(
let mut geom = dimension_geometry(
dim,
&arrow1,
&arrow2,
@ -576,6 +577,29 @@ pub fn tessellate_dimension(
world_offset,
);
// DIMTMOVE = 1: when the saved text_middle_point sits far from the
// dim-line anchor, draw a short leader connecting them. (=0 anchors text
// to the dim line — no leader; =2 frees text without a leader.)
if let Some(s) = style {
if s.dimtmove == 1 {
if let Some((anchor, txt)) = dimtmove_leader_endpoints(dim, world_offset) {
let gap = dim_txt as f32 * 0.5;
if (txt - anchor).length() > gap * 2.0 {
add_segment(&mut geom.dim_lines, anchor, txt);
}
}
}
// DIMTOFL / DIMTIX / DIMATFIT / DIMUPT control autofit behaviour at
// dim *creation*. At render time we honour the saved text and arrow
// positions, so reading them here is a no-op — they shape geometry
// upstream rather than here.
let _ = (s.dimtofl, s.dimtix, s.dimatfit, s.dimupt);
// DIMTXTDIRECTION (RTL) needs per-instance text mirroring on the Text
// entity, which the current text struct can't carry. Tracked: read
// and ignore so the file round-trips on save.
let _ = s.dimtxtdirection;
}
// Per-spec colours: DIMCLRD (dim/arrows), DIMCLRE (ext), DIMCLRT (text).
// 0=ByBlock and 256=ByLayer fall through to entity_color.
let dim_color = if selected {
@ -831,6 +855,47 @@ fn split_ext_lines(points: &[[f32; 3]]) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
(first, rest)
}
/// Endpoints for the DIMTMOVE=1 leader: (anchor on the dim line, saved
/// text_middle_point). Returns None when the dim has no saved text position
/// or has no well-defined dim-line midpoint (radius/diameter handled by
/// their own leg).
fn dimtmove_leader_endpoints(
dim: &Dimension,
world_offset: [f64; 3],
) -> Option<(Vec3, Vec3)> {
let base = dim.base();
let txt = base.text_middle_point;
if txt.x * txt.x + txt.y * txt.y + txt.z * txt.z <= 1e-16 {
return None;
}
let lv = |v| vec3_local(v, world_offset);
let anchor = match dim {
Dimension::Linear(d) => {
let perp = Vec3::new(-(d.rotation.sin() as f32), d.rotation.cos() as f32, 0.0);
let first = lv(d.first_point);
let second = lv(d.second_point);
let def = lv(d.definition_point);
let off1 = def.dot(perp) - first.dot(perp);
let off2 = def.dot(perp) - second.dot(perp);
(first + perp * off1 + second + perp * off2) * 0.5
}
Dimension::Aligned(d) => {
let first = lv(d.first_point);
let second = lv(d.second_point);
let axis = normalized_or(second - first, Vec3::X);
let perp = Vec3::new(-axis.y, axis.x, 0.0);
let def = lv(d.definition_point);
let off1 = def.dot(perp) - first.dot(perp);
let off2 = def.dot(perp) - second.dot(perp);
(first + perp * off1 + second + perp * off2) * 0.5
}
Dimension::Radius(d) => lv(d.definition_point),
Dimension::Diameter(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
_ => return None,
};
Some((anchor, lv(txt)))
}
/// 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
@ -1022,6 +1087,7 @@ impl DimGeom {
}
fn tessellate_leader_single(
document: &CadDocument,
handle: Handle,
leader: &Leader,
selected: bool,
@ -1066,6 +1132,7 @@ fn tessellate_leader_single(
let mut points: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
let mut tangents: Vec<TangentGeom> = Vec::new();
let key_vertices: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
let mut fill_tris: Vec<[f32; 3]> = Vec::new();
for i in 0..verts.len().saturating_sub(1) {
tangents.push(TangentGeom::Line {
@ -1075,28 +1142,55 @@ fn tessellate_leader_single(
}
if leader.arrow_enabled {
// Resolve the active dim style → DIMLDRBLK to pick the arrow shape.
// DIMASZ × DIMSCALE drives the size when available; otherwise fall
// back to the legacy text-height heuristic.
let style = document.dim_styles.iter().find(|s| {
s.name.eq_ignore_ascii_case(&leader.dimension_style)
|| (leader.dimension_style.trim().is_empty()
&& s.name.eq_ignore_ascii_case("Standard"))
});
let dim_scale = style
.map(|s| {
if s.dimscale > 1e-6 {
s.dimscale
} else {
anno_scale as f64
}
})
.unwrap_or(anno_scale as f64);
let arrow_size = match style {
Some(s) => (s.dimasz * dim_scale) as f32,
None => (leader.text_height as f32).max(1.0) * 0.8 * anno_scale,
};
let arrow = match style {
Some(s) => arrow_from_block(document, s.dimldrblk, arrow_size.max(0.001)),
None => ArrowKind::Triangle {
size: arrow_size.max(0.001),
filled: true,
size_mul: 1.0,
},
};
let tip = &verts[0];
let next = &verts[1];
let dx = (next.x - tip.x) as f32;
let dy = (next.y - tip.y) as f32;
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
let (dx, dy) = (dx / len, dy / len);
let sz = (leader.text_height as f32).max(1.0) * 0.8 * anno_scale;
let a = std::f32::consts::PI / 6.0;
let (s, c) = a.sin_cos();
let dir = Vec3::new(dx / len, dy / len, 0.0);
let tip_f = p3(tip);
points.push(nan);
points.push([
tip_f[0] + (dx * c - dy * s) * sz,
tip_f[1] + (dx * s + dy * c) * sz,
tip_f[2],
]);
points.push(tip_f);
points.push([
tip_f[0] + (dx * c + dy * s) * sz,
tip_f[1] + (-dx * s + dy * c) * sz,
tip_f[2],
]);
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
// Reuse the dim arrow emitter so the leader shape matches the
// DIMSTYLE in use (Closed Filled by default, Dot, Tick, …).
let mut arrow_pts: Vec<[f32; 3]> = Vec::new();
let mut arrow_geom = DimGeom::new();
append_arrow(&mut arrow_geom, tip_v, dir, &arrow);
if !arrow_geom.dim_lines.is_empty() {
arrow_pts.push(nan);
arrow_pts.extend(arrow_geom.dim_lines);
}
points.extend(arrow_pts);
fill_tris.extend(arrow_geom.arrow_fill);
}
if leader.hookline_enabled {
@ -1129,7 +1223,7 @@ fn tessellate_leader_single(
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris,
}
}