2026-05-12 10:36:15 +03:00
|
|
|
use super::helpers::{entity_type_key, entity_type_label, title_case_word};
|
2026-05-23 17:16:15 +03:00
|
|
|
use super::{OpenCADStudio, VARIES_LABEL};
|
2026-06-26 23:06:40 +03:00
|
|
|
use crate::io::linetypes;
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::view::dispatch;
|
2026-03-22 22:44:32 +03:00
|
|
|
use crate::ui;
|
|
|
|
|
use acadrust::{EntityType, Handle};
|
|
|
|
|
|
2026-07-08 18:57:11 +02:00
|
|
|
/// Above this many selected objects the Properties panel skips per-entity
|
|
|
|
|
/// property aggregation (which is O(n) per row, plus an O(n²) group filter) and
|
|
|
|
|
/// shows a count-only summary instead. Bulk edits still go through the ribbon.
|
|
|
|
|
const MAX_PROP_AGGREGATE: usize = 2_000;
|
|
|
|
|
|
2026-07-26 22:30:14 +03:00
|
|
|
fn material_color_text(color: &acadrust::objects::MaterialColor) -> String {
|
|
|
|
|
let rgb = color
|
|
|
|
|
.rgb
|
|
|
|
|
.map(|value| format!("#{:06X}", value as u32 & 0x00ff_ffff))
|
|
|
|
|
.unwrap_or_else(|| "Object".to_string());
|
|
|
|
|
format!("{rgb}, factor {:.3}", color.factor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn material_map_text(map: &acadrust::objects::MaterialMap) -> String {
|
|
|
|
|
let source = match map.source {
|
|
|
|
|
1 => {
|
|
|
|
|
if map.file_name.trim().is_empty() {
|
|
|
|
|
"File".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
map.file_name.clone()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
2 => format!(
|
|
|
|
|
"Procedural {}",
|
|
|
|
|
map.texture.as_ref().map_or(0, |texture| texture.mode)
|
|
|
|
|
),
|
|
|
|
|
_ => "None".to_string(),
|
|
|
|
|
};
|
|
|
|
|
format!(
|
|
|
|
|
"{source}; blend {:.3}; projection {}; tiling {}; auto {}",
|
|
|
|
|
map.blend_factor, map.projection, map.tiling, map.auto_transform
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn visual_style_properties_text(style: &acadrust::objects::VisualStyle) -> String {
|
|
|
|
|
style
|
|
|
|
|
.properties
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(index, property)| {
|
|
|
|
|
format!(
|
|
|
|
|
"{}: {:?} (enabled {})",
|
|
|
|
|
index, property.value, property.enabled
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
impl OpenCADStudio {
|
2026-03-22 22:44:32 +03:00
|
|
|
/// Rebuild the PropertiesPanel from the current entity selection.
|
|
|
|
|
/// Preserves UI state (open pickers, edit buffer) across refreshes.
|
|
|
|
|
pub(super) fn refresh_properties(&mut self) {
|
|
|
|
|
let i = self.active_tab;
|
2026-07-26 23:38:38 +03:00
|
|
|
if !crate::entities::object_data::cache_is_prepared(
|
|
|
|
|
&self.tabs[i].scene.object_data_cache,
|
|
|
|
|
) {
|
|
|
|
|
self.tabs[i].scene.object_data_cache =
|
|
|
|
|
crate::entities::object_data::build_cache(&self.tabs[i].scene.document);
|
|
|
|
|
}
|
2026-06-16 04:02:46 +03:00
|
|
|
// Note: the color-picker dropdown is intentionally NOT carried over — a
|
|
|
|
|
// rebuild means the selection (or a property) changed, so the dropdown
|
|
|
|
|
// closes, matching the deselect / reselect / click-away expectation.
|
2026-03-22 22:44:32 +03:00
|
|
|
let color_palette_open = self.tabs[i].properties.color_palette_open;
|
|
|
|
|
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
|
2026-07-22 11:11:10 +03:00
|
|
|
// Expanded coordinate groups persist across rebuilds AND selection
|
|
|
|
|
// changes — it's a per-user view preference, not per-entity state.
|
|
|
|
|
let expanded_groups = std::mem::take(&mut self.tabs[i].properties.expanded_groups);
|
2026-07-01 07:42:50 +03:00
|
|
|
// Which entities the previous panel was built for — an uncommitted
|
|
|
|
|
// edit buffer only survives a rebuild for the *same* selection.
|
|
|
|
|
let prev_handles = std::mem::take(&mut self.tabs[i].properties.source_handles);
|
2026-03-22 22:44:32 +03:00
|
|
|
let selected_group = self.tabs[i].properties.selected_group.clone();
|
|
|
|
|
|
2026-05-20 05:43:55 +03:00
|
|
|
// Seed the per-thread unit context from the document header so the
|
|
|
|
|
// entity property builders (which only see f64 values) can format
|
|
|
|
|
// lengths/angles per LUNITS / LUPREC / AUNITS / AUPREC.
|
|
|
|
|
{
|
|
|
|
|
let h = &self.tabs[i].scene.document.header;
|
|
|
|
|
crate::entities::common::set_unit_context(crate::entities::common::UnitContext {
|
|
|
|
|
lunits: h.linear_unit_format,
|
|
|
|
|
luprec: h.linear_unit_precision,
|
|
|
|
|
aunits: h.angular_unit_format,
|
|
|
|
|
auprec: h.angular_unit_precision,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
let layer_names: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.layers
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
let linetype_items: Vec<ui::properties::LinetypeItem> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|lt| {
|
|
|
|
|
let name = if lt.name.is_empty() {
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
lt.name.clone()
|
|
|
|
|
};
|
|
|
|
|
let art = linetypes::extract_pattern(<.description);
|
|
|
|
|
ui::properties::LinetypeItem { name, art }
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let text_style_names: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|style| style.name.trim().to_string())
|
|
|
|
|
.filter(|name| !name.is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-07-08 17:53:05 +03:00
|
|
|
// Current-Vertex focus survives only while the same object stays
|
|
|
|
|
// selected; a changed selection resets to the first vertex. Seed the
|
|
|
|
|
// per-thread focus so the polyline property builder / editor targets it.
|
|
|
|
|
let cur_handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.selected_entities()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
.collect();
|
|
|
|
|
let prop_vertex = if cur_handles == prev_handles {
|
|
|
|
|
self.tabs[i].properties.prop_vertex
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
let new_panel = {
|
|
|
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|
|
|
|
let mut panel = match selected.len() {
|
2026-07-26 23:38:38 +03:00
|
|
|
0 => {
|
2026-07-27 17:37:17 +03:00
|
|
|
use crate::scene::model::object::{PropSection, PropValue, Property};
|
|
|
|
|
|
|
|
|
|
let tab = &self.tabs[i];
|
|
|
|
|
let scene = &tab.scene;
|
|
|
|
|
let doc = &scene.document;
|
|
|
|
|
let header = &doc.header;
|
|
|
|
|
let camera = scene.camera.borrow();
|
|
|
|
|
let (viewport_width, viewport_height) = scene.selection.borrow().vp_size;
|
|
|
|
|
let aspect = if viewport_height > 0.0 {
|
|
|
|
|
viewport_width as f64 / viewport_height as f64
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
let view_height = camera.ortho_size() as f64 * 2.0;
|
|
|
|
|
let view_width = view_height * aspect;
|
|
|
|
|
let format_length = crate::entities::common::format_length;
|
|
|
|
|
let read_only = |label: &str, value: String| Property {
|
|
|
|
|
label: label.to_string(),
|
|
|
|
|
field: "drawing_property",
|
|
|
|
|
value: PropValue::ReadOnly(value),
|
|
|
|
|
};
|
|
|
|
|
let current_layer = if header.current_layer_name.is_empty() {
|
|
|
|
|
tab.active_layer.clone()
|
|
|
|
|
} else {
|
|
|
|
|
header.current_layer_name.clone()
|
|
|
|
|
};
|
|
|
|
|
let current_linetype = if !header.current_linetype_name.is_empty() {
|
|
|
|
|
header.current_linetype_name.clone()
|
|
|
|
|
} else if !header.current_linetype_handle.is_null() {
|
|
|
|
|
doc.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|line_type| {
|
|
|
|
|
line_type.handle == header.current_linetype_handle
|
|
|
|
|
})
|
|
|
|
|
.map(|line_type| line_type.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string())
|
|
|
|
|
} else {
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
};
|
|
|
|
|
let material = if header.current_material_handle.is_null() {
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
doc.objects
|
|
|
|
|
.get(&header.current_material_handle)
|
|
|
|
|
.and_then(|object| match object {
|
|
|
|
|
acadrust::objects::ObjectType::Material(material) => {
|
|
|
|
|
Some(material.name.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string())
|
|
|
|
|
};
|
|
|
|
|
let plot_style = match header.current_plotstyle_type {
|
|
|
|
|
1 => "ByBlock",
|
|
|
|
|
2 => "ByColor",
|
|
|
|
|
3 => "ByObject",
|
|
|
|
|
_ => "ByLayer",
|
|
|
|
|
};
|
|
|
|
|
let layout_plot_table = doc.objects.values().find_map(|object| {
|
|
|
|
|
let acadrust::objects::ObjectType::Layout(layout) = object else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
(layout.name == scene.current_layout
|
|
|
|
|
&& !layout.plot_style_sheet.trim().is_empty())
|
|
|
|
|
.then(|| layout.plot_style_sheet.clone())
|
|
|
|
|
});
|
|
|
|
|
let plot_table = layout_plot_table
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
(!header.stylesheet.trim().is_empty())
|
|
|
|
|
.then(|| header.stylesheet.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "None".to_string());
|
|
|
|
|
let ucs_per_viewport = scene
|
|
|
|
|
.active_viewport
|
|
|
|
|
.and_then(|handle| doc.get_entity(handle))
|
|
|
|
|
.and_then(|entity| match entity {
|
|
|
|
|
acadrust::EntityType::Viewport(viewport) => {
|
|
|
|
|
Some(viewport.ucs_per_viewport)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
let ucs_name = tab
|
|
|
|
|
.active_ucs
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|ucs| ucs.name.trim())
|
|
|
|
|
.filter(|name| !name.is_empty())
|
|
|
|
|
.unwrap_or("World")
|
|
|
|
|
.to_string();
|
|
|
|
|
let annotation_scale = if header.current_annotation_scale.trim().is_empty() {
|
|
|
|
|
"1:1".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
header.current_annotation_scale.clone()
|
|
|
|
|
};
|
|
|
|
|
let sections = vec![
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "General".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
read_only("AC Version", doc.version.as_str().to_string()),
|
|
|
|
|
Property {
|
|
|
|
|
label: "Color".to_string(),
|
|
|
|
|
field: "color",
|
|
|
|
|
value: PropValue::ColorChoice(header.current_entity_color),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
|
|
|
|
label: "Layer".to_string(),
|
|
|
|
|
field: "layer",
|
|
|
|
|
value: PropValue::LayerChoice(current_layer),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
|
|
|
|
label: "Linetype".to_string(),
|
|
|
|
|
field: "linetype",
|
|
|
|
|
value: PropValue::LinetypeChoice(current_linetype),
|
|
|
|
|
},
|
|
|
|
|
read_only(
|
|
|
|
|
"Linetype scale",
|
|
|
|
|
format_length(header.current_entity_linetype_scale),
|
|
|
|
|
),
|
|
|
|
|
Property {
|
|
|
|
|
label: "Lineweight".to_string(),
|
|
|
|
|
field: "line_weight",
|
|
|
|
|
value: PropValue::LwChoice(
|
|
|
|
|
acadrust::types::LineWeight::from_value(
|
|
|
|
|
header.current_line_weight,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
read_only("Transparency", "ByLayer".to_string()),
|
|
|
|
|
read_only("Thickness", format_length(header.thickness)),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "3D Visualization".to_string(),
|
|
|
|
|
props: vec![read_only("Material", material)],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "Plot style".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
read_only("Plot style", plot_style.to_string()),
|
|
|
|
|
read_only("Plot style table", plot_table.clone()),
|
|
|
|
|
read_only(
|
|
|
|
|
"Plot table attached to",
|
|
|
|
|
if plot_table == "None" {
|
|
|
|
|
"None".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
scene.current_layout.clone()
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
|
|
|
|
"Plot table type",
|
|
|
|
|
if header.plotstyle_mode {
|
|
|
|
|
"Named plot styles".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
"Color-dependent plot styles".to_string()
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "View".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
read_only("Center X", format_length(camera.target.x)),
|
|
|
|
|
read_only("Center Y", format_length(camera.target.y)),
|
|
|
|
|
read_only("Center Z", format_length(camera.target.z)),
|
|
|
|
|
read_only("Height", format_length(view_height)),
|
|
|
|
|
read_only("Width", format_length(view_width)),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
|
|
|
|
title: "Misc".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
read_only("Annotation scale", annotation_scale),
|
|
|
|
|
read_only(
|
|
|
|
|
"UCS icon On",
|
|
|
|
|
if self.show_ucs_icon { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
|
|
|
|
"UCS icon at origin",
|
|
|
|
|
if self.ucs_icon_at_origin { "Yes" } else { "No" }
|
|
|
|
|
.to_string(),
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
|
|
|
|
"UCS per viewport",
|
|
|
|
|
if ucs_per_viewport { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
|
|
|
|
read_only("UCS Name", ucs_name),
|
|
|
|
|
read_only("Visual Style", tab.visual_style.clone()),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-07-26 23:38:38 +03:00
|
|
|
ui::PropertiesPanel {
|
2026-07-27 17:37:17 +03:00
|
|
|
title: "No selection".to_string(),
|
2026-07-26 23:38:38 +03:00
|
|
|
sections,
|
|
|
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|
|
|
|
linetype_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
linetype_items.clone(),
|
|
|
|
|
),
|
|
|
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
ui::properties::lw_options(),
|
|
|
|
|
),
|
|
|
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
crate::scene::model::hatch_patterns::names(),
|
|
|
|
|
),
|
|
|
|
|
linetype_items,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
1 => {
|
2026-07-26 22:30:14 +03:00
|
|
|
let (handle, source_entity) = selected[0];
|
|
|
|
|
let contextual = crate::scene::annotative::entity_for_active_context(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
source_entity,
|
|
|
|
|
);
|
|
|
|
|
let entity = contextual.as_ref();
|
2026-03-22 22:44:32 +03:00
|
|
|
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
|
|
|
|
let mut sections =
|
|
|
|
|
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
2026-03-31 08:18:29 +03:00
|
|
|
|
2026-07-12 23:16:26 +03:00
|
|
|
// Turn the Material row into an editable picker: the source
|
|
|
|
|
// options (ByLayer / ByBlock) plus every named material the
|
|
|
|
|
// drawing defines. A custom flag (3) shows the stored handle's
|
|
|
|
|
// material name as the selection.
|
2026-07-02 23:25:02 +03:00
|
|
|
{
|
2026-07-12 23:16:26 +03:00
|
|
|
let doc = &self.tabs[i].scene.document;
|
2026-07-02 23:25:02 +03:00
|
|
|
let common = entity.common();
|
2026-07-12 23:16:26 +03:00
|
|
|
let mat_names: Vec<String> = doc
|
|
|
|
|
.objects
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(_, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::Material(m) => Some(m.name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let selected = match common.material_flags {
|
|
|
|
|
0 => "ByLayer".to_string(),
|
|
|
|
|
1 => "ByBlock".to_string(),
|
|
|
|
|
_ => common
|
|
|
|
|
.material_handle
|
|
|
|
|
.and_then(|mh| {
|
|
|
|
|
doc.objects.iter().find_map(|(h, o)| match o {
|
2026-07-02 23:25:02 +03:00
|
|
|
acadrust::objects::ObjectType::Material(m) if *h == mh => {
|
|
|
|
|
Some(m.name.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
2026-07-12 23:16:26 +03:00
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string()),
|
|
|
|
|
};
|
|
|
|
|
let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
|
|
|
|
options.extend(mat_names);
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|p| p.field == "material")
|
|
|
|
|
{
|
|
|
|
|
row.value = crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected: selected.clone(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
};
|
2026-07-02 23:25:02 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-26 22:30:14 +03:00
|
|
|
let effective_handle = match common.material_flags {
|
|
|
|
|
3 => common.material_handle,
|
|
|
|
|
0 => doc
|
|
|
|
|
.layers
|
|
|
|
|
.get(&common.layer)
|
|
|
|
|
.map(|layer| layer.material)
|
|
|
|
|
.filter(|handle| handle.is_valid()),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
if let Some(acadrust::objects::ObjectType::Material(material)) =
|
|
|
|
|
effective_handle.and_then(|handle| doc.objects.get(&handle))
|
|
|
|
|
{
|
|
|
|
|
use crate::entities::common::ro_prop;
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
|
|
|
|
title: "Material Details".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
ro_prop("Name", "mat_name", material.name.clone()),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Description",
|
|
|
|
|
"mat_description",
|
|
|
|
|
material.description.clone(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Ambient",
|
|
|
|
|
"mat_ambient",
|
|
|
|
|
material_color_text(&material.ambient_color),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Diffuse",
|
|
|
|
|
"mat_diffuse",
|
|
|
|
|
material_color_text(&material.diffuse_color),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Specular",
|
|
|
|
|
"mat_specular",
|
|
|
|
|
material_color_text(&material.specular_color),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Gloss",
|
|
|
|
|
"mat_gloss",
|
|
|
|
|
format!("{:.3}", material.specular_gloss_factor),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Opacity",
|
|
|
|
|
"mat_opacity",
|
|
|
|
|
format!("{:.3}", material.opacity_percent),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Reflectivity",
|
|
|
|
|
"mat_reflectivity",
|
|
|
|
|
format!("{:.3}", material.reflectivity),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Translucence",
|
|
|
|
|
"mat_translucence",
|
|
|
|
|
format!("{:.3}", material.translucence),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Refraction",
|
|
|
|
|
"mat_refraction",
|
|
|
|
|
format!("{:.3}", material.refraction_index),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Self Illumination",
|
|
|
|
|
"mat_self_illumination",
|
|
|
|
|
format!("{:.3}", material.self_illumination),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Luminance",
|
|
|
|
|
"mat_luminance",
|
|
|
|
|
format!(
|
|
|
|
|
"{:.3} (mode {})",
|
|
|
|
|
material.luminance, material.luminance_mode
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Diffuse Map",
|
|
|
|
|
"mat_diffuse_map",
|
|
|
|
|
material_map_text(&material.diffuse_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Specular Map",
|
|
|
|
|
"mat_specular_map",
|
|
|
|
|
material_map_text(&material.specular_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Reflection Map",
|
|
|
|
|
"mat_reflection_map",
|
|
|
|
|
material_map_text(&material.reflection_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Opacity Map",
|
|
|
|
|
"mat_opacity_map",
|
|
|
|
|
material_map_text(&material.opacity_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Bump Map",
|
|
|
|
|
"mat_bump_map",
|
|
|
|
|
material_map_text(&material.bump_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Refraction Map",
|
|
|
|
|
"mat_refraction_map",
|
|
|
|
|
material_map_text(&material.refraction_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Normal Map",
|
|
|
|
|
"mat_normal_map",
|
|
|
|
|
material_map_text(&material.normal_map),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Render Flags",
|
|
|
|
|
"mat_render_flags",
|
|
|
|
|
format!(
|
|
|
|
|
"illumination {}; channels {}; mode {}; two-sided {}",
|
|
|
|
|
material.illumination_model,
|
|
|
|
|
material.channel_flags,
|
|
|
|
|
material.mode,
|
|
|
|
|
material.two_sided_material
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Advanced",
|
|
|
|
|
"mat_advanced",
|
|
|
|
|
format!(
|
|
|
|
|
"normal {:.3}; bump {:.3}; reflect {:.3}; transmit {:.3}",
|
|
|
|
|
material.normal_map_strength,
|
|
|
|
|
material.indirect_bump_scale,
|
|
|
|
|
material.reflectance_scale,
|
|
|
|
|
material.transmittance_scale
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
let common = entity.common();
|
|
|
|
|
if let Some(acadrust::objects::ObjectType::BookColor(book)) = common
|
|
|
|
|
.color_book_handle
|
|
|
|
|
.filter(|handle| handle.is_valid())
|
|
|
|
|
.and_then(|handle| doc.objects.get(&handle))
|
|
|
|
|
{
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|row| row.field == "color")
|
|
|
|
|
{
|
|
|
|
|
row.value =
|
|
|
|
|
crate::scene::model::object::PropValue::ColorChoice(
|
|
|
|
|
book.color,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
use crate::entities::common::ro_prop;
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
|
|
|
|
title: "Color Book".to_string(),
|
|
|
|
|
props: vec![
|
|
|
|
|
ro_prop("Book", "book_color_book", book.book_name.clone()),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Color Name",
|
|
|
|
|
"book_color_name",
|
|
|
|
|
book.color_name.clone(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Color",
|
|
|
|
|
"book_color_value",
|
|
|
|
|
format!("{:?}", book.color),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let style_handles = [
|
|
|
|
|
("Full", common.full_visual_style_handle),
|
|
|
|
|
("Face", common.face_visual_style_handle),
|
|
|
|
|
("Edge", common.edge_visual_style_handle),
|
|
|
|
|
];
|
|
|
|
|
for (scope, handle) in style_handles {
|
|
|
|
|
let Some((handle, style)) = handle
|
|
|
|
|
.filter(|handle| handle.is_valid())
|
|
|
|
|
.and_then(|handle| {
|
|
|
|
|
doc.objects.get(&handle).and_then(|object| match object {
|
|
|
|
|
acadrust::objects::ObjectType::VisualStyle(style) => {
|
|
|
|
|
Some((handle, style))
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
use crate::entities::common::ro_prop;
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
|
|
|
|
title: format!("{scope} Visual Style"),
|
|
|
|
|
props: vec![
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Handle",
|
|
|
|
|
"vs_handle",
|
|
|
|
|
format!("{:X}", handle.value()),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Description",
|
|
|
|
|
"vs_description",
|
|
|
|
|
style.description.clone(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Type",
|
|
|
|
|
"vs_type",
|
|
|
|
|
style.style_type.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Face",
|
|
|
|
|
"vs_face",
|
|
|
|
|
format!(
|
|
|
|
|
"lighting {}; quality {}; color {}; modifiers {}",
|
|
|
|
|
style.face_lighting_model,
|
|
|
|
|
style.face_lighting_quality,
|
|
|
|
|
style.face_color_mode,
|
|
|
|
|
style.face_modifier
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Edges",
|
|
|
|
|
"vs_edges",
|
|
|
|
|
format!(
|
|
|
|
|
"model {}; style {}",
|
|
|
|
|
style.edge_model, style.edge_style
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Extended Lighting",
|
|
|
|
|
"vs_extended_lighting",
|
|
|
|
|
style.extended_lighting_model.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Internal",
|
|
|
|
|
"vs_internal",
|
|
|
|
|
style.internal_use_only.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Property Bag",
|
|
|
|
|
"vs_properties",
|
|
|
|
|
visual_style_properties_text(style),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matches!(
|
|
|
|
|
entity,
|
|
|
|
|
acadrust::EntityType::Solid3D(_)
|
|
|
|
|
| acadrust::EntityType::Region(_)
|
|
|
|
|
| acadrust::EntityType::Body(_)
|
|
|
|
|
| acadrust::EntityType::Surface(_)
|
|
|
|
|
| acadrust::EntityType::Mesh(_)
|
|
|
|
|
| acadrust::EntityType::PolygonMesh(_)
|
|
|
|
|
| acadrust::EntityType::PolyfaceMesh(_)
|
|
|
|
|
) {
|
|
|
|
|
if let Some(mesh) = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.meshes
|
|
|
|
|
.get(&handle)
|
|
|
|
|
.or_else(|| self.tabs[i].scene.block_meshes.get(&handle))
|
|
|
|
|
{
|
|
|
|
|
use crate::entities::common::ro_prop;
|
|
|
|
|
let metrics = mesh.metrics;
|
|
|
|
|
let mut props = vec![
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Vertices",
|
|
|
|
|
"mesh_vertices",
|
|
|
|
|
metrics.vertices.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Triangles",
|
|
|
|
|
"mesh_triangles",
|
|
|
|
|
metrics.triangles.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Surface Area",
|
|
|
|
|
"mesh_surface_area",
|
|
|
|
|
format!("{:.6}", metrics.surface_area),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Centroid",
|
|
|
|
|
"mesh_centroid",
|
|
|
|
|
format!(
|
|
|
|
|
"{:.6}, {:.6}, {:.6}",
|
|
|
|
|
metrics.centroid[0],
|
|
|
|
|
metrics.centroid[1],
|
|
|
|
|
metrics.centroid[2]
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Tessellation",
|
|
|
|
|
"mesh_complete",
|
|
|
|
|
if mesh.complete { "Complete" } else { "Partial" },
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
if mesh.complete
|
|
|
|
|
&& matches!(
|
|
|
|
|
entity,
|
|
|
|
|
acadrust::EntityType::Solid3D(_)
|
|
|
|
|
| acadrust::EntityType::Region(_)
|
|
|
|
|
| acadrust::EntityType::Body(_)
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
props.insert(
|
|
|
|
|
3,
|
|
|
|
|
ro_prop(
|
|
|
|
|
"Volume",
|
|
|
|
|
"mesh_volume",
|
|
|
|
|
format!("{:.6}", metrics.volume),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
|
|
|
|
title: "Mass Properties".to_string(),
|
|
|
|
|
props,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-07-02 23:25:02 +03:00
|
|
|
}
|
|
|
|
|
|
2026-07-26 23:38:38 +03:00
|
|
|
sections.extend(crate::entities::object_data::sections(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&self.tabs[i].scene.object_data_cache,
|
|
|
|
|
handle,
|
|
|
|
|
entity,
|
|
|
|
|
));
|
|
|
|
|
|
2026-07-21 10:27:50 +03:00
|
|
|
// Uniform-scale checkbox for block references (#427):
|
|
|
|
|
// while the three scale factors are equal (and the user
|
|
|
|
|
// hasn't opted into per-axis editing) the panel shows one
|
|
|
|
|
// Scale row plus a checked "Uniform scale" box; unchecking
|
|
|
|
|
// expands the familiar Scale X/Y/Z rows.
|
|
|
|
|
if let acadrust::EntityType::Insert(ins) = entity {
|
|
|
|
|
let eq = (ins.x_scale() - ins.y_scale()).abs() < 1e-12
|
|
|
|
|
&& (ins.x_scale() - ins.z_scale()).abs() < 1e-12;
|
|
|
|
|
let uniform =
|
|
|
|
|
eq && !self.props_asym_scale.contains(&handle.value());
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
let Some(xi) =
|
|
|
|
|
section.props.iter().position(|p| p.field == "x_scale")
|
|
|
|
|
else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let toggle = crate::scene::model::object::Property {
|
|
|
|
|
label: "Uniform scale".into(),
|
|
|
|
|
field: "ins_uniform",
|
|
|
|
|
value: crate::scene::model::object::PropValue::BoolToggle {
|
|
|
|
|
field: "ins_uniform",
|
|
|
|
|
value: uniform,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if uniform {
|
|
|
|
|
section
|
|
|
|
|
.props
|
|
|
|
|
.retain(|p| p.field != "y_scale" && p.field != "z_scale");
|
|
|
|
|
let xi = section
|
|
|
|
|
.props
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|p| p.field == "x_scale")
|
|
|
|
|
.unwrap_or(xi.min(section.props.len()));
|
|
|
|
|
section.props[xi] = crate::entities::common::edit_prop(
|
|
|
|
|
"Scale",
|
|
|
|
|
"u_scale",
|
|
|
|
|
ins.x_scale(),
|
|
|
|
|
);
|
|
|
|
|
section.props.insert(xi, toggle);
|
|
|
|
|
} else {
|
|
|
|
|
section.props.insert(xi, toggle);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 23:16:26 +03:00
|
|
|
// In named plot-style mode (PSTYLEMODE=1) the Plot style row
|
|
|
|
|
// picks a named style from the drawing's plot style table. In
|
|
|
|
|
// the color-dependent mode it stays read-only (the object's
|
|
|
|
|
// color drives the plot style), so it is left untouched.
|
|
|
|
|
if self.tabs[i].scene.document.header.plotstyle_mode {
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
let doc = &self.tabs[i].scene.document;
|
2026-07-12 23:16:26 +03:00
|
|
|
let dict_h = doc.header.acad_plotstylename_dict_handle;
|
|
|
|
|
if let Some(dict) = crate::scene::annotative::as_dict(doc, dict_h) {
|
|
|
|
|
let common = entity.common();
|
|
|
|
|
let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
|
|
|
|
options.extend(dict.entries.iter().map(|(n, _)| n.clone()));
|
|
|
|
|
let selected = match common.plotstyle_flags {
|
|
|
|
|
0 => "ByLayer".to_string(),
|
|
|
|
|
1 => "ByBlock".to_string(),
|
|
|
|
|
_ => common
|
|
|
|
|
.plotstyle_handle
|
|
|
|
|
.and_then(|ph| {
|
|
|
|
|
dict.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, h)| *h == ph)
|
|
|
|
|
.map(|(n, _)| n.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string()),
|
|
|
|
|
};
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
2026-07-12 23:16:26 +03:00
|
|
|
section.props.iter_mut().find(|p| p.field == "plot_style")
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
{
|
2026-07-12 23:16:26 +03:00
|
|
|
row.value = crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected: selected.clone(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
};
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Turn the MLEADER handle-backed rows (multileader style, text
|
|
|
|
|
// style, arrowhead block, leader linetype) into editable name
|
|
|
|
|
// pickers. The current handle resolves to a display name; the
|
|
|
|
|
// options list every candidate the drawing offers. Applying a
|
|
|
|
|
// pick resolves the name back to a handle in the update loop.
|
|
|
|
|
if let acadrust::EntityType::MultiLeader(ml) = entity {
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
// Option lists.
|
|
|
|
|
let mleader_styles: Vec<String> = doc
|
|
|
|
|
.objects
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(_, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(s) => {
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
Some(s.name.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
2026-07-12 23:16:26 +03:00
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
// A leader with no linetype handle draws ByBlock; expose that
|
|
|
|
|
// as the first option so the default is selectable.
|
|
|
|
|
let ltype_names: Vec<String> = std::iter::once("ByBlock".to_string())
|
|
|
|
|
.chain(
|
|
|
|
|
doc.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
.filter(|n| !n.is_empty()),
|
|
|
|
|
)
|
|
|
|
|
.collect();
|
|
|
|
|
// Arrowheads are blocks; the closed-filled default has no
|
|
|
|
|
// block, so seed the list with it and add the arrowhead
|
|
|
|
|
// blocks (leading underscore) present in the drawing.
|
|
|
|
|
let arrow_names: Vec<String> = std::iter::once("Closed filled".to_string())
|
|
|
|
|
.chain(
|
|
|
|
|
doc.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|b| b.name.starts_with('_'))
|
|
|
|
|
.map(|b| b.name.clone()),
|
|
|
|
|
)
|
|
|
|
|
.collect();
|
|
|
|
|
let tstyle_names = text_style_names.clone();
|
|
|
|
|
// Currently selected names.
|
|
|
|
|
let cur_style = ml
|
|
|
|
|
.style_handle
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
doc.objects.iter().find_map(|(oh, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(s)
|
|
|
|
|
if *oh == h =>
|
|
|
|
|
{
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
|
|
|
let cur_tstyle = ml
|
|
|
|
|
.text_style_handle
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
doc.text_styles.iter().find(|s| s.handle == h).map(|s| s.name.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
|
|
|
let cur_arrow = ml
|
|
|
|
|
.arrowhead_handle
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
doc.block_records.iter().find(|b| b.handle == h).map(|b| b.name.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "Closed filled".to_string());
|
|
|
|
|
let cur_ltype = ml
|
|
|
|
|
.line_type_handle
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
doc.line_types.iter().find(|l| l.handle == h).map(|l| l.name.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "ByBlock".to_string());
|
|
|
|
|
let mut set_choice =
|
|
|
|
|
|field: &str, selected: String, options: Vec<String>| {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|p| p.field == field)
|
|
|
|
|
{
|
|
|
|
|
row.value =
|
|
|
|
|
crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected: selected.clone(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
set_choice("mleader_style", cur_style, mleader_styles);
|
|
|
|
|
set_choice("text_style_handle", cur_tstyle, tstyle_names);
|
|
|
|
|
set_choice("arrowhead_handle", cur_arrow, arrow_names);
|
|
|
|
|
set_choice("line_type_handle", cur_ltype, ltype_names);
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:30:45 +03:00
|
|
|
// Inject viewport-only properties that require doc access.
|
2026-03-31 08:18:29 +03:00
|
|
|
if let acadrust::EntityType::Viewport(vp) = entity {
|
|
|
|
|
let frozen_names: Vec<String> = vp
|
|
|
|
|
.frozen_layers
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|&h| {
|
2026-05-12 10:36:15 +03:00
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.layers
|
|
|
|
|
.iter()
|
2026-03-31 08:18:29 +03:00
|
|
|
.find(|l| l.handle == h)
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-04-01 00:30:45 +03:00
|
|
|
|
|
|
|
|
// Collect available UCS names for the name picker.
|
2026-05-12 10:36:15 +03:00
|
|
|
let ucs_names: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.ucss
|
2026-04-01 00:30:45 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Current UCS name (resolved from vp.ucs_handle).
|
2026-05-12 10:36:15 +03:00
|
|
|
let current_ucs = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.ucss
|
2026-04-01 00:30:45 +03:00
|
|
|
.iter()
|
|
|
|
|
.find(|u| u.handle == vp.ucs_handle)
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
// Collect available named view names.
|
2026-05-12 10:36:15 +03:00
|
|
|
let view_names: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.views
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|v| v.name.clone())
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
|
2026-03-31 08:18:29 +03:00
|
|
|
if let Some(geom) = sections.last_mut() {
|
2026-06-16 11:33:46 +03:00
|
|
|
geom.props.push(crate::scene::model::object::Property {
|
2026-03-31 08:18:29 +03:00
|
|
|
label: "Frozen Layers".to_string(),
|
|
|
|
|
field: "frozen_layers",
|
2026-06-16 11:33:46 +03:00
|
|
|
value: crate::scene::model::object::PropValue::EditText(
|
2026-03-31 08:18:29 +03:00
|
|
|
frozen_names.join(", "),
|
|
|
|
|
),
|
|
|
|
|
});
|
2026-04-01 00:30:45 +03:00
|
|
|
if !ucs_names.is_empty() {
|
2026-06-16 11:33:46 +03:00
|
|
|
geom.props.push(crate::scene::model::object::Property {
|
2026-04-01 00:30:45 +03:00
|
|
|
label: "UCS Name".to_string(),
|
|
|
|
|
field: "vp_ucs_name",
|
2026-06-16 11:33:46 +03:00
|
|
|
value: crate::scene::model::object::PropValue::Choice {
|
2026-04-01 00:30:45 +03:00
|
|
|
selected: current_ucs,
|
|
|
|
|
options: ucs_names,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
if !view_names.is_empty() {
|
2026-06-16 11:33:46 +03:00
|
|
|
geom.props.push(crate::scene::model::object::Property {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
label: "Named View".to_string(),
|
|
|
|
|
field: "vp_named_view",
|
2026-06-16 11:33:46 +03:00
|
|
|
value: crate::scene::model::object::PropValue::Choice {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
selected: String::new(),
|
|
|
|
|
options: view_names,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-31 08:18:29 +03:00
|
|
|
}
|
2026-06-04 16:18:01 +03:00
|
|
|
|
|
|
|
|
// Drive the viewport scale picker from the drawing's
|
|
|
|
|
// own scale list instead of a built-in set.
|
|
|
|
|
let file_scales = self.tabs[i].scene.scale_list();
|
|
|
|
|
if !file_scales.is_empty() {
|
|
|
|
|
let eff = crate::scene::vp_effective_scale(
|
|
|
|
|
vp.custom_scale,
|
|
|
|
|
vp.view_height,
|
|
|
|
|
vp.height,
|
|
|
|
|
);
|
|
|
|
|
let selected = file_scales
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, _, f)| (f - eff).abs() < 0.001 * f.max(0.001))
|
|
|
|
|
.map(|(n, _, _)| n.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let options: Vec<String> =
|
|
|
|
|
file_scales.iter().map(|(n, _, _)| n.clone()).collect();
|
|
|
|
|
if let Some(geom) = sections.last_mut() {
|
|
|
|
|
if let Some(prop) =
|
|
|
|
|
geom.props.iter_mut().find(|p| p.field == "vscale_std")
|
|
|
|
|
{
|
2026-06-16 11:33:46 +03:00
|
|
|
prop.value = crate::scene::model::object::PropValue::Choice {
|
2026-06-04 16:18:01 +03:00
|
|
|
selected,
|
|
|
|
|
options,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:18:29 +03:00
|
|
|
}
|
|
|
|
|
|
feat(properties): dimension style-derived property groups
Resolve a dimension's DimStyle from the document and inject its full spec
groups — Lines & Arrows, Text, Fit, Primary Units, Alternate Units,
Tolerances — mirroring the ~80 dimension variables (dimasz, dimclrd, dimtxt,
dimdec, dimtol, dimzin bit flags, …) as read-only rows. The panel already had
doc access for the dim-style picker; extend it to append style_sections(style)
after the dimension's own Misc/Geometry groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:17:54 +03:00
|
|
|
// Inject DimStyle picker + style-derived groups for Dimensions.
|
|
|
|
|
if let acadrust::EntityType::Dimension(d) = entity {
|
2026-05-12 10:36:15 +03:00
|
|
|
let dim_style_names: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.dim_styles
|
2026-04-02 19:06:20 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
if !dim_style_names.is_empty() {
|
|
|
|
|
// Current style is already shown as EditText in the geom section;
|
|
|
|
|
// replace/upgrade it to a Choice if we have a list.
|
|
|
|
|
if let Some(geom) = sections.last_mut() {
|
|
|
|
|
// Find and replace the style_name EditText with a Choice.
|
2026-05-12 10:36:15 +03:00
|
|
|
if let Some(prop) =
|
|
|
|
|
geom.props.iter_mut().find(|p| p.field == "style_name")
|
|
|
|
|
{
|
2026-04-02 19:06:20 +03:00
|
|
|
let current = match &prop.value {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::model::object::PropValue::EditText(s) => s.clone(),
|
2026-04-02 19:06:20 +03:00
|
|
|
_ => String::new(),
|
|
|
|
|
};
|
2026-06-16 11:33:46 +03:00
|
|
|
prop.value = crate::scene::model::object::PropValue::Choice {
|
2026-04-02 19:06:20 +03:00
|
|
|
selected: current,
|
|
|
|
|
options: dim_style_names,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(properties): dimension style-derived property groups
Resolve a dimension's DimStyle from the document and inject its full spec
groups — Lines & Arrows, Text, Fit, Primary Units, Alternate Units,
Tolerances — mirroring the ~80 dimension variables (dimasz, dimclrd, dimtxt,
dimdec, dimtol, dimzin bit flags, …) as read-only rows. The panel already had
doc access for the dim-style picker; extend it to append style_sections(style)
after the dimension's own Misc/Geometry groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:17:54 +03:00
|
|
|
|
|
|
|
|
// Append the resolved dimension style's groups
|
|
|
|
|
// (Lines & Arrows, Text, Fit, Units, Tolerances).
|
|
|
|
|
let style_name = d.base().style_name.clone();
|
|
|
|
|
if let Some(style) =
|
|
|
|
|
self.tabs[i].scene.document.dim_styles.iter().find(|s| {
|
|
|
|
|
s.name.eq_ignore_ascii_case(&style_name)
|
|
|
|
|
|| (style_name.trim().is_empty()
|
|
|
|
|
&& s.name.eq_ignore_ascii_case("Standard"))
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
sections.extend(crate::entities::dimension::style_sections(style));
|
2026-07-13 17:31:09 +03:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
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)
|
|
|
|
|
.unwrap_or_else(|| acadrust::types::Color::from_index(style.dimclrd));
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"dim_line_color",
|
|
|
|
|
PropValue::ColorChoice(dim_c),
|
|
|
|
|
);
|
feat(properties): dimension style-derived property groups
Resolve a dimension's DimStyle from the document and inject its full spec
groups — Lines & Arrows, Text, Fit, Primary Units, Alternate Units,
Tolerances — mirroring the ~80 dimension variables (dimasz, dimclrd, dimtxt,
dimdec, dimtol, dimzin bit flags, …) as read-only rows. The panel already had
doc access for the dim-style picker; extend it to append style_sections(style)
after the dimension's own Misc/Geometry groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:17:54 +03:00
|
|
|
}
|
2026-04-02 19:06:20 +03:00
|
|
|
}
|
|
|
|
|
|
2026-07-03 00:05:58 +03:00
|
|
|
// ── Doc-dependent property rows ──────────────────────────
|
|
|
|
|
// Rows whose value lives on another object (a block record,
|
|
|
|
|
// an underlay definition, a dimension / multileader style)
|
|
|
|
|
// are left empty by the entity builders and resolved here,
|
|
|
|
|
// where the document is reachable.
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
match entity {
|
|
|
|
|
// Block reference: the referenced block's units and the
|
|
|
|
|
// unit-scale factor against the drawing's INSUNITS.
|
|
|
|
|
acadrust::EntityType::Insert(ins) => {
|
|
|
|
|
let host = doc.header.insertion_units;
|
|
|
|
|
let src = doc
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&ins.block_name)
|
|
|
|
|
.map(|br| br.units)
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
set_row(&mut sections, "block_unit", insunits_name(src).to_string());
|
|
|
|
|
let host_mm = if host == 0 { 1.0 } else { insunits_to_mm(host) };
|
|
|
|
|
let src_mm = if src == 0 { 1.0 } else { insunits_to_mm(src) };
|
|
|
|
|
let factor = if host_mm.abs() > 1e-12 { src_mm / host_mm } else { 1.0 };
|
|
|
|
|
set_row(&mut sections, "unit_factor", format!("{factor:.4}"));
|
2026-07-22 11:11:10 +03:00
|
|
|
|
|
|
|
|
// Name row: editable for regular blocks — pick an
|
|
|
|
|
// existing definition to re-point this reference, or
|
|
|
|
|
// type a new name to rename the definition (every
|
|
|
|
|
// insert of it follows). Anonymous (*) and
|
|
|
|
|
// xref(-dependent) blocks keep the read-only row.
|
|
|
|
|
let regular = |br: &acadrust::tables::BlockRecord| {
|
|
|
|
|
!br.is_anonymous()
|
|
|
|
|
&& !br.flags.is_xref
|
|
|
|
|
&& !br.name.contains('|')
|
|
|
|
|
};
|
|
|
|
|
let editable = doc
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&ins.block_name)
|
|
|
|
|
.map(®ular)
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
if editable {
|
|
|
|
|
let mut options: Vec<String> = doc
|
|
|
|
|
.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|br| regular(br))
|
|
|
|
|
.map(|br| br.name.clone())
|
|
|
|
|
.collect();
|
|
|
|
|
options.sort_by(|a, b| {
|
|
|
|
|
a.to_lowercase().cmp(&b.to_lowercase())
|
|
|
|
|
});
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"block",
|
|
|
|
|
crate::scene::model::object::PropValue::EditChoice {
|
|
|
|
|
value: ins.block_name.clone(),
|
|
|
|
|
options,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
|
|
|
|
// Underlay: name + path from the referenced definition.
|
|
|
|
|
acadrust::EntityType::Underlay(ul) => {
|
|
|
|
|
if let Some((name, path)) =
|
|
|
|
|
doc.objects.iter().find_map(|(h, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::UnderlayDefinition(def)
|
|
|
|
|
if *h == ul.definition_handle =>
|
|
|
|
|
{
|
|
|
|
|
let nm = if !def.name.is_empty() {
|
|
|
|
|
def.name.clone()
|
|
|
|
|
} else {
|
|
|
|
|
def.page_name.clone()
|
|
|
|
|
};
|
|
|
|
|
Some((nm, def.file_path.clone()))
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
set_row(&mut sections, "ul_name", name);
|
|
|
|
|
set_row(&mut sections, "ul_path", path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Leader: text style / vertical text placement / overall
|
|
|
|
|
// scale come from its dimension style.
|
|
|
|
|
acadrust::EntityType::Leader(ld) => {
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
// Dim style row → dropdown of the drawing's dim styles.
|
|
|
|
|
let names: Vec<String> = doc
|
|
|
|
|
.dim_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
.filter(|n| !n.is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
if !names.is_empty() {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(p) = section
|
|
|
|
|
.props
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|p| p.field == "dimension_style")
|
|
|
|
|
{
|
|
|
|
|
let cur = match &p.value {
|
|
|
|
|
crate::scene::model::object::PropValue::EditText(s) => {
|
|
|
|
|
s.clone()
|
|
|
|
|
}
|
|
|
|
|
_ => ld.dimension_style.clone(),
|
|
|
|
|
};
|
|
|
|
|
p.value = crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected: cur,
|
|
|
|
|
options: names.clone(),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
}
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
// Lines & Arrows / Text / Fit default to the assigned
|
|
|
|
|
// dimension style (the same source the tessellator
|
|
|
|
|
// uses); most rows are editable per-object overrides
|
|
|
|
|
// stored in ACAD_DSTYLE. Arrow size / block / overall
|
|
|
|
|
// scale and dim-line lineweight drive the render; text
|
|
|
|
|
// offset / vertical position round-trip to file but
|
|
|
|
|
// don't change the leader glyph here (its annotation
|
|
|
|
|
// is a separate entity). Dim-line colour is read-only.
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
if let Some(ds) = find_dim_style(doc, &ld.dimension_style) {
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
use crate::entities::dim_override as dov;
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
|
|
|
|
let xd = &ld.common.extended_data;
|
|
|
|
|
|
|
|
|
|
// Arrow block (DIMLDRBLK): override handle → name,
|
|
|
|
|
// else the style's arrow. Options are the arrowhead
|
|
|
|
|
// blocks the drawing carries plus the default.
|
|
|
|
|
let arrow_label = match dov::handle(xd, dov::DIMLDRBLK) {
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.handle == h)
|
|
|
|
|
.map(|b| arrowhead_label(&b.name))
|
|
|
|
|
.unwrap_or_else(|| "Closed filled".to_string()),
|
|
|
|
|
None => leader_arrow_label(doc, ds, ld.arrow_enabled),
|
|
|
|
|
};
|
|
|
|
|
let mut arrow_opts: Vec<String> =
|
|
|
|
|
std::iter::once("Closed filled".to_string())
|
|
|
|
|
.chain(
|
|
|
|
|
doc.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|b| b.name.starts_with('_'))
|
|
|
|
|
.map(|b| arrowhead_label(&b.name)),
|
|
|
|
|
)
|
|
|
|
|
.collect();
|
|
|
|
|
// Keep the current value selectable even when it
|
|
|
|
|
// isn't in the standard list (e.g. "None", or a
|
|
|
|
|
// style arrow whose block isn't underscore-named).
|
|
|
|
|
if !arrow_opts.contains(&arrow_label) {
|
|
|
|
|
arrow_opts.insert(0, arrow_label.clone());
|
|
|
|
|
}
|
|
|
|
|
set_row_value(
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"arrow_block",
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: arrow_label,
|
|
|
|
|
options: arrow_opts,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let asz = dov::real(xd, dov::DIMASZ).unwrap_or(ds.dimasz);
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"arrow_size",
|
|
|
|
|
PropValue::EditText(format!("{asz:.4}")),
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
);
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
|
|
|
|
let lwd = dov::int(xd, dov::DIMLWD).unwrap_or(ds.dimlwd);
|
|
|
|
|
let lw_sel = dim_lineweight_label(lwd);
|
|
|
|
|
let mut lw_opts = lineweight_options();
|
|
|
|
|
if !lw_opts.contains(&lw_sel) {
|
|
|
|
|
lw_opts.insert(0, lw_sel.clone());
|
|
|
|
|
}
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"dim_line_lw",
|
|
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: lw_sel,
|
|
|
|
|
options: lw_opts,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-13 17:31:09 +03:00
|
|
|
// Dim-line colour: a per-object ACAD_DSTYLE
|
|
|
|
|
// override (code 176, an ACI index) wins over the
|
|
|
|
|
// style's DIMCLRD. Editable — the picked colour is
|
|
|
|
|
// written back as that override so it round-trips.
|
|
|
|
|
let dim_c = dov::color(xd, dov::DIMCLRD)
|
|
|
|
|
.unwrap_or_else(|| acadrust::types::Color::from_index(ds.dimclrd));
|
|
|
|
|
set_row_value(
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"dim_line_color",
|
2026-07-13 17:31:09 +03:00
|
|
|
PropValue::ColorChoice(dim_c),
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
);
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
|
|
|
|
let gap = dov::real(xd, dov::DIMGAP).unwrap_or(ds.dimgap);
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"text_offset",
|
|
|
|
|
PropValue::EditText(format!("{gap:.4}")),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let tad = dov::int(xd, dov::DIMTAD).unwrap_or(ds.dimtad);
|
|
|
|
|
set_row_value(
|
2026-07-03 00:05:58 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"text_pos_vert",
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: dimtad_label(tad).to_string(),
|
|
|
|
|
options: tad_options(),
|
|
|
|
|
},
|
2026-07-03 00:05:58 +03:00
|
|
|
);
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
|
|
|
|
|
let scl = dov::real(xd, dov::DIMSCALE).unwrap_or(ds.dimscale);
|
|
|
|
|
set_row_value(
|
2026-07-03 00:05:58 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"dim_scale_overall",
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
PropValue::EditText(format!("{scl:.4}")),
|
2026-07-03 00:05:58 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Feature-control frame: FCF text style is the dimension
|
|
|
|
|
// style's DIMTXSTY.
|
|
|
|
|
acadrust::EntityType::Tolerance(tol) => {
|
|
|
|
|
if let Some(ds) = find_dim_style(doc, &tol.dimension_style_name) {
|
|
|
|
|
if !ds.dimtxsty.is_empty() {
|
|
|
|
|
set_row(&mut sections, "tol_text_style", ds.dimtxsty.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// MultiLeader: max points + segment-angle constraints
|
|
|
|
|
// are MLeaderStyle settings, not stored on the entity.
|
|
|
|
|
acadrust::EntityType::MultiLeader(ml) => {
|
|
|
|
|
if let Some(sh) = ml.style_handle {
|
|
|
|
|
if let Some((mx, a1, a2)) =
|
|
|
|
|
doc.objects.iter().find_map(|(h, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(s)
|
|
|
|
|
if *h == sh =>
|
|
|
|
|
{
|
|
|
|
|
Some((
|
|
|
|
|
s.max_leader_points,
|
|
|
|
|
s.first_segment_angle,
|
|
|
|
|
s.second_segment_angle,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
set_row(&mut sections, "max_leader_points", mx.to_string());
|
|
|
|
|
set_row(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"first_segment_angle",
|
|
|
|
|
format!("{a1:.4}"),
|
|
|
|
|
);
|
|
|
|
|
set_row(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"second_segment_angle",
|
|
|
|
|
format!("{a2:.4}"),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 19:26:11 +03:00
|
|
|
// 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.
|
|
|
|
|
{
|
2026-07-12 23:16:26 +03:00
|
|
|
// Which entities show an Annotative row, the field it uses,
|
|
|
|
|
// and — for those that don't already carry the row
|
|
|
|
|
// (dimension / table) — the existing field to insert it
|
|
|
|
|
// after. MLeader uses its editable toggle field.
|
|
|
|
|
let anno: Option<(&str, Option<&str>)> = match entity {
|
|
|
|
|
acadrust::EntityType::Text(_)
|
|
|
|
|
| acadrust::EntityType::MText(_)
|
2026-07-13 22:38:29 +03:00
|
|
|
| acadrust::EntityType::Insert(_)
|
2026-07-12 23:16:26 +03:00
|
|
|
| acadrust::EntityType::Leader(_) => Some(("annotative", None)),
|
|
|
|
|
acadrust::EntityType::MultiLeader(_) => {
|
|
|
|
|
Some(("enable_annotation_scale", None))
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Dimension(_) => {
|
|
|
|
|
Some(("annotative", Some("style_name")))
|
2026-07-12 19:26:11 +03:00
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
acadrust::EntityType::Table(_) => {
|
|
|
|
|
Some(("annotative", Some("tbl_style_handle")))
|
2026-07-12 19:26:11 +03:00
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
2026-07-12 23:16:26 +03:00
|
|
|
if let Some((anno_field, insert_after)) = anno {
|
|
|
|
|
let is_anno = crate::scene::annotative::is_annotative(doc, entity);
|
|
|
|
|
// Dimensions/tables carry no Annotative row yet — add one
|
|
|
|
|
// right after their style row.
|
|
|
|
|
if let Some(anchor) = insert_after {
|
|
|
|
|
insert_row_after(
|
|
|
|
|
&mut sections,
|
|
|
|
|
anchor,
|
|
|
|
|
crate::entities::common::ro_prop(
|
|
|
|
|
"Annotative",
|
|
|
|
|
"annotative",
|
|
|
|
|
"No",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-13 22:38:29 +03:00
|
|
|
// Objects that carry a per-object annotation context
|
|
|
|
|
// (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.
|
2026-07-12 19:26:11 +03:00
|
|
|
if anno_field == "annotative" {
|
2026-07-13 22:38:29 +03:00
|
|
|
match entity {
|
|
|
|
|
acadrust::EntityType::MText(t) => set_row_value(
|
2026-07-13 09:32:22 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"annotative",
|
|
|
|
|
crate::scene::model::object::PropValue::BoolToggle {
|
|
|
|
|
field: "is_annotative",
|
|
|
|
|
value: t.is_annotative,
|
|
|
|
|
},
|
2026-07-13 22:38:29 +03:00
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Text(_)
|
|
|
|
|
| acadrust::EntityType::Insert(_) => set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"annotative",
|
|
|
|
|
crate::scene::model::object::PropValue::BoolToggle {
|
|
|
|
|
field: "annotative_ctx",
|
|
|
|
|
value: is_anno,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
_ => set_row(
|
2026-07-13 09:32:22 +03:00
|
|
|
&mut sections,
|
|
|
|
|
"annotative",
|
|
|
|
|
if is_anno { "Yes" } else { "No" }.to_string(),
|
2026-07-13 22:38:29 +03:00
|
|
|
),
|
2026-07-13 09:32:22 +03:00
|
|
|
}
|
2026-07-12 19:26:11 +03:00
|
|
|
}
|
|
|
|
|
if is_anno {
|
2026-07-12 23:16:26 +03:00
|
|
|
// The applied annotation scale follows the current
|
|
|
|
|
// annotation scale (CANNOSCALE / the status-bar
|
|
|
|
|
// scale pill), not a per-object stored value.
|
2026-07-12 19:26:11 +03:00
|
|
|
insert_row_after(
|
|
|
|
|
&mut sections,
|
|
|
|
|
anno_field,
|
|
|
|
|
crate::entities::common::ro_prop(
|
|
|
|
|
"Annotative scale",
|
|
|
|
|
"annotative_scale",
|
2026-07-12 23:16:26 +03:00
|
|
|
doc.header.current_annotation_scale.clone(),
|
2026-07-12 19:26:11 +03:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
if !group_names.is_empty() {
|
|
|
|
|
let label = group_names.join(", ");
|
|
|
|
|
if let Some(general) = sections.first_mut() {
|
2026-06-16 11:33:46 +03:00
|
|
|
general.props.push(crate::scene::model::object::Property {
|
2026-03-22 22:44:32 +03:00
|
|
|
label: "Group".to_string(),
|
|
|
|
|
field: "group",
|
2026-06-16 11:33:46 +03:00
|
|
|
value: crate::scene::model::object::PropValue::ReadOnly(label),
|
2026-03-22 22:44:32 +03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 09:19:57 +03:00
|
|
|
let title = match entity {
|
|
|
|
|
acadrust::EntityType::Insert(ins) => {
|
|
|
|
|
let is_xref = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|br| br.name == ins.block_name)
|
|
|
|
|
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
if is_xref {
|
|
|
|
|
"External Reference".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
entity_type_label(entity)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => entity_type_label(entity),
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
ui::PropertiesPanel {
|
|
|
|
|
choice_combos: sections
|
|
|
|
|
.iter()
|
|
|
|
|
.flat_map(|section| section.props.iter())
|
|
|
|
|
.filter_map(|prop| match &prop.value {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::model::object::PropValue::Choice { options, .. } => Some((
|
2026-03-22 22:44:32 +03:00
|
|
|
prop.field.to_string(),
|
|
|
|
|
iced::widget::combo_box::State::new(options.clone()),
|
|
|
|
|
)),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
sections,
|
|
|
|
|
title,
|
|
|
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|
|
|
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
|
|
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::model::hatch_patterns::names(),
|
2026-03-22 22:44:32 +03:00
|
|
|
),
|
|
|
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
ui::properties::lw_options(),
|
|
|
|
|
),
|
|
|
|
|
linetype_items,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-08 18:57:11 +02:00
|
|
|
// Property aggregation is O(n) per row plus an O(n²) group filter
|
|
|
|
|
// (`group.handles.contains` scans per entity), stalling the rebuild
|
|
|
|
|
// for seconds at tens of thousands of objects. Above the cap show a
|
|
|
|
|
// count-only panel; bulk edits still go through the ribbon.
|
|
|
|
|
n if n > MAX_PROP_AGGREGATE => ui::PropertiesPanel {
|
|
|
|
|
title: format!("{} objects selected", n),
|
|
|
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|
|
|
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
|
|
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
ui::properties::lw_options(),
|
|
|
|
|
),
|
|
|
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
crate::scene::model::hatch_patterns::names(),
|
|
|
|
|
),
|
|
|
|
|
linetype_items,
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
_ => {
|
|
|
|
|
let groups = build_selection_groups(&selected);
|
|
|
|
|
let active_group = selected_group
|
|
|
|
|
.and_then(|group| groups.iter().find(|g| g.label == group.label).cloned())
|
|
|
|
|
.or_else(|| groups.first().cloned());
|
|
|
|
|
|
|
|
|
|
let filtered: Vec<(Handle, &EntityType)> = active_group
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|group| {
|
|
|
|
|
selected
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(handle, _)| group.handles.contains(handle))
|
|
|
|
|
.copied()
|
|
|
|
|
.collect()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let sections = aggregate_sections(&filtered, &text_style_names);
|
|
|
|
|
ui::PropertiesPanel {
|
|
|
|
|
choice_combos: sections
|
|
|
|
|
.iter()
|
|
|
|
|
.flat_map(|section| section.props.iter())
|
|
|
|
|
.filter_map(|prop| match &prop.value {
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::model::object::PropValue::Choice { options, .. } => Some((
|
2026-03-22 22:44:32 +03:00
|
|
|
prop.field.to_string(),
|
|
|
|
|
iced::widget::combo_box::State::new(options.clone()),
|
|
|
|
|
)),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
sections,
|
|
|
|
|
title: format!("{} objects selected", selected.len()),
|
|
|
|
|
selection_group_combo: iced::widget::combo_box::State::new(groups.clone()),
|
|
|
|
|
selection_groups: groups,
|
|
|
|
|
selected_group: active_group,
|
|
|
|
|
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
|
|
|
|
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
|
|
|
|
|
hatch_pattern_combo: iced::widget::combo_box::State::new(
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::model::hatch_patterns::names(),
|
2026-03-22 22:44:32 +03:00
|
|
|
),
|
|
|
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
ui::properties::lw_options(),
|
|
|
|
|
),
|
|
|
|
|
linetype_items,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
panel.color_palette_open = color_palette_open;
|
2026-07-01 07:42:50 +03:00
|
|
|
let new_handles: Vec<acadrust::Handle> = selected.iter().map(|(h, _)| *h).collect();
|
|
|
|
|
// Carry the in-progress edits only when the selection is unchanged
|
|
|
|
|
// (a commit-triggered rebuild); a genuine selection change starts
|
|
|
|
|
// with a clean buffer so no stale value leaks onto the new entity.
|
|
|
|
|
panel.edit_buf = if prev_handles == new_handles {
|
|
|
|
|
edit_buf
|
|
|
|
|
} else {
|
|
|
|
|
Default::default()
|
|
|
|
|
};
|
2026-07-22 11:11:10 +03:00
|
|
|
panel.expanded_groups = expanded_groups;
|
2026-07-01 07:42:50 +03:00
|
|
|
panel.source_handles = new_handles;
|
2026-07-08 17:53:05 +03:00
|
|
|
panel.prop_vertex = prop_vertex;
|
2026-03-22 22:44:32 +03:00
|
|
|
panel
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.tabs[i].properties = new_panel;
|
|
|
|
|
self.refresh_selected_grips();
|
2026-05-19 13:40:16 +03:00
|
|
|
self.sync_ribbon_from_selection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Drive the Home-ribbon Layer / Color / Linetype / Lineweight dropdowns
|
|
|
|
|
/// from the current entity selection. With no selection the ribbon falls
|
|
|
|
|
/// back to the active creation defaults (per-tab active_layer + ByLayer).
|
|
|
|
|
/// Mixed selections keep the prior value (we'd need a UI "*Varies*"
|
|
|
|
|
/// marker to do better).
|
|
|
|
|
pub(super) fn sync_ribbon_from_selection(&mut self) {
|
|
|
|
|
let i = self.active_tab;
|
2026-06-12 01:46:17 +03:00
|
|
|
// The Start (welcome) tab has no document — keep the ribbon's
|
|
|
|
|
// current-layer chip empty rather than re-seeding it with a default.
|
|
|
|
|
if self.tabs[i].is_start {
|
|
|
|
|
self.ribbon.active_layer = String::new();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-19 13:40:16 +03:00
|
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if selected.is_empty() {
|
2026-05-20 04:47:15 +03:00
|
|
|
// Creation defaults: prefer the file's saved CECOLOR / CELTYPE /
|
|
|
|
|
// CELWEIGHT (and current_layer_name); fall back to ByLayer when
|
|
|
|
|
// those slots are still at their factory default.
|
|
|
|
|
let header = &self.tabs[i].scene.document.header;
|
|
|
|
|
let layer = if header.current_layer_name.is_empty() {
|
|
|
|
|
self.tabs[i].active_layer.clone()
|
|
|
|
|
} else {
|
|
|
|
|
header.current_layer_name.clone()
|
|
|
|
|
};
|
2026-05-19 13:40:16 +03:00
|
|
|
self.ribbon.active_layer = layer;
|
2026-05-20 04:47:15 +03:00
|
|
|
self.ribbon.active_color = header.current_entity_color;
|
|
|
|
|
// current_linetype_name may be empty when only the handle was
|
|
|
|
|
// written; resolve via line_types table in that case.
|
|
|
|
|
let lt = if !header.current_linetype_name.is_empty() {
|
|
|
|
|
header.current_linetype_name.clone()
|
|
|
|
|
} else if !header.current_linetype_handle.is_null() {
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|lt| lt.handle == header.current_linetype_handle)
|
|
|
|
|
.map(|lt| lt.name.clone())
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string())
|
|
|
|
|
} else {
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
};
|
|
|
|
|
self.ribbon.active_linetype = lt;
|
|
|
|
|
self.ribbon.active_lineweight =
|
|
|
|
|
acadrust::types::LineWeight::from_value(header.current_line_weight);
|
2026-05-19 13:40:16 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut layer: Option<String> = None;
|
|
|
|
|
let mut color: Option<acadrust::types::Color> = None;
|
|
|
|
|
let mut linetype: Option<String> = None;
|
|
|
|
|
let mut lineweight: Option<acadrust::types::LineWeight> = None;
|
|
|
|
|
let mut layer_mixed = false;
|
|
|
|
|
let mut color_mixed = false;
|
|
|
|
|
let mut linetype_mixed = false;
|
|
|
|
|
let mut lineweight_mixed = false;
|
|
|
|
|
|
|
|
|
|
for (_h, e) in &selected {
|
|
|
|
|
let c = e.common();
|
|
|
|
|
let lt = if c.linetype.is_empty() {
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
c.linetype.clone()
|
|
|
|
|
};
|
|
|
|
|
match &layer {
|
|
|
|
|
None => layer = Some(c.layer.clone()),
|
|
|
|
|
Some(prev) if prev != &c.layer => layer_mixed = true,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
match &color {
|
|
|
|
|
None => color = Some(c.color),
|
|
|
|
|
Some(prev) if prev != &c.color => color_mixed = true,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
match &linetype {
|
|
|
|
|
None => linetype = Some(lt),
|
|
|
|
|
Some(prev) if prev != < => linetype_mixed = true,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
match &lineweight {
|
|
|
|
|
None => lineweight = Some(c.line_weight),
|
|
|
|
|
Some(prev) if prev != &c.line_weight => lineweight_mixed = true,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !layer_mixed {
|
|
|
|
|
if let Some(l) = layer {
|
|
|
|
|
self.ribbon.active_layer = l;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !color_mixed {
|
|
|
|
|
if let Some(c) = color {
|
|
|
|
|
self.ribbon.active_color = c;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !linetype_mixed {
|
|
|
|
|
if let Some(l) = linetype {
|
|
|
|
|
self.ribbon.active_linetype = l;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !lineweight_mixed {
|
|
|
|
|
if let Some(lw) = lineweight {
|
|
|
|
|
self.ribbon.active_lineweight = lw;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rebuild the cached selected_grips from the current entity selection.
|
|
|
|
|
pub(super) fn refresh_selected_grips(&mut self) {
|
|
|
|
|
let i = self.active_tab;
|
2026-05-01 12:16:46 +03:00
|
|
|
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
|
|
|
|
// Paper-space entity coordinates are NOT offset by world_offset (same rule
|
|
|
|
|
// as wire tessellation in wires_for_block). Only subtract in model space.
|
2026-05-12 10:36:15 +03:00
|
|
|
let wo = if is_paper {
|
|
|
|
|
[0.0f64; 3]
|
|
|
|
|
} else {
|
2026-06-23 22:42:33 +03:00
|
|
|
[0.0_f64; 3]
|
2026-05-12 10:36:15 +03:00
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
let (new_handle, new_grips) = {
|
|
|
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if selected.len() == 1 {
|
|
|
|
|
let (handle, entity) = selected[0];
|
2026-07-26 22:30:14 +03:00
|
|
|
let contextual = crate::scene::annotative::entity_for_active_context(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
entity,
|
|
|
|
|
);
|
|
|
|
|
let grips = dispatch::grips(contextual.as_ref())
|
2026-04-27 18:27:46 +03:00
|
|
|
.into_iter()
|
|
|
|
|
.map(|mut g| {
|
2026-06-11 00:04:37 +03:00
|
|
|
// Subtract in f64: at UTM magnitudes an f32 cast before
|
|
|
|
|
// the offset costs ~1 unit and draws the grip off the
|
|
|
|
|
// wire.
|
|
|
|
|
g.world.x -= wo[0];
|
|
|
|
|
g.world.y -= wo[1];
|
|
|
|
|
g.world.z -= wo[2];
|
2026-04-27 18:27:46 +03:00
|
|
|
g
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-03-22 22:44:32 +03:00
|
|
|
(Some(handle), grips)
|
|
|
|
|
} else {
|
|
|
|
|
(None, vec![])
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.tabs[i].selected_handle = new_handle;
|
|
|
|
|
self.tabs[i].selected_grips = new_grips;
|
2026-06-11 20:11:19 +03:00
|
|
|
// Append the dynamic-block visibility (lookup) grip, if the lone
|
|
|
|
|
// selection is a visibility-parametric block reference.
|
|
|
|
|
self.refresh_visibility_grip(wo);
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn property_target_handles(&self, i: usize) -> Vec<Handle> {
|
|
|
|
|
let handles = self.tabs[i].properties.selected_handles();
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
handles
|
|
|
|
|
} else {
|
|
|
|
|
self.tabs[i].selected_handle.into_iter().collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:53:55 +03:00
|
|
|
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
|
2026-07-26 22:30:14 +03:00
|
|
|
let mut context_object_changed = false;
|
2026-06-08 22:53:55 +03:00
|
|
|
for &handle in handles {
|
2026-07-26 22:30:14 +03:00
|
|
|
context_object_changed |=
|
|
|
|
|
crate::scene::annotative::sync_active_context_from_entity(
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
handle,
|
|
|
|
|
);
|
2026-07-21 12:22:15 +03:00
|
|
|
// Hatch / SOLID fills render from prebuilt cached models; rebuild
|
|
|
|
|
// them or pattern edits (scale, background, …) stay invisible
|
|
|
|
|
// (#415).
|
|
|
|
|
self.tabs[i].scene.refresh_fill_model(handle);
|
2026-06-08 22:53:55 +03:00
|
|
|
}
|
2026-07-26 22:30:14 +03:00
|
|
|
if context_object_changed {
|
|
|
|
|
self.tabs[i].scene.poison_undo_recording();
|
|
|
|
|
}
|
2026-06-21 02:12:58 +03:00
|
|
|
// Solid (ACIS) meshes bake their colour into the mesh, so a colour /
|
|
|
|
|
// layer change needs an explicit recolour — re-tessellating wires
|
|
|
|
|
// alone wouldn't update them.
|
2026-07-24 23:25:33 +03:00
|
|
|
self.tabs[i].scene.recolor_meshes_for_handles(handles);
|
|
|
|
|
let changes: Vec<_> = handles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
|
|
|
|
.collect();
|
|
|
|
|
self.tabs[i].scene.bump_entities(&changes);
|
2026-06-08 22:53:55 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
/// Add an entity to the correct space (model or paper space layout).
|
2026-05-31 14:44:18 +03:00
|
|
|
pub(super) fn commit_entity(&mut self, entity: acadrust::EntityType) {
|
|
|
|
|
let _ = self.commit_entity_handle(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Like [`commit_entity`] but returns the handle the new entity was given
|
|
|
|
|
/// (or `None` if it could not be added). Lets callers follow up — e.g.
|
|
|
|
|
/// open the in-place text editor on a freshly created MultiLeader.
|
|
|
|
|
pub(super) fn commit_entity_handle(
|
|
|
|
|
&mut self,
|
|
|
|
|
mut entity: acadrust::EntityType,
|
|
|
|
|
) -> Option<Handle> {
|
2026-03-22 22:44:32 +03:00
|
|
|
let i = self.active_tab;
|
|
|
|
|
let layer = &self.tabs[i].active_layer;
|
|
|
|
|
if layer != "0" || entity.as_entity().layer().is_empty() {
|
|
|
|
|
entity.as_entity_mut().set_layer(layer.clone());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 16:05:54 +03:00
|
|
|
// A new dimension adopts the current dimension style (DIMSTYLE), like
|
|
|
|
|
// AutoCAD — the DIM commands leave the default "Standard" on the entity,
|
|
|
|
|
// so stamp the header's current style here. ADDSELECTED sets DIMSTYLE to
|
|
|
|
|
// the template's first, so a cloned dimension keeps its style (#239).
|
|
|
|
|
if let acadrust::EntityType::Dimension(ref mut d) = entity {
|
|
|
|
|
let cur = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.header
|
|
|
|
|
.current_dimstyle_name
|
|
|
|
|
.clone();
|
|
|
|
|
if !cur.trim().is_empty() {
|
|
|
|
|
d.base_mut().style_name = cur;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 05:43:55 +03:00
|
|
|
// INSUNITS: when inserting a block whose BlockRecord.units differ
|
|
|
|
|
// from the host's header.insertion_units, scale the new INSERT so
|
|
|
|
|
// 1 source-unit equals the matching host length. When either side
|
|
|
|
|
// is unitless (0) AutoCAD falls back to MEASUREMENT (0 = Imperial /
|
|
|
|
|
// inches, 1 = Metric / mm); honour the same fallback.
|
|
|
|
|
if let acadrust::EntityType::Insert(ref mut ins) = entity {
|
|
|
|
|
let header = &self.tabs[i].scene.document.header;
|
|
|
|
|
let measurement_fallback = if header.measurement == 1 { 4 } else { 1 };
|
|
|
|
|
let host_raw = header.insertion_units;
|
|
|
|
|
let host_units = if host_raw == 0 { measurement_fallback } else { host_raw };
|
|
|
|
|
let src_raw = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&ins.block_name)
|
|
|
|
|
.map(|br| br.units)
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
let src_units = if src_raw == 0 { measurement_fallback } else { src_raw };
|
|
|
|
|
if src_units != host_units {
|
|
|
|
|
let ratio = insunits_to_mm(src_units) / insunits_to_mm(host_units);
|
|
|
|
|
if ratio.is_finite() && (ratio - 1.0).abs() > 1e-9 {
|
|
|
|
|
ins.set_x_scale(ratio);
|
|
|
|
|
ins.set_y_scale(ratio);
|
|
|
|
|
ins.set_z_scale(ratio);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
|
|
|
|
crate::scene::view::dispatch::apply_common_prop(
|
2026-03-22 22:44:32 +03:00
|
|
|
&mut entity,
|
|
|
|
|
"linetype",
|
|
|
|
|
&self.ribbon.active_linetype.clone(),
|
|
|
|
|
);
|
2026-06-16 11:33:46 +03:00
|
|
|
crate::scene::view::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight);
|
2026-05-20 04:47:15 +03:00
|
|
|
// CELTSCALE (header.current_entity_linetype_scale): new entities
|
|
|
|
|
// pick up the document's saved per-entity linetype scale. The user
|
|
|
|
|
// can override per entity later via the properties panel.
|
|
|
|
|
let celtscale = self.tabs[i].scene.document.header.current_entity_linetype_scale;
|
|
|
|
|
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
|
|
|
|
|
entity.common_mut().linetype_scale = celtscale;
|
|
|
|
|
}
|
2026-06-14 01:15:52 +03:00
|
|
|
|
|
|
|
|
// A new dimension inherits the document's current dimension style
|
|
|
|
|
// (DIMSTYLE) instead of staying at the entity "Standard" default. Only
|
|
|
|
|
// fill in when still at the default so an explicitly-styled dimension
|
|
|
|
|
// is preserved. See #92.
|
|
|
|
|
if let acadrust::EntityType::Dimension(ref mut d) = entity {
|
|
|
|
|
let cur = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.header
|
|
|
|
|
.current_dimstyle_name
|
|
|
|
|
.clone();
|
|
|
|
|
let s = d.base().style_name.clone();
|
|
|
|
|
if (s.is_empty() || s.eq_ignore_ascii_case("Standard")) && !cur.is_empty() {
|
|
|
|
|
d.base_mut().style_name = cur;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-14 01:19:00 +03:00
|
|
|
|
|
|
|
|
// MultiLeader / Table inherit the document's current style (#92). These
|
|
|
|
|
// styles live in the objects dictionary, so resolve the current style
|
|
|
|
|
// name to its object handle. Left untouched when the command already
|
|
|
|
|
// assigned a style or no matching style object exists.
|
|
|
|
|
match &mut entity {
|
|
|
|
|
acadrust::EntityType::MultiLeader(ml) if ml.style_handle.is_none() => {
|
|
|
|
|
let name = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.header
|
|
|
|
|
.current_mleader_style_name
|
|
|
|
|
.clone();
|
|
|
|
|
if !name.is_empty() {
|
2026-06-14 09:59:12 +03:00
|
|
|
let found =
|
|
|
|
|
self.tabs[i].scene.document.objects.iter().find_map(|(h, o)| match o {
|
|
|
|
|
acadrust::objects::ObjectType::MultiLeaderStyle(s)
|
|
|
|
|
if s.name.eq_ignore_ascii_case(&name) =>
|
|
|
|
|
{
|
|
|
|
|
Some((*h, s.clone()))
|
2026-06-14 01:19:00 +03:00
|
|
|
}
|
2026-06-14 09:59:12 +03:00
|
|
|
_ => None,
|
2026-06-14 01:19:00 +03:00
|
|
|
});
|
2026-06-14 09:59:12 +03:00
|
|
|
if let Some((h, s)) = found {
|
|
|
|
|
ml.style_handle = Some(h);
|
|
|
|
|
// Inherit the style's settings so a new multileader
|
|
|
|
|
// reflects the current MLeaderStyle (the renderer reads
|
|
|
|
|
// these entity fields). See #94.
|
|
|
|
|
// The entity and style enums are distinct types with
|
|
|
|
|
// matching discriminants — round-trip through i16.
|
|
|
|
|
ml.content_type = (s.content_type as i16).into();
|
|
|
|
|
ml.path_type = (s.path_type as i16).into();
|
|
|
|
|
ml.line_color = s.line_color;
|
|
|
|
|
ml.line_type_handle = s.line_type_handle;
|
|
|
|
|
ml.line_weight = s.line_weight;
|
|
|
|
|
ml.enable_landing = s.enable_landing;
|
|
|
|
|
ml.enable_dogleg = s.enable_dogleg;
|
|
|
|
|
ml.dogleg_length = s.landing_distance;
|
|
|
|
|
ml.arrowhead_handle = s.arrowhead_handle;
|
|
|
|
|
ml.arrowhead_size = s.arrowhead_size;
|
|
|
|
|
ml.text_style_handle = s.text_style_handle;
|
|
|
|
|
ml.text_color = s.text_color;
|
|
|
|
|
ml.text_frame = s.text_frame;
|
|
|
|
|
ml.text_height = s.text_height;
|
|
|
|
|
ml.context.text_height = s.text_height;
|
|
|
|
|
ml.text_left_attachment = (s.text_left_attachment as i16).into();
|
|
|
|
|
ml.text_right_attachment = (s.text_right_attachment as i16).into();
|
|
|
|
|
ml.text_top_attachment = (s.text_top_attachment as i16).into();
|
|
|
|
|
ml.text_bottom_attachment = (s.text_bottom_attachment as i16).into();
|
|
|
|
|
ml.text_attachment_direction =
|
|
|
|
|
(s.text_attachment_direction as i16).into();
|
|
|
|
|
ml.text_alignment = (s.text_alignment as i16).into();
|
|
|
|
|
ml.text_angle_type = (s.text_angle_type as i16).into();
|
|
|
|
|
ml.block_content_handle = s.block_content_handle;
|
|
|
|
|
ml.block_content_color = s.block_content_color;
|
|
|
|
|
ml.block_connection_type = (s.block_content_connection as i16).into();
|
|
|
|
|
ml.block_rotation = s.block_content_rotation;
|
|
|
|
|
ml.block_scale = acadrust::types::Vector3::new(
|
|
|
|
|
s.block_content_scale_x,
|
|
|
|
|
s.block_content_scale_y,
|
|
|
|
|
s.block_content_scale_z,
|
|
|
|
|
);
|
|
|
|
|
ml.scale_factor = s.scale_factor;
|
|
|
|
|
}
|
2026-06-14 01:19:00 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Table(t) if t.table_style_handle.is_none() => {
|
|
|
|
|
let name = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.header
|
|
|
|
|
.current_table_style_name
|
|
|
|
|
.clone();
|
|
|
|
|
if !name.is_empty() {
|
|
|
|
|
t.table_style_handle =
|
|
|
|
|
self.tabs[i].scene.document.objects.iter().find_map(|(h, o)| {
|
|
|
|
|
match o {
|
|
|
|
|
acadrust::objects::ObjectType::TableStyle(s)
|
|
|
|
|
if s.name.eq_ignore_ascii_case(&name) =>
|
|
|
|
|
{
|
|
|
|
|
Some(*h)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-05-01 21:00:27 +03:00
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
if matches!(&entity, acadrust::EntityType::Viewport(_))
|
|
|
|
|
&& self.tabs[i].scene.current_layout != "Model"
|
|
|
|
|
{
|
2026-03-31 07:27:03 +03:00
|
|
|
// Assign a unique viewport ID (max existing id + 1, min 2).
|
|
|
|
|
if let acadrust::EntityType::Viewport(ref mut vp) = entity {
|
|
|
|
|
let layout_block = self.tabs[i].scene.current_layout_block_handle_pub();
|
|
|
|
|
let max_id = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.entities()
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
if let acadrust::EntityType::Viewport(v) = e {
|
2026-05-12 10:36:15 +03:00
|
|
|
if v.common.owner_handle == layout_block {
|
|
|
|
|
Some(v.id)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-03-31 07:27:03 +03:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.max()
|
|
|
|
|
.unwrap_or(1);
|
|
|
|
|
vp.id = (max_id + 1).max(2);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
let layout = self.tabs[i].scene.current_layout.clone();
|
|
|
|
|
match self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.add_entity_to_layout(entity, &layout)
|
|
|
|
|
{
|
2026-03-31 07:33:11 +03:00
|
|
|
Ok(new_handle) => {
|
2026-07-24 23:25:33 +03:00
|
|
|
if self.tabs[i].scene.is_recording_undo() {
|
|
|
|
|
self.tabs[i].scene.record_undo_before(new_handle, None);
|
|
|
|
|
self.tabs[i].scene.poison_undo_recording();
|
|
|
|
|
}
|
2026-03-31 07:33:11 +03:00
|
|
|
self.tabs[i].scene.auto_fit_viewport(new_handle);
|
2026-06-25 18:55:20 +03:00
|
|
|
// Adding a viewport straight onto the document layout
|
|
|
|
|
// bypasses Scene::add_entity, which is what normally
|
|
|
|
|
// invalidates the wire-tessellation cache. Without this the
|
|
|
|
|
// new viewport's border isn't tessellated until the next
|
|
|
|
|
// zoom/pan forces a rebuild.
|
|
|
|
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
2026-05-31 14:44:18 +03:00
|
|
|
Some(new_handle)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error(&format!("Viewport could not be added: {e}"));
|
|
|
|
|
None
|
2026-03-31 07:33:11 +03:00
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-05-31 14:44:18 +03:00
|
|
|
Some(self.tabs[i].scene.add_entity(entity))
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Multi-selection property aggregation ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
pub(super) fn build_selection_groups(
|
|
|
|
|
selected: &[(Handle, &EntityType)],
|
|
|
|
|
) -> Vec<ui::properties::SelectionGroup> {
|
|
|
|
|
let mut groups = vec![ui::properties::SelectionGroup {
|
|
|
|
|
label: format!("All({})", selected.len()),
|
|
|
|
|
handles: selected.iter().map(|(handle, _)| *handle).collect(),
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
let mut by_type: std::collections::BTreeMap<String, Vec<Handle>> =
|
|
|
|
|
std::collections::BTreeMap::new();
|
|
|
|
|
for (handle, entity) in selected {
|
|
|
|
|
by_type
|
|
|
|
|
.entry(entity_type_key(entity))
|
|
|
|
|
.or_default()
|
|
|
|
|
.push(*handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (kind, handles) in by_type {
|
|
|
|
|
groups.push(ui::properties::SelectionGroup {
|
|
|
|
|
label: format!("{}({})", title_case_word(&kind), handles.len()),
|
|
|
|
|
handles,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groups
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn aggregate_sections(
|
|
|
|
|
selected: &[(Handle, &EntityType)],
|
|
|
|
|
text_style_names: &[String],
|
2026-06-16 11:33:46 +03:00
|
|
|
) -> Vec<crate::scene::model::object::PropSection> {
|
2026-03-22 22:44:32 +03:00
|
|
|
if selected.is_empty() {
|
|
|
|
|
return vec![];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
let mut all_sections: Vec<Vec<crate::scene::model::object::PropSection>> = selected
|
2026-03-22 22:44:32 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut result = all_sections.remove(0);
|
|
|
|
|
for sections in all_sections {
|
|
|
|
|
result = merge_sections(&result, §ions);
|
|
|
|
|
}
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn merge_sections(
|
2026-06-16 11:33:46 +03:00
|
|
|
left: &[crate::scene::model::object::PropSection],
|
|
|
|
|
right: &[crate::scene::model::object::PropSection],
|
|
|
|
|
) -> Vec<crate::scene::model::object::PropSection> {
|
2026-03-22 22:44:32 +03:00
|
|
|
left.iter()
|
|
|
|
|
.filter_map(|section| {
|
2026-05-12 10:36:15 +03:00
|
|
|
let rhs = right
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|candidate| candidate.title == section.title)?;
|
2026-06-16 11:33:46 +03:00
|
|
|
let props: Vec<crate::scene::model::object::Property> = section
|
2026-03-22 22:44:32 +03:00
|
|
|
.props
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|prop| {
|
2026-05-12 10:36:15 +03:00
|
|
|
let other = rhs
|
|
|
|
|
.props
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|candidate| candidate.field == prop.field)?;
|
2026-06-16 11:33:46 +03:00
|
|
|
Some(crate::scene::model::object::Property {
|
2026-03-22 22:44:32 +03:00
|
|
|
label: prop.label.clone(),
|
|
|
|
|
field: prop.field,
|
|
|
|
|
value: merge_prop_value(&prop.value, &other.value),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if props.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
2026-06-16 11:33:46 +03:00
|
|
|
Some(crate::scene::model::object::PropSection {
|
2026-03-22 22:44:32 +03:00
|
|
|
title: section.title.clone(),
|
|
|
|
|
props,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn merge_prop_value(
|
2026-06-16 11:33:46 +03:00
|
|
|
left: &crate::scene::model::object::PropValue,
|
|
|
|
|
right: &crate::scene::model::object::PropValue,
|
|
|
|
|
) -> crate::scene::model::object::PropValue {
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
if left == right {
|
|
|
|
|
return left.clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match (left, right) {
|
|
|
|
|
(PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => {
|
|
|
|
|
PropValue::LayerChoice(VARIES_LABEL.into())
|
|
|
|
|
}
|
|
|
|
|
(PropValue::ColorChoice(_), PropValue::ColorChoice(_))
|
|
|
|
|
| (PropValue::ColorVaries, _)
|
|
|
|
|
| (_, PropValue::ColorVaries) => PropValue::ColorVaries,
|
|
|
|
|
(PropValue::LwChoice(_), PropValue::LwChoice(_))
|
|
|
|
|
| (PropValue::LwVaries, _)
|
|
|
|
|
| (_, PropValue::LwVaries) => PropValue::LwVaries,
|
|
|
|
|
(PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => {
|
|
|
|
|
PropValue::LinetypeChoice(VARIES_LABEL.into())
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
PropValue::Choice { options, .. },
|
|
|
|
|
PropValue::Choice {
|
|
|
|
|
options: other_options,
|
|
|
|
|
..
|
|
|
|
|
},
|
|
|
|
|
) if options == other_options => PropValue::Choice {
|
|
|
|
|
selected: VARIES_LABEL.into(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
},
|
2026-07-22 11:11:10 +03:00
|
|
|
(
|
|
|
|
|
PropValue::EditChoice { options, .. },
|
|
|
|
|
PropValue::EditChoice {
|
|
|
|
|
options: other_options,
|
|
|
|
|
..
|
|
|
|
|
},
|
|
|
|
|
) if options == other_options => PropValue::EditChoice {
|
|
|
|
|
value: VARIES_LABEL.into(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
(PropValue::EditText(_), PropValue::EditText(_)) => {
|
|
|
|
|
PropValue::EditText(VARIES_LABEL.into())
|
|
|
|
|
}
|
|
|
|
|
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
|
|
|
|
PropValue::ReadOnly(VARIES_LABEL.into())
|
|
|
|
|
}
|
|
|
|
|
(PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => {
|
|
|
|
|
PropValue::HatchPatternChoice(VARIES_LABEL.into())
|
|
|
|
|
}
|
|
|
|
|
(
|
|
|
|
|
PropValue::BoolToggle { field, .. },
|
|
|
|
|
PropValue::BoolToggle {
|
2026-05-12 10:36:15 +03:00
|
|
|
field: other_field, ..
|
2026-03-22 22:44:32 +03:00
|
|
|
},
|
|
|
|
|
) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()),
|
|
|
|
|
_ => left.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-20 05:43:55 +03:00
|
|
|
|
2026-07-03 00:05:58 +03:00
|
|
|
/// Set the first property row matching `field` (across all sections) to a
|
|
|
|
|
/// read-only `value`. No-op when the field is absent. Used to fill the
|
|
|
|
|
/// doc-dependent placeholder rows the entity builders leave empty.
|
|
|
|
|
fn set_row(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
value: String,
|
|
|
|
|
) {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) = section.props.iter_mut().find(|p| p.field == field) {
|
|
|
|
|
row.value = crate::scene::model::object::PropValue::ReadOnly(value);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
/// Replace a row's value with an arbitrary control (editable field, dropdown,
|
|
|
|
|
/// colour picker …) rather than plain read-only text.
|
|
|
|
|
fn set_row_value(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
value: crate::scene::model::object::PropValue,
|
|
|
|
|
) {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) = section.props.iter_mut().find(|p| p.field == field) {
|
|
|
|
|
row.value = value;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Inverse of `dim_lineweight_label`: a lineweight label → DIMLWD enum value.
|
|
|
|
|
pub(crate) fn dim_lineweight_from_label(label: &str) -> i16 {
|
|
|
|
|
match label.trim() {
|
|
|
|
|
"ByLayer" => -1,
|
|
|
|
|
"ByBlock" => -2,
|
|
|
|
|
"Default" => -3,
|
|
|
|
|
s => s
|
|
|
|
|
.trim_end_matches("mm")
|
|
|
|
|
.trim()
|
|
|
|
|
.parse::<f64>()
|
|
|
|
|
.map(|mm| (mm * 100.0).round() as i16)
|
|
|
|
|
.unwrap_or(-3),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The vertical-text-position dropdown options, matching `dimtad_label`.
|
|
|
|
|
pub(crate) fn tad_options() -> Vec<String> {
|
|
|
|
|
["Centered", "Above", "Outside", "JIS", "Below"]
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Inverse of `dimtad_label`: a vertical-position label → DIMTAD value.
|
|
|
|
|
pub(crate) fn dimtad_from_label(label: &str) -> i16 {
|
|
|
|
|
match label.trim() {
|
|
|
|
|
"Above" => 1,
|
|
|
|
|
"Outside" => 2,
|
|
|
|
|
"JIS" => 3,
|
|
|
|
|
"Below" => 4,
|
|
|
|
|
_ => 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 00:05:58 +03:00
|
|
|
/// Resolve a dimension style by name (case-insensitive), falling back to
|
|
|
|
|
/// "Standard" when the name is blank.
|
|
|
|
|
fn find_dim_style<'a>(
|
|
|
|
|
doc: &'a acadrust::CadDocument,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Option<&'a acadrust::tables::DimStyle> {
|
|
|
|
|
doc.dim_styles.iter().find(|s| {
|
|
|
|
|
s.name.eq_ignore_ascii_case(name)
|
|
|
|
|
|| (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 19:26:11 +03:00
|
|
|
/// Insert `row` immediately after the first property whose field matches.
|
|
|
|
|
fn insert_row_after(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
row: crate::scene::model::object::Property,
|
|
|
|
|
) {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(idx) = section.props.iter().position(|p| p.field == field) {
|
|
|
|
|
section.props.insert(idx + 1, row);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 00:05:58 +03:00
|
|
|
/// Vertical text placement (DIMTAD) label.
|
|
|
|
|
fn dimtad_label(dimtad: i16) -> &'static str {
|
|
|
|
|
match dimtad {
|
|
|
|
|
1 => "Above",
|
|
|
|
|
2 => "Outside",
|
|
|
|
|
3 => "JIS",
|
|
|
|
|
4 => "Below",
|
|
|
|
|
_ => "Centered",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
/// Friendly arrowhead name from an arrowhead block-record name (the `_CLOSED…`
|
|
|
|
|
/// style internal names map to their palette labels; a null/empty name is the
|
|
|
|
|
/// closed-filled default).
|
feat(properties): editable per-object leader dimension overrides
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
|
|
|
pub(crate) fn arrowhead_label(name: &str) -> String {
|
feat(properties): rework Text/MText/MLeader/Leader panels to standard layout
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 18:01:44 +03:00
|
|
|
let key = name.trim().trim_start_matches('_').to_ascii_uppercase();
|
|
|
|
|
let label = match key.as_str() {
|
|
|
|
|
"" | "CLOSEDFILLED" => "Closed filled",
|
|
|
|
|
"CLOSED" => "Closed",
|
|
|
|
|
"CLOSEDBLANK" => "Closed blank",
|
|
|
|
|
"DOT" => "Dot",
|
|
|
|
|
"DOTSMALL" => "Dot small",
|
|
|
|
|
"DOTBLANK" => "Dot blank",
|
|
|
|
|
"SMALLDOTBLANK" => "Dot small blank",
|
|
|
|
|
"ORIGIN" => "Origin indicator",
|
|
|
|
|
"ORIGIN2" => "Origin indicator 2",
|
|
|
|
|
"OPEN" => "Open",
|
|
|
|
|
"OPEN90" => "Right angle",
|
|
|
|
|
"OPEN30" => "Open 30",
|
|
|
|
|
"NONE" => "None",
|
|
|
|
|
"OBLIQUE" => "Oblique",
|
|
|
|
|
"ARCHTICK" => "Architectural tick",
|
|
|
|
|
"BOXBLANK" => "Box",
|
|
|
|
|
"BOXFILLED" => "Box filled",
|
|
|
|
|
"DATUMBLANK" => "Datum triangle",
|
|
|
|
|
"DATUMFILLED" => "Datum triangle filled",
|
|
|
|
|
"INTEGRAL" => "Integral",
|
|
|
|
|
_ => return name.to_string(),
|
|
|
|
|
};
|
|
|
|
|
label.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The leader's arrowhead label, resolved from the dim style's DIMLDRBLK block.
|
|
|
|
|
fn leader_arrow_label(
|
|
|
|
|
doc: &acadrust::CadDocument,
|
|
|
|
|
ds: &acadrust::tables::DimStyle,
|
|
|
|
|
arrow_enabled: bool,
|
|
|
|
|
) -> String {
|
|
|
|
|
if !arrow_enabled {
|
|
|
|
|
return "None".to_string();
|
|
|
|
|
}
|
|
|
|
|
if ds.dimldrblk.is_null() {
|
|
|
|
|
return "Closed filled".to_string();
|
|
|
|
|
}
|
|
|
|
|
doc.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.handle == ds.dimldrblk)
|
|
|
|
|
.map(|b| arrowhead_label(&b.name))
|
|
|
|
|
.unwrap_or_else(|| "Closed filled".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 00:05:58 +03:00
|
|
|
/// Human-readable INSUNITS name (DXF group 70 unit codes).
|
|
|
|
|
fn insunits_name(code: i16) -> &'static str {
|
|
|
|
|
match code {
|
|
|
|
|
1 => "Inches",
|
|
|
|
|
2 => "Feet",
|
|
|
|
|
3 => "Miles",
|
|
|
|
|
4 => "Millimeters",
|
|
|
|
|
5 => "Centimeters",
|
|
|
|
|
6 => "Meters",
|
|
|
|
|
7 => "Kilometers",
|
|
|
|
|
8 => "Microinches",
|
|
|
|
|
9 => "Mils",
|
|
|
|
|
10 => "Yards",
|
|
|
|
|
11 => "Angstroms",
|
|
|
|
|
12 => "Nanometers",
|
|
|
|
|
13 => "Microns",
|
|
|
|
|
14 => "Decimeters",
|
|
|
|
|
15 => "Decameters",
|
|
|
|
|
16 => "Hectometers",
|
|
|
|
|
17 => "Gigameters",
|
|
|
|
|
18 => "Astronomical Units",
|
|
|
|
|
19 => "Light Years",
|
|
|
|
|
20 => "Parsecs",
|
|
|
|
|
21 => "US Survey Feet",
|
|
|
|
|
_ => "Unitless",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 05:43:55 +03:00
|
|
|
/// Convert INSUNITS (DXF group 70) to millimetres.
|
|
|
|
|
/// 0 = unitless / unknown: returns 1.0 so the caller treats it as "do not scale".
|
|
|
|
|
fn insunits_to_mm(code: i16) -> f64 {
|
|
|
|
|
match code {
|
|
|
|
|
1 => 25.4, // Inches
|
|
|
|
|
2 => 304.8, // Feet
|
|
|
|
|
3 => 1_609_344.0, // Miles
|
|
|
|
|
4 => 1.0, // Millimeters
|
|
|
|
|
5 => 10.0, // Centimeters
|
|
|
|
|
6 => 1_000.0, // Meters
|
|
|
|
|
7 => 1_000_000.0, // Kilometers
|
|
|
|
|
8 => 0.000_025_4, // Microinches
|
|
|
|
|
9 => 0.025_4, // Mils
|
|
|
|
|
10 => 914.4, // Yards
|
|
|
|
|
11 => 1.0e-7, // Angstroms
|
|
|
|
|
12 => 1.0e-6, // Nanometers
|
|
|
|
|
13 => 0.001, // Microns
|
|
|
|
|
14 => 100.0, // Decimeters
|
|
|
|
|
15 => 10_000.0, // Decameters
|
|
|
|
|
16 => 100_000.0, // Hectometers
|
|
|
|
|
17 => 1.0e12, // Gigameters
|
|
|
|
|
18 => 1.496e14, // Astronomical Units
|
|
|
|
|
19 => 9.461e18, // Light Years
|
|
|
|
|
20 => 3.086e19, // Parsecs
|
|
|
|
|
21 => 304.800_609_6, // US Survey Feet
|
|
|
|
|
_ => 1.0,
|
|
|
|
|
}
|
|
|
|
|
}
|