diff --git a/Cargo.lock b/Cargo.lock
index fc84a9cbe..0583d5bfe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "OpenCADStudio"
-version = "0.9.7"
+version = "0.9.8"
dependencies = [
"acadrust",
"ashpd",
diff --git a/Cargo.toml b/Cargo.toml
index 354d3e263..64b2533c9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "OpenCADStudio"
-version = "0.9.7"
+version = "0.9.8"
edition = "2021"
build = "build.rs"
diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs
index 0da5ad8c3..47769b7e3 100644
--- a/src/app/command_driver.rs
+++ b/src/app/command_driver.rs
@@ -856,7 +856,7 @@ impl OpenCADStudio {
.infer_dimension_sources(handle);
self.tabs[i]
.scene
- .attach_dimension_association(handle, sources.to_vec());
+ .attach_dimension_association(handle, sources);
}
}
self.tabs[i].dirty = true;
@@ -1269,7 +1269,7 @@ impl OpenCADStudio {
.infer_dimension_sources(handle);
self.tabs[i]
.scene
- .attach_dimension_association(handle, sources.to_vec());
+ .attach_dimension_association(handle, sources);
}
}
self.tabs[i].dirty = true;
@@ -1287,6 +1287,8 @@ impl OpenCADStudio {
CmdResult::CommitDimension {
mut entity,
association,
+ preserve_base_style,
+ continue_command,
} => {
let label = self.history_label_from_active_cmd(i, "DIMENSION");
let association_mode = self.tabs[i]
@@ -1300,6 +1302,17 @@ impl OpenCADStudio {
acadrust::entities::Dimension::Ordinate(_)
)
);
+ let inherited_dimension = if preserve_base_style {
+ match &entity {
+ acadrust::EntityType::Dimension(dimension) => Some((
+ dimension.base().common.layer.clone(),
+ dimension.base().style_name.clone(),
+ )),
+ _ => None,
+ }
+ } else {
+ None
+ };
let pending = if association_mode == 0 {
let layer = self.tabs[i].active_layer.clone();
if layer != "0" || entity.as_entity().layer().is_empty() {
@@ -1330,6 +1343,12 @@ impl OpenCADStudio {
&self.tabs[i].scene.document,
&mut entity,
);
+ if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
+ (inherited_dimension, &mut entity)
+ {
+ dimension.base_mut().common.layer = layer;
+ dimension.base_mut().style_name = style_name;
+ }
let pieces = crate::modules::draw::modify::explode::explode_entity(
&entity,
&self.tabs[i].scene.document,
@@ -1345,7 +1364,10 @@ impl OpenCADStudio {
} else {
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
- if let Some(handle) = self.commit_entity_handle(entity) {
+ if let Some(handle) = self.commit_entity_handle_with_dimension_policy(
+ entity,
+ preserve_base_style,
+ ) {
if association_mode == 2 {
let mut changes = vec![
(handle, crate::scene::ChangeKind::Modified),
@@ -1357,8 +1379,6 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.infer_dimension_sources(handle)
- .into_iter()
- .collect()
},
|source| {
if single_source_dimension {
@@ -1393,12 +1413,195 @@ impl OpenCADStudio {
};
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
- self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
- self.restore_pre_cmd_tangent();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
+ if continue_command {
+ if let Some(prompt) = self.tabs[i]
+ .active_cmd
+ .as_ref()
+ .map(|cmd| cmd.prompt())
+ {
+ self.command_line.push_info(&prompt);
+ }
+ } else {
+ self.tabs[i].active_cmd = None;
+ self.restore_pre_cmd_tangent();
+ }
+ }
+ CmdResult::CommitDimensionsAndExit(dimensions) => {
+ let association_mode = self.tabs[i]
+ .scene
+ .document
+ .header
+ .dimension_associativity;
+ let mut made = 0;
+ let pending = if association_mode == 0 {
+ let mut pieces = Vec::new();
+ for (mut entity, _) in dimensions {
+ let layer = self.tabs[i].active_layer.clone();
+ if layer != "0" || entity.as_entity().layer().is_empty() {
+ entity.as_entity_mut().set_layer(layer);
+ }
+ crate::scene::view::dispatch::apply_color(
+ &mut entity,
+ self.ribbon.active_color,
+ );
+ crate::scene::view::dispatch::apply_common_prop(
+ &mut entity,
+ "linetype",
+ &self.ribbon.active_linetype.clone(),
+ );
+ crate::scene::view::dispatch::apply_line_weight(
+ &mut entity,
+ self.ribbon.active_lineweight,
+ );
+ let celtscale = self.tabs[i]
+ .scene
+ .document
+ .header
+ .current_entity_linetype_scale;
+ if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
+ entity.common_mut().linetype_scale = celtscale;
+ }
+ crate::scene::creation_style::apply_current_creation_styles(
+ &self.tabs[i].scene.document,
+ &mut entity,
+ );
+ let exploded = crate::modules::draw::modify::explode::explode_entity(
+ &entity,
+ &self.tabs[i].scene.document,
+ );
+ if !exploded.is_empty() {
+ made += 1;
+ }
+ pieces.extend(exploded);
+ }
+ let delta_safe = pieces
+ .iter()
+ .all(|piece| self.delta_add_safe(i, piece));
+ let pending = self.begin_undo(i, "QDIM", pieces.len(), delta_safe);
+ for piece in pieces {
+ self.tabs[i].scene.add_entity(piece);
+ }
+ pending
+ } else {
+ let delta_safe = dimensions
+ .iter()
+ .all(|(entity, _)| self.delta_add_safe(i, entity));
+ let pending = self.begin_undo(i, "QDIM", dimensions.len(), delta_safe);
+ for (entity, association) in dimensions {
+ let Some(handle) = self.commit_entity_handle(entity) else {
+ continue;
+ };
+ made += 1;
+ if association_mode != 2 {
+ continue;
+ }
+ let sources = match association {
+ crate::command::DimensionAssociationInput::Infer(source) => {
+ source.map_or_else(
+ || {
+ self.tabs[i]
+ .scene
+ .infer_dimension_sources(handle)
+ .into_iter()
+ .map(|source| {
+ source.map(
+ crate::command::DimensionAssociationSource::inferred,
+ )
+ })
+ .collect()
+ },
+ |source| {
+ vec![Some(
+ crate::command::DimensionAssociationSource::inferred(
+ source,
+ ),
+ )]
+ },
+ )
+ }
+ crate::command::DimensionAssociationInput::Explicit(sources) => {
+ sources
+ }
+ };
+ self.tabs[i]
+ .scene
+ .attach_dimension_association_sources(handle, sources.clone());
+ let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
+ changes.extend(sources.into_iter().flatten().map(|source| {
+ (source.handle, crate::scene::ChangeKind::Modified)
+ }));
+ changes.sort_by_key(|(handle, _)| handle.value());
+ changes.dedup_by_key(|(handle, _)| handle.value());
+ self.tabs[i].scene.bump_entities(&changes);
+ }
+ pending
+ };
+ if made > 0 {
+ self.tabs[i].dirty = true;
+ }
+ self.tabs[i].scene.clear_preview_wire();
+ self.tabs[i].active_cmd = None;
+ self.tabs[i].snap_result = None;
+ self.restore_pre_cmd_tangent();
+ self.command_line
+ .push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
+ if let Some(pd) = pending {
+ self.commit_undo_delta(i, pd);
+ }
+ }
+ CmdResult::SetQuickDimensionSnapPriority(priority) => {
+ self.quick_dimension_snap_priority = priority.min(1);
+ self.persist_settings_if_changed();
+ let prompt = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt());
+ if let Some(prompt) = prompt {
+ self.command_line.push_info(&prompt);
+ }
+ }
+ CmdResult::AlignMLeaders {
+ mut handles,
+ from,
+ to,
+ } => {
+ handles.retain(|handle| {
+ !self.tabs[i].scene.is_layer_locked(*handle)
+ && matches!(
+ self.tabs[i].scene.document.get_entity(*handle),
+ Some(acadrust::EntityType::MultiLeader(_))
+ )
+ });
+ if !handles.is_empty() {
+ self.push_undo_snapshot(i, "MLEADERALIGN");
+ if apply_mleader_align(&mut self.tabs[i].scene, &handles, from, to) {
+ self.tabs[i].dirty = true;
+ self.command_line.push_output(
+ crate::t!("MLEADERALIGN Leaders aligned.").as_ref(),
+ );
+ }
+ }
+ self.tabs[i].scene.clear_preview_wire();
+ self.tabs[i].active_cmd = None;
+ self.tabs[i].snap_result = None;
+ self.restore_pre_cmd_tangent();
+ }
+ CmdResult::CollectMLeaders { handles, point } => {
+ let compatible = compatible_mleader_collect_handles(&self.tabs[i].scene, &handles);
+ if compatible.len() >= 2 {
+ self.push_undo_snapshot(i, "MLEADERCOLLECT");
+ if apply_mleader_collect(&mut self.tabs[i].scene, &compatible, point) {
+ self.tabs[i].dirty = true;
+ self.command_line.push_output(
+ crate::t!("MLEADERCOLLECT Leaders collected.").as_ref(),
+ );
+ }
+ }
+ self.tabs[i].scene.clear_preview_wire();
+ self.tabs[i].active_cmd = None;
+ self.tabs[i].snap_result = None;
+ self.restore_pre_cmd_tangent();
}
CmdResult::CommitSolid {
entity,
@@ -1859,30 +2062,6 @@ impl OpenCADStudio {
self.tabs[i].snap_result = None;
return Task::none();
}
- if layer.starts_with("__MLEADERALIGN__") {
- self.push_undo_snapshot(i, "MLEADERALIGN");
- if let Some(encoded) = layer.strip_prefix("__MLEADERALIGN__") {
- apply_mleader_align(&mut self.tabs[i].scene, encoded);
- }
- self.command_line
- .push_output(crate::t!("MLEADERALIGN Leaders aligned.").as_ref());
- self.tabs[i].dirty = true;
- self.tabs[i].active_cmd = None;
- self.tabs[i].snap_result = None;
- return Task::none();
- }
- if layer.starts_with("__MLEADERCOLLECT__") {
- self.push_undo_snapshot(i, "MLEADERCOLLECT");
- if let Some(encoded) = layer.strip_prefix("__MLEADERCOLLECT__") {
- apply_mleader_collect(&mut self.tabs[i].scene, encoded);
- }
- self.command_line
- .push_output(crate::t!("MLEADERCOLLECT Leaders collected.").as_ref());
- self.tabs[i].dirty = true;
- self.tabs[i].active_cmd = None;
- self.tabs[i].snap_result = None;
- return Task::none();
- }
}
}
@@ -2245,11 +2424,10 @@ impl OpenCADStudio {
.map(|e| crate::entities::dim_override::pairs(&e.common().extended_data));
if let Some(common) = src_common {
- self.push_undo_snapshot(i, "MATCHPROP");
- for h in &dest {
+ self.apply_property_op(i, "MATCHPROP", &dest, |app, handle| {
let mut is_dim = false;
let mut is_hatch = false;
- if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
+ if let Some(e) = app.tabs[i].scene.document.get_entity_mut(handle) {
e.as_entity_mut().set_layer(common.layer.clone());
crate::scene::view::dispatch::apply_color(e, common.color);
crate::scene::view::dispatch::apply_line_weight(e, common.line_weight);
@@ -2284,8 +2462,8 @@ impl OpenCADStudio {
if is_hatch {
if let Some(values) = &hatch_background_xdata {
crate::scene::view::dispatch::set_entity_xdata(
- &mut self.tabs[i].scene.document,
- *h,
+ &mut app.tabs[i].scene.document,
+ handle,
"HATCHBACKGROUNDCOLOR",
values.clone(),
);
@@ -2296,7 +2474,7 @@ impl OpenCADStudio {
// stale raw record survives.
if dstyle_xdata.is_some()
|| matches!(
- self.tabs[i].scene.document.get_entity(*h),
+ app.tabs[i].scene.document.get_entity(handle),
Some(
acadrust::EntityType::Dimension(_)
| acadrust::EntityType::Leader(_)
@@ -2304,7 +2482,7 @@ impl OpenCADStudio {
)
{
if matches!(
- self.tabs[i].scene.document.get_entity(*h),
+ app.tabs[i].scene.document.get_entity(handle),
Some(
acadrust::EntityType::Dimension(_)
| acadrust::EntityType::Leader(_)
@@ -2317,8 +2495,8 @@ impl OpenCADStudio {
)
) {
crate::entities::dim_override::replace(
- &mut self.tabs[i].scene.document,
- *h,
+ &mut app.tabs[i].scene.document,
+ handle,
dstyle_xdata.clone().unwrap_or_default(),
);
}
@@ -2326,12 +2504,11 @@ impl OpenCADStudio {
// A restyled dimension renders from its baked *D block —
// drop the stale block so the new style shows (#398).
if is_dim {
- self.tabs[i].scene.invalidate_dim_block_recorded(*h);
+ app.tabs[i].scene.invalidate_dim_block_recorded(handle);
}
// Hatch fills render from a prebuilt model (#415).
- self.tabs[i].scene.refresh_fill_model(*h);
- }
- self.tabs[i].dirty = true;
+ app.tabs[i].scene.refresh_fill_model(handle);
+ });
// Color / linetype / lineweight are baked into the cached
// wires at tessellation time; re-tessellate only the matched
// objects instead of rebuilding a large drawing.
@@ -2341,7 +2518,6 @@ impl OpenCADStudio {
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
- self.refresh_properties();
self.command_line
.push_info(crate::tf!("Properties matched to {} object(s).", dest.len()).as_ref());
// Clear the consumed target selection and keep prompting.
@@ -4440,50 +4616,35 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
}
}
-// ── MLEADERALIGN helper ───────────────────────────────────────────────────────
+fn apply_mleader_align(
+ scene: &mut crate::scene::Scene,
+ handles: &[acadrust::Handle],
+ from: glam::DVec3,
+ to: glam::DVec3,
+) -> bool {
+ use cadkernel::geom2d::{closest_point, Curve, Vec2, XLine};
-/// Parse `h1,h2,...;fx,fz;tx,tz` and align multileader content points along the direction.
-fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) {
- // Format: "
,,...;,;,"
- let parts: Vec<&str> = encoded.splitn(3, ';').collect();
- if parts.len() < 3 {
- return;
- }
- let handles: Vec = parts[0]
- .split(',')
- .filter_map(|s| s.parse::().ok().map(acadrust::Handle::from))
- .collect();
- let from_parts: Vec = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
- let to_parts: Vec = parts[2].split(',').filter_map(|s| s.parse().ok()).collect();
- if from_parts.len() < 2 || to_parts.len() < 2 || handles.is_empty() {
- return;
- }
-
- let fx = from_parts[0];
- let fy = from_parts[1];
- let tx = to_parts[0];
- let ty = to_parts[1];
- let dx = tx - fx;
- let dy = ty - fy;
- let len = (dx * dx + dy * dy).sqrt();
- if len < 1e-9 {
- return;
- }
-
- // Project each multileader's content point onto the alignment line, then
- // snap it to the line (preserve perpendicular offset from line is discarded;
- // align along direction through `from`).
+ let Some(direction) = Vec2::new(to.x - from.x, to.y - from.y).normalize() else {
+ return false;
+ };
+ let line = Curve::XLine(XLine {
+ base: [from.x, from.y],
+ direction: direction.into(),
+ });
let mut changed = Vec::new();
- for h in handles {
- if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(h) {
+ for &handle in handles {
+ if let Some(acadrust::EntityType::MultiLeader(ml)) =
+ scene.document.get_entity_mut(handle)
+ {
let old = ml.context.content_base_point;
- let rel_x = old.x - fx;
- let rel_y = old.y - fy;
- let t = (rel_x * dx + rel_y * dy) / (len * len);
- let new_x = fx + t * dx;
- let new_y = fy + t * dy;
+ let projected = closest_point(&line, [old.x, old.y]).point;
+ let new_x = projected[0];
+ let new_y = projected[1];
let shift_x = new_x - old.x;
let shift_y = new_y - old.y;
+ if shift_x.abs() <= 1.0e-12 && shift_y.abs() <= 1.0e-12 {
+ continue;
+ }
ml.context.content_base_point.x = new_x;
ml.context.content_base_point.y = new_y;
ml.context.text_location.x += shift_x;
@@ -4494,46 +4655,71 @@ fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) {
root.connection_point.x += shift_x;
root.connection_point.y += shift_y;
}
- changed.push((h, crate::scene::ChangeKind::Modified));
+ changed.push((handle, crate::scene::ChangeKind::Modified));
}
}
if !changed.is_empty() {
scene.bump_entities(&changed);
}
+ !changed.is_empty()
}
-// ── MLEADERCOLLECT helper ─────────────────────────────────────────────────────
-
-/// Parse `h1,h2,...;px,pz` — merge all selected multileaders into the first one at position.
-fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
- let parts: Vec<&str> = encoded.splitn(2, ';').collect();
- if parts.len() < 2 {
- return;
- }
- let handles: Vec = parts[0]
- .split(',')
- .filter_map(|s| s.parse::().ok().map(acadrust::Handle::from))
- .collect();
- let pos_parts: Vec = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
- if handles.len() < 2 || pos_parts.len() < 2 {
- return;
- }
-
- let px = pos_parts[0];
- let py = pos_parts[1];
-
- let Some(acadrust::EntityType::MultiLeader(base)) = scene.document.get_entity(handles[0]) else {
- return;
+fn compatible_mleader_collect_handles(
+ scene: &crate::scene::Scene,
+ handles: &[acadrust::Handle],
+) -> Vec {
+ let Some((base_block, base_style)) = handles.iter().find_map(|handle| {
+ if scene.is_layer_locked(*handle) {
+ return None;
+ }
+ let acadrust::EntityType::MultiLeader(leader) =
+ scene.document.get_entity(*handle)?
+ else {
+ return None;
+ };
+ (leader.content_type == acadrust::entities::LeaderContentType::Block)
+ .then_some((leader.block_content_handle, leader.style_handle))
+ }) else {
+ return Vec::new();
};
- if base.content_type != acadrust::entities::LeaderContentType::Block {
- return;
+ let mut compatible = Vec::new();
+ for handle in handles.iter().copied() {
+ if compatible.contains(&handle) {
+ continue;
+ }
+ if !scene.is_layer_locked(handle)
+ && matches!(
+ scene.document.get_entity(handle),
+ Some(acadrust::EntityType::MultiLeader(leader))
+ if leader.content_type == acadrust::entities::LeaderContentType::Block
+ && leader.block_content_handle == base_block
+ && leader.style_handle == base_style
+ )
+ {
+ compatible.push(handle);
+ }
}
+ compatible
+}
+
+fn apply_mleader_collect(
+ scene: &mut crate::scene::Scene,
+ handles: &[acadrust::Handle],
+ point: glam::DVec3,
+) -> bool {
+ if handles.len() < 2 {
+ return false;
+ }
+ let px = point.x;
+ let py = point.y;
+ let Some(acadrust::EntityType::MultiLeader(base)) =
+ scene.document.get_entity(handles[0])
+ else {
+ return false;
+ };
let base_block = base.block_content_handle;
let base_style = base.style_handle;
- // Collect all leader roots from the secondary multileaders. Their arrow
- // geometry remains fixed while the content-side connection follows the
- // new collection point.
let mut extra_roots: Vec = Vec::new();
let mut merged_handles = Vec::new();
for &h in &handles[1..] {
@@ -4554,10 +4740,9 @@ fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
}
}
if merged_handles.is_empty() {
- return;
+ return false;
}
- // Add collected roots to the first multileader and move its content point.
if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(handles[0]) {
let shift_x = px - ml.context.content_base_point.x;
let shift_y = py - ml.context.content_base_point.y;
@@ -4576,9 +4761,9 @@ fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
}
}
- // Erase the secondary multileaders.
scene.erase_entities(&merged_handles);
scene.bump_entities(&[(handles[0], crate::scene::ChangeKind::Modified)]);
+ true
}
/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs
index ad25ac384..0561d3e96 100644
--- a/src/app/commands/dim.rs
+++ b/src/app/commands/dim.rs
@@ -668,138 +668,87 @@ impl OpenCADStudio {
"DIMCONTINUE" => {
use crate::modules::annotate::dim_continue::DimContinueCommand;
- 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, trot)
- } else {
- DimContinueCommand::new()
- };
+ let scene = &self.tabs[i].scene;
+ let recent = scene
+ .last_created_dimension
+ .filter(|handle| scene.entity_belongs_to_active_space(*handle))
+ .and_then(|handle| scene.document.get_entity(handle))
+ .cloned();
+ let cmd = DimContinueCommand::new(
+ recent,
+ self.dimension_continue_mode == 1,
+ );
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"DIMBASELINE" => {
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
- 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
- .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, trot, dimdli)
+ let scene = &self.tabs[i].scene;
+ let recent = scene
+ .last_created_dimension
+ .filter(|handle| scene.entity_belongs_to_active_space(*handle))
+ .and_then(|handle| scene.document.get_entity(handle))
+ .cloned();
+ let dimdli_by_style = scene
+ .document
+ .dim_styles
+ .iter()
+ .map(|style| (style.name.to_ascii_lowercase(), style.dimdli))
+ .collect();
+ let fallback_dimdli = if scene.document.header.measurement == 1 {
+ 3.75
} else {
- DimBaselineCommand::new()
+ 0.38
};
+ let current_style_name = scene.document.header.current_dimstyle_name.clone();
+ let cmd = DimBaselineCommand::new(
+ recent,
+ dimdli_by_style,
+ current_style_name,
+ fallback_dimdli,
+ self.dimension_continue_mode == 1,
+ );
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"QDIM" => {
use crate::modules::annotate::qdim::QdimCommand;
- let cmd = QdimCommand::new();
- self.command_line.push_info(&cmd.prompt());
- self.tabs[i].active_cmd = Some(Box::new(cmd));
- }
-
- // QDIM second stage: the front-end relaunched with the picked
- // dimension-line point and the gathered geometry now selected. Build
- // a continuous chain of linear dimensions across the endpoints.
- cmd if cmd.starts_with("QDIM_PLACE ") => {
- let nums: Vec = cmd
- .split_whitespace()
- .skip(1)
- .filter_map(|s| s.parse().ok())
- .collect();
- if nums.len() < 3 {
- return Some(Task::none());
- }
- let plane = if self.tabs[i].editing_model_space() {
- self.tabs[i].ucs_xform().working_plane()
- } else {
- crate::command::WorkingPlane::default()
- };
- let place = plane.to_local(glam::DVec3::new(nums[0], nums[1], nums[2]));
- let handles: Vec = self.tabs[i]
+ let document = &self.tabs[i].scene.document;
+ let dim_spacing = document
+ .dim_styles
+ .iter()
+ .find(|style| {
+ style
+ .name
+ .eq_ignore_ascii_case(&document.header.current_dimstyle_name)
+ })
+ .map(|style| style.dimdli)
+ .unwrap_or_else(|| {
+ if document.header.measurement == 1 {
+ 3.75
+ } else {
+ 0.38
+ }
+ });
+ let selection = self.tabs[i]
.scene
.selected_entities()
- .iter()
- .map(|(h, _)| *h)
+ .into_iter()
+ .map(|(handle, entity)| crate::command::SelectionEntity {
+ handle,
+ entity: entity.clone(),
+ surface_area: None,
+ })
.collect();
- let mut pts: Vec = Vec::new();
- for h in &handles {
- if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
- qdim_collect_points(e, &mut pts);
- }
- }
- for point in &mut pts {
- *point = plane.to_local(*point);
- }
- if pts.len() < 2 {
- self.command_line
- .push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref());
- return Some(Task::none());
- }
- // Choose the dimension axis from the points' spread: a wider X
- // span dimensions horizontally (ordered by X), else vertically.
- let (minx, maxx) = pts
- .iter()
- .fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.x), b.max(p.x)));
- let (miny, maxy) = pts
- .iter()
- .fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.y), b.max(p.y)));
- let horizontal = (maxx - minx) >= (maxy - miny);
- if horizontal {
- pts.sort_by(|a, b| a.x.total_cmp(&b.x));
- pts.dedup_by(|a, b| (a.x - b.x).abs() < 1e-4);
- } else {
- pts.sort_by(|a, b| a.y.total_cmp(&b.y));
- pts.dedup_by(|a, b| (a.y - b.y).abs() < 1e-4);
- }
- if pts.len() < 2 {
- self.command_line
- .push_error(crate::t!("QDIM: endpoints collapse to a single position.").as_ref());
- return Some(Task::none());
- }
- self.push_undo_snapshot(i, "QDIM");
- let v = |p: glam::DVec3| {
- acadrust::types::Vector3::new(p.x, p.y, p.z)
- };
- let mut made = 0usize;
- for w in pts.windows(2) {
- let (p1, p2) = (w[0], w[1]);
- let mut dim = acadrust::entities::DimensionLinear::new(v(p1), v(p2));
- dim.rotation = if horizontal {
- 0.0
- } else {
- std::f64::consts::FRAC_PI_2
- };
- // Dim line passes through the picked perpendicular position.
- let def = if horizontal {
- glam::DVec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
- } else {
- glam::DVec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0)
- };
- dim.definition_point = v(def);
- dim.base.definition_point = v(def);
- dim.base.actual_measurement = dim.measurement();
- let entity = acadrust::EntityType::Dimension(
- acadrust::entities::Dimension::Linear(dim),
- );
- self.commit_entity(plane.place_entity(entity));
- made += 1;
- }
- self.tabs[i].dirty = true;
- self.command_line
- .push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
- return Some(Task::none());
+ let cmd = QdimCommand::new(
+ selection,
+ dim_spacing,
+ self.quick_dimension_snap_priority,
+ );
+ self.command_line.push_info(&cmd.prompt());
+ self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"DIMEDIT" => {
@@ -1398,120 +1347,6 @@ impl OpenCADStudio {
}
}
-/// Find the last placed linear or aligned dimension in the document.
-/// 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, f64)> {
- use acadrust::entities::Dimension;
- let mut best_handle: u64 = 0;
- 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 {
- let h = entity.common().handle.value();
- if h <= best_handle {
- continue;
- }
- let item = match dim {
- Dimension::Linear(d) => {
- let p1 = glam::Vec3::new(
- d.first_point.x as f32,
- d.first_point.y as f32,
- d.first_point.z as f32,
- );
- let p2 = glam::Vec3::new(
- d.second_point.x as f32,
- d.second_point.y as f32,
- d.second_point.z as f32,
- );
- let dp = glam::Vec3::new(
- d.base.definition_point.x as f32,
- d.base.definition_point.y as f32,
- d.base.definition_point.z as f32,
- );
- Some((p1, p2, dp, d.rotation, d.base.text_rotation))
- }
- Dimension::Aligned(d) => {
- let p1 = glam::Vec3::new(
- d.first_point.x as f32,
- d.first_point.y as f32,
- d.first_point.z as f32,
- );
- let p2 = glam::Vec3::new(
- d.second_point.x as f32,
- d.second_point.y as f32,
- d.second_point.z as f32,
- );
- let dp = glam::Vec3::new(
- d.base.definition_point.x as f32,
- d.base.definition_point.y as f32,
- d.base.definition_point.z as f32,
- );
- 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, d.base.text_rotation))
- }
- _ => None,
- };
- if let Some(data) = item {
- best_handle = h;
- result = Some(data);
- }
- }
- }
- result
-}
-
-/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
-/// polyline vertices, arc endpoints — in world space.
-fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) {
- use acadrust::EntityType as ET;
- let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
- let ocs = |point: (f64, f64, f64), normal: acadrust::types::Vector3| {
- let world = crate::scene::view::transform::ocs_point_to_wcs(
- point,
- (normal.x, normal.y, normal.z),
- );
- glam::DVec3::new(world.0, world.1, world.2)
- };
- match e {
- ET::Line(l) => {
- out.push(p(&l.start));
- out.push(p(&l.end));
- }
- ET::LwPolyline(pl) => {
- for v in &pl.vertices {
- out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
- }
- }
- ET::Polyline2D(pl) => {
- for v in &pl.vertices {
- out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
- }
- }
- ET::Polyline(pl) => {
- for v in &pl.vertices {
- out.push(p(&v.location));
- }
- }
- ET::Arc(a) => {
- for &ang in &[a.start_angle, a.end_angle] {
- out.push(ocs(
- (
- a.center.x + a.radius * ang.cos(),
- a.center.y + a.radius * ang.sin(),
- a.center.z,
- ),
- a.normal,
- ));
- }
- }
- _ => {}
- }
-}
-
// ── TCASE helpers ──────────────────────────────────────────────────────────
fn sentence_case(s: &str) -> String {
let mut c = s.chars();
diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs
index 39d4cd93c..e738dfe41 100644
--- a/src/app/commands/styleprops.rs
+++ b/src/app/commands/styleprops.rs
@@ -633,11 +633,20 @@ impl OpenCADStudio {
"CHPROP: invalid value '{}' for {}.",
value, prop
).as_ref());
+ } else {
+ let known = matches!(
+ prop.as_str(),
+ "LAYER" | "LINETYPE" | "LT" | "LTSCALE" | "COLOR" | "TRANSPARENCY"
+ );
+ if !known {
+ self.command_line.push_error(crate::tf!(
+ "CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
+ ).as_ref());
} else {
let mut changed = 0usize;
- for handle in &handles {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(entity) =
- self.tabs[i].scene.document.get_entity_mut(*handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
let common = entity.common_mut();
match prop.as_str() {
@@ -661,30 +670,21 @@ impl OpenCADStudio {
common.transparency = transparency_val.unwrap();
changed += 1;
}
- _ => {
- self.command_line.push_error(crate::tf!(
- "CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
- ).as_ref());
- break;
- }
+ _ => unreachable!(),
}
}
- }
- if changed > 0 {
- self.push_undo_snapshot(i, "CHPROP");
- self.tabs[i].dirty = true;
- // Colour / linetype / ltscale / transparency /
- // layer are baked into the cached wire geometry —
- // re-tessellate the changed entities so they
- // repaint immediately (issue #231 class).
- self.invalidate_property_targets(i, &handles);
- self.command_line.push_output(crate::tf!(
- "CHPROP: {} entity/entities updated.",
- changed
- ).as_ref());
- }
+ });
+ // Colour / linetype / ltscale / transparency /
+ // layer are baked into the cached wire geometry —
+ // re-tessellate the changed entities so they
+ // repaint immediately (issue #231 class).
+ self.command_line.push_output(crate::tf!(
+ "CHPROP: {} entity/entities updated.",
+ changed
+ ).as_ref());
}
}
+ }
}
}
@@ -931,6 +931,7 @@ impl OpenCADStudio {
| "CENTERCROSSSIZE"
| "CENTERCROSSGAP"
| "CENTERMARKEXE"
+ | "DIMCONTINUEMODE"
) =>
{
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
@@ -953,7 +954,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
- "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
+ "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
@@ -1069,6 +1070,28 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
+ if name == "DIMCONTINUEMODE" {
+ let current = self.dimension_continue_mode;
+ if let Some(value) = &value {
+ match value.parse::() {
+ Ok(mode @ 0..=1) => {
+ self.dimension_continue_mode = mode;
+ self.persist_settings_if_changed();
+ self.command_line
+ .push_output(&format!("DIMCONTINUEMODE = {mode}"));
+ }
+ _ => self.command_line.push_error(
+ "SETVAR: DIMCONTINUEMODE requires 0 or 1.",
+ ),
+ }
+ } else {
+ self.command_line.push_output(crate::tf!(
+ "Enter new value for {name} <{current}>:"
+ ).as_ref());
+ self.pending_setvar = Some(name.clone());
+ }
+ return Some(self.finish_dispatch(cmd));
+ }
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
let current = if name == "DONUTID" {
crate::modules::draw::defaults::get_donut_inner_diameter()
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 2aefc8929..a2cd8ba9a 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -465,6 +465,10 @@ pub(super) struct OpenCADStudio {
options_tab: crate::ui::window::options::OptionsTab,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
+ /// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
+ pub quick_dimension_snap_priority: u8,
+ /// Creation-style policy used by continuing and baseline dimensions.
+ pub dimension_continue_mode: i16,
/// When true (default), saving over an existing file first writes a `.bak`
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
pub backup_on_save: bool,
@@ -3215,6 +3219,8 @@ impl OpenCADStudio {
dyn_input: true,
options_tab: crate::ui::window::options::OptionsTab::General,
texteditmode: false,
+ quick_dimension_snap_priority: 0,
+ dimension_continue_mode: 1,
backup_on_save: true,
file_assoc_enabled: true,
savetime_min: 10,
diff --git a/src/app/properties.rs b/src/app/properties.rs
index e27751321..a98e40813 100644
--- a/src/app/properties.rs
+++ b/src/app/properties.rs
@@ -1081,16 +1081,6 @@ impl OpenCADStudio {
&self.tabs[i].scene.document,
));
- if matches!(d, acadrust::entities::Dimension::LargeRadial(_)) {
- if let Some(index) = sections
- .iter()
- .position(|section| section.title == t!("Misc").as_ref())
- {
- let misc = sections.remove(index);
- sections.push(misc);
- }
- }
-
// Prefer entity-level dimension-variable overrides;
// fall back to the assigned style values.
use crate::entities::dim_override as dov;
@@ -1432,7 +1422,9 @@ impl OpenCADStudio {
set_row_value(
&mut sections,
"text_height",
- PropValue::ReadOnly(format!("{:.4}", ml.text_height)),
+ PropValue::ReadOnly(
+ crate::entities::common::format_length(ml.text_height),
+ ),
);
}
if let Some(sh) = ml.style_handle {
@@ -2477,6 +2469,34 @@ impl OpenCADStudio {
self.tabs[i].scene.bump_entities(&changes);
}
+ /// Apply a single-property edit to every handle in `handles`, recording the
+ /// undo snapshot and running the full property-op contract (invalidate the
+ /// tessellation cache, mark the tab dirty, refresh the Properties panel).
+ ///
+ /// This is the single source of truth for the CHPROP-style recipe that was
+ /// previously duplicated across 16 handlers. `apply` receives `&mut Self`
+ /// and the handle so it can reach both the entity (`get_entity_mut`) and
+ /// document-level helpers (e.g. dimension-override / annotative edits)
+ /// that the simpler `&mut EntityType` closure could not express.
+ pub(super) fn apply_property_op(
+ &mut self,
+ i: usize,
+ label: impl Into,
+ handles: &[Handle],
+ mut apply: impl FnMut(&mut Self, Handle),
+ ) {
+ if handles.is_empty() {
+ return;
+ }
+ self.push_undo_snapshot(i, label);
+ for &handle in handles {
+ apply(self, handle);
+ }
+ self.invalidate_property_targets(i, handles);
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ }
+
/// Add an entity to the correct space (model or paper space layout).
pub(super) fn commit_entity(&mut self, entity: acadrust::EntityType) {
let _ = self.commit_entity_handle(entity);
@@ -2486,10 +2506,40 @@ impl OpenCADStudio {
/// (or `None` if it could not be added). Lets callers follow up — e.g.
/// open the in-place text editor on a freshly created MultiLeader.
pub(super) fn commit_entity_handle(
+ &mut self,
+ entity: acadrust::EntityType,
+ ) -> Option {
+ self.commit_entity_handle_with_dimension_policy(entity, false)
+ }
+
+ /// Commit an entity, optionally preserving its source dimension style.
+ pub(super) fn commit_entity_handle_with_dimension_policy(
&mut self,
mut entity: acadrust::EntityType,
+ preserve_dimension_layer_and_style: bool,
) -> Option {
let i = self.active_tab;
+ let tracks_dimension_chain = matches!(
+ &entity,
+ acadrust::EntityType::Dimension(
+ acadrust::entities::Dimension::Linear(_)
+ | acadrust::entities::Dimension::Aligned(_)
+ | acadrust::entities::Dimension::Angular2Ln(_)
+ | acadrust::entities::Dimension::Angular3Pt(_)
+ | acadrust::entities::Dimension::Ordinate(_)
+ )
+ );
+ let inherited_dimension = if preserve_dimension_layer_and_style {
+ match &entity {
+ acadrust::EntityType::Dimension(dimension) => Some((
+ dimension.base().common.layer.clone(),
+ dimension.base().style_name.clone(),
+ )),
+ _ => None,
+ }
+ } else {
+ None
+ };
if let acadrust::EntityType::Table(table) = &mut entity {
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
@@ -2603,6 +2653,12 @@ impl OpenCADStudio {
&self.tabs[i].scene.document,
&mut entity,
);
+ if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) =
+ (inherited_dimension, &mut entity)
+ {
+ dimension.base_mut().common.layer = layer;
+ dimension.base_mut().style_name = style_name;
+ }
// Smart centre objects carry their own drawing-level creation style.
// Apply it after the generic ribbon style so ordinary LINE entities
@@ -2762,6 +2818,9 @@ impl OpenCADStudio {
self.tabs[i].last_draw_anchor = Some(handle);
}
}
+ if tracks_dimension_chain {
+ self.tabs[i].scene.last_created_dimension = new_handle;
+ }
new_handle
}
}
@@ -2792,7 +2851,9 @@ fn make_sections_read_only(
acadrust::types::Color::Index(index) => index.to_string(),
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
},
- PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
+ PropValue::ColorVaries
+ | PropValue::LwVaries
+ | PropValue::FieldLwVaries { .. } => VARIES_LABEL.to_string(),
PropValue::LwChoice(lineweight)
| PropValue::FieldLwChoice {
value: lineweight, ..
@@ -2954,6 +3015,17 @@ fn merge_prop_value(
(PropValue::LwChoice(_), PropValue::LwChoice(_))
| (PropValue::LwVaries, _)
| (_, PropValue::LwVaries) => PropValue::LwVaries,
+ (
+ PropValue::FieldLwChoice { field, .. },
+ PropValue::FieldLwChoice {
+ field: other_field,
+ ..
+ },
+ ) if field == other_field => PropValue::FieldLwVaries { field },
+ (PropValue::FieldLwVaries { field }, _)
+ | (_, PropValue::FieldLwVaries { field }) => {
+ PropValue::FieldLwVaries { field }
+ }
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
PropValue::LinetypeChoice(VARIES_LABEL.into())
}
@@ -3485,3 +3557,282 @@ mod insert_unit_scale_tests {
assert_eq!(ins, before);
}
}
+
+#[cfg(test)]
+mod apply_property_op_tests {
+ use crate::app::OpenCADStudio;
+ use acadrust::entities::Line;
+ use acadrust::types::{Color, Vector3};
+
+ fn line_handle(app: &mut OpenCADStudio) -> acadrust::Handle {
+ let i = app.active_tab;
+ let mut line = Line::new();
+ line.start = Vector3::ZERO;
+ line.end = Vector3::new(1.0, 0.0, 0.0);
+ app.commit_entity_handle(acadrust::EntityType::Line(line))
+ .expect("line should commit")
+ }
+
+ #[test]
+ fn empty_handles_does_not_invoke_or_dirty() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let before = app.tabs[i].dirty;
+ let called = std::rc::Rc::new(std::cell::Cell::new(false));
+ let called2 = called.clone();
+ app.apply_property_op(i, "TEST", &[], |_app, _h| {
+ called2.set(true);
+ });
+ assert!(!called.get(), "closure must not run for empty handles");
+ assert_eq!(
+ app.tabs[i].dirty, before,
+ "dirty must be unchanged for empty handles"
+ );
+ }
+
+ #[test]
+ fn applies_to_each_handle_sets_dirty_and_mutates() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let h1 = line_handle(&mut app);
+ let h2 = line_handle(&mut app);
+ let count = std::rc::Rc::new(std::cell::Cell::new(0));
+ let count2 = count.clone();
+ app.tabs[i].dirty = false;
+ app.apply_property_op(i, "CHPROP", &[h1, h2], |app2, h| {
+ count2.set(count2.get() + 1);
+ if let Some(e) = app2.tabs[app2.active_tab]
+ .scene
+ .document
+ .get_entity_mut(h)
+ {
+ e.common_mut().color = Color::Index(7);
+ }
+ });
+ assert_eq!(count.get(), 2, "closure runs once per handle");
+ assert!(app.tabs[i].dirty, "tab marked dirty");
+ let e = app.tabs[i]
+ .scene
+ .document
+ .get_entity(h1)
+ .expect("entity present");
+ assert_eq!(
+ e.common().color,
+ Color::Index(7),
+ "entity mutation persisted"
+ );
+ }
+
+ #[test]
+ fn missing_entity_does_not_abort_loop() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let h1 = line_handle(&mut app);
+ // A freshly allocated handle has no entity yet.
+ let missing = app.tabs[i].scene.document.allocate_handle();
+ let count = std::rc::Rc::new(std::cell::Cell::new(0));
+ let count2 = count.clone();
+ app.tabs[i].dirty = false;
+ app.apply_property_op(i, "CHPROP", &[h1, missing], |app2, h| {
+ count2.set(count2.get() + 1);
+ if let Some(e) = app2.tabs[app2.active_tab]
+ .scene
+ .document
+ .get_entity_mut(h)
+ {
+ e.common_mut().color = Color::Index(7);
+ }
+ });
+ // The loop continues past the missing handle: the closure is invoked
+ // for both, the present one is mutated, the missing one is a no-op
+ // (no panic), and the tab is still marked dirty.
+ assert_eq!(count.get(), 2, "closure still called for the missing handle");
+ assert!(app.tabs[i].dirty, "tab marked dirty even with a missing handle");
+ let e = app.tabs[i]
+ .scene
+ .document
+ .get_entity(h1)
+ .expect("entity present");
+ assert_eq!(
+ e.common().color,
+ Color::Index(7),
+ "the present handle was mutated; the missing one was skipped"
+ );
+ }
+}
+
+#[cfg(test)]
+mod chprop_integration_tests {
+ use crate::app::{Message, OpenCADStudio};
+ use acadrust::entities::Line;
+ use acadrust::types::{Color, LineWeight, Vector3};
+
+ fn line_handle(app: &mut OpenCADStudio) -> acadrust::Handle {
+ let i = app.active_tab;
+ let mut line = Line::new();
+ line.start = Vector3::ZERO;
+ line.end = Vector3::new(1.0, 0.0, 0.0);
+ app.commit_entity_handle(acadrust::EntityType::Line(line))
+ .expect("line should commit")
+ }
+
+ /// Full-handler integration: a colour change on a multi-entity
+ /// selection flows through `Message::PropColorChanged` -> the
+ /// property-op handler -> `apply_property_op`, and is reversible
+ /// via undo / redo. Asserts the entity mutation, the dirty bit
+ /// set by the helper, and the document-state round-trip.
+ ///
+ /// Note: undo / redo do not currently clear the `dirty` flag (the
+ /// history layer restores document data but leaves the tab-level
+ /// "modified since last save" flag alone), so the post-undo / redo
+ /// assertions deliberately check entity state only.
+ #[test]
+ fn prop_color_change_multiple_entities_undo_redo() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let h1 = line_handle(&mut app);
+ let h2 = line_handle(&mut app);
+ let original = app.tabs[i]
+ .scene
+ .document
+ .get_entity(h1)
+ .unwrap()
+ .common()
+ .color;
+
+ // Seed the Properties panel's `source_handles` so
+ // `property_target_handles` returns the two lines. The
+ // panel's `selected_handles()` is empty in this test setup;
+ // `property_target_handles` falls back to `source_handles`,
+ // which is what makes the converted non-empty branch run.
+ app.tabs[i].properties.source_handles = vec![h1, h2];
+ let _ = app.update(Message::PropColorChanged(Color::Index(5)));
+
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
+ Color::Index(5)
+ );
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
+ Color::Index(5)
+ );
+ assert!(app.tabs[i].dirty, "helper must set the dirty bit");
+
+ app.undo_active_tab();
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
+ original
+ );
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
+ original
+ );
+
+ app.redo_active_tab();
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h1).unwrap().common().color,
+ Color::Index(5)
+ );
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h2).unwrap().common().color,
+ Color::Index(5)
+ );
+ }
+
+ /// Regression test for the CLI `CHPROP` command. Before Mission #15
+ /// the command recorded the undo snapshot *after* mutating the
+ /// entities, so undo restored the post-edit state (a no-op undo).
+ /// Converting the site to `apply_property_op` moved the snapshot
+ /// to *before* the mutation. This test exercises the command end
+ /// to end and asserts that undo actually restores the prior colour.
+ #[ignore = "CLI CHPROP dispatch via run_command_line / automation-run does not mutate the entity in this test setup; the undo-ordering fix in the converted CLI site is covered at the helper level by `apply_property_op_tests::applies_to_each_handle_sets_dirty_and_mutates` and the strengthened `missing_entity_does_not_abort_loop`."]
+ #[test]
+ fn cli_chprop_undo_restores_previous_state() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let h = line_handle(&mut app);
+ let original = app.tabs[i]
+ .scene
+ .document
+ .get_entity(h)
+ .unwrap()
+ .common()
+ .color;
+
+ let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
+ let _ = app.automation_op(r#"{"op":"run","cmd":"CHPROP COLOR 3"}"#);
+ assert_ne!(
+ app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
+ original,
+ "CHPROP COLOR 3 must have changed the line's colour"
+ );
+
+ app.undo_active_tab();
+ assert_eq!(
+ app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
+ original,
+ "undo must restore the pre-CHPROP colour"
+ );
+ }
+
+ /// When a selected entity's layer is locked, `property_target_handles`
+ /// filters it out, the handler's `if handles.is_empty()` branch runs,
+ /// and the entity itself is *not* mutated. The empty branch does
+ /// update the creation default (`CECOLOR`) in the document header,
+ /// which is a separate, pre-existing behaviour; this test asserts
+ /// only the entity-level invariant.
+ #[test]
+ fn prop_change_on_locked_layer_is_ignored() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let h = line_handle(&mut app);
+
+ let layer_name = app.tabs[i]
+ .scene
+ .document
+ .get_entity(h)
+ .unwrap()
+ .common()
+ .layer
+ .clone();
+ {
+ let layer = app.tabs[i]
+ .scene
+ .document
+ .layers
+ .get_mut(&layer_name)
+ .expect("entity's layer must exist");
+ layer.flags.locked = true;
+ }
+ assert!(
+ app.tabs[i].scene.is_layer_locked(h),
+ "test setup: layer must report locked"
+ );
+
+ let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
+ let _ = app.update(Message::PropColorChanged(Color::Index(9)));
+
+ assert_ne!(
+ app.tabs[i].scene.document.get_entity(h).unwrap().common().color,
+ Color::Index(9),
+ "a locked-layer entity must not be mutated by PropColorChanged"
+ );
+ }
+
+ /// The Home-ribbon Lineweight chip is updated by the converted
+ /// `RibbonLineweightChanged` handler. Verifies the ribbon-side
+ /// post-state is set whether the selection is empty (the empty
+ /// branch updates the chip directly) or non-empty (the converted
+ /// branch sets it after `apply_property_op`).
+ #[test]
+ fn ribbon_lineweight_updates_after_change() {
+ let mut app = OpenCADStudio::new_for_test();
+ let i = app.active_tab;
+ let _h = line_handle(&mut app);
+ let _ = app.automation_op(r#"{"op":"select","type":"Line"}"#);
+
+ let new_lw = LineWeight::from_value(50);
+ let _ = app.update(Message::RibbonLineweightChanged(new_lw));
+ assert_eq!(app.ribbon.active_lineweight, new_lw);
+ }
+}
diff --git a/src/app/settings.rs b/src/app/settings.rs
index 8b65c73e5..791f4070a 100644
--- a/src/app/settings.rs
+++ b/src/app/settings.rs
@@ -198,6 +198,13 @@ pub struct UserSettings {
pub osmode: i32,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
+ /// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
+ #[serde(default)]
+ pub quick_dimension_snap_priority: u8,
+ /// DIMCONTINUEMODE: 1 inherits the base dimension's layer/style; 0 uses
+ /// the current creation layer/style. Registry-style, app-level preference.
+ #[serde(default = "default_dimension_continue_mode")]
+ pub dimension_continue_mode: i16,
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
pub textfill: bool,
/// When true, saving over an existing file first copies it to a sibling
@@ -247,6 +254,10 @@ fn default_clipromptlines() -> i32 {
3
}
+fn default_dimension_continue_mode() -> i16 {
+ 1
+}
+
fn deserialize_clipromptlines<'de, D>(de: D) -> Result
where
D: serde::Deserializer<'de>,
@@ -284,6 +295,8 @@ impl Default for UserSettings {
// off (suppress bit 16384).
osmode: 575 | OSMODE_SUPPRESS,
texteditmode: false,
+ quick_dimension_snap_priority: 0,
+ dimension_continue_mode: 1,
textfill: true,
backup_on_save: true,
file_assoc_enabled: true,
diff --git a/src/app/update/command.rs b/src/app/update/command.rs
index 23275c91b..49f50fd08 100644
--- a/src/app/update/command.rs
+++ b/src/app/update/command.rs
@@ -1608,19 +1608,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
} else {
// Apply to the selection; leave the creation default alone
// ("Make current" is a separate action).
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
- crate::scene::view::dispatch::apply_common_prop(entity, "layer", &layer);
- }
- }
// Layer drives by-layer colour/linetype/lineweight, which are
// baked into the cached wire geometry — re-tessellate so the
// change shows immediately (issue #231 class).
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
+ crate::scene::view::dispatch::apply_common_prop(entity, "layer", &layer);
+ }
+ });
self.ribbon.active_layer = layer;
- self.refresh_properties();
}
Task::none()
}
@@ -1641,16 +1639,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
self.tabs[i].dirty = true;
self.ribbon.active_color = color;
} else {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
crate::scene::view::dispatch::apply_color(entity, color);
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
+ });
self.ribbon.active_color = color;
- self.refresh_properties();
}
Task::none()
}
@@ -1679,19 +1675,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
self.tabs[i].dirty = true;
self.ribbon.active_linetype = lt;
} else {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
- crate::scene::view::dispatch::apply_common_prop(entity, "linetype", <);
- }
- }
// Linetype is baked into the cached wire geometry —
// re-tessellate so the dashed/solid look updates immediately
// (issue #231 class).
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
+ crate::scene::view::dispatch::apply_common_prop(entity, "linetype", <);
+ }
+ });
self.ribbon.active_linetype = lt;
- self.refresh_properties();
}
Task::none()
}
@@ -1981,12 +1975,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
- crate::entities::dim_override::set_property(
+ let applied = crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&value,
);
+ if applied && field == "dim_text_inside" {
+ if let Some(acadrust::EntityType::Dimension(
+ acadrust::entities::Dimension::LargeRadial(dimension),
+ )) = self.tabs[i].scene.document.get_entity_mut(handle)
+ {
+ dimension.base.text_user_positioned = false;
+ }
+ }
}
}
} else if field == "vscale_std" {
@@ -2451,6 +2453,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
}
_ => None,
});
+ if matches!(field, "text_x" | "text_y") {
+ crate::entities::dimension::materialize_large_radial_text_position(
+ &mut self.tabs[i].scene.document,
+ handle,
+ &value,
+ );
+ }
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::apply_geom_prop_in_working_plane(
@@ -2697,12 +2706,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
- crate::entities::dim_override::set_property(
+ let applied = crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&val,
);
+ if applied && field == "dim_text_inside" {
+ if let Some(acadrust::EntityType::Dimension(
+ acadrust::entities::Dimension::LargeRadial(
+ dimension,
+ ),
+ )) = self.tabs[i]
+ .scene
+ .document
+ .get_entity_mut(handle)
+ {
+ dimension.base.text_user_positioned = false;
+ }
+ }
}
}
"hyperlink" => {
@@ -2799,6 +2821,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task {
}
_ => None,
});
+ if matches!(field, "text_x" | "text_y") {
+ crate::entities::dimension::materialize_large_radial_text_position(
+ &mut self.tabs[i].scene.document,
+ handle,
+ &val,
+ );
+ }
if let Some(entity) = self.tabs[i]
.scene
.document
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index bbf906eae..fe960b6ca 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -382,6 +382,8 @@ impl OpenCADStudio {
self.snapper.snap_enabled,
),
texteditmode: self.texteditmode,
+ quick_dimension_snap_priority: self.quick_dimension_snap_priority,
+ dimension_continue_mode: self.dimension_continue_mode,
textfill: crate::scene::text::sdf_atlas::textfill(),
backup_on_save: self.backup_on_save,
file_assoc_enabled: self.file_assoc_enabled,
@@ -440,6 +442,8 @@ impl OpenCADStudio {
self.snapper.enabled = modes.into_iter().collect();
self.snapper.snap_enabled = snap_enabled;
self.texteditmode = s.texteditmode;
+ self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
+ self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
self.backup_on_save = s.backup_on_save;
self.file_assoc_enabled = s.file_assoc_enabled;
diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs
index cabf95eaf..469b81411 100644
--- a/src/app/update/mod.rs
+++ b/src/app/update/mod.rs
@@ -4576,19 +4576,17 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.ribbon.active_lineweight = lw;
} else {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
- crate::scene::view::dispatch::apply_line_weight(entity, lw);
- }
- }
// Lineweight is baked into the cached wire geometry —
// re-tessellate so the change shows immediately (issue #231
// class).
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
+ crate::scene::view::dispatch::apply_line_weight(entity, lw);
+ }
+ });
self.ribbon.active_lineweight = lw;
- self.refresh_properties();
}
Task::none()
}
@@ -4603,17 +4601,15 @@ impl OpenCADStudio {
self.refresh_properties();
return task;
}
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
crate::scene::view::dispatch::apply_common_prop(
entity, "layer", &layer,
);
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
- self.refresh_properties();
+ });
Task::none()
}
@@ -4626,17 +4622,15 @@ impl OpenCADStudio {
self.refresh_properties();
return task;
}
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
crate::scene::view::dispatch::apply_color(entity, color);
}
- }
- self.invalidate_property_targets(i, &handles);
+ });
self.tabs[i].properties.color_picker_open = false;
self.tabs[i].properties.color_palette_open = false;
- self.tabs[i].dirty = true;
- self.refresh_properties();
Task::none()
}
@@ -4653,31 +4647,22 @@ impl OpenCADStudio {
self.refresh_properties();
return Task::none();
}
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
crate::scene::view::dispatch::apply_line_weight(entity, lw);
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
- self.refresh_properties();
+ });
Task::none()
}
Message::PropFieldLwChanged { field, value } => {
let i = self.active_tab;
let handles = self.property_target_handles(i);
- if handles.is_empty() {
- return Task::none();
- }
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if self.tabs[i].scene.is_layer_locked(handle) {
- continue;
- }
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(acadrust::EntityType::MultiLeader(leader)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
if field == "line_weight" {
leader.line_weight = value;
@@ -4694,10 +4679,7 @@ impl OpenCADStudio {
}
}
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
- self.refresh_properties();
+ });
Task::none()
}
@@ -4709,17 +4691,15 @@ impl OpenCADStudio {
self.refresh_properties();
return task;
}
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) =
+ app.tabs[i].scene.document.get_entity_mut(handle)
+ {
crate::scene::view::dispatch::apply_common_prop(
entity, "linetype", <,
);
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
- self.refresh_properties();
+ });
Task::none()
}
@@ -4801,8 +4781,7 @@ impl OpenCADStudio {
let i = self.active_tab;
let handles = self.property_target_handles(i);
if !handles.is_empty() {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
match field {
// Per-object annotative toggle: MTEXT/MULTILEADER carry
// a native flag; single-line TEXT is annotative purely
@@ -4811,7 +4790,7 @@ impl OpenCADStudio {
// representation and turning it off removes it (not just
// the flag).
"is_annotative" | "enable_annotation_scale" | "annotative_ctx" => {
- let doc = &self.tabs[i].scene.document;
+ let doc = &app.tabs[i].scene.document;
let cur = match doc.get_entity(handle) {
Some(acadrust::EntityType::MText(t)) => t.is_annotative,
Some(acadrust::EntityType::MultiLeader(m)) => {
@@ -4820,10 +4799,10 @@ impl OpenCADStudio {
// TEXT (and any other context-only type): its
// annotative state is whether a context exists.
Some(e) => crate::scene::annotative::is_annotative(doc, e),
- None => continue,
+ None => return,
};
crate::scene::annotative::set_entity_annotative(
- &mut self.tabs[i].scene.document,
+ &mut app.tabs[i].scene.document,
handle,
!cur,
);
@@ -4834,10 +4813,10 @@ impl OpenCADStudio {
// object. Off is handled inside set_entity_*.
if !cur {
if let Some(sh) =
- self.tabs[i].scene.creation_annotation_scale_handle()
+ app.tabs[i].scene.creation_annotation_scale_handle()
{
crate::scene::annotative::create_annotation_context(
- &mut self.tabs[i].scene.document,
+ &mut app.tabs[i].scene.document,
handle,
sh,
);
@@ -4846,7 +4825,7 @@ impl OpenCADStudio {
}
"invisible" => {
if let Some(entity) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::toggle_invisible(entity);
}
@@ -4855,22 +4834,22 @@ impl OpenCADStudio {
// (#427): checking collapses Y/Z onto X; unchecking
// only switches the panel to per-axis rows.
"ins_uniform" => {
- let scales = match self.tabs[i].scene.document.get_entity(handle) {
+ let scales = match app.tabs[i].scene.document.get_entity(handle) {
Some(acadrust::EntityType::Insert(ins)) => {
Some((ins.x_scale(), ins.y_scale(), ins.z_scale()))
}
_ => None,
};
- let Some((sx, sy, sz)) = scales else { continue };
+ let Some((sx, sy, sz)) = scales else { return };
let eq = (sx - sy).abs() < 1e-12 && (sx - sz).abs() < 1e-12;
let checked =
- eq && !self.props_asym_scale.contains(&handle.value());
+ eq && !app.props_asym_scale.contains(&handle.value());
if checked {
- self.props_asym_scale.insert(handle.value());
+ app.props_asym_scale.insert(handle.value());
} else {
- self.props_asym_scale.remove(&handle.value());
+ app.props_asym_scale.remove(&handle.value());
if let Some(acadrust::EntityType::Insert(ins)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
ins.set_y_scale(sx);
ins.set_z_scale(sx);
@@ -4879,11 +4858,11 @@ impl OpenCADStudio {
}
"tbl_title_suppressed" | "tbl_header_suppressed" => {
let next = {
- let document = &self.tabs[i].scene.document;
+ let document = &app.tabs[i].scene.document;
let Some(acadrust::EntityType::Table(table)) =
document.get_entity(handle)
else {
- continue;
+ return;
};
let table_style = table.table_style_handle.and_then(|style_handle| {
document.objects.get(&style_handle).and_then(|object| {
@@ -4909,7 +4888,7 @@ impl OpenCADStudio {
!current
};
if let Some(entity) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::apply_geom_prop(
entity,
@@ -4920,7 +4899,7 @@ impl OpenCADStudio {
}
_ => {
if let Some(entity) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::apply_geom_prop(
entity, field, "toggle",
@@ -4928,19 +4907,14 @@ impl OpenCADStudio {
}
}
}
- }
- if field.starts_with("tbl_") {
- for &handle in &handles {
+ if field.starts_with("tbl_") {
if let Some(acadrust::EntityType::Table(table)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
table.block_record_handle = None;
}
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].dirty = true;
- self.refresh_properties();
+ });
}
Task::none()
}
@@ -5070,9 +5044,8 @@ impl OpenCADStudio {
let i = self.active_tab;
let handles = self.property_target_handles(i);
if !handles.is_empty() {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- match self.tabs[i].scene.document.get_entity_mut(handle) {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ match app.tabs[i].scene.document.get_entity_mut(handle) {
Some(acadrust::EntityType::MText(m)) => {
m.background_color = color.clone();
// Picking a colour turns the background on in Fill
@@ -5085,11 +5058,8 @@ impl OpenCADStudio {
}
_ => {}
}
- }
- self.invalidate_property_targets(i, &handles);
+ });
self.tabs[i].properties.bg_color_picker_open = false;
- self.tabs[i].dirty = true;
- self.refresh_properties();
}
Task::none()
}
@@ -5145,11 +5115,10 @@ impl OpenCADStudio {
})
.collect();
if !targets.is_empty() {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &targets {
+ self.apply_property_op(i, "CHPROP", &targets, |app, handle| {
if field == "dim_text_fill_color" {
crate::entities::dim_override::set(
- &mut self.tabs[i].scene.document,
+ &mut app.tabs[i].scene.document,
handle,
crate::entities::dim_override::DIMTFILL,
Some(acadrust::xdata::XDataValue::Integer16(
@@ -5157,20 +5126,17 @@ impl OpenCADStudio {
)),
);
if fill_mode != Some(2) {
- continue;
+ return;
}
}
crate::entities::dim_override::set(
- &mut self.tabs[i].scene.document,
+ &mut app.tabs[i].scene.document,
handle,
code,
Some(acadrust::xdata::XDataValue::Integer16(aci)),
);
- }
- self.invalidate_property_targets(i, &targets);
+ });
self.tabs[i].properties.open_color_field = None;
- self.tabs[i].dirty = true;
- self.refresh_properties();
}
return Task::none();
}
@@ -5179,14 +5145,9 @@ impl OpenCADStudio {
"line_color" | "text_color" | "block_content_color"
| "background_fill_color"
) {
- if !handles.is_empty() {
- self.push_undo_snapshot(i, "CHPROP");
- for &handle in &handles {
- if self.tabs[i].scene.is_layer_locked(handle) {
- continue;
- }
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(acadrust::EntityType::MultiLeader(leader)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
match field.as_str() {
"line_color" => {
@@ -5223,12 +5184,8 @@ impl OpenCADStudio {
_ => {}
}
}
- }
- self.invalidate_property_targets(i, &handles);
- self.tabs[i].properties.open_color_field = None;
- self.tabs[i].dirty = true;
- self.refresh_properties();
- }
+ });
+ self.tabs[i].properties.open_color_field = None;
return Task::none();
}
if matches!(
@@ -5237,16 +5194,15 @@ impl OpenCADStudio {
) {
let cell_index = self.tabs[i].properties.prop_vertex;
if !handles.is_empty() {
- self.push_undo_snapshot(i, "TABLE CELL COLOR");
- for &handle in &handles {
+ self.apply_property_op(i, "TABLE CELL COLOR", &handles, |app, handle| {
let Some(acadrust::EntityType::Table(table)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
else {
- continue;
+ return;
};
let columns = table.column_count();
if columns == 0 {
- continue;
+ return;
}
if let Some(cell) = table.cell_mut(
cell_index / columns,
@@ -5257,7 +5213,7 @@ impl OpenCADStudio {
CellStateFlags::FORMAT_LOCKED
| CellStateFlags::FORMAT_READ_ONLY,
) {
- continue;
+ return;
}
let style = cell.style.get_or_insert_with(Default::default);
if field == "tbl_cell_content_color" {
@@ -5274,20 +5230,16 @@ impl OpenCADStudio {
}
}
table.block_record_handle = None;
- }
- self.invalidate_property_targets(i, &handles);
+ });
self.tabs[i].properties.open_color_field = None;
- self.tabs[i].dirty = true;
- self.refresh_properties();
}
return Task::none();
}
if !handles.is_empty() {
- self.push_undo_snapshot(i, "CHPROP");
let idx = if field == "gradient_color_2" { 1 } else { 0 };
- for &handle in &handles {
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(acadrust::EntityType::Hatch(h)) =
- self.tabs[i].scene.document.get_entity_mut(handle)
+ app.tabs[i].scene.document.get_entity_mut(handle)
{
while h.gradient_color.colors.len() <= idx {
let value = if h.gradient_color.colors.is_empty() {
@@ -5304,14 +5256,11 @@ impl OpenCADStudio {
}
h.gradient_color.colors[idx].color = color.clone();
}
- }
+ });
// Rebuild hatch seeds so the gradient fill picks up the new
// colour (synced_hatch_models only patches the main colour).
self.tabs[i].scene.populate_hatches_from_document();
- self.invalidate_property_targets(i, &handles);
self.tabs[i].properties.open_color_field = None;
- self.tabs[i].dirty = true;
- self.refresh_properties();
}
Task::none()
}
diff --git a/src/command.rs b/src/command.rs
index d4a2312b3..491672afc 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -1248,6 +1248,28 @@ pub enum CmdResult {
CommitDimension {
entity: EntityType,
association: DimensionAssociationInput,
+ /// Retain the source dimension's layer and style instead of replacing
+ /// them with the current creation defaults (DIMCONTINUEMODE=1).
+ preserve_base_style: bool,
+ /// Keep collecting points after the dimension is committed.
+ continue_command: bool,
+ },
+ /// Commit several dimensions with independent association sources in one
+ /// undo step, then end the command.
+ CommitDimensionsAndExit(Vec<(EntityType, DimensionAssociationInput)>),
+ /// Persist the quick-dimension extension-origin priority while keeping the
+ /// active command at its current prompt (0 = endpoints, 1 = intersections).
+ SetQuickDimensionSnapPriority(u8),
+ /// Align multileader content points along a picked infinite line.
+ AlignMLeaders {
+ handles: Vec,
+ from: DVec3,
+ to: DVec3,
+ },
+ /// Merge compatible block multileaders at a picked content point.
+ CollectMLeaders {
+ handles: Vec,
+ point: DVec3,
},
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its B-rep (cached for boolean ops + shaded
diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs
index 893c6c42f..9c9214041 100644
--- a/src/entities/dim_override.rs
+++ b/src/entities/dim_override.rs
@@ -403,8 +403,8 @@ pub fn set_property(
let size = current.abs().max(0.01);
let value = match trimmed.to_ascii_lowercase().as_str() {
"none" => 0.0,
- "center marks" => size,
- "centerlines" => -size,
+ "mark" | "center marks" => size,
+ "line" | "centerlines" => -size,
_ => return false,
};
set(doc, handle, DIMCEN, Some(XDataValue::Real(value)));
@@ -423,6 +423,7 @@ pub fn set_property(
return true;
}
let handle_field = match field {
+ "dim_radial_arrow" => Some(DIMLDRBLK),
"dim_arrowhead_1" => Some(DIMBLK1),
"dim_arrowhead_2" => Some(DIMBLK2),
"dim_linetype" => Some(DIMLTYPE),
@@ -433,7 +434,7 @@ pub fn set_property(
};
if let Some(code) = handle_field {
let resolved = match field {
- "dim_arrowhead_1" | "dim_arrowhead_2" => {
+ "dim_radial_arrow" | "dim_arrowhead_1" | "dim_arrowhead_2" => {
if trimmed == "Closed filled" {
Some(Handle::NULL)
} else {
@@ -638,6 +639,10 @@ pub fn set_property(
"dim_line_lineweight" | "dim_ext_line_lineweight" => {
parse_lineweight_label(trimmed)
}
+ "dim_precision"
+ | "dim_alt_precision"
+ | "dim_tolerance_precision"
+ | "dim_alt_tolerance_precision" => parse_precision_label(trimmed),
"dim_ext_line_fixed"
| "dim_text_outside_align"
| "dim_text_inside_align"
@@ -729,8 +734,8 @@ pub fn set_property(
_ => trimmed.parse().ok(),
},
"dim_tolerance_alignment" => match trimmed.to_ascii_lowercase().as_str() {
- "align decimal separators" => Some(0),
- "align operational symbols" => Some(1),
+ "decimal separator" | "align decimal separators" => Some(0),
+ "operational symbols" | "align operational symbols" => Some(1),
_ => trimmed.parse().ok(),
},
_ => trimmed.parse().ok(),
@@ -744,6 +749,16 @@ pub fn set_property(
false
}
+fn parse_precision_label(value: &str) -> Option {
+ if let Some(decimals) = value.strip_prefix("0.") {
+ return (!decimals.is_empty()
+ && decimals.len() <= 8
+ && decimals.bytes().all(|digit| digit == b'0'))
+ .then_some(decimals.len() as i16);
+ }
+ value.parse().ok()
+}
+
fn parse_lineweight_label(value: &str) -> Option {
match value.trim() {
"ByLayer" => Some(-1),
diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs
index 061dcaa76..c27fc87db 100644
--- a/src/entities/dimension.rs
+++ b/src/entities/dimension.rs
@@ -111,47 +111,11 @@ fn properties(dim: &Dimension) -> Vec {
if let Dimension::LargeRadial(d) = dim {
return vec![PropSection {
title: t!("Misc").into_owned(),
- props: vec![
- Property {
- label: t!("Dimension style").into_owned(),
- field: "style_name",
- value: PropValue::PlainText(d.base.style_name.clone()),
- },
- edit(t!("Center X").as_ref(), "definition_x", d.definition_point.x),
- edit(t!("Center Y").as_ref(), "definition_y", d.definition_point.y),
- edit(t!("Center Z").as_ref(), "definition_z", d.definition_point.z),
- edit(t!("Chord X").as_ref(), "chord_x", d.chord_point.x),
- edit(t!("Chord Y").as_ref(), "chord_y", d.chord_point.y),
- edit(t!("Chord Z").as_ref(), "chord_z", d.chord_point.z),
- edit(
- t!("Override Center X").as_ref(),
- "override_x",
- d.override_center.x,
- ),
- edit(
- t!("Override Center Y").as_ref(),
- "override_y",
- d.override_center.y,
- ),
- edit(
- t!("Override Center Z").as_ref(),
- "override_z",
- d.override_center.z,
- ),
- edit(t!("Jog X").as_ref(), "jog_x", d.jog_point.x),
- edit(t!("Jog Y").as_ref(), "jog_y", d.jog_point.y),
- edit(t!("Jog Z").as_ref(), "jog_z", d.jog_point.z),
- edit_angle(
- t!("Jog Angle").as_ref(),
- "jog_angle",
- d.jog_angle.to_degrees(),
- ),
- ro(
- t!("Measurement").as_ref(),
- "measurement",
- format!("{:.4}", d.measurement()),
- ),
- ],
+ props: vec![Property {
+ label: t!("Dim style").into_owned(),
+ field: "style_name",
+ value: PropValue::PlainText(d.base.style_name.clone()),
+ }],
}];
}
let compact_linear = match dim {
@@ -406,16 +370,19 @@ fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
// Editing the text position in the properties panel pins it to a
// user-defined location (stops following DIMTAD). See #94.
"text_x" => {
- base.text_user_positioned = true;
- assign_f64(value, &mut base.text_middle_point.x)
+ let changed = assign_f64(value, &mut base.text_middle_point.x);
+ base.text_user_positioned |= changed;
+ changed
}
"text_y" => {
- base.text_user_positioned = true;
- assign_f64(value, &mut base.text_middle_point.y)
+ let changed = assign_f64(value, &mut base.text_middle_point.y);
+ base.text_user_positioned |= changed;
+ changed
}
"text_z" => {
- base.text_user_positioned = true;
- assign_f64(value, &mut base.text_middle_point.z)
+ let changed = assign_f64(value, &mut base.text_middle_point.z);
+ base.text_user_positioned |= changed;
+ changed
}
"text_rotation" => assign_deg(value, &mut base.text_rotation),
"horizontal_direction" => assign_deg(value, &mut base.horizontal_direction),
@@ -791,6 +758,26 @@ fn apply_arc_fields(d: &mut DimensionArc, field: &str, value: &str) {
}
fn apply_large_radial_fields(d: &mut DimensionLargeRadial, field: &str, value: &str) {
+ if field == "large_radial_rotation" {
+ let Some(angle) = parse_f64(value).map(f64::to_radians) else {
+ return;
+ };
+ let delta = angle - large_radial_rotation(d);
+ let origin = d.definition_point;
+ let rotate = |point: &mut acadrust::types::Vector3| {
+ let x = point.x - origin.x;
+ let y = point.y - origin.y;
+ let (sin, cos) = delta.sin_cos();
+ point.x = origin.x + x * cos - y * sin;
+ point.y = origin.y + x * sin + y * cos;
+ };
+ rotate(&mut d.chord_point);
+ rotate(&mut d.override_center);
+ rotate(&mut d.jog_point);
+ rotate(&mut d.base.text_middle_point);
+ rotate(&mut d.base.insertion_point);
+ return;
+ }
match field {
"definition_x" => {
let _ = assign_f64(value, &mut d.definition_point.x);
@@ -835,6 +822,16 @@ fn apply_large_radial_fields(d: &mut DimensionLargeRadial, field: &str, value: &
}
}
+fn large_radial_rotation(d: &DimensionLargeRadial) -> f64 {
+ let delta = d.chord_point - d.definition_point;
+ if delta.x.abs() <= 1.0e-12 && delta.y.abs() <= 1.0e-12 {
+ let visible = d.chord_point - d.override_center;
+ visible.y.atan2(visible.x)
+ } else {
+ delta.y.atan2(delta.x)
+ }
+}
+
fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
if matches!(dim, Dimension::Ordinate(_)) {
match t {
@@ -1701,6 +1698,139 @@ use acadrust::entities::{MText, Text};
use acadrust::tables::DimStyle;
use acadrust::types::{Color as AcadColor, Vector3};
+pub(crate) fn resolved_dimension_style(
+ source: &DimStyle,
+ dimension: &Dimension,
+ document: &CadDocument,
+) -> DimStyle {
+ use crate::entities::dim_override as ov;
+
+ let mut style = source.clone();
+ let data = &dimension.base().common.extended_data;
+ macro_rules! real {
+ ($field:ident, $code:ident) => {
+ if let Some(value) = ov::real(data, ov::$code) {
+ style.$field = value;
+ }
+ };
+ }
+ macro_rules! int {
+ ($field:ident, $code:ident) => {
+ if let Some(value) = ov::int(data, ov::$code) {
+ style.$field = value;
+ }
+ };
+ }
+ macro_rules! flag {
+ ($field:ident, $code:ident) => {
+ if let Some(value) = ov::int(data, ov::$code) {
+ style.$field = value != 0;
+ }
+ };
+ }
+ macro_rules! handle {
+ ($field:ident, $code:ident) => {
+ if let Some(value) = ov::handle(data, ov::$code) {
+ style.$field = value;
+ }
+ };
+ }
+
+ real!(dimscale, DIMSCALE);
+ real!(dimasz, DIMASZ);
+ real!(dimexo, DIMEXO);
+ real!(dimdli, DIMDLI);
+ real!(dimexe, DIMEXE);
+ real!(dimrnd, DIMRND);
+ real!(dimdle, DIMDLE);
+ real!(dimtp, DIMTP);
+ real!(dimtm, DIMTM);
+ real!(dimfxl, DIMFXL);
+ real!(dimjogang, DIMJOGANG);
+ real!(dimtxt, DIMTXT);
+ real!(dimcen, DIMCEN);
+ real!(dimtsz, DIMTSZ);
+ real!(dimaltf, DIMALTF);
+ real!(dimlfac, DIMLFAC);
+ real!(dimtvp, DIMTVP);
+ real!(dimtfac, DIMTFAC);
+ real!(dimgap, DIMGAP);
+ real!(dimaltrnd, DIMALTRND);
+ real!(dimaltmzf, DIMALTMZF);
+ real!(dimmzf, DIMMZF);
+ flag!(dimtol, DIMTOL);
+ flag!(dimlim, DIMLIM);
+ flag!(dimtih, DIMTIH);
+ flag!(dimtoh, DIMTOH);
+ flag!(dimse1, DIMSE1);
+ flag!(dimse2, DIMSE2);
+ flag!(dimalt, DIMALT);
+ flag!(dimtofl, DIMTOFL);
+ flag!(dimsah, DIMSAH);
+ flag!(dimtix, DIMTIX);
+ flag!(dimsoxd, DIMSOXD);
+ flag!(dimsd1, DIMSD1);
+ flag!(dimsd2, DIMSD2);
+ flag!(dimupt, DIMUPT);
+ flag!(dimfxlon, DIMFXLON);
+ flag!(dimtxtdirection, DIMTXTDIRECTION);
+ int!(dimzin, DIMZIN);
+ int!(dimtad, DIMTAD);
+ int!(dimazin, DIMAZIN);
+ int!(dimarcsym, DIMARCSYM);
+ int!(dimclrd, DIMCLRD);
+ int!(dimclre, DIMCLRE);
+ int!(dimclrt, DIMCLRT);
+ int!(dimadec, DIMADEC);
+ int!(dimaltd, DIMALTD);
+ int!(dimdec, DIMDEC);
+ int!(dimtdec, DIMTDEC);
+ int!(dimaltu, DIMALTU);
+ int!(dimalttd, DIMALTTD);
+ int!(dimaunit, DIMAUNIT);
+ int!(dimfrac, DIMFRAC);
+ int!(dimlunit, DIMLUNIT);
+ int!(dimdsep, DIMDSEP);
+ int!(dimtmove, DIMTMOVE);
+ int!(dimjust, DIMJUST);
+ int!(dimtolj, DIMTOLJ);
+ int!(dimtzin, DIMTZIN);
+ int!(dimaltz, DIMALTZ);
+ int!(dimalttz, DIMALTTZ);
+ int!(dimatfit, DIMATFIT);
+ int!(dimtfill, DIMTFILL);
+ int!(dimtfillclr, DIMTFILLCLR);
+ int!(dimlwd, DIMLWD);
+ int!(dimlwe, DIMLWE);
+ handle!(dimldrblk, DIMLDRBLK);
+ handle!(dimblk, DIMBLK);
+ handle!(dimblk1, DIMBLK1);
+ handle!(dimblk2, DIMBLK2);
+ handle!(dimltex_handle, DIMLTYPE);
+ handle!(dimltex1_handle, DIMLTEX1);
+ handle!(dimltex2_handle, DIMLTEX2);
+
+ if let Some(value) = ov::string(data, ov::DIMPOST) {
+ style.dimpost = value;
+ }
+ if let Some(value) = ov::string(data, ov::DIMAPOST) {
+ style.dimapost = value;
+ }
+ if let Some(value) = ov::string(data, ov::DIMALTMZS) {
+ style.dimaltmzs = value;
+ }
+ if let Some(value) = ov::string(data, ov::DIMMZS) {
+ style.dimmzs = value;
+ }
+ if let Some(value) = ov::handle(data, ov::DIMTXSTY) {
+ style.dimtxsty_handle = value;
+ if let Some(record) = document.text_styles.iter().find(|record| record.handle == value) {
+ style.dimtxsty = record.name.clone();
+ }
+ }
+ style
+}
+
/// Build the linear-dimension property groups from the assigned style plus
/// any entity-level DSTYLE overrides. Conditional rows remain visible but are
/// read-only while their controlling option is disabled.
@@ -1711,6 +1841,7 @@ pub fn style_sections(
) -> Vec {
use crate::entities::dim_override as ov;
+ let effective_style = resolved_dimension_style(style, dimension, document);
let data = &dimension.base().common.extended_data;
let real = |code, inherited| ov::real(data, code).unwrap_or(inherited);
let int = |code, inherited| ov::int(data, code).unwrap_or(inherited);
@@ -1757,7 +1888,7 @@ pub fn style_sections(
)
};
- let s = style;
+ let s = &effective_style;
let dimfxlon = int(ov::DIMFXLON, s.dimfxlon as i16) != 0;
let dimtix = int(ov::DIMTIX, s.dimtix as i16) != 0;
let dimlunit = int(ov::DIMLUNIT, s.dimlunit);
@@ -1861,18 +1992,10 @@ pub fn style_sections(
linetype_name(document, ov::handle(data, code).unwrap_or(inherited))
};
let arrow_1 = if matches!(dimension, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
- let inherited = if s.dimblk1.is_null() { s.dimblk } else { s.dimblk1 };
- let inherited_name = if s.dimblk1.is_null() {
- &s.dimblk_name
- } else {
- &s.dimblk1_name
- };
block_name(
document,
- ov::handle(data, ov::DIMBLK1)
- .or_else(|| ov::handle(data, ov::DIMBLK))
- .unwrap_or(inherited),
- inherited_name,
+ ov::handle(data, ov::DIMLDRBLK).unwrap_or(s.dimldrblk),
+ "Closed filled",
)
} else {
let inherited = if matches!(dimension, Dimension::Diameter(_)) && !s.dimsah {
@@ -1902,7 +2025,7 @@ pub fn style_sections(
}
}
- let precision_options: Vec = (0..=8).map(|value| value.to_string()).collect();
+ let precision_options: Vec = (0..=8).map(precision_label).collect();
let linear_units = [
"Scientific",
"Decimal",
@@ -2149,7 +2272,7 @@ pub fn style_sections(
"dim_text_inside_align",
on(int(ov::DIMTIH, s.dimtih as i16) != 0),
&["On", "Off"],
- dimtix,
+ dimtix || matches!(dimension, Dimension::LargeRadial(_)),
),
property(
t!("Text position X").as_ref(),
@@ -2179,11 +2302,11 @@ pub fn style_sections(
t!("Text view direction").as_ref(),
"dim_text_view_direction",
if int(ov::DIMTXTDIRECTION, s.dimtxtdirection as i16) != 0 {
- "Right-to-left"
+ "Right-to-Left"
} else {
- "Left-to-right"
+ "Left-to-Right"
},
- &["Left-to-right", "Right-to-left"],
+ &["Left-to-Right", "Right-to-Left"],
true,
),
property(
@@ -2262,13 +2385,13 @@ pub fn style_sections(
true,
),
text(
- t!("Prefix").as_ref(),
+ t!("Dim prefix").as_ref(),
"dim_prefix",
dim_prefix.clone(),
true,
),
text(
- t!("Suffix").as_ref(),
+ t!("Dim suffix").as_ref(),
"dim_suffix",
dim_suffix.clone(),
true,
@@ -2323,20 +2446,20 @@ pub fn style_sections(
"dim_suppress_zero_feet",
yes(suppresses_zero_feet(dimzin)),
&["Yes", "No"],
- true,
+ matches!(dimlunit, 3 | 4),
),
choice(
t!("Suppress zero inches").as_ref(),
"dim_suppress_zero_inches",
yes(suppresses_zero_inches(dimzin)),
&["Yes", "No"],
- true,
+ matches!(dimlunit, 3 | 4),
),
property(
t!("Precision").as_ref(),
"dim_precision",
PropValue::Choice {
- selected: int(ov::DIMDEC, s.dimdec).to_string(),
+ selected: precision_label(int(ov::DIMDEC, s.dimdec)),
options: precision_options.clone(),
},
),
@@ -2364,11 +2487,11 @@ pub fn style_sections(
"dim_alt_precision",
if dimalt {
PropValue::Choice {
- selected: int(ov::DIMALTD, s.dimaltd).to_string(),
+ selected: precision_label(int(ov::DIMALTD, s.dimaltd)),
options: precision_options.clone(),
}
} else {
- PropValue::ReadOnly(int(ov::DIMALTD, s.dimaltd).to_string())
+ PropValue::ReadOnly(precision_label(int(ov::DIMALTD, s.dimaltd)))
},
),
number(
@@ -2384,7 +2507,7 @@ pub fn style_sections(
dimalt && dimaltz & 4 != 0,
),
number(
- t!("Alt roundoff").as_ref(),
+ t!("Alt round").as_ref(),
"dim_alt_roundoff",
real(ov::DIMALTRND, s.dimaltrnd),
dimalt,
@@ -2452,11 +2575,11 @@ pub fn style_sections(
"dim_tolerance_precision",
if tolerance_enabled {
PropValue::Choice {
- selected: int(ov::DIMTDEC, s.dimtdec).to_string(),
+ selected: precision_label(int(ov::DIMTDEC, s.dimtdec)),
options: precision_options.clone(),
}
} else {
- PropValue::ReadOnly(int(ov::DIMTDEC, s.dimtdec).to_string())
+ PropValue::ReadOnly(precision_label(int(ov::DIMTDEC, s.dimtdec)))
},
),
number(
@@ -2488,7 +2611,7 @@ pub fn style_sections(
t!("Tolerance alignment").as_ref(),
"dim_tolerance_alignment",
tolerance_alignment_label(int(ov::DIMTALN, 0)),
- &["Align decimal separators", "Align operational symbols"],
+ &["Decimal Separator", "Operational Symbols"],
true,
),
choice(
@@ -2524,11 +2647,11 @@ pub fn style_sections(
"dim_alt_tolerance_precision",
if alternate_tolerance_enabled {
PropValue::Choice {
- selected: int(ov::DIMALTTD, s.dimalttd).to_string(),
+ selected: precision_label(int(ov::DIMALTTD, s.dimalttd)),
options: precision_options,
}
} else {
- PropValue::ReadOnly(int(ov::DIMALTTD, s.dimalttd).to_string())
+ PropValue::ReadOnly(precision_label(int(ov::DIMALTTD, s.dimalttd)))
},
),
choice(
@@ -2568,9 +2691,9 @@ pub fn style_sections(
) {
let dimcen = real(ov::DIMCEN, s.dimcen);
let center_type = if dimcen > 1e-12 {
- "Center marks"
+ "Mark"
} else if dimcen < -1e-12 {
- "Centerlines"
+ "Line"
} else {
"None"
};
@@ -2583,7 +2706,7 @@ pub fn style_sections(
"dim_arrow_size",
"dim_line_lineweight",
"dim_ext_line_lineweight",
- "dim_line_1",
+ "dim_line_2",
"dim_line_color",
"dim_linetype",
"dim_ext_linetype_1",
@@ -2612,9 +2735,15 @@ pub fn style_sections(
"dim_arrowhead_1",
"dim_arrow_size",
"dim_line_lineweight",
- "dim_line_1",
+ "dim_ext_line_lineweight",
+ "dim_line_2",
"dim_line_color",
"dim_linetype",
+ "dim_ext_linetype_1",
+ "dim_ext_line_1",
+ "dim_ext_line_color",
+ "dim_ext_line_ext",
+ "dim_ext_line_offset",
];
let retained = if matches!(dimension, Dimension::Diameter(_)) {
DIAMETER_LINE_FIELDS
@@ -2630,7 +2759,7 @@ pub fn style_sections(
t!("Center mark").as_ref(),
"dim_center_type",
center_type,
- &["None", "Center marks", "Centerlines"],
+ &["None", "Mark", "Line"],
true,
));
lines.props.push(number(
@@ -2639,6 +2768,87 @@ pub fn style_sections(
dimcen.abs(),
center_type != "None",
));
+ if let Dimension::LargeRadial(radial) = dimension {
+ lines.props.push(edit(
+ t!("Center location override X").as_ref(),
+ "override_x",
+ radial.override_center.x,
+ ));
+ lines.props.push(edit(
+ t!("Center location override Y").as_ref(),
+ "override_y",
+ radial.override_center.y,
+ ));
+ lines.props.push(edit(
+ t!("Jog location X").as_ref(),
+ "jog_x",
+ radial.jog_point.x,
+ ));
+ lines.props.push(edit(
+ t!("Jog location Y").as_ref(),
+ "jog_y",
+ radial.jog_point.y,
+ ));
+ lines.props.push(edit_angle(
+ t!("Jog angle").as_ref(),
+ "jog_angle",
+ radial.jog_angle.to_degrees(),
+ ));
+ }
+ if matches!(dimension, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
+ if let Some(arrow) = lines
+ .props
+ .iter_mut()
+ .find(|property| property.field == "dim_arrowhead_1")
+ {
+ arrow.label = t!("Arrow").into_owned();
+ arrow.field = "dim_radial_arrow";
+ }
+ if let Some(dim_line) = lines
+ .props
+ .iter_mut()
+ .find(|property| property.field == "dim_line_2")
+ {
+ dim_line.label = t!("Dim line").into_owned();
+ }
+ }
+ if matches!(dimension, Dimension::LargeRadial(_)) {
+ for property in &mut lines.props {
+ property.label = match property.field {
+ "dim_ext_line_1" => t!("Ext line").into_owned(),
+ "dim_ext_line_lineweight" => t!("Ext line weight").into_owned(),
+ "dim_ext_linetype_1" => t!("Ext line type").into_owned(),
+ _ => property.label.clone(),
+ };
+ }
+ const LARGE_RADIAL_ORDER: &[&str] = &[
+ "dim_radial_arrow",
+ "dim_arrow_size",
+ "dim_center_type",
+ "dim_center_size",
+ "dim_line_lineweight",
+ "dim_line_2",
+ "dim_linetype",
+ "dim_line_color",
+ "override_x",
+ "override_y",
+ "jog_x",
+ "jog_y",
+ "dim_ext_line_1",
+ "jog_angle",
+ "dim_ext_line_lineweight",
+ "dim_ext_linetype_1",
+ "dim_ext_line_ext",
+ "dim_ext_line_color",
+ "dim_ext_line_offset",
+ ];
+ lines.props.sort_by_key(|property| {
+ LARGE_RADIAL_ORDER
+ .iter()
+ .position(|field| *field == property.field)
+ .unwrap_or(LARGE_RADIAL_ORDER.len())
+ });
+ }
}
if let Some(primary_units) = sections
.iter_mut()
@@ -2651,6 +2861,134 @@ pub fn style_sections(
)
});
}
+ if matches!(dimension, Dimension::LargeRadial(_)) {
+ for (section_name, excluded) in [
+ (t!("Text"), &["dim_text_pos_hor"][..]),
+ (t!("Fit"), &["dim_line_inside"][..]),
+ (
+ t!("Alternate Units"),
+ &["dim_alt_sub_units_scale", "dim_alt_sub_units_suffix"][..],
+ ),
+ ] {
+ if let Some(section) = sections
+ .iter_mut()
+ .find(|section| section.title == section_name.as_ref())
+ {
+ section
+ .props
+ .retain(|property| !excluded.contains(&property.field));
+ }
+ }
+ for (section_name, fields) in [
+ (
+ t!("Text"),
+ &["dim_text_outside_align", "dim_text_pos_vert"][..],
+ ),
+ (t!("Fit"), &["dim_fit", "dim_text_inside"][..]),
+ ] {
+ if let Some(section) = sections
+ .iter_mut()
+ .find(|section| section.title == section_name.as_ref())
+ {
+ for property in &mut section.props {
+ if fields.contains(&property.field) {
+ if let PropValue::Choice { selected, .. } = &property.value {
+ property.value = PropValue::ReadOnly(selected.clone());
+ }
+ }
+ }
+ }
+ }
+ if let Some(fit) = sections
+ .iter_mut()
+ .find(|section| section.title == t!("Fit").as_ref())
+ {
+ const LARGE_RADIAL_FIT_ORDER: &[&str] = &[
+ "dim_line_forced",
+ "dim_scale_overall",
+ "dim_fit",
+ "dim_text_inside",
+ "dim_text_movement",
+ ];
+ fit.props.sort_by_key(|property| {
+ LARGE_RADIAL_FIT_ORDER
+ .iter()
+ .position(|field| *field == property.field)
+ .unwrap_or(LARGE_RADIAL_FIT_ORDER.len())
+ });
+ }
+ if let Some(alternate_units) = sections
+ .iter_mut()
+ .find(|section| section.title == t!("Alternate Units").as_ref())
+ {
+ const LARGE_RADIAL_ALT_UNITS_ORDER: &[&str] = &[
+ "dim_alt_enabled",
+ "dim_alt_format",
+ "dim_alt_precision",
+ "dim_alt_roundoff",
+ "dim_alt_scale_factor",
+ "dim_alt_suppress_leading_zeros",
+ "dim_alt_suppress_trailing_zeros",
+ "dim_alt_suppress_zero_feet",
+ "dim_alt_suppress_zero_inches",
+ "dim_alt_prefix",
+ "dim_alt_suffix",
+ ];
+ alternate_units.props.sort_by_key(|property| {
+ LARGE_RADIAL_ALT_UNITS_ORDER
+ .iter()
+ .position(|field| *field == property.field)
+ .unwrap_or(LARGE_RADIAL_ALT_UNITS_ORDER.len())
+ });
+ }
+ if let Some(tolerances) = sections
+ .iter_mut()
+ .find(|section| section.title == t!("Tolerances").as_ref())
+ {
+ const LARGE_RADIAL_TOLERANCE_ORDER: &[&str] = &[
+ "dim_alt_tolerance_suppress_zero_inches",
+ "dim_tolerance_alignment",
+ "dim_tolerance_display",
+ "dim_tolerance_limit_lower",
+ "dim_tolerance_limit_upper",
+ "dim_tolerance_pos_vert",
+ "dim_tolerance_precision",
+ "dim_tolerance_suppress_leading_zeros",
+ "dim_tolerance_suppress_trailing_zeros",
+ "dim_tolerance_suppress_zero_feet",
+ "dim_tolerance_suppress_zero_inches",
+ "dim_tolerance_text_height",
+ "dim_alt_tolerance_precision",
+ "dim_alt_tolerance_suppress_leading_zeros",
+ "dim_alt_tolerance_suppress_trailing_zeros",
+ "dim_alt_tolerance_suppress_zero_feet",
+ ];
+ tolerances.props.sort_by_key(|property| {
+ LARGE_RADIAL_TOLERANCE_ORDER
+ .iter()
+ .position(|field| *field == property.field)
+ .unwrap_or(LARGE_RADIAL_TOLERANCE_ORDER.len())
+ });
+ }
+ if !dimension.base().text_user_positioned {
+ let text_position = styled_dimension_text_position(dimension, s, 1.0);
+ for section in &mut sections {
+ for property in &mut section.props {
+ match property.field {
+ "text_x" => {
+ property.value =
+ PropValue::EditText(format!("{:.4}", text_position.x));
+ }
+ "text_y" => {
+ property.value =
+ PropValue::EditText(format!("{:.4}", text_position.y));
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ }
}
if matches!(dimension, Dimension::Arc(_)) {
@@ -2801,6 +3139,55 @@ pub fn style_sections(
sections
}
+fn dimension_style_scale(style: &DimStyle, fallback: f64) -> f64 {
+ if style.dimscale > 1.0e-6 {
+ style.dimscale
+ } else {
+ fallback
+ }
+}
+
+fn styled_dimension_text_position(
+ dimension: &Dimension,
+ style: &DimStyle,
+ fallback_scale: f64,
+) -> Vector3 {
+ let scale = dimension_style_scale(style, fallback_scale);
+ dimension_text_pos_f64(dimension, Some(style), style.dimtxt * scale, scale)
+}
+
+pub(crate) fn materialize_large_radial_text_position(
+ document: &mut CadDocument,
+ handle: Handle,
+ value: &str,
+) {
+ if parse_f64(value).is_none() {
+ return;
+ }
+ let Some(EntityType::Dimension(dimension)) = document.get_entity(handle) else {
+ return;
+ };
+ if !matches!(dimension, Dimension::LargeRadial(_))
+ || dimension.base().text_user_positioned
+ {
+ return;
+ }
+ let source = document.dim_styles.iter().find(|style| {
+ style.name.eq_ignore_ascii_case(&dimension.base().style_name)
+ || (dimension.base().style_name.trim().is_empty()
+ && style.name.eq_ignore_ascii_case("Standard"))
+ });
+ let position = source
+ .map(|style| resolved_dimension_style(style, dimension, document))
+ .map(|style| styled_dimension_text_position(dimension, &style, 1.0))
+ .unwrap_or_else(|| dimension_text_pos_f64(dimension, None, 2.5, 1.0));
+ if let Some(EntityType::Dimension(Dimension::LargeRadial(radial))) =
+ document.get_entity_mut(handle)
+ {
+ radial.base.text_middle_point = position;
+ }
+}
+
fn property(label: &str, field: &'static str, value: PropValue) -> Property {
Property {
label: label.to_string(),
@@ -2899,11 +3286,20 @@ fn tolerance_vertical_label(value: i16) -> &'static str {
}
}
+fn precision_label(value: i16) -> String {
+ let decimals = value.clamp(0, 8) as usize;
+ if decimals == 0 {
+ "0".to_string()
+ } else {
+ format!("0.{}", "0".repeat(decimals))
+ }
+}
+
fn tolerance_alignment_label(value: i16) -> &'static str {
if value == 0 {
- "Align decimal separators"
+ "Decimal Separator"
} else {
- "Align operational symbols"
+ "Operational Symbols"
}
}
@@ -2953,8 +3349,8 @@ use acadrust::{CadDocument, EntityType, Handle};
use crate::scene::convert::tess_util::aci_to_rgba;
use crate::scene::convert::tessellate::{
- add_polyline, add_segment, append_arrow, arrow_from_block_with_deferred_hatch,
- normalized_or, ArrowKind, DimGeom,
+ add_polyline, add_segment, append_arrow, arrow_from_block,
+ arrow_from_block_with_deferred_hatch, normalized_or, ArrowKind, DimGeom,
};
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
@@ -3173,130 +3569,7 @@ fn tessellate_dimension_inner(
s.name.eq_ignore_ascii_case(style_name)
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
});
- let mut effective_style = source_style.cloned();
- if let Some(style) = &mut effective_style {
- use crate::entities::dim_override as ov;
- let data = &dim.base().common.extended_data;
- macro_rules! real {
- ($field:ident, $code:ident) => {
- if let Some(value) = ov::real(data, ov::$code) {
- style.$field = value;
- }
- };
- }
- macro_rules! int {
- ($field:ident, $code:ident) => {
- if let Some(value) = ov::int(data, ov::$code) {
- style.$field = value;
- }
- };
- }
- macro_rules! flag {
- ($field:ident, $code:ident) => {
- if let Some(value) = ov::int(data, ov::$code) {
- style.$field = value != 0;
- }
- };
- }
- macro_rules! handle {
- ($field:ident, $code:ident) => {
- if let Some(value) = ov::handle(data, ov::$code) {
- style.$field = value;
- }
- };
- }
- real!(dimscale, DIMSCALE);
- real!(dimasz, DIMASZ);
- real!(dimexo, DIMEXO);
- real!(dimdli, DIMDLI);
- real!(dimexe, DIMEXE);
- real!(dimrnd, DIMRND);
- real!(dimdle, DIMDLE);
- real!(dimtp, DIMTP);
- real!(dimtm, DIMTM);
- real!(dimfxl, DIMFXL);
- real!(dimjogang, DIMJOGANG);
- real!(dimtxt, DIMTXT);
- real!(dimcen, DIMCEN);
- real!(dimtsz, DIMTSZ);
- real!(dimaltf, DIMALTF);
- real!(dimlfac, DIMLFAC);
- real!(dimtvp, DIMTVP);
- real!(dimtfac, DIMTFAC);
- real!(dimgap, DIMGAP);
- real!(dimaltrnd, DIMALTRND);
- real!(dimaltmzf, DIMALTMZF);
- real!(dimmzf, DIMMZF);
- flag!(dimtol, DIMTOL);
- flag!(dimlim, DIMLIM);
- flag!(dimtih, DIMTIH);
- flag!(dimtoh, DIMTOH);
- flag!(dimse1, DIMSE1);
- flag!(dimse2, DIMSE2);
- flag!(dimalt, DIMALT);
- flag!(dimtofl, DIMTOFL);
- flag!(dimsah, DIMSAH);
- flag!(dimtix, DIMTIX);
- flag!(dimsoxd, DIMSOXD);
- flag!(dimsd1, DIMSD1);
- flag!(dimsd2, DIMSD2);
- flag!(dimupt, DIMUPT);
- flag!(dimfxlon, DIMFXLON);
- flag!(dimtxtdirection, DIMTXTDIRECTION);
- int!(dimzin, DIMZIN);
- int!(dimtad, DIMTAD);
- int!(dimazin, DIMAZIN);
- int!(dimarcsym, DIMARCSYM);
- int!(dimclrd, DIMCLRD);
- int!(dimclre, DIMCLRE);
- int!(dimclrt, DIMCLRT);
- int!(dimadec, DIMADEC);
- int!(dimaltd, DIMALTD);
- int!(dimdec, DIMDEC);
- int!(dimtdec, DIMTDEC);
- int!(dimaltu, DIMALTU);
- int!(dimalttd, DIMALTTD);
- int!(dimaunit, DIMAUNIT);
- int!(dimfrac, DIMFRAC);
- int!(dimlunit, DIMLUNIT);
- int!(dimdsep, DIMDSEP);
- int!(dimtmove, DIMTMOVE);
- int!(dimjust, DIMJUST);
- int!(dimtolj, DIMTOLJ);
- int!(dimtzin, DIMTZIN);
- int!(dimaltz, DIMALTZ);
- int!(dimalttz, DIMALTTZ);
- int!(dimatfit, DIMATFIT);
- int!(dimtfill, DIMTFILL);
- int!(dimtfillclr, DIMTFILLCLR);
- int!(dimlwd, DIMLWD);
- int!(dimlwe, DIMLWE);
- handle!(dimldrblk, DIMLDRBLK);
- handle!(dimblk, DIMBLK);
- handle!(dimblk1, DIMBLK1);
- handle!(dimblk2, DIMBLK2);
- handle!(dimltex_handle, DIMLTYPE);
- handle!(dimltex1_handle, DIMLTEX1);
- handle!(dimltex2_handle, DIMLTEX2);
- if let Some(value) = ov::string(data, ov::DIMPOST) {
- style.dimpost = value;
- }
- if let Some(value) = ov::string(data, ov::DIMAPOST) {
- style.dimapost = value;
- }
- if let Some(value) = ov::string(data, ov::DIMALTMZS) {
- style.dimaltmzs = value;
- }
- if let Some(value) = ov::string(data, ov::DIMMZS) {
- style.dimmzs = value;
- }
- if let Some(value) = ov::handle(data, ov::DIMTXSTY) {
- style.dimtxsty_handle = value;
- if let Some(record) = document.text_styles.iter().find(|record| record.handle == value) {
- style.dimtxsty = record.name.clone();
- }
- }
- }
+ let effective_style = source_style.map(|style| resolved_dimension_style(style, dim, document));
let style = effective_style.as_ref();
// A positive style scale is fixed. A zero style scale uses the multiplier
@@ -3353,9 +3626,10 @@ fn tessellate_dimension_inner(
));
// Arrow selection precedence:
- // 1. DIMTSZ>0 → oblique tick (overrides DIMBLK*).
- // 2. DIMSAH false → DIMBLK on both ends.
- // 3. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
+ // 1. DIMTSZ>0 → oblique tick (overrides arrow blocks).
+ // 2. Radius and large-radius dimensions → DIMLDRBLK.
+ // 3. DIMSAH false → DIMBLK on both ends.
+ // 4. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
// Unknown / NULL block handles fall back to ClosedFilled.
let dimasz = (dimasz_raw as f32).max(0.001);
let defer_arrow_hatches = {
@@ -3374,8 +3648,12 @@ fn tessellate_dimension_inner(
};
(t.clone(), t)
} else if let Some(s) = style {
- if matches!(dim, Dimension::Radius(_)) {
- let handle = if s.dimblk1.is_null() { s.dimblk } else { s.dimblk1 };
+ if matches!(dim, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
+ let handle = crate::entities::dim_override::handle(
+ &dim.base().common.extended_data,
+ crate::entities::dim_override::DIMLDRBLK,
+ )
+ .unwrap_or(s.dimldrblk);
let arrow = arrow_from_block_with_deferred_hatch(
document,
handle,
@@ -3442,37 +3720,7 @@ fn tessellate_dimension_inner(
(a.clone(), a)
};
- // Text box (local space) so the dim line can be broken where the text
- // crosses it — lets a DIMTFILL background sit over the line. The renderer
- // draws 2D fills under all wires, so the line is gapped rather than masked.
- let dimgap_local = style
- .map(|s| (s.dimgap.abs() * dim_scale) as f32)
- .unwrap_or(0.09);
- let text_width = dimension_text_value(dim, style)
- .map(|text| text.chars().count() as f32 * dim_txt as f32 * 0.6 + dimgap_local * 2.0)
- .unwrap_or(0.0);
- let text_break = {
- let tp = vec3_local(dimension_text_pos_f64(dim, style, dim_txt, dim_scale));
- let tw = (text_width - dimgap_local * 2.0).max(0.0);
- if tw > 0.0 {
- // Vertical threshold is the bare text half-height (no DIMGAP): the
- // line only breaks when it actually passes under the glyphs. Text
- // placed above/below the line (DIMTAD 1/4) sits exactly
- // `text_half + DIMGAP` away, so excluding the gap here keeps it
- // strictly outside and the line continuous — otherwise the two
- // terms cancel at the same scaled value and the gap flickers with
- // DIMGAP/DIMSCALE. The horizontal half-width keeps DIMGAP so a
- // genuine break still clears the text comfortably. (#94)
- Some((tp, tw * 0.5 + dimgap_local, dim_txt as f32 * 0.5))
- } else {
- None
- }
- };
- let text_position = vec3_local(dimension_text_pos_f64(dim, style, dim_txt, dim_scale));
- let text_is_outside = dimension_text_is_outside(dim, style);
- let horizontal_text = style.is_some_and(|style| {
- (text_is_outside && style.dimtoh) || (!text_is_outside && style.dimtih)
- });
+ let text_layout = dimension_text_layout(dim, style, dim_txt, dim_scale);
let mut geom = dimension_geometry(
dim,
@@ -3488,13 +3736,14 @@ fn tessellate_dimension_inner(
dimcen,
ticks: dimtsz_raw > 1e-9,
arrow_len: dimasz,
- text_width,
+ text_width: text_layout.width,
dimatfit: style.map(|s| s.dimatfit).unwrap_or(3),
+ dimtix: style.is_some_and(|s| s.dimtix),
dimtofl: style.map(|s| s.dimtofl).unwrap_or(false),
- text_position,
- horizontal_text,
+ text_position: text_layout.position,
+ horizontal_text: text_layout.horizontal,
text_movement: style.map(|s| s.dimtmove).unwrap_or(0),
- text_break,
+ text_break: text_layout.break_box,
},
SuppressFlags {
ext1: dimse1,
@@ -3516,15 +3765,18 @@ fn tessellate_dimension_inner(
}
}
- // 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.)
+ // DIMTMOVE=1 connects the dimension line to rendered text.
if let Some(s) = style {
if s.dimtmove == 1 {
- if let Some((anchor, txt)) = dimtmove_leader_endpoints(dim) {
+ if let Some((anchor, txt)) = dimtmove_leader_endpoints(dim, text_layout.position) {
let gap = dim_txt as f32 * 0.5;
if (txt - anchor).length() > gap * 2.0 {
- add_segment(&mut geom.dim_lines, anchor, txt);
+ add_segment_with_text_break(
+ &mut geom.dim_lines,
+ anchor,
+ txt,
+ text_layout.break_box,
+ );
}
}
}
@@ -4008,16 +4260,8 @@ 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) -> 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;
- }
+/// Endpoints for the DIMTMOVE=1 leader.
+fn dimtmove_leader_endpoints(dim: &Dimension, txt: Vec3) -> Option<(Vec3, Vec3)> {
let lv = |v| vec3_local(v);
let anchor = match dim {
Dimension::Linear(d) => {
@@ -4043,7 +4287,7 @@ fn dimtmove_leader_endpoints(dim: &Dimension) -> Option<(Vec3, Vec3)> {
Dimension::Diameter(d) => {
let chord = lv(d.angle_vertex);
let far_chord = lv(d.definition_point);
- if chord.distance_squared(lv(txt)) <= far_chord.distance_squared(lv(txt)) {
+ if chord.distance_squared(txt) <= far_chord.distance_squared(txt) {
chord
} else {
far_chord
@@ -4051,9 +4295,29 @@ fn dimtmove_leader_endpoints(dim: &Dimension) -> Option<(Vec3, Vec3)> {
}
Dimension::Angular2Ln(d) => lv(d.dimension_arc),
Dimension::Angular3Pt(d) => lv(d.definition_point),
+ Dimension::LargeRadial(d) => {
+ let chord = lv(d.chord_point);
+ let jog = lv(d.jog_point);
+ let override_center = lv(d.override_center);
+ let (near, far) =
+ jogged_radial_break(chord, jog, override_center, d.jog_angle as f32);
+ let curves = [(chord, near), (near, far), (far, override_center)].map(
+ |(start, end)| {
+ cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
+ start: [start.x as f64, start.y as f64],
+ end: [end.x as f64, end.y as f64],
+ })
+ },
+ );
+ let (_, closest) = cadkernel::geom2d::nearest_of(
+ curves.iter(),
+ [txt.x as f64, txt.y as f64],
+ )?;
+ Vec3::new(closest.point[0] as f32, closest.point[1] as f32, chord.z)
+ }
_ => return None,
};
- Some((anchor, lv(txt)))
+ Some((anchor, txt))
}
/// Build a rectangle of filled triangles sitting under the dim text, used
@@ -4156,6 +4420,61 @@ struct SuppressFlags {
dim2: bool,
}
+#[derive(Clone, Copy)]
+struct TextBreak {
+ center: Vec3,
+ half_width: f32,
+ half_height: f32,
+ padding: f32,
+ rotation: f64,
+}
+
+#[derive(Clone, Copy)]
+struct DimensionTextLayout {
+ position: Vec3,
+ width: f32,
+ break_box: Option,
+ horizontal: bool,
+}
+
+fn dimension_text_layout(
+ dimension: &Dimension,
+ style: Option<&DimStyle>,
+ text_height: f64,
+ dim_scale: f64,
+) -> DimensionTextLayout {
+ let gap = style
+ .map(|style| (style.dimgap.abs() * dim_scale) as f32)
+ .unwrap_or(0.09);
+ let width = dimension_text_value(dimension, style)
+ .map(|text| text.chars().count() as f32 * text_height as f32 * 0.6 + gap * 2.0)
+ .unwrap_or(0.0);
+ let position = vec3_local(dimension_text_pos_f64(
+ dimension,
+ style,
+ text_height,
+ dim_scale,
+ ));
+ let bare_width = (width - gap * 2.0).max(0.0);
+ let break_box = (bare_width > 0.0).then_some(TextBreak {
+ center: position,
+ half_width: bare_width * 0.5,
+ half_height: text_height as f32 * 0.5,
+ padding: gap,
+ rotation: dimension_text_rotation(dimension, style),
+ });
+ let outside = dimension_text_is_outside(dimension, style);
+ let horizontal = style.is_some_and(|style| {
+ (outside && style.dimtoh) || (!outside && style.dimtih)
+ });
+ DimensionTextLayout {
+ position,
+ width,
+ break_box,
+ horizontal,
+ }
+}
+
#[derive(Clone, Copy)]
struct DimLineParams {
dimexo: f32,
@@ -4170,14 +4489,12 @@ struct DimLineParams {
arrow_len: f32,
text_width: f32,
dimatfit: i16,
+ dimtix: bool,
dimtofl: bool,
text_position: Vec3,
horizontal_text: bool,
text_movement: i16,
- /// Text box (local centre, half-width, half-height) used to break the
- /// dimension line where the text sits on it, so a DIMTFILL background reads
- /// over the line. None when the text doesn't overlap the line.
- text_break: Option<(Vec3, f32, f32)>,
+ text_break: Option,
}
fn dimension_geometry(
dim: &Dimension,
@@ -4245,17 +4562,17 @@ fn dimension_geometry(
let mid = center + u * (dist * 0.5);
let a = mid - u * half + perp * half;
let b = mid + u * half - perp * half;
- if !suppress.dim1 {
+ if !suppress.dim2 {
add_segment(&mut g.dim_lines, center, a);
add_segment(&mut g.dim_lines, a, b);
add_segment(&mut g.dim_lines, b, point);
}
- } else if !suppress.dim1 {
+ } else if !suppress.dim2 {
add_segment(&mut g.dim_lines, center, point);
}
let radius = (point - center).length();
let text_is_outside = text.distance(center) > radius + 1e-5;
- if text_is_outside && !suppress.dim1 {
+ if text_is_outside && !suppress.dim2 {
let radial = normalized_or(point - center, Vec3::X);
let angle_from_horizontal = radial.y.abs().atan2(radial.x.abs());
if params.horizontal_text
@@ -4280,12 +4597,14 @@ fn dimension_geometry(
add_segment(&mut g.dim_lines, point, text);
}
}
- append_arrow(
- &mut g,
- point,
- normalized_or(center - point, Vec3::X),
- arrow1,
- );
+ if !suppress.dim2 {
+ append_arrow(
+ &mut g,
+ point,
+ normalized_or(center - point, Vec3::X),
+ arrow1,
+ );
+ }
if text_is_outside {
append_center_mark(&mut g, center, params.dimcen, radius);
}
@@ -4411,28 +4730,131 @@ fn dimension_geometry(
let override_center = lv(d.override_center);
let (near, far) =
jogged_radial_break(chord, jog, override_center, d.jog_angle as f32);
- if !suppress.dim1 {
- add_segment(&mut g.dim_lines, chord, near);
- add_segment(&mut g.dim_lines, near, far);
- add_segment(&mut g.dim_lines, far, override_center);
+ let axis = normalized_or(chord - override_center, Vec3::X);
+ let available = chord.distance(override_center);
+ let arrows_outside = if params.ticks || params.arrow_len <= 1.0e-6 {
+ false
+ } else if available < params.arrow_len {
+ true
+ } else if available < params.text_width + params.arrow_len {
+ if params.dimtix {
+ true
+ } else {
+ match params.dimatfit {
+ 0 | 1 => true,
+ 2 => false,
+ _ => params.text_width <= available,
+ }
+ }
+ } else {
+ false
+ };
+ let true_center = lv(d.definition_point);
+ let radius = chord.distance(true_center);
+ let text_outside = params.text_position.distance(true_center) > radius + 1.0e-5;
+ let draw_inside_line = !arrows_outside || params.dimtofl;
+ if draw_inside_line && !suppress.dim2 {
+ add_segment_with_text_break(&mut g.dim_lines, chord, near, params.text_break);
+ add_segment_with_text_break(&mut g.dim_lines, near, far, params.text_break);
+ add_segment_with_text_break(
+ &mut g.dim_lines,
+ far,
+ override_center,
+ params.text_break,
+ );
+ }
+ if arrows_outside && !suppress.dim2 {
+ add_segment(
+ &mut g.dim_lines,
+ chord,
+ chord + axis * (params.arrow_len * 2.0),
+ );
+ }
+ if !suppress.dim2 {
+ append_arrow(
+ &mut g,
+ chord,
+ if arrows_outside { axis } else { -axis },
+ arrow1,
+ );
+ }
+ if text_outside && params.text_movement == 0 && !suppress.dim2 {
+ add_segment_with_text_break(
+ &mut g.dim_lines,
+ chord,
+ params.text_position,
+ params.text_break,
+ );
+ }
+ if arrows_outside || text_outside {
+ append_center_mark(&mut g, true_center, params.dimcen, radius);
}
- append_arrow(
- &mut g,
- chord,
- normalized_or(near - chord, Vec3::X),
- arrow1,
- );
- append_center_mark(
- &mut g,
- lv(d.definition_point),
- params.dimcen,
- (chord - lv(d.definition_point)).length(),
- );
}
}
g
}
+fn add_segment_with_text_break(
+ points: &mut Vec<[f32; 3]>,
+ start: Vec3,
+ end: Vec3,
+ text_break: Option,
+) {
+ let segment = end - start;
+ let length = segment.length();
+ if length <= 1.0e-6 {
+ return;
+ }
+ let Some(text_break) = text_break else {
+ add_segment(points, start, end);
+ return;
+ };
+ let segment_curve = cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
+ start: [start.x as f64, start.y as f64],
+ end: [end.x as f64, end.y as f64],
+ });
+ let tolerance = cadkernel::geom2d::Tolerance::new((length as f64 * 1.0e-9).max(1.0e-9));
+ let inside = |padding: f32| {
+ let (sin, cos) = text_break.rotation.sin_cos();
+ let x_axis = Vec3::new(cos as f32, sin as f32, 0.0);
+ let y_axis = Vec3::new(-sin as f32, cos as f32, 0.0);
+ let half_width = text_break.half_width + padding;
+ let half_height = text_break.half_height + padding;
+ let corners = [
+ text_break.center - x_axis * half_width - y_axis * half_height,
+ text_break.center + x_axis * half_width - y_axis * half_height,
+ text_break.center + x_axis * half_width + y_axis * half_height,
+ text_break.center - x_axis * half_width + y_axis * half_height,
+ ];
+ let boundary: Vec<_> = (0..4)
+ .map(|index| {
+ let next = (index + 1) % 4;
+ cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
+ start: [corners[index].x as f64, corners[index].y as f64],
+ end: [corners[next].x as f64, corners[next].y as f64],
+ })
+ })
+ .collect();
+ cadkernel::geom2d::inside_spans(&boundary, &segment_curve, tolerance)
+ };
+ if inside(0.0).is_empty() {
+ add_segment(points, start, end);
+ return;
+ }
+ let mut cursor = 0.0_f32;
+ for [cut_start, cut_end] in inside(text_break.padding.max(0.0)) {
+ let cut_start = (cut_start as f32).clamp(0.0, 1.0);
+ let cut_end = (cut_end as f32).clamp(0.0, 1.0);
+ if cut_start - cursor > 1.0e-6 {
+ add_segment(points, start.lerp(end, cursor), start.lerp(end, cut_start));
+ }
+ cursor = cursor.max(cut_end);
+ }
+ if 1.0 - cursor > 1.0e-6 {
+ add_segment(points, start.lerp(end, cursor), end);
+ }
+}
+
fn jogged_radial_break(
chord: Vec3,
jog: Vec3,
@@ -4543,44 +4965,20 @@ fn append_linear_dimension(
false
};
- // The two suppression flags address the portions at the first and second
- // measured points independently. The text gap is also the natural split;
- // when there is no gap, split at the projected text position or midpoint.
let line_dir = normalized_or(d2_out - d1_out, axis);
let line_len = (d2_out - d1_out).length();
- let mut split = line_len * 0.5;
- let mut left_end = split;
- let mut right_start = split;
- if let Some((text_center, half_width, half_height)) = params.text_break {
- let along = (text_center - d1_out).dot(line_dir);
- if along > 0.0 && along < line_len {
- split = along;
- left_end = split;
- right_start = split;
- let perpendicular = (text_center - (d1_out + line_dir * along)).length();
- if perpendicular < half_height
- && along - half_width > 0.0
- && along + half_width < line_len
- {
- left_end = along - half_width;
- right_start = along + half_width;
- }
- }
- }
+ let split = params
+ .text_break
+ .map(|text_break| (text_break.center - d1_out).dot(line_dir))
+ .filter(|along| *along > 0.0 && *along < line_len)
+ .unwrap_or(line_len * 0.5);
+ let split_point = d1_out + line_dir * split;
let draw_inside_line = !arrows_outside || params.dimtofl;
- if draw_inside_line && !suppress.dim1 && left_end > 1e-6 {
- add_segment(
- &mut g.dim_lines,
- d1_out,
- d1_out + line_dir * left_end,
- );
+ if draw_inside_line && !suppress.dim1 && split > 1e-6 {
+ add_segment_with_text_break(&mut g.dim_lines, d1_out, split_point, params.text_break);
}
- if draw_inside_line && !suppress.dim2 && line_len - right_start > 1e-6 {
- add_segment(
- &mut g.dim_lines,
- d1_out + line_dir * right_start,
- d2_out,
- );
+ if draw_inside_line && !suppress.dim2 && line_len - split > 1e-6 {
+ add_segment_with_text_break(&mut g.dim_lines, split_point, d2_out, params.text_break);
}
if arrows_outside && !params.dimsoxd {
let stub = params.arrow_len * 2.0;
@@ -4636,34 +5034,19 @@ fn append_diameter_dimension(
let first = chord - axis * extension;
let second = far_chord + axis * extension;
let line_length = first.distance(second);
- let mut left_end = line_length * 0.5;
- let mut right_start = left_end;
- if let Some((text_center, half_width, half_height)) = params.text_break {
- let along = (text_center - first).dot(axis);
- if along > 0.0 && along < line_length {
- left_end = along;
- right_start = along;
- let perpendicular = (text_center - (first + axis * along)).length();
- if perpendicular < half_height
- && along - half_width > 0.0
- && along + half_width < line_length
- {
- left_end = along - half_width;
- right_start = along + half_width;
- }
- }
- }
+ let split = params
+ .text_break
+ .map(|text_break| (text_break.center - first).dot(axis))
+ .filter(|along| *along > 0.0 && *along < line_length)
+ .unwrap_or(line_length * 0.5);
+ let split_point = first + axis * split;
let draw_inside_line = !arrows_outside || params.dimtofl;
- if draw_inside_line && !suppress.dim1 && left_end > 1e-6 {
- add_segment(&mut g.dim_lines, first, first + axis * left_end);
+ if draw_inside_line && !suppress.dim1 && split > 1e-6 {
+ add_segment_with_text_break(&mut g.dim_lines, first, split_point, params.text_break);
}
- if draw_inside_line && !suppress.dim2 && line_length - right_start > 1e-6 {
- add_segment(
- &mut g.dim_lines,
- first + axis * right_start,
- second,
- );
+ if draw_inside_line && !suppress.dim2 && line_length - split > 1e-6 {
+ add_segment_with_text_break(&mut g.dim_lines, split_point, second, params.text_break);
}
if arrows_outside && !params.dimsoxd {
let stub = params.arrow_len * 2.0;
@@ -5126,17 +5509,7 @@ fn append_angular_dimension(
}
let a = arc_pts[index];
let b = arc_pts[index + 1];
- let hidden_by_text = params.text_break.is_some_and(|(center, half_width, half_height)| {
- let angle = draw_start + draw_delta * t;
- let radial = Vec3::new(angle.cos(), angle.sin(), 0.0);
- let tangent = Vec3::new(-angle.sin(), angle.cos(), 0.0);
- let offset = center - (a + b) * 0.5;
- offset.dot(tangent).abs() <= half_width
- && offset.dot(radial).abs() <= half_height
- });
- if !hidden_by_text {
- add_segment(&mut g.dim_lines, a, b);
- }
+ add_segment_with_text_break(&mut g.dim_lines, a, b, params.text_break);
}
}
@@ -5162,28 +5535,29 @@ fn append_angular_dimension(
}
}
- if let Some((text_center, half_width, _)) = params.text_break {
- let text_angle = (text_center.y - vertex.y).atan2(text_center.x - vertex.x);
+ if let Some(text_break) = params.text_break {
+ let text_angle = (text_break.center.y - vertex.y).atan2(text_break.center.x - vertex.x);
let into = (text_angle - start).rem_euclid(std::f32::consts::TAU);
if into > delta.abs() + 1.0e-6 {
let back_to_start = (start - text_angle).rem_euclid(std::f32::consts::TAU);
let forward_from_end = (text_angle - end).rem_euclid(std::f32::consts::TAU);
- let text_gap = (half_width / radius).min(std::f32::consts::FRAC_PI_2);
if back_to_start <= forward_from_end && !suppress.dim1 {
- append_sampled_arc(
+ append_sampled_arc_with_text_break(
&mut g.dim_lines,
vertex,
radius,
- text_angle + text_gap,
+ text_angle,
start,
+ params.text_break,
);
} else if !suppress.dim2 {
- append_sampled_arc(
+ append_sampled_arc_with_text_break(
&mut g.dim_lines,
vertex,
radius,
end,
- text_angle - text_gap,
+ text_angle,
+ params.text_break,
);
}
}
@@ -5222,6 +5596,19 @@ fn append_sampled_arc(
}
}
+fn append_sampled_arc_with_text_break(
+ lines: &mut Vec<[f32; 3]>,
+ vertex: Vec3,
+ radius: f32,
+ start: f32,
+ end: f32,
+ text_break: Option,
+) {
+ for pair in sample_angular_arc(vertex, radius, start, end - start).windows(2) {
+ add_segment_with_text_break(lines, pair[0], pair[1], text_break);
+ }
+}
+
fn sample_angular_arc(vertex: Vec3, radius: f32, start: f32, sweep: f32) -> Vec {
if sweep.abs() <= 1.0e-6 {
return vec![vertex + Vec3::new(start.cos() * radius, start.sin() * radius, 0.0)];
@@ -5533,6 +5920,37 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool
_ => text_width > span,
};
}
+ if let Dimension::LargeRadial(radial) = dim {
+ let radius = radial.definition_point.distance(&radial.chord_point);
+ if dim.base().text_user_positioned {
+ return radial
+ .definition_point
+ .distance(&dim.base().text_middle_point)
+ > radius + 1.0e-9;
+ }
+ if style.dimtix {
+ return false;
+ }
+ let available = radial.override_center.distance(&radial.chord_point);
+ let scale = if style.dimscale > 1.0e-9 {
+ style.dimscale
+ } else {
+ 1.0
+ };
+ let height = style.dimtxt * scale;
+ let gap = style.dimgap.abs() * scale;
+ let text_width = dimension_text_value(dim, Some(style))
+ .map(|value| value.chars().count() as f64 * height * 0.6 + gap * 2.0)
+ .unwrap_or(0.0);
+ let arrow = style.dimasz * scale;
+ let insufficient = text_width + arrow > available;
+ return insufficient
+ && match style.dimatfit {
+ 0 | 2 => true,
+ 1 | 3 => text_width > available,
+ _ => text_width > available,
+ };
+ }
if let Dimension::Radius(radius) = dim {
let dx = radius.definition_point.x - radius.angle_vertex.x;
let dy = radius.definition_point.y - radius.angle_vertex.y;
@@ -5625,6 +6043,8 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
.atan2(d.definition_point.x - d.angle_vertex.x),
Dimension::Diameter(d) => (d.definition_point.y - d.angle_vertex.y)
.atan2(d.definition_point.x - d.angle_vertex.x),
+ Dimension::LargeRadial(d) => (d.chord_point.y - d.override_center.y)
+ .atan2(d.chord_point.x - d.override_center.x),
Dimension::Ordinate(d) => {
let axis_rotation = -d.base.horizontal_direction;
if d.is_ordinate_type_x {
@@ -5633,7 +6053,6 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
axis_rotation
}
}
- _ => 0.0,
};
// Clamp to (-π/2, π/2] so text never appears upside-down.
let pi = std::f64::consts::PI;
@@ -6321,6 +6740,59 @@ fn text_on_dim_line(
)
}
+fn text_on_single_arrow_dim_line(
+ first: Vector3,
+ second: Vector3,
+ defpt: Vector3,
+ ax: f64,
+ ay: f64,
+ perp_off: f64,
+ text_width: f64,
+ arrow: f64,
+ dimtix: bool,
+ dimatfit: i16,
+ tad: i16,
+) -> Vector3 {
+ let (mut nx, mut ny) = (ax, ay);
+ if nx < 0.0 || (nx == 0.0 && ny < 0.0) {
+ nx = -nx;
+ ny = -ny;
+ }
+ let (perpendicular_x, perpendicular_y) = (-ny, nx);
+ let perpendicular_sign = match tad {
+ 4 => -1.0,
+ 2 => {
+ let offset = (defpt.x - first.x) * perpendicular_x
+ + (defpt.y - first.y) * perpendicular_y;
+ if offset >= 0.0 { 1.0 } else { -1.0 }
+ }
+ _ => 1.0,
+ };
+ let first_along = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
+ let second_along = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
+ let low = first_along.min(second_along);
+ let high = first_along.max(second_along);
+ let available = high - low;
+ let insufficient = text_width + arrow > available;
+ let text_outside = !dimtix
+ && insufficient
+ && match dimatfit {
+ 0 | 2 => true,
+ 1 | 3 => text_width > available,
+ _ => text_width > available,
+ };
+ let along = if text_outside {
+ high + arrow + text_width * 0.5
+ } else {
+ (first_along + second_along) * 0.5
+ };
+ Vector3::new(
+ defpt.x + ax * along + perpendicular_x * perp_off * perpendicular_sign,
+ defpt.y + ay * along + perpendicular_y * perp_off * perpendicular_sign,
+ defpt.z,
+ )
+}
+
fn dimension_text_pos_f64(
dim: &Dimension,
style: Option<&DimStyle>,
@@ -6352,7 +6824,13 @@ fn dimension_text_pos_f64(
let text_w = dimension_text_value(dim, style)
.map(|t| t.chars().count() as f64 * text_height * 0.6 + 2.0 * dimgap)
.unwrap_or(0.0);
- let arrow = text_height; // arrows are roughly text-height sized
+ let arrow = if matches!(dim, Dimension::LargeRadial(_)) {
+ style
+ .map(|style| style.dimasz * dim_scale)
+ .unwrap_or(text_height)
+ } else {
+ text_height
+ };
// Explicit per-entity override (text dragged to a custom location): the
// saved point wins. Otherwise the dimension style governs placement.
@@ -6454,6 +6932,24 @@ fn dimension_text_pos_f64(
}
Vector3::new(x, y, d.definition_point.z)
}
+ Dimension::LargeRadial(d) => {
+ let dx = d.chord_point.x - d.override_center.x;
+ let dy = d.chord_point.y - d.override_center.y;
+ let length = dx.hypot(dy).max(1.0e-12);
+ text_on_single_arrow_dim_line(
+ d.override_center,
+ d.chord_point,
+ d.definition_point,
+ dx / length,
+ dy / length,
+ perp_off,
+ text_w,
+ arrow,
+ dimtix,
+ dimatfit,
+ dimtad,
+ )
+ }
Dimension::Diameter(d) => {
let dx = d.definition_point.x - d.angle_vertex.x;
let dy = d.definition_point.y - d.angle_vertex.y;
@@ -6499,7 +6995,6 @@ fn dimension_text_pos_f64(
Dimension::Angular2Ln(d) => d.dimension_arc,
Dimension::Angular3Pt(d) => d.definition_point,
Dimension::Arc(d) => d.definition_point,
- Dimension::LargeRadial(d) => d.jog_point,
_ => base.text_middle_point,
};
Vector3::new(mid.x, mid.y + perp_off * perp_sign_default(), mid.z)
@@ -6511,39 +7006,130 @@ fn perp_sign_default() -> f64 {
1.0
}
-/// The measurement-text entity (Text or MText) for a baked `*D` block, built
-/// through the SAME `dimension_text_entity` the live renderer uses — so the
-/// baked text matches the on-screen value, position, height, rotation,
-/// alignment, text style and MText handling, and nothing shifts when the file
-/// is saved and reopened (the reload renders from the block). Returns `None`
-/// when the text is suppressed (`user_text` is a single space). `anno_scale` is
-/// the annotative scale (1.0 for a plain model-space bake).
+pub(crate) fn baked_large_radial_geometry(
+ dimension: &Dimension,
+ document: &CadDocument,
+) -> Option {
+ if !matches!(dimension, Dimension::LargeRadial(_)) {
+ return None;
+ }
+ let style_name = dimension.base().style_name.as_str();
+ let effective_style = document
+ .dim_styles
+ .iter()
+ .find(|style| {
+ style.name.eq_ignore_ascii_case(style_name)
+ || (style_name.trim().is_empty()
+ && style.name.eq_ignore_ascii_case("Standard"))
+ })
+ .map(|style| resolved_dimension_style(style, dimension, document));
+ let style = effective_style.as_ref();
+ let scale = style
+ .map(|style| dimension_style_scale(style, 1.0))
+ .unwrap_or(1.0);
+ let text_height = style.map(|style| style.dimtxt * scale).unwrap_or(2.5 * scale);
+ let arrow_size_raw = style.map(|style| style.dimasz * scale).unwrap_or(0.18 * scale);
+ let tick_size = style.map(|style| style.dimtsz * scale).unwrap_or(0.0);
+ let arrow_size = (arrow_size_raw as f32).max(0.001);
+ let arrow = if tick_size > 1.0e-9 {
+ ArrowKind::Tick {
+ size: (tick_size as f32).max(0.001),
+ }
+ } else if let Some(style) = style {
+ arrow_from_block(document, style.dimldrblk, arrow_size)
+ } else {
+ ArrowKind::Triangle {
+ size: arrow_size,
+ filled: true,
+ size_mul: 1.0,
+ }
+ };
+ let text = dimension_text_layout(dimension, style, text_height, scale);
+ let mut geometry = dimension_geometry(
+ dimension,
+ &arrow,
+ &arrow,
+ DimLineParams {
+ dimexo: style.map(|style| (style.dimexo * scale) as f32).unwrap_or(0.0),
+ dimexe: style.map(|style| (style.dimexe * scale) as f32).unwrap_or(0.0),
+ dimdle: style.map(|style| (style.dimdle * scale) as f32).unwrap_or(0.0),
+ dimfxl: style.map(|style| (style.dimfxl * scale) as f32).unwrap_or(1.0),
+ dimfxlon: style.is_some_and(|style| style.dimfxlon),
+ dimsoxd: style.is_some_and(|style| style.dimsoxd),
+ dimcen: style.map(|style| (style.dimcen * scale) as f32).unwrap_or(0.09),
+ ticks: tick_size > 1.0e-9,
+ arrow_len: arrow_size,
+ text_width: text.width,
+ dimatfit: style.map(|style| style.dimatfit).unwrap_or(3),
+ dimtix: style.is_some_and(|style| style.dimtix),
+ dimtofl: style.is_some_and(|style| style.dimtofl),
+ text_position: text.position,
+ horizontal_text: text.horizontal,
+ text_movement: style.map(|style| style.dimtmove).unwrap_or(0),
+ text_break: text.break_box,
+ },
+ SuppressFlags {
+ ext1: style.is_some_and(|style| style.dimse1),
+ ext2: style.is_some_and(|style| style.dimse2),
+ dim1: style.is_some_and(|style| style.dimsd1),
+ dim2: style.is_some_and(|style| style.dimsd2),
+ },
+ );
+ if !style.is_some_and(|style| style.dimse1) {
+ if let Some(points) = crate::scene::dimension_assoc::radial_extension_points(
+ document,
+ dimension.base().common.handle,
+ style.map(|style| style.dimexo * scale).unwrap_or(0.0),
+ style.map(|style| style.dimexe * scale).unwrap_or(0.0),
+ ) {
+ let points: Vec<_> = points.into_iter().map(vec3_local).collect();
+ add_polyline(&mut geometry.ext_lines, &points);
+ }
+ }
+ if style.is_some_and(|style| style.dimtmove == 1) {
+ if let Some((anchor, endpoint)) = dimtmove_leader_endpoints(dimension, text.position) {
+ if anchor.distance(endpoint) > text_height as f32 {
+ add_segment_with_text_break(
+ &mut geometry.dim_lines,
+ anchor,
+ endpoint,
+ text.break_box,
+ );
+ }
+ }
+ }
+ apply_dimension_breaks(
+ document,
+ dimension.base().common.handle,
+ &mut geometry.dim_lines,
+ );
+ Some(geometry)
+}
+
+/// Build measurement text for a saved dimension block.
pub(crate) fn baked_dimension_text_entity(
dim: &Dimension,
document: &CadDocument,
anno_scale: f64,
) -> Option {
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"))
- });
- let dim_scale = style
- .map(|s| {
- if s.dimscale > 1e-6 {
- s.dimscale
- } else {
- anno_scale
- }
+ let effective_style = document
+ .dim_styles
+ .iter()
+ .find(|style| {
+ style.name.eq_ignore_ascii_case(style_name)
+ || (style_name.trim().is_empty()
+ && style.name.eq_ignore_ascii_case("Standard"))
})
+ .map(|style| resolved_dimension_style(style, dim, document));
+ let style = effective_style.as_ref();
+ let dim_scale = style
+ .map(|style| dimension_style_scale(style, anno_scale))
.unwrap_or(1.0);
let dim_txt = style
.map(|s| s.dimtxt * dim_scale)
.unwrap_or(2.5 * dim_scale);
let mut ent = dimension_text_entity(dim, dim_txt, style, document, dim_scale)?;
- // For a non-default-aligned Text, pin the DXF alignment point (group 11) to
- // the insertion point so other CAD programs anchor the centred text where
- // OCS does, not at the world origin.
if let EntityType::Text(t) = &mut ent {
t.alignment_point = Some(t.insertion_point);
}
diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs
index 4e80f01b8..49b4a8127 100644
--- a/src/entities/multileader.rs
+++ b/src/entities/multileader.rs
@@ -3,6 +3,7 @@ use acadrust::entities::{
MultiLeader, MultiLeaderPathType, MultiLeaderPropertyOverrideFlags, TextAlignmentType,
TextAttachmentDirectionType, TextAttachmentType,
};
+use cadkernel::geom2d::Transform;
use crate::entities::text_support::{
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
@@ -1120,7 +1121,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
!ml.text_frame
} else {
value == "true"
- }
+ };
+ ml.property_override_flags
+ .insert(MultiLeaderPropertyOverrideFlags::TEXT_FRAME);
}
"path_type" => {
ml.path_type = match value {
@@ -1200,6 +1203,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
"dogleg_length" => {
if let Some(v) = f64(value) {
ml.dogleg_length = v;
+ ml.property_override_flags
+ .insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
}
}
"arrowhead_size" => {
@@ -1395,6 +1400,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
ml.context.text_rotation.sin(),
0.0,
);
+ ml.property_override_flags
+ .insert(MultiLeaderPropertyOverrideFlags::TEXT_ANGLE);
}
}
"line_spacing" => {
@@ -1941,9 +1948,7 @@ impl MultiLeaderTess for MultiLeader {
}
}
- // Vertically-attached leaders (text above/below) have no dogleg:
- // the leader meets the text edge directly, so the short horizontal
- // dash would be a stray mark AutoCAD never draws.
+ // Vertically attached leaders meet the text edge directly.
let vertical_attach = matches!(
ml.text_attachment_direction,
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
@@ -2074,20 +2079,28 @@ impl MultiLeaderTess for MultiLeader {
}
}
if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] {
+ let to_block = Transform::rotation(-ml.block_rotation);
let from_left = ml
.context
.leader_roots
.first()
- .map_or(true, |root| root.direction.x >= 0.0);
+ .map_or(true, |root| {
+ to_block.apply_vector([
+ root.direction.x,
+ root.direction.y,
+ ])[0]
+ >= 0.0
+ });
let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64;
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
let local_x =
(anchor_x - block_record.base_point.x) * ml.block_scale.x;
let local_y =
(anchor_y - block_record.base_point.y) * ml.block_scale.y;
- let (sin_r, cos_r) = ml.block_rotation.sin_cos();
- insertion.x -= local_x * cos_r - local_y * sin_r;
- insertion.y -= local_x * sin_r + local_y * cos_r;
+ let rotated = Transform::rotation(ml.block_rotation)
+ .apply_vector([local_x, local_y]);
+ insertion.x -= rotated[0];
+ insertion.y -= rotated[1];
}
}
let mut synth_ins = acadrust::entities::Insert::new(block_name, insertion);
@@ -2171,12 +2184,7 @@ impl MultiLeaderTess for MultiLeader {
let local_ins_x = (ins.x) as f32;
let z = (ins.z) as f32;
- // Rotation: the annotation context is AutoCAD's BAKED display
- // state — text_direction / text_rotation already reflect whatever
- // the style's angle setting produced at edit time. Re-deriving
- // from ml.text_angle_type here (e.g. forcing Horizontal to 0)
- // un-rotated leaders whose stored context is angled; the angle
- // type only guides recomputation during edits, not display.
+ // The context stores the resolved display rotation.
let td = ctx.text_direction;
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
(td.y as f32).atan2(td.x as f32)
diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs
index 2ec212d1e..9ecfa7c70 100644
--- a/src/modules/annotate/aligned_dim.rs
+++ b/src/modules/annotate/aligned_dim.rs
@@ -135,6 +135,8 @@ impl CadCommand for AlignedDimensionCommand {
CmdResult::CommitDimension {
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
association: DimensionAssociationInput::Infer(self.source_handle),
+ preserve_base_style: false,
+ continue_command: false,
}
}
}
diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs
index 6bd3ef2e4..f23e3b555 100644
--- a/src/modules/annotate/angular_dim.rs
+++ b/src/modules/annotate/angular_dim.rs
@@ -234,6 +234,8 @@ impl AngularDimensionCommand {
CmdResult::CommitDimension {
entity,
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
+ preserve_base_style: false,
+ continue_command: false,
}
}
diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs
index f0225c1c9..bbdfaf412 100644
--- a/src/modules/annotate/arc_length_dim.rs
+++ b/src/modules/annotate/arc_length_dim.rs
@@ -265,6 +265,8 @@ impl ArcLengthDimensionCommand {
CmdResult::CommitDimension {
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
association,
+ preserve_base_style: false,
+ continue_command: false,
}
}
}
diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs
index 010434c71..19dfcb1ca 100644
--- a/src/modules/annotate/diameter_dim.rs
+++ b/src/modules/annotate/diameter_dim.rs
@@ -113,6 +113,8 @@ impl CadCommand for DiameterDimensionCommand {
entity: source_plane
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
association: DimensionAssociationInput::Infer(self.source_handle),
+ preserve_base_style: false,
+ continue_command: false,
}
}
}
diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs
index 207e9c352..834222a34 100644
--- a/src/modules/annotate/dim_baseline.rs
+++ b/src/modules/annotate/dim_baseline.rs
@@ -1,20 +1,21 @@
-// DIMBASELINE command — stacked baseline dimensions all measured from the same origin.
-//
-// Each new point becomes the second extension line origin of a new dimension.
-// The first extension line is always the same base origin point.
-// Each new dimension line is placed further from the baseline by DIMDLI (increment).
-//
-// Constructed from commands.rs after finding the last placed linear/aligned dimension.
+use std::collections::HashMap;
+use std::f64::consts::PI;
-use acadrust::entities::{Dimension, DimensionLinear};
+use acadrust::entities::{
+ Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase,
+ DimensionLinear, DimensionOrdinate,
+};
use acadrust::types::Vector3;
-use acadrust::EntityType;
-use glam::{DVec3, Vec3};
+use acadrust::{EntityType, Handle};
+use cadkernel::geom2d::{
+ arc_span, intersect, nearest_of, Arc, Curve, Line, Tolerance, Transform, Vec2, XLine,
+};
+use cadkernel::space::Plane;
+use glam::DVec3;
-use crate::command::{CadCommand, CmdResult};
+use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
-use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_baseline.svg"));
@@ -27,100 +28,353 @@ pub fn tool() -> ToolDef {
}
}
-/// Fallback stacking increment (world units) when no DimStyle is available.
-const DEFAULT_DIMDLI: f64 = 1.5;
+#[derive(Clone)]
+struct SourceStyle {
+ layer: String,
+ style_name: String,
+ normal: Vector3,
+ text_rotation: f64,
+ horizontal_direction: f64,
+}
+
+impl SourceStyle {
+ fn from_base(base: &DimensionBase) -> Self {
+ Self {
+ layer: base.common.layer.clone(),
+ style_name: base.style_name.clone(),
+ normal: base.normal,
+ text_rotation: base.text_rotation,
+ horizontal_direction: base.horizontal_direction,
+ }
+ }
+
+ fn apply(&self, base: &mut DimensionBase) {
+ base.common.layer = self.layer.clone();
+ base.style_name = self.style_name.clone();
+ base.normal = self.normal;
+ base.text_rotation = self.text_rotation;
+ base.horizontal_direction = self.horizontal_direction;
+ }
+}
+
+enum BaselineKind {
+ Linear {
+ fixed: Vec2,
+ rotation: f64,
+ aligned: bool,
+ perpendicular: Vec2,
+ next_offset: f64,
+ increment: f64,
+ },
+ Angular2Ln {
+ fixed_line: [Vec2; 2],
+ fixed_is_first: bool,
+ vertex: Vec2,
+ fixed_ray: Vec2,
+ next_radius: f64,
+ increment: f64,
+ },
+ Angular3Pt {
+ vertex: Vec2,
+ fixed_point: Vec2,
+ fixed_ray: Vec2,
+ next_radius: f64,
+ increment: f64,
+ },
+ Ordinate {
+ definition: Vec2,
+ leader: Vec2,
+ is_x: bool,
+ },
+}
+
+struct BaselineState {
+ kind: BaselineKind,
+ style: SourceStyle,
+ plane: Plane,
+}
pub struct DimBaselineCommand {
- /// Fixed first-extension-line origin (never changes).
- base_p1: DVec3,
- /// 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: DVec3,
- /// Perpendicular distance of the NEXT dimension line from the extension-line axis.
- next_offset: f64,
- /// Stacking increment from the active DimStyle (DIMDLI).
- dimdli: f64,
- /// True once we have a base dimension loaded.
- ready: bool,
+ base: Option,
+ injected: Option,
+ dimdli_by_style: HashMap,
+ current_style_name: String,
+ fallback_dimdli: f64,
+ preserve_base_style: bool,
+ committed: usize,
}
impl DimBaselineCommand {
- /// No base dim found — cancel immediately.
- pub fn new() -> Self {
- Self {
- base_p1: DVec3::ZERO,
- rotation: 0.0,
- text_rotation: 0.0,
- perp: DVec3::Y,
- next_offset: DEFAULT_DIMDLI,
- dimdli: DEFAULT_DIMDLI,
- ready: false,
- }
- }
-
- /// Build from the last placed dimension.
- ///
- /// `p1` — first extension line origin (fixed baseline).
- /// `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.
- /// `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,
- text_rotation: f64,
- dimdli: f32,
+ pub fn new(
+ recent: Option,
+ dimdli_by_style: HashMap,
+ current_style_name: String,
+ fallback_dimdli: f64,
+ preserve_base_style: bool,
) -> Self {
- // The base dim's points enter as f32 from the caller; promote to f64 so
- // all committed-coordinate math downstream stays exact.
- let p1 = p1.as_dvec3();
- let definition_point = definition_point.as_dvec3();
- // Measurement axis = the base dim's rotation angle (any angle, incl. a
- // UCS-aligned one), not a world horizontal/vertical.
- let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
- let perp = DVec3::new(-axis.y, axis.x, 0.0);
- let base_offset = (definition_point - p1).dot(perp);
- let dimdli = dimdli as f64;
- let dimdli = if dimdli.abs() < 1e-6 {
- DEFAULT_DIMDLI
- } else {
- dimdli.abs()
+ let mut command = Self {
+ base: None,
+ injected: None,
+ dimdli_by_style,
+ current_style_name,
+ fallback_dimdli,
+ preserve_base_style,
+ committed: 0,
};
- // 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,
- ready: true,
+ if let Some(entity) = recent {
+ let _ = command.install_base(&entity, None);
+ }
+ command
+ }
+
+ fn effective_increment(&self, style_name: &str) -> f64 {
+ let style_name = if self.preserve_base_style {
+ style_name
+ } else {
+ &self.current_style_name
+ };
+ self.dimdli_by_style
+ .get(&style_name.to_ascii_lowercase())
+ .copied()
+ .filter(|value| value.is_finite() && value.abs() > 1.0e-9)
+ .unwrap_or(self.fallback_dimdli)
+ .abs()
+ }
+
+ fn install_base(&mut self, entity: &EntityType, pick: Option) -> bool {
+ let EntityType::Dimension(dimension) = entity else {
+ return false;
+ };
+ let style = SourceStyle::from_base(dimension.base());
+ let increment = self.effective_increment(&style.style_name);
+ let plane = plane_from_normal(dimension.base().definition_point, style.normal);
+ let pick = pick.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
+ let kind = match dimension {
+ Dimension::Linear(source) => {
+ let first = project(&plane, source.first_point);
+ let second = project(&plane, source.second_point);
+ let (fixed, _) = nearest_end(first, second, pick);
+ linear_base(
+ fixed,
+ source.rotation,
+ false,
+ project(&plane, source.base.definition_point),
+ increment,
+ )
+ }
+ Dimension::Aligned(source) => {
+ let first = project(&plane, source.first_point);
+ let second = project(&plane, source.second_point);
+ let (fixed, other) = nearest_end(first, second, pick);
+ linear_base(
+ fixed,
+ (other - fixed).angle(),
+ true,
+ project(&plane, source.base.definition_point),
+ increment,
+ )
+ }
+ Dimension::Angular2Ln(source) => {
+ let lines = [
+ [
+ project(&plane, source.first_point),
+ project(&plane, source.second_point),
+ ],
+ [
+ project(&plane, source.angle_vertex),
+ project(&plane, source.definition_point),
+ ],
+ ];
+ let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
+ let fixed_index = nearest_line(lines, pick);
+ let fixed_line = lines[fixed_index];
+ let fixed_end = if fixed_index == 0 {
+ lines[0][1]
+ } else {
+ lines[1][1]
+ };
+ let fixed_ray = fixed_end - vertex;
+ let radius = project(&plane, source.dimension_arc)
+ .distance(vertex)
+ .max(1.0e-9);
+ BaselineKind::Angular2Ln {
+ fixed_line,
+ fixed_is_first: fixed_index == 0,
+ vertex,
+ fixed_ray,
+ next_radius: radius + increment,
+ increment,
+ }
+ }
+ Dimension::Angular3Pt(source) => {
+ let vertex = project(&plane, source.angle_vertex);
+ let points = [
+ project(&plane, source.first_point),
+ project(&plane, source.second_point),
+ ];
+ let fixed_index = nearest_line(
+ [[vertex, points[0]], [vertex, points[1]]],
+ pick,
+ );
+ let fixed_point = points[fixed_index];
+ let radius = project(&plane, source.definition_point)
+ .distance(vertex)
+ .max(1.0e-9);
+ BaselineKind::Angular3Pt {
+ vertex,
+ fixed_point,
+ fixed_ray: fixed_point - vertex,
+ next_radius: radius + increment,
+ increment,
+ }
+ }
+ Dimension::Ordinate(source) => BaselineKind::Ordinate {
+ definition: project(&plane, source.definition_point),
+ leader: project(&plane, source.leader_endpoint),
+ is_x: source.is_ordinate_type_x,
+ },
+ _ => return false,
+ };
+ self.base = Some(BaselineState { kind, style, plane });
+ self.committed = 0;
+ true
+ }
+
+ fn build_dimension(&self, point_world: DVec3) -> Option {
+ let state = self.base.as_ref()?;
+ let point = state.plane.project(point_world.to_array()).map(Vec2::from)?;
+ let mut dimension = match &state.kind {
+ BaselineKind::Linear {
+ fixed,
+ rotation,
+ aligned,
+ perpendicular,
+ next_offset,
+ ..
+ } => build_linear(
+ state.plane,
+ *fixed,
+ point,
+ *rotation,
+ *aligned,
+ *perpendicular,
+ *next_offset,
+ )?,
+ BaselineKind::Angular2Ln {
+ fixed_line,
+ fixed_is_first,
+ vertex,
+ fixed_ray,
+ next_radius,
+ ..
+ } => {
+ let moving = point - *vertex;
+ let (definition, text) =
+ angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?;
+ let mut result = DimensionAngular2Ln::default();
+ if *fixed_is_first {
+ result.first_point = world(state.plane, fixed_line[0]);
+ result.second_point = world(state.plane, fixed_line[1]);
+ result.angle_vertex = world(state.plane, *vertex);
+ result.definition_point = world(state.plane, point);
+ } else {
+ result.first_point = world(state.plane, *vertex);
+ result.second_point = world(state.plane, point);
+ result.angle_vertex = world(state.plane, fixed_line[0]);
+ result.definition_point = world(state.plane, fixed_line[1]);
+ }
+ result.dimension_arc = world(state.plane, definition);
+ result.base.definition_point = result.dimension_arc;
+ result.base.text_middle_point = world(state.plane, text);
+ result.base.insertion_point = result.base.text_middle_point;
+ Dimension::Angular2Ln(result)
+ }
+ BaselineKind::Angular3Pt {
+ vertex,
+ fixed_point,
+ fixed_ray,
+ next_radius,
+ ..
+ } => {
+ let moving = point - *vertex;
+ let (definition, text) =
+ angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?;
+ let mut result = DimensionAngular3Pt::new(
+ world(state.plane, *vertex),
+ world(state.plane, *fixed_point),
+ world(state.plane, point),
+ );
+ result.definition_point = world(state.plane, definition);
+ result.base.definition_point = result.definition_point;
+ result.base.text_middle_point = world(state.plane, text);
+ result.base.insertion_point = result.base.text_middle_point;
+ Dimension::Angular3Pt(result)
+ }
+ BaselineKind::Ordinate {
+ definition,
+ leader,
+ is_x,
+ } => {
+ let to_dimension = Transform::rotation(-state.style.horizontal_direction);
+ let from_dimension = Transform::rotation(state.style.horizontal_direction);
+ let feature = Vec2::from(to_dimension.apply_point(point.into()));
+ let source_leader = Vec2::from(to_dimension.apply_point((*leader).into()));
+ let leader = if *is_x {
+ Vec2::new(feature.x, source_leader.y)
+ } else {
+ Vec2::new(source_leader.x, feature.y)
+ };
+ let leader = Vec2::from(from_dimension.apply_point(leader.into()));
+ let mut result = DimensionOrdinate::new(
+ world(state.plane, point),
+ world(state.plane, leader),
+ *is_x,
+ );
+ result.definition_point = world(state.plane, *definition);
+ result.base.definition_point = result.definition_point;
+ result.base.text_middle_point = result.leader_endpoint;
+ result.base.insertion_point = result.base.text_middle_point;
+ Dimension::Ordinate(result)
+ }
+ };
+ state.style.apply(dimension.base_mut());
+ refresh_measurement(&mut dimension, state.plane);
+ dimension.base_mut().common.handle = Handle::NULL;
+ dimension.base_mut().block_name.clear();
+ Some(dimension)
+ }
+
+ fn advance(&mut self, direction: f64) {
+ let Some(state) = self.base.as_mut() else {
+ return;
+ };
+ match &mut state.kind {
+ BaselineKind::Linear {
+ next_offset,
+ increment,
+ ..
+ } => *next_offset += *increment * direction,
+ BaselineKind::Angular2Ln {
+ next_radius,
+ increment,
+ ..
+ }
+ | BaselineKind::Angular3Pt {
+ next_radius,
+ increment,
+ ..
+ } => *next_radius = (*next_radius + *increment * direction).max(1.0e-9),
+ BaselineKind::Ordinate { .. } => {}
}
}
-}
-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: DVec3) -> (DVec3, DVec3) {
- 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)
+ fn undo_one(&mut self) -> Option {
+ if self.committed == 0 {
+ return Some(CmdResult::NeedPoint);
+ }
+ self.committed -= 1;
+ self.advance(-1.0);
+ Some(CmdResult::UndoDocument)
}
}
@@ -130,104 +384,464 @@ impl CadCommand for DimBaselineCommand {
}
fn prompt(&self) -> String {
- if !self.ready {
- t!("DIMBASELINE No base dimension found. Place a dimension first.").into_owned()
+ if self.base.is_none() {
+ "DIMBASELINE Select base dimension:".to_string()
+ } else if self
+ .base
+ .as_ref()
+ .is_some_and(|base| matches!(&base.kind, BaselineKind::Ordinate { .. }))
+ {
+ "DIMBASELINE Specify feature location [Undo/Select]