feat(dimension): complete linear workflow

This commit is contained in:
Hakan Seven 2026-08-24 23:42:49 +03:00
commit 959197bb3c
22 changed files with 2576 additions and 258 deletions

View file

@ -801,7 +801,21 @@ impl OpenCADStudio {
// the structure snapshot fallback.
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
self.commit_entity(entity);
let is_linear_dimension = matches!(
entity,
acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_))
);
let committed = self.commit_entity_handle(entity);
if is_linear_dimension {
if let Some(handle) = committed {
let sources = self.tabs[i]
.scene
.infer_linear_dimension_sources(handle);
self.tabs[i]
.scene
.attach_linear_dimension_association(handle, sources);
}
}
self.tabs[i].dirty = true;
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
@ -1191,7 +1205,21 @@ impl OpenCADStudio {
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
self.commit_entity(entity);
let is_linear_dimension = matches!(
entity,
acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_))
);
let committed = self.commit_entity_handle(entity);
if is_linear_dimension {
if let Some(handle) = committed {
let sources = self.tabs[i]
.scene
.infer_linear_dimension_sources(handle);
self.tabs[i]
.scene
.attach_linear_dimension_association(handle, sources);
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
@ -1201,6 +1229,27 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitAssociativeDimension { entity, source } => {
let label = self.history_label_from_active_cmd(i, "DIMLINEAR");
let pending = self.begin_undo(i, label, 1, false);
if let Some(handle) = self.commit_entity_handle(entity) {
self.tabs[i]
.scene
.attach_linear_dimension_association(handle, [Some(source), Some(source)]);
self.tabs[i].scene.bump_entities(&[
(handle, crate::scene::ChangeKind::Modified),
(source, crate::scene::ChangeKind::Modified),
]);
}
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(pending) = pending {
self.commit_undo_delta(i, pending);
}
}
CmdResult::CommitSolid {
entity,
solid,

View file

@ -1218,6 +1218,8 @@ pub enum ColorPickTarget {
Properties,
/// Selected entities' background colour (hatch / MTEXT background row).
PropertiesBg,
/// A named per-entity Properties colour field.
PropertiesField(String),
/// Current creation colour (ribbon).
Ribbon,
/// A layer's colour, by panel row index.

View file

@ -1006,6 +1006,21 @@ impl OpenCADStudio {
// Inject DimStyle picker + style-derived groups for Dimensions.
if let acadrust::EntityType::Dimension(d) = entity {
if let Some(general) = sections.first_mut() {
general.props.retain(|property| property.field != "handle");
let associative =
crate::scene::dimension_assoc::dimension_is_associative(
&self.tabs[i].scene.document,
d.base().common.handle,
);
general.props.push(crate::scene::model::object::Property {
label: t!("Associative").into_owned(),
field: "associative",
value: crate::scene::model::object::PropValue::ReadOnly(
if associative { "Yes" } else { "No" }.to_string(),
),
});
}
let dim_style_names: Vec<String> = self.tabs[i]
.scene
.document
@ -1046,12 +1061,14 @@ impl OpenCADStudio {
&& s.name.eq_ignore_ascii_case("Standard"))
})
{
sections.extend(crate::entities::dimension::style_sections(style));
sections.extend(crate::entities::dimension::style_sections(
style,
d,
&self.tabs[i].scene.document,
));
// The style groups are a read-only mirror, but the
// dim-line colour is an editable per-object override:
// prefer the ACAD_DSTYLE code-176 (ACI) override, else
// the style's DIMCLRD.
// Prefer entity-level dimension-variable overrides;
// fall back to the assigned style values.
use crate::entities::dim_override as dov;
use crate::scene::model::object::PropValue;
let dim_c = dov::color(&d.base().common.extended_data, dov::DIMCLRD)
@ -1061,6 +1078,20 @@ impl OpenCADStudio {
"dim_line_color",
PropValue::ColorChoice(dim_c),
);
for (field, code, inherited) in [
("dim_ext_line_color", dov::DIMCLRE, style.dimclre),
("dim_text_color", dov::DIMCLRT, style.dimclrt),
] {
let color = dov::color(&d.base().common.extended_data, code)
.unwrap_or_else(|| {
acadrust::types::Color::from_index(inherited)
});
set_row_value(
&mut sections,
field,
PropValue::ColorChoice(color),
);
}
}
}
@ -1323,10 +1354,8 @@ impl OpenCADStudio {
}
// Annotative Yes/No + a conditional "Annotative scale" row.
// Read-only: annotative state comes from the entity's style
// (or its own flag) and the assigned annotation-scale name(s)
// are walked from the entity's extension dictionary — both
// need the document, so they are resolved here.
// Annotative state and assigned scale names need the
// document, so they are resolved here.
{
// Which entities show an Annotative row, the field it uses,
// and — for those that don't already carry the row
@ -1368,7 +1397,7 @@ impl OpenCADStudio {
// (MTEXT via its native flag, single-line TEXT via the
// context alone) get an editable toggle: turning it on
// synthesizes a real per-scale representation. The
// remaining types are style-driven and stay read-only.
// remaining style-only types stay read-only.
if anno_field == "annotative" {
match entity {
acadrust::EntityType::MText(t) => set_row_value(
@ -1381,7 +1410,8 @@ impl OpenCADStudio {
),
acadrust::EntityType::Text(_)
| acadrust::EntityType::Insert(_)
| acadrust::EntityType::Hatch(_) => set_row_value(
| acadrust::EntityType::Hatch(_)
| acadrust::EntityType::Dimension(_) => set_row_value(
&mut sections,
"annotative",
crate::scene::model::object::PropValue::BoolToggle {
@ -1397,16 +1427,26 @@ impl OpenCADStudio {
}
}
if is_anno {
// The applied annotation scale follows the current
// annotation scale (CANNOSCALE / the status-bar
// scale pill), not a per-object stored value.
let memberships = crate::scene::annotative::object_scale_memberships(
doc,
entity.common().handle,
);
let assigned_scales = if memberships.is_empty() {
doc.header.current_annotation_scale.clone()
} else {
memberships
.into_iter()
.map(|(name, _)| name)
.collect::<Vec<_>>()
.join(", ")
};
insert_row_after(
&mut sections,
anno_field,
crate::entities::common::ro_prop(
t!("Annotative scale").as_ref(),
"annotative_scale",
doc.header.current_annotation_scale.clone(),
assigned_scales,
),
);
}
@ -2411,18 +2451,7 @@ fn set_row_value(
/// The lineweight dropdown options (named defaults + the standard millimetre
/// steps), matching the labels `dim_lineweight_label` produces.
pub(crate) fn lineweight_options() -> Vec<String> {
let mut v = vec![
"ByLayer".to_string(),
"ByBlock".to_string(),
"Default".to_string(),
];
for lw in [
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158,
200, 211,
] {
v.push(format!("{:.2} mm", lw as f64 / 100.0));
}
v
crate::entities::common::lineweight_options()
}
/// Inverse of `dim_lineweight_label`: a lineweight label → DIMLWD enum value.
@ -2548,13 +2577,7 @@ fn leader_arrow_label(
/// DIMLWD lineweight enum → label.
fn dim_lineweight_label(dimlwd: i16) -> String {
match dimlwd {
-1 => "ByLayer".to_string(),
-2 => "ByBlock".to_string(),
-3 => "Default".to_string(),
v if v >= 0 => format!("{:.2} mm", v as f64 / 100.0),
_ => "Default".to_string(),
}
crate::entities::common::lineweight_label(dimlwd)
}
/// Human-readable INSUNITS name (DXF group 70 unit codes).

View file

@ -1140,6 +1140,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if matches!(
item.action,
GripMenuAction::Stretch
| GripMenuAction::MoveWithDimLine
| GripMenuAction::MoveWithLeader
| GripMenuAction::MoveIndependent
) {
@ -1839,7 +1840,24 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
self.push_undo_snapshot(i, "CHPROP");
if field == "vscale_std" {
if field.starts_with("dim_") {
for &handle in &handles {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&value,
);
}
}
} else if field == "vscale_std" {
for &handle in &handles {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
@ -2410,6 +2428,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
continue;
}
match field {
_ if field.starts_with("dim_") => {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&val,
);
}
}
"hyperlink" => {
// Stored in the standard PE_URL XDATA
// record; an empty value clears it.

View file

@ -4834,8 +4834,25 @@ impl OpenCADStudio {
// line with the rest of the dim-colour stack (index-only through
// the file layer). Guarded to leaders / dimensions so a mixed
// selection can't stamp the override onto other entities.
if field == "dim_line_color" {
if matches!(
field.as_str(),
"dim_line_color"
| "dim_ext_line_color"
| "dim_text_color"
| "dim_text_fill_color"
) {
let fill_mode = (field == "dim_text_fill_color").then(|| match color {
acadrust::types::Color::None => 0,
acadrust::types::Color::ByBlock => 1,
_ => 2,
});
let aci = color.approximate_index();
let code = match field.as_str() {
"dim_ext_line_color" => crate::entities::dim_override::DIMCLRE,
"dim_text_color" => crate::entities::dim_override::DIMCLRT,
"dim_text_fill_color" => crate::entities::dim_override::DIMTFILLCLR,
_ => crate::entities::dim_override::DIMCLRD,
};
let targets: Vec<acadrust::Handle> = handles
.iter()
.copied()
@ -4850,10 +4867,23 @@ impl OpenCADStudio {
if !targets.is_empty() {
self.push_undo_snapshot(i, "CHPROP");
for &handle in &targets {
if field == "dim_text_fill_color" {
crate::entities::dim_override::set(
&mut self.tabs[i].scene.document,
handle,
crate::entities::dim_override::DIMTFILL,
Some(acadrust::xdata::XDataValue::Integer16(
fill_mode.unwrap_or(2),
)),
);
if fill_mode != Some(2) {
continue;
}
}
crate::entities::dim_override::set(
&mut self.tabs[i].scene.document,
handle,
crate::entities::dim_override::DIMCLRD,
code,
Some(acadrust::xdata::XDataValue::Integer16(aci)),
);
}
@ -7527,6 +7557,7 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].properties.color_picker_open = false;
self.tabs[i].properties.open_color_field = None;
self.tabs[i].layers.color_picker_row = None;
Task::none()

View file

@ -1146,6 +1146,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Some(crate::app::ColorPickTarget::PropertiesBg) => {
Some(Message::PropBgColorChanged(color))
}
Some(crate::app::ColorPickTarget::PropertiesField(field)) => {
Some(Message::PropColorFieldChanged { field, color })
}
Some(crate::app::ColorPickTarget::MText) => {
Some(Message::MTextColorChanged(color))
}

View file

@ -474,6 +474,7 @@ fn mtext_editor_content<'a>(
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: false,
..Default::default()
},
Message::MTextColorChanged,
Message::MTextColorPickerToggle,

View file

@ -1212,6 +1212,11 @@ pub enum CmdResult {
CommitEntitiesAndExit(Vec<EntityType>),
/// Commit an acadrust entity to the document and end the command.
CommitAndExit(EntityType),
/// Commit an object-selected linear dimension and retain its source link.
CommitAssociativeDimension {
entity: EntityType,
source: Handle,
},
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its B-rep (cached for boolean ops + shaded
/// rendering). Ends the command.

View file

@ -575,6 +575,33 @@ pub fn edit_scalar_prop(label: &str, field: &'static str, value: f64) -> Propert
}
}
/// Standard lineweight choices shared by entity-specific Properties rows.
pub fn lineweight_options() -> Vec<String> {
let mut options = vec![
"ByLayer".to_string(),
"ByBlock".to_string(),
"Default".to_string(),
];
for value in [
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120,
140, 158, 200, 211,
] {
options.push(format!("{:.2} mm", value as f64 / 100.0));
}
options
}
/// DIMLWD/DIMLWE lineweight value displayed by a Properties dropdown.
pub fn lineweight_label(value: i16) -> String {
match value {
-1 => "ByLayer".to_string(),
-2 => "ByBlock".to_string(),
-3 => "Default".to_string(),
value if value >= 0 => format!("{:.2} mm", value as f64 / 100.0),
_ => "Default".to_string(),
}
}
pub fn ro_prop(label: &str, field: &'static str, value: impl Into<String>) -> Property {
Property {
label: label.into(),

View file

@ -9,7 +9,7 @@
use acadrust::types::Color;
use acadrust::xdata::{ExtendedData, XDataValue};
use acadrust::{CadDocument, Handle};
use acadrust::{CadDocument, EntityType, Handle};
// DXF group codes of the dimension variables surfaced on the leader panel.
pub const DIMSCALE: i16 = 40; // overall scale (real)
@ -77,7 +77,12 @@ pub const DIMATFIT: i16 = 289;
pub const DIMFXLON: i16 = 290;
pub const DIMTFILL: i16 = 69;
pub const DIMTFILLCLR: i16 = 70;
pub const DIMTXTDIRECTION: i16 = 295;
pub const DIMTXTDIRECTION: i16 = 294;
pub const DIMALTMZF: i16 = 295;
pub const DIMALTMZS: i16 = 296;
pub const DIMMZF: i16 = 297;
pub const DIMMZS: i16 = 298;
pub const DIMTALN: i16 = 392;
pub const DIMTXSTY: i16 = 340;
pub const DIMBLK: i16 = 342;
pub const DIMBLK1: i16 = 343;
@ -229,3 +234,479 @@ pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option<XData
}
write_pairs(doc, handle, kept);
}
pub fn property_real_code(field: &str) -> Option<i16> {
Some(match field {
"dim_arrow_size" => DIMASZ,
"dim_line_ext" => DIMDLE,
"dim_ext_line_ext" => DIMEXE,
"dim_ext_line_offset" => DIMEXO,
"dim_ext_line_fixed_length" => DIMFXL,
"dim_text_height" => DIMTXT,
"dim_text_offset" => DIMGAP,
"dim_scale_overall" => DIMSCALE,
"dim_roundoff" => DIMRND,
"dim_scale_linear" => DIMLFAC,
"dim_alt_scale_factor" => DIMALTF,
"dim_alt_sub_units_scale" => DIMALTMZF,
"dim_alt_roundoff" => DIMALTRND,
"dim_sub_units_scale" => DIMMZF,
"dim_tolerance_limit_lower" => DIMTM,
"dim_tolerance_limit_upper" => DIMTP,
"dim_tolerance_text_height" => DIMTFAC,
_ => return None,
})
}
pub fn property_int_code(field: &str) -> Option<i16> {
Some(match field {
"dim_line_lineweight" => DIMLWD,
"dim_ext_line_lineweight" => DIMLWE,
"dim_ext_line_fixed" => DIMFXLON,
"dim_text_pos_vert" => DIMTAD,
"dim_text_pos_hor" => DIMJUST,
"dim_text_outside_align" => DIMTOH,
"dim_text_inside_align" => DIMTIH,
"dim_fit" => DIMATFIT,
"dim_text_inside" => DIMTIX,
"dim_text_movement" => DIMTMOVE,
"dim_line_forced" => DIMTOFL,
"dim_line_inside" => DIMSOXD,
"dim_units" => DIMLUNIT,
"dim_precision" => DIMDEC,
"dim_decimal_separator" => DIMDSEP,
"dim_fractional_type" => DIMFRAC,
"dim_text_view_direction" => DIMTXTDIRECTION,
"dim_alt_enabled" => DIMALT,
"dim_alt_format" => DIMALTU,
"dim_alt_precision" => DIMALTD,
"dim_alt_tolerance_precision" => DIMALTTD,
"dim_tolerance_precision" => DIMTDEC,
"dim_tolerance_pos_vert" => DIMTOLJ,
"dim_tolerance_alignment" => DIMTALN,
_ => return None,
})
}
pub fn property_string_code(field: &str) -> Option<i16> {
Some(match field {
"dim_sub_units_suffix" => DIMMZS,
"dim_alt_sub_units_suffix" => DIMALTMZS,
_ => return None,
})
}
fn yes(value: &str) -> Option<i16> {
match value.trim().to_ascii_lowercase().as_str() {
"yes" | "on" | "true" | "1" => Some(1),
"no" | "off" | "false" | "0" => Some(0),
_ => None,
}
}
fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 {
let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else {
return 0;
};
if let Some(value) = int(&dimension.base().common.extended_data, code) {
return value;
}
let Some(style) = doc.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"))
}) else {
return 0;
};
match code {
DIMSD1 => style.dimsd1 as i16,
DIMSD2 => style.dimsd2 as i16,
DIMSE1 => style.dimse1 as i16,
DIMSE2 => style.dimse2 as i16,
DIMZIN => style.dimzin,
DIMALTZ => style.dimaltz,
DIMTZIN => style.dimtzin,
DIMALTTZ => style.dimalttz,
DIMTOL => style.dimtol as i16,
DIMLIM => style.dimlim as i16,
DIMALT => style.dimalt as i16,
DIMTFILL => style.dimtfill,
DIMTALN => 0,
_ => 0,
}
}
fn inherited_real(doc: &CadDocument, handle: Handle, code: i16) -> f64 {
let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else {
return 0.0;
};
if let Some(value) = real(&dimension.base().common.extended_data, code) {
return value;
}
let Some(style) = doc.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"))
}) else {
return 0.0;
};
match code {
DIMGAP => style.dimgap,
DIMTP => style.dimtp,
DIMTM => style.dimtm,
_ => 0.0,
}
}
fn inherited_string(doc: &CadDocument, handle: Handle, code: i16) -> String {
let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else {
return String::new();
};
if let Some(value) = string(&dimension.base().common.extended_data, code) {
return value;
}
let Some(style) = doc.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"))
}) else {
return String::new();
};
match code {
DIMPOST => style.dimpost.clone(),
DIMAPOST => style.dimapost.clone(),
_ => String::new(),
}
}
fn split_template(value: &str) -> (&str, &str) {
value.split_once("<>").unwrap_or(("", value))
}
pub fn set_property(
doc: &mut CadDocument,
handle: Handle,
field: &str,
value: &str,
) -> bool {
let trimmed = value.trim();
let handle_field = match field {
"dim_arrowhead_1" => Some(DIMBLK1),
"dim_arrowhead_2" => Some(DIMBLK2),
"dim_linetype" => Some(DIMLTYPE),
"dim_ext_linetype_1" => Some(DIMLTEX1),
"dim_ext_linetype_2" => Some(DIMLTEX2),
"dim_text_style" => Some(DIMTXSTY),
_ => None,
};
if let Some(code) = handle_field {
let resolved = match field {
"dim_arrowhead_1" | "dim_arrowhead_2" => {
if trimmed == "Closed filled" {
Some(Handle::NULL)
} else {
doc.block_records
.iter()
.find(|record| record.name == trimmed)
.map(|record| record.handle)
}
}
"dim_text_style" => doc
.text_styles
.iter()
.find(|style| style.name == trimmed)
.map(|style| style.handle),
_ => doc
.line_types
.iter()
.find(|line_type| line_type.name == trimmed)
.map(|line_type| line_type.handle),
};
let Some(resolved) = resolved else {
return false;
};
set(doc, handle, code, Some(XDataValue::Handle(resolved)));
return true;
}
let inverse_boolean_code = match field {
"dim_line_1" => Some(DIMSD1),
"dim_line_2" => Some(DIMSD2),
"dim_ext_line_1" => Some(DIMSE1),
"dim_ext_line_2" => Some(DIMSE2),
_ => None,
};
if let Some(code) = inverse_boolean_code {
let Some(visible) = yes(trimmed) else {
return false;
};
set(doc, handle, code, Some(XDataValue::Integer16((visible == 0) as i16)));
return true;
}
let bit_field = match field {
"dim_suppress_leading_zeros" => Some((DIMZIN, 4)),
"dim_suppress_trailing_zeros" => Some((DIMZIN, 8)),
"dim_alt_suppress_leading_zeros" => Some((DIMALTZ, 4)),
"dim_alt_suppress_trailing_zeros" => Some((DIMALTZ, 8)),
"dim_tolerance_suppress_leading_zeros" => Some((DIMTZIN, 4)),
"dim_tolerance_suppress_trailing_zeros" => Some((DIMTZIN, 8)),
"dim_alt_tolerance_suppress_leading_zeros" => Some((DIMALTTZ, 4)),
"dim_alt_tolerance_suppress_trailing_zeros" => Some((DIMALTTZ, 8)),
_ => None,
};
if let Some((code, bit)) = bit_field {
let Some(enabled) = yes(trimmed) else {
return false;
};
let current = inherited_int(doc, handle, code);
let value = if enabled != 0 {
current | bit
} else {
current & !bit
};
set(doc, handle, code, Some(XDataValue::Integer16(value)));
return true;
}
let zero_base = match field {
"dim_suppress_zero_feet" => Some((DIMZIN, true)),
"dim_suppress_zero_inches" => Some((DIMZIN, false)),
"dim_alt_suppress_zero_feet" => Some((DIMALTZ, true)),
"dim_alt_suppress_zero_inches" => Some((DIMALTZ, false)),
"dim_tolerance_suppress_zero_feet" => Some((DIMTZIN, true)),
"dim_tolerance_suppress_zero_inches" => Some((DIMTZIN, false)),
"dim_alt_tolerance_suppress_zero_feet" => Some((DIMALTTZ, true)),
"dim_alt_tolerance_suppress_zero_inches" => Some((DIMALTTZ, false)),
_ => None,
};
if let Some((code, feet_field)) = zero_base {
let Some(enabled) = yes(trimmed).map(|value| value != 0) else {
return false;
};
let current = inherited_int(doc, handle, code);
let mut suppress_feet = matches!(current & 3, 0 | 3);
let mut suppress_inches = matches!(current & 3, 0 | 2);
if feet_field {
suppress_feet = enabled;
} else {
suppress_inches = enabled;
}
let base = match (suppress_feet, suppress_inches) {
(true, true) => 0,
(false, false) => 1,
(false, true) => 2,
(true, false) => 3,
};
set(
doc,
handle,
code,
Some(XDataValue::Integer16((current & !3) | base)),
);
return true;
}
if field == "dim_text_fill_color" {
let mode = match trimmed.to_ascii_lowercase().as_str() {
"none" => 0,
"background" => 1,
"color" => 2,
_ => return false,
};
set(doc, handle, DIMTFILL, Some(XDataValue::Integer16(mode)));
return true;
}
if matches!(
field,
"dim_prefix" | "dim_suffix" | "dim_alt_prefix" | "dim_alt_suffix"
) {
let code = if field.starts_with("dim_alt_") {
DIMAPOST
} else {
DIMPOST
};
let current = inherited_string(doc, handle, code);
let (old_prefix, old_suffix) = split_template(&current);
let (prefix, suffix) = if field.ends_with("prefix") {
(value, old_suffix)
} else {
(old_prefix, value)
};
set(
doc,
handle,
code,
Some(XDataValue::String(format!("{prefix}<>{suffix}"))),
);
return true;
}
if field == "dim_tolerance_display" {
let current_gap = inherited_real(doc, handle, DIMGAP).abs();
let upper = inherited_real(doc, handle, DIMTP);
let lower = inherited_real(doc, handle, DIMTM);
let (tolerance, limits, basic) = match trimmed.to_ascii_lowercase().as_str() {
"symmetrical" => {
set(doc, handle, DIMTM, Some(XDataValue::Real(upper)));
(1, 0, false)
}
"deviation" => {
if (upper - lower).abs() <= 1e-12 {
let distinct_lower = if upper.abs() > 1e-12 { 0.0 } else { 0.0001 };
set(
doc,
handle,
DIMTM,
Some(XDataValue::Real(distinct_lower)),
);
}
(1, 0, false)
}
"limits" => (0, 1, false),
"basic" => (0, 0, true),
"none" => (0, 0, false),
_ => return false,
};
set(doc, handle, DIMTOL, Some(XDataValue::Integer16(tolerance)));
set(doc, handle, DIMLIM, Some(XDataValue::Integer16(limits)));
let gap = if basic {
-current_gap.max(0.0001)
} else {
current_gap
};
set(doc, handle, DIMGAP, Some(XDataValue::Real(gap)));
return true;
}
if let Some(code) = property_real_code(field) {
if trimmed.is_empty() {
set(doc, handle, code, None);
} else if let Ok(number) = trimmed.parse::<f64>() {
let number = if field == "dim_text_offset"
&& inherited_real(doc, handle, DIMGAP) < 0.0
{
-number.abs()
} else {
number
};
set(doc, handle, code, Some(XDataValue::Real(number)));
} else {
return false;
}
return true;
}
if let Some(code) = property_string_code(field) {
set(
doc,
handle,
code,
(!trimmed.is_empty()).then(|| XDataValue::String(value.to_string())),
);
return true;
}
if let Some(code) = property_int_code(field) {
let parsed = match field {
"dim_line_lineweight" | "dim_ext_line_lineweight" => {
parse_lineweight_label(trimmed)
}
"dim_ext_line_fixed"
| "dim_text_outside_align"
| "dim_text_inside_align"
| "dim_text_inside"
| "dim_line_forced"
| "dim_line_inside"
| "dim_alt_enabled" => yes(trimmed),
"dim_decimal_separator" => trimmed
.chars()
.next()
.map(|character| character as i16),
"dim_units" => match trimmed.to_ascii_lowercase().as_str() {
"scientific" => Some(1),
"decimal" => Some(2),
"engineering" => Some(3),
"architectural" => Some(4),
"fractional" => Some(5),
"desktop" => Some(6),
_ => trimmed.parse().ok(),
},
"dim_fractional_type" => match trimmed.to_ascii_lowercase().as_str() {
"horizontal" => Some(0),
"diagonal" => Some(1),
"not stacked" => Some(2),
_ => trimmed.parse().ok(),
},
"dim_text_view_direction" => match trimmed.to_ascii_lowercase().as_str() {
"left-to-right" => Some(0),
"right-to-left" => Some(1),
_ => trimmed.parse().ok(),
},
"dim_text_pos_vert" => match trimmed.to_ascii_lowercase().as_str() {
"centered" => Some(0),
"above" => Some(1),
"outside" => Some(2),
"jis" => Some(3),
"below" => Some(4),
_ => trimmed.parse().ok(),
},
"dim_text_pos_hor" => match trimmed.to_ascii_lowercase().as_str() {
"centered" => Some(0),
"at extension line 1" => Some(1),
"at extension line 2" => Some(2),
"over extension line 1" => Some(3),
"over extension line 2" => Some(4),
_ => trimmed.parse().ok(),
},
"dim_fit" => match trimmed.to_ascii_lowercase().as_str() {
"both text and arrows" => Some(0),
"arrows" => Some(1),
"text" => Some(2),
"best fit" => Some(3),
_ => trimmed.parse().ok(),
},
"dim_text_movement" => match trimmed.to_ascii_lowercase().as_str() {
"keep dim line with text" => Some(0),
"move text, add leader" => Some(1),
"move text, no leader" => Some(2),
_ => trimmed.parse().ok(),
},
"dim_alt_format" => match trimmed.to_ascii_lowercase().as_str() {
"scientific" => Some(1),
"decimal" => Some(2),
"engineering" => Some(3),
"architectural stacked" => Some(4),
"fractional stacked" => Some(5),
"architectural" => Some(6),
"fractional" => Some(7),
"desktop" => Some(8),
_ => trimmed.parse().ok(),
},
"dim_tolerance_pos_vert" => match trimmed.to_ascii_lowercase().as_str() {
"bottom" => Some(0),
"middle" => Some(1),
"top" => Some(2),
_ => trimmed.parse().ok(),
},
"dim_tolerance_alignment" => match trimmed.to_ascii_lowercase().as_str() {
"align decimal separators" => Some(0),
"align operational symbols" => Some(1),
_ => trimmed.parse().ok(),
},
_ => trimmed.parse().ok(),
};
let Some(number) = parsed else {
return false;
};
set(doc, handle, code, Some(XDataValue::Integer16(number)));
return true;
}
false
}
fn parse_lineweight_label(value: &str) -> Option<i16> {
match value.trim() {
"ByLayer" => Some(-1),
"ByBlock" => Some(-2),
"Default" => Some(-3),
value => value
.trim_end_matches("mm")
.trim()
.parse::<f64>()
.ok()
.map(|millimetres| (millimetres * 100.0).round() as i16),
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
use acadrust::entities::{Dimension, DimensionLinear};
use acadrust::types::Vector3;
use acadrust::EntityType;
use cadkernel::geom2d::{closest_point, Curve, Line as KernelLine};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@ -57,6 +58,25 @@ enum Step {
DimensionLine { first: DVec3, second: DVec3 },
}
#[derive(Clone, Copy)]
enum AxisMode {
Automatic,
Horizontal,
Vertical,
Rotated(f64),
}
impl AxisMode {
fn axis(self, first: DVec3, second: DVec3, point: DVec3) -> DVec3 {
match self {
Self::Automatic => measure_axis(first, second, point),
Self::Horizontal => DVec3::X,
Self::Vertical => DVec3::Y,
Self::Rotated(angle) => DVec3::new(angle.cos(), angle.sin(), 0.0),
}
}
}
pub struct LinearDimensionCommand {
step: Step,
plane: WorkingPlane,
@ -68,6 +88,13 @@ pub struct LinearDimensionCommand {
text_angle: Option<f64>,
/// True while the next typed value is captured as the text angle.
awaiting_angle: bool,
/// True while a rotation value for the Rotated option is being entered.
awaiting_rotation: bool,
axis_mode: AxisMode,
selecting_object: bool,
picked_entity: Option<EntityType>,
source_handle: Option<acadrust::Handle>,
mtext_override: bool,
}
impl LinearDimensionCommand {
@ -79,6 +106,12 @@ impl LinearDimensionCommand {
awaiting_text: false,
text_angle: None,
awaiting_angle: false,
awaiting_rotation: false,
axis_mode: AxisMode::Automatic,
selecting_object: false,
picked_entity: None,
source_handle: None,
mtext_override: false,
}
}
}
@ -94,18 +127,32 @@ impl CadCommand for LinearDimensionCommand {
fn prompt(&self) -> String {
if self.awaiting_text {
return t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned();
return if self.mtext_override {
t!("DIMLINEAR Enter formatted dimension text (blank = measured value):")
.into_owned()
} else {
t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned()
};
}
if self.awaiting_angle {
return t!("DIMLINEAR Specify text angle (degrees):").into_owned();
}
if self.awaiting_rotation {
return t!("DIMLINEAR Specify dimension line angle (degrees):").into_owned();
}
if self.selecting_object {
return t!("DIMLINEAR Select object to dimension:").into_owned();
}
match self.step {
Step::FirstPoint => t!("DIMLINEAR Specify first extension line origin:").into_owned(),
Step::FirstPoint => {
t!("DIMLINEAR Specify first extension line origin or press Enter to select object:")
.into_owned()
}
Step::SecondPoint(_) => {
t!("DIMLINEAR Specify second extension line origin [Text/Angle]:").into_owned()
t!("DIMLINEAR Specify second extension line origin:").into_owned()
}
Step::DimensionLine { .. } => {
t!("DIMLINEAR Specify dimension line location [Text/Angle]:").into_owned()
t!("DIMLINEAR Specify dimension line location [Mtext/Text/Angle/Horizontal/Vertical/Rotated]:").into_owned()
}
}
}
@ -117,6 +164,9 @@ impl CadCommand for LinearDimensionCommand {
CmdResult::NeedPoint
}
Step::SecondPoint(first) => {
if pt.distance_squared(first) <= 1e-24 {
return CmdResult::NeedPoint;
}
self.step = Step::DimensionLine { first, second: pt };
CmdResult::NeedPoint
}
@ -125,21 +175,29 @@ impl CadCommand for LinearDimensionCommand {
let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let mut dim = DimensionLinear::new(v3(first), v3(second));
let axis = measure_axis(first, second, pt);
let axis = self.axis_mode.axis(first, second, pt);
dim.rotation = axis.y.atan2(axis.x);
dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt);
dim.set_offset(dimension_line_offset(second, pt, axis));
dim.base.definition_point = dim.definition_point;
dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis));
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
dim.base.user_text = self.text_override.clone();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
// An explicit text angle overrides the UCS-derived rotation.
if let Some(a) = self.text_angle {
dim.base.text_rotation = a;
}
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
let entity = self.plane.place_entity(EntityType::Dimension(
Dimension::Linear(dim),
)))
));
if let Some(source) = self.source_handle {
CmdResult::CommitAssociativeDimension { entity, source }
} else {
CmdResult::CommitAndExit(entity)
}
}
}
}
@ -154,6 +212,14 @@ impl CadCommand for LinearDimensionCommand {
self.awaiting_angle = false;
return CmdResult::NeedPoint;
}
if self.awaiting_rotation {
self.awaiting_rotation = false;
return CmdResult::NeedPoint;
}
if matches!(self.step, Step::FirstPoint) {
self.selecting_object = true;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
@ -168,7 +234,7 @@ impl CadCommand for LinearDimensionCommand {
fn point_step_accepts_keywords(&self) -> bool {
// While typing the override text or angle, route input as a value, not
// a point pick / keyword.
!self.awaiting_text && !self.awaiting_angle
!self.awaiting_text && !self.awaiting_angle && !self.awaiting_rotation
}
fn wants_text_with_spaces(&self) -> bool {
@ -199,8 +265,24 @@ impl CadCommand for LinearDimensionCommand {
self.awaiting_angle = false;
return Some(CmdResult::NeedPoint);
}
if self.awaiting_rotation {
if let Some(angle) = crate::entities::common::parse_typed_angle(text.trim()) {
self.axis_mode = AxisMode::Rotated(angle);
}
self.awaiting_rotation = false;
return Some(CmdResult::NeedPoint);
}
if !matches!(self.step, Step::DimensionLine { .. }) {
return None;
}
match text.trim().to_uppercase().as_str() {
"T" | "TEXT" | "M" | "MTEXT" => {
"T" | "TEXT" => {
self.mtext_override = false;
self.awaiting_text = true;
Some(CmdResult::NeedPoint)
}
"M" | "MTEXT" => {
self.mtext_override = true;
self.awaiting_text = true;
Some(CmdResult::NeedPoint)
}
@ -208,10 +290,54 @@ impl CadCommand for LinearDimensionCommand {
self.awaiting_angle = true;
Some(CmdResult::NeedPoint)
}
"H" | "HORIZONTAL" => {
self.axis_mode = AxisMode::Horizontal;
Some(CmdResult::NeedPoint)
}
"V" | "VERTICAL" => {
self.axis_mode = AxisMode::Vertical;
Some(CmdResult::NeedPoint)
}
"R" | "ROTATED" => {
self.awaiting_rotation = true;
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn needs_entity_pick(&self) -> bool {
self.selecting_object
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
let Some((first, second)) = dimension_source_points(&entity, point) else {
return CmdResult::NeedPoint;
};
if first.distance_squared(second) <= 1e-24 {
return CmdResult::NeedPoint;
}
self.source_handle = Some(handle);
self.selecting_object = false;
self.step = Step::DimensionLine { first, second };
CmdResult::NeedPoint
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.step {
Step::FirstPoint => None,
@ -220,7 +346,7 @@ impl CadCommand for LinearDimensionCommand {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let axis = measure_axis(first, second, pt);
let axis = self.axis_mode.axis(first, second, pt);
let points = linear_dimension_preview(first, second, pt, axis)
.into_iter()
.map(|point| self.plane.to_world(point))
@ -235,6 +361,97 @@ fn v3(pt: DVec3) -> Vector3 {
Vector3::new(pt.x, pt.y, pt.z)
}
fn dimension_line_offset(second: DVec3, point: DVec3, axis: DVec3) -> f64 {
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
(point - second).dot(perpendicular)
}
fn dimension_source_points(entity: &EntityType, click: DVec3) -> Option<(DVec3, DVec3)> {
let point = |p: Vector3| DVec3::new(p.x, p.y, p.z);
match entity {
EntityType::Line(line) => Some((point(line.start), point(line.end))),
EntityType::Arc(arc) => Some((point(arc.start_point_wcs()), point(arc.end_point_wcs()))),
EntityType::LwPolyline(polyline) => nearest_planar_source(
polyline
.vertices
.iter()
.map(|vertex| [vertex.location.x, vertex.location.y]),
polyline.is_closed,
polyline.elevation,
polyline.normal,
click,
),
EntityType::Polyline2D(polyline) => {
let vertices = crate::entities::polyline::drawn_vertices2d(polyline)
.unwrap_or_else(|| polyline.vertices.clone());
nearest_planar_source(
vertices
.iter()
.map(|vertex| [vertex.location.x, vertex.location.y]),
polyline.is_closed(),
polyline.elevation,
polyline.normal,
click,
)
}
_ => None,
}
}
fn nearest_planar_source(
points: impl IntoIterator<Item = [f64; 2]>,
closed: bool,
elevation: f64,
normal: Vector3,
click: DVec3,
) -> Option<(DVec3, DVec3)> {
let click = crate::scene::view::transform::wcs_point_to_ocs(
(click.x, click.y, click.z),
(normal.x, normal.y, normal.z),
);
let (first, second) = nearest_segment(points, closed, [click.0, click.1])?;
Some((
ocs_point(first, elevation, normal),
ocs_point(second, elevation, normal),
))
}
fn nearest_segment(
points: impl IntoIterator<Item = [f64; 2]>,
closed: bool,
click: [f64; 2],
) -> Option<([f64; 2], [f64; 2])> {
let points: Vec<_> = points.into_iter().collect();
if points.len() < 2 {
return None;
}
let count = if closed { points.len() } else { points.len() - 1 };
(0..count)
.map(|index| {
let first = points[index];
let second = points[(index + 1) % points.len()];
let distance = closest_point(
&Curve::Line(KernelLine {
start: first,
end: second,
}),
click,
)
.distance;
(distance, first, second)
})
.min_by(|a, b| a.0.total_cmp(&b.0))
.map(|(_, first, second)| (first, second))
}
fn ocs_point(point: [f64; 2], elevation: f64, normal: Vector3) -> DVec3 {
let point = crate::scene::view::transform::ocs_point_to_wcs(
(point[0], point[1], elevation),
(normal.x, normal.y, normal.z),
);
DVec3::new(point.0, point.1, point.2)
}
fn preview_wire(points: Vec<DVec3>) -> WireModel {
WireModel {
point_marker: None,
@ -290,7 +507,23 @@ fn dim_line_endpoints(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> (
fn linear_dimension_preview(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> Vec<DVec3> {
let (d1, d2) = dim_line_endpoints(first, second, def, axis);
let nan = DVec3::new(f64::NAN, f64::NAN, f64::NAN);
vec![first, d1, nan, second, d2, nan, d1, d2]
let arrow = 0.22;
let perp = DVec3::new(-axis.y, axis.x, 0.0);
let text = linear_text_pos(first, second, def, axis);
let half_width = ((second - first).length().log10().max(0.0) + 1.0) * 0.18;
let half_height = 0.16;
vec![
first, d1, nan, second, d2, nan, d1, d2, nan,
d1, d1 + axis * arrow + perp * arrow * 0.45, nan,
d1, d1 + axis * arrow - perp * arrow * 0.45, nan,
d2, d2 - axis * arrow + perp * arrow * 0.45, nan,
d2, d2 - axis * arrow - perp * arrow * 0.45, nan,
text - axis * half_width - perp * half_height,
text + axis * half_width - perp * half_height,
text + axis * half_width + perp * half_height,
text - axis * half_width + perp * half_height,
text - axis * half_width - perp * half_height,
]
}
fn linear_text_pos(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> DVec3 {

View file

@ -0,0 +1,250 @@
use acadrust::entities::Dimension;
use acadrust::objects::{
AssocDimensionAssociation, AssocDimensionReference, AssociativeData,
AssociativeObject, ObjectType,
};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use super::{ChangeKind, Scene};
fn point_distance_squared(first: Vector3, second: Vector3) -> f64 {
let dx = first.x - second.x;
let dy = first.y - second.y;
let dz = first.z - second.z;
dx * dx + dy * dy + dz * dz
}
fn ocs_point(x: f64, y: f64, elevation: f64, normal: Vector3) -> Vector3 {
let point = crate::scene::view::transform::ocs_point_to_wcs(
(x, y, elevation),
(normal.x, normal.y, normal.z),
);
Vector3::new(point.0, point.1, point.2)
}
fn source_points(entity: &EntityType) -> Vec<Vector3> {
match entity {
EntityType::Line(line) => vec![line.start, line.end],
EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()],
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
.map(|vertex| {
ocs_point(
vertex.location.x,
vertex.location.y,
polyline.elevation,
polyline.normal,
)
})
.collect(),
EntityType::Polyline2D(polyline) => polyline
.vertices
.iter()
.map(|vertex| {
ocs_point(
vertex.location.x,
vertex.location.y,
polyline.elevation,
polyline.normal,
)
})
.collect(),
_ => Vec::new(),
}
}
fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
source_points(entity)
.into_iter()
.enumerate()
.min_by(|(_, first), (_, second)| {
point_distance_squared(*first, point)
.total_cmp(&point_distance_squared(*second, point))
})
.map(|(index, _)| index as i32)
}
fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option<Vector3> {
let source = *reference.xrefs.first()?;
let entity = scene.document.get_entity(source)?;
source_points(entity)
.get(reference.main_gs_marker.max(0) as usize)
.copied()
}
pub(crate) fn dimension_is_associative(
document: &acadrust::CadDocument,
dimension: Handle,
) -> bool {
document.objects.values().any(|object| {
let ObjectType::Associative(object) = object else {
return false;
};
let AssociativeData::DimensionAssociation(association) = &object.data else {
return false;
};
association.dimension == dimension
&& association.associativity != 0
&& association.references.iter().flatten().any(|reference| {
reference
.xrefs
.iter()
.any(|source| document.get_entity(*source).is_some())
})
})
}
impl Scene {
pub(crate) fn attach_linear_dimension_association(
&mut self,
dimension: Handle,
sources: [Option<Handle>; 2],
) {
let Some(EntityType::Dimension(Dimension::Linear(linear))) =
self.document.get_entity(dimension)
else {
return;
};
let first_point = linear.first_point;
let second_point = linear.second_point;
let source_data = [first_point, second_point].map(|point| point);
let resolved: [Option<(Handle, i32)>; 2] = std::array::from_fn(|index| {
let source = sources[index]?;
let entity = self.document.get_entity(source)?;
source_marker(entity, source_data[index]).map(|marker| (source, marker))
});
if resolved.iter().all(Option::is_none) {
return;
}
let reference = |source: Handle, marker: i32, point: Vector3| AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type: 1,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_point: point,
..AssocDimensionReference::default()
};
let mut references: [Vec<AssocDimensionReference>; 4] =
std::array::from_fn(|_| Vec::new());
let mut associativity = 0;
for (index, resolved) in resolved.into_iter().enumerate() {
if let Some((source, marker)) = resolved {
associativity |= 1 << index;
references[index].push(reference(source, marker, source_data[index]));
}
}
let association_handle = self.document.allocate_handle();
let mut object = AssociativeObject::new("DIMASSOC", "AcDbDimAssoc");
object.handle = association_handle;
object.reactors.push(dimension);
object.data = AssociativeData::DimensionAssociation(AssocDimensionAssociation {
associativity,
dimension,
references,
..AssocDimensionAssociation::default()
});
self.document
.objects
.insert(association_handle, ObjectType::Associative(object));
let mut reactor_targets = vec![dimension];
reactor_targets.extend(sources.into_iter().flatten());
reactor_targets.sort_by_key(|handle| handle.value());
reactor_targets.dedup();
for handle in reactor_targets {
if let Some(entity) = self.document.get_entity_mut(handle) {
if !entity.common().reactors.contains(&association_handle) {
entity.common_mut().reactors.push(association_handle);
}
}
}
}
pub(crate) fn infer_linear_dimension_sources(
&self,
dimension: Handle,
) -> [Option<Handle>; 2] {
let Some(EntityType::Dimension(Dimension::Linear(linear))) =
self.document.get_entity(dimension)
else {
return [None, None];
};
[linear.first_point, linear.second_point].map(|point| {
self.document
.entities()
.filter(|entity| entity.common().handle != dimension)
.filter_map(|entity| {
source_points(entity)
.into_iter()
.map(|candidate| point_distance_squared(candidate, point))
.min_by(f64::total_cmp)
.map(|distance| (distance, entity.common().handle))
})
.filter(|(distance, _)| *distance <= 1e-16)
.min_by(|first, second| first.0.total_cmp(&second.0))
.map(|(_, handle)| handle)
})
}
pub(crate) fn refresh_associative_dimensions(
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
let changed: rustc_hash::FxHashSet<_> =
changes.iter().map(|(handle, _)| *handle).collect();
if changed.is_empty() {
return Vec::new();
}
let associations: Vec<_> = self
.document
.objects
.values()
.filter_map(|object| {
let ObjectType::Associative(object) = object else {
return None;
};
let AssociativeData::DimensionAssociation(association) = &object.data else {
return None;
};
association
.references
.iter()
.flatten()
.any(|reference| reference.xrefs.iter().any(|handle| changed.contains(handle)))
.then_some(association.clone())
})
.collect();
let mut refreshed = Vec::new();
for association in associations {
let first = association.references[0]
.first()
.and_then(|reference| resolve_reference(self, reference));
let second = association.references[1]
.first()
.and_then(|reference| resolve_reference(self, reference));
if first.is_none() && second.is_none() {
continue;
}
if let Some(EntityType::Dimension(Dimension::Linear(linear))) =
self.document.get_entity_mut(association.dimension)
{
if let Some(first) = first {
linear.first_point = first;
}
if let Some(second) = second {
linear.second_point = second;
}
linear.base.actual_measurement = linear.measurement();
linear.base.definition_point = linear.definition_point;
refreshed.push((association.dimension, ChangeKind::Modified));
}
}
refreshed
}
}

View file

@ -22,6 +22,7 @@ pub mod view;
mod boundary;
mod camera_ops;
pub(crate) mod centerline;
pub(crate) mod dimension_assoc;
pub(crate) mod centermark;
mod entity;
mod group_layer;
@ -2400,6 +2401,11 @@ impl Scene {
changes.push(change);
}
}
for change in self.refresh_associative_dimensions(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);
}
}
for change in self.refresh_associative_hatches(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);

View file

@ -16,6 +16,8 @@ use crate::t;
pub struct ColorExtras {
pub by_layer: bool,
pub by_block: bool,
pub none: bool,
pub background: bool,
}
/// Encode a colour as the ACI integer string the style editors store
@ -38,7 +40,7 @@ pub fn iced_to_acad_color(color: Color) -> AcadColor {
AcadColor::Rgb { r, g, b }
}
/// Return the closest AutoCAD Color Index for an RGB colour.
/// Return the closest CAD Color Index for an RGB colour.
pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
let mut best = 7;
let mut best_distance = u32::MAX;
@ -209,7 +211,27 @@ pub fn color_list<'a>(
.into()
};
let logical_row = |color: AcadColor, label: &'static str| -> Element<'a, Message> {
let (bg, _) = acad_color_display(color);
button(
row![swatch(bg), text(t!(label)).size(11)]
.spacing(5)
.align_y(iced::Center),
)
.on_press(on_select(color))
.style(list_row_style)
.padding([2, 4])
.width(Length::Fill)
.into()
};
let mut list = column![].spacing(1);
if extras.none {
list = list.push(named_row(AcadColor::None));
}
if extras.background {
list = list.push(logical_row(AcadColor::ByBlock, "Background"));
}
if extras.by_layer {
list = list.push(named_row(AcadColor::ByLayer));
}
@ -297,7 +319,7 @@ pub fn index_color_page<'a>(
.into()
};
// AutoCAD's first standard indexed colours.
// The first standard indexed CAD colours.
let mut standard = row![].spacing(4);
for idx in 1u8..=9 {
standard = standard.push(swatch_button(AcadColor::Index(idx), 22.0));
@ -402,7 +424,7 @@ pub fn index_color_page<'a>(
tabs,
text("Standard colors").size(11),
standard,
text("AutoCAD Color Index (ACI) 10249").size(11),
text("CAD Color Index (ACI) 10249").size(11),
text("Color family → · Shade / intensity ↓")
.size(9)

View file

@ -834,6 +834,7 @@ impl PropertiesPanel {
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
Message::PropBgColorChanged,
Message::PropBgColorPickerToggle,
@ -851,18 +852,35 @@ impl PropertiesPanel {
// entity's main colour. Used by hatch gradient colours and the dim-line
// colour override (Leader / Dimension). Dim colours legitimately take
// ByLayer / ByBlock; gradient colours do not.
if field == "gradient_color_1" || field == "gradient_color_2" || field == "dim_line_color" {
if matches!(
field,
"gradient_color_1"
| "gradient_color_2"
| "dim_line_color"
| "dim_ext_line_color"
| "dim_text_color"
| "dim_text_fill_color"
) {
let open = self.open_color_field.as_deref() == Some(field);
let fsel = field.to_string();
let extras = if field == "dim_line_color" {
let full_palette_field = field.to_string();
let extras = if field == "dim_text_fill_color" {
crate::ui::color_select::ColorExtras {
none: true,
background: true,
..Default::default()
}
} else if field.starts_with("dim_") {
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
}
} else {
crate::ui::color_select::ColorExtras {
by_layer: false,
by_block: false,
..Default::default()
}
};
let selector = crate::ui::color_select::color_selector(
@ -874,7 +892,10 @@ impl PropertiesPanel {
color: c,
},
Message::PropColorFieldToggle(field.to_string()),
Message::PropColorFieldToggle(field.to_string()),
Message::OpenColorWindow(
crate::app::ColorPickTarget::PropertiesField(full_palette_field),
color,
),
);
return prop_row_widget(label, selector);
}
@ -884,6 +905,7 @@ impl PropertiesPanel {
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
Message::PropColorChanged,
Message::PropColorPickerToggle,

View file

@ -1135,6 +1135,7 @@ impl Ribbon {
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
Message::RibbonColorChanged,
Message::OpenColorWindow(

View file

@ -518,6 +518,7 @@ pub fn view_window<'a>(
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
move |c| Message::DsEdit(f_sel.clone(), crate::ui::color_select::color_to_aci_string(c)),
Message::DsColorMore(fld.clone()),

View file

@ -115,7 +115,11 @@ fn color_row<'a>(
let selector = crate::ui::color_select::color_selector(
current,
open,
crate::ui::color_select::ColorExtras { by_layer: true, by_block: true },
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
move |color| Message::MLeaderStyleEdit {
field,
value: crate::ui::color_select::color_to_aci_string(color),

View file

@ -317,7 +317,11 @@ fn cell_editor<'a>(v: &TableStyleView<'a>, row_index: u8) -> Element<'a, Message
let selector = crate::ui::color_select::color_selector(
current,
v.color_open == Some((row_index, field)),
crate::ui::color_select::ColorExtras { by_layer: true, by_block: true },
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
..Default::default()
},
move |color| Message::TableStyleCellEdit {
row: row_index,
field,

View file

@ -412,6 +412,7 @@ fn editor_layer_row<'a>(
crate::ui::color_select::ColorExtras {
by_layer: false,
by_block: false,
..Default::default()
},
move |color| Message::LayerStateEditorLayerColor(index, color),
Message::LayerStateEditorLayerColorToggle(index),

View file

@ -813,6 +813,7 @@ fn layer_row<'a>(
crate::ui::color_select::ColorExtras {
by_layer: false,
by_block: false,
..Default::default()
},
Message::LayerColorSet,
Message::LayerColorPickerToggle(index),