Merge remote-tracking branch 'upstream/main' into fix/716-perpendicular-tracking

This commit is contained in:
gianlucafiore committed 2026-08-09 11:43:08 -03:00
commit af7613f1d8
38 files changed
+45393 -32358

No files matched your search

Generated
+19 -5
View File
@@ -16,7 +16,7 @@ dependencies = [
name = "OpenCADStudio"
version = "0.9.4"
dependencies = [
"acadifc",
"acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=fe98e42)",
"ashpd",
"bincode",
"bytemuck",
@@ -96,6 +96,20 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "acadifc"
version = "0.5.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=fe98e42#fe98e425578c54facd021d8df74d86052b2f848b"
dependencies = [
"acadrust",
"base64 0.22.1",
"cadkernel",
"serde",
"serde_json",
"sha2 0.10.9",
"thiserror 1.0.69",
]
[[package]]
name = "acadrust"
version = "0.4.1"
@@ -935,7 +949,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git#5499242f1160018e066f78d4e462c9532e665a96"
source = "git+https://github.com/HakanSeven12/cadkernel.git#c9e104b99e8901c4804e7cece67f038f249a33f5"
dependencies = [
"cavalier_contours",
]
@@ -3462,7 +3476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
dependencies = [
"bytecount",
"memchr 2.8.3",
"memchr 1.0.2",
"nom 8.0.0",
]
@@ -3939,7 +3953,7 @@ dependencies = [
name = "ocs_plugin_api"
version = "0.1.0"
dependencies = [
"acadifc",
"acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=c65d396)",
"bincode",
"getrandom 0.2.17",
"interprocess",
@@ -3954,7 +3968,7 @@ dependencies = [
name = "ocs_web_worker"
version = "0.1.0"
dependencies = [
"acadifc",
"acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=c65d396)",
"bincode",
"console_error_panic_hook",
"getrandom 0.3.4",
+1 -1
View File
@@ -38,7 +38,7 @@ env_logger = "0.11"
# The CAD stack is reached through acadifc, which re-exports the codec and
# the geometry kernel. Aliased to `acadrust` so existing `use acadrust::…`
# paths keep resolving.
acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "c65d396", features = ["serde", "offset"] }
acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "fe98e42", features = ["serde", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+19 -10
View File
@@ -339,10 +339,8 @@ pub(super) fn polar_constrain(
))
}
/// Polar constraint that only engages when the cursor is within `tol_px`
/// screen pixels of the nearest polar ray; otherwise the cursor is left free
/// so POLAR behaves as if off when pointing away from every angle (issue #70).
pub(super) fn polar_constrain_near(
/// Return the polar-constrained point while its ray is engaged.
pub(super) fn polar_constrain_if_near(
pt: glam::DVec3,
base: glam::DVec3,
step_deg: f32,
@@ -351,7 +349,7 @@ pub(super) fn polar_constrain_near(
bounds: iced::Rectangle,
tol_px: f32,
xf: &UcsXform,
) -> glam::DVec3 {
) -> Option<glam::DVec3> {
let snapped = polar_constrain(pt, base, step_deg, xf);
let to_screen = |w: glam::DVec3| {
let ndc = view_rot.project_point3((w - eye).as_vec3());
@@ -362,11 +360,22 @@ pub(super) fn polar_constrain_near(
};
let (cx, cy) = to_screen(pt);
let (sx, sy) = to_screen(snapped);
if ((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px {
snapped
} else {
pt
}
(((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px).then_some(snapped)
}
/// Constrain near an engaged polar ray; otherwise keep the cursor free.
pub(super) fn polar_constrain_near(
pt: glam::DVec3,
base: glam::DVec3,
step_deg: f32,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
tol_px: f32,
xf: &UcsXform,
) -> glam::DVec3 {
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf)
.unwrap_or(pt)
}
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
+1 -2
View File
@@ -430,8 +430,7 @@ pub(super) struct OpenCADStudio {
/// cursor is on a tracking ray. Lets a typed distance place a point along
/// the ray from the tracking point (issue #69). `None` when not aligned.
otrack_active: Option<(glam::DVec3, glam::DVec3)>,
/// Kind of the currently active tracking reference, kept separately from
/// `otrack_active` so typed-distance behavior remains unchanged.
/// Active OTRACK ray kind, separate from typed-distance geometry.
otrack_kind: Option<crate::snap::TrackingKind>,
/// Whether Tangent snap was enabled before a tangent-pick command started.
pre_cmd_tangent: Option<bool>,
+1 -1
View File
@@ -1384,7 +1384,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&& destination_is_current
{
self.command_line.push_error_once(
crate::tr!("recovery-save-new-file-required").as_ref(),
crate::tr!("recovery", "save-new-file-required").as_ref(),
);
self.restore_failed_save_continuation(continuation, i);
self.active_tab = i;
+6 -4
View File
@@ -3137,6 +3137,8 @@ impl OpenCADStudio {
self.snapper.otrack_enabled ^= true;
if !self.snapper.otrack_enabled {
self.snapper.clear_tracking();
self.otrack_active = None;
self.otrack_kind = None;
}
Task::none()
}
@@ -3997,23 +3999,23 @@ impl OpenCADStudio {
}
Text::EmptyOrUnsupported => {
self.command_line.push_error(
crate::tr!("clipboard-no-supported-content").as_ref(),
crate::tr!("clipboard", "no-supported-content").as_ref(),
);
Task::none()
}
Text::Unavailable => {
self.command_line
.push_error(crate::tr!("clipboard-unavailable").as_ref());
.push_error(crate::tr!("clipboard", "unavailable").as_ref());
Task::none()
}
Text::Occupied => {
self.command_line
.push_error(crate::tr!("clipboard-occupied").as_ref());
.push_error(crate::tr!("clipboard", "occupied").as_ref());
Task::none()
}
Text::ConversionFailed => {
self.command_line
.push_error(crate::tr!("clipboard-conversion-failed").as_ref());
.push_error(crate::tr!("clipboard", "conversion-failed").as_ref());
Task::none()
}
}
+63 -154
View File
@@ -4,8 +4,9 @@
use super::util::*;
use super::{format_size, VIEWCUBE_HIT_SIZE};
use crate::app::helpers::{
axis_lock_apply, axis_lock_capture, drafting_axes, drafting_constrain, parse_coord, polar_constrain_near,
ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
axis_lock_apply, axis_lock_capture, drafting_axes, drafting_constrain, parse_coord,
polar_constrain_if_near, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
CoordKind,
};
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
use crate::modules::ModuleEvent;
@@ -117,6 +118,45 @@ fn over_ucs_icon(p: Point, h: &crate::ui::overlay::UcsIconHit) -> bool {
}
impl OpenCADStudio {
fn active_construction_ray(
&self,
tab: usize,
cursor: glam::DVec3,
base: glam::DVec3,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Option<(glam::DVec3, glam::DVec3)> {
let ucs = self.tabs[tab].ucs_xform();
let mut target = if self.ortho_mode {
drafting_constrain(
cursor,
base,
&ucs,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
)
} else if self.polar_mode {
polar_constrain_if_near(
cursor,
base,
self.polar_increment_deg,
view_rot,
eye,
bounds,
self.snapper.osnap_radius_px,
&ucs,
)?
} else {
return None;
};
if self.tabs[tab].active_ucs.is_none() {
target.z = base.z;
}
Some((base, target))
}
/// Write PDSIZE from the dialog buffer with the current relative/absolute
/// sign. A relative size is stored negative; absolute positive. Switching to
/// absolute with an empty/zero size seeds a positive value from the current
@@ -1034,60 +1074,10 @@ impl OpenCADStudio {
// snapping must drop its foot from this point, including when a
// hot-grip set is moved by the same drag vector.
self.snapper.from_point = Some(grip.origin_world.as_vec3());
self.snapper.from_point = Some(grip.origin_world.as_vec3());
// Keep the updated drafting basis from main so grid/snap follows the
// current drafting axes, including isometric drafting and SNAPANG.
let (go, gr) = self.drafting_grid_basis(i);
let base = grip.origin_world;
let ucs_xf = self.tabs[i].ucs_xform();
let mut construction_cursor = if self.ortho_mode {
Some(drafting_constrain(
raw,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
raw,
base,
self.polar_increment_deg,
view_rot,
eye,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
// POLAR being enabled is not enough: the cursor must actually
// be engaged on one of the polar tracking rays.
let delta = pt - base;
let step = (self.polar_increment_deg as f64).to_radians();
let angle = delta.y.atan2(delta.x);
let polar_engaged = step > 1e-9
&& (((angle / step).round() * step - angle).abs() < 1e-3)
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
if polar_engaged {
Some(pt)
} else {
None
}
} else {
None
};
if let Some(pt) = construction_cursor.as_mut() {
if self.tabs[i].active_ucs.is_none() {
pt.z = base.z;
}
}
let construction_ray =
self.active_construction_ray(i, raw, base, view_rot, eye, bounds);
// `raw` is already model space (viewport camera or paper→model),
// and the wires are model space, so the snap result is model.
let snap_hit = self.snapper.snap(
@@ -1099,7 +1089,7 @@ impl OpenCADStudio {
bounds,
go,
gr,
construction_cursor,
construction_ray,
);
self.tabs[i].snap_result = snap_hit;
@@ -1519,60 +1509,18 @@ impl OpenCADStudio {
// the downcast point here.
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
// Build the temporary construction direction before OSNAP.
// Intersection can then test the active rubber-band against
// existing geometry (#704).
let construction_cursor = if is_window_corner {
let construction_ray = if is_window_corner {
None
} else {
self.last_point.and_then(|base| {
let ucs_xf = self.tabs[i].ucs_xform();
let mut pt = if self.ortho_mode {
Some(drafting_constrain(
snap_cursor,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
snap_cursor,
base,
self.polar_increment_deg,
view_rot,
eye,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
// POLAR being enabled is not enough: the cursor must actually
// be engaged on one of the polar tracking rays.
let delta = pt - base;
let step = (self.polar_increment_deg as f64).to_radians();
let angle = delta.y.atan2(delta.x);
let polar_engaged = step > 1e-9
&& (((angle / step).round() * step - angle).abs() < 1e-3)
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
if polar_engaged {
Some(pt)
} else {
None
}
} else {
None
}?;
if self.tabs[i].active_ucs.is_none() {
pt.z = base.z;
}
Some(pt)
self.active_construction_ray(
i,
snap_cursor,
base,
view_rot,
eye,
bounds,
)
})
};
@@ -1585,7 +1533,7 @@ impl OpenCADStudio {
bounds,
go,
gr,
construction_cursor,
construction_ray,
)
};
@@ -2907,57 +2855,18 @@ impl OpenCADStudio {
let (go, gr) = self.drafting_grid_basis(i);
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
let construction_cursor = if is_window_corner {
let construction_ray = if is_window_corner {
None
} else {
self.last_point.and_then(|base| {
let ucs_xf = self.tabs[i].ucs_xform();
let mut pt = if self.ortho_mode {
Some(drafting_constrain(
self.active_construction_ray(
i,
snap_cursor,
base,
&ucs_xf,
self.isometric_drafting,
self.iso_plane,
self.snap_angle_deg,
))
} else if self.polar_mode {
let pt = polar_constrain_near(
snap_cursor,
base,
self.polar_increment_deg,
view_rot,
eye,
bounds,
self.snapper.osnap_radius_px,
&ucs_xf,
);
// POLAR being enabled is not enough: the cursor must actually
// be engaged on one of the polar tracking rays.
let delta = pt - base;
let step = (self.polar_increment_deg as f64).to_radians();
let angle = delta.y.atan2(delta.x);
let polar_engaged = step > 1e-9
&& (((angle / step).round() * step - angle).abs() < 1e-3)
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
if polar_engaged {
Some(pt)
} else {
None
}
} else {
None
}?;
if self.tabs[i].active_ucs.is_none() {
pt.z = base.z;
}
Some(pt)
view_rot,
eye,
bounds,
)
})
};
@@ -2970,7 +2879,7 @@ impl OpenCADStudio {
bounds,
go,
gr,
construction_cursor,
construction_ray,
)
};
// Snap runs in model space; the result is already model.
+4 -4
View File
@@ -449,7 +449,7 @@ pub(super) fn viewport_controls<'a>(
crate::ui::icons::SPLIT_V,
false,
Message::SplitModelViewport(false),
crate::tr!("viewport-split-vertical"),
crate::tr!("viewport", "split-vertical"),
"VPORTS 2V",
))
.push(sep())
@@ -457,7 +457,7 @@ pub(super) fn viewport_controls<'a>(
crate::ui::icons::SPLIT_H,
false,
Message::SplitModelViewport(true),
crate::tr!("viewport-split-horizontal"),
crate::tr!("viewport", "split-horizontal"),
"VPORTS 2H",
));
// Drag handle + close: only meaningful with more than one model tile.
@@ -478,7 +478,7 @@ pub(super) fn viewport_controls<'a>(
)
.interaction(iced::mouse::Interaction::Grab)
.on_press(Message::PaneMoveStart);
let drag = viewport_tooltip(drag, crate::tr!("viewport-move"), "VPORTS");
let drag = viewport_tooltip(drag, crate::tr!("viewport", "move"), "VPORTS");
bar = bar
.push(sep())
.push(drag)
@@ -486,7 +486,7 @@ pub(super) fn viewport_controls<'a>(
.push(danger_btn(
crate::ui::icons::CLOSE,
Message::CloseModelViewport,
crate::tr!("viewport-close"),
crate::tr!("viewport", "close"),
"VPORTS SINGLE",
));
}
+29 -29
View File
@@ -812,10 +812,10 @@ impl OpenCADStudio {
let tracking_hint = match self.otrack_kind {
Some(crate::snap::TrackingKind::Perpendicular) => {
Some("Perpendicular".to_string())
Some(crate::tr!("common", "perpendicular"))
}
Some(crate::snap::TrackingKind::Extension) => {
Some("Extension".to_string())
Some(crate::tr!("common", "extension"))
}
_ => None,
};
@@ -2636,7 +2636,7 @@ fn start_page_content<'a>(
button(
row![
crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 14.0),
text(crate::tr!("start-donate")).size(14),
text(crate::tr!("start", "donate")).size(14),
]
.spacing(5)
.align_y(iced::Center),
@@ -2650,8 +2650,8 @@ fn start_page_content<'a>(
};
let primary_row = WrapFlow::new(vec![
outline_btn(crate::tr!("start-new-drawing"), Message::TabNew).into(),
outline_btn(crate::tr!("start-open-file"), Message::OpenFile).into(),
outline_btn(crate::tr!("start", "new-drawing"), Message::TabNew).into(),
outline_btn(crate::tr!("start", "open-file"), Message::OpenFile).into(),
donate_btn.into(),
])
.spacing_x(12.0)
@@ -2661,16 +2661,16 @@ fn start_page_content<'a>(
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut secondary_items: Vec<Element<'a, Message>> = vec![
outline_btn(
crate::tr!("start-send-feedback"),
crate::tr!("start", "send-feedback"),
Message::RibbonToolClick {
tool_id: "REPORT".to_string(),
event: crate::modules::ModuleEvent::Command("REPORT".to_string()),
},
)
.into(),
outline_btn(crate::tr!("action-options"), Message::OptionsOpen).into(),
outline_btn(crate::tr!("action", "options"), Message::OptionsOpen).into(),
];
secondary_items.push(outline_btn(crate::tr!("action-plugins"), Message::PluginManagerOpen).into());
secondary_items.push(outline_btn(crate::tr!("action", "plugins"), Message::PluginManagerOpen).into());
// The web build is already in the browser, so only the desktop offers a
// link to the web version.
#[cfg(not(target_arch = "wasm32"))]
@@ -2693,7 +2693,7 @@ fn start_page_content<'a>(
.report_natural_width(action_width_out.clone());
let sponsors = column![
text(crate::tr!("start-sponsors")).size(15),
text(crate::tr!("start", "sponsors")).size(15),
mouse_area(
container(
iced::widget::svg(iced::widget::svg::Handle::from_memory(include_bytes!(
@@ -2795,7 +2795,7 @@ fn start_page_content<'a>(
// whole thumbnail remains visible when that width changes.
let thumb_h =
(panel_w - VIDEO_PANEL_PADDING * 2.0 - VIDEO_SCROLL_GUTTER) * 9.0 / 16.0;
let mut list = column![text(crate::tr!("start-tutorials")).size(15)]
let mut list = column![text(crate::tr!("start", "tutorials")).size(15)]
.spacing(10)
.width(Fill)
// Keep the scrollbar off the thumbnails.
@@ -2835,14 +2835,14 @@ fn start_page_content<'a>(
}
if videos.is_empty() {
let note = if videos_loading {
crate::tr!("start-loading-videos")
crate::tr!("start", "loading-videos")
} else {
crate::tr!("start-videos-online")
crate::tr!("start", "videos-online")
};
list = list.push(text(note).size(12).style(start_muted_style));
}
let playlist_btn = mouse_area(
container(text(crate::tr!("start-open-playlist")).size(12))
container(text(crate::tr!("start", "open-playlist")).size(12))
.padding([6, 10])
.width(Fill)
.center_x(Fill)
@@ -2895,7 +2895,7 @@ fn start_page_content<'a>(
// web builds read the CI-generated snapshot. Both sources mark pinned
// discussions and sort them before the rest of the list.
let discussions_panel: Element<'a, Message> = {
let mut list = column![text(crate::tr!("start-discussions")).size(15)]
let mut list = column![text(crate::tr!("start", "discussions")).size(15)]
.spacing(8)
.width(Fill);
for discussion in discussions {
@@ -2908,7 +2908,7 @@ fn start_page_content<'a>(
.align_y(iced::Center);
if discussion.pinned {
meta = meta.push(
text(crate::tr!("start-pinned"))
text(crate::tr!("start", "pinned"))
.size(10)
.style(start_primary_style),
);
@@ -2951,14 +2951,14 @@ fn start_page_content<'a>(
}
if discussions.is_empty() {
let note = if discussions_loading {
crate::tr!("start-loading-discussions")
crate::tr!("start", "loading-discussions")
} else {
crate::tr!("start-discussions-online")
crate::tr!("start", "discussions-online")
};
list = list.push(text(note).size(12).style(start_muted_style));
}
let open_btn = mouse_area(
container(text(crate::tr!("start-open-discussions")).size(12))
container(text(crate::tr!("start", "open-discussions")).size(12))
.padding([6, 10])
.width(Fill)
.center_x(Fill)
@@ -3015,7 +3015,7 @@ fn start_page_content<'a>(
// shows, so the rail always invites support.
let supporters: Element<'a, Message> = {
let mut list = column![
text(crate::tr!("start-supporters")).size(15),
text(crate::tr!("start", "supporters")).size(15),
Space::new().height(iced::Length::Fixed(12.0)),
]
.spacing(6)
@@ -3040,7 +3040,7 @@ fn start_page_content<'a>(
container(
iced::widget::row![
crate::ui::icons::themed_danger_text(crate::ui::icons::HEART, 13.0),
text(crate::tr!("start-support-on-patreon")).size(12),
text(crate::tr!("start", "support-on-patreon")).size(12),
]
.spacing(6)
.align_y(iced::Center),
@@ -3158,11 +3158,11 @@ fn start_page_content<'a>(
})
};
let tab_bar = Row::with_children(vec![
tab_btn(crate::tr!("start-recent-files"), super::StartSection::Recent).into(),
tab_btn(crate::tr!("start-videos"), super::StartSection::Videos).into(),
tab_btn(crate::tr!("start-welcome"), super::StartSection::Welcome).into(),
tab_btn(crate::tr!("start-discussions"), super::StartSection::Discussions).into(),
tab_btn(crate::tr!("start-supporters"), super::StartSection::Supporters).into(),
tab_btn(crate::tr!("start", "recent-files"), super::StartSection::Recent).into(),
tab_btn(crate::tr!("start", "videos"), super::StartSection::Videos).into(),
tab_btn(crate::tr!("start", "welcome"), super::StartSection::Welcome).into(),
tab_btn(crate::tr!("start", "discussions"), super::StartSection::Discussions).into(),
tab_btn(crate::tr!("start", "supporters"), super::StartSection::Supporters).into(),
])
.spacing(6.0)
.align_y(iced::Center)
@@ -3238,11 +3238,11 @@ pub(super) fn recent_files_panel<'a>(
) -> Element<'a, Message> {
// Title mirrors the Supporters rail: size 15 in the bright text colour,
// followed by a 12px gap before the content.
let title = text(crate::tr!("start-recent-documents")).size(15);
let title = text(crate::tr!("start", "recent-documents")).size(15);
let body: Element<'a, Message> = if recents.is_empty() {
container(
text(crate::tr!("start-no-recent-files"))
text(crate::tr!("start", "no-recent-files"))
.size(12)
.style(start_muted_style)
)
@@ -3270,7 +3270,7 @@ pub(super) fn recent_files_panel<'a>(
// directory line.
#[cfg(target_arch = "wasm32")]
let dir = if dir.is_empty() {
crate::tr!("start-browser-storage")
crate::tr!("start", "browser-storage")
} else {
dir
};
@@ -3381,7 +3381,7 @@ pub(super) fn recent_files_panel<'a>(
.padding([2, 6])
.width(iced::Length::Fixed(46.0));
let limit_row = row![
text(crate::tr!("start-keep-recent-files")).size(11).style(start_muted_style).width(Fill),
text(crate::tr!("start", "keep-recent-files")).size(11).style(start_muted_style).width(Fill),
button(crate::ui::icons::themed(crate::ui::icons::MINUS, 11.0))
.on_press(Message::SetRecentLimit(shown.saturating_sub(STEP)))
.padding([3, 6])
+32 -32
View File
@@ -10,43 +10,43 @@ impl OpenCADStudio {
pub(super) fn modal_title(&self) -> String {
use super::super::ModalKind as K;
match self.active_modal {
Some(K::About) => crate::tr!("modal-about"),
Some(K::Shortcuts) => crate::tr!("modal-keyboard-shortcuts"),
Some(K::Aliases) => crate::tr!("modal-command-aliases"),
Some(K::Options) => crate::tr!("action-options"),
Some(K::FindReplace) => crate::tr!("modal-find-replace"),
Some(K::PluginManager) => crate::tr!("modal-plugin-manager"),
Some(K::UpdateNotice) => crate::tr!("modal-update-available"),
Some(K::Layers) => crate::tr!("modal-layer-manager"),
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
Some(K::About) => crate::tr!("modal", "about"),
Some(K::Shortcuts) => crate::tr!("modal", "keyboard-shortcuts"),
Some(K::Aliases) => crate::tr!("modal", "command-aliases"),
Some(K::Options) => crate::tr!("action", "options"),
Some(K::FindReplace) => crate::tr!("modal", "find-replace"),
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
Some(K::Plot) => crate::tr!("modal-plot"),
Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"),
Some(K::Plot) => crate::tr!("modal", "plot"),
Some(K::PrintAll) => t!("Print All").into_owned(),
Some(K::LayoutManager) => crate::tr!("modal-layout-manager"),
Some(K::ScaleManager) => crate::tr!("modal-scale-manager"),
Some(K::AnnoObjectScale) => crate::tr!("modal-annotation-object-scale"),
Some(K::Plotstyle) => crate::tr!("modal-plot-style-editor"),
Some(K::TextStyle) => crate::tr!("modal-text-style-manager"),
Some(K::MlStyle) => crate::tr!("modal-multiline-style-manager"),
Some(K::TableStyle) => crate::tr!("modal-table-style-manager"),
Some(K::MLeaderStyle) => crate::tr!("modal-multileader-style-manager"),
Some(K::DimStyle) => crate::tr!("modal-dimension-style-manager"),
Some(K::AssocPrompt) => crate::tr!("modal-default-application"),
Some(K::AecDropWarning) => crate::tr!("modal-save-warning"),
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
Some(K::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
Some(K::MlStyle) => crate::tr!("modal", "multiline-style-manager"),
Some(K::TableStyle) => crate::tr!("modal", "table-style-manager"),
Some(K::MLeaderStyle) => crate::tr!("modal", "multileader-style-manager"),
Some(K::DimStyle) => crate::tr!("modal", "dimension-style-manager"),
Some(K::AssocPrompt) => crate::tr!("modal", "default-application"),
Some(K::AecDropWarning) => crate::tr!("modal", "save-warning"),
#[cfg(not(target_arch = "wasm32"))]
Some(K::FileInUse) => crate::tr!("modal-unable-save"),
Some(K::FileInUse) => crate::tr!("modal", "unable-save"),
#[cfg(not(target_arch = "wasm32"))]
Some(K::ExternalChange) => crate::tr!("modal-drawing-changed"),
Some(K::LayerDeleteWarning) => crate::tr!("modal-delete-layer"),
Some(K::Unsaved) => crate::tr!("modal-unsaved-changes"),
Some(K::PointStyle) => crate::tr!("modal-point-style"),
Some(K::AttributeEditor) => crate::tr!("modal-attribute-editor"),
Some(K::SaveDialog) => crate::tr!("modal-save-drawing-as"),
Some(K::Recovery) => crate::tr!("modal-recovery-report"),
Some(K::RecoveryPrompt) => crate::tr!("modal-recovery-prompt"),
Some(K::ExternalChange) => crate::tr!("modal", "drawing-changed"),
Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"),
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
Some(K::Recovery) => crate::tr!("modal", "recovery-report"),
Some(K::RecoveryPrompt) => crate::tr!("modal", "recovery-prompt"),
None => String::new(),
}
}
@@ -1528,7 +1528,7 @@ fn unsaved_changes_dialog_window(
column![
text(name.to_owned()).size(14),
iced::widget::Space::new().height(4),
text(crate::tr!("modal-unsaved-save-prompt"))
text(crate::tr!("modal", "unsaved-save-prompt"))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(20),
+15
View File
@@ -710,6 +710,21 @@ mod tests {
assert!(curve.is_closed());
}
/// The whole stack resolves from here: OCS reaches the B-rep layer and
/// the ACIS bridge through the same alias every other CAD type comes in
/// by. A compile-time check, so a chain that stops resolving is caught
/// where it happens rather than the next time somebody reaches for it.
#[test]
fn the_solid_layer_and_the_acis_bridge_are_reachable() {
let solid = acadrust::kernel::brep::make::cuboid([0.0; 3], [1.0; 3])
.expect("the kernel builds its own primitives");
assert!(solid.validate().is_empty());
assert_eq!(solid.euler_characteristic(), 2);
let document = acadrust::entities::acis::types::SatDocument::new();
let (bodies, loss) = acadrust::acis::lift(&document);
assert!(bodies.is_empty() && loss.is_empty(), "an empty document lifts to nothing");
}
#[test]
fn entities_that_are_not_curves_say_so() {
assert!(entity_curve(&EntityType::Point(Default::default())).is_none());
+37 -26
View File
@@ -88,19 +88,19 @@ impl Language {
pub fn label(self) -> String {
match self {
Language::System => crate::tr!("language-system"),
Language::EnUs => crate::tr!("language-english"),
Language::TrTr => crate::tr!("language-turkish"),
Language::NlNl => crate::tr!("language-dutch"),
Language::FrFr => crate::tr!("language-french"),
Language::DeDe => crate::tr!("language-german"),
Language::HiIn => crate::tr!("language-hindi"),
Language::RuRu => crate::tr!("language-russian"),
Language::ZhCn => crate::tr!("language-chinese-simplified"),
Language::EsEs => crate::tr!("language-spanish"),
Language::PtBr => crate::tr!("language-portuguese"),
Language::ArSa => crate::tr!("language-arabic"),
Language::JaJp => crate::tr!("language-japanese"),
Language::System => crate::tr!("language", "system"),
Language::EnUs => crate::tr!("language", "english"),
Language::TrTr => crate::tr!("language", "turkish"),
Language::NlNl => crate::tr!("language", "dutch"),
Language::FrFr => crate::tr!("language", "french"),
Language::DeDe => crate::tr!("language", "german"),
Language::HiIn => crate::tr!("language", "hindi"),
Language::RuRu => crate::tr!("language", "russian"),
Language::ZhCn => crate::tr!("language", "chinese-simplified"),
Language::EsEs => crate::tr!("language", "spanish"),
Language::PtBr => crate::tr!("language", "portuguese"),
Language::ArSa => crate::tr!("language", "arabic"),
Language::JaJp => crate::tr!("language", "japanese"),
}
}
}
@@ -200,8 +200,8 @@ pub fn active_language_tag() -> String {
/// translatable data.
pub fn translate(source: impl AsRef<str>) -> Cow<'static, str> {
let source = source.as_ref();
locale_catalog::message_id(source)
.map(|message_id| Cow::Owned(loader().get(message_id)))
locale_catalog::message_attribute(source)
.map(|(message_id, attribute_id)| Cow::Owned(loader().get_attr(message_id, attribute_id)))
.unwrap_or_else(|| Cow::Owned(source.to_string()))
}
@@ -213,8 +213,8 @@ pub fn translate_args(
args: &[(&str, String)],
) -> Cow<'static, str> {
let source = source.as_ref();
let mut translated = locale_catalog::message_id(source)
.map(|message_id| loader().get(message_id))
let mut translated = locale_catalog::message_attribute(source)
.map(|(message_id, attribute_id)| loader().get_attr(message_id, attribute_id))
.unwrap_or_else(|| source.to_string());
for (name, value) in args {
translated = translated.replace(&format!("__ocs_arg_{name}__"), value);
@@ -229,13 +229,13 @@ pub fn translate_args(
/// sentence. File names, handles, counts, and command values therefore remain
/// data instead of being sent through translation.
pub fn translate_format(template: &str, rendered: String) -> Cow<'static, str> {
let Some(message_id) = locale_catalog::message_id(template) else {
let Some((message_id, attribute_id)) = locale_catalog::message_attribute(template) else {
return Cow::Owned(rendered);
};
let Some(values) = format_values(template, &rendered) else {
return Cow::Owned(rendered);
};
let mut translated = loader().get(message_id);
let mut translated = loader().get_attr(message_id, attribute_id);
for (index, value) in values.iter().enumerate() {
translated = translated.replace(&format!("__ocs_fmt_{index}__"), value);
}
@@ -297,19 +297,30 @@ fn format_values(template: &str, rendered: &str) -> Option<Vec<String>> {
/// until they provide their own localization bundle.
pub fn ribbon_module_title(id: &str, fallback: &str) -> String {
match id {
"draw" => crate::tr!("ribbon-tab-draw"),
"annotate" => crate::tr!("ribbon-tab-annotate"),
"insert" => crate::tr!("ribbon-tab-insert"),
"model" => crate::tr!("ribbon-tab-model"),
"layout" => crate::tr!("ribbon-tab-layout"),
"manage" => crate::tr!("ribbon-tab-manage"),
"view" => crate::tr!("ribbon-tab-view"),
"draw" => crate::tr!("ribbon-tab", "draw"),
"annotate" => crate::tr!("ribbon-tab", "annotate"),
"insert" => crate::tr!("ribbon-tab", "insert"),
"model" => crate::tr!("ribbon-tab", "model"),
"layout" => crate::tr!("ribbon-tab", "layout"),
"manage" => crate::tr!("ribbon-tab", "manage"),
"view" => crate::tr!("ribbon-tab", "view"),
_ => fallback.to_string(),
}
}
#[macro_export]
macro_rules! tr {
($message_id:literal, $attribute_id:literal $(,)?) => {
i18n_embed_fl::fl!($crate::i18n::loader(), $message_id, $attribute_id)
};
($message_id:literal, $attribute_id:literal, $($name:ident = $value:expr),+ $(,)?) => {
i18n_embed_fl::fl!(
$crate::i18n::loader(),
$message_id,
$attribute_id,
$($name = $value),+
)
};
($message_id:literal $(,)?) => {
i18n_embed_fl::fl!($crate::i18n::loader(), $message_id)
};
+3172 -3172
View File
File diff suppressed because it is too large. Load diff
+14 -14
View File
@@ -165,25 +165,25 @@ impl AreaCommand {
fn result_message(measurement: AreaMeasurement) -> String {
match measurement.perimeter {
Some(perimeter) => crate::tr!(
"area-result",
"area", "result",
area = format!("{:.4}", measurement.area),
perimeter = format!("{perimeter:.4}"),
),
None => crate::tr!("area-result-area-only", area = format!("{:.4}", measurement.area)),
None => crate::tr!("area", "result-area-only", area = format!("{:.4}", measurement.area)),
}
}
fn running_result_message(&self, measurement: AreaMeasurement) -> String {
match measurement.perimeter {
Some(perimeter) => crate::tr!(
"area-running-result",
"area", "running-result",
area = format!("{:.4}", measurement.area),
perimeter = format!("{perimeter:.4}"),
total_area = format!("{:.4}", self.total_area),
total_perimeter = format!("{:.4}", self.total_perimeter),
),
None => crate::tr!(
"area-running-result-area-only",
"area", "running-result-area-only",
area = format!("{:.4}", measurement.area),
total_area = format!("{:.4}", self.total_area),
),
@@ -224,30 +224,30 @@ impl CadCommand for AreaCommand {
fn prompt(&self) -> String {
if self.objects_gathering {
return crate::tr!(
"area-prompt-objects",
"area", "prompt-objects",
count = self.selected_entities.len()
);
}
if !self.points.is_empty() {
return crate::tr!("area-prompt-next", count = self.points.len());
return crate::tr!("area", "prompt-next", count = self.points.len());
}
match self.mode {
AreaMode::Single => crate::tr!("area-prompt-first"),
AreaMode::Add => crate::tr!("area-prompt-add"),
AreaMode::Subtract => crate::tr!("area-prompt-subtract"),
AreaMode::Single => crate::tr!("area", "prompt-first"),
AreaMode::Add => crate::tr!("area", "prompt-add"),
AreaMode::Subtract => crate::tr!("area", "prompt-subtract"),
}
}
fn options(&self) -> Vec<CmdOption> {
if self.objects_gathering {
return vec![Self::option(crate::tr!("area-option-back"), "BACK")];
return vec![Self::option(crate::tr!("area", "option-back"), "BACK")];
}
if !self.points.is_empty() {
return Vec::new();
}
let objects = || Self::option(crate::tr!("area-option-objects"), "OBJECTS");
let add = || Self::option(crate::tr!("area-option-add"), "ADD");
let subtract = || Self::option(crate::tr!("area-option-subtract"), "SUBTRACT");
let objects = || Self::option(crate::tr!("area", "option-objects"), "OBJECTS");
let add = || Self::option(crate::tr!("area", "option-add"), "ADD");
let subtract = || Self::option(crate::tr!("area", "option-subtract"), "SUBTRACT");
match self.mode {
AreaMode::Single => vec![objects(), add(), subtract()],
AreaMode::Add => vec![objects(), subtract()],
@@ -323,7 +323,7 @@ impl CadCommand for AreaCommand {
let Some((measurement, handles)) = self.selection_measurement() else {
self.selected_entities.clear();
return CmdResult::ReportMeasurementAndDeselect(crate::tr!(
"area-objects-not-measurable"
"area", "objects-not-measurable"
));
};
if self.mode != AreaMode::Single {
+4 -4
View File
@@ -61,24 +61,24 @@ impl CadCommand for CopyCommand {
fn prompt(&self) -> String {
if self.awaiting_count {
return crate::tr!("command-copy-array-count");
return crate::tr!("command-copy", "array-count");
}
match &self.step {
Step::Base => crate::tr!(
"command-copy-base",
"command-copy", "base",
count = (self.handles.len() as i64),
),
Step::Placing(base) => {
if let Some(n) = self.array_count {
crate::tr!(
"command-copy-array-target",
"command-copy", "array-target",
count = (n as i64),
x = format!("{:.3}", base.x),
y = format!("{:.3}", base.y),
)
} else {
crate::tr!(
"command-copy-target",
"command-copy", "target",
count = (self.count as i64),
x = format!("{:.3}", base.x),
y = format!("{:.3}", base.y),
+1 -30
View File
@@ -15,7 +15,7 @@ use acadrust::kernel::geom2d::{self, Ellipse};
/// Re-exported unchanged: these already speak in plain `f64`, so there is no
/// call-shape difference for this module to absorb.
pub use acadrust::kernel::geom2d::{angle_within_arc, arc_parameter, lerp, normalize_angle};
pub use acadrust::kernel::geom2d::{arc_parameter, lerp, normalize_angle};
/// Preview geometry keeps the density the commands have always used.
const SEGMENTS_PER_RADIAN: f64 = geom2d::DEFAULT_SEGMENTS_PER_RADIAN;
@@ -51,35 +51,6 @@ pub fn line_circle(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64)
geom2d::line_circle([px, py], [dx, dy], [cx, cy], r)
}
/// `(s_on_line, t_on_ellipse)` pairs where a line meets an ellipse.
///
/// `nx, ny` is the unit major axis.
#[allow(clippy::too_many_arguments)]
pub fn line_ellipse(
px: f64,
py: f64,
dx: f64,
dy: f64,
cx: f64,
cy: f64,
a: f64,
b: f64,
nx: f64,
ny: f64,
) -> Vec<(f64, f64)> {
geom2d::line_ellipse(
[px, py],
[dx, dy],
&Ellipse {
centre: [cx, cy],
major_radius: a,
minor_radius: b,
major_axis: [nx, ny],
},
)
}
/// The two endpoints of a straight segment, as render vertices.
pub fn line_points(start: [f64; 3], end: [f64; 3]) -> Vec<[f32; 3]> {
narrow(vec![start, end])
}
+2 -2
View File
@@ -54,11 +54,11 @@ impl CadCommand for MoveCommand {
fn prompt(&self) -> String {
match &self.step {
Step::Base => crate::tr!(
"command-move-base",
"command-move", "base",
count = (self.handles.len() as i64),
),
Step::Target(base) => crate::tr!(
"command-move-target",
"command-move", "target",
x = format!("{:.3}", base.x),
y = format!("{:.3}", base.y),
),
+49 -294
View File
@@ -14,9 +14,8 @@ use std::f64::consts::TAU;
// its call shapes to the loose scalars and f32 render vertices used here.
use super::geom;
use super::geom::{
angle_within_arc as in_arc, arc_parameter as arc_t, arc_points as arc_pts,
ellipse_points as ellipse_pts, lerp as lerp2,
line_circle as lc, line_ellipse as le, line_line as ll, normalize_angle as norm,
arc_parameter as arc_t, arc_points as arc_pts, ellipse_points as ellipse_pts,
lerp as lerp2, normalize_angle as norm,
};
use crate::modules::draw::fence::{crossing_box_preview, FencePick};
@@ -131,14 +130,9 @@ enum Geo {
t1: f64, // end parameter (may be > 2π if wrapped)
},
/// A NURBS boundary, carried exactly.
///
/// `segs` is a sampling of the same curve, kept only for the two
/// hand-rolled ray walks in the polyline-extend paths. Every crossing
/// that decides a trim uses `curve`.
Spline {
handle: Handle,
curve: Box<NurbsCurve>,
segs: Vec<([f64; 2], [f64; 2])>,
},
}
@@ -250,17 +244,10 @@ fn geo_from_entity(h: Handle, e: &EntityType) -> Option<Geo> {
t1,
})
}
Curve::Nurbs(curve) => {
// Carried exactly. The sampled polyline beside it is only for the
// two hand-rolled ray walks in the polyline-extend paths; every
// crossing that decides a trim goes through the curve itself.
let sampled = curve.tessellate(16);
(sampled.len() >= 2).then(|| Geo::Spline {
handle: h,
curve: Box::new(curve),
segs: sampled.windows(2).map(|w| (w[0], w[1])).collect(),
})
}
Curve::Nurbs(curve) => Some(Geo::Spline {
handle: h,
curve: Box::new(curve),
}),
// A polyline reaches here already taken apart into its segments.
Curve::Polyline(_) => None,
}
@@ -987,6 +974,47 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<
/// Extend the first or last segment of an LwPolyline to the nearest boundary.
/// Click point (DXF XY) determines which end to extend.
/// The nearest boundary crossing past one end of a segment.
///
/// `t` runs on the infinite line through `from` → `to`: zero at `from`, one
/// at `to`. Extending the far end wants the smallest `t` above one; extending
/// the near end wants the largest `t` below zero. `None` when nothing lies
/// that way.
///
/// The crossings come from `cut_params`, which is the same kernel call every
/// other cut in this file uses. What was here instead was a match over every
/// `Geo` variant with its own line-line, line-circle and line-ellipse test —
/// twice, once for a line and once for a polyline's terminal segment — and a
/// boundary spline was walked as sixty-four chords rather than intersected.
fn extension_hit(
from: [f64; 2],
to: [f64; 2],
handle: Handle,
geos: &[Geo],
beyond_end: bool,
) -> Option<f64> {
let direction = [to[0] - from[0], to[1] - from[1]];
if direction[0].hypot(direction[1]) < 1e-9 {
return None;
}
// An infinite line rather than the segment: the crossing being looked for
// is by definition off the end of the drawn part.
let ray = Curve::XLine(KernelXLine {
base: from,
direction,
});
let hits = cut_params(&ray, handle, geos);
if beyond_end {
hits.into_iter()
.filter(|t| *t > 1.0 + 1e-6)
.min_by(f64::total_cmp)
} else {
hits.into_iter()
.filter(|t| *t < -1e-6)
.max_by(f64::total_cmp)
}
}
fn extend_lwpoly(
poly: &LwPolyline,
click_x: f64,
@@ -1030,124 +1058,7 @@ fn extend_lwpoly(
return None;
}
// t_click on the segment: 0 = ax/ay, 1 = bx/by. We're extending beyond t=1.
let target = poly.common.handle;
let mut best_t = f64::INFINITY;
for geo in geos {
match geo {
Geo::Line { handle, p1, p2 } => {
if *handle == target {
continue;
}
let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]);
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if (-1e-9..=1.0 + 1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Arc {
handle,
cx,
cy,
r,
a0,
a1,
} => {
if *handle == target {
continue;
}
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
let ix = ax + t * dx;
let iy = ay + t * dy;
if in_arc((iy - cy).atan2(ix - cx), *a0, *a1) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Circle { handle, cx, cy, r } => {
if *handle == target {
continue;
}
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
if t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Ray {
handle,
bx: rbx,
by: rby,
dx: rdx,
dy: rdy,
} => {
if *handle == target {
continue;
}
if let Some((t, u)) = ll(ax, ay, dx, dy, *rbx, *rby, *rdx, *rdy) {
if u >= -1e-9 && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::InfLine {
handle,
bx: ibx,
by: iby,
dx: idx,
dy: idy,
} => {
if *handle == target {
continue;
}
if let Some((t, _)) = ll(ax, ay, dx, dy, *ibx, *iby, *idx, *idy) {
if t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Ellipse {
handle,
cx,
cy,
a,
b,
nx,
ny,
t0,
t1,
} => {
if *handle == target {
continue;
}
for (t, t_ell) in le(ax, ay, dx, dy, *cx, *cy, *a, *b, *nx, *ny) {
if in_arc(t_ell, *t0, *t1) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Spline { handle, segs, .. } => {
if *handle == target {
continue;
}
for (p1, p2) in segs {
let ex = p2[0] - p1[0];
let ey = p2[1] - p1[1];
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if (-1e-9..=1.0 + 1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
}
}
}
if !best_t.is_finite() {
return None;
}
let best_t = extension_hit([ax, ay], [bx, by], poly.common.handle, geos, true)?;
let new_x = ax + best_t * dx;
let new_y = ay + best_t * dy;
@@ -1173,165 +1084,9 @@ fn extend_line(orig: &LineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType>
let bx = orig.end.x;
let by = orig.end.y;
let (dx, dy) = (bx - ax, by - ay);
let target = orig.common.handle;
let extend_end = t_click >= 0.5;
let best_t = extension_hit([ax, ay], [bx, by], orig.common.handle, geos, extend_end)?;
let mut best_t = if extend_end {
f64::INFINITY
} else {
f64::NEG_INFINITY
};
for geo in geos {
match geo {
Geo::Line { handle, p1, p2 } => {
if *handle == target {
continue;
}
let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]);
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if !(-1e-9..=1.0 + 1e-9).contains(&u) {
continue;
}
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
Geo::Arc {
handle,
cx,
cy,
r,
a0,
a1,
} => {
if *handle == target {
continue;
}
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
let ix = ax + t * dx;
let iy = ay + t * dy;
if !in_arc((iy - cy).atan2(ix - cx), *a0, *a1) {
continue;
}
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
Geo::Circle { handle, cx, cy, r } => {
if *handle == target {
continue;
}
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
Geo::Ray {
handle,
bx: rbx,
by: rby,
dx: rdx,
dy: rdy,
} => {
if *handle == target {
continue;
}
if let Some((t, u)) = ll(ax, ay, dx, dy, *rbx, *rby, *rdx, *rdy) {
if u >= -1e-9 {
// only forward along the Ray
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
}
Geo::InfLine {
handle,
bx: ibx,
by: iby,
dx: idx,
dy: idy,
} => {
if *handle == target {
continue;
}
if let Some((t, _u)) = ll(ax, ay, dx, dy, *ibx, *iby, *idx, *idy) {
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
Geo::Ellipse {
handle,
cx: ecx,
cy: ecy,
a,
b,
nx,
ny,
t0: et0,
t1: et1,
} => {
if *handle == target {
continue;
}
for (t, t_ell) in le(ax, ay, dx, dy, *ecx, *ecy, *a, *b, *nx, *ny) {
if !in_arc(t_ell, *et0, *et1) {
continue;
}
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
Geo::Spline { handle, segs, .. } => {
if *handle == target {
continue;
}
for (p1, p2) in segs {
let ex = p2[0] - p1[0];
let ey = p2[1] - p1[1];
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if !(-1e-9..=1.0 + 1e-9).contains(&u) {
continue;
}
if extend_end && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
if !extend_end && t < -1e-6 && t > best_t {
best_t = t;
}
}
}
}
}
}
if !best_t.is_finite() {
return None;
}
let mut line = orig.clone();
line.common.handle = Handle::NULL;
let new_x = ax + best_t * dx;
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/recap.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "RECAP", label: "Autodesk\nReCap", icon: ICON, event: ModuleEvent::Command("RECAP".to_string()) }
ToolDef { id: "RECAP", label: "Point\nClouds", icon: ICON, event: ModuleEvent::Command("RECAP".to_string()) }
}
+41 -33
View File
@@ -1,12 +1,24 @@
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
use std::sync::Arc;
//
// The boundary is a closed polygon in world XY coordinates.
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
// no line geometry is ever tessellated on the CPU.
use std::sync::Arc;
use acadrust::kernel::geom2d::{
inside_spans, Curve as KernelCurve, Line as KernelLine, Tolerance, XLine as KernelXLine,
};
pub const MAX_HATCH_BOUNDARY_VERTS: usize = 1024;
/// How near two crossings along a pattern line must be to count as one.
///
/// The boundary reaching here is already a chord approximation of the drawn
/// curves, so this only has to be finer than that sampling and coarser than
/// the rounding in it.
const CLIP_TOLERANCE_LINEAR: f64 = 1.0e-6;
/// One line family from a PAT-format hatch pattern.
///
/// Format mirrors the standard PAT line format:
@@ -269,6 +281,13 @@ impl HatchModel {
if edges.is_empty() {
return Vec::new();
}
// The boundary as kernel curves, built once for every pattern line to
// be clipped against.
let clip_tolerance = Tolerance::new(CLIP_TOLERANCE_LINEAR);
let boundary: Vec<KernelCurve> = edges
.iter()
.map(|(a, b)| KernelCurve::Line(KernelLine { start: *a, end: *b }))
.collect();
// ── AABB of the boundary in world coords.
let mut min_x = f64::INFINITY;
@@ -363,39 +382,28 @@ impl HatchModel {
let lx = origin[0] + kf * step_x;
let ly = origin[1] + kf * step_y;
// Intersect line P(t) = L + t·(cos_a, sin_a) against each
// boundary edge; collect t-values where the edge actually
// crosses (s ∈ [0,1]).
let mut ts: Vec<f64> = Vec::with_capacity(8);
for &(a, b) in &edges {
let ex = b[0] - a[0];
let ey = b[1] - a[1];
let det = ex * sin_a - ey * cos_a; // = sin_a·ex − cos_a·ey
if det.abs() < 1e-9 {
continue;
}
let rx = a[0] - lx;
let ry = a[1] - ly;
let t = (ex * ry - ey * rx) / det;
let s = (cos_a * ry - sin_a * rx) / det;
if s >= 0.0 && s <= 1.0 {
ts.push(t);
}
}
if ts.len() < 2 {
continue;
}
ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
// De-duplicate near-coincident hits (line clipping a vertex).
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-5);
if ts.len() < 2 {
continue;
}
// The stretches of this pattern line that fall inside the
// boundary. The direction is a unit vector, so the kernel's
// parameter is distance along the line — the same `t` the
// dash walk below steps in.
//
// Each span is tested for containment rather than taken from
// alternating crossings. A line that grazes a corner crosses
// twice in one place, and an alternating walk inverts from
// there on: the fill lands in the holes and the solid comes
// out empty.
let spans = inside_spans(
&boundary,
&KernelCurve::XLine(KernelXLine {
base: [lx, ly],
direction: [cos_a, sin_a],
}),
clip_tolerance,
);
// Even-odd: emit segments between consecutive pairs.
for pair in ts.chunks_exact(2) {
let t0 = pair[0];
let t1 = pair[1];
for span in spans {
let t0 = span[0];
let t1 = span[1];
if t1 - t0 < 1e-6 {
continue;
}
+36 -68
View File
@@ -883,7 +883,7 @@ impl Snapper {
// rotated or isometric grid the user sees.
grid_origin: Vec3,
grid_axes: (Vec3, Vec3, Vec3),
construction_cursor: Option<glam::DVec3>,
construction_ray: Option<(glam::DVec3, glam::DVec3)>,
) -> Option<SnapResult> {
// Object-snap selection is priority-then-distance, NOT nearest-wins.
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
@@ -1232,42 +1232,45 @@ impl Snapper {
}
}
// ── Intersection with active construction segment (#704) ──────────
//
// The existing Intersection pass below only compares document
// entities with each other. Also compare the rubber-band currently
// being drawn against existing geometry.
if self.is_on(SnapType::Intersection) {
if let (Some(from), Some(to)) = (self.from_point, construction_cursor) {
let from = from.as_dvec3();
if (to - from).length_squared() > 1e-18 {
// Active/deferred Intersection uses the actual nearby wires directly.
// The interaction candidate set may contain wires while its optional
// segment index is empty.
for wire in wires.iter() {
if !wire_in_range(wire) {
continue;
let mut try_ray_intersections = |origin: glam::DVec3, through: glam::DVec3| {
if (through - origin).length_squared() <= 1e-18 {
return;
}
if let Some(segments) = &local_segments {
for segment in segments {
if let Some(point) =
ray_segment_intersect_3d(origin, through, segment.a, segment.b)
{
if (point - origin).length_squared() > 1e-18 {
try_pt(point, SnapType::Intersection);
}
for index in 0..wire.points.len().saturating_sub(1) {
let b0 = wp_f64(wire, index);
let b1 = wp_f64(wire, index + 1);
if let Some(pt) = ray_segment_intersect_3d(from, to, b0, b1) {
if (pt - from).length_squared() > 1e-18 {
try_pt(pt, SnapType::Intersection);
}
}
}
} else {
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
for index in 0..wire.points.len().saturating_sub(1) {
if let Some(point) = ray_segment_intersect_3d(
origin,
through,
wp_f64(wire, index),
wp_f64(wire, index + 1),
) {
if (point - origin).length_squared() > 1e-18 {
try_pt(point, SnapType::Intersection);
}
}
}
}
}
};
if self.is_on(SnapType::Intersection) {
if let Some((origin, through)) = construction_ray {
try_ray_intersections(origin, through);
}
}
// ── Intersection with active perpendicular OTRACK ray (#704) ──────
//
// A perpendicular tracking ray is not part of the command's ordinary
// ORTHO/POLAR construction cursor. When the cursor is actually tracking
// one of those rays, allow Intersection to stop it on nearby geometry.
// Check engaged perpendicular tracking rays against nearby geometry.
if self.is_on(SnapType::Intersection)
&& self.otrack_enabled
&& self.is_on(SnapType::Perpendicular)
@@ -1278,50 +1281,23 @@ impl Snapper {
};
for &dir in perp_dirs {
// Project the cursor onto this perpendicular tracking line.
let rel = cursor_world - origin;
let t = rel.x * dir.x + rel.y * dir.y;
let aligned = glam::DVec3::new(
origin.x + dir.x * t,
origin.y + dir.y * t,
origin.z,
);
// Only consider the ray when the cursor is genuinely tracking it.
// This prevents Intersection from behaving like Nearest.
let aligned_screen = world_to_screen(aligned, view_rot, eye, bounds);
if dist2(aligned_screen, cursor_screen) > radius2 {
continue;
}
// Perpendicular tracking is an infinite line, but once the cursor
// chooses a side we only want intersections forward on that side.
let dir_to_cursor = if t >= 0.0 { dir } else { -dir };
let through = origin + dir_to_cursor;
for wire in wires.iter() {
if !wire_in_range(wire) {
continue;
}
for index in 0..wire.points.len().saturating_sub(1) {
let b0 = wp_f64(wire, index);
let b1 = wp_f64(wire, index + 1);
if let Some(pt) =
ray_segment_intersect_3d(origin, through, b0, b1)
{
// Do not re-offer the tracking origin itself.
if (pt - origin).length_squared() > 1e-18 {
try_pt(pt, SnapType::Intersection);
}
}
}
}
try_ray_intersections(origin, origin + dir_to_cursor);
}
}
}
drop(try_ray_intersections);
// ── Intersection — segment-segment intersections (pairwise, gated) ──
if self.is_on(SnapType::Intersection)
@@ -2118,13 +2094,7 @@ fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<gla
))
}
/// Intersection between a forward construction ray and an existing segment.
///
/// The ray starts at `ray_origin` and points toward `ray_through`, but extends
/// indefinitely forward. The existing geometry remains a finite segment.
///
/// Like `seg_intersect_3d`, this is a true 3D intersection: the XY projections
/// must cross and both geometries must have the same Z at the crossing.
/// True 3D intersection of a forward ray and a finite segment.
fn ray_segment_intersect_3d(
ray_origin: glam::DVec3,
ray_through: glam::DVec3,
@@ -2147,8 +2117,6 @@ fn ray_segment_intersect_3d(
let t = (ex * d2y - ey * d2x) / cross;
let s = (ex * d1y - ey * d1x) / cross;
// `t >= 0`: only forward along the construction ray.
// `s` must remain inside the existing finite segment.
if t < 0.0 || s < 0.0 || s > 1.0 {
return None;
}
+4 -4
View File
@@ -183,8 +183,8 @@ impl CommandLine {
pub fn new() -> Self {
let mut cl = Self::default();
cl.history_height = HISTORY_HEIGHT_DEFAULT;
cl.push_info(&crate::tr!("command-line-ready"));
cl.push_info(&crate::tr!("command-line-hint"));
cl.push_info(&crate::tr!("command-line", "ready"));
cl.push_info(&crate::tr!("command-line", "hint"));
cl
}
@@ -522,7 +522,7 @@ impl CommandLine {
}
});
let prompt = container(
text(crate::tr!("command-line-label")).size(11).style(|theme: &Theme| iced::widget::text::Style {
text(crate::tr!("command-line", "label")).size(11).style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().success.base.color),
}),
)
@@ -559,7 +559,7 @@ impl CommandLine {
..Default::default()
}
});
let literal_tip = container(text(crate::tr!("command-line-literal-spaces")).size(11))
let literal_tip = container(text(crate::tr!("command-line", "literal-spaces")).size(11))
.padding([3, 6])
.style(container::bordered_box);
let literal_btn = tooltip(literal_btn, literal_tip, tooltip::Position::Top).gap(4);
+4 -7
View File
@@ -2193,7 +2193,7 @@ struct DynInputCanvas {
boxes: Vec<DynBox>,
/// The active command's current prompt, drawn just above the boxes.
prompt: String,
/// Informational tracking/reference mode currently engaged.
/// Active tracking-reference label.
tracking_hint: Option<String>,
}
@@ -2543,9 +2543,7 @@ impl DynInputCanvas {
};
Self::draw_box(frame, b, center, bounds, theme);
}
// Guided dynamic-input layouts place their editable boxes on the
// construction geometry, so keep the tracking-reference pill near
// the crosshair instead.
// Keep the tracking hint near the crosshair in guided layouts.
if let Some(text) = self.tracking_hint.as_deref() {
let hw = (text.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0;
@@ -2636,12 +2634,11 @@ impl DynInputCanvas {
});
x += w + DYN_GAP;
}
// Informational tracking reference, using the same pill style as
// the command prompt. Keep it below the editable value row.
// Place the tracking hint below the value row.
if self.tracking_hint.is_some() {
let mut hy = by + DYN_BOX_H + 3.0;
// If there is no room below, place it above the prompt/value block.
// Move above when bottom space is insufficient.
if hy + DYN_BOX_H > bounds.height {
hy = (py - DYN_BOX_H - 3.0).max(0.0);
}
+1 -1
View File
@@ -106,7 +106,7 @@ pub fn view_window<'a>(
);
}
let close = button(text(crate::tr!("action-close")).size(12))
let close = button(text(crate::tr!("action", "close")).size(12))
.on_press(Message::CloseModal)
.padding([6, 18])
.style(button::secondary);
+15 -15
View File
@@ -91,12 +91,12 @@ pub fn view_window<'a>(
let palette = ui_theme.palette.to_iced();
let colors = [
(crate::tr!("options-color-background"), palette.background),
(crate::tr!("options-color-text"), palette.text),
(crate::tr!("options-color-primary"), palette.primary),
(crate::tr!("options-color-success"), palette.success),
(crate::tr!("options-color-warning"), palette.warning),
(crate::tr!("options-color-danger"), palette.danger),
(crate::tr!("options", "color-background"), palette.background),
(crate::tr!("options", "color-text"), palette.text),
(crate::tr!("options", "color-primary"), palette.primary),
(crate::tr!("options", "color-success"), palette.success),
(crate::tr!("options", "color-warning"), palette.warning),
(crate::tr!("options", "color-danger"), palette.danger),
];
let mut color_controls = column![].spacing(8);
@@ -126,16 +126,16 @@ pub fn view_window<'a>(
);
}
let close = button(text(crate::tr!("action-close")).size(12))
let close = button(text(crate::tr!("action", "close")).size(12))
.on_press(Message::CloseModal)
.padding([6, 18])
.style(button::secondary);
let general = column![
text(crate::tr!("options-language-section")).size(15),
text(crate::tr!("options", "language-section")).size(15),
Space::new().height(10),
row![
text(crate::tr!("options-language-label")).size(12).width(150),
text(crate::tr!("options", "language-label")).size(12).width(150),
iced::widget::pick_list(
selected_language,
language_options,
@@ -147,10 +147,10 @@ pub fn view_window<'a>(
.spacing(12)
.align_y(iced::Center),
Space::new().height(22),
text(crate::tr!("options-open-save-section")).size(15),
text(crate::tr!("options", "open-save-section")).size(15),
Space::new().height(10),
row![
text(crate::tr!("options-default-save-format-label")).size(12).width(150),
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
iced::widget::pick_list(
selected_format,
crate::io::SAVE_FORMAT_OPTIONS,
@@ -162,7 +162,7 @@ pub fn view_window<'a>(
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options-default-save-format-help"))
text(crate::tr!("options", "default-save-format-help"))
.size(11)
.width(sizing.width),
Space::new().height(14),
@@ -204,10 +204,10 @@ pub fn view_window<'a>(
});
let display = column![
text(crate::tr!("options-theme-section")).size(15),
text(crate::tr!("options", "theme-section")).size(15),
Space::new().height(10),
row![
text(crate::tr!("options-theme-label")).size(12).width(150),
text(crate::tr!("options", "theme-label")).size(12).width(150),
iced::widget::pick_list(
selected_theme,
theme_options,
@@ -219,7 +219,7 @@ pub fn view_window<'a>(
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options-theme-help"))
text(crate::tr!("options", "theme-help"))
.size(11)
.width(sizing.width),
Space::new().height(12),
+20 -20
View File
@@ -49,32 +49,32 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
let recovered = report.status == RecoveryStatus::Recovered;
let heading = if recovered {
crate::tr!("recovery-opened-with-repairs")
crate::tr!("recovery", "opened-with-repairs")
} else {
crate::tr!("recovery-open-failed")
crate::tr!("recovery", "open-failed")
};
let description = if recovered {
crate::tr!("recovery-repaired-description")
crate::tr!("recovery", "repaired-description")
} else {
crate::tr!("recovery-failed-description")
crate::tr!("recovery", "failed-description")
};
let status = report.status;
let metrics = row![
metric(
crate::tr!("recovery-entities-checked"),
crate::tr!("recovery", "entities-checked"),
report.entities_scanned.to_string(),
),
metric(
crate::tr!("recovery-issues-found"),
crate::tr!("recovery", "issues-found"),
report.issues_found().to_string(),
),
metric(
crate::tr!("recovery-entities-removed"),
crate::tr!("recovery", "entities-removed"),
report.removed_total().to_string(),
),
metric(
crate::tr!("recovery-references-checked"),
crate::tr!("recovery", "references-checked"),
report.references_checked.to_string(),
),
]
@@ -86,7 +86,7 @@ pub fn view_window<'a>(
details = details.push(
text(format!(
"{}: {}",
crate::tr!("recovery-referenced-entities-removed"),
crate::tr!("recovery", "referenced-entities-removed"),
report.referenced_entities_removed
))
.size(11),
@@ -96,7 +96,7 @@ pub fn view_window<'a>(
details = details.push(
text(format!(
"{}: {}",
crate::tr!("recovery-references-unavailable"),
crate::tr!("recovery", "references-unavailable"),
report
.references_missing
.saturating_add(report.references_failed)
@@ -112,7 +112,7 @@ pub fn view_window<'a>(
}
if let Some(path) = &report.log_path {
details = details.push(
text(format!("{}: {}", crate::tr!("recovery-log-path"), path.display()))
text(format!("{}: {}", crate::tr!("recovery", "log-path"), path.display()))
.size(10)
.style(muted_style),
);
@@ -120,14 +120,14 @@ pub fn view_window<'a>(
details = details.push(
text(format!(
"{}: {error}",
crate::tr!("recovery-log-write-failed")
crate::tr!("recovery", "log-write-failed")
))
.size(10)
.style(status_style(RecoveryStatus::Failed)),
);
} else if cfg!(target_arch = "wasm32") {
details = details.push(
text(crate::tr!("recovery-log-download-ready"))
text(crate::tr!("recovery", "log-download-ready"))
.size(10)
.style(muted_style),
);
@@ -142,7 +142,7 @@ pub fn view_window<'a>(
let mut actions = row![Space::new().width(Fill)].spacing(8);
if recovered && report.save_as_required && allow_save_copy {
actions = actions.push(
button(text(crate::tr!("recovery-save-copy")).size(12))
button(text(crate::tr!("recovery", "save-copy")).size(12))
.on_press(Message::RecoverySaveAs)
.style(button::primary)
.padding([6, 14]),
@@ -150,14 +150,14 @@ pub fn view_window<'a>(
}
if report.log_path.is_some() || cfg!(target_arch = "wasm32") {
actions = actions.push(
button(text(crate::tr!("recovery-show-log")).size(12))
button(text(crate::tr!("recovery", "show-log")).size(12))
.on_press(Message::RecoveryShowLog)
.style(button::secondary)
.padding([6, 14]),
);
}
actions = actions.push(
button(text(crate::tr!("action-close")).size(12))
button(text(crate::tr!("action", "close")).size(12))
.on_press(Message::RecoveryClose)
.style(button::secondary)
.padding([6, 14]),
@@ -200,11 +200,11 @@ pub fn view_prompt<'a>(
};
let actions = row![
Space::new().width(content_width),
button(text(crate::tr!("recovery-decline")).size(12))
button(text(crate::tr!("recovery", "decline")).size(12))
.on_press(Message::RecoveryDecline)
.style(button::secondary)
.padding([6, 14]),
button(text(crate::tr!("recovery-attempt")).size(12))
button(text(crate::tr!("recovery", "attempt")).size(12))
.on_press(Message::RecoveryAttempt)
.style(button::primary)
.padding([6, 14]),
@@ -213,9 +213,9 @@ pub fn view_prompt<'a>(
container(
column![
text(crate::tr!("recovery-prompt-heading")).size(20),
text(crate::tr!("recovery", "prompt-heading")).size(20),
text(file_name).size(13),
text(crate::tr!("recovery-prompt-description"))
text(crate::tr!("recovery", "prompt-description"))
.size(11)
.style(muted_style),
container(scrollable(text(error).size(10)).height(content_height))