Merge main into Pallet-placement

Resolved conflicts:
- app/update/mod.rs: keep the dock's dock_open_at_default for the XREF
  manager; take main's stable xref row identity (row_index(&id)).
- app/view/mod.rs: keep the edge layout from app::dock; drop main's copy
  of the old edge-stack code (main only reformatted it there).
- ui/window/layers.rs: keep main's new visible_indices next to the
  branch's view_panel.
Files the branch only reformatted take main's version. Adds the Lao
(lo-LA) translations for the new dock texts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-11 18:44:00 +00:00
commit 2fca3fd18d
136 files changed
+12408 -949

No files matched your search

+21 -21
View File
@@ -1245,30 +1245,30 @@ impl OpenCADStudio {
} else {
// EXPLMODE 0 keeps references that are not uniformly and
// positively scaled whole.
let selected: Vec<_> = if super::blockvars::explmode(
&self.tabs[i].scene.document,
) == 0
{
selected
.into_iter()
.filter(|(_, entity)| match entity {
codec::EntityType::Insert(ins) => {
let (x, y, z) = (ins.x_scale(), ins.y_scale(), ins.z_scale());
let uniform =
x > 0.0 && (x - y).abs() <= 1e-10 && (x - z).abs() <= 1e-10;
if !uniform {
self.command_line.push_output(
let selected: Vec<_> =
if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 {
selected
.into_iter()
.filter(|(_, entity)| match entity {
codec::EntityType::Insert(ins) => {
let (x, y, z) =
(ins.x_scale(), ins.y_scale(), ins.z_scale());
let uniform = x > 0.0
&& (x - y).abs() <= 1e-10
&& (x - z).abs() <= 1e-10;
if !uniform {
self.command_line.push_output(
"The object is not uniformly and positively scaled.",
);
}
uniform
}
uniform
}
_ => true,
})
.collect()
} else {
selected
};
_ => true,
})
.collect()
} else {
selected
};
let replacements = plan_explode(&selected, &self.tabs[i].scene.document);
let exploded = replacements.len();
if exploded > 0 {
+76
View File
@@ -1145,6 +1145,82 @@ impl OpenCADStudio {
for (handle, _) in &selected {
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
use crate::entities::traits::EntityTypeOps;
// A solid's properties come from its B-rep: exact
// where the kernel knows the shape, a fine mesh
// otherwise — not the display mesh.
let acis = match entity {
codec::EntityType::Solid3D(solid) => Some(&solid.acis_data),
codec::EntityType::Body(body) => Some(&body.acis_data),
_ => None,
};
if let Some(acis) = acis {
let properties =
crate::scene::convert::solid3d_tess::kernel_acis_body(acis)
.and_then(|body| kernel::brep::mass_properties(&body));
let Some(p) = properties else {
self.command_line.push_error(
crate::tf!(
"MASSPROP: {} has no closed body to measure.",
crate::entities::names::dxf_name(entity),
)
.as_ref(),
);
continue;
};
let [x, y, z] = p.centroid;
let [ix, iy, iz] = p.moment_of_inertia;
let [pxy, pyz, pzx] = p.product_of_inertia;
let [rx, ry, rz] = p.radii_of_gyration;
let [i1, i2, i3] = p.principal_moments;
self.command_line.push_output(
crate::tf!(
"{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})",
crate::entities::names::dxf_name(entity),
p.volume,
x,
y,
z,
)
.as_ref(),
);
self.command_line.push_output(
crate::tf!(
"Moments of inertia X={:.4} Y={:.4} Z={:.4}",
ix,
iy,
iz,
)
.as_ref(),
);
self.command_line.push_output(
crate::tf!(
"Products of inertia XY={:.4} YZ={:.4} ZX={:.4}",
pxy,
pyz,
pzx,
)
.as_ref(),
);
self.command_line.push_output(
crate::tf!(
"Radii of gyration X={:.4} Y={:.4} Z={:.4}",
rx,
ry,
rz,
)
.as_ref(),
);
self.command_line.push_output(
crate::tf!(
"Principal moments about centroid I={:.4} J={:.4} K={:.4}",
i1,
i2,
i3,
)
.as_ref(),
);
continue;
}
if let Some(props) = entity.mass_props() {
self.command_line.push_output(
crate::tf!(
+96
View File
@@ -649,6 +649,102 @@ impl OpenCADStudio {
.push_info(crate::t!("LAYUNISO: all layers restored.").as_ref());
}
"QLAYER" => {
use crate::modules::draw::layers::quick_layer::{
QuickLayerCommand, QuickLayerEntry,
};
let layers: Vec<QuickLayerEntry> = self.tabs[i]
.layers
.layers
.iter()
.filter(|layer| !layer.name.starts_with('*'))
.map(|layer| {
QuickLayerEntry::new(
layer.name.clone(),
layer.color.clone(),
layer.visible,
layer.frozen,
layer.locked,
)
})
.collect();
let selected: Vec<codec::Handle> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(handle, _)| handle)
.collect();
let command = QuickLayerCommand::new_with_entries(layers, selected);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
cmd if cmd.starts_with("QLAYER_APPLY_HEX ") => {
use crate::modules::draw::layers::quick_layer::QuickLayerCommand;
let encoded = cmd.trim_start_matches("QLAYER_APPLY_HEX").trim();
let Some(requested) = QuickLayerCommand::decode_layer_name(encoded) else {
self.command_line
.push_error(crate::t!("QLAYER: invalid encoded layer name.").as_ref());
return Some(Task::none());
};
let resolved = self.tabs[i]
.scene
.document
.layers
.names()
.find(|name| name.eq_ignore_ascii_case(&requested))
.map(|name| name.to_string());
let Some(layer) = resolved else {
self.command_line
.push_error(crate::tf!("QLAYER: no layer named \"{requested}\".").as_ref());
return Some(Task::none());
};
let handles: Vec<codec::Handle> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(handle, _)| handle)
.collect();
if handles.is_empty() {
match self.set_current_layer_name(i, &layer) {
Ok(()) => {
self.command_line.push_info(
crate::tf!("QLAYER: current layer set to \"{layer}\".").as_ref(),
);
}
Err(error) => {
self.command_line.push_error(&error);
}
}
} else {
let count = handles.len();
self.apply_property_op(i, "QLAYER", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_common_prop(
entity, "layer", &layer,
);
}
});
self.ribbon.active_layer = layer.clone();
self.command_line.push_info(
crate::tf!("QLAYER: moved {count} object(s) to layer \"{layer}\".")
.as_ref(),
);
}
}
"LAYMATCH" | "LAYMCH" => {
use crate::modules::draw::layers::match_layer::LayMatchCommand;
let dest: Vec<_> = self.tabs[i]
+1
View File
@@ -706,6 +706,7 @@ inventory::submit!(crate::command::CommandRegistration {
"PDFOSNAP",
"UOSNAP",
"FIELDDISPLAY",
"SURFACEASSOCIATIVITY",
"FIELDEVAL",
"PDFIMPORTMODE",
"PDFIMPORTFILTER",
+97 -19
View File
@@ -293,7 +293,7 @@ impl OpenCADStudio {
if name.is_empty() {
None
} else {
Some(name.clone())
Some(name.to_uppercase())
}
})
.collect();
@@ -320,6 +320,17 @@ impl OpenCADStudio {
.map(|ds| ds.dimtxsty.clone()),
)
.filter(|s| !s.is_empty())
.map(|s| s.to_uppercase())
.collect();
// Complex linetypes reach their text and shape styles by handle.
let linetype_styles: rustc_hash::FxHashSet<codec::Handle> = self.tabs[i]
.scene
.document
.line_types
.iter()
.flat_map(|lt| lt.elements.iter())
.filter_map(|element| element.complex.as_ref())
.map(|complex| complex.style_handle)
.collect();
// Linetypes: entity references plus every layer's own
// linetype — a ByLayer entity reaches it through the layer.
@@ -344,6 +355,7 @@ impl OpenCADStudio {
.map(|l| l.line_type.clone()),
)
.filter(|s| !s.is_empty())
.map(|s| s.to_uppercase())
.collect();
// Live blocks are reachable from layouts and style roots
@@ -409,7 +421,9 @@ impl OpenCADStudio {
.document
.layers
.iter()
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
.filter(|l| {
l.name != "0" && !used_layers.contains(&l.name.to_uppercase())
})
.map(|l| l.name.clone())
.collect()
} else {
@@ -421,7 +435,15 @@ impl OpenCADStudio {
.document
.text_styles
.iter()
.filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name))
// Unnamed shape-file records serve complex linetypes
// and are never listed for purging.
.filter(|s| {
!s.name.is_empty()
&& !s.is_shape_file
&& !s.name.eq_ignore_ascii_case("Standard")
&& !used_text_styles.contains(&s.name.to_uppercase())
&& !linetype_styles.contains(&s.handle)
})
.map(|s| s.name.clone())
.collect()
} else {
@@ -436,7 +458,7 @@ impl OpenCADStudio {
.iter()
.filter(|lt| {
!standard.iter().any(|s| s.eq_ignore_ascii_case(&lt.name))
&& !used_linetypes.contains(&lt.name)
&& !used_linetypes.contains(&lt.name.to_uppercase())
})
.map(|lt| lt.name.clone())
.collect()
@@ -483,16 +505,23 @@ impl OpenCADStudio {
snapshot_pushed = true;
}
// Apply removals (mutable)
for name in &layer_remove {
self.tabs[i].scene.document.layers.remove(name);
}
for name in &style_remove {
self.tabs[i].scene.document.text_styles.remove(name);
}
for name in &lt_remove {
self.tabs[i].scene.document.line_types.remove(name);
}
// Apply removals (mutable). Count what actually went: an
// entry the table cannot remove by name would otherwise be
// listed again on every pass and never let the loop end.
let doc = &mut self.tabs[i].scene.document;
let layers_gone = layer_remove
.iter()
.filter(|n| doc.layers.remove(n).is_some())
.count();
let styles_gone = style_remove
.iter()
.filter(|n| doc.text_styles.remove(n).is_some())
.count();
let lts_gone = lt_remove
.iter()
.filter(|n| doc.line_types.remove(n).is_some())
.count();
let mut blocks_gone = 0usize;
for name in &block_remove {
// Drop the block definition's member entities (and the
// BLOCK/ENDBLK delimiters) before the record so no orphaned
@@ -509,12 +538,23 @@ impl OpenCADStudio {
for h in handles {
self.tabs[i].scene.document.remove_entity(h);
}
self.tabs[i].scene.document.block_records.remove(name);
if self.tabs[i]
.scene
.document
.block_records
.remove(name)
.is_some()
{
blocks_gone += 1;
}
}
n_layers += layers_gone;
n_styles += styles_gone;
n_lts += lts_gone;
n_blocks += blocks_gone;
if layers_gone + styles_gone + lts_gone + blocks_gone == 0 {
break;
}
n_layers += layer_remove.len();
n_styles += style_remove.len();
n_lts += lt_remove.len();
n_blocks += block_remove.len();
}
// Remove draw-order tables whose owning blocks are gone.
@@ -1246,6 +1286,7 @@ impl OpenCADStudio {
| "DGNOSNAP"
| "UOSNAP"
| "FIELDDISPLAY"
| "SURFACEASSOCIATIVITY"
| "PDFIMPORTMODE"
| "PDFIMPORTFILTER"
| "PDFIMPORTLAYERS"
@@ -1484,6 +1525,36 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
if name == "SURFACEASSOCIATIVITY" {
use crate::scene::model::sweep_model;
let current = i16::from(sweep_model::surface_associativity());
match value
.as_deref()
.map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v)))
{
Some(Some(mode)) => {
if current != mode {
sweep_model::set_surface_associativity(mode == 1);
self.save_config();
}
}
Some(None) => {
self.command_line
.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
self.command_line.push_output(&format!(
"Enter new value for SURFACEASSOCIATIVITY <{current}>:"
));
self.pending_setvar = Some(name.clone());
}
None => {
self.command_line.push_output(&format!(
"Enter new value for SURFACEASSOCIATIVITY <{current}>:"
));
self.pending_setvar = Some(name.clone());
}
}
return Some(self.finish_dispatch(cmd));
}
if name == "FIELDDISPLAY" {
let current = i16::from(crate::entities::field::display());
match value
@@ -3889,6 +3960,13 @@ fn rename_symbol(doc: &mut codec::CadDocument, ty: &str, old: &str, new: &str) -
if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
doc.header.current_text_style_name = new.to_string();
}
// Dimension styles name their text style; follow the rename (same
// as rename_style_storage — see #42).
for ds in doc.dim_styles.iter_mut() {
if ds.dimtxsty.eq_ignore_ascii_case(old) {
ds.dimtxsty = new.to_string();
}
}
true
}
"DIMSTYLE" => {
+1 -1
View File
@@ -814,7 +814,7 @@ impl OpenCADStudio {
vp.view_height,
vp.height,
),
vp.status.is_on,
vp.is_on(),
vp.status.locked,
))
} else {