forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'upstream/main' into fix/716-perpendicular-tracking
This commit is contained in:
commit
af7613f1d8
38 files changed
+45393
-32358
No files matched your search
Generated
+19
-5
@@ -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
@@ -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"] }
|
||||
|
||||
+3505
-172
File diff suppressed because it is too large.
Load diff
+3490
-3431
File diff suppressed because it is too large.
Load diff
+3493
-3433
File diff suppressed because it is too large.
Load diff
+3493
-584
File diff suppressed because it is too large.
Load diff
+3491
-3432
File diff suppressed because it is too large.
Load diff
+3480
-3421
File diff suppressed because it is too large.
Load diff
+3481
-150
File diff suppressed because it is too large.
Load diff
+3492
-3433
File diff suppressed because it is too large.
Load diff
+3493
-160
File diff suppressed because it is too large.
Load diff
+3495
-3436
File diff suppressed because it is too large.
Load diff
+3414
-3355
File diff suppressed because it is too large.
Load diff
+3475
-3418
File diff suppressed because it is too large.
Load diff
+19
-10
@@ -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
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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()) }
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user