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-08-26 13:54:20 +03:00
|
|
|
use crate::scene::model::object::PropValue;
|
2026-03-22 22:44:32 +03:00
|
|
|
use crate::ui;
|
2026-08-02 22:09:25 +03:00
|
|
|
use crate::t;
|
2026-08-17 21:32:28 +03:00
|
|
|
use acadrust::types::{Transform, Vector3};
|
|
|
|
|
use acadrust::{Entity, EntityType, Handle};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
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 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-08-12 23:23:50 +01:00
|
|
|
let active_field = std::mem::take(&mut self.tabs[i].properties.active_field);
|
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;
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
crate::entities::common::set_unit_context(
|
|
|
|
|
crate::entities::common::UnitContext::from_header(h),
|
|
|
|
|
);
|
2026-05-20 05:43:55 +03:00
|
|
|
}
|
feat(properties): stop offering a height the style has fixed
A text style with a non-zero height fixes the size of everything drawn in
it — the CAD that writes the file skips the height prompt for such a
style, and shows the height without letting it be changed. OCS offered
the field either way, so a drawing could be edited into disagreeing with
its own style.
The height row is now read-only when the style fixes it, for TEXT, MTEXT
and both attribute kinds. `num_prop` already draws a numeric row either
way, so the value still reads the same, just grey.
The panel builders are handed an entity, not the document it came from,
and widening that trait would touch every entity type. The lookup rides
beside the unit context instead, seeded from the same place, since it
answers the same kind of question: something the drawing settles that a
row needs in order to render itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:45:46 +03:00
|
|
|
{
|
|
|
|
|
// Which text styles fix their own height — the height rows read it
|
|
|
|
|
// to decide whether they are editable.
|
|
|
|
|
crate::entities::common::set_fixed_text_heights(&self.tabs[i].scene.document);
|
|
|
|
|
}
|
2026-05-20 05:43:55 +03:00
|
|
|
|
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
|
|
|
|
|
};
|
2026-07-31 23:36:07 +03:00
|
|
|
let prop_vertex = if cur_handles.len() == 1 {
|
|
|
|
|
let vertex_count = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.get_entity(cur_handles[0])
|
|
|
|
|
.and_then(|entity| match entity {
|
|
|
|
|
acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()),
|
|
|
|
|
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
2026-08-20 12:13:35 +03:00
|
|
|
acadrust::EntityType::Spline(spline) => {
|
|
|
|
|
Some(if crate::entities::spline::shows_fit_points(spline) {
|
2026-08-20 00:18:45 +03:00
|
|
|
spline.fit_points.len()
|
|
|
|
|
} else {
|
2026-08-20 12:13:35 +03:00
|
|
|
crate::entities::spline::control_vertex_count(spline)
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-08-24 13:00:04 +03:00
|
|
|
acadrust::EntityType::Table(table) => {
|
|
|
|
|
Some(table.row_count().saturating_mul(table.column_count()))
|
|
|
|
|
}
|
2026-07-31 23:36:07 +03:00
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1)))
|
|
|
|
|
} else {
|
|
|
|
|
prop_vertex
|
|
|
|
|
};
|
2026-07-31 22:05:30 +03:00
|
|
|
let prop_vertex_indicator_active = if cur_handles == prev_handles {
|
|
|
|
|
self.tabs[i].properties.prop_vertex_indicator_active
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
2026-07-08 17:53:05 +03:00
|
|
|
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
2026-08-24 13:00:04 +03:00
|
|
|
crate::entities::table::set_prop_current_cell(prop_vertex);
|
2026-08-24 18:09:34 +03:00
|
|
|
crate::entities::table::set_prop_current_cell_active(prop_vertex_indicator_active);
|
2026-07-08 17:53:05 +03:00
|
|
|
|
2026-08-02 12:00:15 +03:00
|
|
|
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
|
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())
|
|
|
|
|
};
|
2026-08-25 10:37:56 +03:00
|
|
|
let plot_style = if header.plotstyle_mode {
|
|
|
|
|
"ByColor"
|
|
|
|
|
} else {
|
|
|
|
|
match header.current_plotstyle_type {
|
|
|
|
|
1 => "ByBlock",
|
|
|
|
|
2 => "Normal",
|
|
|
|
|
3 => "ByObject",
|
|
|
|
|
_ => "ByLayer",
|
|
|
|
|
}
|
2026-07-27 17:37:17 +03:00
|
|
|
};
|
|
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("General").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("AC Version").as_ref(), doc.version.as_str().to_string()),
|
2026-07-27 17:37:17 +03:00
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Color").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
field: "color",
|
|
|
|
|
value: PropValue::ColorChoice(header.current_entity_color),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Layer").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
field: "layer",
|
|
|
|
|
value: PropValue::LayerChoice(current_layer),
|
|
|
|
|
},
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Linetype").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
field: "linetype",
|
|
|
|
|
value: PropValue::LinetypeChoice(current_linetype),
|
|
|
|
|
},
|
|
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Linetype scale").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
format_length(header.current_entity_linetype_scale),
|
|
|
|
|
),
|
|
|
|
|
Property {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Lineweight").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
field: "line_weight",
|
|
|
|
|
value: PropValue::LwChoice(
|
|
|
|
|
acadrust::types::LineWeight::from_value(
|
|
|
|
|
header.current_line_weight,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-08-23 21:02:59 +01:00
|
|
|
Property {
|
|
|
|
|
label: t!("Transparency").into_owned(),
|
|
|
|
|
field: "transparency",
|
|
|
|
|
value: PropValue::EditChoice {
|
|
|
|
|
value: "ByLayer".to_string(),
|
|
|
|
|
options: vec!["ByLayer".to_string(), "ByBlock".to_string()],
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("Thickness").as_ref(), format_length(header.thickness)),
|
2026-07-27 17:37:17 +03:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("3D Visualization").into_owned(),
|
2026-08-23 21:02:59 +01:00
|
|
|
props: vec![Property {
|
|
|
|
|
label: t!("Material").into_owned(),
|
|
|
|
|
field: "material",
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
selected: material.clone(),
|
|
|
|
|
options: {
|
|
|
|
|
let mut opts = vec![
|
|
|
|
|
"ByLayer".to_string(),
|
|
|
|
|
"ByBlock".to_string(),
|
|
|
|
|
"Global".to_string(),
|
|
|
|
|
];
|
|
|
|
|
if !opts.contains(&material) && !material.is_empty() {
|
|
|
|
|
opts.push(material.clone());
|
|
|
|
|
}
|
|
|
|
|
opts
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}],
|
2026-07-27 17:37:17 +03:00
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Plot style").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
props: vec![
|
2026-08-23 21:02:59 +01:00
|
|
|
Property {
|
|
|
|
|
label: t!("Plot style").into_owned(),
|
|
|
|
|
field: "plot_style",
|
2026-08-25 10:37:56 +03:00
|
|
|
value: if !header.plotstyle_mode {
|
2026-08-23 21:02:59 +01:00
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: plot_style.to_string(),
|
|
|
|
|
options: vec![
|
|
|
|
|
"ByLayer".to_string(),
|
|
|
|
|
"ByBlock".to_string(),
|
|
|
|
|
"Normal".to_string(),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
PropValue::ReadOnlyWithTooltip {
|
2026-08-25 18:08:52 +03:00
|
|
|
value: t!("ByColor").into_owned(),
|
2026-08-23 21:02:59 +01:00
|
|
|
tooltip: t!("Plot style is locked to color in Color-Dependent (CTB) mode").into_owned(),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("Plot style table").as_ref(), plot_table.clone()),
|
2026-07-27 17:37:17 +03:00
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Plot table attached to").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
if plot_table == "None" {
|
|
|
|
|
"None".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
scene.current_layout.clone()
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Plot table type").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
if header.plotstyle_mode {
|
2026-08-25 18:08:52 +03:00
|
|
|
t!("Color-dependent plot styles").into_owned()
|
2026-07-27 17:37:17 +03:00
|
|
|
} else {
|
2026-08-25 18:08:52 +03:00
|
|
|
t!("Named plot styles").into_owned()
|
2026-07-27 17:37:17 +03:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("View").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("Center X").as_ref(), format_length(camera.target.x)),
|
|
|
|
|
read_only(t!("Center Y").as_ref(), format_length(camera.target.y)),
|
|
|
|
|
read_only(t!("Center Z").as_ref(), format_length(camera.target.z)),
|
|
|
|
|
read_only(t!("Height").as_ref(), format_length(view_height)),
|
|
|
|
|
read_only(t!("Width").as_ref(), format_length(view_width)),
|
2026-07-27 17:37:17 +03:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Misc").into_owned(),
|
2026-07-27 17:37:17 +03:00
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("Annotation scale").as_ref(), annotation_scale),
|
2026-07-27 17:37:17 +03:00
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("UCS icon On").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
if self.show_ucs_icon { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("UCS icon at origin").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
if self.ucs_icon_at_origin { "Yes" } else { "No" }
|
|
|
|
|
.to_string(),
|
|
|
|
|
),
|
|
|
|
|
read_only(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("UCS per viewport").as_ref(),
|
2026-07-27 17:37:17 +03:00
|
|
|
if ucs_per_viewport { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
2026-08-02 22:09:25 +03:00
|
|
|
read_only(t!("UCS Name").as_ref(), ucs_name),
|
|
|
|
|
read_only(t!("Visual Style").as_ref(), tab.visual_style.clone()),
|
2026-07-27 17:37:17 +03:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-07-26 23:38:38 +03:00
|
|
|
ui::PropertiesPanel {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("No selection").into_owned(),
|
2026-08-23 21:02:59 +01:00
|
|
|
choice_combos: sections
|
|
|
|
|
.iter()
|
|
|
|
|
.flat_map(|section| section.props.iter())
|
|
|
|
|
.filter_map(|prop| match &prop.value {
|
|
|
|
|
crate::scene::model::object::PropValue::Choice { options, .. } => Some((
|
|
|
|
|
prop.field.to_string(),
|
|
|
|
|
iced::widget::combo_box::State::new(
|
|
|
|
|
options
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.map(ui::properties::LocalizedChoice::new)
|
|
|
|
|
.collect(),
|
|
|
|
|
),
|
|
|
|
|
)),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
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(),
|
|
|
|
|
),
|
|
|
|
|
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];
|
2026-08-02 12:00:15 +03:00
|
|
|
let contextual = crate::scene::annotative::entity_for_annotation_context(
|
2026-07-26 22:30:14 +03:00
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
source_entity,
|
2026-08-02 12:00:15 +03:00
|
|
|
annotation_scale_handle,
|
2026-07-26 22:30:14 +03:00
|
|
|
);
|
2026-08-04 23:17:54 +03:00
|
|
|
let plane = if self.tabs[i].editing_model_space() {
|
|
|
|
|
self.tabs[i].ucs_xform().working_plane()
|
|
|
|
|
} else {
|
|
|
|
|
crate::command::WorkingPlane::default()
|
|
|
|
|
};
|
|
|
|
|
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
|
|
|
|
|
let entity = &display_entity;
|
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-08-11 20:14:51 +03:00
|
|
|
sections.extend(
|
|
|
|
|
crate::scene::model::solid_history::primitive_properties(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
handle,
|
|
|
|
|
),
|
|
|
|
|
);
|
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 selected = match common.material_flags {
|
|
|
|
|
0 => "ByLayer".to_string(),
|
|
|
|
|
1 => "ByBlock".to_string(),
|
2026-08-23 21:02:59 +01:00
|
|
|
2 => "Global".to_string(),
|
2026-07-12 23:16:26 +03:00
|
|
|
_ => 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
|
|
|
})
|
2026-08-23 21:02:59 +01:00
|
|
|
.unwrap_or_else(|| "Global".to_string()),
|
2026-07-12 23:16:26 +03:00
|
|
|
};
|
2026-08-23 21:02:59 +01:00
|
|
|
let mut options = vec![
|
|
|
|
|
"ByLayer".to_string(),
|
|
|
|
|
"ByBlock".to_string(),
|
|
|
|
|
"Global".to_string(),
|
|
|
|
|
];
|
|
|
|
|
if !options.contains(&selected) && !selected.is_empty() {
|
|
|
|
|
options.push(selected.clone());
|
|
|
|
|
}
|
2026-07-12 23:16:26 +03:00
|
|
|
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 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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Color Book").into_owned(),
|
2026-07-26 22:30:14 +03:00
|
|
|
props: vec![
|
2026-08-02 22:09:25 +03:00
|
|
|
ro_prop(t!("Book").as_ref(), "book_color_book", book.book_name.clone()),
|
2026-07-26 22:30:14 +03:00
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Color Name").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"book_color_name",
|
|
|
|
|
book.color_name.clone(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Color").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("%{scope} Visual Style", scope = scope).into_owned(),
|
2026-07-26 22:30:14 +03:00
|
|
|
props: vec![
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Handle").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_handle",
|
|
|
|
|
format!("{:X}", handle.value()),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Description").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_description",
|
|
|
|
|
style.description.clone(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Type").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_type",
|
|
|
|
|
style.style_type.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Face").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_face",
|
|
|
|
|
format!(
|
|
|
|
|
"lighting {}; quality {}; color {}; modifiers {}",
|
|
|
|
|
style.face_lighting_model,
|
|
|
|
|
style.face_lighting_quality,
|
|
|
|
|
style.face_color_mode,
|
|
|
|
|
style.face_modifier
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Edges").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_edges",
|
|
|
|
|
format!(
|
|
|
|
|
"model {}; style {}",
|
|
|
|
|
style.edge_model, style.edge_style
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Extended Lighting").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_extended_lighting",
|
|
|
|
|
style.extended_lighting_model.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Internal").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"vs_internal",
|
|
|
|
|
style.internal_use_only.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Property Bag").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"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(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Vertices").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"mesh_vertices",
|
|
|
|
|
metrics.vertices.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Triangles").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"mesh_triangles",
|
|
|
|
|
metrics.triangles.to_string(),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Surface Area").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"mesh_surface_area",
|
|
|
|
|
format!("{:.6}", metrics.surface_area),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Centroid").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"mesh_centroid",
|
|
|
|
|
format!(
|
|
|
|
|
"{:.6}, {:.6}, {:.6}",
|
|
|
|
|
metrics.centroid[0],
|
|
|
|
|
metrics.centroid[1],
|
|
|
|
|
metrics.centroid[2]
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Tessellation").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"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(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Volume").as_ref(),
|
2026-07-26 22:30:14 +03:00
|
|
|
"mesh_volume",
|
|
|
|
|
format!("{:.6}", metrics.volume),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("Mass Properties").into_owned(),
|
2026-07-26 22:30:14 +03:00
|
|
|
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-08-23 21:27:30 +02:00
|
|
|
{
|
|
|
|
|
use crate::entities::common::ro_prop;
|
|
|
|
|
let xd = &source_entity.common().extended_data;
|
|
|
|
|
if !xd.is_empty() {
|
|
|
|
|
let mut xd_props = Vec::new();
|
|
|
|
|
for rec in xd.records() {
|
|
|
|
|
let value_text = rec
|
|
|
|
|
.values
|
|
|
|
|
.iter()
|
2026-08-24 16:09:03 +03:00
|
|
|
.map(|value| format!("{value:?}"))
|
2026-08-23 21:27:30 +02:00
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ");
|
|
|
|
|
xd_props.push(ro_prop(
|
2026-08-24 16:09:03 +03:00
|
|
|
t!("Application").as_ref(),
|
2026-08-23 21:27:30 +02:00
|
|
|
"xdata_value",
|
2026-08-24 16:09:03 +03:00
|
|
|
format!("{}: {value_text}", rec.application_name),
|
2026-08-23 21:27:30 +02:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
sections.push(crate::scene::model::object::PropSection {
|
|
|
|
|
title: t!("Extended Data").into_owned(),
|
|
|
|
|
props: xd_props,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Uniform scale").into_owned(),
|
2026-07-21 10:27:50 +03:00
|
|
|
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(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Scale").as_ref(),
|
2026-07-21 10:27:50 +03:00
|
|
|
"u_scale",
|
|
|
|
|
ins.x_scale(),
|
|
|
|
|
);
|
|
|
|
|
section.props.insert(xi, toggle);
|
|
|
|
|
} else {
|
|
|
|
|
section.props.insert(xi, toggle);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 10:37:56 +03:00
|
|
|
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;
|
2026-08-23 21:02:59 +01:00
|
|
|
let common = entity.common();
|
|
|
|
|
let mut options = vec![
|
|
|
|
|
"ByLayer".to_string(),
|
|
|
|
|
"ByBlock".to_string(),
|
|
|
|
|
"Normal".to_string(),
|
|
|
|
|
];
|
2026-07-12 23:16:26 +03:00
|
|
|
if let Some(dict) = crate::scene::annotative::as_dict(doc, dict_h) {
|
2026-08-23 21:02:59 +01:00
|
|
|
for (n, _) in &dict.entries {
|
|
|
|
|
if !options.contains(n) {
|
|
|
|
|
options.push(n.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let selected = match common.plotstyle_flags {
|
|
|
|
|
0 => "ByLayer".to_string(),
|
|
|
|
|
1 => "ByBlock".to_string(),
|
|
|
|
|
2 => "Normal".to_string(),
|
|
|
|
|
_ => common
|
|
|
|
|
.plotstyle_handle
|
|
|
|
|
.and_then(|ph| {
|
|
|
|
|
crate::scene::annotative::as_dict(doc, dict_h).and_then(|dict| {
|
2026-07-12 23:16:26 +03:00
|
|
|
dict.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, h)| *h == ph)
|
|
|
|
|
.map(|(n, _)| n.clone())
|
|
|
|
|
})
|
2026-08-23 21:02:59 +01:00
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string()),
|
|
|
|
|
};
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|p| p.field == "plot_style")
|
|
|
|
|
{
|
|
|
|
|
row.value = crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected: selected.clone(),
|
|
|
|
|
options: options.clone(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|p| p.field == "plot_style")
|
|
|
|
|
{
|
|
|
|
|
row.value = crate::scene::model::object::PropValue::ReadOnlyWithTooltip {
|
2026-08-25 18:08:52 +03:00
|
|
|
value: t!("ByColor").into_owned(),
|
2026-08-23 21:02:59 +01:00
|
|
|
tooltip: t!("Plot style is locked to color in Color-Dependent (CTB) mode").into_owned(),
|
|
|
|
|
};
|
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-08-02 22:09:25 +03:00
|
|
|
label: t!("Frozen Layers").into_owned(),
|
2026-03-31 08:18:29 +03:00
|
|
|
field: "frozen_layers",
|
2026-08-20 14:58:12 +03:00
|
|
|
value: crate::scene::model::object::PropValue::PlainText(
|
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-08-02 22:09:25 +03:00
|
|
|
label: t!("UCS Name").into_owned(),
|
2026-04-01 00:30:45 +03:00
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: t!("Named View").into_owned(),
|
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
|
|
|
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-08-24 15:32:37 +03:00
|
|
|
if let Some(general) = sections.first_mut() {
|
|
|
|
|
general.props.retain(|property| property.field != "handle");
|
|
|
|
|
let associative =
|
|
|
|
|
crate::scene::dimension_assoc::dimension_is_associative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
d.base().common.handle,
|
|
|
|
|
);
|
|
|
|
|
general.props.push(crate::scene::model::object::Property {
|
|
|
|
|
label: t!("Associative").into_owned(),
|
|
|
|
|
field: "associative",
|
|
|
|
|
value: crate::scene::model::object::PropValue::ReadOnly(
|
|
|
|
|
if associative { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
}
|
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() {
|
2026-08-20 14:58:12 +03:00
|
|
|
// Current style is already shown as text in the geom section;
|
2026-04-02 19:06:20 +03:00
|
|
|
// replace/upgrade it to a Choice if we have a list.
|
|
|
|
|
if let Some(geom) = sections.last_mut() {
|
2026-08-20 14:58:12 +03:00
|
|
|
// Replace the style name 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-08-20 14:58:12 +03:00
|
|
|
crate::scene::model::object::PropValue::PlainText(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"))
|
|
|
|
|
})
|
|
|
|
|
{
|
2026-08-24 12:10:29 +03:00
|
|
|
sections.extend(crate::entities::dimension::style_sections(
|
|
|
|
|
style,
|
|
|
|
|
d,
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
));
|
2026-07-13 17:31:09 +03:00
|
|
|
|
2026-08-24 15:32:37 +03:00
|
|
|
// Prefer entity-level dimension-variable overrides;
|
|
|
|
|
// fall back to the assigned style values.
|
2026-07-13 17:31:09 +03:00
|
|
|
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),
|
|
|
|
|
);
|
2026-08-24 12:10:29 +03:00
|
|
|
for (field, code, inherited) in [
|
|
|
|
|
("dim_ext_line_color", dov::DIMCLRE, style.dimclre),
|
|
|
|
|
("dim_text_color", dov::DIMCLRT, style.dimclrt),
|
|
|
|
|
] {
|
|
|
|
|
let color = dov::color(&d.base().common.extended_data, code)
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
acadrust::types::Color::from_index(inherited)
|
|
|
|
|
});
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
field,
|
|
|
|
|
PropValue::ColorChoice(color),
|
|
|
|
|
);
|
|
|
|
|
}
|
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());
|
2026-08-17 21:32:28 +03:00
|
|
|
let factor = insert_unit_scale(host, src).unwrap_or(1.0);
|
|
|
|
|
set_row(&mut sections, "unit_factor", format_unit_factor(factor));
|
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 {
|
2026-08-20 14:58:12 +03:00
|
|
|
crate::scene::model::object::PropValue::PlainText(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
|
|
|
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) => {
|
2026-08-26 13:54:20 +03:00
|
|
|
use crate::entities::dim_override as dov;
|
|
|
|
|
let style = crate::entities::tolerance::resolve_dim_style(tol, doc);
|
|
|
|
|
let style_name = style
|
|
|
|
|
.map(|entry| entry.name.clone())
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
if tol.dimension_style_name.trim().is_empty() {
|
|
|
|
|
"Standard".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
tol.dimension_style_name.clone()
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
let mut dim_style_names: Vec<String> = doc
|
|
|
|
|
.dim_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|entry| entry.name.clone())
|
|
|
|
|
.filter(|name| !name.trim().is_empty())
|
|
|
|
|
.collect();
|
|
|
|
|
if !dim_style_names
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|name| name.eq_ignore_ascii_case(&style_name))
|
|
|
|
|
{
|
|
|
|
|
dim_style_names.push(style_name.clone());
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
2026-08-26 13:54:20 +03:00
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tol_dim_style",
|
|
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: style_name,
|
|
|
|
|
options: dim_style_names,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let text_style_name = dov::handle(
|
|
|
|
|
&tol.common.extended_data,
|
|
|
|
|
dov::DIMTXSTY,
|
|
|
|
|
)
|
|
|
|
|
.and_then(|handle| {
|
|
|
|
|
doc.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|entry| entry.handle == handle)
|
|
|
|
|
})
|
|
|
|
|
.map(|entry| entry.name.clone())
|
|
|
|
|
.or_else(|| style.map(|entry| entry.dimtxsty.clone()))
|
|
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
2026-08-26 14:19:24 +03:00
|
|
|
let text_height_editable = doc
|
|
|
|
|
.text_styles
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|entry| {
|
|
|
|
|
entry.name.eq_ignore_ascii_case(&text_style_name)
|
|
|
|
|
})
|
|
|
|
|
.is_none_or(|entry| !entry.has_fixed_height());
|
2026-08-26 13:54:20 +03:00
|
|
|
let mut names = text_style_names.clone();
|
|
|
|
|
if !names
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|name| name.eq_ignore_ascii_case(&text_style_name))
|
|
|
|
|
{
|
|
|
|
|
names.push(text_style_name.clone());
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
2026-08-26 13:54:20 +03:00
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tol_text_style",
|
|
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: text_style_name,
|
|
|
|
|
options: names,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let height = dov::real(
|
|
|
|
|
&tol.common.extended_data,
|
|
|
|
|
dov::DIMTXT,
|
|
|
|
|
)
|
|
|
|
|
.or_else(|| style.map(|entry| entry.dimtxt))
|
|
|
|
|
.unwrap_or(tol.text_height);
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tol_text_height",
|
2026-08-26 14:19:24 +03:00
|
|
|
if text_height_editable {
|
|
|
|
|
PropValue::EditText(format!("{height:.4}"))
|
|
|
|
|
} else {
|
|
|
|
|
PropValue::ReadOnly(format!("{height:.4}"))
|
|
|
|
|
},
|
2026-08-26 13:54:20 +03:00
|
|
|
);
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
|
|
|
|
// 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-08-24 13:00:04 +03:00
|
|
|
acadrust::EntityType::Table(table) => {
|
2026-08-24 18:09:34 +03:00
|
|
|
use acadrust::entities::table::{
|
|
|
|
|
CellEdgeFlags, CellStylePropertyFlags, CellValueType,
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-24 13:00:04 +03:00
|
|
|
let mut names: Vec<String> = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.filter_map(|object| match object {
|
|
|
|
|
acadrust::objects::ObjectType::TableStyle(style)
|
|
|
|
|
if !style.name.trim().is_empty() =>
|
|
|
|
|
{
|
|
|
|
|
Some(style.name.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
names.sort_by_key(|name| name.to_ascii_lowercase());
|
|
|
|
|
names.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
|
2026-08-24 18:09:34 +03:00
|
|
|
let selected_style = table
|
2026-08-24 13:00:04 +03:00
|
|
|
.table_style_handle
|
|
|
|
|
.and_then(|handle| doc.objects.get(&handle))
|
|
|
|
|
.and_then(|object| match object {
|
|
|
|
|
acadrust::objects::ObjectType::TableStyle(style) => {
|
2026-08-24 18:09:34 +03:00
|
|
|
Some(style)
|
2026-08-24 13:00:04 +03:00
|
|
|
}
|
|
|
|
|
_ => None,
|
2026-08-24 18:09:34 +03:00
|
|
|
});
|
|
|
|
|
let selected = selected_style
|
|
|
|
|
.map(|style| style.name.clone())
|
2026-08-24 13:00:04 +03:00
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
|
|
|
if !names.iter().any(|name| name.eq_ignore_ascii_case(&selected)) {
|
|
|
|
|
names.insert(0, selected.clone());
|
|
|
|
|
}
|
|
|
|
|
set_row_value(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_style_handle",
|
|
|
|
|
crate::scene::model::object::PropValue::Choice {
|
|
|
|
|
selected,
|
|
|
|
|
options: names,
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-08-24 18:09:34 +03:00
|
|
|
|
|
|
|
|
let title_suppressed =
|
|
|
|
|
crate::entities::table::resolved_title_suppressed(
|
|
|
|
|
table,
|
|
|
|
|
selected_style,
|
|
|
|
|
);
|
|
|
|
|
let header_suppressed =
|
|
|
|
|
crate::entities::table::resolved_header_suppressed(
|
|
|
|
|
table,
|
|
|
|
|
selected_style,
|
|
|
|
|
);
|
|
|
|
|
let flow_up = crate::entities::table::resolved_flow_up(
|
|
|
|
|
table,
|
|
|
|
|
selected_style,
|
|
|
|
|
);
|
|
|
|
|
let (horizontal_margin, vertical_margin) =
|
|
|
|
|
crate::entities::table::resolved_table_margins(
|
|
|
|
|
table,
|
|
|
|
|
selected_style,
|
|
|
|
|
);
|
|
|
|
|
update_row_toggle(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_title_suppressed",
|
|
|
|
|
title_suppressed,
|
|
|
|
|
);
|
|
|
|
|
update_row_toggle(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_header_suppressed",
|
|
|
|
|
header_suppressed,
|
|
|
|
|
);
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_flow_direction",
|
|
|
|
|
if flow_up { "Up" } else { "Down" }.to_string(),
|
|
|
|
|
);
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_horizontal_margin",
|
|
|
|
|
crate::entities::common::format_length(horizontal_margin),
|
|
|
|
|
);
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_vertical_margin",
|
|
|
|
|
crate::entities::common::format_length(vertical_margin),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let columns = table.column_count();
|
|
|
|
|
if columns > 0 {
|
|
|
|
|
let cell_index = prop_vertex.min(
|
|
|
|
|
table.row_count()
|
|
|
|
|
.saturating_mul(columns)
|
|
|
|
|
.saturating_sub(1),
|
|
|
|
|
);
|
|
|
|
|
let row_index = cell_index / columns;
|
|
|
|
|
let column_index = cell_index % columns;
|
|
|
|
|
if let (Some(row), Some(cell)) = (
|
|
|
|
|
table.rows.get(row_index),
|
|
|
|
|
table.cell(row_index, column_index),
|
|
|
|
|
) {
|
|
|
|
|
let document_row_style = selected_style.map(|style| {
|
|
|
|
|
let kind = match (
|
|
|
|
|
title_suppressed,
|
|
|
|
|
header_suppressed,
|
|
|
|
|
row_index,
|
|
|
|
|
) {
|
|
|
|
|
(false, _, 0) => 0,
|
|
|
|
|
(false, false, 1) | (true, false, 0) => 1,
|
|
|
|
|
_ => 2,
|
|
|
|
|
};
|
|
|
|
|
match kind {
|
|
|
|
|
0 => &style.title_row_style,
|
|
|
|
|
1 => &style.header_row_style,
|
|
|
|
|
_ => &style.data_row_style,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
let local = |property| {
|
|
|
|
|
crate::entities::table::style_for_property(
|
|
|
|
|
table,
|
|
|
|
|
row,
|
|
|
|
|
column_index,
|
|
|
|
|
cell,
|
|
|
|
|
property,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let alignment = local(CellStylePropertyFlags::ALIGNMENT)
|
|
|
|
|
.map(|style| style.alignment)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style.map(|style| style.alignment as i32)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(5);
|
|
|
|
|
let alignment = match alignment {
|
|
|
|
|
1 => "Top Left",
|
|
|
|
|
2 => "Top Center",
|
|
|
|
|
3 => "Top Right",
|
|
|
|
|
4 => "Middle Left",
|
|
|
|
|
6 => "Middle Right",
|
|
|
|
|
7 => "Bottom Left",
|
|
|
|
|
8 => "Bottom Center",
|
|
|
|
|
9 => "Bottom Right",
|
|
|
|
|
_ => "Middle Center",
|
|
|
|
|
};
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_alignment",
|
|
|
|
|
alignment.to_string(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let text_style_name = local(CellStylePropertyFlags::TEXT_STYLE)
|
|
|
|
|
.map(|style| style.text_style_name.clone())
|
|
|
|
|
.filter(|name| !name.is_empty())
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style
|
|
|
|
|
.map(|style| style.text_style_name.clone())
|
|
|
|
|
.filter(|name| !name.is_empty())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_text_style",
|
|
|
|
|
text_style_name,
|
|
|
|
|
);
|
|
|
|
|
let text_height = local(CellStylePropertyFlags::TEXT_HEIGHT)
|
|
|
|
|
.map(|style| style.text_height)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style.map(|style| style.text_height)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(0.18);
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_text_height",
|
|
|
|
|
crate::entities::common::format_length(text_height),
|
|
|
|
|
);
|
|
|
|
|
let content_color = local(
|
|
|
|
|
CellStylePropertyFlags::CONTENT_COLOR,
|
|
|
|
|
)
|
|
|
|
|
.map(|style| style.content_color)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style.map(|style| style.text_color)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(acadrust::types::Color::ByBlock);
|
|
|
|
|
update_row_color(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_content_color",
|
|
|
|
|
content_color,
|
|
|
|
|
);
|
|
|
|
|
let background_style =
|
|
|
|
|
local(CellStylePropertyFlags::BACKGROUND_COLOR);
|
|
|
|
|
let background_color = background_style
|
|
|
|
|
.map(|style| style.background_color)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style.map(|style| style.fill_color)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(acadrust::types::Color::ByBlock);
|
|
|
|
|
update_row_color(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_background_color",
|
|
|
|
|
background_color,
|
|
|
|
|
);
|
|
|
|
|
update_row_toggle(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_fill",
|
|
|
|
|
background_style
|
|
|
|
|
.map(|style| style.fill_enabled)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style
|
|
|
|
|
.map(|style| style.fill_enabled)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(false),
|
|
|
|
|
);
|
|
|
|
|
let format = local(CellStylePropertyFlags::DATA_FORMAT)
|
|
|
|
|
.map(|style| style.value_format.clone())
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style
|
|
|
|
|
.map(|style| style.format_string.clone())
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_format",
|
|
|
|
|
format,
|
|
|
|
|
);
|
|
|
|
|
let data_type = cell
|
|
|
|
|
.contents
|
|
|
|
|
.first()
|
|
|
|
|
.map(|content| content.value.value_type)
|
|
|
|
|
.filter(|kind| *kind != CellValueType::Unknown)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
local(CellStylePropertyFlags::DATA_TYPE)
|
|
|
|
|
.map(|style| {
|
|
|
|
|
CellValueType::from(
|
|
|
|
|
style.value_data_type.max(0) as u32,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
document_row_style.map(|style| {
|
|
|
|
|
CellValueType::from(style.data_type.max(0) as u32)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(CellValueType::String);
|
|
|
|
|
let data_type = match data_type {
|
|
|
|
|
CellValueType::Long => "Integer",
|
|
|
|
|
CellValueType::Double => "Decimal",
|
|
|
|
|
CellValueType::Date => "Date",
|
|
|
|
|
CellValueType::Point2D => "Point 2D",
|
|
|
|
|
CellValueType::Point3D => "Point 3D",
|
|
|
|
|
CellValueType::Handle => "Handle",
|
|
|
|
|
_ => "Text",
|
|
|
|
|
};
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"tbl_cell_data_type",
|
|
|
|
|
data_type.to_string(),
|
|
|
|
|
);
|
|
|
|
|
for (field, property, fallback) in [
|
|
|
|
|
(
|
|
|
|
|
"tbl_cell_margin_left",
|
|
|
|
|
CellStylePropertyFlags::MARGIN_LEFT,
|
|
|
|
|
horizontal_margin,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"tbl_cell_margin_top",
|
|
|
|
|
CellStylePropertyFlags::MARGIN_TOP,
|
|
|
|
|
vertical_margin,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"tbl_cell_margin_right",
|
|
|
|
|
CellStylePropertyFlags::MARGIN_RIGHT,
|
|
|
|
|
horizontal_margin,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"tbl_cell_margin_bottom",
|
|
|
|
|
CellStylePropertyFlags::MARGIN_BOTTOM,
|
|
|
|
|
vertical_margin,
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
let value = local(property)
|
|
|
|
|
.map(|style| match field {
|
|
|
|
|
"tbl_cell_margin_left" => style.margin_left,
|
|
|
|
|
"tbl_cell_margin_top" => style.margin_top,
|
|
|
|
|
"tbl_cell_margin_right" => style.margin_right,
|
|
|
|
|
_ => style.margin_bottom,
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(fallback);
|
|
|
|
|
update_row_text(
|
|
|
|
|
&mut sections,
|
|
|
|
|
field,
|
|
|
|
|
crate::entities::common::format_length(value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let border_visible = |edge: CellEdgeFlags| {
|
|
|
|
|
let local_style = [
|
|
|
|
|
cell.style.as_ref(),
|
|
|
|
|
row.style.as_ref(),
|
|
|
|
|
table
|
|
|
|
|
.columns
|
|
|
|
|
.get(column_index)
|
|
|
|
|
.and_then(|column| column.style.as_ref()),
|
|
|
|
|
table.base_style.as_ref(),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
.find(|style| {
|
|
|
|
|
style.applied_border_edges.contains(edge)
|
|
|
|
|
});
|
|
|
|
|
if let Some(style) = local_style {
|
|
|
|
|
return if edge == CellEdgeFlags::TOP {
|
|
|
|
|
!style.top_border.invisible
|
|
|
|
|
} else if edge == CellEdgeFlags::RIGHT {
|
|
|
|
|
!style.right_border.invisible
|
|
|
|
|
} else if edge == CellEdgeFlags::BOTTOM {
|
|
|
|
|
!style.bottom_border.invisible
|
|
|
|
|
} else {
|
|
|
|
|
!style.left_border.invisible
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
document_row_style
|
|
|
|
|
.map(|style| {
|
|
|
|
|
if edge == CellEdgeFlags::TOP {
|
|
|
|
|
!style.top_border.is_invisible
|
|
|
|
|
} else if edge == CellEdgeFlags::RIGHT {
|
|
|
|
|
!style.right_border.is_invisible
|
|
|
|
|
} else if edge == CellEdgeFlags::BOTTOM {
|
|
|
|
|
!style.bottom_border.is_invisible
|
|
|
|
|
} else {
|
|
|
|
|
!style.left_border.is_invisible
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(true)
|
|
|
|
|
};
|
|
|
|
|
for (field, edge) in [
|
|
|
|
|
("tbl_cell_border_top", CellEdgeFlags::TOP),
|
|
|
|
|
("tbl_cell_border_right", CellEdgeFlags::RIGHT),
|
|
|
|
|
("tbl_cell_border_bottom", CellEdgeFlags::BOTTOM),
|
|
|
|
|
("tbl_cell_border_left", CellEdgeFlags::LEFT),
|
|
|
|
|
] {
|
|
|
|
|
update_row_toggle(
|
|
|
|
|
&mut sections,
|
|
|
|
|
field,
|
|
|
|
|
border_visible(edge),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-24 13:00:04 +03:00
|
|
|
}
|
2026-07-03 00:05:58 +03:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 19:26:11 +03:00
|
|
|
// Annotative Yes/No + a conditional "Annotative scale" row.
|
2026-08-24 15:32:37 +03:00
|
|
|
// Annotative state and assigned scale names need the
|
|
|
|
|
// document, so they are resolved here.
|
2026-07-12 19:26:11 +03:00
|
|
|
{
|
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
|
2026-08-26 14:19:01 +03:00
|
|
|
// (dimension / table / tolerance) — the existing field to insert it
|
2026-07-12 23:16:26 +03:00
|
|
|
// 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-08-20 15:33:09 +03:00
|
|
|
| acadrust::EntityType::Leader(_)
|
|
|
|
|
| acadrust::EntityType::Hatch(_) => Some(("annotative", None)),
|
2026-07-12 23:16:26 +03:00
|
|
|
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-08-26 14:19:01 +03:00
|
|
|
acadrust::EntityType::Tolerance(_) => {
|
|
|
|
|
Some(("annotative", Some("tol_dim_style")))
|
|
|
|
|
}
|
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 {
|
2026-08-26 15:32:37 +03:00
|
|
|
let is_anno = crate::scene::annotative::is_annotative(doc, entity)
|
|
|
|
|
|| match entity {
|
2026-08-27 01:00:23 +03:00
|
|
|
acadrust::EntityType::Dimension(
|
|
|
|
|
acadrust::entities::Dimension::Arc(dimension),
|
|
|
|
|
) => {
|
2026-08-26 15:32:37 +03:00
|
|
|
crate::scene::annotative::dim_style_is_annotative(
|
|
|
|
|
doc,
|
2026-08-27 01:00:23 +03:00
|
|
|
&dimension.base.style_name,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Tolerance(_) => {
|
|
|
|
|
crate::scene::annotative::annotation_style_is_annotative(
|
|
|
|
|
doc, entity,
|
2026-08-26 15:32:37 +03:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
2026-08-26 14:19:01 +03:00
|
|
|
// Dimensions/tables/tolerances carry no Annotative row yet — add one
|
2026-07-12 23:16:26 +03:00
|
|
|
// right after their style row.
|
|
|
|
|
if let Some(anchor) = insert_after {
|
|
|
|
|
insert_row_after(
|
|
|
|
|
&mut sections,
|
|
|
|
|
anchor,
|
|
|
|
|
crate::entities::common::ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Annotative").as_ref(),
|
2026-07-12 23:16:26 +03:00
|
|
|
"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
|
2026-08-24 15:32:37 +03:00
|
|
|
// remaining style-only types 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
|
|
|
),
|
2026-08-26 15:32:37 +03:00
|
|
|
acadrust::EntityType::Dimension(
|
|
|
|
|
acadrust::entities::Dimension::Arc(_),
|
|
|
|
|
) => set_row(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"annotative",
|
|
|
|
|
if is_anno { "Yes" } else { "No" }.to_string(),
|
|
|
|
|
),
|
2026-07-13 22:38:29 +03:00
|
|
|
acadrust::EntityType::Text(_)
|
2026-08-20 15:33:09 +03:00
|
|
|
| acadrust::EntityType::Insert(_)
|
2026-08-24 15:32:37 +03:00
|
|
|
| acadrust::EntityType::Hatch(_)
|
|
|
|
|
| acadrust::EntityType::Dimension(_) => set_row_value(
|
2026-07-13 22:38:29 +03:00
|
|
|
&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-08-24 15:32:37 +03:00
|
|
|
let memberships = crate::scene::annotative::object_scale_memberships(
|
|
|
|
|
doc,
|
|
|
|
|
entity.common().handle,
|
|
|
|
|
);
|
|
|
|
|
let assigned_scales = if memberships.is_empty() {
|
|
|
|
|
doc.header.current_annotation_scale.clone()
|
|
|
|
|
} else {
|
|
|
|
|
memberships
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(name, _)| name)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ")
|
|
|
|
|
};
|
2026-07-12 19:26:11 +03:00
|
|
|
insert_row_after(
|
|
|
|
|
&mut sections,
|
|
|
|
|
anno_field,
|
|
|
|
|
crate::entities::common::ro_prop(
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("Annotative scale").as_ref(),
|
2026-07-12 19:26:11 +03:00
|
|
|
"annotative_scale",
|
2026-08-24 15:32:37 +03:00
|
|
|
assigned_scales,
|
2026-07-12 19:26:11 +03:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 00:14:08 +03:00
|
|
|
// Single-line text height rows depend on both the
|
|
|
|
|
// justification and the active annotation scale. Aligned
|
|
|
|
|
// text derives its paper height from the two endpoints,
|
|
|
|
|
// while annotative text exposes that paper height and a
|
|
|
|
|
// separate calculated model height.
|
|
|
|
|
if let acadrust::EntityType::Text(text) = entity {
|
|
|
|
|
let aligned = matches!(
|
|
|
|
|
text.horizontal_alignment,
|
|
|
|
|
acadrust::entities::TextHorizontalAlignment::Aligned
|
|
|
|
|
);
|
|
|
|
|
let annotative = crate::scene::annotative::is_annotative(doc, entity);
|
|
|
|
|
let model_factor = if annotative {
|
|
|
|
|
annotation_scale_handle
|
|
|
|
|
.and_then(|handle| match doc.objects.get(&handle) {
|
|
|
|
|
Some(acadrust::objects::ObjectType::Scale(scale)) => Some(
|
|
|
|
|
scale.inverse_factor()
|
|
|
|
|
/ self.tabs[i].scene.annotation_scale_unit_factor(),
|
|
|
|
|
),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(self.tabs[i].scene.annotation_scale as f64)
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
let paper_height = if aligned {
|
|
|
|
|
crate::entities::text::text_run_placement_at_scale(
|
|
|
|
|
text,
|
|
|
|
|
doc,
|
|
|
|
|
model_factor as f32,
|
|
|
|
|
)
|
|
|
|
|
.height as f64
|
|
|
|
|
} else {
|
|
|
|
|
text.height
|
|
|
|
|
};
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
if let Some(row) =
|
|
|
|
|
section.props.iter_mut().find(|row| row.field == "height")
|
|
|
|
|
{
|
|
|
|
|
if annotative {
|
|
|
|
|
row.label = t!("Paper text height").into_owned();
|
|
|
|
|
}
|
|
|
|
|
if aligned {
|
|
|
|
|
row.value = crate::scene::model::object::PropValue::ReadOnly(
|
|
|
|
|
crate::entities::common::format_length(paper_height),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if annotative {
|
|
|
|
|
insert_row_after(
|
|
|
|
|
&mut sections,
|
|
|
|
|
"height",
|
|
|
|
|
crate::entities::common::ro_prop(
|
|
|
|
|
t!("Model text height").as_ref(),
|
|
|
|
|
"model_text_height",
|
|
|
|
|
crate::entities::common::format_length(
|
|
|
|
|
paper_height * model_factor,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-08-02 22:09:25 +03:00
|
|
|
label: t!("Group").into_owned(),
|
2026-03-22 22:44:32 +03:00
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
t!("External Reference").into_owned()
|
2026-05-15 09:19:57 +03:00
|
|
|
} 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(),
|
2026-08-02 22:09:25 +03:00
|
|
|
iced::widget::combo_box::State::new(
|
|
|
|
|
options
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.map(ui::properties::LocalizedChoice::new)
|
|
|
|
|
.collect(),
|
|
|
|
|
),
|
2026-03-22 22:44:32 +03:00
|
|
|
)),
|
|
|
|
|
_ => 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()),
|
|
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("%{count} objects selected", count = n).into_owned(),
|
2026-07-08 18:57:11 +02:00
|
|
|
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(),
|
|
|
|
|
),
|
|
|
|
|
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();
|
|
|
|
|
|
2026-08-04 23:17:54 +03:00
|
|
|
let plane = if self.tabs[i].editing_model_space() {
|
|
|
|
|
self.tabs[i].ucs_xform().working_plane()
|
|
|
|
|
} else {
|
|
|
|
|
crate::command::WorkingPlane::default()
|
|
|
|
|
};
|
|
|
|
|
let local_entities: Vec<(Handle, EntityType)> = filtered
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(handle, entity)| {
|
|
|
|
|
(*handle, dispatch::entity_in_working_plane(entity, plane))
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let local_refs: Vec<(Handle, &EntityType)> = local_entities
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(handle, entity)| (*handle, entity))
|
|
|
|
|
.collect();
|
2026-08-11 20:14:51 +03:00
|
|
|
let mut sections = aggregate_sections(&local_refs, &text_style_names);
|
|
|
|
|
sections.extend(aggregate_solid_history_sections(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&local_refs.iter().map(|(handle, _)| *handle).collect::<Vec<_>>(),
|
|
|
|
|
));
|
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(),
|
2026-08-02 22:09:25 +03:00
|
|
|
iced::widget::combo_box::State::new(
|
|
|
|
|
options
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.map(ui::properties::LocalizedChoice::new)
|
|
|
|
|
.collect(),
|
|
|
|
|
),
|
2026-03-22 22:44:32 +03:00
|
|
|
)),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
sections,
|
2026-08-02 22:09:25 +03:00
|
|
|
title: t!("%{count} objects selected", count = selected.len()).into_owned(),
|
2026-03-22 22:44:32 +03:00
|
|
|
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()),
|
|
|
|
|
lineweight_combo: iced::widget::combo_box::State::new(
|
|
|
|
|
ui::properties::lw_options(),
|
|
|
|
|
),
|
|
|
|
|
linetype_items,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-08-15 14:29:20 +01:00
|
|
|
// Precompute the focused-id → field-key map for O(1) lookups on
|
|
|
|
|
// `PropSyncActive`; derived from `sections`, so rebuild it here.
|
|
|
|
|
panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections);
|
2026-03-22 22:44:32 +03:00
|
|
|
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-08-12 23:23:50 +01:00
|
|
|
// The active-row highlight only survives a rebuild for the same
|
|
|
|
|
// selection (like the edit buffer); a selection change clears it so
|
|
|
|
|
// an old row isn't marked active against new content.
|
|
|
|
|
panel.active_field = if prev_handles == new_handles {
|
|
|
|
|
active_field
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
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-07-31 22:05:30 +03:00
|
|
|
panel.prop_vertex_indicator_active = prop_vertex_indicator_active;
|
2026-08-12 12:46:41 +03:00
|
|
|
let property_handles = panel.selected_handles();
|
|
|
|
|
let property_handles = if property_handles.is_empty() {
|
|
|
|
|
&panel.source_handles
|
|
|
|
|
} else {
|
|
|
|
|
&property_handles
|
|
|
|
|
};
|
|
|
|
|
let locked_only = !property_handles.is_empty()
|
|
|
|
|
&& property_handles
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
|
|
|
|
if locked_only {
|
|
|
|
|
make_sections_read_only(&mut panel.sections);
|
2026-08-15 14:29:20 +01:00
|
|
|
// Rows demoted to read-only no longer back an editable field;
|
|
|
|
|
// drop them from the id→key map so focus can't map onto them.
|
|
|
|
|
panel.field_key_by_id =
|
|
|
|
|
crate::ui::properties::build_field_key_map(&panel.sections);
|
2026-08-12 12:46:41 +03:00
|
|
|
panel.edit_buf.clear();
|
2026-08-12 23:23:50 +01:00
|
|
|
panel.active_field = None;
|
2026-08-12 12:46:41 +03:00
|
|
|
panel.color_picker_open = false;
|
|
|
|
|
panel.color_palette_open = false;
|
|
|
|
|
panel.bg_color_picker_open = false;
|
|
|
|
|
panel.open_color_field = None;
|
|
|
|
|
panel.hatch_pattern_picker_open = false;
|
|
|
|
|
panel.edit_choice_open = false;
|
|
|
|
|
}
|
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-08-12 12:46:41 +03:00
|
|
|
let locked_active_grip = self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
|
|
|
|
grip.targets
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|target| self.tabs[i].scene.is_layer_locked(target.handle))
|
|
|
|
|
});
|
|
|
|
|
if locked_active_grip {
|
|
|
|
|
self.cancel_active_grip_edit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
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-07-31 22:05:30 +03:00
|
|
|
let (new_handle, new_grips, new_grip_handles) = {
|
2026-08-02 12:00:15 +03:00
|
|
|
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
|
2026-03-22 22:44:32 +03:00
|
|
|
let selected = self.tabs[i].scene.selected_entities();
|
2026-08-12 12:46:41 +03:00
|
|
|
let single_handle = (selected.len() == 1
|
|
|
|
|
&& !self.tabs[i].scene.is_layer_locked(selected[0].0))
|
|
|
|
|
.then(|| selected[0].0);
|
2026-07-31 22:05:30 +03:00
|
|
|
let mut grips = Vec::new();
|
|
|
|
|
let mut handles = Vec::new();
|
|
|
|
|
for (handle, entity) in selected {
|
2026-08-12 12:46:41 +03:00
|
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-02 12:00:15 +03:00
|
|
|
let contextual = crate::scene::annotative::entity_for_annotation_context(
|
2026-07-26 22:30:14 +03:00
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
entity,
|
2026-08-02 12:00:15 +03:00
|
|
|
annotation_scale_handle,
|
2026-07-26 22:30:14 +03:00
|
|
|
);
|
2026-08-11 19:59:24 +03:00
|
|
|
let mut entity_grips = dispatch::grips(contextual.as_ref());
|
2026-08-13 09:46:56 -03:00
|
|
|
// Dimension::grips() cannot see the document, so an automatic dimension
|
|
|
|
|
// text grip cannot resolve its real DIMSTYLE/annotation-scaled position
|
|
|
|
|
// there. Correct it here, where both the document and displayed annotation
|
|
|
|
|
// scale are available.
|
|
|
|
|
if let acadrust::EntityType::Dimension(dim) = contextual.as_ref() {
|
|
|
|
|
if matches!(
|
|
|
|
|
dim,
|
|
|
|
|
acadrust::entities::Dimension::Linear(_)
|
|
|
|
|
| acadrust::entities::Dimension::Aligned(_)
|
|
|
|
|
) && !dim.base().text_user_positioned
|
|
|
|
|
{
|
|
|
|
|
let anno_scale = annotation_scale_handle
|
|
|
|
|
.and_then(|handle| {
|
|
|
|
|
match self.tabs[i].scene.document.objects.get(&handle) {
|
|
|
|
|
Some(acadrust::objects::ObjectType::Scale(scale)) => Some(
|
|
|
|
|
scale.inverse_factor()
|
|
|
|
|
/ self.tabs[i].scene.annotation_scale_unit_factor(),
|
|
|
|
|
),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(self.tabs[i].scene.annotation_scale as f64);
|
|
|
|
|
|
|
|
|
|
if let Some(position) =
|
|
|
|
|
crate::entities::dimension::dimension_text_grip_position(
|
|
|
|
|
dim,
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
anno_scale,
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
// The text grip is the final native grip of Linear/Aligned dims.
|
|
|
|
|
// While the text is still automatic, make this a point/stretch grip
|
|
|
|
|
// rather than a midpoint-translate grip. That makes the first drag use
|
|
|
|
|
// the displayed automatic position as its absolute starting point instead
|
|
|
|
|
// of translating the stale DWG text_middle_point.
|
|
|
|
|
if let Some(text_grip) = entity_grips.last_mut() {
|
|
|
|
|
text_grip.world =
|
|
|
|
|
glam::DVec3::new(position.x, position.y, position.z);
|
|
|
|
|
text_grip.is_midpoint = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-11 19:59:24 +03:00
|
|
|
entity_grips.extend(crate::scene::model::solid_history::primitive_grips(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
handle,
|
|
|
|
|
));
|
|
|
|
|
for mut grip in entity_grips {
|
2026-07-31 22:05:30 +03:00
|
|
|
// Subtract in f64: at UTM magnitudes an f32 cast before
|
|
|
|
|
// the offset costs ~1 unit and draws the grip off the wire.
|
|
|
|
|
grip.world.x -= wo[0];
|
|
|
|
|
grip.world.y -= wo[1];
|
|
|
|
|
grip.world.z -= wo[2];
|
|
|
|
|
handles.push(handle);
|
|
|
|
|
grips.push(grip);
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
2026-07-31 22:05:30 +03:00
|
|
|
(single_handle, grips, handles)
|
2026-03-22 22:44:32 +03:00
|
|
|
};
|
|
|
|
|
self.tabs[i].selected_handle = new_handle;
|
|
|
|
|
self.tabs[i].selected_grips = new_grips;
|
2026-07-31 22:05:30 +03:00
|
|
|
self.tabs[i].selected_grip_handles = new_grip_handles;
|
|
|
|
|
let available: rustc_hash::FxHashSet<_> = self.tabs[i]
|
|
|
|
|
.selected_grip_handles
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.zip(self.tabs[i].selected_grips.iter().map(|grip| grip.id))
|
|
|
|
|
.collect();
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.hot_grips
|
|
|
|
|
.retain(|key| available.contains(key));
|
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> {
|
2026-08-12 12:46:41 +03:00
|
|
|
let mut handles = self.tabs[i].properties.selected_handles();
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
handles = self.tabs[i].properties.source_handles.clone();
|
|
|
|
|
}
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
handles.extend(self.tabs[i].selected_handle);
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
2026-08-12 12:46:41 +03:00
|
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
|
|
|
handles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn has_property_selection(&self, i: usize) -> bool {
|
|
|
|
|
!self.tabs[i].properties.selected_handles().is_empty()
|
|
|
|
|
|| !self.tabs[i].properties.source_handles.is_empty()
|
|
|
|
|
|| self.tabs[i].selected_handle.is_some()
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
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 {
|
fix(dim): draw a dimension from the picture it carries
A DIMENSION references the block "that contains the entities that make up
the dimension picture", and that block is the picture: AutoCAD requires
it and draws it, BricsCAD draws it when present and only renders from the
dimension variables when it is missing. OCS re-derived the picture from
DIMVARS every time and ignored the block entirely.
That works while a style agrees with what the drawing actually drew. A
Revit-exported schedule here does not: its style stores DIMTXT already in
drawing units beside a DIMSCALE meant to scale it, so the text came out
three hundred times too tall, and each dimension carries a DIMFXL
override that is likewise already in drawing units, so the extension
lines ran nearly a million units past the points they measure.
Neither is guesswork about units now. The block holds the geometry the
drawing was authored with, so it is drawn: the contents are already in
world space and the block's base point is the origin, and the identity
instance that carries them exists only to reuse the scene graph's
ByBlock / layer-0 colour resolution. Every wire answers to the dimension,
so a pick still selects the dimension rather than one of its pieces.
Re-deriving stays as the fallback for a dimension with no block.
Annotative dimensions keep the old path: their several representations
are separate blocks and choosing between them is what the annotation
machinery already does, while the doctrine above assumes one picture.
A picture is only good while its inputs hold, so it is dropped when they
change: editing a dimension's properties, and saving the dimension style
it is drawn with. Entity edits already dropped it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:44:30 +03:00
|
|
|
// A dimension is drawn from the block holding its picture, and that
|
|
|
|
|
// picture was made under the settings just edited. Drop it so the
|
|
|
|
|
// dimension is drawn afresh — otherwise the edit changes the stored
|
|
|
|
|
// variables and nothing on screen.
|
|
|
|
|
if matches!(
|
|
|
|
|
self.tabs[i].scene.document.get_entity(handle),
|
|
|
|
|
Some(acadrust::EntityType::Dimension(_))
|
|
|
|
|
) {
|
|
|
|
|
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
|
|
|
|
}
|
2026-08-02 12:00:15 +03:00
|
|
|
context_object_changed |= self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.sync_displayed_annotation_context(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;
|
2026-08-24 13:00:04 +03:00
|
|
|
if let acadrust::EntityType::Table(table) = &mut entity {
|
|
|
|
|
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
|
|
|
|
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
|
|
|
|
use acadrust::entities::table::CellStateFlags;
|
|
|
|
|
use acadrust::objects::{
|
|
|
|
|
ClassObject, ClassObjectData, DataLink, ObjectType,
|
|
|
|
|
};
|
|
|
|
|
let document = &mut self.tabs[i].scene.document;
|
|
|
|
|
let link_handle = document.allocate_handle();
|
|
|
|
|
let mut object = ClassObject::new(ClassObjectData::DataLink(DataLink {
|
|
|
|
|
data_adapter: "CSV".to_string(),
|
|
|
|
|
description: path.clone(),
|
|
|
|
|
tooltip: path.clone(),
|
|
|
|
|
connection_string: path.clone(),
|
|
|
|
|
status_flags: 1,
|
|
|
|
|
update_status: "Linked".to_string(),
|
|
|
|
|
..DataLink::default()
|
|
|
|
|
}));
|
|
|
|
|
object.handle = link_handle;
|
|
|
|
|
document
|
|
|
|
|
.objects
|
|
|
|
|
.insert(link_handle, ObjectType::ClassObject(object));
|
|
|
|
|
let rows = table.row_count() as i32;
|
|
|
|
|
let columns = table.column_count() as i32;
|
|
|
|
|
for row in &mut table.rows {
|
|
|
|
|
for cell in &mut row.cells {
|
|
|
|
|
cell.has_linked_data = true;
|
|
|
|
|
cell.data_link_handle = Some(link_handle);
|
|
|
|
|
cell.data_link_rows = rows;
|
|
|
|
|
cell.data_link_columns = columns;
|
|
|
|
|
cell.state.insert(
|
|
|
|
|
CellStateFlags::LINKED
|
|
|
|
|
| CellStateFlags::CONTENT_LOCKED
|
|
|
|
|
| CellStateFlags::FORMAT_LOCKED,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
table.name = std::path::Path::new(&path)
|
|
|
|
|
.file_stem()
|
|
|
|
|
.and_then(|value| value.to_str())
|
|
|
|
|
.unwrap_or("Linked table")
|
|
|
|
|
.to_string();
|
|
|
|
|
table.description = path;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-01 19:09:19 +03:00
|
|
|
let tracks_draw_anchor = self.tabs[i].active_cmd.is_some()
|
|
|
|
|
&& matches!(
|
|
|
|
|
&entity,
|
|
|
|
|
acadrust::EntityType::Line(_)
|
|
|
|
|
| acadrust::EntityType::Arc(_)
|
|
|
|
|
| acadrust::EntityType::LwPolyline(_)
|
|
|
|
|
| acadrust::EntityType::Polyline(_)
|
|
|
|
|
| acadrust::EntityType::Polyline2D(_)
|
|
|
|
|
| acadrust::EntityType::Polyline3D(_)
|
|
|
|
|
);
|
2026-03-22 22:44:32 +03:00
|
|
|
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-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
|
2026-08-17 00:36:49 +07:00
|
|
|
// 1 source-unit equals the matching host length.
|
2026-05-20 05:43:55 +03:00
|
|
|
if let acadrust::EntityType::Insert(ref mut ins) = entity {
|
2026-08-17 00:36:49 +07:00
|
|
|
let host_units = self.tabs[i].scene.document.header.insertion_units;
|
|
|
|
|
let src_units = self.tabs[i]
|
2026-05-20 05:43:55 +03:00
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&ins.block_name)
|
|
|
|
|
.map(|br| br.units)
|
|
|
|
|
.unwrap_or(0);
|
2026-08-17 00:36:49 +07:00
|
|
|
if let Some(ratio) = insert_unit_scale(host_units, src_units) {
|
2026-08-17 21:32:28 +03:00
|
|
|
if !apply_insert_unit_scale(ins, ratio) {
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
t!("INSERT unit scale is outside the supported range.").as_ref(),
|
|
|
|
|
);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-05-20 05:43:55 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2026-08-03 16:43:37 +03:00
|
|
|
crate::scene::creation_style::apply_current_creation_styles(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&mut entity,
|
|
|
|
|
);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-08-21 12:05:18 +03:00
|
|
|
// Smart centre objects carry their own drawing-level creation style.
|
2026-08-21 11:01:18 +03:00
|
|
|
// Apply it after the generic ribbon style so ordinary LINE entities
|
|
|
|
|
// keep the existing path while centre lines honour their settings.
|
2026-08-21 12:05:18 +03:00
|
|
|
let center_line = acadrust::entities::CenterLineAssociation::read(
|
2026-08-21 11:01:18 +03:00
|
|
|
&entity.common().extended_data,
|
2026-08-21 12:05:18 +03:00
|
|
|
).is_some();
|
|
|
|
|
let center_mark = acadrust::entities::CenterMarkAssociation::read(
|
|
|
|
|
&entity.common().extended_data,
|
|
|
|
|
).is_some();
|
|
|
|
|
if center_line || center_mark {
|
2026-08-21 11:01:18 +03:00
|
|
|
let settings = self.tabs[i].scene.centerline_settings();
|
|
|
|
|
if !settings.layer.eq_ignore_ascii_case("Current") {
|
|
|
|
|
entity.common_mut().layer = settings.layer;
|
|
|
|
|
}
|
|
|
|
|
if !settings.linetype.eq_ignore_ascii_case("Current") {
|
|
|
|
|
entity.common_mut().linetype = settings.linetype;
|
|
|
|
|
}
|
|
|
|
|
entity.common_mut().linetype_scale = settings.linetype_scale;
|
2026-08-21 12:05:18 +03:00
|
|
|
let application = if center_mark {
|
|
|
|
|
acadrust::entities::CENTERMARK_XDATA_APPLICATION
|
|
|
|
|
} else {
|
|
|
|
|
acadrust::entities::CENTERLINE_XDATA_APPLICATION
|
|
|
|
|
};
|
2026-08-21 11:01:18 +03:00
|
|
|
if !self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.app_ids
|
2026-08-21 12:05:18 +03:00
|
|
|
.contains(application)
|
2026-08-21 11:01:18 +03:00
|
|
|
{
|
2026-08-21 12:05:18 +03:00
|
|
|
let mut app = acadrust::tables::AppId::new(application);
|
2026-08-21 11:01:18 +03:00
|
|
|
app.handle = self.tabs[i].scene.document.allocate_handle();
|
|
|
|
|
let _ = self.tabs[i].scene.document.app_ids.add(app);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 12:00:15 +03:00
|
|
|
let text_style_annotative = match &entity {
|
|
|
|
|
acadrust::EntityType::Text(text) => {
|
|
|
|
|
crate::scene::annotative::text_style_is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&text.style,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::MText(text) => {
|
|
|
|
|
crate::scene::annotative::text_style_is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&text.style,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::AttributeEntity(attribute) => {
|
|
|
|
|
crate::scene::annotative::text_style_is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&attribute.text_style,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::AttributeDefinition(attribute) => {
|
|
|
|
|
crate::scene::annotative::text_style_is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&attribute.text_style,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
|
|
|
|
if text_style_annotative {
|
|
|
|
|
match &mut entity {
|
|
|
|
|
acadrust::EntityType::MText(text) => text.is_annotative = true,
|
|
|
|
|
acadrust::EntityType::AttributeEntity(attribute) => {
|
|
|
|
|
attribute.flags.annotative = true
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::AttributeDefinition(attribute) => {
|
|
|
|
|
attribute.flags.annotative = true
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let needs_annotation_context = crate::scene::annotative::is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&entity,
|
|
|
|
|
) || crate::scene::annotative::annotation_style_is_annotative(
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
&entity,
|
|
|
|
|
);
|
2026-05-01 21:00:27 +03:00
|
|
|
|
2026-08-01 19:09:19 +03:00
|
|
|
let new_handle = if matches!(&entity, acadrust::EntityType::Viewport(_))
|
2026-03-22 22:44:32 +03:00
|
|
|
&& 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);
|
|
|
|
|
}
|
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
|
2026-07-28 17:16:27 +03:00
|
|
|
// bypasses Scene::add_entity; publish the exact new handle
|
|
|
|
|
// so only its border is tessellated.
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.bump_entities(&[(new_handle, crate::scene::ChangeKind::Added)]);
|
2026-05-31 14:44:18 +03:00
|
|
|
Some(new_handle)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
.push_error(crate::tf!("Viewport could not be added: {e}").as_ref());
|
2026-05-31 14:44:18 +03:00
|
|
|
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-08-01 19:09:19 +03:00
|
|
|
};
|
|
|
|
|
|
2026-08-02 12:00:15 +03:00
|
|
|
if needs_annotation_context {
|
|
|
|
|
if let (Some(handle), Some(scale)) = (
|
|
|
|
|
new_handle,
|
|
|
|
|
self.tabs[i].scene.creation_annotation_scale_handle(),
|
|
|
|
|
) {
|
|
|
|
|
crate::scene::annotative::create_annotation_context(
|
|
|
|
|
&mut self.tabs[i].scene.document,
|
|
|
|
|
handle,
|
|
|
|
|
scale,
|
|
|
|
|
);
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:09:19 +03:00
|
|
|
if tracks_draw_anchor {
|
|
|
|
|
if let Some(handle) = new_handle {
|
|
|
|
|
self.tabs[i].last_draw_anchor = Some(handle);
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
2026-08-01 19:09:19 +03:00
|
|
|
new_handle
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 12:46:41 +03:00
|
|
|
fn make_sections_read_only(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
) {
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
|
|
|
|
|
|
|
|
|
for property in sections
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.flat_map(|section| section.props.iter_mut())
|
|
|
|
|
{
|
|
|
|
|
let text = match &property.value {
|
|
|
|
|
PropValue::ReadOnly(value)
|
2026-08-23 21:02:59 +01:00
|
|
|
| PropValue::ReadOnlyWithTooltip { value, .. }
|
2026-08-12 12:46:41 +03:00
|
|
|
| PropValue::EditText(value)
|
2026-08-20 14:58:12 +03:00
|
|
|
| PropValue::PlainText(value)
|
2026-08-12 12:46:41 +03:00
|
|
|
| PropValue::LayerChoice(value)
|
|
|
|
|
| PropValue::LinetypeChoice(value)
|
|
|
|
|
| PropValue::HatchPatternChoice(value) => value.clone(),
|
|
|
|
|
PropValue::Choice { selected, .. } => selected.clone(),
|
|
|
|
|
PropValue::EditChoice { value, .. } => value.clone(),
|
|
|
|
|
PropValue::ColorChoice(color) => match color {
|
|
|
|
|
acadrust::types::Color::None => "None".to_string(),
|
|
|
|
|
acadrust::types::Color::ByLayer => "ByLayer".to_string(),
|
|
|
|
|
acadrust::types::Color::ByBlock => "ByBlock".to_string(),
|
|
|
|
|
acadrust::types::Color::Index(index) => index.to_string(),
|
|
|
|
|
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
|
|
|
|
},
|
|
|
|
|
PropValue::ColorVaries | PropValue::LwVaries => VARIES_LABEL.to_string(),
|
|
|
|
|
PropValue::LwChoice(lineweight) => {
|
|
|
|
|
ui::properties::LwItem(*lineweight).to_string()
|
|
|
|
|
}
|
|
|
|
|
PropValue::BoolToggle { value, .. } => {
|
|
|
|
|
if *value { t!("Yes") } else { t!("No") }.into_owned()
|
|
|
|
|
}
|
|
|
|
|
PropValue::Stepper { display, .. } => display.clone(),
|
|
|
|
|
PropValue::AttrText { value, .. } => value.clone(),
|
|
|
|
|
};
|
|
|
|
|
property.field = "locked_read_only";
|
|
|
|
|
property.value = PropValue::ReadOnly(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: format!("{} ({})", t!("All").into_owned(), selected.len()),
|
2026-03-22 22:44:32 +03:00
|
|
|
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 {
|
2026-08-02 22:09:25 +03:00
|
|
|
label: format!("{}({})", t!(title_case_word(&kind)), handles.len()),
|
2026-03-22 22:44:32 +03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-08-21 20:52:03 +03:00
|
|
|
// Sum the filled area while individual Area rows may still vary.
|
2026-08-21 12:57:19 +03:00
|
|
|
if selected.len() > 1
|
|
|
|
|
&& selected
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|(_, entity)| matches!(entity, acadrust::EntityType::Hatch(_)))
|
|
|
|
|
{
|
|
|
|
|
let total = selected
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(_, entity)| match entity {
|
|
|
|
|
acadrust::EntityType::Hatch(hatch) => {
|
|
|
|
|
Some(crate::entities::hatch::boundary_area(hatch))
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.sum::<f64>();
|
|
|
|
|
set_row(&mut result, "cumulative_area", format!("{total:.4}"));
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 20:14:51 +03:00
|
|
|
fn aggregate_solid_history_sections(
|
|
|
|
|
document: &acadrust::CadDocument,
|
|
|
|
|
handles: &[Handle],
|
|
|
|
|
) -> Vec<crate::scene::model::object::PropSection> {
|
|
|
|
|
let mut sections = handles.iter().map(|handle| {
|
|
|
|
|
crate::scene::model::solid_history::primitive_properties(document, *handle)
|
|
|
|
|
});
|
|
|
|
|
let Some(mut merged) = sections.next() else {
|
|
|
|
|
return Vec::new();
|
|
|
|
|
};
|
|
|
|
|
if merged.is_empty() {
|
|
|
|
|
return Vec::new();
|
|
|
|
|
}
|
|
|
|
|
for next in sections {
|
|
|
|
|
if next.is_empty() {
|
|
|
|
|
return Vec::new();
|
|
|
|
|
}
|
|
|
|
|
merged = merge_sections(&merged, &next);
|
|
|
|
|
}
|
|
|
|
|
merged
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
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,
|
|
|
|
|
..
|
|
|
|
|
},
|
2026-08-23 21:02:59 +01:00
|
|
|
) => {
|
|
|
|
|
let mut merged_options = options.clone();
|
|
|
|
|
for opt in other_options {
|
|
|
|
|
if !merged_options.contains(opt) {
|
|
|
|
|
merged_options.push(opt.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
PropValue::Choice {
|
|
|
|
|
selected: VARIES_LABEL.into(),
|
|
|
|
|
options: merged_options,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-22 11:11:10 +03:00
|
|
|
(
|
|
|
|
|
PropValue::EditChoice { options, .. },
|
|
|
|
|
PropValue::EditChoice {
|
|
|
|
|
options: other_options,
|
|
|
|
|
..
|
|
|
|
|
},
|
2026-08-23 21:02:59 +01:00
|
|
|
) => {
|
|
|
|
|
let mut merged_options = options.clone();
|
|
|
|
|
for opt in other_options {
|
|
|
|
|
if !merged_options.contains(opt) {
|
|
|
|
|
merged_options.push(opt.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
PropValue::EditChoice {
|
|
|
|
|
value: VARIES_LABEL.into(),
|
|
|
|
|
options: merged_options,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
(PropValue::EditText(_), PropValue::EditText(_)) => {
|
|
|
|
|
PropValue::EditText(VARIES_LABEL.into())
|
|
|
|
|
}
|
2026-08-20 14:58:12 +03:00
|
|
|
(PropValue::PlainText(_), PropValue::PlainText(_)) => {
|
|
|
|
|
PropValue::PlainText(VARIES_LABEL.into())
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
|
|
|
|
PropValue::ReadOnly(VARIES_LABEL.into())
|
|
|
|
|
}
|
2026-08-23 21:02:59 +01:00
|
|
|
(PropValue::ReadOnlyWithTooltip { tooltip, .. }, PropValue::ReadOnlyWithTooltip { .. }) => {
|
|
|
|
|
PropValue::ReadOnlyWithTooltip {
|
|
|
|
|
value: VARIES_LABEL.into(),
|
|
|
|
|
tooltip: tooltip.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(PropValue::ReadOnly(_), PropValue::ReadOnlyWithTooltip { tooltip, .. })
|
|
|
|
|
| (PropValue::ReadOnlyWithTooltip { tooltip, .. }, PropValue::ReadOnly(_)) => {
|
|
|
|
|
PropValue::ReadOnlyWithTooltip {
|
|
|
|
|
value: VARIES_LABEL.into(),
|
|
|
|
|
tooltip: tooltip.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
(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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 18:09:34 +03:00
|
|
|
fn update_row_text(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
value: String,
|
|
|
|
|
) {
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
match &mut row.value {
|
|
|
|
|
PropValue::ReadOnly(current)
|
|
|
|
|
| PropValue::EditText(current)
|
|
|
|
|
| PropValue::PlainText(current) => *current = value,
|
|
|
|
|
PropValue::ReadOnlyWithTooltip { value: current, .. } => *current = value,
|
|
|
|
|
PropValue::Choice { selected, .. } => *selected = value,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_row_toggle(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
value: bool,
|
|
|
|
|
) {
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
match &mut row.value {
|
|
|
|
|
PropValue::BoolToggle { value: current, .. } => *current = value,
|
|
|
|
|
PropValue::ReadOnly(current) => {
|
|
|
|
|
*current = if value { t!("Yes") } else { t!("No") }.into_owned()
|
|
|
|
|
}
|
|
|
|
|
PropValue::ReadOnlyWithTooltip { value: current, .. } => {
|
|
|
|
|
*current = if value { t!("Yes") } else { t!("No") }.into_owned()
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_row_color(
|
|
|
|
|
sections: &mut [crate::scene::model::object::PropSection],
|
|
|
|
|
field: &str,
|
|
|
|
|
color: acadrust::types::Color,
|
|
|
|
|
) {
|
|
|
|
|
use crate::scene::model::object::PropValue;
|
|
|
|
|
let label = match color {
|
|
|
|
|
acadrust::types::Color::None => "None".to_string(),
|
|
|
|
|
acadrust::types::Color::ByLayer => "ByLayer".to_string(),
|
|
|
|
|
acadrust::types::Color::ByBlock => "ByBlock".to_string(),
|
|
|
|
|
acadrust::types::Color::Index(index) => index.to_string(),
|
|
|
|
|
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
|
|
|
|
};
|
|
|
|
|
for section in sections.iter_mut() {
|
|
|
|
|
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
match &mut row.value {
|
|
|
|
|
PropValue::ColorChoice(current) => *current = color,
|
|
|
|
|
PropValue::ReadOnly(current) => *current = label,
|
|
|
|
|
PropValue::ReadOnlyWithTooltip { value: current, .. } => *current = label,
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
/// The lineweight dropdown options (named defaults + the standard millimetre
|
|
|
|
|
/// steps), matching the labels `dim_lineweight_label` produces.
|
|
|
|
|
pub(crate) fn lineweight_options() -> Vec<String> {
|
2026-08-24 14:10:14 +03:00
|
|
|
crate::entities::common::lineweight_options()
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 {
|
2026-08-24 14:10:14 +03:00
|
|
|
crate::entities::common::lineweight_label(dimlwd)
|
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-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",
|
2026-08-17 21:32:28 +03:00
|
|
|
22 => "US Survey Inches",
|
|
|
|
|
23 => "US Survey Yards",
|
|
|
|
|
24 => "US Survey Miles",
|
|
|
|
|
0 => "Unitless",
|
|
|
|
|
_ => "Unknown",
|
2026-07-03 00:05:58 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 21:32:28 +03:00
|
|
|
/// Unit-conversion scale for a new INSERT.
|
2026-08-17 00:36:49 +07:00
|
|
|
fn insert_unit_scale(host_units: i16, src_units: i16) -> Option<f64> {
|
2026-08-17 21:32:28 +03:00
|
|
|
let host_mm = insunits_to_mm(host_units)?;
|
|
|
|
|
let src_mm = insunits_to_mm(src_units)?;
|
|
|
|
|
let ratio = src_mm / host_mm;
|
2026-08-17 00:36:49 +07:00
|
|
|
if !ratio.is_finite() || (ratio - 1.0).abs() <= 1e-9 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
Some(ratio)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 21:32:28 +03:00
|
|
|
fn format_unit_factor(factor: f64) -> String {
|
|
|
|
|
let magnitude = factor.abs();
|
|
|
|
|
if magnitude > 0.0 && !(1.0e-4..1.0e7).contains(&magnitude) {
|
|
|
|
|
format!("{factor:.4e}")
|
|
|
|
|
} else {
|
|
|
|
|
format!("{factor:.4}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 05:43:55 +03:00
|
|
|
/// Convert INSUNITS (DXF group 70) to millimetres.
|
2026-08-17 21:32:28 +03:00
|
|
|
fn insunits_to_mm(code: i16) -> Option<f64> {
|
|
|
|
|
Some(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.495_978_707e14, // Astronomical Units
|
|
|
|
|
19 => 9.460_730_472_580_8e18, // Light Years
|
|
|
|
|
20 => 3.085_677_581_491_367_3e19, // Parsecs
|
|
|
|
|
21 => 1_200_000.0 / 3_937.0, // US Survey Feet
|
|
|
|
|
22 => 100_000.0 / 3_937.0, // US Survey Inches
|
|
|
|
|
23 => 3_600_000.0 / 3_937.0, // US Survey Yards
|
|
|
|
|
24 => 6_336_000_000.0 / 3_937.0, // US Survey Miles
|
|
|
|
|
_ => return None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_insert_unit_scale(ins: &mut acadrust::entities::Insert, ratio: f64) -> bool {
|
|
|
|
|
const MIN_INSERT_SCALE: f64 = 1.0e-12;
|
|
|
|
|
if [ins.x_scale(), ins.y_scale(), ins.z_scale()]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|scale| scale * ratio)
|
|
|
|
|
.any(|scale| !scale.is_finite() || scale.abs() < MIN_INSERT_SCALE)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
2026-05-20 05:43:55 +03:00
|
|
|
}
|
2026-08-17 21:32:28 +03:00
|
|
|
|
|
|
|
|
let origin = ins.get_transform().apply(Vector3::ZERO);
|
|
|
|
|
ins.apply_transform(&Transform::from_translation(-origin));
|
|
|
|
|
ins.apply_transform(&Transform::from_scale(ratio));
|
|
|
|
|
ins.apply_transform(&Transform::from_translation(origin));
|
|
|
|
|
true
|
2026-05-20 05:43:55 +03:00
|
|
|
}
|
2026-08-17 00:36:49 +07:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod insert_unit_scale_tests {
|
2026-08-17 21:32:28 +03:00
|
|
|
use super::{
|
|
|
|
|
apply_insert_unit_scale, format_unit_factor, insert_unit_scale, insunits_to_mm,
|
|
|
|
|
};
|
|
|
|
|
use acadrust::entities::{AttributeEntity, Insert};
|
|
|
|
|
use acadrust::types::Vector3;
|
2026-08-17 00:36:49 +07:00
|
|
|
|
|
|
|
|
const UNITLESS: i16 = 0;
|
|
|
|
|
const INCHES: i16 = 1;
|
|
|
|
|
const MILLIMETERS: i16 = 4;
|
|
|
|
|
const CENTIMETERS: i16 = 5;
|
|
|
|
|
const METERS: i16 = 6;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unitless_block_is_inserted_as_authored() {
|
|
|
|
|
// Reported case: a unitless block with a 1000-unit edge inserted into a
|
|
|
|
|
// millimetre drawing must keep that edge, not gain a conversion factor.
|
|
|
|
|
assert_eq!(insert_unit_scale(MILLIMETERS, UNITLESS), None);
|
|
|
|
|
assert_eq!(insert_unit_scale(INCHES, UNITLESS), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unitless_drawing_does_not_scale_a_measured_block() {
|
|
|
|
|
assert_eq!(insert_unit_scale(UNITLESS, METERS), None);
|
|
|
|
|
assert_eq!(insert_unit_scale(UNITLESS, INCHES), None);
|
|
|
|
|
assert_eq!(insert_unit_scale(UNITLESS, UNITLESS), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn matching_units_do_not_scale() {
|
|
|
|
|
assert_eq!(insert_unit_scale(METERS, METERS), None);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 21:32:28 +03:00
|
|
|
#[test]
|
|
|
|
|
fn unknown_units_do_not_scale() {
|
|
|
|
|
assert_eq!(insunits_to_mm(0), None);
|
|
|
|
|
assert_eq!(insunits_to_mm(25), None);
|
|
|
|
|
assert_eq!(insert_unit_scale(MILLIMETERS, 25), None);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 00:36:49 +07:00
|
|
|
#[test]
|
|
|
|
|
fn differing_units_convert_through_millimetres() {
|
|
|
|
|
let ratio = insert_unit_scale(MILLIMETERS, METERS).expect("metres into mm should scale");
|
|
|
|
|
assert!((ratio - 1000.0).abs() < 1e-9, "got {ratio}");
|
|
|
|
|
|
|
|
|
|
let ratio = insert_unit_scale(MILLIMETERS, INCHES).expect("inches into mm should scale");
|
|
|
|
|
assert!((ratio - 25.4).abs() < 1e-9, "got {ratio}");
|
|
|
|
|
|
|
|
|
|
let ratio = insert_unit_scale(METERS, CENTIMETERS).expect("cm into m should scale");
|
|
|
|
|
assert!((ratio - 0.01).abs() < 1e-12, "got {ratio}");
|
|
|
|
|
}
|
2026-08-17 21:32:28 +03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn survey_units_have_expected_ratios() {
|
|
|
|
|
let survey_foot = insunits_to_mm(21).expect("survey feet");
|
|
|
|
|
let survey_inch = insunits_to_mm(22).expect("survey inches");
|
|
|
|
|
let survey_yard = insunits_to_mm(23).expect("survey yards");
|
|
|
|
|
let survey_mile = insunits_to_mm(24).expect("survey miles");
|
|
|
|
|
|
|
|
|
|
assert!((survey_foot / survey_inch - 12.0).abs() < 1e-12);
|
|
|
|
|
assert!((survey_yard / survey_foot - 3.0).abs() < 1e-12);
|
|
|
|
|
assert!((survey_mile / survey_foot - 5280.0).abs() < 1e-9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn astronomical_units_use_precise_si_values() {
|
|
|
|
|
assert_eq!(insunits_to_mm(18), Some(1.495_978_707e14));
|
|
|
|
|
assert_eq!(insunits_to_mm(19), Some(9.460_730_472_580_8e18));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
insunits_to_mm(20),
|
|
|
|
|
Some(3.085_677_581_491_367_3e19)
|
|
|
|
|
);
|
|
|
|
|
assert_ne!(format_unit_factor(1.0e-7), "0.0000");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn applying_unit_scale_composes_with_insert_transform() {
|
|
|
|
|
let mut ins = Insert::new("Block", Vector3::new(12.0, -4.0, 3.0));
|
|
|
|
|
ins.set_x_scale(-2.0);
|
|
|
|
|
ins.set_y_scale(3.0);
|
|
|
|
|
ins.set_z_scale(-4.0);
|
|
|
|
|
ins.rotation = 0.37;
|
|
|
|
|
let insertion = ins.get_transform().apply(Vector3::ZERO);
|
|
|
|
|
let attribute_position = insertion + Vector3::new(2.0, -1.0, 0.5);
|
|
|
|
|
let mut attribute = AttributeEntity::simple("TAG", "Value");
|
|
|
|
|
attribute.insertion_point = attribute_position;
|
|
|
|
|
ins.attributes.push(attribute);
|
|
|
|
|
|
|
|
|
|
assert!(apply_insert_unit_scale(&mut ins, 25.4));
|
|
|
|
|
|
|
|
|
|
let scaled_insertion = ins.get_transform().apply(Vector3::ZERO);
|
|
|
|
|
let expected_attribute = insertion + (attribute_position - insertion) * 25.4;
|
|
|
|
|
assert!((scaled_insertion - insertion).length() < 1e-9);
|
|
|
|
|
assert!((ins.attributes[0].insertion_point - expected_attribute).length() < 1e-9);
|
|
|
|
|
assert!((ins.x_scale() + 50.8).abs() < 1e-9);
|
|
|
|
|
assert!((ins.y_scale() - 76.2).abs() < 1e-9);
|
|
|
|
|
assert!((ins.z_scale() + 101.6).abs() < 1e-9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn applying_large_unit_scale_keeps_distant_insert_fixed() {
|
|
|
|
|
let mut ins = Insert::new("Block", Vector3::new(1.0e12, -2.0e12, 3.0e12));
|
|
|
|
|
let insertion = ins.get_transform().apply(Vector3::ZERO);
|
|
|
|
|
|
|
|
|
|
assert!(apply_insert_unit_scale(&mut ins, 1.0e20));
|
|
|
|
|
|
|
|
|
|
let scaled_insertion = ins.get_transform().apply(Vector3::ZERO);
|
|
|
|
|
assert_eq!(scaled_insertion, insertion);
|
|
|
|
|
assert_eq!(ins.x_scale(), 1.0e20);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unsupported_tiny_unit_scale_is_not_applied() {
|
|
|
|
|
let mut ins = Insert::new("Block", Vector3::new(1.0, 2.0, 3.0));
|
|
|
|
|
let before = ins.clone();
|
|
|
|
|
|
|
|
|
|
assert!(!apply_insert_unit_scale(&mut ins, 1.0e-13));
|
|
|
|
|
assert_eq!(ins, before);
|
|
|
|
|
}
|
2026-08-17 00:36:49 +07:00
|
|
|
}
|