Take the first selected entity as a template, adopt its general properties (layer, colour, linetype, lineweight, linetype scale) as the current defaults, then start the matching draw command so the object drawn next inherits them. Supports Point, Line, Circle, Arc, Ellipse, all polylines (PLINE), Text, MText, Spline, Hatch, Solid, Ray and XLine; other types report that creation is unsupported. Registered for command-line autocomplete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1202 lines
51 KiB
Rust
1202 lines
51 KiB
Rust
use super::*;
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
|
match cmd {
|
|
"3DORBIT" | "3O" => {
|
|
self.command_line
|
|
.push_info("3D Orbit: drag with right mouse button.");
|
|
}
|
|
|
|
// ── Selection utilities ───────────────────────────────────────
|
|
"SELECTALL" | "SA" => {
|
|
use crate::scene::Scene;
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
.scene
|
|
.entity_wires()
|
|
.iter()
|
|
.filter_map(|w| Scene::handle_from_wire_name(&w.name))
|
|
// Objects on a locked layer aren't selectable.
|
|
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
|
|
.collect();
|
|
let count = handles.len();
|
|
for h in handles {
|
|
self.tabs[i].scene.select_entity(h, false);
|
|
}
|
|
self.command_line
|
|
.push_output(&format!("SELECTALL: {} object(s) selected.", count));
|
|
self.refresh_properties();
|
|
}
|
|
|
|
"DESELECT" | "DE" | "DESELALL" => {
|
|
self.tabs[i].scene.deselect_all();
|
|
self.command_line.push_output("Deselected.");
|
|
self.refresh_properties();
|
|
}
|
|
|
|
"SELECTSIMILAR" | "SELSIM" => {
|
|
let added = self.tabs[i].scene.select_similar();
|
|
self.command_line
|
|
.push_output(&format!("Select Similar: {} added.", added));
|
|
self.refresh_properties();
|
|
}
|
|
|
|
// ADDSELECTED — draw a new object of the same type as the selected
|
|
// one, inheriting its general properties. (#239)
|
|
"ADDSELECTED" => {
|
|
return Some(self.cmd_add_selected(i));
|
|
}
|
|
|
|
// QSELECT builds a selection set by object type / property; FILTER is
|
|
// the same criteria-based selection.
|
|
"QSELECT" | "QS" | "FILTER" | "FI" => {
|
|
return Some(Task::done(Message::QSelectOpen));
|
|
}
|
|
|
|
// CAL / QUICKCALC <expression> — evaluate an arithmetic expression
|
|
// (+ - * /, parentheses, unary minus, decimals).
|
|
cmd if cmd == "CAL"
|
|
|| cmd == "QUICKCALC"
|
|
|| cmd == "QC"
|
|
|| cmd.starts_with("CAL ")
|
|
|| cmd.starts_with("QUICKCALC ")
|
|
|| cmd.starts_with("QC ") =>
|
|
{
|
|
let expr = cmd.splitn(2, char::is_whitespace).nth(1).unwrap_or("").trim();
|
|
if expr.is_empty() {
|
|
self.command_line
|
|
.push_info("Usage: CAL <expression> e.g. CAL (2+3)*4");
|
|
} else {
|
|
match arith_eval(expr) {
|
|
Ok(v) => self.command_line.push_output(&format!("= {v}")),
|
|
Err(e) => self.command_line.push_error(&format!("CAL: {e}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── LIST — entity info ────────────────────────────────────────
|
|
"LIST" | "LI" => {
|
|
let selected: Vec<_> = self.tabs[i].scene.selected_entities();
|
|
if selected.is_empty() {
|
|
self.command_line
|
|
.push_error("LIST: no entities selected. Select entities first.");
|
|
} else {
|
|
for (handle, _) in &selected {
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
|
let type_name = crate::entities::names::dxf_name(entity);
|
|
let common = entity.common();
|
|
let color_str = common
|
|
.color
|
|
.index()
|
|
.map(|c| c.to_string())
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
|
let linetype =
|
|
if common.linetype.is_empty() || common.linetype == "ByLayer" {
|
|
"ByLayer".to_string()
|
|
} else {
|
|
common.linetype.clone()
|
|
};
|
|
// Entity-specific details
|
|
let details = entity_list_details(entity);
|
|
self.command_line.push_output(&format!(
|
|
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
|
handle.value(),
|
|
common.layer,
|
|
color_str,
|
|
linetype,
|
|
if details.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("\n {details}")
|
|
}
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// DBLIST — list data for every entity in the drawing (LIST over the
|
|
// whole database rather than the current selection).
|
|
"DBLIST" => {
|
|
// Format every entity first so the immutable document borrow is
|
|
// released before writing to the command line.
|
|
let lines: Vec<String> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|entity| {
|
|
let type_name = crate::entities::names::dxf_name(entity);
|
|
let common = entity.common();
|
|
let color_str = common
|
|
.color
|
|
.index()
|
|
.map(|c| c.to_string())
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
|
let linetype = if common.linetype.is_empty() || common.linetype == "ByLayer"
|
|
{
|
|
"ByLayer".to_string()
|
|
} else {
|
|
common.linetype.clone()
|
|
};
|
|
let details = entity_list_details(entity);
|
|
format!(
|
|
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
|
common.handle.value(),
|
|
common.layer,
|
|
color_str,
|
|
linetype,
|
|
if details.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("\n {details}")
|
|
}
|
|
)
|
|
})
|
|
.collect();
|
|
if lines.is_empty() {
|
|
self.command_line
|
|
.push_info("DBLIST: drawing has no entities.");
|
|
} else {
|
|
let count = lines.len();
|
|
for l in lines {
|
|
self.command_line.push_output(&l);
|
|
}
|
|
self.command_line.push_output(&format!(
|
|
"DBLIST: {count} entit{}.",
|
|
if count == 1 { "y" } else { "ies" }
|
|
));
|
|
}
|
|
}
|
|
|
|
// ── Break / Join ─────────────────────────────────────────────────
|
|
"JOIN" | "J" => {
|
|
use crate::modules::draw::modify::join::JoinCommand;
|
|
let cmd = JoinCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"BREAK" | "BR" => {
|
|
use crate::modules::draw::modify::break_cmd::BreakInteractiveCommand;
|
|
let cmd = BreakInteractiveCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"BREAKATPOINT" | "BAP" => {
|
|
use crate::modules::draw::modify::break_cmd::BreakAtPointCommand;
|
|
let cmd = BreakAtPointCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"PEDIT" | "PE" => {
|
|
use crate::modules::draw::modify::pedit::PeditCommand;
|
|
let cmd_obj = PeditCommand::new();
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
}
|
|
|
|
"SPLINEDIT" | "SPE" => {
|
|
use crate::modules::draw::modify::splinedit::SplineditCommand;
|
|
let cmd_obj = SplineditCommand::new();
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
}
|
|
|
|
// Bare ATTEDIT (and the ATE alias) open the attribute editor dialog.
|
|
// If a single block with attributes is already selected it opens on
|
|
// that block; otherwise the pick command runs and the editor opens
|
|
// once a block is chosen (see `command_driver`).
|
|
"ATTEDIT" | "ATE" => {
|
|
let selected_attr_insert = {
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
if sel.len() == 1 {
|
|
let (h, e) = sel[0];
|
|
match e {
|
|
acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => {
|
|
Some(h)
|
|
}
|
|
_ => None,
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
if let Some(handle) = selected_attr_insert {
|
|
self.open_attribute_editor(handle);
|
|
} else {
|
|
use crate::modules::draw::modify::attedit::AtteditCommand;
|
|
let cmd_obj = AtteditCommand::new();
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
}
|
|
}
|
|
|
|
// ── REFEDIT — in-place block editing ─────────────────────────────
|
|
"REFEDIT" => {
|
|
use crate::modules::draw::modify::refedit::RefEditPickCommand;
|
|
// If a session is already active, tell the user.
|
|
if self.tabs[i].refedit_session.is_some() {
|
|
self.command_line
|
|
.push_error("REFEDIT: a session is already active. Use REFCLOSE first.");
|
|
} else {
|
|
// Check if a single INSERT is already selected.
|
|
let selected: Vec<_> =
|
|
self.tabs[i].scene.selected_entities().into_iter().collect();
|
|
if selected.len() == 1 {
|
|
if let Some(acadrust::EntityType::Insert(_)) =
|
|
selected.first().map(|(_, e)| e)
|
|
{
|
|
let handle = selected[0].0;
|
|
// Skip pick phase — jump straight to begin.
|
|
let _ =
|
|
self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value()));
|
|
return Some(Task::none());
|
|
}
|
|
}
|
|
let cmd_obj = RefEditPickCommand::new();
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
}
|
|
}
|
|
|
|
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
|
use crate::modules::draw::modify::refedit::{
|
|
apply_insert_transform, RefEditSession,
|
|
};
|
|
use acadrust::Handle;
|
|
|
|
let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
|
let insert_handle = Handle::new(handle_u64);
|
|
|
|
// Get INSERT entity.
|
|
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
|
Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
|
|
_ => {
|
|
self.command_line
|
|
.push_error("REFEDIT: selected object is not an INSERT.");
|
|
return Some(Task::none());
|
|
}
|
|
};
|
|
|
|
// Build the INSERT's full placement transform (OCS + rotation +
|
|
// scale, including non-uniform / mirrored) and its inverse, so
|
|
// edits round-trip back to block-local coordinates on SAVE.
|
|
let sx = insert.x_scale();
|
|
let sy = insert.y_scale();
|
|
let sz = insert.z_scale();
|
|
let forward = insert.get_transform();
|
|
let inverse = {
|
|
use acadrust::types::{Matrix3, Matrix4, Transform};
|
|
let ocs_t =
|
|
Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose());
|
|
let t_inv = Matrix4::translation(
|
|
-insert.insert_point.x,
|
|
-insert.insert_point.y,
|
|
-insert.insert_point.z,
|
|
);
|
|
let r_inv = Matrix4::rotation_z(-insert.rotation);
|
|
let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
|
|
// inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ
|
|
Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t)
|
|
};
|
|
|
|
// Find the block record.
|
|
let br_handle = match self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get(&insert.block_name)
|
|
{
|
|
Some(br) => br.handle,
|
|
None => {
|
|
self.command_line.push_error(&format!(
|
|
"REFEDIT: block \"{}\" not found.",
|
|
insert.block_name
|
|
));
|
|
return Some(Task::none());
|
|
}
|
|
};
|
|
|
|
// Collect block-local entities (skip structural Block/BlockEnd/AttDef).
|
|
let block_entities: Vec<_> = {
|
|
let br = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get(&insert.block_name)
|
|
.unwrap();
|
|
br.entity_handles
|
|
.iter()
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
.filter(|e| {
|
|
!matches!(
|
|
e,
|
|
acadrust::EntityType::Block(_)
|
|
| acadrust::EntityType::BlockEnd(_)
|
|
| acadrust::EntityType::AttributeDefinition(_)
|
|
)
|
|
})
|
|
.collect()
|
|
};
|
|
|
|
if block_entities.is_empty() {
|
|
self.command_line.push_error("REFEDIT: block is empty.");
|
|
return Some(Task::none());
|
|
}
|
|
|
|
let session = RefEditSession {
|
|
block_name: insert.block_name.clone(),
|
|
br_handle,
|
|
temp_handles: vec![],
|
|
forward,
|
|
inverse,
|
|
};
|
|
|
|
self.push_undo_snapshot(i, "REFEDIT");
|
|
self.tabs[i].refedit_session = Some(session.clone());
|
|
|
|
// Add block entities to model space with INSERT transform applied.
|
|
let mut temp_handles = Vec::new();
|
|
for mut entity in block_entities {
|
|
apply_insert_transform(&mut entity, &session);
|
|
entity.common_mut().handle = acadrust::Handle::NULL;
|
|
entity.common_mut().owner_handle = acadrust::Handle::NULL;
|
|
let h = self.tabs[i].scene.add_entity(entity);
|
|
temp_handles.push(h);
|
|
}
|
|
self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone();
|
|
|
|
// Fade everything except the entities being edited, so the
|
|
// surrounding drawing stays visible for context but the block's
|
|
// geometry stands out. (#136)
|
|
self.tabs[i]
|
|
.scene
|
|
.set_refedit_keep(Some(temp_handles.iter().copied().collect()));
|
|
|
|
// Select the temp entities so user can see what they're editing.
|
|
self.tabs[i].scene.deselect_all();
|
|
for h in &temp_handles {
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
|
|
// No active command — the user edits the block's geometry freely
|
|
// (move, grips, draw, erase…) and runs REFCLOSE when done. (#136)
|
|
self.tabs[i].active_cmd = None;
|
|
self.command_line.push_info(&format!(
|
|
"REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.",
|
|
insert.block_name
|
|
));
|
|
}
|
|
|
|
"REFCLOSE" => {
|
|
if self.tabs[i].refedit_session.is_some() {
|
|
use crate::modules::draw::modify::refedit::RefCloseCommand;
|
|
let cmd_obj = RefCloseCommand::new();
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
} else {
|
|
self.command_line
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
}
|
|
}
|
|
|
|
"REFCLOSE_SAVE" => {
|
|
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
|
use crate::modules::draw::modify::refedit::apply_insert_inverse_transform;
|
|
|
|
let session = match self.tabs[i].refedit_session.take() {
|
|
Some(s) => s,
|
|
None => {
|
|
self.command_line
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
return Some(Task::none());
|
|
}
|
|
};
|
|
|
|
self.push_undo_snapshot(i, "REFCLOSE");
|
|
|
|
// Collect the edited temp entities.
|
|
let new_entities: Vec<acadrust::EntityType> = session
|
|
.temp_handles
|
|
.iter()
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
.collect();
|
|
|
|
// Remove temp entities from model space.
|
|
self.tabs[i].scene.erase_entities(&session.temp_handles);
|
|
|
|
// Apply inverse INSERT transform → block-local coordinates.
|
|
let new_entities: Vec<_> = new_entities
|
|
.into_iter()
|
|
.map(|mut entity| {
|
|
apply_insert_inverse_transform(&mut entity, &session);
|
|
let mut entity = normalize_entity_for_block(entity);
|
|
entity.common_mut().handle = acadrust::Handle::NULL;
|
|
entity.common_mut().owner_handle = session.br_handle;
|
|
entity
|
|
})
|
|
.collect();
|
|
|
|
// Remove old block entities from the document.
|
|
let old_handles: Vec<_> = match self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get(&session.block_name)
|
|
{
|
|
Some(br) => br.entity_handles.clone(),
|
|
None => vec![],
|
|
};
|
|
for h in &old_handles {
|
|
self.tabs[i].scene.document.remove_entity(*h);
|
|
}
|
|
// Flush the entity_handles list from the block record.
|
|
if let Some(br) = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get_mut(&session.block_name)
|
|
{
|
|
br.entity_handles.clear();
|
|
}
|
|
|
|
// Add the new block entities.
|
|
for entity in new_entities {
|
|
let _ = self.tabs[i].scene.document.add_entity(entity);
|
|
}
|
|
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&format!(
|
|
"REFCLOSE: Block \"{}\" saved. All references updated.",
|
|
session.block_name
|
|
));
|
|
// End the edit fade before rebuilding, so the restored geometry
|
|
// recolours bright. (#136)
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
// Rebuild hatch/image/mesh caches since block content changed.
|
|
self.tabs[i].scene.rebuild_derived_caches();
|
|
}
|
|
|
|
"REFCLOSE_DISCARD" => {
|
|
let session = match self.tabs[i].refedit_session.take() {
|
|
Some(s) => s,
|
|
None => {
|
|
self.command_line
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
return Some(Task::none());
|
|
}
|
|
};
|
|
// Remove temp entities without modifying the block.
|
|
self.tabs[i].scene.erase_entities(&session.temp_handles);
|
|
self.tabs[i].scene.deselect_all();
|
|
// End the edit fade — restore the drawing to full brightness.
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
self.command_line
|
|
.push_output("REFCLOSE: Changes discarded.");
|
|
}
|
|
|
|
"ALIGN" | "AL" => {
|
|
use crate::modules::draw::modify::align::AlignCommand;
|
|
let cmd = AlignCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"LENGTHEN" | "LEN" => {
|
|
use crate::modules::draw::modify::lengthen::LengthenCommand;
|
|
let cmd = LengthenCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"DIVIDE" | "DIV" => {
|
|
use crate::modules::draw::inquiry::divide::DivideCommand;
|
|
let cmd = DivideCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"MEASURE" | "ME" => {
|
|
use crate::modules::draw::inquiry::divide::MeasureCommand;
|
|
let cmd = MeasureCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
// ── Inquiry ──────────────────────────────────────────────────────
|
|
"DIST" | "DI" => {
|
|
use crate::modules::draw::inquiry::dist::DistCommand;
|
|
let cmd = DistCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"ID" => {
|
|
use crate::modules::draw::inquiry::id::IdCommand;
|
|
let cmd = IdCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"AREA" => {
|
|
use crate::modules::draw::inquiry::area::AreaCommand;
|
|
let cmd = AreaCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
// ── MASSPROP — area, perimeter, centroid of selected entities ────
|
|
"MASSPROP" => {
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|
if selected.is_empty() {
|
|
self.command_line
|
|
.push_error("MASSPROP: no entities selected. Select entities first.");
|
|
} else {
|
|
for (handle, _) in &selected {
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
|
use crate::entities::traits::EntityTypeOps;
|
|
if let Some(props) = entity.mass_props() {
|
|
self.command_line.push_output(&format!(
|
|
"{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})",
|
|
crate::entities::names::dxf_name(entity),
|
|
props.area,
|
|
props.perimeter,
|
|
props.cx,
|
|
props.cy,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
|
|
"FLATTEN" => {
|
|
let handles: Vec<acadrust::Handle> = {
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
if sel.is_empty() {
|
|
// Flatten all entities
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|e| e.common().handle)
|
|
.collect()
|
|
} else {
|
|
sel.into_iter().map(|(h, _)| h).collect()
|
|
}
|
|
};
|
|
if handles.is_empty() {
|
|
self.command_line.push_error("FLATTEN: no entities.");
|
|
} else {
|
|
self.push_undo_snapshot(i, "FLATTEN");
|
|
for h in &handles {
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|
flatten_entity_z(e);
|
|
}
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&format!(
|
|
"FLATTEN: {} entity(ies) moved to Z=0.",
|
|
handles.len()
|
|
));
|
|
self.refresh_properties();
|
|
}
|
|
}
|
|
|
|
// ── QSELECT — quick-select entities by property ───────────────────
|
|
// QSELECT TYPE <type> — select all entities of given type
|
|
// QSELECT LAYER <name> — select all entities on layer
|
|
// QSELECT COLOR <n> — select all entities with color index n
|
|
// QSELECT LINETYPE <name> — select all entities with linetype
|
|
cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => {
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
|
|
let prop = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
let val = parts.get(1).map(|s| s.trim()).unwrap_or("").to_uppercase();
|
|
|
|
let matched: Vec<acadrust::Handle> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| {
|
|
let c = e.common();
|
|
match prop.as_str() {
|
|
"TYPE" => crate::entities::names::dxf_name(e).to_uppercase() == val,
|
|
"LAYER" => c.layer.to_uppercase() == val,
|
|
"COLOR" => c
|
|
.color
|
|
.index()
|
|
.map(|n| n.to_string() == val)
|
|
.unwrap_or(val == "BYLAYER"),
|
|
"LINETYPE" => c.linetype.to_uppercase() == val,
|
|
_ => false,
|
|
}
|
|
})
|
|
.map(|e| e.common().handle)
|
|
.collect();
|
|
|
|
if prop.is_empty() {
|
|
self.command_line
|
|
.push_info("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE <value>");
|
|
} else if matched.is_empty() {
|
|
self.command_line
|
|
.push_output("QSELECT: no matching entities.");
|
|
} else {
|
|
self.tabs[i].scene.deselect_all();
|
|
for h in &matched {
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
}
|
|
self.command_line
|
|
.push_output(&format!("QSELECT: {} entity(ies) selected.", matched.len()));
|
|
self.refresh_properties();
|
|
}
|
|
}
|
|
|
|
// ── COUNT — entity statistics ─────────────────────────────────────
|
|
cmd if cmd == "COUNT" || cmd.starts_with("COUNT ") => {
|
|
let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase());
|
|
let mut counts: std::collections::BTreeMap<String, usize> = Default::default();
|
|
for e in self.tabs[i].scene.document.entities() {
|
|
let layer = &e.common().layer;
|
|
let type_name = crate::entities::names::dxf_name(e);
|
|
let key = match &filter {
|
|
Some(f) if f == "LAYER" => layer.clone(),
|
|
Some(f) if f == "TYPE" => type_name.to_string(),
|
|
Some(f) => {
|
|
// Filter by layer name
|
|
if layer.to_uppercase() != *f {
|
|
continue;
|
|
}
|
|
type_name.to_string()
|
|
}
|
|
None => type_name.to_string(),
|
|
};
|
|
*counts.entry(key).or_default() += 1;
|
|
}
|
|
let total: usize = counts.values().sum();
|
|
for (k, n) in &counts {
|
|
self.command_line.push_output(&format!(" {k}: {n}"));
|
|
}
|
|
self.command_line
|
|
.push_output(&format!("COUNT: {total} entity(ies) total."));
|
|
}
|
|
|
|
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
|
|
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
|
|
return Some(Task::done(Message::DataExtractionSave(csv)));
|
|
}
|
|
|
|
// ── Find / Replace ────────────────────────────────────────────────
|
|
// FIND <search> — list all Text/MText/Dimension containing <search>
|
|
// FIND <search> REPLACE <rep> — replace first occurrence (case-insensitive)
|
|
// FINDALL <search> REPLACE <rep> — replace all occurrences
|
|
cmd if cmd == "FIND"
|
|
|| cmd.starts_with("FIND ")
|
|
|| cmd == "FINDALL"
|
|
|| cmd.starts_with("FINDALL ") =>
|
|
{
|
|
let all_mode = cmd.starts_with("FINDALL");
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
// Split at " REPLACE " keyword (case-insensitive)
|
|
let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ")
|
|
{
|
|
(&rest[..pos], Some(rest[pos + 9..].trim()))
|
|
} else {
|
|
(rest, None)
|
|
};
|
|
|
|
if search.is_empty() {
|
|
self.command_line.push_error("FIND: specify search text.");
|
|
} else {
|
|
let search_lc = search.to_lowercase();
|
|
let mut count = 0usize;
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|e| {
|
|
use crate::entities::traits::EntityTypeOps;
|
|
let txt = e.text_content()?;
|
|
if txt.to_lowercase().contains(&search_lc) {
|
|
Some(e.common().handle)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
if let Some(rep) = replacement {
|
|
// Replace mode
|
|
let targets: Vec<_> = if all_mode {
|
|
handles.clone()
|
|
} else {
|
|
handles.iter().copied().take(1).collect()
|
|
};
|
|
if targets.is_empty() {
|
|
self.command_line
|
|
.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
} else {
|
|
self.push_undo_snapshot(i, "FIND/REPLACE");
|
|
for h in &targets {
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|
crate::entities::traits::EntityTypeOps::replace_text(
|
|
e, search, rep,
|
|
);
|
|
count += 1;
|
|
}
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&format!(
|
|
"FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".",
|
|
count, search, rep
|
|
));
|
|
self.refresh_properties();
|
|
}
|
|
} else {
|
|
// List mode
|
|
if handles.is_empty() {
|
|
self.command_line
|
|
.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
} else {
|
|
for h in &handles {
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
|
use crate::entities::traits::EntityTypeOps;
|
|
let txt = e.text_content().unwrap_or_default();
|
|
self.command_line.push_output(&format!(
|
|
" Handle {:X}: \"{}\"",
|
|
h.value(),
|
|
txt
|
|
));
|
|
}
|
|
}
|
|
self.command_line.push_output(&format!(
|
|
"FIND: {} match(es) for \"{}\".",
|
|
handles.len(),
|
|
search
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => return None,
|
|
}
|
|
Some(self.finish_dispatch(cmd))
|
|
}
|
|
|
|
/// ADDSELECTED — start the draw command that creates the same kind of
|
|
/// object as the currently-selected one, adopting its general properties
|
|
/// (layer, colour, linetype, lineweight, linetype scale) as the current
|
|
/// defaults so the new object is drawn to match. Issue #239.
|
|
pub(super) fn cmd_add_selected(&mut self, i: usize) -> Task<Message> {
|
|
// Use the first selected object as the template.
|
|
let Some(handle) = self.tabs[i].scene.selected.iter().next().copied() else {
|
|
self.command_line.push_info(
|
|
"ADDSELECTED: select an object first, then run ADDSELECTED to draw a new one like it.",
|
|
);
|
|
return Task::none();
|
|
};
|
|
// Pull the template's type + general properties into owned values, then
|
|
// drop the borrow so the document can be mutated below.
|
|
let info = self.tabs[i].scene.document.get_entity(handle).map(|e| {
|
|
let c = e.common();
|
|
(
|
|
add_selected_verb(e),
|
|
crate::entities::names::dxf_name(e).to_string(),
|
|
c.layer.clone(),
|
|
c.color,
|
|
c.linetype.clone(),
|
|
c.linetype_scale,
|
|
c.line_weight,
|
|
)
|
|
});
|
|
let Some((verb, kind, layer, color, linetype, lt_scale, lw)) = info else {
|
|
self.command_line
|
|
.push_error("ADDSELECTED: selected object not found.");
|
|
return Task::none();
|
|
};
|
|
let Some(verb) = verb else {
|
|
self.command_line
|
|
.push_error(&format!("ADDSELECTED: creating a new {kind} is not supported."));
|
|
return Task::none();
|
|
};
|
|
|
|
// Clear the selection so adopting the properties as defaults doesn't
|
|
// rewrite the template, and so the draw starts on a clean slate.
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
// Adopt the template's general properties as the current defaults. The
|
|
// entity-creation path stamps new objects from the tab's active layer
|
|
// and the ribbon's active colour / linetype / lineweight, mirrored into
|
|
// the header so they persist (CLAYER / CECOLOR / CELTYPE / CELWEIGHT /
|
|
// CELTSCALE).
|
|
let layer_handle = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.layers
|
|
.get(&layer)
|
|
.map(|l| l.handle)
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
let lt_handle = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.line_types
|
|
.iter()
|
|
.find(|x| x.name.eq_ignore_ascii_case(&linetype))
|
|
.map(|x| x.handle)
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
{
|
|
let header = &mut self.tabs[i].scene.document.header;
|
|
header.current_layer_name = layer.clone();
|
|
header.current_layer_handle = layer_handle;
|
|
header.current_entity_color = color;
|
|
header.current_linetype_name = linetype.clone();
|
|
header.current_linetype_handle = lt_handle;
|
|
header.current_line_weight = lw.value();
|
|
header.current_entity_linetype_scale = lt_scale;
|
|
}
|
|
self.tabs[i].active_layer = layer.clone();
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|
self.tabs[i].dirty = true;
|
|
self.ribbon.active_layer = layer.clone();
|
|
self.ribbon.active_color = color;
|
|
self.ribbon.active_linetype = linetype;
|
|
self.ribbon.active_lineweight = lw;
|
|
self.refresh_properties();
|
|
|
|
self.command_line.push_output(&format!(
|
|
"Add Selected: drawing a new {kind} on layer \"{layer}\"."
|
|
));
|
|
// Launch the matching draw command (installs its interactive step).
|
|
self.dispatch_command(verb)
|
|
}
|
|
}
|
|
|
|
/// Map a template entity to the draw-command verb that creates the same kind of
|
|
/// object, or `None` when there is no interactive creator for it. Used by
|
|
/// ADDSELECTED (issue #239).
|
|
fn add_selected_verb(entity: &acadrust::EntityType) -> Option<&'static str> {
|
|
use acadrust::EntityType;
|
|
Some(match entity {
|
|
EntityType::Point(_) => "POINT",
|
|
EntityType::Line(_) => "LINE",
|
|
EntityType::Circle(_) => "CIRCLE",
|
|
EntityType::Arc(_) => "ARC",
|
|
EntityType::Ellipse(_) => "ELLIPSE",
|
|
EntityType::LwPolyline(_)
|
|
| EntityType::Polyline(_)
|
|
| EntityType::Polyline2D(_)
|
|
| EntityType::Polyline3D(_) => "PLINE",
|
|
EntityType::Text(_) => "TEXT",
|
|
EntityType::MText(_) => "MTEXT",
|
|
EntityType::Spline(_) => "SPLINE",
|
|
EntityType::Hatch(_) => "HATCH",
|
|
EntityType::Solid(_) => "SOLID",
|
|
EntityType::Ray(_) => "RAY",
|
|
EntityType::XLine(_) => "XLINE",
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn entity_list_details(entity: &acadrust::EntityType) -> String {
|
|
use std::f64::consts::PI;
|
|
match entity {
|
|
acadrust::EntityType::Line(l) => format!(
|
|
"from ({:.4},{:.4},{:.4}) to ({:.4},{:.4},{:.4}) len={:.4}",
|
|
l.start.x,
|
|
l.start.y,
|
|
l.start.z,
|
|
l.end.x,
|
|
l.end.y,
|
|
l.end.z,
|
|
((l.end.x - l.start.x).powi(2)
|
|
+ (l.end.y - l.start.y).powi(2)
|
|
+ (l.end.z - l.start.z).powi(2))
|
|
.sqrt()
|
|
),
|
|
acadrust::EntityType::Circle(c) => format!(
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} area={:.4}",
|
|
c.center.x,
|
|
c.center.y,
|
|
c.center.z,
|
|
c.radius,
|
|
PI * c.radius * c.radius
|
|
),
|
|
acadrust::EntityType::Arc(a) => format!(
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} start={:.2}° end={:.2}°",
|
|
a.center.x,
|
|
a.center.y,
|
|
a.center.z,
|
|
a.radius,
|
|
a.start_angle.to_degrees(),
|
|
a.end_angle.to_degrees()
|
|
),
|
|
acadrust::EntityType::LwPolyline(p) => format!(
|
|
"{} vertices closed={} elevation={:.4}",
|
|
p.vertices.len(),
|
|
p.is_closed,
|
|
p.elevation
|
|
),
|
|
acadrust::EntityType::Text(t) => format!(
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
t.value, t.height, t.insertion_point.x, t.insertion_point.y
|
|
),
|
|
acadrust::EntityType::MText(t) => format!(
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
t.value.chars().take(40).collect::<String>(),
|
|
t.height,
|
|
t.insertion_point.x,
|
|
t.insertion_point.y
|
|
),
|
|
acadrust::EntityType::Insert(ins) => format!(
|
|
"block=\"{}\" at ({:.4},{:.4},{:.4}) scale=({:.4},{:.4},{:.4}) rot={:.2}°",
|
|
ins.block_name,
|
|
ins.insert_point.x,
|
|
ins.insert_point.y,
|
|
ins.insert_point.z,
|
|
ins.x_scale(),
|
|
ins.y_scale(),
|
|
ins.z_scale(),
|
|
ins.rotation.to_degrees()
|
|
),
|
|
acadrust::EntityType::Spline(s) => format!(
|
|
"{} ctrl pts degree={} closed={}",
|
|
s.control_points.len(),
|
|
s.degree,
|
|
s.flags.closed
|
|
),
|
|
acadrust::EntityType::Ellipse(e) => format!(
|
|
"center ({:.4},{:.4}) major_len={:.4} ratio={:.4}",
|
|
e.center.x,
|
|
e.center.y,
|
|
e.major_axis_length(),
|
|
e.minor_axis_ratio
|
|
),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
fn flatten_entity_z(entity: &mut acadrust::EntityType) {
|
|
match entity {
|
|
acadrust::EntityType::Line(l) => {
|
|
l.start.z = 0.0;
|
|
l.end.z = 0.0;
|
|
}
|
|
acadrust::EntityType::Circle(c) => {
|
|
c.center.z = 0.0;
|
|
}
|
|
acadrust::EntityType::Arc(a) => {
|
|
a.center.z = 0.0;
|
|
}
|
|
acadrust::EntityType::LwPolyline(p) => {
|
|
p.elevation = 0.0;
|
|
}
|
|
acadrust::EntityType::Text(t) => {
|
|
t.insertion_point.z = 0.0;
|
|
}
|
|
acadrust::EntityType::MText(t) => {
|
|
t.insertion_point.z = 0.0;
|
|
}
|
|
acadrust::EntityType::Insert(ins) => {
|
|
ins.insert_point.z = 0.0;
|
|
}
|
|
acadrust::EntityType::Point(p) => {
|
|
p.location.z = 0.0;
|
|
}
|
|
acadrust::EntityType::Spline(s) => {
|
|
for cp in &mut s.control_points {
|
|
cp.z = 0.0;
|
|
}
|
|
for fp in &mut s.fit_points {
|
|
fp.z = 0.0;
|
|
}
|
|
}
|
|
acadrust::EntityType::Ellipse(e) => {
|
|
e.center.z = 0.0;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// ── DATAEXTRACTION ─────────────────────────────────────────────────────────
|
|
|
|
/// Build a CSV string with one row per entity in model space.
|
|
/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo
|
|
fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
|
|
use acadrust::EntityType;
|
|
|
|
let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n");
|
|
|
|
let ms_handle = doc.header.model_space_block_handle;
|
|
for e in doc.entities() {
|
|
// Skip Block/EndBlock sentinels and paper-space entities.
|
|
if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
|
|
continue;
|
|
}
|
|
if !ms_handle.is_null() && e.common().owner_handle != ms_handle {
|
|
continue;
|
|
}
|
|
let type_name = crate::entities::names::dxf_name(e);
|
|
let handle = format!("{:X}", e.common().handle.value());
|
|
let layer = csv_escape(&e.common().layer);
|
|
let color = format!("{}", e.common().color);
|
|
let lt = csv_escape(&e.common().linetype);
|
|
let extra = csv_escape(&entity_extra_info(e));
|
|
out.push_str(&format!(
|
|
"{type_name},{handle},{layer},{color},{lt},{extra}\n"
|
|
));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Return a short geometry summary for CSV ExtraInfo column.
|
|
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
|
|
use acadrust::EntityType;
|
|
match entity {
|
|
EntityType::Line(e) => format!(
|
|
"({:.3},{:.3})-({:.3},{:.3})",
|
|
e.start.x, e.start.y, e.end.x, e.end.y
|
|
),
|
|
EntityType::Circle(e) => {
|
|
format!("C({:.3},{:.3}) R={:.3}", e.center.x, e.center.y, e.radius)
|
|
}
|
|
EntityType::Arc(e) => format!(
|
|
"C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°",
|
|
e.center.x,
|
|
e.center.y,
|
|
e.radius,
|
|
e.start_angle.to_degrees(),
|
|
e.end_angle.to_degrees()
|
|
),
|
|
EntityType::Text(e) => e.value.clone(),
|
|
EntityType::MText(e) => e.value.chars().take(60).collect(),
|
|
EntityType::Insert(e) => format!(
|
|
"BLK={} @({:.3},{:.3})",
|
|
e.block_name, e.insert_point.x, e.insert_point.y
|
|
),
|
|
EntityType::LwPolyline(e) => format!("{} vertices", e.vertices.len()),
|
|
EntityType::Polyline(e) => format!("{} vertices", e.vertices.len()),
|
|
EntityType::Polyline2D(e) => format!("{} vertices", e.vertices.len()),
|
|
EntityType::Polyline3D(e) => format!("{} vertices", e.vertices.len()),
|
|
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
|
|
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
|
|
EntityType::Spline(e) => format!("{} ctrl pts", e.control_points.len()),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline).
|
|
fn csv_escape(s: &str) -> String {
|
|
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
|
format!("\"{}\"", s.replace('"', "\"\""))
|
|
} else {
|
|
s.to_string()
|
|
}
|
|
}
|
|
|
|
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
|
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
|
// decimal numbers. Self-contained (no external dependency).
|
|
fn arith_eval(expr: &str) -> Result<f64, String> {
|
|
let mut p = ArithParser {
|
|
chars: expr.chars().filter(|c| !c.is_whitespace()).collect(),
|
|
pos: 0,
|
|
};
|
|
let v = p.expr()?;
|
|
if p.pos != p.chars.len() {
|
|
return Err(format!("unexpected '{}'", p.chars[p.pos]));
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
struct ArithParser {
|
|
chars: Vec<char>,
|
|
pos: usize,
|
|
}
|
|
|
|
impl ArithParser {
|
|
fn peek(&self) -> Option<char> {
|
|
self.chars.get(self.pos).copied()
|
|
}
|
|
|
|
// expr = term (('+' | '-') term)*
|
|
fn expr(&mut self) -> Result<f64, String> {
|
|
let mut v = self.term()?;
|
|
while let Some(op) = self.peek() {
|
|
if op == '+' || op == '-' {
|
|
self.pos += 1;
|
|
let rhs = self.term()?;
|
|
v = if op == '+' { v + rhs } else { v - rhs };
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
// term = factor (('*' | '/') factor)*
|
|
fn term(&mut self) -> Result<f64, String> {
|
|
let mut v = self.factor()?;
|
|
while let Some(op) = self.peek() {
|
|
if op == '*' || op == '/' {
|
|
self.pos += 1;
|
|
let rhs = self.factor()?;
|
|
if op == '/' {
|
|
if rhs == 0.0 {
|
|
return Err("division by zero".into());
|
|
}
|
|
v /= rhs;
|
|
} else {
|
|
v *= rhs;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
// factor = ('+' | '-') factor | '(' expr ')' | number
|
|
fn factor(&mut self) -> Result<f64, String> {
|
|
match self.peek() {
|
|
Some('+') => {
|
|
self.pos += 1;
|
|
self.factor()
|
|
}
|
|
Some('-') => {
|
|
self.pos += 1;
|
|
Ok(-self.factor()?)
|
|
}
|
|
Some('(') => {
|
|
self.pos += 1;
|
|
let v = self.expr()?;
|
|
if self.peek() != Some(')') {
|
|
return Err("missing ')'".into());
|
|
}
|
|
self.pos += 1;
|
|
Ok(v)
|
|
}
|
|
Some(c) if c.is_ascii_digit() || c == '.' => self.number(),
|
|
Some(c) => Err(format!("unexpected '{c}'")),
|
|
None => Err("unexpected end of expression".into()),
|
|
}
|
|
}
|
|
|
|
fn number(&mut self) -> Result<f64, String> {
|
|
let start = self.pos;
|
|
while let Some(c) = self.peek() {
|
|
if c.is_ascii_digit() || c == '.' {
|
|
self.pos += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
let s: String = self.chars[start..self.pos].iter().collect();
|
|
s.parse::<f64>().map_err(|_| format!("bad number '{s}'"))
|
|
}
|
|
}
|