fix(annotate): align annotation ribbon workflows

This commit is contained in:
ramox81 committed 2026-09-14 11:55:08 +03:00
1 parent fc1788df65
commit 6505b27818
26 files changed
+1921 -345

No files matched your search

+595 -23
View File
@@ -2777,6 +2777,33 @@ impl OpenCADStudio {
}
self.refresh_properties();
}
CmdResult::UpdateEntityAndFinish { handle, entity } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
let label = self.history_label_from_active_cmd(i, "EDIT");
self.push_undo_snapshot(i, label);
let is_dimension = matches!(entity, acadrust::EntityType::Dimension(_));
if let Some(current) = self.tabs[i].scene.document.get_entity_mut(handle) {
*current = entity;
if is_dimension {
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
}
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
} else {
self.command_line.push_error(
crate::t!("The selected object is no longer available.").as_ref(),
);
}
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.refresh_properties();
}
CmdResult::AddSketchConstraint { kind, refs, driving_param, label } => {
if let Err(message) = self.tabs[i]
.scene
@@ -2948,36 +2975,111 @@ impl OpenCADStudio {
if new_entities.len() == 1 {
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
let layer = xl.common.layer.clone();
if layer.starts_with("__DIMBREAK__")
|| layer.starts_with("__DIMBREAK_AUTO__")
{
// DIMBREAK needs a break-gap field on the dimension
// model (not yet present) to store and render the gap.
// Report honestly rather than claiming success while
// changing nothing. (#181 / DIM-020)
self.command_line
.push_info(crate::t!("DIMBREAK: not yet implemented — nothing changed.").as_ref());
if layer.starts_with("__DIMBREAK_") {
self.push_undo_snapshot(i, "DIMBREAK");
let result = apply_dimbreak(&mut self.tabs[i].scene, &layer);
if result.changed.is_empty() {
self.command_line.push_info(&result.message);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
return Task::none();
}
for handle in &result.changed {
self.tabs[i].scene.invalidate_dim_block_recorded(*handle);
}
let changes: Vec<_> = result
.changed
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(&result.message);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
return Task::none();
}
if layer.starts_with("__DIMSPACE__") {
self.push_undo_snapshot(i, "DIMSPACE");
if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") {
apply_dimspace(&mut self.tabs[i].scene, encoded);
}
self.push_undo_snapshot(i, "DIMSPACE");
self.command_line.push_output(crate::t!("DIMSPACE Spacing adjusted.").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("__DIMJOG__") {
// DIMJOGLINE needs a jog-point field on the dimension
// model (not yet present) to store and render the jog.
// Report honestly rather than faking success. (DIM-019)
self.command_line
.push_info(crate::t!("DIMJOGLINE: not yet implemented — nothing changed.").as_ref());
if layer.starts_with("__DIMJOG__")
|| layer.starts_with("__DIMJOG_REMOVE__")
{
let remove = layer.starts_with("__DIMJOG_REMOVE__");
let point = layer
.strip_prefix("__DIMJOG__")
.and_then(|encoded| encoded.split_once('|'))
.and_then(|(_, coordinates)| {
let mut values = coordinates
.split(',')
.filter_map(|value| value.parse::<f64>().ok());
Some(acadrust::types::Vector3::new(
values.next()?,
values.next()?,
values.next()?,
))
});
let valid = matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(
acadrust::entities::Dimension::Linear(_)
| acadrust::entities::Dimension::Aligned(_)
))
);
if !valid || (!remove && point.is_none()) {
self.command_line.push_error(
crate::t!("DIMJOGLINE: select a linear or aligned dimension.")
.as_ref(),
);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
return Task::none();
}
self.push_undo_snapshot(i, "DIMJOGLINE");
if remove {
crate::scene::view::dispatch::set_entity_xdata(
&mut self.tabs[i].scene.document,
handle,
"ACAD_DSTYLE_DIMJAG_POSITION",
None,
);
} else if let Some(point) = point {
use acadrust::xdata::XDataValue;
crate::scene::view::dispatch::set_entity_xdata(
&mut self.tabs[i].scene.document,
handle,
"ACAD_DSTYLE_DIMJAG_POSITION",
Some(vec![
XDataValue::Integer16(387),
XDataValue::Integer16(3),
XDataValue::Integer16(389),
XDataValue::Point3D(point),
]),
);
}
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
self.tabs[i].scene.bump_entities(&[(
handle,
crate::scene::ChangeKind::Modified,
)]);
self.tabs[i].dirty = true;
self.command_line.push_output(
if remove {
crate::t!("DIMJOGLINE: jog removed.")
} else {
crate::t!("DIMJOGLINE: jog added.")
}
.as_ref(),
);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
return Task::none();
@@ -5621,6 +5723,72 @@ impl OpenCADStudio {
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
}
CmdResult::EditDimensions {
mut handles,
operation,
} => {
handles.retain(|handle| {
!self.tabs[i].scene.is_layer_locked(*handle)
&& matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(acadrust::EntityType::Dimension(_))
)
});
if handles.is_empty() {
self.command_line
.push_error(crate::t!("DIMEDIT: no editable dimensions were selected.").as_ref());
} else {
self.push_undo_snapshot(i, "DIMEDIT");
for handle in &handles {
if let Some(acadrust::EntityType::Dimension(dimension)) =
self.tabs[i].scene.document.get_entity_mut(*handle)
{
use crate::command::DimensionEditOperation as Operation;
match &operation {
Operation::Home => {
let base = dimension.base_mut();
base.text_middle_point = acadrust::types::Vector3::ZERO;
base.insertion_point = acadrust::types::Vector3::ZERO;
base.text_user_positioned = false;
base.text_rotation = 0.0;
}
Operation::NewText(text) => {
dimension.base_mut().set_text_override(
(!text.is_empty()).then_some(text.clone()),
);
}
Operation::Rotate(degrees) => {
dimension.base_mut().text_rotation = degrees.to_radians();
}
Operation::Oblique(degrees) => match dimension {
acadrust::entities::Dimension::Linear(value) => {
value.ext_line_rotation = degrees.to_radians();
}
acadrust::entities::Dimension::Aligned(value) => {
value.ext_line_rotation = degrees.to_radians();
}
_ => {}
},
}
}
self.tabs[i].scene.invalidate_dim_block_recorded(*handle);
}
let changes: Vec<_> = handles
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::tf!("DIMEDIT: updated {} dimension(s).", handles.len()).as_ref(),
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
}
CmdResult::DdeditEntity { handle, new_text } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
@@ -6176,6 +6344,382 @@ fn remap_object(
// ── DIMSPACE helper ───────────────────────────────────────────────────────────
struct DimBreakResult {
changed: Vec<Handle>,
message: String,
}
#[derive(Clone, Copy)]
enum DimBreakOperation {
Auto,
Object(Handle),
Manual(glam::DVec3, glam::DVec3),
Remove,
}
fn parse_dim_break_handles(encoded: &str) -> Vec<Handle> {
encoded
.split(';')
.filter_map(|value| value.parse::<u64>().ok())
.map(Handle::from)
.filter(|handle| !handle.is_null())
.collect()
}
fn parse_dim_break_point(encoded: &str) -> Option<glam::DVec3> {
let mut values = encoded.split(',').map(str::parse::<f64>);
Some(glam::DVec3::new(
values.next()?.ok()?,
values.next()?.ok()?,
values.next()?.ok()?,
))
}
fn parse_dim_break(encoded: &str) -> Option<(Vec<Handle>, DimBreakOperation)> {
if let Some(handles) = encoded.strip_prefix("__DIMBREAK_AUTO__") {
return Some((parse_dim_break_handles(handles), DimBreakOperation::Auto));
}
if let Some(handles) = encoded.strip_prefix("__DIMBREAK_REMOVE__") {
return Some((parse_dim_break_handles(handles), DimBreakOperation::Remove));
}
if let Some(rest) = encoded.strip_prefix("__DIMBREAK_OBJECT__") {
let (handles, crossing) = rest.split_once('|')?;
return Some((
parse_dim_break_handles(handles),
DimBreakOperation::Object(Handle::from(crossing.parse::<u64>().ok()?)),
));
}
if let Some(rest) = encoded.strip_prefix("__DIMBREAK_MANUAL__") {
let mut parts = rest.split('|');
let handles = parse_dim_break_handles(parts.next()?);
let first = parse_dim_break_point(parts.next()?)?;
let second = parse_dim_break_point(parts.next()?)?;
return Some((handles, DimBreakOperation::Manual(first, second)));
}
None
}
fn wire_segments(
models: &[crate::scene::model::wire_model::WireModel],
) -> Vec<(glam::DVec3, glam::DVec3)> {
let mut output = Vec::new();
for model in models {
for run in model.points.split(|point| !point[0].is_finite()) {
for pair in run.windows(2) {
let first = glam::DVec3::new(
pair[0][0] as f64,
pair[0][1] as f64,
pair[0][2] as f64,
);
let second = glam::DVec3::new(
pair[1][0] as f64,
pair[1][1] as f64,
pair[1][2] as f64,
);
if first.is_finite()
&& second.is_finite()
&& first.distance_squared(second) > 1.0e-18
{
output.push((first, second));
}
}
}
}
output
}
fn segment_intersection_xy(
first: (glam::DVec3, glam::DVec3),
second: (glam::DVec3, glam::DVec3),
) -> Option<(glam::DVec3, glam::DVec3)> {
let p = first.0;
let r = first.1 - first.0;
let q = second.0;
let s = second.1 - second.0;
let cross = |a: glam::DVec3, b: glam::DVec3| a.x * b.y - a.y * b.x;
let denominator = cross(r, s);
if denominator.abs() <= 1.0e-12 {
return None;
}
let qp = q - p;
let t = cross(qp, s) / denominator;
let u = cross(qp, r) / denominator;
if !(-1.0e-9..=1.0 + 1.0e-9).contains(&t)
|| !(-1.0e-9..=1.0 + 1.0e-9).contains(&u)
{
return None;
}
let direction = r.normalize_or_zero();
if direction.length_squared() <= 1.0e-18 {
return None;
}
let center = p + r * t.clamp(0.0, 1.0);
let half_gap = 0.25_f64.min(first.0.distance(first.1) * 0.2);
Some((
center - direction * half_gap,
center + direction * half_gap,
))
}
fn break_object_handle(document: &acadrust::CadDocument, dimension: Handle) -> Option<Handle> {
document.objects.iter().find_map(|(handle, object)| {
let acadrust::objects::ObjectType::DataObject(object) = object else {
return None;
};
let acadrust::objects::DataObjectData::BreakData(data) = &object.data else {
return None;
};
(data.dimension_reference == dimension).then_some(*handle)
})
}
fn remove_dimension_break_data(document: &mut acadrust::CadDocument, dimension: Handle) -> bool {
let handles: Vec<_> = document
.objects
.iter()
.filter_map(|(handle, object)| match object {
acadrust::objects::ObjectType::DataObject(object) => match &object.data {
acadrust::objects::DataObjectData::BreakData(data)
if data.dimension_reference == dimension => Some(*handle),
_ => None,
},
_ => None,
})
.collect();
if handles.is_empty() {
return false;
}
for handle in &handles {
document.objects.remove(handle);
}
if let Some(dictionary_handle) = document.extension_dictionary_handle(dimension) {
if let Some(acadrust::objects::ObjectType::Dictionary(dictionary)) =
document.objects.get_mut(&dictionary_handle)
{
dictionary.entries.retain(|(name, handle)| {
!name.eq_ignore_ascii_case("ACAD_BREAKDATA") && !handles.contains(handle)
});
dictionary
.hard_owner_entries
.retain(|name| !name.eq_ignore_ascii_case("ACAD_BREAKDATA"));
}
}
true
}
fn write_dimension_break_data(
document: &mut acadrust::CadDocument,
dimension: Handle,
reserved_reference: Handle,
references: Vec<acadrust::objects::BreakPointReference>,
) -> bool {
use acadrust::objects::{BreakData, DataObject, DataObjectData, Dictionary, ObjectType};
if references.is_empty() {
return remove_dimension_break_data(document, dimension);
}
if let Some(handle) = break_object_handle(document, dimension) {
if let Some(ObjectType::DataObject(object)) = document.objects.get_mut(&handle) {
object.data = DataObjectData::BreakData(BreakData {
version: 0,
dimension_reference: dimension,
reserved_reference,
point_references: references,
});
return true;
}
}
let dictionary_handle = match document.extension_dictionary_handle(dimension) {
Some(handle) => handle,
None => {
let handle = document.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = dimension;
dictionary.hard_owner = true;
document
.objects
.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(entity) = document.get_entity_mut(dimension) {
entity.common_mut().xdictionary_handle = Some(handle);
}
handle
}
};
let object_handle = document.allocate_handle();
let mut object = DataObject::new(DataObjectData::BreakData(BreakData {
version: 0,
dimension_reference: dimension,
reserved_reference,
point_references: references,
}));
object.handle = object_handle;
object.owner = dictionary_handle;
document
.objects
.insert(object_handle, ObjectType::DataObject(object));
if let Some(ObjectType::Dictionary(dictionary)) =
document.objects.get_mut(&dictionary_handle)
{
dictionary
.entries
.retain(|(name, _)| !name.eq_ignore_ascii_case("ACAD_BREAKDATA"));
dictionary.add_entry("ACAD_BREAKDATA", object_handle);
dictionary.set_entry_hard_owner("ACAD_BREAKDATA", true);
}
true
}
fn break_reference(
identifier: i32,
reference_type: i32,
first: glam::DVec3,
second: glam::DVec3,
) -> acadrust::objects::BreakPointReference {
acadrust::objects::BreakPointReference {
version: 0,
reserved: 0,
reference_type,
flags: 0,
identifier,
first_point: acadrust::types::Vector3::new(first.x, first.y, first.z),
second_point: acadrust::types::Vector3::new(second.x, second.y, second.z),
trailing_version: 0,
}
}
fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakResult {
let Some((requested, operation)) = parse_dim_break(encoded) else {
return DimBreakResult {
changed: Vec::new(),
message: crate::t!("DIMBREAK: invalid operation data.").into_owned(),
};
};
let dimensions: Vec<_> = requested
.into_iter()
.filter(|handle| {
matches!(
scene.document.get_entity(*handle),
Some(acadrust::EntityType::Dimension(_))
) && scene.locked_layer_name(*handle).is_none()
})
.collect();
if dimensions.is_empty() {
return DimBreakResult {
changed: Vec::new(),
message: crate::t!("DIMBREAK: no editable dimensions were selected.").into_owned(),
};
}
if matches!(operation, DimBreakOperation::Remove) {
let changed: Vec<_> = dimensions
.into_iter()
.filter(|handle| remove_dimension_break_data(&mut scene.document, *handle))
.collect();
return DimBreakResult {
message: crate::tf!("DIMBREAK: removed breaks from {} dimension(s).", changed.len())
.into_owned(),
changed,
};
}
let crossing_handles: Vec<Handle> = match operation {
DimBreakOperation::Object(handle) => vec![handle],
DimBreakOperation::Auto => scene
.document
.entities()
.map(|entity| entity.common().handle)
.filter(|handle| {
!dimensions.contains(handle) && scene.entity_belongs_to_active_space(*handle)
})
.collect(),
DimBreakOperation::Manual(_, _) | DimBreakOperation::Remove => Vec::new(),
};
let crossing_segments: Vec<_> = crossing_handles
.iter()
.flat_map(|handle| wire_segments(&scene.wire_models_for(&[*handle])))
.collect();
let mut pending = Vec::new();
for dimension in &dimensions {
let existing: Vec<_> = break_object_handle(&scene.document, *dimension)
.and_then(|handle| scene.document.objects.get(&handle))
.and_then(|object| match object {
acadrust::objects::ObjectType::DataObject(object) => match &object.data {
acadrust::objects::DataObjectData::BreakData(data) => {
Some(data.point_references.clone())
}
_ => None,
},
_ => None,
})
.unwrap_or_default();
let mut references = if matches!(operation, DimBreakOperation::Manual(_, _)) {
existing
} else {
existing
.into_iter()
.filter(|reference| reference.reference_type == 2)
.collect()
};
match operation {
DimBreakOperation::Manual(first, second) => {
references.push(break_reference(references.len() as i32, 2, first, second));
}
DimBreakOperation::Auto | DimBreakOperation::Object(_) => {
let dimension_segments = wire_segments(&scene.wire_models_for(&[*dimension]));
let mut intersections = Vec::new();
for dim_segment in &dimension_segments {
for crossing_segment in &crossing_segments {
if let Some(points) =
segment_intersection_xy(*dim_segment, *crossing_segment)
{
let center = (points.0 + points.1) * 0.5;
if intersections.iter().all(
|(first, second): &(glam::DVec3, glam::DVec3)| {
center.distance_squared((*first + *second) * 0.5) > 1.0e-8
},
) {
intersections.push(points);
}
}
}
}
let start = references.len() as i32;
references.extend(intersections.into_iter().enumerate().map(
|(index, points)| {
break_reference(start + index as i32, 1, points.0, points.1)
},
));
}
DimBreakOperation::Remove => {}
}
pending.push((*dimension, references));
}
let reserved = match operation {
DimBreakOperation::Object(handle) => handle,
_ => Handle::NULL,
};
let changed: Vec<_> = pending
.into_iter()
.filter_map(|(dimension, references)| {
write_dimension_break_data(&mut scene.document, dimension, reserved, references)
.then_some(dimension)
})
.collect();
DimBreakResult {
message: if changed.is_empty() {
crate::t!("DIMBREAK: no intersections were found.").into_owned()
} else {
crate::tf!("DIMBREAK: updated {} dimension(s).", changed.len()).into_owned()
},
changed,
}
}
/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
// Format: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
@@ -6188,7 +6732,11 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
.split(';')
.filter_map(|s| s.parse::<u64>().ok())
.collect();
let spacing: f64 = parts[2].parse().unwrap_or(0.0);
let requested_spacing = if parts[2].eq_ignore_ascii_case("AUTO") {
None
} else {
parts[2].parse::<f64>().ok()
};
use acadrust::entities::Dimension;
let base_h = acadrust::Handle::from(base_val);
@@ -6196,25 +6744,27 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
// dim line's perpendicular coordinate. Spacing steps each parallel dim along
// this perp IN THE DRAWING PLANE — offsetting Z had no effect on the dim
// line, which is computed from def·perp with perp.z = 0. (#181 / DIM-021)
let (perp, base_coord) = match scene.document.get_entity(base_h) {
Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => {
let (perp, base_coord, auto_spacing) = match scene.document.get_entity(base_h) {
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Linear(d))) => {
let (s, c) = d.rotation.sin_cos();
let perp = (-s, c);
let dp = d.definition_point;
(perp, dp.x * perp.0 + dp.y * perp.1)
let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier());
(perp, dp.x * perp.0 + dp.y * perp.1, spacing)
}
Some(acadrust::EntityType::Dimension(Dimension::Aligned(d))) => {
Some(acadrust::EntityType::Dimension(dimension @ Dimension::Aligned(d))) => {
let dx = d.second_point.x - d.first_point.x;
let dy = d.second_point.y - d.first_point.y;
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
let perp = (-dy / len, dx / len);
let dp = d.definition_point;
(perp, dp.x * perp.0 + dp.y * perp.1)
let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier());
(perp, dp.x * perp.0 + dp.y * perp.1, spacing)
}
_ => return,
};
let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
let effective_spacing = requested_spacing.unwrap_or(auto_spacing);
let mut changes = Vec::new();
for (idx, &hv) in other_vals.iter().enumerate() {
let h = acadrust::Handle::from(hv);
@@ -6256,6 +6806,28 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
}
}
fn dimension_auto_spacing(
document: &acadrust::CadDocument,
dimension: &acadrust::entities::Dimension,
annotation_multiplier: f64,
) -> f64 {
let style = document
.dim_styles
.iter()
.find(|style| style.name.eq_ignore_ascii_case(&dimension.base().style_name))
.map(|style| crate::entities::dimension::resolved_dimension_style(style, dimension, document));
style
.map(|style| {
let scale = if style.dimscale > 1.0e-9 {
style.dimscale
} else {
annotation_multiplier.max(1.0e-9)
};
(style.dimdli.abs() * scale).max(1.0e-6)
})
.unwrap_or(3.75)
}
fn apply_mleader_align(
scene: &mut crate::scene::Scene,
handles: &[acadrust::Handle],
+151 -27
View File
@@ -1041,55 +1041,110 @@ impl OpenCADStudio {
return Some(Task::done(Message::PlotDialogOpen));
}
// ── Recognized commands whose full implementation is pending ─────────
// These verbs are surfaced by the ribbon / menus but their feature is
// still being built. Acknowledge them with an honest status so the
// button responds instead of reporting an unknown command; each is
// replaced by its real handler as the feature lands.
// OBJECTSCALE ADD — add the active scale representation to every
// selected object that supports per-scale context data.
"OBJECTSCALE ADD" => {
let handles: Vec<acadrust::Handle> = self.tabs[i]
// Add or remove the current representation on the selected
// annotative objects. With no preselection, gather one through the
// regular selection engine before applying the action.
action @ ("OBJECTSCALE ADD" | "OBJECTSCALE DELETE") => {
let selected: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
if selected.is_empty() {
let action = if action.ends_with("ADD") {
"OBJECTSCALE ADD"
} else {
"OBJECTSCALE DELETE"
};
let command = crate::modules::annotate::annotation_scale::AnnotationScaleSelectionCommand::new(
"OBJECTSCALE",
action,
);
self.command_line
.push_error(crate::t!("OBJECTSCALE: select objects first.").as_ref());
.push_info(&crate::command::CadCommand::prompt(&command));
self.tabs[i].active_cmd = Some(Box::new(command));
return Some(Task::none());
}
self.push_undo_snapshot(i, "OBJECTSCALE");
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
self.command_line
.push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
return Some(Task::none());
};
let mut n = 0usize;
for h in &handles {
if crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
*h,
scale,
) {
crate::scene::annotative::set_entity_annotative(
&mut self.tabs[i].scene.document,
*h,
true,
let adding = action.ends_with("ADD");
let handles: Vec<_> = selected
.into_iter()
.filter(|handle| {
let memberships = crate::scene::annotative::object_scale_memberships(
&self.tabs[i].scene.document,
*handle,
);
n += 1;
let member = memberships.iter().any(|(_, current)| *current == scale);
if adding {
!member
&& self.tabs[i]
.scene
.document
.get_entity(*handle)
.is_some_and(crate::scene::annotative::supports_annotation_context)
} else {
member
}
})
.collect();
if handles.is_empty() {
self.command_line.push_info(
if adding {
crate::t!("OBJECTSCALE: the selected objects already have the current scale, or do not support annotation scales.")
} else {
crate::t!("OBJECTSCALE: the selected objects do not have the current scale.")
}
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "OBJECTSCALE");
let mut changed = Vec::new();
for handle in handles {
let ok = if adding {
let created = crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
handle,
scale,
);
if created {
crate::scene::annotative::set_entity_annotative(
&mut self.tabs[i].scene.document,
handle,
true,
);
}
created
} else {
crate::scene::annotative::remove_annotation_context_for_scale(
&mut self.tabs[i].scene.document,
handle,
scale,
)
};
if ok {
changed.push(handle);
}
}
let changes: Vec<_> = handles
.into_iter()
let changes: Vec<_> = changed
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"OBJECTSCALE: added the active scale to {n} object(s)."
"OBJECTSCALE: {} the current scale on {} object(s).",
if adding { "added" } else { "removed" },
changed.len()
).as_ref());
return Some(Task::none());
}
@@ -1459,6 +1514,75 @@ impl OpenCADStudio {
return Some(Task::done(Message::AnnoObjectScaleOpen));
}
// Reset every alternate scale representation to the position of
// the representation visible at the current annotation scale.
"ANNORESET" => {
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(handle, _)| *handle)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
let command = crate::modules::annotate::annotation_scale::AnnotationScaleSelectionCommand::new(
"ANNORESET",
"ANNORESET",
);
self.command_line
.push_info(&crate::command::CadCommand::prompt(&command));
self.tabs[i].active_cmd = Some(Box::new(command));
return Some(Task::none());
}
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
let candidates: Vec<_> = handles
.into_iter()
.filter(|handle| {
crate::scene::annotative::object_scale_memberships(
&self.tabs[i].scene.document,
*handle,
)
.len()
> 1
})
.collect();
if candidates.is_empty() {
self.command_line.push_info(
crate::t!("ANNORESET: no selected object has alternate scale representations.")
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "ANNORESET");
let mut changed = Vec::new();
for handle in candidates {
if crate::scene::annotative::reset_annotation_context_positions(
&mut self.tabs[i].scene.document,
handle,
scale,
) {
changed.push(handle);
}
}
if !changed.is_empty() {
let changes: Vec<_> = changed
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
}
self.command_line.push_output(
crate::tf!(
"ANNORESET: synchronized {} object(s) from the current scale.",
changed.len()
)
.as_ref(),
);
return Some(Task::none());
}
// DATALINK <path.csv> — create a persistent linked table.
"DATALINK" => {
use crate::command::ValuePromptCommand;
+1
View File
@@ -400,6 +400,7 @@ inventory::submit!(crate::command::CommandRegistration {
"ANNOUPDATE",
"SCALELISTEDIT",
"OBJECTSCALE",
"ANNORESET",
// Import CSV into a table + LandXML survey points.
"DATALINK",
"DATALINKUPDATE",
+15
View File
@@ -1314,6 +1314,14 @@ impl Default for LoftOptions {
}
}
#[derive(Clone, Debug)]
pub enum DimensionEditOperation {
Home,
NewText(String),
Rotate(f64),
Oblique(f64),
}
/// Returned by every `CadCommand` method to tell main.rs what to do.
#[allow(dead_code)]
pub enum CmdResult {
@@ -1412,6 +1420,8 @@ pub enum CmdResult {
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
/// Erase `handle` and replace with new entities; command stays active.
ReplaceEntity(Handle, Vec<EntityType>),
/// Update one entity in place, preserve its handle, and end the command.
UpdateEntityAndFinish { handle: Handle, entity: EntityType },
/// Replace / delete multiple entities and add new ones; command ends.
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
@@ -1592,6 +1602,11 @@ pub enum CmdResult {
QuickPrint(Vec<Handle>),
/// Replace the text content of a Text/MText entity in-place.
DdeditEntity { handle: Handle, new_text: String },
/// Apply one DIMEDIT operation to every selected dimension.
EditDimensions {
handles: Vec<Handle>,
operation: DimensionEditOperation,
},
/// Open the in-place editor (plain box or rich MText editor, per type) for
/// a text-bearing entity picked by a command such as DDEDIT.
EditTextEntity { handle: Handle },
+100 -1
View File
@@ -3450,6 +3450,93 @@ fn apply_dimension_breaks(
*lines = output;
}
fn dimension_jog_point(dimension: &Dimension) -> Option<Vec3> {
if !matches!(dimension, Dimension::Linear(_) | Dimension::Aligned(_)) {
return None;
}
dimension
.base()
.common
.extended_data
.get_record("ACAD_DSTYLE_DIMJAG_POSITION")
.and_then(|record| {
record.values.iter().rev().find_map(|value| match value {
acadrust::xdata::XDataValue::Point3D(point) => Some(vec3_local(*point)),
_ => None,
})
})
}
fn apply_dimension_jog(lines: &mut Vec<[f32; 3]>, requested: Vec3, size: f32, angle: f32) {
if lines.len() < 2 {
return;
}
let mut best: Option<(usize, Vec3, f32)> = None;
for (index, segment) in lines.windows(2).enumerate() {
if segment[0][0].is_nan() || segment[1][0].is_nan() {
continue;
}
let first = Vec3::from_array(segment[0]);
let second = Vec3::from_array(segment[1]);
let delta = second - first;
let length_squared = delta.length_squared();
if length_squared <= 1.0e-12 {
continue;
}
let parameter = ((requested - first).dot(delta) / length_squared).clamp(0.0, 1.0);
let point = first + delta * parameter;
let distance = point.distance_squared(requested);
if best.is_none_or(|(_, _, current)| distance < current) {
best = Some((index, point, distance));
}
}
let Some((target, center, _)) = best else {
return;
};
let first = Vec3::from_array(lines[target]);
let second = Vec3::from_array(lines[target + 1]);
let Some(direction) = (second - first).try_normalize() else {
return;
};
let perpendicular = Vec3::new(-direction.y, direction.x, 0.0);
let half = size.max(1.0e-3).min(first.distance(second) * 0.2);
let amplitude = (half * (angle * 0.5).tan().abs()).clamp(half * 0.35, half * 1.5);
let jog_start = center - direction * half;
let jog_end = center + direction * half;
let jog = [
jog_start,
center - direction * half * 0.25 + perpendicular * amplitude,
center + direction * half * 0.25 - perpendicular * amplitude,
jog_end,
];
let mut output = Vec::with_capacity(lines.len() + 4);
output.extend_from_slice(&lines[..target]);
if !output.is_empty() && !output.last().is_some_and(|point| point[0].is_nan()) {
output.push([f32::NAN; 3]);
}
if first.distance_squared(jog_start) > 1.0e-12 {
output.push(first.to_array());
output.push(jog_start.to_array());
output.push([f32::NAN; 3]);
}
output.extend(jog.into_iter().map(|point| point.to_array()));
if jog_end.distance_squared(second) > 1.0e-12 {
output.push([f32::NAN; 3]);
output.push(jog_end.to_array());
output.push(second.to_array());
}
if target + 2 < lines.len() {
if !output.last().is_some_and(|point| point[0].is_nan())
&& !lines[target + 2][0].is_nan()
{
output.push([f32::NAN; 3]);
}
output.extend_from_slice(&lines[target + 2..]);
}
*lines = output;
}
pub trait DimensionTess {
fn tessellate(
&self,
@@ -3783,12 +3870,19 @@ fn tessellate_dimension_inner(
// DIMUPT governs interactive creation-time text placement; saved
// geometry already carries the resulting position.
let _ = s.dimupt;
let _ = (s.dimarcsym, s.dimjogang);
let _ = s.dimarcsym;
// DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT
// supersedes it. Read but not honoured.
let _ = s.dimunit;
}
if let Some(point) = dimension_jog_point(dim) {
let jog_angle = style
.map(|style| style.dimjogang as f32)
.unwrap_or(std::f32::consts::FRAC_PI_4);
apply_dimension_jog(&mut geom.dim_lines, point, dim_txt as f32 * 0.6, jog_angle);
}
apply_dimension_breaks(document, handle, &mut geom.dim_lines);
apply_dimension_breaks(document, handle, &mut geom.ext_lines);
// Dimension entity fields that the render path doesn't yet use but are
// preserved on save:
// - base.insertion_point: legacy anchor reference; render uses
@@ -7229,6 +7323,11 @@ pub(crate) fn baked_large_radial_geometry(
dimension.base().common.handle,
&mut geometry.dim_lines,
);
apply_dimension_breaks(
document,
dimension.base().common.handle,
&mut geometry.ext_lines,
);
Some(geometry)
}
+15 -2
View File
@@ -6,7 +6,7 @@ use acadrust::EntityType;
use glam::DVec3;
use crate::command::{
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -164,11 +164,24 @@ impl CadCommand for AlignedDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
fn options(&self) -> Vec<CmdOption> {
if !matches!(self.step, Step::DimLine { .. }) || self.awaiting_text || self.awaiting_angle {
return Vec::new();
}
vec![
CmdOption::new("MText", "MTEXT"),
CmdOption::new("Text", "TEXT"),
CmdOption::new("Angle", "ANGLE"),
]
}
fn point_step_accepts_keywords(&self) -> bool {
// While entering the override text or angle it is a value, not a point
// step.
+3 -1
View File
@@ -489,8 +489,10 @@ impl CadCommand for AngularDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle || self.awaiting_quadrant {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+50
View File
@@ -0,0 +1,50 @@
use acadrust::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
/// Selection front-end used by the annotation-scale ribbon actions. The
/// action itself runs only after the normal selection engine completes, so
/// window/crossing selection, removal, Escape and Previous all keep their
/// usual behavior.
pub struct AnnotationScaleSelectionCommand {
name: &'static str,
action: &'static str,
}
impl AnnotationScaleSelectionCommand {
pub fn new(name: &'static str, action: &'static str) -> Self {
Self { name, action }
}
}
impl CadCommand for AnnotationScaleSelectionCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
crate::tf!("{} Select annotative objects:", self.name).into_owned()
}
fn is_selection_gathering(&self) -> bool {
true
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
if handles.is_empty() {
CmdResult::Cancel
} else {
CmdResult::Relaunch(self.action.to_string(), handles)
}
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+3 -1
View File
@@ -353,8 +353,10 @@ impl CadCommand for ArcLengthDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+3 -1
View File
@@ -139,8 +139,10 @@ impl CadCommand for DiameterDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+165 -43
View File
@@ -1,18 +1,9 @@
// DIMBREAK command — insert a break in a dimension/extension line where
// another object crosses it.
//
// Simplified workflow:
// 1. Select the dimension to break
// 2. Select the object that crosses it (or Enter for AUTO mode on all crossings)
//
// Because breaking a dimension line requires deep geometric intersection logic
// that depends on the renderer, this implementation records the target handles
// and emits a ReplaceEntity sentinel that commands.rs intercepts to apply the break.
// DIMBREAK — create automatic, object-driven or manual gaps in dimensions.
use acadrust::Handle;
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::t;
@@ -28,20 +19,54 @@ pub fn tool() -> ToolDef {
}
enum Step {
PickDim,
PickCrossing { dim_handle: Handle },
PickDimension,
PickDimensions(Vec<Handle>),
PickCrossing(Vec<Handle>),
ManualFirst(Vec<Handle>),
ManualSecond(Vec<Handle>, DVec3),
}
pub struct DimBreakCommand {
step: Step,
picked_entity: Option<EntityType>,
}
impl DimBreakCommand {
pub fn new() -> Self {
Self {
step: Step::PickDim,
step: Step::PickDimension,
picked_entity: None,
}
}
fn marker(layer: String, primary: Handle) -> CmdResult {
use acadrust::entities::XLine;
let mut marker = XLine::default();
marker.common.layer = layer;
CmdResult::ReplaceEntity(primary, vec![EntityType::XLine(marker)])
}
fn handles(handles: &[Handle]) -> String {
handles
.iter()
.map(|handle| handle.value().to_string())
.collect::<Vec<_>>()
.join(";")
}
fn automatic(handles: &[Handle]) -> CmdResult {
Self::marker(
format!("__DIMBREAK_AUTO__{}", Self::handles(handles)),
handles[0],
)
}
fn remove(handles: &[Handle]) -> CmdResult {
Self::marker(
format!("__DIMBREAK_REMOVE__{}", Self::handles(handles)),
handles[0],
)
}
}
impl CadCommand for DimBreakCommand {
@@ -51,54 +76,151 @@ impl CadCommand for DimBreakCommand {
fn prompt(&self) -> String {
match &self.step {
Step::PickDim => t!("DIMBREAK Select dimension to break:").into_owned(),
Step::PickCrossing { .. } => {
t!("DIMBREAK Select object to break at, or Enter for Auto:").into_owned()
Step::PickDimension => {
t!("DIMBREAK Select dimension to add/remove break or [Multiple]:").into_owned()
}
Step::PickDimensions(handles) => crate::tf!(
"DIMBREAK Select dimensions ({} selected, press Enter when done):",
handles.len()
)
.into_owned(),
Step::PickCrossing(_) => t!(
"DIMBREAK Select object to break dimension or [Auto/Manual/Remove] <Auto>:"
)
.into_owned(),
Step::ManualFirst(_) => t!("DIMBREAK Specify first break point:").into_owned(),
Step::ManualSecond(_, _) => {
t!("DIMBREAK Specify second break point:").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::PickDimension => vec![CmdOption::new("Multiple", "MULTIPLE")],
Step::PickCrossing(_) => vec![
CmdOption::new("Auto", "AUTO"),
CmdOption::new("Manual", "MANUAL"),
CmdOption::new("Remove", "REMOVE"),
],
_ => Vec::new(),
}
}
fn input_kind(&self) -> InputKind {
InputKind::Point
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::PickDimension | Step::PickCrossing(_))
}
fn needs_entity_pick(&self) -> bool {
true
matches!(
self.step,
Step::PickDimension | Step::PickDimensions(_) | Step::PickCrossing(_)
)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDimension | Step::PickDimensions(_))
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
match &self.step {
Step::PickDim => {
self.step = Step::PickCrossing { dim_handle: handle };
match &mut self.step {
Step::PickDimension => {
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
return CmdResult::ReportError(
t!("DIMBREAK: select a dimension.").into_owned(),
);
}
self.step = Step::PickCrossing(vec![handle]);
CmdResult::NeedPoint
}
Step::PickCrossing { dim_handle } => {
let dim_h = *dim_handle;
// Emit sentinel: the actual break logic is in commands.rs
use acadrust::entities::XLine;
let mut xl = XLine::default();
xl.common.layer = format!("__DIMBREAK__{},{}", dim_h.value(), handle.value());
CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)])
Step::PickDimensions(handles) => {
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
return CmdResult::ReportError(
t!("DIMBREAK: select a dimension.").into_owned(),
);
}
if !handles.contains(&handle) {
handles.push(handle);
}
CmdResult::NeedPoint
}
Step::PickCrossing(handles) => Self::marker(
format!(
"__DIMBREAK_OBJECT__{}|{}",
Self::handles(handles),
handle.value()
),
handles[0],
),
Step::ManualFirst(_) | Step::ManualSecond(_, _) => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
match &mut self.step {
Step::PickDimension if matches!(keyword.as_str(), "M" | "MULTIPLE") => {
self.step = Step::PickDimensions(Vec::new());
Some(CmdResult::NeedPoint)
}
Step::PickCrossing(handles) if matches!(keyword.as_str(), "A" | "AUTO") => {
Some(Self::automatic(handles))
}
Step::PickCrossing(handles) if matches!(keyword.as_str(), "M" | "MANUAL") => {
self.step = Step::ManualFirst(handles.clone());
Some(CmdResult::NeedPoint)
}
Step::PickCrossing(handles) if matches!(keyword.as_str(), "R" | "REMOVE") => {
Some(Self::remove(handles))
}
_ => None,
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match &self.step {
Step::ManualFirst(handles) => {
self.step = Step::ManualSecond(handles.clone(), point);
CmdResult::NeedPoint
}
Step::ManualSecond(handles, first) => Self::marker(
format!(
"__DIMBREAK_MANUAL__{}|{:.15},{:.15},{:.15}|{:.15},{:.15},{:.15}",
Self::handles(handles),
first.x,
first.y,
first.z,
point.x,
point.y,
point.z
),
handles[0],
),
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
// Auto mode: apply to all crossings
if let Step::PickCrossing { dim_handle } = &self.step {
let dim_h = *dim_handle;
use acadrust::entities::XLine;
let mut xl = XLine::default();
xl.common.layer = format!("__DIMBREAK_AUTO__{}", dim_h.value());
return CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)]);
match &self.step {
Step::PickDimensions(handles) if !handles.is_empty() => {
self.step = Step::PickCrossing(handles.clone());
CmdResult::NeedPoint
}
Step::PickCrossing(handles) => Self::automatic(handles),
_ => CmdResult::Cancel,
}
CmdResult::Cancel
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] }); // DimBreakCommand
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] });
+105 -43
View File
@@ -1,13 +1,11 @@
// DIMEDIT command — edit dimension text or reset it to the measured value.
//
// Workflow:
// 1. Pick a dimension entity
// 2. Enter new text (blank = reset to auto-measured value, "<>" = measured value placeholder)
// DIMEDIT — edit dimension text, rotation, extension-line obliquing or home position.
use acadrust::Handle;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionEditOperation, InputKind,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::t;
@@ -23,8 +21,11 @@ pub fn tool() -> ToolDef {
}
enum Step {
PickDim,
EnterText { handle: Handle },
Choose,
NewText,
Rotate,
Oblique,
Select(DimensionEditOperation),
}
pub struct DimEditCommand {
@@ -33,9 +34,12 @@ pub struct DimEditCommand {
impl DimEditCommand {
pub fn new() -> Self {
Self {
step: Step::PickDim,
}
Self { step: Step::Choose }
}
fn select(&mut self, operation: DimensionEditOperation) -> CmdResult {
self.step = Step::Select(operation);
CmdResult::NeedPoint
}
}
@@ -45,48 +49,106 @@ impl CadCommand for DimEditCommand {
}
fn prompt(&self) -> String {
match &self.step {
Step::PickDim => t!("DIMEDIT Select dimension:").into_owned(),
Step::EnterText { .. } => {
t!("DIMEDIT Enter text override (blank = reset to measured):").into_owned()
match self.step {
Step::Choose => {
t!("DIMEDIT Enter type of dimension editing [Home/New/Rotate/Oblique] <Home>:")
.into_owned()
}
Step::NewText => t!("DIMEDIT Enter new dimension text <measured value>:").into_owned(),
Step::Rotate => t!("DIMEDIT Specify angle for dimension text:").into_owned(),
Step::Oblique => {
t!("DIMEDIT Enter obliquing angle (press Enter for none):").into_owned()
}
Step::Select(_) => t!("DIMEDIT Select objects:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
if !matches!(self.step, Step::Choose) {
return Vec::new();
}
vec![
CmdOption::new("Home", "HOME"),
CmdOption::new("New", "NEW"),
CmdOption::new("Rotate", "ROTATE"),
CmdOption::new("Oblique", "OBLIQUE"),
]
}
fn input_kind(&self) -> InputKind {
match self.step {
Step::NewText => InputKind::FreeText,
Step::Select(_) => InputKind::Point,
Step::Choose | Step::Rotate | Step::Oblique => InputKind::SingleToken,
}
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, Step::Select(_))
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
let Step::Select(operation) = &self.step else {
return CmdResult::Cancel;
};
if handles.is_empty() {
CmdResult::Cancel
} else {
CmdResult::EditDimensions {
handles,
operation: operation.clone(),
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDim)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.step = Step::EnterText { handle };
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
matches!(self.step, Step::EnterText { .. })
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let handle = match &self.step {
Step::EnterText { handle } => *handle,
_ => return None,
};
// Empty = reset to auto-measured, "<>" = keep measured, else override
let new_text = text.trim().to_string();
Some(CmdResult::DdeditEntity { handle, new_text })
let value = text.trim();
let keyword = value.to_ascii_uppercase();
match self.step {
Step::Choose => match keyword.as_str() {
"H" | "HOME" => Some(self.select(DimensionEditOperation::Home)),
"N" | "NEW" => {
self.step = Step::NewText;
Some(CmdResult::NeedPoint)
}
"R" | "ROTATE" => {
self.step = Step::Rotate;
Some(CmdResult::NeedPoint)
}
"O" | "OBLIQUE" => {
self.step = Step::Oblique;
Some(CmdResult::NeedPoint)
}
_ => None,
},
Step::NewText => Some(self.select(DimensionEditOperation::NewText(
if value == "<>" { String::new() } else { text.to_string() },
))),
Step::Rotate => value
.parse::<f64>()
.ok()
.map(|degrees| self.select(DimensionEditOperation::Rotate(degrees))),
Step::Oblique => value
.parse::<f64>()
.ok()
.filter(|degrees| (-85.0..=85.0).contains(degrees))
.map(|degrees| self.select(DimensionEditOperation::Oblique(degrees))),
Step::Select(_) => None,
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match self.step {
Step::Choose => self.select(DimensionEditOperation::Home),
Step::NewText => self.select(DimensionEditOperation::NewText(String::new())),
Step::Oblique => self.select(DimensionEditOperation::Oblique(0.0)),
Step::Rotate | Step::Select(_) => CmdResult::Cancel,
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] }); // DimEditCommand
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] });
+169 -69
View File
@@ -1,13 +1,10 @@
// DIMJOGLINE command — add a jog (zigzag) symbol to a linear or aligned dimension.
//
// Workflow:
// 1. Pick the dimension
// 2. Click the position on the dimension line where the jog should appear
// DIMJOGLINE — add or remove a jog on a linear/aligned dimension.
use acadrust::Handle;
use glam::DVec3;
use acadrust::entities::Dimension;
use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3};
use crate::command::{CadCommand, CmdResult};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -30,12 +27,76 @@ enum Step {
pub struct DimJogLineCommand {
step: Step,
remove: bool,
picked_entity: Option<EntityType>,
dimension: Option<Dimension>,
}
impl DimJogLineCommand {
pub fn new() -> Self {
Self {
step: Step::PickDim,
remove: false,
picked_entity: None,
dimension: None,
}
}
fn result(handle: Handle, point: Option<DVec3>) -> CmdResult {
use acadrust::entities::XLine;
let mut marker = XLine::default();
marker.common.layer = match point {
Some(point) => format!(
"__DIMJOG__{}|{:.15},{:.15},{:.15}",
handle.value(), point.x, point.y, point.z
),
None => format!("__DIMJOG_REMOVE__{}", handle.value()),
};
CmdResult::ReplaceEntity(handle, vec![EntityType::XLine(marker)])
}
fn default_position(dimension: &Dimension) -> Option<DVec3> {
let (first, second, definition, axis) = match dimension {
Dimension::Linear(value) => (
value.first_point,
value.second_point,
value.definition_point,
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
),
Dimension::Aligned(value) => {
let first = DVec3::new(value.first_point.x, value.first_point.y, value.first_point.z);
let second = DVec3::new(value.second_point.x, value.second_point.y, value.second_point.z);
(
value.first_point,
value.second_point,
value.definition_point,
(second - first).try_normalize().unwrap_or(DVec3::X),
)
}
_ => return None,
};
let first = DVec3::new(first.x, first.y, first.z);
let second = DVec3::new(second.x, second.y, second.z);
let definition = DVec3::new(definition.x, definition.y, definition.z);
let axis = axis.try_normalize().unwrap_or(DVec3::X);
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
let offset = definition.dot(perpendicular);
let first_on_line = first + perpendicular * (offset - first.dot(perpendicular));
let second_on_line = second + perpendicular * (offset - second.dot(perpendicular));
let midpoint = (first_on_line + second_on_line) * 0.5;
let text = dimension.base().text_middle_point;
let text = DVec3::new(text.x, text.y, text.z);
let line = second_on_line - first_on_line;
let length_squared = line.length_squared();
if length_squared <= 1.0e-18 {
return Some(midpoint);
}
let parameter = (text - first_on_line).dot(line) / length_squared;
if text.is_finite() && (0.0..=1.0).contains(&parameter) {
Some((first_on_line + text) * 0.5)
} else {
Some(midpoint)
}
}
}
@@ -46,90 +107,129 @@ impl CadCommand for DimJogLineCommand {
}
fn prompt(&self) -> String {
match &self.step {
Step::PickDim => t!("DIMJOGLINE Select linear or aligned dimension:").into_owned(),
Step::PickJogPos { .. } => t!("DIMJOGLINE Specify jog location:").into_owned(),
match self.step {
Step::PickDim if self.remove => {
t!("DIMJOGLINE Select dimension to remove jog:").into_owned()
}
Step::PickDim => {
t!("DIMJOGLINE Select dimension to add jog or [Remove]:").into_owned()
}
Step::PickJogPos { .. } => {
t!("DIMJOGLINE Specify jog location (or press Enter):").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
if matches!(self.step, Step::PickDim) && !self.remove {
vec![CmdOption::new("Remove", "REMOVE")]
} else {
Vec::new()
}
}
fn input_kind(&self) -> InputKind {
InputKind::Point
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::PickDim) && !self.remove
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDim)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDim)
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
let Some(EntityType::Dimension(dimension @ (Dimension::Linear(_) | Dimension::Aligned(_)))) =
self.picked_entity.take()
else {
return CmdResult::ReportError(
t!("DIMJOGLINE: select a linear or aligned dimension.").into_owned(),
);
};
if handle.is_null() {
return CmdResult::NeedPoint;
}
if self.remove {
return Self::result(handle, None);
}
self.dimension = Some(dimension);
self.step = Step::PickJogPos { handle };
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PickJogPos { handle } = &self.step {
let h = *handle;
// Emit sentinel for commands.rs to store the jog position
use acadrust::entities::XLine;
let mut xl = XLine::default();
xl.common.layer = format!("__DIMJOG__{},{:.6},{:.6}", h.value(), pt.x, pt.z);
return CmdResult::ReplaceEntity(h, vec![acadrust::EntityType::XLine(xl)]);
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if matches!(self.step, Step::PickDim)
&& matches!(text.trim().to_ascii_uppercase().as_str(), "R" | "REMOVE")
{
self.remove = true;
Some(CmdResult::NeedPoint)
} else {
None
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::PickJogPos { handle } => Self::result(handle, Some(point)),
Step::PickDim => CmdResult::NeedPoint,
}
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match (&self.step, &self.dimension) {
(Step::PickJogPos { handle }, Some(dimension)) => Self::default_position(dimension)
.map(|point| Self::result(*handle, Some(point)))
.unwrap_or(CmdResult::Cancel),
_ => CmdResult::Cancel,
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
if !matches!(self.step, Step::PickJogPos { .. }) {
return None;
}
let d = 0.3_f32;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "dimjog_preview".into(),
// Jog zigzag in the XY drawing plane (Z is elevation, ~0). The old
// marker varied Z, so in the top-down view it collapsed to a flat
// line instead of a zigzag. (#150)
points: vec![
[pt.x - d, pt.y, pt.z],
[pt.x - d * 0.3, pt.y + d, pt.z],
[pt.x + d * 0.3, pt.y - d, pt.z],
[pt.x + d, pt.y, pt.z],
],
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.2,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
let axis = self
.dimension
.as_ref()
.and_then(|dimension| match dimension {
Dimension::Linear(value) => {
Some(Vec3::new(value.rotation.cos() as f32, value.rotation.sin() as f32, 0.0))
}
Dimension::Aligned(value) => Some(Vec3::new(
(value.second_point.x - value.first_point.x) as f32,
(value.second_point.y - value.first_point.y) as f32,
0.0,
)),
_ => None,
})
.and_then(Vec3::try_normalize)
.unwrap_or(Vec3::X);
let perpendicular = Vec3::new(-axis.y, axis.x, 0.0);
let center = point.as_vec3();
let size = 0.3;
let points = vec![
center - axis * size,
center - axis * size * 0.25 + perpendicular * size,
center + axis * size * 0.25 - perpendicular * size,
center + axis * size,
];
let mut preview = WireModel::default();
preview.name = "dimjog_preview".into();
preview.points = points.into_iter().map(|point| point.to_array()).collect();
preview.color = WireModel::CYAN;
preview.line_weight_px = 1.2;
Some(preview)
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] }); // DimJogLineCommand
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] });
+106 -22
View File
@@ -3,12 +3,12 @@
// Workflow:
// 1. Select the base dimension
// 2. Select the other dimensions to space (click each, Enter to finish)
// 3. Enter spacing value (or 0 for automatic equal spacing)
// 3. Enter a non-negative spacing value, or choose Auto
use acadrust::Handle;
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, SelectionEntity};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::t;
@@ -31,12 +31,14 @@ enum Step {
pub struct DimSpaceCommand {
step: Step,
picked_entity: Option<EntityType>,
}
impl DimSpaceCommand {
pub fn new() -> Self {
Self {
step: Step::PickBase,
picked_entity: None,
}
}
}
@@ -54,12 +56,66 @@ impl CadCommand for DimSpaceCommand {
count = others.len()
)
.into_owned(),
Step::EnterSpacing { .. } => t!("DIMSPACE Enter value (0 = auto):").into_owned(),
Step::EnterSpacing { .. } => {
t!("DIMSPACE Specify distance between dimensions or [Auto] <Auto>:")
.into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::EnterSpacing { .. })
.then(|| vec![CmdOption::new("Auto", "AUTO")])
.unwrap_or_default()
}
fn input_kind(&self) -> InputKind {
if matches!(self.step, Step::EnterSpacing { .. }) {
InputKind::SingleToken
} else {
InputKind::Point
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::PickBase | Step::PickOthers { .. })
matches!(self.step, Step::PickBase)
}
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickBase)
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, Step::PickOthers { .. })
}
fn selection_entities_exclude_locked(&self) -> bool {
true
}
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
let Step::PickOthers { base, others } = &mut self.step else {
return;
};
others.clear();
for selected in entities {
if selected.handle != *base
&& matches!(
selected.entity,
EntityType::Dimension(
acadrust::entities::Dimension::Linear(_)
| acadrust::entities::Dimension::Aligned(_)
)
)
&& !others.contains(&selected.handle)
{
others.push(selected.handle);
}
}
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
@@ -68,29 +124,60 @@ impl CadCommand for DimSpaceCommand {
}
match &mut self.step {
Step::PickBase => {
if !matches!(
self.picked_entity.take(),
Some(EntityType::Dimension(
acadrust::entities::Dimension::Linear(_)
| acadrust::entities::Dimension::Aligned(_)
))
) {
return CmdResult::ReportError(
t!("DIMSPACE: select a linear or aligned dimension.").into_owned(),
);
}
self.step = Step::PickOthers {
base: handle,
others: vec![],
};
CmdResult::NeedPoint
}
Step::PickOthers { others, .. } => {
if !others.contains(&handle) {
others.push(handle);
}
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn wants_text_input(&self) -> bool {
matches!(self.step, Step::EnterSpacing { .. })
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
let Step::PickOthers { base, others } = &self.step else {
return CmdResult::Cancel;
};
if others.is_empty() {
return CmdResult::ReportError(
t!("DIMSPACE: select one or more linear or aligned dimensions.").into_owned(),
);
}
let base = *base;
let others = others.clone();
self.step = Step::EnterSpacing { base, others };
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let Step::EnterSpacing { base, others } = &self.step {
let spacing: f64 = text.trim().parse().unwrap_or(0.0);
let token = text.trim();
let spacing = if matches!(token.to_ascii_uppercase().as_str(), "A" | "AUTO") {
"AUTO".to_string()
} else {
let Ok(value) = token.parse::<f64>() else {
return Some(CmdResult::ReportError(
t!("DIMSPACE: enter a non-negative spacing value or Auto.").into_owned(),
));
};
if value < 0.0 {
return Some(CmdResult::ReportError(
t!("DIMSPACE: spacing value cannot be negative.").into_owned(),
));
}
value.to_string()
};
let b = *base;
let o = others.clone();
// Emit sentinel for commands.rs to handle
@@ -116,15 +203,12 @@ impl CadCommand for DimSpaceCommand {
}
fn on_enter(&mut self) -> CmdResult {
match &self.step {
Step::PickOthers { base, others } if !others.is_empty() => {
let b = *base;
let o = others.clone();
self.step = Step::EnterSpacing { base: b, others: o };
CmdResult::NeedPoint
}
_ => CmdResult::Cancel,
if matches!(self.step, Step::EnterSpacing { .. }) {
return self
.on_text_input("AUTO")
.unwrap_or(CmdResult::Cancel);
}
CmdResult::Cancel
}
}
+210 -69
View File
@@ -1,13 +1,10 @@
// DIMTEDIT command — reposition the text of an existing dimension.
//
// Workflow:
// 1. Pick a dimension entity
// 2. Click the new text position (entity data is injected by update.rs after pick)
// DIMTEDIT — reposition, justify, home or rotate existing dimension text.
use acadrust::entities::{AttachmentPointType, Dimension};
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -29,18 +26,47 @@ enum Step {
handle: Handle,
entity: Option<EntityType>,
},
EnterAngle {
handle: Handle,
entity: Option<EntityType>,
},
}
pub struct DimTeditCommand {
step: Step,
picked_entity: Option<EntityType>,
}
#[derive(Clone, Copy)]
enum Placement {
Left,
Right,
Center,
Home,
}
impl DimTeditCommand {
pub fn new() -> Self {
Self {
step: Step::PickDim,
picked_entity: None,
}
}
fn finish_placement(
handle: Handle,
entity: &mut Option<EntityType>,
placement: Placement,
) -> CmdResult {
let Some(mut entity) = entity.take() else {
return CmdResult::NeedPoint;
};
let EntityType::Dimension(dimension) = &mut entity else {
return CmdResult::Cancel;
};
apply_placement(dimension, placement);
CmdResult::UpdateEntityAndFinish { handle, entity }
}
}
impl CadCommand for DimTeditCommand {
@@ -49,47 +75,123 @@ impl CadCommand for DimTeditCommand {
}
fn prompt(&self) -> String {
match &self.step {
match self.step {
Step::PickDim => t!("DIMTEDIT Select dimension:").into_owned(),
Step::PickTextPos { .. } => {
t!("DIMTEDIT Specify new location for dimension text:").into_owned()
Step::PickTextPos { .. } => t!(
"DIMTEDIT Specify new location for dimension text or [Left/Right/Center/Home/Angle]:"
)
.into_owned(),
Step::EnterAngle { .. } => {
t!("DIMTEDIT Specify angle for dimension text:").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
if !matches!(self.step, Step::PickTextPos { .. }) {
return Vec::new();
}
vec![
CmdOption::new("Left", "LEFT"),
CmdOption::new("Right", "RIGHT"),
CmdOption::new("Center", "CENTER"),
CmdOption::new("Home", "HOME"),
CmdOption::new("Angle", "ANGLE"),
]
}
fn input_kind(&self) -> InputKind {
match self.step {
Step::EnterAngle { .. } => InputKind::SingleToken,
Step::PickDim | Step::PickTextPos { .. } => InputKind::Point,
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::PickTextPos { .. })
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDim)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickDim)
}
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
if !matches!(self.picked_entity.as_ref(), Some(EntityType::Dimension(_))) {
self.picked_entity = None;
return CmdResult::ReportError(
t!("DIMTEDIT: select a dimension.").into_owned(),
);
}
self.step = Step::PickTextPos {
handle,
entity: None,
entity: self.picked_entity.take(),
};
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PickTextPos { handle, entity } = &mut self.step {
let h = *handle;
if let Some(mut ent) = entity.take() {
if let EntityType::Dimension(ref mut d) = ent {
let new_pt = acadrust::types::Vector3::new(pt.x, pt.y, pt.z);
d.base_mut().text_middle_point = new_pt;
d.base_mut().insertion_point = new_pt;
// Pin the text to this location, else the renderer recomputes
// the style-default placement and the move is discarded.
d.base_mut().text_user_positioned = true;
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
match &mut self.step {
Step::PickTextPos { handle, entity } => match keyword.as_str() {
"L" | "LEFT" => Some(Self::finish_placement(*handle, entity, Placement::Left)),
"R" | "RIGHT" => {
Some(Self::finish_placement(*handle, entity, Placement::Right))
}
return CmdResult::ReplaceEntity(h, vec![ent]);
"C" | "CENTER" => {
Some(Self::finish_placement(*handle, entity, Placement::Center))
}
"H" | "HOME" => Some(Self::finish_placement(*handle, entity, Placement::Home)),
"A" | "ANGLE" => {
let handle = *handle;
let entity = entity.take();
self.step = Step::EnterAngle { handle, entity };
Some(CmdResult::NeedPoint)
}
_ => None,
},
Step::EnterAngle { handle, entity } => {
let degrees = text.trim().parse::<f64>().ok()?;
let Some(mut entity) = entity.take() else {
return Some(CmdResult::NeedPoint);
};
let EntityType::Dimension(dimension) = &mut entity else {
return Some(CmdResult::Cancel);
};
dimension.base_mut().text_rotation = degrees.to_radians();
Some(CmdResult::UpdateEntityAndFinish {
handle: *handle,
entity,
})
}
// No entity yet — wait for inject
CmdResult::NeedPoint
} else {
CmdResult::NeedPoint
Step::PickDim => None,
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
let Step::PickTextPos { handle, entity } = &mut self.step else {
return CmdResult::NeedPoint;
};
let Some(mut entity) = entity.take() else {
return CmdResult::NeedPoint;
};
let EntityType::Dimension(dimension) = &mut entity else {
return CmdResult::Cancel;
};
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
let base = dimension.base_mut();
base.text_middle_point = point;
base.insertion_point = point;
base.text_user_positioned = true;
CmdResult::UpdateEntityAndFinish {
handle: *handle,
entity,
}
}
@@ -97,63 +199,102 @@ impl CadCommand for DimTeditCommand {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
if !matches!(self.step, Step::PickTextPos { .. }) {
return None;
}
let point = point.as_vec3();
let d = 0.2_f32;
Some(WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "dimtedit_preview".into(),
// Marker box in the XY drawing plane (Z is elevation, ~0). The old
// box varied Z, so in the top-down view it collapsed to a flat line
// instead of a square. (#150)
points: vec![
[pt.x - d, pt.y - d, pt.z],
[pt.x + d, pt.y - d, pt.z],
[pt.x + d, pt.y + d, pt.z],
[pt.x - d, pt.y + d, pt.z],
[pt.x - d, pt.y - d, pt.z],
[point.x - d, point.y - d, point.z],
[point.x + d, point.y - d, point.z],
[point.x + d, point.y + d, point.z],
[point.x - d, point.y + d, point.z],
[point.x - d, point.y - d, point.z],
],
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
..WireModel::default()
})
}
fn inject_picked_entity(&mut self, entity: EntityType) {
if let Step::PickTextPos { entity: slot, .. } = &mut self.step {
*slot = Some(entity);
match &mut self.step {
Step::PickTextPos { entity: slot, .. } | Step::EnterAngle { entity: slot, .. } => {
*slot = Some(entity);
}
Step::PickDim => self.picked_entity = Some(entity),
}
}
}
fn dimension_line_endpoints(dimension: &Dimension) -> Option<(DVec3, DVec3)> {
let (first, second, definition, axis) = match dimension {
Dimension::Linear(value) => (
value.first_point,
value.second_point,
value.definition_point,
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
),
Dimension::Aligned(value) => {
let delta = DVec3::new(
value.second_point.x - value.first_point.x,
value.second_point.y - value.first_point.y,
0.0,
);
if delta.length_squared() <= 1.0e-18 {
return None;
}
(
value.first_point,
value.second_point,
value.definition_point,
delta.normalize(),
)
}
_ => return None,
};
let first = DVec3::new(first.x, first.y, first.z);
let second = DVec3::new(second.x, second.y, second.z);
let definition = DVec3::new(definition.x, definition.y, definition.z);
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
let project = |point: DVec3| {
point + perpendicular * (definition - point).dot(perpendicular)
};
let first = project(first);
let second = project(second);
if first.dot(axis) <= second.dot(axis) {
Some((first, second))
} else {
Some((second, first))
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] }); // DimTeditCommand
fn apply_placement(dimension: &mut Dimension, placement: Placement) {
if matches!(placement, Placement::Home) {
let base = dimension.base_mut();
base.text_middle_point = acadrust::types::Vector3::ZERO;
base.insertion_point = acadrust::types::Vector3::ZERO;
base.text_user_positioned = false;
base.attachment_point = AttachmentPointType::MiddleCenter;
return;
}
let Some((left, right)) = dimension_line_endpoints(dimension) else {
return;
};
let (point, attachment) = match placement {
Placement::Left => (left, AttachmentPointType::MiddleLeft),
Placement::Right => (right, AttachmentPointType::MiddleRight),
Placement::Center => ((left + right) * 0.5, AttachmentPointType::MiddleCenter),
Placement::Home => unreachable!(),
};
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
let base = dimension.base_mut();
base.text_middle_point = point;
base.insertion_point = point;
base.text_user_positioned = true;
base.attachment_point = attachment;
}
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] });
+3 -1
View File
@@ -230,8 +230,10 @@ impl CadCommand for JoggedRadiusDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+99 -15
View File
@@ -10,12 +10,12 @@
// (DXF group 340); editing/erasing them stays in sync via that link.
use acadrust::entities::mtext::AttachmentPoint;
use acadrust::entities::{Leader, LeaderCreationType, MText};
use acadrust::entities::{Leader, LeaderCreationType, LeaderPathType, MText};
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::{DVec3, Mat4, Vec3};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -31,8 +31,17 @@ pub fn tool() -> ToolDef {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Step {
Points,
Format,
}
pub struct LeaderCommand {
verts: Vec<DVec3>,
step: Step,
path_type: LeaderPathType,
arrow_enabled: bool,
plane: WorkingPlane,
dimension_style: String,
text_style: String,
@@ -56,6 +65,9 @@ impl LeaderCommand {
Self {
verts: Vec::new(),
step: Step::Points,
path_type: LeaderPathType::StraightLine,
arrow_enabled: true,
plane: WorkingPlane::default(),
dimension_style: defaults.style_name,
text_style: defaults.text_style_name,
@@ -91,7 +103,7 @@ impl LeaderCommand {
let mut leader_points = local.clone();
let first = local[0];
let elbow = local[1];
let elbow = *local.last().unwrap_or(&first);
let sign = if elbow.x >= first.x { 1.0 } else { -1.0 };
let landing_end = DVec3::new(
@@ -102,7 +114,7 @@ impl LeaderCommand {
leader_points.push(landing_end);
let leader = build_leader(
let mut leader = build_leader(
&leader_points,
Mat4::IDENTITY,
&self.dimension_style,
@@ -110,6 +122,8 @@ impl LeaderCommand {
self.gap,
self.arrow_size,
);
leader.path_type = self.path_type;
leader.arrow_enabled = self.arrow_enabled;
// The MTEXT starts at the real end of the landing.
let (anchor, attach) =
@@ -150,25 +164,95 @@ impl CadCommand for LeaderCommand {
}
fn prompt(&self) -> String {
if self.verts.is_empty() {
t!("LEADER Specify arrowhead point:").into_owned()
if self.step == Step::Format {
return t!("LEADER Enter leader formatting option [Spline/Straight/Arrow/None] <exit>:")
.into_owned();
}
match self.verts.len() {
0 => t!("LEADER Specify leader start point:").into_owned(),
1 => t!("LEADER Specify next point:").into_owned(),
_ => t!("LEADER Specify next point or [Annotation/Format/Undo] <Annotation>:")
.into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
if self.step == Step::Format {
return vec![
CmdOption::new("Spline", "SPLINE"),
CmdOption::new("Straight", "STRAIGHT"),
CmdOption::new("Arrow", "ARROW"),
CmdOption::new("None", "NONE"),
];
}
if self.verts.len() >= 2 {
vec![
CmdOption::new("Annotation", "ANNOTATION"),
CmdOption::new("Format", "FORMAT"),
CmdOption::new("Undo", "UNDO"),
]
} else {
t!("LEADER Specify landing point:").into_owned()
Vec::new()
}
}
fn input_kind(&self) -> InputKind {
if self.step == Step::Format {
InputKind::SingleToken
} else {
InputKind::Point
}
}
fn point_step_accepts_keywords(&self) -> bool {
self.step == Step::Points && self.verts.len() >= 2
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
if self.step == Step::Format {
match keyword.as_str() {
"S" | "SPLINE" => self.path_type = LeaderPathType::Spline,
"ST" | "STRAIGHT" => self.path_type = LeaderPathType::StraightLine,
"A" | "ARROW" => self.arrow_enabled = true,
"N" | "NONE" => self.arrow_enabled = false,
_ => return None,
}
self.step = Step::Points;
return Some(CmdResult::NeedPoint);
}
if self.verts.len() < 2 {
return None;
}
match keyword.as_str() {
"A" | "ANNOTATION" => Some(self.finish()),
"F" | "FORMAT" => {
self.step = Step::Format;
Some(CmdResult::NeedPoint)
}
"U" | "UNDO" => {
self.verts.pop();
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.verts.push(pt);
if self.verts.len() >= 2 {
self.finish()
} else {
CmdResult::NeedPoint
if self.step != Step::Points {
return CmdResult::NeedPoint;
}
self.verts.push(pt);
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
self.finish()
if self.step == Step::Format {
self.step = Step::Points;
CmdResult::NeedPoint
} else {
self.finish()
}
}
fn on_escape(&mut self) -> CmdResult {
@@ -176,7 +260,7 @@ impl CadCommand for LeaderCommand {
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.verts.is_empty() {
if self.step != Step::Points || self.verts.is_empty() {
return None;
}
let mut pts: Vec<Vec3> = self
+22 -2
View File
@@ -6,7 +6,7 @@ use cadkernel::geom2d::{
};
use crate::command::{
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -224,11 +224,31 @@ impl CadCommand for LinearDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle || self.awaiting_rotation {
InputKind::SingleToken
} else {
InputKind::Point
}
}
fn options(&self) -> Vec<CmdOption> {
if !matches!(self.step, Step::DimensionLine { .. })
|| self.awaiting_text
|| self.awaiting_angle
|| self.awaiting_rotation
{
return Vec::new();
}
vec![
CmdOption::new("MText", "MTEXT"),
CmdOption::new("Text", "TEXT"),
CmdOption::new("Angle", "ANGLE"),
CmdOption::new("Horizontal", "HORIZONTAL"),
CmdOption::new("Vertical", "VERTICAL"),
CmdOption::new("Rotated", "ROTATED"),
]
}
fn point_step_accepts_keywords(&self) -> bool {
// While typing the override text or angle, route input as a value, not
// a point pick / keyword.
+2
View File
@@ -472,6 +472,8 @@ impl CadCommand for MLeaderCommand {
fn input_kind(&self) -> InputKind {
if matches!(self.step, Step::Layer | Step::PreEnterText) {
InputKind::FreeText
} else if matches!(self.step, Step::PickPoints | Step::SelectMText) {
InputKind::Point
} else {
InputKind::SingleToken
}
+36 -21
View File
@@ -1,6 +1,7 @@
// Annotate module — dimension, text, leader, table, and markup tools.
pub mod aligned_dim;
pub mod annotation_scale;
pub mod arc_length_dim;
pub mod angular_dim;
pub mod data_extract;
@@ -205,29 +206,33 @@ impl CadModule for AnnotateModule {
RibbonGroup {
title: "Annotation Scaling",
tools: vec![
RibbonItem::Tool(crate::modules::ToolDef {
id: "ANNOSCALE",
label: "Scale List",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/scale_list.svg"
)),
event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()),
}),
RibbonItem::Tool(crate::modules::ToolDef {
id: "OBJECTSCALE",
label: "Add Scale",
RibbonItem::LabeledDropdown {
id: "ANNOTATION_CURRENT_SCALE",
label: "Add Current Scale",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/add_scale.svg"
)),
// The quick "mark annotative at the current scale"
// action; bare OBJECTSCALE opens the manage dialog.
event: crate::modules::ModuleEvent::Command(
"OBJECTSCALE ADD".to_string(),
),
}),
items: vec![
(
"OBJECTSCALE ADD",
"Add Current Scale",
crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/add_scale.svg"
)),
),
(
"OBJECTSCALE DELETE",
"Delete Current Scale",
crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/add_scale.svg"
)),
),
],
default: "OBJECTSCALE ADD",
},
RibbonItem::Tool(crate::modules::ToolDef {
id: "SCALELISTEDIT",
label: "Scale Edit",
label: "Scale List",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/scale_list.svg"
)),
@@ -236,13 +241,23 @@ impl CadModule for AnnotateModule {
),
}),
RibbonItem::Tool(crate::modules::ToolDef {
id: "SYNCPVIEWPORTS",
label: "Sync Scales",
id: "OBJECTSCALE",
label: "Add/Delete Scales",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/add_scale.svg"
)),
event: crate::modules::ModuleEvent::Command(
"OBJECTSCALE".to_string(),
),
}),
RibbonItem::Tool(crate::modules::ToolDef {
id: "ANNORESET",
label: "Sync Scale Positions",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/sync.svg"
)),
event: crate::modules::ModuleEvent::Command(
"SYNCPVIEWPORTS".to_string(),
"ANNORESET".to_string(),
),
}),
],
+2
View File
@@ -246,6 +246,8 @@ impl CadCommand for MTextCommand {
fn input_kind(&self) -> InputKind {
if matches!(self.step, Step::Style) {
InputKind::FreeText
} else if matches!(self.step, Step::FirstCorner | Step::OppositeCorner) {
InputKind::Point
} else {
InputKind::SingleToken
}
+3 -1
View File
@@ -142,8 +142,10 @@ impl CadCommand for OrdinateDimCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+3 -1
View File
@@ -137,8 +137,10 @@ impl CadCommand for RadiusDimensionCommand {
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
} else {
} else if self.awaiting_angle {
InputKind::SingleToken
} else {
InputKind::Point
}
}
+12 -1
View File
@@ -9,7 +9,7 @@ use cadkernel::space::{PlanarCurve, Plane, Vec3};
use glam::DVec3;
use rustc_hash::FxHashMap;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
use crate::entities::curve::{curve_points, entity_curve, ocs_plane};
use crate::entities::lwpolyline::{is_revision_cloud, revision_cloud_from_curve};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -666,6 +666,17 @@ impl CadCommand for RevCloudCommand {
true
}
fn input_kind(&self) -> InputKind {
match self.stage {
Stage::ArcLength | Stage::Style | Stage::Reverse(_) => InputKind::SingleToken,
Stage::Create
| Stage::Object
| Stage::ModifySelect
| Stage::ModifyDraw(_)
| Stage::ModifyErase(_) => InputKind::Point,
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.stage, Stage::Create | Stage::ModifyDraw(_))
}
+10 -1
View File
@@ -8,7 +8,7 @@ use cadkernel::space::Plane;
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -270,6 +270,15 @@ impl CadCommand for WipeoutCommand {
)
}
fn input_kind(&self) -> InputKind {
match self.mode {
WipeoutMode::Frames | WipeoutMode::ErasePolyline => InputKind::SingleToken,
WipeoutMode::Draw | WipeoutMode::Polyline | WipeoutMode::Rectangular => {
InputKind::Point
}
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(
self.mode,
+38
View File
@@ -644,6 +644,44 @@ pub fn active_object_context_for_scale(
default.or(first)
}
/// Reset every alternate annotation-scale representation to the placement of
/// the representation displayed at `scale_handle`. The leaf identity, scale
/// handle and default marker stay intact; only the context payload is copied.
pub fn reset_annotation_context_positions(
doc: &mut CadDocument,
entity: Handle,
scale_handle: Option<Handle>,
) -> bool {
let Some(source) = active_object_context_for_scale(doc, entity, scale_handle) else {
return false;
};
let source_handle = source.handle;
let source_kind = source.kind.clone();
let targets: Vec<Handle> = annotation_scales_dict(doc, entity)
.and_then(|collection| as_dict(doc, collection))
.map(|collection| {
collection
.entries
.iter()
.map(|(_, handle)| *handle)
.filter(|handle| *handle != source_handle)
.collect()
})
.unwrap_or_default();
let mut changed = false;
for handle in targets {
let Some(ObjectType::ObjectContextData(context)) = doc.objects.get_mut(&handle) else {
continue;
};
if context.kind != source_kind {
context.kind = source_kind.clone();
changed = true;
}
}
changed
}
pub fn effective_annotation_scale_for(
doc: &CadDocument,
entity: &EntityType,