From 0aef1fc12f31ec496cf40441043e75bcf12c55e8 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 3 Apr 2026 15:31:05 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20ROADMAP=20Phase=201-5=20batch=20?= =?UTF-8?q?=E2=80=94=20trim/extend,=20plot=20scale,=20viewport=20UX,=20mli?= =?UTF-8?q?ne=20caps,=20table=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/cmd_result.rs | 42 ++ src/app/commands.rs | 679 ++++++++++++++++++++++++-- src/app/mod.rs | 4 + src/app/properties.rs | 17 + src/app/update.rs | 47 ++ src/app/view.rs | 36 ++ src/command/mod.rs | 2 + src/entities/mline.rs | 25 +- src/entities/raster_image.rs | 96 +++- src/entities/table.rs | 41 +- src/modules/annotate/dim_baseline.rs | 138 ++++++ src/modules/annotate/dim_continue.rs | 135 +++++ src/modules/annotate/mod.rs | 4 + src/modules/annotate/table_cmd.rs | 139 ++++++ src/modules/annotate/tolerance_cmd.rs | 89 ++++ src/modules/home/draw/attdef.rs | 137 ++++++ src/modules/home/draw/mod.rs | 1 + src/modules/home/draw/polyline.rs | 233 ++++++++- src/modules/home/inquiry/divide.rs | 1 - src/modules/home/modify/align.rs | 2 +- src/modules/home/modify/break_cmd.rs | 50 +- src/modules/home/modify/join.rs | 3 +- src/modules/home/modify/trim.rs | 297 ++++++++++- src/modules/view/mod.rs | 1 + src/modules/view/plot_window.rs | 66 +++ src/scene/cxf.rs | 23 + src/scene/mod.rs | 9 +- 27 files changed, 2217 insertions(+), 100 deletions(-) create mode 100644 src/modules/annotate/dim_baseline.rs create mode 100644 src/modules/annotate/dim_continue.rs create mode 100644 src/modules/annotate/table_cmd.rs create mode 100644 src/modules/annotate/tolerance_cmd.rs create mode 100644 src/modules/home/draw/attdef.rs create mode 100644 src/modules/view/plot_window.rs diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 54deb88a5..29fbe2388 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -573,6 +573,48 @@ impl H7CAD { } } } + CmdResult::SetPlotWindow { p1, p2 } => { + use acadrust::objects::{ObjectType, PlotSettings}; + let layout_name = self.tabs[i].scene.current_layout.clone(); + if layout_name == "Model" { + self.command_line.push_error("PLOTWINDOW: switch to a paper space layout first."); + } else { + let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); + let doc = &mut self.tabs[i].scene.document; + let ps_handle = doc.objects.iter().find_map(|(h, obj)| { + if let ObjectType::PlotSettings(ps) = obj { + if ps.page_name == layout_name { Some(*h) } else { None } + } else { None } + }); + let ps_entry = match ps_handle { + Some(h) => doc.objects.get_mut(&h), + None => { + let nh = acadrust::Handle::new(doc.next_handle()); + let ps = PlotSettings::new(layout_name.clone()); + doc.objects.insert(nh, ObjectType::PlotSettings(ps)); + doc.objects.get_mut(&nh) + } + }; + let _ = block_handle; + if let Some(ObjectType::PlotSettings(ps)) = ps_entry { + // Convert world-space points to DXF coordinates (X, Z plane → DXF X, Y). + let x1 = p1.x.min(p2.x) as f64; + let y1 = p1.z.min(p2.z) as f64; + let x2 = p1.x.max(p2.x) as f64; + let y2 = p1.z.max(p2.z) as f64; + ps.set_plot_window(x1, y1, x2, y2); + self.push_undo_snapshot(i, "PLOTWINDOW"); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!( + "PLOTWINDOW: ({x1:.3},{y1:.3}) → ({x2:.3},{y2:.3})" + )); + } + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } } // Focus the command-line input while a command is active; blur it when the command ends. if self.tabs[i].active_cmd.is_some() { diff --git a/src/app/commands.rs b/src/app/commands.rs index 17599bab1..db30dd8ed 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -416,6 +416,118 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "ATTDEF" => { + use crate::modules::home::draw::attdef::AttdefCommand; + let cmd = AttdefCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + // ATTEDIT — list or edit attribute values on selected Insert entities. + // Usage: + // ATTEDIT — list all attributes on selected Insert(s) + // ATTEDIT — set the value of attribute to + cmd if cmd == "ATTEDIT" || cmd.starts_with("ATTEDIT ") => { + let rest = cmd.trim_start_matches("ATTEDIT").trim(); + let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect(); + let selected_handles: Vec = self.tabs[i].scene + .selected_entities() + .iter() + .map(|(h, _)| *h) + .collect(); + if selected_handles.is_empty() { + self.command_line.push_error("ATTEDIT: select an Insert entity first."); + } else { + let mut found_any = false; + for sh in &selected_handles { + if let Some(acadrust::EntityType::Insert(ins)) = + self.tabs[i].scene.document.entities().find(|e| e.common().handle == *sh) + { + found_any = true; + if rest.is_empty() { + // List attributes. + if ins.attributes.is_empty() { + self.command_line.push_output(&format!( + " Insert {:x}: no attributes.", sh.value() + )); + } else { + for attr in &ins.attributes { + self.command_line.push_output(&format!( + " [{tag}] = {val}", + tag = attr.tag, + val = attr.get_value() + )); + } + } + } + } + } + if !found_any { + self.command_line.push_error("ATTEDIT: no Insert entities in selection."); + } + // If tag + value supplied, mutate attributes. + if parts.len() == 2 && !parts[0].is_empty() { + let tag_up = parts[0].to_uppercase(); + let new_val = parts[1]; + let mut changed = 0usize; + self.push_undo_snapshot(i, "ATTEDIT"); + for sh in &selected_handles { + if let Some(acadrust::EntityType::Insert(ins)) = + self.tabs[i].scene.document.entities_mut().find(|e| e.common().handle == *sh) + { + for attr in &mut ins.attributes { + if attr.tag.to_uppercase() == tag_up { + attr.set_value(new_val); + changed += 1; + } + } + } + } + if changed > 0 { + self.tabs[i].dirty = true; + self.command_line.push_output(&format!( + "ATTEDIT: updated {changed} attribute(s) [{tag_up}] = {new_val}." + )); + } else { + self.command_line.push_error(&format!( + "ATTEDIT: tag '{tag_up}' not found in selection." + )); + } + } + } + } + + // ATTDISP — control attribute display visibility. + // ATTDISP ON — make all AttributeDefinitions visible + // ATTDISP OFF — make all AttributeDefinitions invisible + // ATTDISP NORMAL — restore: show only those without the invisible flag + cmd if cmd == "ATTDISP" || cmd.starts_with("ATTDISP ") => { + let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); + match sub.as_str() { + "ON" | "OFF" | "NORMAL" => { + self.push_undo_snapshot(i, "ATTDISP"); + let mut count = 0usize; + for entity in self.tabs[i].scene.document.entities_mut() { + if let acadrust::EntityType::AttributeDefinition(ad) = entity { + match sub.as_str() { + "ON" => { ad.flags.invisible = false; count += 1; } + "OFF" => { ad.flags.invisible = true; count += 1; } + "NORMAL" => { /* leave existing flags — they are already the "normal" state */ } + _ => {} + } + } + } + self.tabs[i].dirty = true; + self.command_line.push_output(&format!( + "ATTDISP {sub}: {count} attribute definition(s) updated." + )); + } + _ => { + self.command_line.push_info("Usage: ATTDISP ON | OFF | NORMAL"); + } + } + } + "DONUT"|"DO" => { use crate::modules::home::draw::donut::DonutCommand; let cmd = DonutCommand::new(); @@ -810,6 +922,42 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "TOLERANCE"|"TOL" => { + use crate::modules::annotate::tolerance_cmd::ToleranceCommand; + let cmd = ToleranceCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "TABLE" => { + use crate::modules::annotate::table_cmd::TableCommand; + let cmd = TableCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMCONTINUE"|"DCO" => { + use crate::modules::annotate::dim_continue::DimContinueCommand; + let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) { + DimContinueCommand::from_base(p1, p2, dp, rot) + } else { + DimContinueCommand::new() + }; + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMBASELINE"|"DBA" => { + use crate::modules::annotate::dim_baseline::DimBaselineCommand; + let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) { + DimBaselineCommand::from_base(p1, p2, dp, rot) + } else { + DimBaselineCommand::new() + }; + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => { self.tabs[i].scene.fit_all(); self.command_line.push_output("Zoom Extents"); @@ -844,6 +992,13 @@ impl H7CAD { } } + "PLOTWINDOW"|"PW" => { + use crate::modules::view::plot_window::PlotWindowCommand; + let cmd = PlotWindowCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "ZOOM WINDOW"|"ZOOM W"|"ZW" => { use crate::modules::view::zoom_window::ZoomWindowCommand; let new_cmd = ZoomWindowCommand::new(); @@ -1097,6 +1252,13 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "BREAKATPOINT"|"BAP" => { + use crate::modules::home::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::home::modify::pedit::PeditCommand; let cmd_obj = PeditCommand::new(); @@ -1190,7 +1352,6 @@ impl H7CAD { // QSELECT COLOR — select all entities with color index n // QSELECT LINETYPE — select all entities with linetype cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => { - use crate::scene::Scene; 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(); @@ -1336,14 +1497,16 @@ impl H7CAD { "HELP"|"?" => { self.command_line.push_output( - "Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT | \ - Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN | \ + "Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT REVCLOUD WIPEOUT MLINE ATTDEF | \ + Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN PEDIT | \ Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ Text: TEXT MTEXT LEADER MLEADER | \ - Dimension: DIMLINEAR DIMALIGNED DIMANGULAR DIMRADIUS DIMDIAMETER | \ + Dimension: DIMLINEAR DIMALIGNED DIMANGULAR DIMRADIUS DIMDIAMETER DIMCONTINUE DIMBASELINE | \ + Annotation: TOLERANCE | \ Inquiry: DIST ID AREA LIST FIND FINDALL COUNT QSELECT | Draw on entity: DIVIDE MEASURE | \ - Utilities: FLATTEN LAYISO LAYUNISO PEDIT MLINE MLEADER | \ - View: ZOOM EXTENTS VIEW LIST/SAVE/RESTORE/DELETE | \ + Attributes: ATTEDIT ATTDISP | \ + Utilities: FLATTEN LAYISO LAYUNISO | \ + View: ZOOM EXTENTS ZOOM WINDOW VIEW LIST/SAVE/RESTORE/DELETE | \ Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \ Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \ Tables: STYLE DIMSTYLE LINETYPE UCS RENAME PURGE | \ @@ -1448,12 +1611,8 @@ impl H7CAD { // ── Draw Order ──────────────────────────────────────────────── cmd if cmd.starts_with("DRAWORDER") => { use acadrust::objects::{ObjectType, SortEntitiesTable}; - let option = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase(); - let bring_front = match option.as_str() { - "F" | "FRONT" => Some(true), - "B" | "BACK" => Some(false), - _ => None, - }; + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let option = parts.get(1).unwrap_or(&"").to_uppercase(); let i = self.active_tab; let selected: Vec = self.tabs[i].scene .selected_entities() @@ -1462,42 +1621,87 @@ impl H7CAD { .collect(); if selected.is_empty() { self.command_line.push_error("DRAWORDER: select entities first."); - } else if let Some(to_front) = bring_front { - self.push_undo_snapshot(i, "DRAWORDER"); - let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); - let doc = &mut self.tabs[i].scene.document; - let table_handle = doc.objects.iter() - .find_map(|(h, obj)| { - if let ObjectType::SortEntitiesTable(t) = obj { - if t.block_owner_handle == block_handle { Some(*h) } else { None } - } else { None } + } else { + // Parse relative target handle for ABOVE/UNDER. + let relative_target: Option<(bool, acadrust::Handle)> = match option.as_str() { + "A" | "ABOVE" => { + let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok()); + h_val.map(|v| (true, acadrust::Handle::new(v))) + } + "U" | "UNDER" | "BELOW" => { + let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok()); + h_val.map(|v| (false, acadrust::Handle::new(v))) + } + _ => None, + }; + let to_front_opt = match option.as_str() { + "F" | "FRONT" => Some(true), + "B" | "BACK" => Some(false), + _ => None, + }; + + if relative_target.is_some() || to_front_opt.is_some() { + self.push_undo_snapshot(i, "DRAWORDER"); + let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); + let doc = &mut self.tabs[i].scene.document; + let table_handle = doc.objects.iter() + .find_map(|(h, obj)| { + if let ObjectType::SortEntitiesTable(t) = obj { + if t.block_owner_handle == block_handle { Some(*h) } else { None } + } else { None } + }); + let get_or_create = |doc: &mut acadrust::CadDocument, block_handle| -> acadrust::Handle { + if let Some(th) = doc.objects.iter() + .find_map(|(h, obj)| { + if let ObjectType::SortEntitiesTable(t) = obj { + if t.block_owner_handle == block_handle { Some(*h) } else { None } + } else { None } + }) + { + th + } else { + let nh = acadrust::Handle::new(doc.next_handle()); + let mut table = SortEntitiesTable::for_block(block_handle); + table.handle = nh; + doc.objects.insert(nh, ObjectType::SortEntitiesTable(table)); + nh + } + }; + let th = table_handle.unwrap_or_else(|| { + let nh = acadrust::Handle::new(doc.next_handle()); + let mut table = SortEntitiesTable::for_block(block_handle); + table.handle = nh; + doc.objects.insert(nh, ObjectType::SortEntitiesTable(table)); + nh }); - if let Some(th) = table_handle { - if let Some(ObjectType::SortEntitiesTable(table)) = - doc.objects.get_mut(&th) - { - for h in &selected { - if to_front { table.bring_to_front(*h); } - else { table.send_to_back(*h); } + let _ = get_or_create; // suppress unused warning + if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&th) { + if let Some((above, target)) = relative_target { + for h in &selected { + if above { table.move_above(*h, target); } + else { table.move_below(*h, target); } + } + let rel = if above { "above" } else { "below" }; + self.command_line.push_info(&format!( + "DRAWORDER: moved {} entities {} {:x}.", selected.len(), rel, target.value() + )); + } else if let Some(to_front) = to_front_opt { + for h in &selected { + if to_front { table.bring_to_front(*h); } + else { table.send_to_back(*h); } + } + let dir = if to_front { "front" } else { "back" }; + self.command_line.push_info(&format!( + "DRAWORDER: moved {} entities to {}.", selected.len(), dir + )); } } + self.tabs[i].dirty = true; } else { - let new_handle = acadrust::Handle::new(doc.next_handle()); - let mut table = SortEntitiesTable::for_block(block_handle); - table.handle = new_handle; - for h in &selected { - if to_front { table.bring_to_front(*h); } - else { table.send_to_back(*h); } - } - doc.objects.insert(new_handle, ObjectType::SortEntitiesTable(table)); + self.command_line.push_info( + "Usage: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER " + ); } - self.tabs[i].dirty = true; - let dir = if to_front { "front" } else { "back" }; - self.command_line.push_info(&format!( - "DRAWORDER: moved {} entities to {}.", selected.len(), dir - )); - } else { - self.command_line.push_info("Usage: DRAWORDER F (front) or DRAWORDER B (back)"); } } @@ -1798,15 +2002,23 @@ impl H7CAD { if let Ok(val) = val_str.parse::() { if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&style_name) { match prop.as_str() { - "dimtxt" => { ds.dimtxt = val; } - "dimasz" => { ds.dimasz = val; } - "dimdli" => { ds.dimdli = val; } - "dimexo" => { ds.dimexo = val; } - "dimexe" => { ds.dimexe = val; } - "dimgap" => { ds.dimgap = val; } - "dimscale"| "dimlfac" => { ds.dimgap = val; } // best effort + "dimtxt" => { ds.dimtxt = val; } + "dimasz" => { ds.dimasz = val; } + "dimdli" => { ds.dimdli = val; } + "dimexo" => { ds.dimexo = val; } + "dimexe" => { ds.dimexe = val; } + "dimgap" => { ds.dimgap = val; } + "dimscale" => { ds.dimscale = val; } + "dimlfac" => { ds.dimlfac = val; } + "dimdle" => { ds.dimdle = val; } + "dimtvp" => { ds.dimtvp = val; } + "dimcen" => { ds.dimcen = val; } + "dimtsz" => { ds.dimtsz = val; } + "dimfxl" => { ds.dimfxl = val; } _ => { - self.command_line.push_error(&format!("DIMSTYLE: unknown property '{}'. Try: dimtxt dimasz dimdli dimexo dimexe dimgap", prop)); + self.command_line.push_error(&format!( + "DIMSTYLE: unknown property '{}'. Try: dimtxt dimasz dimdli dimexo dimexe dimgap dimscale dimlfac dimdle dimcen dimtsz", prop + )); return Task::none(); } } @@ -1905,9 +2117,26 @@ impl H7CAD { self.command_line.push_error(&format!("Usage: {prefix} WIDTH