From 4adbb20c5db6ae320ba07948f24becf49c67c587 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 19 May 2026 13:37:37 +0300 Subject: [PATCH] feat(scene): honour the last DimStyle fields end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/commands.rs | 12 ++- src/modules/annotate/dim_baseline.rs | 28 ++++-- src/scene/tessellate.rs | 130 +++++++++++++++++++++++---- 3 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index e9420c701..f7e0ff397 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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() }; diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index ef7ac2839..bd033457a 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -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))) } diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index da2cbf775..5a2104d39 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -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 = 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, } }