From 4161262e51c587275b8a6b174258b80e7033da19 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 8 Oct 2026 11:43:13 -0300 Subject: [PATCH 1/5] fix: preserve linetypes on circles and arcs --- src/scene/pipeline/circle_gpu.rs | 10 ++++-- src/scene/pipeline/wire_arena.rs | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/scene/pipeline/circle_gpu.rs b/src/scene/pipeline/circle_gpu.rs index a59f9614e..33104fcbf 100644 --- a/src/scene/pipeline/circle_gpu.rs +++ b/src/scene/pipeline/circle_gpu.rs @@ -306,7 +306,12 @@ pub fn extract_circle_instances( /// Allocation-free check whether this wire consists entirely of analytical circle/arc instances. pub fn can_extract_circle_instances(wire: &crate::scene::WireModel) -> bool { - if wire.tangent_geoms.is_empty() + // Patterned circular curves stay on the regular wire pipeline. Its dash + // path is shared with LINE/POLYLINE and remains consistent across file + // open/save cycles. The analytical circle path is reserved for continuous + // curves until its linetype phase/LOD behaviour is fully equivalent. + if wire.pattern_length > 0.0 + || wire.tangent_geoms.is_empty() || !wire.fill_tris.is_empty() || wire.fill_is_3d || !wire.text_verts.is_empty() @@ -344,7 +349,8 @@ pub fn extract_circle_instances_into( draw_depth: f32, out: &mut Vec, ) -> bool { - if wire.tangent_geoms.is_empty() + if wire.pattern_length > 0.0 + || wire.tangent_geoms.is_empty() || !wire.fill_tris.is_empty() || wire.fill_is_3d || !wire.text_verts.is_empty() diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 563047eee..8951b309d 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -1774,6 +1774,58 @@ mod tests { assert_eq!(partitioned.regular.len(), 1); } + #[test] + fn patterned_circle_and_arc_stay_in_regular_wire_pipeline() { + use crate::scene::model::wire_model::TangentGeom; + + let mut circle = WireModel::default(); + circle.points = vec![ + [10.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [-10.0, 0.0, 0.0], + ]; + circle.tangent_geoms.push(TangentGeom::PlanarCircle { + center: [0.0, 0.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 10.0, + }); + circle.pattern_length = 1.0; + circle.pattern = [0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + + let mut arc = WireModel::default(); + arc.points = vec![ + [10.0, 0.0, 0.0], + [7.0, 7.0, 0.0], + [0.0, 10.0, 0.0], + ]; + arc.tangent_geoms.push(TangentGeom::Arc { + center: [0.0, 0.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 10.0, + start_angle: 0.0, + end_angle: std::f64::consts::FRAC_PI_2, + }); + arc.pattern_length = 1.0; + arc.pattern = [0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + + let wires = vec![circle, arc]; + let depth_map = rustc_hash::FxHashMap::default(); + let partitioned = partition_wires(&wires, &depth_map); + + assert_eq!( + partitioned.circle_instances.len(), + 0, + "patterned circular curves must not use the analytical circle path" + ); + assert_eq!( + partitioned.regular.len(), + 2, + "patterned circle and arc must fall back to the regular dashed-wire path" + ); + } + #[test] fn partition_wires_partitions_thick_and_tapered_arcs() { use crate::scene::model::wire_model::TangentGeom; From af3118b8267b5b09693f2f63c1737ddf60fdba79 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 8 Oct 2026 18:29:14 -0300 Subject: [PATCH 2/5] fix: preserve layout plot settings and CTB block colors --- src/app/mod.rs | 10 + src/app/update/file.rs | 697 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 650 insertions(+), 57 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 50dc08597..69f7169e6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1109,6 +1109,14 @@ pub(super) struct OpenCADStudio { /// Snapshot of the dialog's settings taken when it opened, restored by the /// `` list entry. plot_prev: Option, + /// Tab/layout that last owned the Plot dialog. Reopening Plot in the same + /// context keeps the user's last paper, scale, CTB and output choices + /// instead of reapplying the layout page setup over them. + plot_prev_context: Option<(usize, String)>, + /// Printer whose capability reply is allowed to reconcile the current + /// sheet. Set only after an explicit printer/page-setup choice; merely + /// opening Plot must never replace the drawing's stored paper size. + plot_reconcile_printer_media: Option, /// Full source settings behind the fields currently shown in Plot. plot_setup_template: Option, /// Paper layouts shown by Print All, in tab order with their selection. @@ -4400,6 +4408,8 @@ impl OpenCADStudio { plot_orientation: crate::io::paper_catalog::Orientation::Landscape, plot_dialog: crate::ui::window::plot::PlotDialogState::default(), plot_prev: None, + plot_prev_context: None, + plot_reconcile_printer_media: None, plot_setup_template: None, print_all_layouts: Vec::new(), print_all_options: false, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 6e0c8f294..c28ba7358 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -408,10 +408,16 @@ fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> { } /// Stamp the owning entity's effective color index onto a plot wire so CTB -/// plot-style lookups (`wire.aci > 0`) apply: an entity's own ACI 1-255 is -/// used as-is, ByLayer resolves through the layer table, and true-color -/// objects keep `aci = 0` ("0 means true-color" — no CTB mapping). Wires -/// whose owner cannot be resolved keep the index they carry. +/// plot-style lookups (`wire.aci > 0`) apply. +/// +/// Direct entities are resolved here from their authored color/layer. Expanded +/// block wires are different: `BlockCache` has already resolved their effective +/// ACI through ByBlock and layer-0/ByLayer inheritance. Their wire name points +/// at the host INSERT rather than the child entity that produced the geometry, +/// so resolving that INSERT again would destroy the child's effective ACI. +/// +/// In particular, a true-color INSERT on an indexed-color layer would overwrite +/// an inherited child ACI with 0 and bypass the CTB entirely. fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireModel) { let Some(handle) = crate::scene::Scene::handle_from_wire_name(&wire.name) else { return; @@ -419,6 +425,14 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode let Some(entity) = scene.document.get_entity(handle) else { return; }; + + // Expanded block geometry already carries the effective ACI calculated by + // BlockCache. `wire.name` identifies the host INSERT for picking, not the + // block child whose color inheritance produced `wire.aci`. + if matches!(entity, codec::EntityType::Insert(_)) { + return; + } + let common = entity.common(); wire.aci = match &common.color { codec::types::Color::Index(index) => *index, @@ -435,6 +449,77 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode }; } +#[cfg(test)] +mod plot_owner_aci_tests { + use super::plot_owner_aci; + + #[test] + fn expanded_block_wire_keeps_its_resolved_effective_aci() { + let mut scene = crate::scene::Scene::new(); + + // Real-world regression case: + // the INSERT itself has explicit true-color white, while its block + // children are layer 0 + ByLayer. BlockCache resolves those children + // through the INSERT's layer and stores that effective ACI on the wire. + let mut insert = codec::entities::Insert::new( + "*U264", + codec::types::Vector3::ZERO, + ); + insert.common.color = codec::types::Color::Rgb { + r: 255, + g: 255, + b: 255, + }; + + let insert_handle = scene + .document + .add_entity(codec::EntityType::Insert(insert)) + .expect("insert must be added"); + + let mut wire = crate::scene::WireModel::default(); + wire.name = insert_handle.value().to_string(); + + // Pretend BlockCache resolved the layer-0 child to indexed color 5. + // The plot preparation step must preserve it instead of replacing it + // with ACI 0 from the host INSERT's true color. + wire.aci = 5; + + plot_owner_aci(&scene, &mut wire); + + assert_eq!( + wire.aci, 5, + "expanded block geometry must keep the effective ACI already resolved by BlockCache" + ); + } + + #[test] + fn direct_entity_still_refreshes_aci_from_its_owner() { + let mut scene = crate::scene::Scene::new(); + + let mut line = codec::entities::Line::from_points( + codec::types::Vector3::ZERO, + codec::types::Vector3::new(1.0, 0.0, 0.0), + ); + line.common.color = codec::types::Color::Index(1); + + let line_handle = scene + .document + .add_entity(codec::EntityType::Line(line)) + .expect("line must be added"); + + let mut wire = crate::scene::WireModel::default(); + wire.name = line_handle.value().to_string(); + wire.aci = 5; + + plot_owner_aci(&scene, &mut wire); + + assert_eq!( + wire.aci, 1, + "ordinary entity plot wires must still resolve ACI from their owner" + ); + } +} + fn plot_scene_content( scene: &crate::scene::Scene, paper_space_last: bool, @@ -1710,6 +1795,22 @@ impl OpenCADStudio { } } + /// Drop Plot-dialog state that belongs to the previously active drawing. + /// + /// Plot settings such as paper size and CTB are document/layout data. + /// A newly opened DWG may reuse the same tab index and layout name as the + /// startup drawing, so `(tab, layout)` alone is not enough to identify the + /// context. Keeping that cache makes the first Plot dialog show the startup + /// A4/no-CTB state; the second opening then appears correct only because the + /// first one refreshed the runtime state. + fn reset_plot_session_for_opened_document(&mut self) { + self.plot_prev = None; + self.plot_prev_context = None; + self.plot_reconcile_printer_media = None; + self.plot_setup_template = None; + self.plot_window = None; + } + pub(super) fn on_file_opened( &mut self, name: String, @@ -1981,6 +2082,12 @@ impl OpenCADStudio { self.tabs[i].scene.load_current_layout_state(); self.tabs[i].refresh_active_ucs(); } + + // The tab may have been reused for the newly opened file. Any Plot + // dialog cache belongs to the previous/startup drawing and must not + // win over this DWG's embedded layout settings on the first Ctrl+P. + self.reset_plot_session_for_opened_document(); + // Object isolation is session-only. A newly opened drawing must // not inherit the previous tab's filter, and persisted entity // visibility remains independent (not an isolation session). @@ -3794,22 +3901,27 @@ impl OpenCADStudio { let previous_style = self.active_plot_style.clone(); let previous_window = self.plot_window; - // Once Print All has an explicit override, reopening Options must keep - // those settings instead of replacing them with the active layout's - // page setup. - let keep_print_all_override = self.print_all_settings_override; - - // Refresh runtime data such as printers, paper sizes, plot styles and - // named page setups. + // Refresh runtime-only data such as printers, available media, + // CTB names, scales and page-setup rows. + // + // `on_plot_dialog_open()` also seeds the editor from the active + // layout. That is correct for normal PLOT, but Print All Options is a + // shared batch override editor: its persisted user choices must win. let task = self.on_plot_dialog_open(); - if keep_print_all_override { - // Restore only the user-editable plot settings. Runtime lists populated - // above remain intact. - self.plot_dialog.copy_settings_from(&previous); - self.active_plot_style = previous_style.clone(); - self.plot_window = previous_window; - } + // Restore only user-editable values. `copy_settings_from` deliberately + // leaves the freshly refreshed runtime lists intact. + // + // Do this even on the first Print All Options opening after restarting + // OCS. Previously this happened only when `print_all_settings_override` + // was already true, so the first opening always fell back to the + // active layout/default A4 and lost the persisted CTB selection. + self.plot_dialog.copy_settings_from(&previous); + + // Preserve the parsed CTB when it matches the restored selection. + // If it is absent/stale, dialog_plot_style() can resolve it by name. + self.active_plot_style = previous_style.clone(); + self.plot_window = previous_window; self.print_all_options_prev = Some(previous); self.print_all_plot_style_prev = Some(previous_style); @@ -4342,7 +4454,15 @@ impl OpenCADStudio { /// layout's plot settings and the printers found on the system. pub(super) fn on_plot_dialog_open(&mut self) -> Task { use crate::io::paper_catalog::Orientation; + + let cur = self.tabs[self.active_tab].scene.current_layout.clone(); + let same_context = self + .plot_prev_context + .as_ref() + .is_some_and(|(tab, layout)| *tab == self.active_tab && layout == &cur); + let previous = self.plot_dialog.clone(); + let scales: Vec<(String, f64)> = self.tabs[self.active_tab] .scene .scale_list() @@ -4427,31 +4547,48 @@ impl OpenCADStudio { self.command_line .push_warning(crate::tf!("Could not list printers: {error}").as_ref()); } - self.plot_prev = Some(previous); - let cur = self.tabs[self.active_tab].scene.current_layout.clone(); + self.plot_prev = Some(previous.clone()); let layout_entry = format!("*{cur}*"); - // The auto-applied page setup must not reset the output device: a - // drawing rarely stores one, so honouring it would snap the dialog - // back to the system default on every open. The user's last choice - // (persisted across sessions with the rest of the dialog) survives - // unless the setup names an explicit device of its own. - let kept_printer = self.plot_dialog.printer.clone(); - let kept_to_file = self.plot_dialog.to_file; - if self.tabs[self.active_tab] - .scene - .plot_settings_for(&cur) - .is_some() - { - self.select_page_setup(&layout_entry); + + if same_context { + // #1654: reopening Plot for the same drawing/layout must keep the + // values the user just edited. The layout page setup is the initial + // seed, not something that should overwrite paper, scale, CTB, + // orientation, area and output choices on every Ctrl+P. + // + // `copy_settings_from` deliberately leaves refreshed runtime lists + // (printers, media, scales, page setups, etc.) untouched. + self.plot_dialog.copy_settings_from(&previous); + self.plot_dialog.selected_setup = previous.selected_setup.clone(); } else { - self.select_page_setup(crate::ui::window::plot::SETUP_PREV); - } - let setup_named_a_device = - self.plot_dialog.printer.is_some() || self.plot_dialog.to_file; - if !setup_named_a_device { - self.plot_dialog.printer = kept_printer; - self.plot_dialog.to_file = kept_to_file; + // First open for this tab/layout: seed the editor from its actual + // page setup. An explicit setup selection later still uses + // `select_page_setup` and therefore intentionally replaces fields. + // + // Keep the chosen output device when a drawing page setup does not + // name one of its own. + let kept_printer = self.plot_dialog.printer.clone(); + let kept_to_file = self.plot_dialog.to_file; + + if self.tabs[self.active_tab] + .scene + .plot_settings_for(&cur) + .is_some() + { + self.select_page_setup(&layout_entry); + } else { + self.select_page_setup(crate::ui::window::plot::SETUP_PREV); + } + + let setup_named_a_device = + self.plot_dialog.printer.is_some() || self.plot_dialog.to_file; + if !setup_named_a_device { + self.plot_dialog.printer = kept_printer; + self.plot_dialog.to_file = kept_to_file; + } } + + self.plot_prev_context = Some((self.active_tab, cur.clone())); // A persisted plot window (saved with the dialog settings) restores // across sessions; a freshly loaded one wins so the two stay in sync. if self.plot_window.is_none() { @@ -4481,29 +4618,57 @@ impl OpenCADStudio { } } self.active_modal = Some(crate::app::ModalKind::Plot); - self.request_printer_media() + // Loading printer capabilities is informational when Plot merely + // opens. It must not replace the sheet stored in the drawing. + self.request_printer_media(false) } /// Keep the dialog's view of the selected printer's media current: use the /// session cache when the printer was asked before, otherwise ask it in /// the background and clear the stale answer meanwhile. PDF output and /// the default printer list the catalogue instead. - fn request_printer_media(&mut self) -> Task { - let d = &mut self.plot_dialog; - let Some(printer) = (!d.to_file).then(|| d.printer.clone()).flatten() else { - d.printer_media = None; + fn request_printer_media(&mut self, reconcile: bool) -> Task { + let Some(printer) = (!self.plot_dialog.to_file) + .then(|| self.plot_dialog.printer.clone()) + .flatten() + else { + self.plot_dialog.printer_media = None; + self.plot_reconcile_printer_media = None; return Task::none(); }; - if let Some(caps) = crate::io::plot_device::cached_printer_capabilities(&printer) { - d.printer_media = Some(caps); + + // PAPERUPDATE is relevant after the user explicitly changes the + // device/page setup. Merely reopening Plot must leave the drawing's + // stored paper untouched. + self.plot_reconcile_printer_media = + reconcile.then(|| printer.clone()); + + if let Some(caps) = + crate::io::plot_device::cached_printer_capabilities(&printer) + { + self.plot_dialog.printer_media = Some(caps); + + if reconcile { + self.reconcile_sheet_with_printer(&printer); + } + return Task::none(); } - d.printer_media = None; + + self.plot_dialog.printer_media = None; let name = printer.clone(); + background_task( crate::t!("Printer capabilities"), move || crate::io::plot_device::printer_capabilities(&printer), - move |caps| Message::PlotDlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia(name, caps)), + move |caps| { + Message::PlotDlg( + crate::ui::window::plot::PlotDlgMsg::PrinterMedia( + name, + caps, + ), + ) + }, ) } @@ -4668,6 +4833,9 @@ impl OpenCADStudio { }; match msg { M::Close => { + // Keep the user's last Plot choices available for the next + // invocation even when the dialog was simply closed. + self.save_config(); self.close_active_modal(); Task::none() } @@ -4685,14 +4853,35 @@ impl OpenCADStudio { self.plot_dialog.printer = Some(s); } self.save_config(); - self.request_printer_media() + // This is an explicit device change, so PAPERUPDATE may + // reconcile an unsupported sheet when its capabilities arrive. + self.request_printer_media(true) } M::PrinterMedia(printer, caps) => { - let d = &mut self.plot_dialog; - if !d.to_file && d.printer.as_deref() == Some(printer.as_str()) { - d.printer_media = caps; - self.reconcile_sheet_with_printer(&printer); + let still_current = + !self.plot_dialog.to_file + && self.plot_dialog.printer.as_deref() + == Some(printer.as_str()); + + if still_current { + self.plot_dialog.printer_media = caps; + + let reconcile = self + .plot_reconcile_printer_media + .as_deref() + .is_some_and(|pending| { + pending.eq_ignore_ascii_case(&printer) + }); + + if reconcile { + // Keep the explicit-device context alive while this + // printer remains selected. Capability updates for the + // same user-selected device must continue to obey + // PAPERUPDATE. + self.reconcile_sheet_with_printer(&printer); + } } + Task::none() } M::CustomPaper(msg) => { @@ -4952,13 +5141,48 @@ impl OpenCADStudio { Task::done(Message::Command("PLOTWINDOW".into())) } M::SelectSetup(name) => { + // Print All Options edits the page setups represented by the + // layout rows. With "Save changes to layout" enabled, preserve + // the setup we are leaving before another one is loaded. + if self.print_all_options + && self.plot_dialog.save_to_layout + && self.plot_dialog.selected_setup != name + { + let previous = self.plot_dialog.selected_setup.clone(); + + if is_layout_entry(&previous) { + let layout = layout_entry_name(&previous).to_string(); + self.apply_dialog_to_named_layout(&layout); + } else if previous + == crate::ui::window::plot::SETUP_PREV + { + // `` is a transient snapshot, not an actual + // page setup. `plot_prev_context` records which layout + // that snapshot came from. + // + // If the user returns to that same layout, commit the + // restored snapshot to it before reloading the row. + if let Some((tab, layout)) = + self.plot_prev_context.clone() + { + let target = format!("*{layout}*"); + + if tab == self.active_tab && name == target { + self.apply_dialog_to_named_layout(&layout); + } + } + } + } + self.select_page_setup(&name); + if self.print_all_options { self.plot_dialog.paper_space = true; self.plot_dialog.area = "Layout".into(); } + // The setup may name a different printer. - self.request_printer_media() + self.request_printer_media(true) } M::SetCurrent => { self.apply_dialog_to_layout(); @@ -5385,6 +5609,38 @@ impl OpenCADStudio { self.apply_plot_page_settings(); } + /// Persist the current Plot editor into a specific layout. + /// + /// This is needed by Print All Options because selecting `*Layout1*`, + /// `*Layout2*`, etc. does not switch `scene.current_layout`. + fn apply_dialog_to_named_layout(&mut self, layout_name: &str) { + use codec::objects::{PlotPaperUnits, PlotSettings}; + + let i = self.active_tab; + + let base = self.tabs[i] + .scene + .plot_settings_for(layout_name) + .unwrap_or_else(|| { + let mut settings = PlotSettings::new(""); + settings.paper_units = PlotPaperUnits::Millimeters; + settings + }); + + let ps = self.plot_settings_from_dialog(base); + + if self.tabs[i] + .scene + .set_layout_plot_settings(layout_name, &ps) + { + self.tabs[i].dirty = true; + + if self.tabs[i].scene.current_layout == layout_name { + self.tabs[i].scene.bump_geometry_no_blocks(); + } + } + } + /// Open a preview PDF, export a PDF, or send the job to the chosen printer. fn on_plot_dlg_commit(&mut self, preview: bool) -> Task { let d = self.plot_dialog.clone(); @@ -5534,10 +5790,21 @@ impl OpenCADStudio { if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles { return None; } - self.active_plot_style + + // Fast path: the selected table is already active. + if let Some(table) = self + .active_plot_style .as_ref() .filter(|table| table.name.eq_ignore_ascii_case(&d.style_name)) - .cloned() + { + return Some(table.clone()); + } + + // The dialog selection is persisted separately from the parsed CTB + // object. After reopening a drawing the name may be correct while the + // in-memory table is still the previous/default one. Plotting must not + // silently ignore the selected CTB in that state. + crate::io::plot_style::PlotStyleTable::load_named(&d.style_name).ok() } pub(in crate::app) fn window_plot_job(&self) -> Option { @@ -6677,6 +6944,322 @@ mod plot_device_persistence_tests { assert!(!app.plot_dialog.to_file); } + /// Reopening Plot in the same tab/layout keeps the user's most recent + /// paper, scale and CTB choices instead of reloading the layout page setup + /// over them. Regression test for #1654. + #[test] + fn reopening_same_plot_context_keeps_recent_settings() { + let mut app = OpenCADStudio::new_for_test(); + + // First open seeds from the drawing/layout as usual. + let _ = app.on_plot_dialog_open(); + + // User edits the high-frequency Plot settings. + app.plot_dialog.paper = "ISO_A3_(297.00_x_420.00_MM)".into(); + app.plot_dialog.paper_width_mm = 297.0; + app.plot_dialog.paper_height_mm = 420.0; + app.plot_dialog.scale = "1:100".into(); + app.plot_dialog.fit_to_paper = false; + app.plot_dialog.style_name = "office.ctb".into(); + app.plot_dialog.apply_plot_styles = true; + + // Reopening Ctrl+P in the same layout must not reapply the layout's + // old page setup and erase those choices. + let _ = app.on_plot_dialog_open(); + + assert_eq!( + app.plot_dialog.paper, + "ISO_A3_(297.00_x_420.00_MM)" + ); + assert_eq!(app.plot_dialog.scale, "1:100"); + assert!(!app.plot_dialog.fit_to_paper); + assert_eq!(app.plot_dialog.style_name, "office.ctb"); + assert!(app.plot_dialog.apply_plot_styles); + } + + /// Opening a different drawing in a reused tab must invalidate the Plot + /// session cache. The first Plot dialog must read paper and CTB from the + /// newly opened document instead of showing the startup drawing's values. + #[test] + fn newly_opened_document_forces_first_plot_dialog_to_read_layout() { + let mut app = OpenCADStudio::new_for_test(); + let i = app.active_tab; + + app.tabs[i].scene.current_layout = "Layout1".into(); + + let mut ps = app.tabs[i] + .scene + .plot_settings_for("Layout1") + .expect("Layout1 must exist"); + + ps.paper_size = "ISO_A2_(420.00_x_594.00_MM)".into(); + ps.paper_width = 420.0; + ps.paper_height = 594.0; + ps.current_style_sheet = + crate::io::plot_style::MONOCHROME_PLOT_STYLE.into(); + ps.flags.plot_plot_styles = true; + + assert!( + app.tabs[i] + .scene + .set_layout_plot_settings("Layout1", &ps) + ); + + // Reproduce stale state inherited from the startup/previous drawing. + app.plot_dialog.paper = + "ISO_A4_(210.00_x_297.00_MM)".into(); + app.plot_dialog.style_name.clear(); + app.plot_dialog.apply_plot_styles = false; + + app.plot_prev = Some(app.plot_dialog.clone()); + app.plot_prev_context = + Some((i, "Layout1".into())); + + // Installing another drawing must make that state unusable. + app.reset_plot_session_for_opened_document(); + + assert!(app.plot_prev.is_none()); + assert!(app.plot_prev_context.is_none()); + + let _ = app.on_plot_dialog_open(); + + assert_eq!( + app.plot_dialog.paper, + "ISO_A2_(420.00_x_594.00_MM)", + "the first Plot opening must use the new drawing's paper" + ); + + assert!( + app.plot_dialog + .style_name + .eq_ignore_ascii_case( + crate::io::plot_style::MONOCHROME_PLOT_STYLE + ), + "the first Plot opening must use the new drawing's CTB" + ); + + assert!( + app.plot_dialog.apply_plot_styles, + "the stored CTB must already be enabled on the first Plot opening" + ); + } + + /// Print All Options is a shared batch-settings editor. On the first + /// opening after restarting OCS it must keep the persisted user choices + /// instead of replacing them with the active layout's page setup. + #[test] + fn print_all_options_keeps_persisted_plot_preferences_on_first_open() { + let mut app = OpenCADStudio::new_for_test(); + + // Simulate preferences restored from the previous application session. + app.plot_dialog.paper = + "ISO_A2_(420.00_x_594.00_MM)".into(); + app.plot_dialog.paper_width_mm = 420.0; + app.plot_dialog.paper_height_mm = 594.0; + app.plot_dialog.scale = "1:1000".into(); + app.plot_dialog.style_name = + crate::io::plot_style::MONOCHROME_PLOT_STYLE.into(); + app.plot_dialog.apply_plot_styles = true; + + // Fresh process: no explicit Print All override exists yet. + app.print_all_settings_override = false; + + let _ = app.on_print_all_options(); + + assert_eq!( + app.plot_dialog.paper, + "ISO_A2_(420.00_x_594.00_MM)", + "first Print All Options opening must keep the persisted paper" + ); + + assert_eq!( + app.plot_dialog.scale, + "1:1000", + "first Print All Options opening must keep the persisted scale" + ); + + assert!( + app.plot_dialog + .style_name + .eq_ignore_ascii_case( + crate::io::plot_style::MONOCHROME_PLOT_STYLE + ), + "first Print All Options opening must keep the persisted CTB" + ); + + assert!( + app.plot_dialog.apply_plot_styles, + "the persisted CTB must remain enabled" + ); + + assert!( + app.print_all_options, + "the dialog must remain in Print All Options mode" + ); + } + + /// `` belongs to the layout that produced the snapshot. + /// Returning to that layout with "Save changes to layout" enabled must + /// preserve the restored paper/CTB instead of reloading the old embedded + /// page setup over it. + #[test] + fn previous_snapshot_is_saved_back_to_its_origin_layout() { + let mut app = OpenCADStudio::new_for_test(); + let i = app.active_tab; + + app.tabs[i].scene.current_layout = "Layout1".into(); + + // Layout1 still contains an old page setup. + let mut stored = app.tabs[i] + .scene + .plot_settings_for("Layout1") + .expect("Layout1 must exist"); + + stored.paper_size = "ISO_A4_(210.00_x_297.00_MM)".into(); + stored.paper_width = 210.0; + stored.paper_height = 297.0; + stored.current_style_sheet.clear(); + stored.flags.plot_plot_styles = false; + + assert!( + app.tabs[i] + .scene + .set_layout_plot_settings("Layout1", &stored) + ); + + // `` is the desired state that originally belonged to + // Layout1. + app.print_all_options = true; + app.plot_prev_context = Some((i, "Layout1".into())); + app.plot_dialog.selected_setup = + crate::ui::window::plot::SETUP_PREV.into(); + + app.plot_dialog.paper = + "ISO_A2_(420.00_x_594.00_MM)".into(); + app.plot_dialog.paper_width_mm = 420.0; + app.plot_dialog.paper_height_mm = 594.0; + app.plot_dialog.style_name = + crate::io::plot_style::MONOCHROME_PLOT_STYLE.into(); + app.plot_dialog.apply_plot_styles = true; + app.plot_dialog.save_to_layout = true; + + let _ = app.on_plot_dlg( + crate::ui::window::plot::PlotDlgMsg::SelectSetup( + "*Layout1*".into(), + ), + ); + + let saved = app.tabs[i] + .scene + .plot_settings_for("Layout1") + .expect("Layout1 must still exist"); + + assert_eq!( + saved.paper_size, + "ISO_A2_(420.00_x_594.00_MM)" + ); + + assert!( + saved.current_style_sheet.eq_ignore_ascii_case( + crate::io::plot_style::MONOCHROME_PLOT_STYLE + ) + ); + + assert!(saved.flags.plot_plot_styles); + + assert_eq!( + app.plot_dialog.paper, + "ISO_A2_(420.00_x_594.00_MM)" + ); + + assert!( + app.plot_dialog + .style_name + .eq_ignore_ascii_case( + crate::io::plot_style::MONOCHROME_PLOT_STYLE + ) + ); + } + + /// A CTB selected in the dialog must be usable even when the parsed + /// active table was not restored with it. This is the state seen after + /// reopening a drawing: the dropdown can name the correct CTB while the + /// first preview otherwise plots using raw layer colours. + #[test] + fn selected_ctb_is_resolved_when_active_table_is_stale() { + let mut app = OpenCADStudio::new_for_test(); + + app.plot_dialog.style_name = + crate::io::plot_style::MONOCHROME_PLOT_STYLE.into(); + app.plot_dialog.apply_plot_styles = true; + app.plot_dialog.style_missing = false; + + // Reproduce a fresh-session mismatch: the visible selection exists, + // but no matching parsed table is active yet. + app.active_plot_style = None; + + let table = app + .dialog_plot_style(&app.plot_dialog) + .expect("selected CTB must be resolved by name"); + + assert!( + table + .name + .eq_ignore_ascii_case( + crate::io::plot_style::MONOCHROME_PLOT_STYLE + ), + "the table used for plotting must match the visible CTB selection" + ); + } + + /// Printer capability discovery on merely opening Plot must never mutate + /// the paper stored in the drawing. PAPERUPDATE is applied only after an + /// explicit device/page-setup choice. + #[test] + fn opening_plot_does_not_replace_sheet_when_printer_media_arrives() { + use crate::io::paper_catalog::{resolve, Margins}; + use crate::io::plot_device::{ + PrinterCapabilities, + PrinterMedia, + }; + + let a2 = + resolve("ISO_A2_(420.00_x_594.00_MM)").expect("A2"); + let a4 = + resolve("ISO_A4_(210.00_x_297.00_MM)").expect("A4"); + + let caps = std::sync::Arc::new(PrinterCapabilities { + media: vec![PrinterMedia { + paper: a4.clone(), + margins: Margins::uniform(3.0), + borderless: false, + }], + default_paper: Some(a4), + }); + + let mut app = OpenCADStudio::new_for_test(); + app.plot_dialog.to_file = false; + app.plot_dialog.printer = + Some("OCS Test Printer".into()); + app.plot_dialog.paper = a2.canonical.to_string(); + app.plot_dialog.paper_update = 1; + + // No explicit printer change is pending: this models the asynchronous + // capability reply caused only by opening the Plot dialog. + app.plot_reconcile_printer_media = None; + + let _ = app.on_plot_dlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia( + "OCS Test Printer".into(), + Some(caps), + )); + + assert_eq!( + app.plot_dialog.paper, + a2.canonical, + "opening Plot must preserve the drawing's stored sheet" + ); + } + /// A layout whose plot settings name an explicit device wins over the /// saved preference: selecting its setup is an explicit device choice. #[test] From ec9f91ec7536ab3442906dbf4f68024c8be3be5d Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 8 Oct 2026 18:47:54 -0300 Subject: [PATCH 3/5] fix test failed --- src/app/update/file.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index c28ba7358..c80cc84b6 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -4560,6 +4560,30 @@ impl OpenCADStudio { // (printers, media, scales, page setups, etc.) untouched. self.plot_dialog.copy_settings_from(&previous); self.plot_dialog.selected_setup = previous.selected_setup.clone(); + + // A custom ratio such as 1:3 may not exist in the drawing's named + // scale list. select_page_setup() normally adds it while loading a + // PlotSettings, but same-context reopening intentionally skips that + // reload. Keep the restored custom scale available to the picker so + // the validation below does not collapse it to 1:1. + if !self.plot_dialog.fit_to_paper + && !self + .plot_dialog + .scales + .iter() + .any(|(name, _)| name == &self.plot_dialog.scale) + { + let factor = plot_dialog_scale_factor(&self.plot_dialog); + + if factor.is_finite() && factor > 0.0 { + let name = self.plot_dialog.scale.clone(); + self.plot_dialog.scales.push((name, factor)); + self.plot_dialog.scales.sort_by(|a, b| { + a.1.partial_cmp(&b.1) + .unwrap_or(std::cmp::Ordering::Equal) + }); + } + } } else { // First open for this tab/layout: seed the editor from its actual // page setup. An explicit setup selection later still uses From aa0b87108cc8a69f664b3bfcd2ec035ffcb31d22 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 9 Oct 2026 08:25:08 +0300 Subject: [PATCH 4/5] =?UTF-8?q?feat(model):=20kernel=20a82f17d=20=E2=80=94?= =?UTF-8?q?=20MASSPROP=20from=20B-rep,=20curved=20OFFSET/FILLET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pins: codec 8189f83, kernel a82f17d, graph d3558bb (all manifests and the python plugin lock). - MASSPROP reports volume, centroid, inertia, products, radii of gyration and principal moments of 3D solids and bodies from their B-rep (exact where analytic, else from a fine mesh). - OFFSET of ellipses and splines through the kernel's curve offset. - FILLET between circles, ellipses and splines through the kernel's curve-curve fillet, trimming arcs and open splines to the tangents. The kernel bump brings the DWG ACIS audit fixes: slices, booleans and press/pull on cones, slanted cylinder sections, survey coordinates, spline walls and touching parts. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 8 +- Cargo.toml | 8 +- crates/ocs_plugin_api/Cargo.toml | 4 +- crates/ocs_web_worker/Cargo.toml | 2 +- locales/ar-SA/opencadstudio.ftl | 6 ++ locales/bg-BG/opencadstudio.ftl | 6 ++ locales/cs-CZ/opencadstudio.ftl | 6 ++ locales/de-DE/opencadstudio.ftl | 6 ++ locales/el-GR/opencadstudio.ftl | 6 ++ locales/en-US/opencadstudio.ftl | 6 ++ locales/es-ES/opencadstudio.ftl | 6 ++ locales/fi-FI/opencadstudio.ftl | 6 ++ locales/fr-FR/opencadstudio.ftl | 6 ++ locales/hi-IN/opencadstudio.ftl | 6 ++ locales/hu-HU/opencadstudio.ftl | 6 ++ locales/it-IT/opencadstudio.ftl | 6 ++ locales/ja-JP/opencadstudio.ftl | 6 ++ locales/ko-KR/opencadstudio.ftl | 6 ++ locales/lo-LA/opencadstudio.ftl | 6 ++ locales/nl-NL/opencadstudio.ftl | 6 ++ locales/pl-PL/opencadstudio.ftl | 6 ++ locales/pt-BR/opencadstudio.ftl | 6 ++ locales/ru-RU/opencadstudio.ftl | 6 ++ locales/tr-TR/opencadstudio.ftl | 6 ++ locales/zh-CN/opencadstudio.ftl | 6 ++ locales/zh-TW/opencadstudio.ftl | 6 ++ plugins/opencad-python/Cargo.lock | 2 +- src/app/commands/inquiry.rs | 57 +++++++++++ src/locale_catalog.rs | 6 ++ src/modules/draw/modify/fillet.rs | 136 +++++++++++++++++++++++++ src/modules/draw/modify/offset.rs | 158 ++++++++++++------------------ 31 files changed, 404 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 661be0bc6..875643b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4205,7 +4205,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4228,7 +4228,7 @@ dependencies = [ [[package]] name = "opencadgraph" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=7f6aae7#7f6aae7aff6b7de87cc1dcdb38a06c3e64ae1922" +source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=d3558bb#d3558bb1ccd63d3819fa0358267bac1ba21ab593" dependencies = [ "opencadkernel", "serde_json", @@ -4237,7 +4237,7 @@ dependencies = [ [[package]] name = "opencadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=99b6b48#99b6b4890704d300596ba502b5a1c8ec1eefa954" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=a82f17d#a82f17dd34af0787023c93ed565220dd1467ff46" dependencies = [ "cavalier_contours", "opencadcodec", @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "opencadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=99b6b48#99b6b4890704d300596ba502b5a1c8ec1eefa954" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=a82f17d#a82f17dd34af0787023c93ed565220dd1467ff46" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index c9480bb41..f62e0ef45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,10 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] } -kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48", features = ["acis", "offset"] } -kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48" } -graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "7f6aae7" } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] } +kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "a82f17d", features = ["acis", "offset"] } +kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "a82f17d" } +graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "d3558bb" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 2ff186185..6667920d1 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true } # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", optional = true, features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -39,7 +39,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # opencadcodec is scanned at build time to generate the embedded type registry. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 00e7bbd79..d643bd257 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 3fa33ea45..8aedf067d 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2521,6 +2521,12 @@ inquiry = .id-specify-point = ID حدد نقطة: .list-no-entities-selected-select-entities-firs = LIST: لم تُحدد كيانات. حدد الكيانات أولًا. .massprop-no-entities-selected-select-entities = MASSPROP: لم تُحدد كيانات. حدد الكيانات أولًا. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ لا يحتوي على جسم مغلق لقياسه. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ الحجم=__ocs_fmt_1__ مركز الثقل=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = عزوم القصور الذاتي X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = جداءات القصور الذاتي XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = أنصاف أقطار الدوران X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = العزوم الرئيسية حول مركز الثقل I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE حدد الكائن المراد قياسه: .measure-specify-segment-length = MEASURE حدد طول المقطع: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM أدخل خيارًا [مسافة/نصف قطر/زاوية/مساحة]: diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index d186cba66..69c6c9f27 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Посочи точка: .list-no-entities-selected-select-entities-firs = LIST: не са избрани обекти. Първо избери обекти. .massprop-no-entities-selected-select-entities = MASSPROP: не са избрани обекти. Първо избери обекти. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ няма затворено тяло за измерване. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Обем=__ocs_fmt_1__ Център на тежестта=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Инерционни моменти X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Центробежни моменти XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Радиуси на инерция X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Главни моменти спрямо центъра на тежестта I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Избери обект за измерване: .measure-specify-segment-length = MEASURE Посочи дължина на сегмент: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Въведи опция [Разстояние/Радиус/Ъгъл/Площ]: diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 07669d6b8..94e44eb2b 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Určete bod: .list-no-entities-selected-select-entities-firs = LIST: nebyly vybrány žádné entity. Nejprve vyberte entity. .massprop-no-entities-selected-select-entities = MASSPROP: nebyly vybrány žádné entity. Nejprve vyberte entity. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nemá žádné uzavřené těleso k měření. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objem=__ocs_fmt_1__ Těžiště=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Momenty setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Deviační momenty XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Poloměry setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Hlavní momenty k těžišti I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Vyberte objekt k měření: .measure-specify-segment-length = MEASURE Zadejte délku segmentu: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter možnost [Vzdálenost/Poloměr/Úhel/Area]: diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index ea5b01467..44e38097d 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Anzugeben ist: .list-no-entities-selected-select-entities-firs = LIST: keine Objekte ausgewählt. .massprop-no-entities-selected-select-entities = MASSPROP: Keine Objekte ausgewählt. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ hat keinen geschlossenen Körper zum Messen. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Schwerpunkt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Trägheitsmomente X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Deviationsmomente XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Trägheitsradien X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Hauptträgheitsmomente um den Schwerpunkt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Objekt auswählen, um zu messen: .measure-specify-segment-length = MEASURE Segmentlänge angeben: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Geben Sie eine Option ein [Distance/Radius/Angle/ARea]: diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 743e2bb7c..1eb4b91ab 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Ορίστε σημείο: .list-no-entities-selected-select-entities-firs = LIST: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες. .massprop-no-entities-selected-select-entities = MASSPROP: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ δεν έχει κλειστό σώμα για μέτρηση. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Όγκος=__ocs_fmt_1__ Κέντρο βάρους=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Ροπές αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Γινόμενα αδράνειας XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Ακτίνες αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Κύριες ροπές ως προς το κέντρο βάρους I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Επιλέξτε αντικείμενο προς μέτρηση: .measure-specify-segment-length = MEASURE Ορίστε μήκος τμήματος: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Δώστε επιλογή [Απόσταση/Ακτίνα/Γωνία/Εμβαδόν]: diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 7c22d658b..377caf653 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Specify point: .list-no-entities-selected-select-entities-firs = LIST: no entities selected. Select entities first. .massprop-no-entities-selected-select-entities = MASSPROP: no entities selected. Select entities first. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ has no closed body to measure. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroid=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Moments of inertia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Products of inertia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Radii of gyration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Principal moments about centroid I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Select object to measure: .measure-specify-segment-length = MEASURE Specify segment length: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]: diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 66d5e31d7..c3b82398b 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Especifique un punto: .list-no-entities-selected-select-entities-firs = LIST: no hay entidades seleccionadas. Seleccione primero las entidades. .massprop-no-entities-selected-select-entities = MASSPROP: no hay entidades seleccionadas. Seleccione primero las entidades. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ no tiene un cuerpo cerrado que medir. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Momentos de inercia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Productos de inercia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Radios de giro X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principales respecto al centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Seleccione el objeto que desea medir: .measure-specify-segment-length = MEASURE Especifique la longitud del segmento: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Introduzca una opción [Distancia/Radio/Ángulo/Área]: diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index fb4ce630b..6503a7115 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Määritä piste: .list-no-entities-selected-select-entities-firs = LIST: entiteettejä ei ole valittu. Valitse ensin entiteetit. .massprop-no-entities-selected-select-entities = MASSPROP: entiteettejä ei ole valittu. Valitse ensin entiteetit. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ ei sisällä suljettua kappaletta mitattavaksi. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Tilavuus=__ocs_fmt_1__ Painopiste=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Hitausmomentit X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Tulohitausmomentit XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Hitaussäteet X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Päähitausmomentit painopisteen suhteen I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Valitse mitattava kohde: .measure-specify-segment-length = MEASURE Määritä segmentin pituus: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter vaihtoehto [etäisyys/säde/kulma/pinta-ala]: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 85bf5e706..df694d068 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Spécifier le point: .list-no-entities-selected-select-entities-firs = LIST : aucune entité sélectionnée. Sélectionnez d'abord les entités. .massprop-no-entities-selected-select-entities = MASSPROP : aucune entité sélectionnée. Sélectionnez d'abord les entités. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ n'a aucun corps fermé à mesurer. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centre de gravité=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Moments d'inertie X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Produits d'inertie XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Rayons de giration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Moments principaux au centre de gravité I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Sélectionnez l'objet à mesurer : .measure-specify-segment-length = MEASURE Spécifier la longueur du segment: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Saisissez une option [Distance/Radius/Angle/ARea] : diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 46e2c5f75..fc162ca59 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2497,6 +2497,12 @@ inquiry = .id-specify-point = ID निर्दिष्ट बिंदु: .list-no-entities-selected-select-entities-firs = LIST: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें। .massprop-no-entities-selected-select-entities = MASSPROP: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें। + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ में मापने के लिए कोई बंद पिंड नहीं है। + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ आयतन=__ocs_fmt_1__ केन्द्रक=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = जड़त्व आघूर्ण X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = जड़त्व गुणनफल XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = घूर्णन त्रिज्याएँ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = केन्द्रक के सापेक्ष मुख्य आघूर्ण I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE ऑब्जेक्ट को मापने के लिए चुनें: .measure-specify-segment-length = MEASURE सेगमेंट की लंबाई निर्दिष्ट करें: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM विकल्प दर्ज करें [दूरी / त्रिज्या / कोण / क्षेत्रफल]: diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 59b432544..ee0e196ab 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Pont megadása: .list-no-entities-selected-select-entities-firs = LIST: nincsenek kiválasztva entitások. Először válassza ki az entitásokat. .massprop-no-entities-selected-select-entities = MASSPROP: nincsenek kiválasztva entitások. Először válassza ki az entitásokat. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nem tartalmaz mérhető zárt testet. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Térfogat=__ocs_fmt_1__ Súlypont=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Tehetetlenségi nyomatékok X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Deviációs nyomatékok XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Tehetetlenségi sugarak X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Fő tehetetlenségi nyomatékok a súlypontra I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Válassza ki a mérendő objektumot: .measure-specify-segment-length = MEASURE Adja meg a szegmens hosszát: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter egy opció [Távolság / Sugár / Szög / Terület]: diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index d2bfc1ae1..f4bdd595f 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Specificare il punto: .list-no-entities-selected-select-entities-firs = LIST: nessuna entità selezionata. Seleziona prima le entità. .massprop-no-entities-selected-select-entities = MASSPROP: nessuna entità selezionata. Seleziona prima le entità. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ non ha un corpo chiuso da misurare. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Baricentro=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Momenti d'inerzia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Prodotti d'inerzia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Raggi di girazione X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Momenti principali rispetto al baricentro I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Selezionare l'oggetto da misurare: .measure-specify-segment-length = MEASURE Specificare la lunghezza del segmento: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter un'opzione [Distanza/Raggio/Angolo/ARea]: diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 72c9b13e0..356c801f5 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2499,6 +2499,12 @@ inquiry = .id-specify-point = ID 点を指定: .list-no-entities-selected-select-entities-firs = LIST: 図形が選択されていません。先に選択してください。 .massprop-no-entities-selected-select-entities = MASSPROP: 図形が選択されていません。先に選択してください。 + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ には計測できる閉じたボディがありません。 + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体積=__ocs_fmt_1__ 重心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = 慣性モーメント X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = 慣性乗積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = 回転半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = 重心まわりの主慣性モーメント I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE 計測するオブジェクトを選択: .measure-specify-segment-length = MEASURE 区間長を指定: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM オプションを入力 [距離/半径/角度/面積]: diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 403135aa8..07e0d9bd0 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID 지점 지정: .list-no-entities-selected-select-entities-firs = LIST: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요. .massprop-no-entities-selected-select-entities = MASSPROP: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 에 측정할 닫힌 바디가 없습니다. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ 부피=__ocs_fmt_1__ 도심=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = 관성 모멘트 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = 관성 곱 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = 회전 반경 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = 도심에 대한 주 모멘트 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE 측정할 물체 선택: .measure-specify-segment-length = MEASURE 세그먼트 길이 지정: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter 옵션 [거리/반경/각도/지역]: diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 8b75a109a..a5d212468 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID ກຳນົດຈຸດ: .list-no-entities-selected-select-entities-firs = LIST: ບໍ່ມີອົງປະກອບຖືກເລືອກ. ກະລຸນາເລືອກອົງປະກອບກ່ອນ. .massprop-no-entities-selected-select-entities = MASSPROP: ບໍ່ມີອົງປະກອບຖືກເລືອກ. ກະລຸນາເລືອກອົງປະກອບກ່ອນ. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ ບໍ່ມີຕົວປິດໃຫ້ວັດແທກ. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ ປະລິມາດ=__ocs_fmt_1__ ຈຸດສູນກາງມວນ=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = ໂມເມັນຄວາມເສື່ອຍ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = ຜົນຄູນຄວາມເສື່ອຍ XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = ລັດສະໝີການໝູນ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = ໂມເມັນຫຼັກອ້ອມຈຸດສູນກາງມວນ I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE ເລືອກວັດຖຸທີ່ຈະວັດແທກ: .measure-specify-segment-length = MEASURE ກຳນົດຄວາມຍາວຂອງສ່ວນແບ່ງ: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM ປ້ອນຕົວເລືອກ [Distance/Radius/Angle/ARea]: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 112069a59..28e91b02a 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Specificeer punt: .list-no-entities-selected-select-entities-firs = LIST: geen objecten geselecteerd. Selecteer eerst objecten. .massprop-no-entities-selected-select-entities = MASSPROP: geen objecten geselecteerd. Selecteer eerst objecten. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ heeft geen gesloten lichaam om te meten. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Zwaartepunt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Traagheidsmomenten X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Deviatiemomenten XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Gyratiestralen X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Hoofdtraagheidsmomenten om het zwaartepunt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Selecteer object om mee te meten: .measure-specify-segment-length = MEASURE segmentlengte specificeren: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Voer een optie in [Afstand / Straal / Hoek / Oppervlakte]: diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 16cec2da3..7044544a6 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID Określ punkt: .list-no-entities-selected-select-entities-firs = LIST: nie wybrano żadnych elementów. Najpierw wybierz elementy. .massprop-no-entities-selected-select-entities = MASSPROP: nie wybrano żadnych elementów. Najpierw wybierz elementy. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nie ma zamkniętej bryły do zmierzenia. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objętość=__ocs_fmt_1__ Środek ciężkości=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Momenty bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Momenty dewiacyjne XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Promienie bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Główne momenty względem środka ciężkości I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Wybierz obiekt do pomiaru: .measure-specify-segment-length = MEASURE Określ długość segmentu: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter opcja [Odległość/Promień/Kąt/Powierzchnia]: diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e32d1d614..7a8e2ad56 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2509,6 +2509,12 @@ inquiry = .id-specify-point = ID Especifique o ponto: .list-no-entities-selected-select-entities-firs = LIST: nenhuma entidade selecionada. Selecione primeiro as entidades. .massprop-no-entities-selected-select-entities = MASSPROP: nenhuma entidade selecionada. Selecione primeiro as entidades. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ não tem um corpo fechado para medir. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Momentos de inércia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Produtos de inércia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Raios de giração X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principais em relação ao centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Selecione o objeto a medir: .measure-specify-segment-length = MEASURE Especifique o comprimento do segmento: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Insira uma opção [Distância/Raio/Ângulo/ÁRea]: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 5734d26e5..b078726bd 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2512,6 +2512,12 @@ inquiry = .id-specify-point = ID Укажите точку: .list-no-entities-selected-select-entities-firs = LIST: не выбрано ни одного объекта. .massprop-no-entities-selected-select-entities = MASSPROP: не выбрано ни одного объекта. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ не имеет замкнутого тела для измерения. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Объём=__ocs_fmt_1__ Центр масс=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Моменты инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Центробежные моменты XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Радиусы инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Главные моменты относительно центра масс I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Выберите объект для измерения: .measure-specify-segment-length = MEASURE Укажите длину сегмента: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Введите опцию [Расстояние/Радиус/Англия/Арея]: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 87e53c2af..54a1c9daf 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2490,6 +2490,12 @@ inquiry = .id-specify-point = ID Noktayı belirtin: .list-no-entities-selected-select-entities-firs = LIST: Nesne seçilmedi. Önce nesneleri seçin. .massprop-no-entities-selected-select-entities = MASSPROP: Nesne seçilmedi. Önce nesneleri seçin. + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ için ölçülecek kapalı gövde yok. + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ Hacim=__ocs_fmt_1__ Ağırlık merkezi=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = Eylemsizlik momentleri X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = Eylemsizlik çarpımları XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = Jirasyon yarıçapları X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = Ağırlık merkezine göre asal momentler I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE Ölçülecek nesneyi seçin: .measure-specify-segment-length = MEASURE Parça uzunluğunu belirtin: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Bir seçenek girin [Mesafe/Yarıçap/Açı/Alan]: diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index ac1e10a16..5eb0d8315 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -2496,6 +2496,12 @@ inquiry = .id-specify-point = ID 指定点: .list-no-entities-selected-select-entities-firs = LIST:没有选择实体。首先选择实体。 .massprop-no-entities-selected-select-entities = MASSPROP:没有选择实体。首先选择实体。 + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 没有可测量的封闭实体。 + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体积=__ocs_fmt_1__ 质心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = 惯性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = 惯性积 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = 回转半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = 关于质心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE 选择要定距等分的对象: .measure-specify-segment-length = MEASURE 指定线段长度: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 输入选项 [距离/半径/角度/面积]: diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 122cf89e5..40e3fb081 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2508,6 +2508,12 @@ inquiry = .id-specify-point = ID 指定點: .list-no-entities-selected-select-entities-firs = LIST:沒有選擇實體。首先選擇實體。 .massprop-no-entities-selected-select-entities = MASSPROP:沒有選擇實體。首先選擇實體。 + .massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 沒有可量測的封閉實體。 + .volume-4-centroid-4-4-4 = __ocs_fmt_0__ 體積=__ocs_fmt_1__ 質心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__) + .moments-of-inertia-x-4-y-4-z-4 = 慣性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .products-of-inertia-xy-4-yz-4-zx-4 = 慣性積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__ + .radii-of-gyration-x-4-y-4-z-4 = 迴轉半徑 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__ + .principal-moments-about-centroid-i-4-j-4-k-4 = 關於質心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__ .measure-select-object-to-measure = MEASURE 選擇要定距等分的物件: .measure-specify-segment-length = MEASURE 指定線段長度: .measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 輸入選項 [距離/半徑/角度/面積]: diff --git a/plugins/opencad-python/Cargo.lock b/plugins/opencad-python/Cargo.lock index 308d68303..e94e40fea 100644 --- a/plugins/opencad-python/Cargo.lock +++ b/plugins/opencad-python/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9" dependencies = [ "anyhow", "bitflags", diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index dda3f01b1..d0a31134b 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -1114,6 +1114,63 @@ 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!( "{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})", diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 19b4ed554..e41c57042 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1933,6 +1933,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "M density" => Some(("properties", "m-density")), "M vertex count" => Some(("properties", "m-vertex-count")), "MASSPROP: no entities selected. Select entities first." => Some(("inquiry", "massprop-no-entities-selected-select-entities")), + "MASSPROP: {} has no closed body to measure." => Some(("inquiry", "massprop-has-no-closed-body-to-measure")), + "{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})" => Some(("inquiry", "volume-4-centroid-4-4-4")), + "Moments of inertia X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "moments-of-inertia-x-4-y-4-z-4")), + "Products of inertia XY={:.4} YZ={:.4} ZX={:.4}" => Some(("inquiry", "products-of-inertia-xy-4-yz-4-zx-4")), + "Radii of gyration X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "radii-of-gyration-x-4-y-4-z-4")), + "Principal moments about centroid I={:.4} J={:.4} K={:.4}" => Some(("inquiry", "principal-moments-about-centroid-i-4-j-4-k-4")), "MATCHPROP Select destination objects (Enter to finish):" => Some(("properties", "matchprop-select-destination-objects-enter-to")), "MATCHPROP Select source object:" => Some(("properties", "matchprop-select-source-object")), "MEASURE Select object to measure:" => Some(("inquiry", "measure-select-object-to-measure")), diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index c36606f7e..f3a6608d4 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -769,6 +769,9 @@ enum FilletEntity { handle: Handle, seg_idx: usize, }, + /// A circle, ellipse or spline: filleted through the kernel's general + /// construction, and trimmed only when it has ends to trim. + Curve(EntityType), } impl FilletEntity { @@ -776,6 +779,9 @@ impl FilletEntity { match e { EntityType::Line(l) => Some(Self::Line(l.clone())), EntityType::Arc(a) => Some(Self::Arc(a.clone())), + EntityType::Circle(_) | EntityType::Ellipse(_) | EntityType::Spline(_) => { + crate::entities::curve::entity_curve_xy(e).map(|_| Self::Curve(e.clone())) + } _ => None, } } @@ -794,6 +800,7 @@ impl FilletEntity { Self::Line(l) => EntityType::Line(l.clone()), Self::Arc(a) => EntityType::Arc(a.clone()), Self::LwPoly { poly, .. } => EntityType::LwPolyline(poly.clone()), + Self::Curve(entity) => entity.clone(), } } @@ -802,6 +809,20 @@ impl FilletEntity { Self::Line(l) => l.start.z, Self::Arc(a) => a.center.z, Self::LwPoly { poly, .. } => poly.elevation, + Self::Curve(EntityType::Circle(c)) => c.center.z, + Self::Curve(EntityType::Ellipse(e)) => e.center.z, + Self::Curve(EntityType::Spline(s)) => s.control_points.first().map_or(0.0, |p| p.z), + Self::Curve(_) => 0.0, + } + } + + /// The shape as a kernel curve in plan. + fn kernel_curve(&self) -> Option { + match self { + Self::Line(l) => Some(line_curve(l)), + Self::Arc(a) => Some(arc_curve(a)), + Self::LwPoly { poly, seg_idx, .. } => Some(line_curve(&lwpoly_seg_as_line(poly, *seg_idx))), + Self::Curve(entity) => crate::entities::curve::entity_curve_xy(entity), } } } @@ -825,6 +846,9 @@ fn compute_fillet_entities( if picks_arc_segment(e1) || picks_arc_segment(e2) { return None; } + if matches!(e1, FilletEntity::Curve(_)) || matches!(e2, FilletEntity::Curve(_)) { + return fillet_curves(e1, click1, e2, click2, radius, z); + } match (e1, e2) { // ── Line × Line ─────────────────────────────────────────────────── @@ -1011,6 +1035,8 @@ fn compute_fillet_entities( None } } + // Answered by `fillet_curves` above. + (FilletEntity::Curve(_), _) | (_, FilletEntity::Curve(_)) => None, } } @@ -1268,6 +1294,116 @@ fn fillet_arc_arc( None } +/// FILLET where one side is a circle, ellipse or spline. The kernel finds the +/// tangent arcs; each side is then cut back to its tangent point, keeping the +/// part that was picked. A closed curve has no end to cut back and is left +/// whole. +fn fillet_curves( + e1: &FilletEntity, + click1: [f64; 2], + e2: &FilletEntity, + click2: [f64; 2], + radius: f64, + z: f64, +) -> Option<(EntityType, EntityType, Option)> { + if radius < 1e-9 { + return None; + } + let (a, b) = (e1.kernel_curve()?, e2.kernel_curve()?); + let common = match e1 { + FilletEntity::Line(l) => l.common.clone(), + FilletEntity::Arc(a) => a.common.clone(), + FilletEntity::LwPoly { poly, .. } => poly.common.clone(), + FilletEntity::Curve(entity) => entity.common().clone(), + }; + for fillet in ranked_fillets(&a, click1, &b, click2, radius) { + let (Some(first), Some(second)) = ( + trim_to_tangent(e1, &a, fillet.tangent1, click1), + trim_to_tangent(e2, &b, fillet.tangent2, click2), + ) else { + continue; + }; + return Some(( + first, + second, + Some(EntityType::Arc(fillet_entity(&fillet, radius, z, &common))), + )); + } + None +} + +/// One side of a fillet cut back to where the arc touches it, the picked part +/// kept. `None` when the touch is off a bounded curve's drawn part — only a +/// line may be extended to meet the arc. +fn trim_to_tangent( + entity: &FilletEntity, + curve: &KernelCurve, + touch: [f64; 2], + click: [f64; 2], +) -> Option { + match entity { + FilletEntity::Line(l) => trim_line_to_point(l, touch, click).map(EntityType::Line), + FilletEntity::LwPoly { poly, seg_idx, .. } => { + let line = lwpoly_seg_as_line(poly, *seg_idx); + let trimmed = trim_line_to_point(&line, touch, click)?; + Some(EntityType::LwPolyline(rebuild_poly_from_trimmed_line(poly, *seg_idx, &line, &trimmed))) + } + FilletEntity::Arc(arc) => { + let (centre, _, start, end, _) = arc_geom(arc); + let at = arc_angle_at(centre, touch); + let clamped = clamp_angle_to_arc(at, start, end); + if (norm_angle(at) - norm_angle(clamped)).abs() > 0.01 { + return None; + } + let picked = clamp_angle_to_arc(arc_angle_at(centre, click), start, end); + Some(EntityType::Arc( + if (clamped - start).rem_euclid(TAU) <= (picked - start).rem_euclid(TAU) { + trim_arc(arc, clamped, end) + } else { + trim_arc(arc, start, clamped) + }, + )) + } + FilletEntity::Curve(source) => { + let at = curve.parameter_at(touch); + let on = curve.point_at(at); + let scale = 1e-6 * (1.0 + touch[0].abs().max(touch[1].abs())); + if (on[0] - touch[0]).hypot(on[1] - touch[1]) > scale.max(1e-6 * curve.length()) { + return None; + } + if curve.is_closed() { + return Some(source.clone()); + } + let keep_end = curve.parameter_at(click) >= at; + match source { + // Its plan image runs the entity's own parameter only when + // it faces up; a mirrored one is left alone. + EntityType::Ellipse(ellipse) if ellipse.normal.z > 0.0 => { + let mut trimmed = ellipse.clone(); + trimmed.common.handle = Handle::NULL; + let sweep = { + let raw = ellipse.end_parameter - ellipse.start_parameter; + if raw <= 0.0 { raw + TAU } else { raw } + }; + let cut = ellipse.start_parameter + at * sweep; + if keep_end { + trimmed.start_parameter = cut; + } else { + trimmed.end_parameter = cut; + } + Some(EntityType::Ellipse(trimmed)) + } + EntityType::Spline(spline) => { + let (low, high) = super::spline_ops::spline_range(spline)?; + let (head, tail) = super::spline_ops::spline_cut(spline, low + at * (high - low))?; + Some(EntityType::Spline(if keep_end { tail } else { head })) + } + _ => None, + } + } + } +} + /// Trim a line endpoint nearest to the intersection point, keeping the click side. fn trim_line_to_point(line: &LineEnt, isect: [f64; 2], click: [f64; 2]) -> Option { let mut l = line.clone(); diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index 843c67a11..576cd9460 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -15,7 +15,6 @@ // offset at once on the cursor's side; a negative one, on the other side. use crate::entities::curve::{entity_curve, lwpolyline_world_xy}; -use crate::modules::draw::modify::spline_ops::spline_sample_xy; use codec::entities::LwVertex; use codec::entities::{ Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, @@ -24,7 +23,6 @@ use codec::entities::{ use codec::{EntityType, Handle}; // Polyline offsetting, and the angle normalisation that goes with it, come // from the kernel; only the entity conversion stays here. -use kernel::geom2d::nurbs::clamped_uniform_knots; use kernel::geom2d::{ offset_polyline, Polyline as KernelPolyline, PolylineVertex as KernelVertex, }; @@ -205,111 +203,77 @@ fn offset_lwpolyline(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Option Option { - let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); - if a < 1e-9 { - return None; - } - let b = a * e.minor_axis_ratio; - let nx = e.major_axis.x / a; - let ny = e.major_axis.y / a; - // Project side_pt onto ellipse local frame and test inside/outside. - let rx = side_pt.x as f64 - e.center.x; - let ry = side_pt.y as f64 - e.center.y; - let xl = rx * nx + ry * ny; - let yl = -rx * ny + ry * nx; - let inside = (xl / a).powi(2) + (yl / b).powi(2) < 1.0; - let sign = if inside { -1.0 } else { 1.0 }; - - let new_a = a + sign * dist; - let new_b = b + sign * dist; - if new_a <= 1e-9 || new_b <= 1e-9 { - return None; - } - - let mut new_e = e.clone(); - new_e.common.handle = Handle::NULL; - // Scale the major_axis vector proportionally. - let scale = new_a / a; - new_e.major_axis.x *= scale; - new_e.major_axis.y *= scale; - new_e.major_axis.z *= scale; - new_e.minor_axis_ratio = new_b / new_a; - Some(EntityType::Ellipse(new_e)) + let source = EntityType::Ellipse(e.clone()); + let mut template = SplineEnt::new(); + template.common = e.common.clone(); + offset_curved(&source, template, dist, side_pt) } -// ── Spline offset ────────────────────────────────────────────────────────── -// -// Strategy: sample the spline into N points, offset each sample point by -// `dist` along the local perpendicular (based on the finite-difference -// tangent), then fit a new spline through the offset points. - fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option { - let (ts_knot, pts) = spline_sample_xy(spl, 64); - let n = pts.len(); - if n < 2 { + offset_curved(&EntityType::Spline(spl.clone()), spl.clone(), dist, side_pt) +} + +fn offset_curved( + source: &EntityType, + mut template: SplineEnt, + dist: f64, + side_pt: Vec3, +) -> Option { + let planar = entity_curve(source)?; + let side = planar + .plane + .project([side_pt.x as f64, side_pt.y as f64, side_pt.z as f64])?; + // Left of the curve's direction of travel is the kernel's positive side. + let curve = &planar.curve; + let at = curve.parameter_at(side); + let step = 1e-6; + let (before, after) = if curve.is_closed() { + (curve.point_at((at - step).rem_euclid(1.0)), curve.point_at((at + step).rem_euclid(1.0))) + } else { + (curve.point_at((at - step).max(0.0)), curve.point_at((at + step).min(1.0))) + }; + let foot = curve.point_at(at); + let along = [after[0] - before[0], after[1] - before[1]]; + let toward = [side[0] - foot[0], side[1] - foot[1]]; + let left = along[0] * toward[1] - along[1] * toward[0] > 0.0; + let signed = if left { dist.abs() } else { -dist.abs() }; + let tolerance = (curve.length() * 1e-6).max(1e-9); + let kernel::geom2d::Curve::Nurbs(parallel) = + kernel::geom2d::offset_curve(curve, signed, tolerance)? + else { return None; - } - - // Determine offset sign from the first non-degenerate tangent. - let sign: f64 = (0..n - 1).find_map(|i| { - let dx = pts[i + 1][0] - pts[i][0]; - let dy = pts[i + 1][1] - pts[i][1]; - let len = (dx * dx + dy * dy).sqrt(); - if len < 1e-12 { - return None; - } - let vx = side_pt.x as f64 - pts[i][0]; - let vy = side_pt.y as f64 - pts[i][1]; - let cross = dx * vy - dy * vx; - Some(if cross >= 0.0 { 1.0 } else { -1.0 }) - })?; - - // Offset each sample point along the local normal. - let offset_pts: Vec = pts + }; + template.common.handle = Handle::NULL; + template.degree = parallel.degree() as i32; + template.knots = parallel.knots().to_vec(); + template.control_points = parallel + .control_points() .iter() - .enumerate() - .map(|(i, p)| { - // Tangent via central / forward / backward difference. - let (dx, dy) = if i == 0 { - let d = [pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]]; - (d[0], d[1]) - } else if i == n - 1 { - let d = [pts[n - 1][0] - pts[n - 2][0], pts[n - 1][1] - pts[n - 2][1]]; - (d[0], d[1]) - } else { - ( - (pts[i + 1][0] - pts[i - 1][0]) * 0.5, - (pts[i + 1][1] - pts[i - 1][1]) * 0.5, - ) - }; - let len = (dx * dx + dy * dy).sqrt().max(1e-12); - let nx = -dy / len; // left perpendicular - let ny = dx / len; - let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0); - codec::types::Vector3::new(p[0] + sign * nx * dist, p[1] + sign * ny * dist, z) + .map(|uv| { + let [x, y, z] = planar.plane.point_at(*uv); + codec::types::Vector3::new(x, y, z) }) .collect(); - - let _ = ts_knot; - // Build a new spline from the offset control points (treat sample pts as fit pts → ctrl pts). - let degree = spl.degree.max(1) as usize; - let new_ctrl: Vec = offset_pts; - let n_ctrl = new_ctrl.len(); - let mut new_spl = spl.clone(); - new_spl.common.handle = Handle::NULL; - new_spl.control_points = new_ctrl; - new_spl.knots = clamped_uniform_knots(degree, n_ctrl); - new_spl.fit_points.clear(); - new_spl.weights.clear(); - Some(EntityType::Spline(new_spl)) + template.weights = if parallel.is_rational() { parallel.weights().to_vec() } else { Vec::new() }; + template.flags.rational = parallel.is_rational(); + template.flags.periodic = false; + template.flags.closed = curve.is_closed(); + if let Some([x, y, z]) = planar.plane.normal() { + template.normal = codec::types::Vector3::new(x, y, z); + } + // A parallel is no longer the curve through the source's fit points. + template.fit_points.clear(); + template.begin_tangent = codec::types::Vector3::new(0.0, 0.0, 0.0); + template.end_tangent = codec::types::Vector3::new(0.0, 0.0, 0.0); + Some(EntityType::Spline(template)) } // ── Dispatch ─────────────────────────────────────────────────────────────── From 2a2d9b55d9a328fb6e2ec4d4adb6b122aa189459 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 9 Oct 2026 10:22:08 +0300 Subject: [PATCH 5/5] Cargo: kernel 0764f79, graph 75c30c2 Island cuts stay inside the face; MASSPROP falls back to a coarser mesh when the finest leaves a face untriangulated. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 875643b27..a5d91485f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4228,7 +4228,7 @@ dependencies = [ [[package]] name = "opencadgraph" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=d3558bb#d3558bb1ccd63d3819fa0358267bac1ba21ab593" +source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=75c30c2#75c30c294299c5a754a9a8e36d19609af758dc9b" dependencies = [ "opencadkernel", "serde_json", @@ -4237,7 +4237,7 @@ dependencies = [ [[package]] name = "opencadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=a82f17d#a82f17dd34af0787023c93ed565220dd1467ff46" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=0764f79#0764f79b176e0165999829f01c5b64e0cc50ae98" dependencies = [ "cavalier_contours", "opencadcodec", @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "opencadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=a82f17d#a82f17dd34af0787023c93ed565220dd1467ff46" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=0764f79#0764f79b176e0165999829f01c5b64e0cc50ae98" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index f62e0ef45..700ad682a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,9 +35,9 @@ clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] } -kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "a82f17d", features = ["acis", "offset"] } -kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "a82f17d" } -graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "d3558bb" } +kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "0764f79", features = ["acis", "offset"] } +kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "0764f79" } +graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "75c30c2" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }