use acadrust::entities::{AttributeDefinition, AttributeEntity, Entity, Insert}; use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Vec3}; use crate::t; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; pub fn tool() -> ToolDef { ToolDef { id: "INSERT", label: "Insert Block", icon: IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")), event: ModuleEvent::Command("INSERT".to_string()), } } enum Step { Name, Point { name: String, }, /// Attribute filling: prompt the user tag by tag. FillAttr { /// The block's attribute definitions — carried in full so the created /// attributes inherit their position / alignment / height / rotation / /// style, not just tag + prompt + default (#255). attdefs: Vec, /// Index of the attdef currently being prompted. idx: usize, /// (tag, value) pairs collected so far, in attdef order. values: Vec<(String, String)>, }, } /// Which numeric value the insertion-point step is currently waiting for after /// a Scale / Rotate keyword. #[derive(Clone, Copy)] enum AwaitKind { Scale, Rotation, } pub struct InsertBlockCommand { available: Vec, step: Step, /// Uniform X/Y scale applied to the placed block (default 1). x_scale: f64, y_scale: f64, /// Rotation applied to the placed block, in radians (default 0). rotation_rad: f64, /// Set while a Scale/Rotate value is being typed at the insertion step. awaiting: Option, /// Pending Insert entity stored while attr-filling is in progress. pending_insert: Option, /// Optional drag preview: the block's wire geometry plus the base point it /// is measured from, so `on_preview_wires` can rubber-band it to the /// cursor. Set by paste-as-block; empty for a plain INSERT. preview: Option<(Vec, Vec3)>, plane: WorkingPlane, } impl InsertBlockCommand { pub fn new(available: Vec) -> Self { Self { available, step: Step::Name, x_scale: 1.0, y_scale: 1.0, rotation_rad: 0.0, awaiting: None, pending_insert: None, preview: None, plane: WorkingPlane::default(), } } /// Start the command already locked to `name`, skipping the name prompt and /// going straight to "specify insertion point". `preview_wires` (measured /// from `base`) rubber-band under the cursor. Used by paste-as-block, which /// has just defined the block and only needs the drop point. pub fn new_for_block(name: String, preview_wires: Vec, base: Vec3) -> Self { Self { available: vec![name.clone()], step: Step::Point { name }, x_scale: 1.0, y_scale: 1.0, rotation_rad: 0.0, awaiting: None, pending_insert: None, preview: Some((preview_wires, base)), plane: WorkingPlane::default(), } } } impl CadCommand for InsertBlockCommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } fn name(&self) -> &'static str { "INSERT" } fn prompt(&self) -> String { match &self.step { Step::Name => { let hint = if self.available.is_empty() { String::new() } else { format!(" [{}]", self.available.join(", ")) }; t!("INSERT Enter block name:%{hint}", hint = hint).into_owned() } Step::Point { name } => match self.awaiting { Some(AwaitKind::Scale) => t!("INSERT Specify scale factor <1>:").into_owned(), Some(AwaitKind::Rotation) => t!("INSERT Specify rotation angle <0>:").into_owned(), None => t!( "INSERT Specify insertion point for \"%{name}\" [Scale/Rotate]:", name = name ) .into_owned(), }, Step::FillAttr { attdefs, idx, .. } => { if let Some(ad) = attdefs.get(*idx) { let default_hint = if ad.default_value.is_empty() { String::new() } else { format!(" <{}>", ad.default_value) }; let prompt_text = if ad.prompt.is_empty() { ad.tag.as_str() } else { ad.prompt.as_str() }; t!( "INSERT %{prompt}%{hint}:", prompt = prompt_text, hint = default_hint ) .into_owned() } else { t!("INSERT Filling attributes...").into_owned() } } } } fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { Step::Name => CmdResult::NeedPoint, Step::Point { name } => { let point = self.plane.to_local(pt); let mut ins = Insert::new( name.clone(), Vector3::new(point.x, point.y, point.z), ); ins.set_x_scale(self.x_scale); ins.set_y_scale(self.y_scale); ins.rotation = self.rotation_rad; ins.apply_transform(&self.plane.to_world_transform()); let block_name = name.clone(); self.pending_insert = Some(ins); // Signal the host to check for attdefs. CmdResult::AttreqNeeded { block_name } } Step::FillAttr { .. } => CmdResult::NeedPoint, } } fn on_enter(&mut self) -> CmdResult { match &self.step { // A bare Enter while a scale/rotation value is awaited keeps the // current default instead of cancelling the command. Step::Point { .. } if self.awaiting.is_some() => { self.awaiting = None; CmdResult::NeedPoint } Step::Name | Step::Point { .. } => CmdResult::Cancel, Step::FillAttr { .. } => { // Treat Enter as accepting the default. self.accept_attr_value("") } } } fn wants_text_input(&self) -> bool { matches!( self.step, Step::Name | Step::FillAttr { .. } | Step::Point { .. } ) } fn point_step_accepts_keywords(&self) -> bool { // The insertion-point step also takes Scale / Rotate keywords while // still accepting a point pick. Once a value is being typed (awaiting), // route the whole number through `on_text_input` instead. matches!(self.step, Step::Point { .. }) && self.awaiting.is_none() } fn wants_text_with_spaces(&self) -> bool { // Block names don't embed whitespace, but attribute values do. matches!(self.step, Step::FillAttr { .. }) } fn on_text_input(&mut self, text: &str) -> Option { match &self.step { Step::Name => { let name = text.trim(); if !self.available.iter().any(|c| c.eq_ignore_ascii_case(name)) { return None; } self.step = Step::Point { name: name.to_string(), }; Some(CmdResult::NeedPoint) } Step::FillAttr { .. } => Some(self.accept_attr_value(text)), Step::Point { .. } => { // Typing the value awaited after a Scale / Rotate keyword. if let Some(kind) = self.awaiting { if let Ok(v) = text.trim().parse::() { match kind { AwaitKind::Scale if v != 0.0 => { self.x_scale = v; self.y_scale = v; } AwaitKind::Scale => {} AwaitKind::Rotation => self.rotation_rad = v.to_radians(), } } self.awaiting = None; return Some(CmdResult::NeedPoint); } match text.trim().to_uppercase().as_str() { "S" | "SCALE" => { self.awaiting = Some(AwaitKind::Scale); Some(CmdResult::NeedPoint) } "R" | "ROTATE" => { self.awaiting = Some(AwaitKind::Rotation); Some(CmdResult::NeedPoint) } _ => None, } } } } fn on_preview_wires(&mut self, pt: DVec3) -> Vec { let pt = pt.as_vec3(); match (&self.step, &self.preview) { (Step::Point { .. }, Some((wires, base))) => { let delta = pt - *base; wires.iter().map(|w| w.translated(delta)).collect() } _ => vec![], } } fn attreq_set_attdefs(&mut self, attdefs: Vec) { self.step = Step::FillAttr { attdefs, idx: 0, values: vec![], }; } fn attreq_take_insert(&mut self) -> Option { self.pending_insert .take() .map(|ins| EntityType::Insert(ins)) } } impl InsertBlockCommand { /// Accept the current attribute value (empty = use default) and advance. /// Returns CommitAndExit when all attdefs have been filled. fn accept_attr_value(&mut self, text: &str) -> CmdResult { let (tag, default, next_idx, total) = match &self.step { Step::FillAttr { attdefs, idx, .. } => { let ad = &attdefs[*idx]; (ad.tag.clone(), ad.default_value.clone(), idx + 1, attdefs.len()) } _ => return CmdResult::Cancel, }; let value = if text.trim().is_empty() { default } else { text.trim().to_string() }; if let Step::FillAttr { ref mut values, ref mut idx, .. } = self.step { values.push((tag, value)); *idx = next_idx; } if next_idx >= total { // All attdefs filled — build the INSERT's attribute list. Each // attribute inherits its ATTDEF's geometry (position, alignment, // height, rotation, style) via `from_definition`, then the block's // insertion transform places it into WCS so it lands where the block // put it instead of stacking at the origin (#255). let (attdefs, values) = match &self.step { Step::FillAttr { attdefs, values, .. } => (attdefs.clone(), values.clone()), _ => (vec![], vec![]), }; let mut ins = match self.pending_insert.take() { Some(i) => i, None => return CmdResult::Cancel, }; let xform = ins.get_transform(); for (ad, (_tag, value)) in attdefs.iter().zip(values.iter()) { let mut attr = AttributeEntity::from_definition(ad, Some(value.clone())); attr.apply_transform(&xform); ins.attributes.push(attr); } CmdResult::CommitAndExit(EntityType::Insert(ins)) } else { CmdResult::NeedPoint } } } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["INSERT"] }); // InsertBlockCommand