Merge pull request #854 from ramox81/codex/align-arc-behavior
This commit is contained in:
commit
a87ca09fa4
15 changed files with 628 additions and 651 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://github.com/ramox81/cadcodec.git?rev=6dcda1a#6dcda1adc5a99edb1e15fcdeb206c6d65d073828"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=dd4cd16#dd4cd16be0e0aec0e23b66d3d27311316883662a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f36bdf6#f36bdf6c967dcdb8d0e740869527090a569dae1d"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=35abd0f#35abd0fbabe224040bbe0e07055d6d172cc61c67"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f36bdf6", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "35abd0f", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
|
|
@ -37,7 +37,7 @@ serde_json = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ publish = false
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|
|
||||||
|
|
@ -764,7 +764,10 @@ impl OpenCADStudio {
|
||||||
// leave the anchor untouched. (#327)
|
// leave the anchor untouched. (#327)
|
||||||
if matches!(
|
if matches!(
|
||||||
entity,
|
entity,
|
||||||
acadrust::EntityType::Line(_) | acadrust::EntityType::Arc(_)
|
acadrust::EntityType::Line(_)
|
||||||
|
| acadrust::EntityType::Arc(_)
|
||||||
|
| acadrust::EntityType::LwPolyline(_)
|
||||||
|
| acadrust::EntityType::Polyline2D(_)
|
||||||
) {
|
) {
|
||||||
self.update_cont_anchor(&entity);
|
self.update_cont_anchor(&entity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,8 +300,8 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
"ARC" => {
|
"ARC" => {
|
||||||
use crate::modules::draw::draw::arc::ArcCommand;
|
use crate::modules::draw::draw::arc::Arc3PCommand;
|
||||||
let new_cmd = ArcCommand::new();
|
let new_cmd = Arc3PCommand::new();
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
@ -311,6 +311,12 @@ impl OpenCADStudio {
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
"ARC_CSE" => {
|
||||||
|
use crate::modules::draw::draw::arc::ArcCommand;
|
||||||
|
let new_cmd = ArcCommand::new();
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
"ARC_SCE" => {
|
"ARC_SCE" => {
|
||||||
use crate::modules::draw::draw::arc::ArcSCECommand;
|
use crate::modules::draw::draw::arc::ArcSCECommand;
|
||||||
let new_cmd = ArcSCECommand::new();
|
let new_cmd = ArcSCECommand::new();
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,8 @@ fn to_render(arc: &Arc) -> RenderEntity {
|
||||||
let ea = arc.end_angle;
|
let ea = arc.end_angle;
|
||||||
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
||||||
|
|
||||||
// Compute OCS basis vectors for this entity's normal.
|
|
||||||
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
||||||
|
|
||||||
// Arc centre in WCS.
|
|
||||||
let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs((cx, cy, cz), normal);
|
let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs((cx, cy, cz), normal);
|
||||||
|
|
||||||
// Arc points in WCS: centre_wcs + r*cos(a)*Ax + r*sin(a)*Ay
|
|
||||||
let arc_pt = |a: f64| {
|
let arc_pt = |a: f64| {
|
||||||
let (c, s) = (a.cos(), a.sin());
|
let (c, s) = (a.cos(), a.sin());
|
||||||
[
|
[
|
||||||
|
|
@ -43,9 +38,13 @@ fn to_render(arc: &Arc) -> RenderEntity {
|
||||||
// curves) deliberately emit no midpoint; see #34.
|
// curves) deliberately emit no midpoint; see #34.
|
||||||
let curve = crate::entities::curve::arc_curve(arc);
|
let curve = crate::entities::curve::arc_curve(arc);
|
||||||
let snap = crate::entities::curve::snap_from(&curve);
|
let snap = crate::entities::curve::snap_from(&curve);
|
||||||
let tangent = TangentGeom::Circle {
|
let tangent = TangentGeom::Arc {
|
||||||
center: [cwx as f32, cwy as f32, cwz as f32],
|
center: [cwx, cwy, cwz],
|
||||||
radius: r as f32,
|
axis_x: [ax.0, ax.1, ax.2],
|
||||||
|
axis_y: [ay.0, ay.1, ay.2],
|
||||||
|
radius: r,
|
||||||
|
start_angle: sa,
|
||||||
|
end_angle: ea,
|
||||||
};
|
};
|
||||||
|
|
||||||
if arc.thickness.abs() > 1e-10 {
|
if arc.thickness.abs() > 1e-10 {
|
||||||
|
|
@ -98,56 +97,14 @@ fn to_render(arc: &Arc) -> RenderEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_points(arc: &Arc) -> [glam::DVec3; 3] {
|
fn control_points(arc: &Arc) -> [glam::DVec3; 3] {
|
||||||
let center = glam::DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
let curve = crate::entities::curve::arc_curve(arc);
|
||||||
let sweep = (arc.end_angle - arc.start_angle).rem_euclid(TAU);
|
|
||||||
let point = |angle: f64| {
|
|
||||||
center
|
|
||||||
+ glam::DVec3::new(
|
|
||||||
arc.radius * angle.cos(),
|
|
||||||
arc.radius * angle.sin(),
|
|
||||||
0.0,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
[
|
[
|
||||||
point(arc.start_angle),
|
glam::DVec3::from_array(curve.point_at(0.0)),
|
||||||
point(arc.start_angle + sweep * 0.5),
|
glam::DVec3::from_array(curve.point_at(0.5)),
|
||||||
point(arc.end_angle),
|
glam::DVec3::from_array(curve.point_at(1.0)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn circumcircle(
|
|
||||||
a: glam::DVec3,
|
|
||||||
b: glam::DVec3,
|
|
||||||
c: glam::DVec3,
|
|
||||||
) -> Option<(glam::DVec3, f64)> {
|
|
||||||
// Work relative to the first point so large drawing coordinates do not
|
|
||||||
// lose the small differences that define the circle.
|
|
||||||
let ab = b - a;
|
|
||||||
let ac = c - a;
|
|
||||||
let bc = c - b;
|
|
||||||
let scale2 = ab
|
|
||||||
.length_squared()
|
|
||||||
.max(ac.length_squared())
|
|
||||||
.max(bc.length_squared());
|
|
||||||
if scale2 < 1.0e-18 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let det = 2.0 * (ab.x * ac.y - ab.y * ac.x);
|
|
||||||
if det.abs() <= scale2 * 1.0e-12 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let ab2 = ab.x * ab.x + ab.y * ab.y;
|
|
||||||
let ac2 = ac.x * ac.x + ac.y * ac.y;
|
|
||||||
let center = a
|
|
||||||
+ glam::DVec3::new(
|
|
||||||
(ab2 * ac.y - ac2 * ab.y) / det,
|
|
||||||
(ab.x * ac2 - ac.x * ab2) / det,
|
|
||||||
0.0,
|
|
||||||
);
|
|
||||||
let radius = center.distance(a);
|
|
||||||
radius.is_finite().then_some((center, radius))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn refit_grips(
|
pub(crate) fn refit_grips(
|
||||||
arc: &mut Arc,
|
arc: &mut Arc,
|
||||||
original: &Arc,
|
original: &Arc,
|
||||||
|
|
@ -169,36 +126,35 @@ pub(crate) fn refit_grips(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((center, radius)) = circumcircle(points[0], points[1], points[2]) else {
|
let plane = crate::entities::curve::arc_curve(original).plane;
|
||||||
|
let Some(a) = plane.project(points[0].to_array()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if radius <= 1.0e-9 {
|
let Some(b) = plane.project(points[1].to_array()) else {
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
let Some(c) = plane.project(points[2].to_array()) else {
|
||||||
let start = (points[0].y - center.y).atan2(points[0].x - center.x);
|
|
||||||
let middle = (points[1].y - center.y).atan2(points[1].x - center.x);
|
|
||||||
let end = (points[2].y - center.y).atan2(points[2].x - center.x);
|
|
||||||
let sweep = (end - start).rem_euclid(TAU);
|
|
||||||
let middle_sweep = (middle - start).rem_euclid(TAU);
|
|
||||||
// Crossing the two fixed points makes the three-point definition
|
|
||||||
// degenerate before it reverses. Keep the last valid preview instead of
|
|
||||||
// swapping the start and end grip identities under the cursor.
|
|
||||||
if sweep <= 1.0e-9 || middle_sweep > sweep + 1.0e-9 {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
let Some(fit) = cadkernel::geom2d::arc_through_points(a, b, c) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
arc.center.x = center.x;
|
arc.center.x = fit.centre[0];
|
||||||
arc.center.y = center.y;
|
arc.center.y = fit.centre[1];
|
||||||
arc.center.z = original.center.z;
|
arc.center.z = original.center.z;
|
||||||
arc.radius = radius;
|
arc.radius = fit.radius;
|
||||||
arc.start_angle = start;
|
arc.start_angle = fit.start_angle;
|
||||||
arc.end_angle = end;
|
arc.end_angle = fit.end_angle;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grips(arc: &Arc) -> Vec<GripDef> {
|
fn grips(arc: &Arc) -> Vec<GripDef> {
|
||||||
let ctr = glam::DVec3::new(arc.center.x, arc.center.y, arc.center.z);
|
let (x, y, z) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||||
|
(arc.center.x, arc.center.y, arc.center.z),
|
||||||
|
(arc.normal.x, arc.normal.y, arc.normal.z),
|
||||||
|
);
|
||||||
|
let ctr = glam::DVec3::new(x, y, z);
|
||||||
let [start, middle, end] = control_points(arc);
|
let [start, middle, end] = control_points(arc);
|
||||||
vec![
|
vec![
|
||||||
center_grip(0, ctr),
|
center_grip(0, ctr),
|
||||||
|
|
@ -215,7 +171,11 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
|
||||||
let sweep = (ea - sa).rem_euclid(TAU);
|
let sweep = (ea - sa).rem_euclid(TAU);
|
||||||
let total_angle = sweep.to_degrees();
|
let total_angle = sweep.to_degrees();
|
||||||
let arc_length = r * sweep;
|
let arc_length = r * sweep;
|
||||||
let area = 0.5 * r * r * sweep;
|
let area = crate::entities::curve::arc_curve(arc)
|
||||||
|
.curve
|
||||||
|
.chord_closed_area()
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
.abs();
|
||||||
|
|
||||||
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
||||||
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
||||||
|
|
@ -240,9 +200,9 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
|
||||||
ro(t!("Start X").as_ref(), "start_x", format!("{sx:.4}")),
|
ro(t!("Start X").as_ref(), "start_x", format!("{sx:.4}")),
|
||||||
ro(t!("Start Y").as_ref(), "start_y", format!("{sy:.4}")),
|
ro(t!("Start Y").as_ref(), "start_y", format!("{sy:.4}")),
|
||||||
ro(t!("Start Z").as_ref(), "start_z", format!("{sz:.4}")),
|
ro(t!("Start Z").as_ref(), "start_z", format!("{sz:.4}")),
|
||||||
edit(t!("Center X").as_ref(), "center_x", arc.center.x),
|
edit(t!("Center X").as_ref(), "center_x", cwx),
|
||||||
edit(t!("Center Y").as_ref(), "center_y", arc.center.y),
|
edit(t!("Center Y").as_ref(), "center_y", cwy),
|
||||||
edit(t!("Center Z").as_ref(), "center_z", arc.center.z),
|
edit(t!("Center Z").as_ref(), "center_z", cwz),
|
||||||
ro(t!("End X").as_ref(), "end_x", format!("{ex:.4}")),
|
ro(t!("End X").as_ref(), "end_x", format!("{ex:.4}")),
|
||||||
ro(t!("End Y").as_ref(), "end_y", format!("{ey:.4}")),
|
ro(t!("End Y").as_ref(), "end_y", format!("{ey:.4}")),
|
||||||
ro(t!("End Z").as_ref(), "end_z", format!("{ez:.4}")),
|
ro(t!("End Z").as_ref(), "end_z", format!("{ez:.4}")),
|
||||||
|
|
@ -264,9 +224,23 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"center_x" => arc.center.x = v,
|
"center_x" | "center_y" | "center_z" => {
|
||||||
"center_y" => arc.center.y = v,
|
let normal = (arc.normal.x, arc.normal.y, arc.normal.z);
|
||||||
"center_z" => arc.center.z = v,
|
let (mut x, mut y, mut z) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||||
|
(arc.center.x, arc.center.y, arc.center.z),
|
||||||
|
normal,
|
||||||
|
);
|
||||||
|
match field {
|
||||||
|
"center_x" => x = v,
|
||||||
|
"center_y" => y = v,
|
||||||
|
"center_z" => z = v,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let (ox, oy, oz) = crate::scene::view::transform::wcs_point_to_ocs((x, y, z), normal);
|
||||||
|
arc.center.x = ox;
|
||||||
|
arc.center.y = oy;
|
||||||
|
arc.center.z = oz;
|
||||||
|
}
|
||||||
"radius" if v > 0.0 => arc.radius = v,
|
"radius" if v > 0.0 => arc.radius = v,
|
||||||
"start_angle" => arc.start_angle = v.to_radians(),
|
"start_angle" => arc.start_angle = v.to_radians(),
|
||||||
"end_angle" => arc.end_angle = v.to_radians(),
|
"end_angle" => arc.end_angle = v.to_radians(),
|
||||||
|
|
@ -277,14 +251,22 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
|
||||||
fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) {
|
fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) {
|
||||||
match (grip_id, apply) {
|
match (grip_id, apply) {
|
||||||
(0, GripApply::Translate(d)) => {
|
(0, GripApply::Translate(d)) => {
|
||||||
arc.center.x += d.x;
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
arc.center.y += d.y;
|
(d.x, d.y, d.z),
|
||||||
arc.center.z += d.z;
|
(arc.normal.x, arc.normal.y, arc.normal.z),
|
||||||
|
);
|
||||||
|
arc.center.x += x;
|
||||||
|
arc.center.y += y;
|
||||||
|
arc.center.z += z;
|
||||||
}
|
}
|
||||||
(0, GripApply::Absolute(p)) => {
|
(0, GripApply::Absolute(p)) => {
|
||||||
arc.center.x = p[0];
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
arc.center.y = p[1];
|
(p[0], p[1], p[2]),
|
||||||
arc.center.z = p[2];
|
(arc.normal.x, arc.normal.y, arc.normal.z),
|
||||||
|
);
|
||||||
|
arc.center.x = x;
|
||||||
|
arc.center.y = y;
|
||||||
|
arc.center.z = z;
|
||||||
}
|
}
|
||||||
(1..=3, GripApply::Absolute(p)) => {
|
(1..=3, GripApply::Absolute(p)) => {
|
||||||
let original = arc.clone();
|
let original = arc.clone();
|
||||||
|
|
@ -386,7 +368,11 @@ impl crate::entities::traits::Grippable for Arc {
|
||||||
if !matches!(action, A::Lengthen) || self.radius <= 1.0e-9 {
|
if !matches!(action, A::Lengthen) || self.radius <= 1.0e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let cursor_angle = (point.y - self.center.y).atan2(point.x - self.center.x);
|
let (x, y, _) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(point.x, point.y, point.z),
|
||||||
|
(self.normal.x, self.normal.y, self.normal.z),
|
||||||
|
);
|
||||||
|
let cursor_angle = (y - self.center.y).atan2(x - self.center.x);
|
||||||
let current_sweep = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
let current_sweep = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
||||||
let desired_sweep = match grip_id {
|
let desired_sweep = match grip_id {
|
||||||
1 => (self.end_angle - cursor_angle).rem_euclid(TAU),
|
1 => (self.end_angle - cursor_angle).rem_euclid(TAU),
|
||||||
|
|
@ -412,7 +398,9 @@ impl crate::entities::traits::Grippable for Arc {
|
||||||
// Hold start_angle, derive new end_angle from arc length
|
// Hold start_angle, derive new end_angle from arc length
|
||||||
// = r * Δθ.
|
// = r * Δθ.
|
||||||
let new_span = value / self.radius;
|
let new_span = value / self.radius;
|
||||||
self.end_angle = self.start_angle + new_span;
|
if new_span < TAU - 1.0e-9 {
|
||||||
|
self.end_angle = self.start_angle + new_span;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
A::Lengthen => {
|
A::Lengthen => {
|
||||||
// Extend either end by `value` arc-length units along
|
// Extend either end by `value` arc-length units along
|
||||||
|
|
@ -422,6 +410,11 @@ impl crate::entities::traits::Grippable for Arc {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let dtheta = value / self.radius;
|
let dtheta = value / self.radius;
|
||||||
|
let current = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
||||||
|
let next = current + dtheta;
|
||||||
|
if next <= 1.0e-9 || next >= TAU - 1.0e-9 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
match grip_id {
|
match grip_id {
|
||||||
1 => self.start_angle -= dtheta,
|
1 => self.start_angle -= dtheta,
|
||||||
2 => self.end_angle += dtheta,
|
2 => self.end_angle += dtheta,
|
||||||
|
|
@ -450,26 +443,18 @@ impl crate::entities::traits::Transformable for Arc {
|
||||||
|
|
||||||
impl crate::entities::traits::MassPropsCalc for acadrust::entities::Arc {
|
impl crate::entities::traits::MassPropsCalc for acadrust::entities::Arc {
|
||||||
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
||||||
use std::f64::consts::TAU;
|
let curve = crate::entities::curve::arc_curve(self);
|
||||||
let r = self.radius;
|
let area = curve.curve.chord_closed_area().unwrap_or(0.0).abs();
|
||||||
let span = {
|
let centroid = curve
|
||||||
let s = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
.curve
|
||||||
if s < 1e-6 {
|
.chord_closed_centroid()
|
||||||
TAU
|
.map(|point| curve.plane.point_at(point))
|
||||||
} else {
|
.unwrap_or_else(|| curve.point_at(0.5));
|
||||||
s
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Sector area (pie slice)
|
|
||||||
let area = 0.5 * r * r * span;
|
|
||||||
let arc_len = r * span;
|
|
||||||
// Centroid of arc (chord midpoint direction)
|
|
||||||
let mid_rad = self.start_angle + span / 2.0;
|
|
||||||
crate::entities::traits::MassProps {
|
crate::entities::traits::MassProps {
|
||||||
area,
|
area,
|
||||||
perimeter: arc_len,
|
perimeter: curve.length(),
|
||||||
cx: self.center.x + r * mid_rad.cos(),
|
cx: centroid[0],
|
||||||
cy: self.center.y + r * mid_rad.sin(),
|
cy: centroid[1],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -79,7 +79,7 @@ impl AreaCommand {
|
||||||
let curve = entity_curve(entity)?;
|
let curve = entity_curve(entity)?;
|
||||||
let perimeter = curve.length();
|
let perimeter = curve.length();
|
||||||
Some(AreaMeasurement {
|
Some(AreaMeasurement {
|
||||||
area: curve.curve.enclosed_area().abs(),
|
area: curve.curve.chord_closed_area()?.abs(),
|
||||||
perimeter: perimeter.is_finite().then_some(perimeter),
|
perimeter: perimeter.is_finite().then_some(perimeter),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,15 @@ fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||||
let cy = arc.center.y;
|
let cy = arc.center.y;
|
||||||
let r = arc.radius;
|
let r = arc.radius;
|
||||||
|
|
||||||
// Project p1 and p2 onto the arc (world XY plane)
|
let plane = crate::entities::curve::arc_curve(arc).plane;
|
||||||
let a1 = angle_on_arc(cx, cy, p1);
|
let Some(q1) = plane.project(p1.to_array()) else {
|
||||||
let a2 = angle_on_arc(cx, cy, p2);
|
return vec![EntityType::Arc(arc.clone())];
|
||||||
|
};
|
||||||
|
let Some(q2) = plane.project(p2.to_array()) else {
|
||||||
|
return vec![EntityType::Arc(arc.clone())];
|
||||||
|
};
|
||||||
|
let a1 = angle_on_arc(cx, cy, DVec3::new(q1[0], q1[1], 0.0));
|
||||||
|
let a2 = angle_on_arc(cx, cy, DVec3::new(q2[0], q2[1], 0.0));
|
||||||
|
|
||||||
let start = arc.start_angle;
|
let start = arc.start_angle;
|
||||||
let end = arc.end_angle;
|
let end = arc.end_angle;
|
||||||
|
|
@ -107,8 +113,7 @@ fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||||
let a1_on = clamp_to_arc(a1, start, end);
|
let a1_on = clamp_to_arc(a1, start, end);
|
||||||
let a2_on = clamp_to_arc(a2, start, end);
|
let a2_on = clamp_to_arc(a2, start, end);
|
||||||
|
|
||||||
// Resulting arc: from a2_on to a1_on (CCW, skipping the removed segment)
|
// Keep the span outside the two break points.
|
||||||
// This matches AutoCAD's break behavior: removes CCW from first to second point.
|
|
||||||
if (a1_on - a2_on).abs() < 0.01 {
|
if (a1_on - a2_on).abs() < 0.01 {
|
||||||
// Single-point break: return original unchanged (no gap)
|
// Single-point break: return original unchanged (no gap)
|
||||||
return vec![EntityType::Arc(arc.clone())];
|
return vec![EntityType::Arc(arc.clone())];
|
||||||
|
|
|
||||||
|
|
@ -213,38 +213,23 @@ fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||||
let cx = arc.center.x as f32;
|
|
||||||
let cy = arc.center.y as f32;
|
|
||||||
|
|
||||||
// Current arc span
|
// Current arc span
|
||||||
let span = arc_span_rad(arc.start_angle, arc.end_angle);
|
let span = arc_span_rad(arc.start_angle, arc.end_angle);
|
||||||
let current_arc_len = arc.radius * span;
|
let current_arc_len = arc.radius * span;
|
||||||
|
|
||||||
let new_arc_len = apply_mode(current_arc_len, mode)?;
|
let new_arc_len = apply_mode(current_arc_len, mode)?;
|
||||||
if new_arc_len < 1e-10 {
|
if new_arc_len < 1e-10 || new_arc_len >= std::f64::consts::TAU * arc.radius - 1.0e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let new_span = new_arc_len / arc.radius;
|
let new_span = new_arc_len / arc.radius;
|
||||||
|
|
||||||
// Which end (start or end angle) is closer to pick?
|
// Which end (start or end angle) is closer to pick?
|
||||||
let start_rad = arc.start_angle;
|
let curve = crate::entities::curve::arc_curve(arc);
|
||||||
let end_rad = arc.end_angle;
|
let start_pt = Vec3::from_array(curve.point_at(0.0).map(|value| value as f32));
|
||||||
|
let end_pt = Vec3::from_array(curve.point_at(1.0).map(|value| value as f32));
|
||||||
let start_pt = Vec3::new(
|
|
||||||
cx + arc.radius as f32 * start_rad.cos() as f32,
|
|
||||||
pick_pt.y,
|
|
||||||
cy + arc.radius as f32 * start_rad.sin() as f32,
|
|
||||||
);
|
|
||||||
let end_pt = Vec3::new(
|
|
||||||
cx + arc.radius as f32 * end_rad.cos() as f32,
|
|
||||||
pick_pt.y,
|
|
||||||
cy + arc.radius as f32 * end_rad.sin() as f32,
|
|
||||||
);
|
|
||||||
let dist_start = (pick_pt - start_pt).length();
|
let dist_start = (pick_pt - start_pt).length();
|
||||||
let dist_end = (pick_pt - end_pt).length();
|
let dist_end = (pick_pt - end_pt).length();
|
||||||
|
|
||||||
let delta_span = new_span - span;
|
|
||||||
|
|
||||||
let mut result = arc.clone();
|
let mut result = arc.clone();
|
||||||
result.common.handle = Handle::NULL;
|
result.common.handle = Handle::NULL;
|
||||||
|
|
||||||
|
|
@ -255,7 +240,6 @@ fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityTyp
|
||||||
// Extend start angle (move start backwards)
|
// Extend start angle (move start backwards)
|
||||||
result.start_angle = arc.end_angle - new_span;
|
result.start_angle = arc.end_angle - new_span;
|
||||||
}
|
}
|
||||||
let _ = delta_span;
|
|
||||||
Some(EntityType::Arc(result))
|
Some(EntityType::Arc(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,8 @@ fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType>
|
||||||
// ── Arc offset ─────────────────────────────────────────────────────────────
|
// ── Arc offset ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn offset_arc(a: &ArcEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
fn offset_arc(a: &ArcEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||||
let px = side_pt.x as f64;
|
let plane = crate::entities::curve::arc_curve(a).plane;
|
||||||
let py = side_pt.y as f64;
|
let [px, py] = plane.project(side_pt.as_dvec3().to_array())?;
|
||||||
let dc = ((px - a.center.x).powi(2) + (py - a.center.y).powi(2)).sqrt();
|
let dc = ((px - a.center.x).powi(2) + (py - a.center.y).powi(2)).sqrt();
|
||||||
|
|
||||||
let new_r = if dc < a.radius {
|
let new_r = if dc < a.radius {
|
||||||
|
|
|
||||||
53
src/scene/cache/block_cache.rs
vendored
53
src/scene/cache/block_cache.rs
vendored
|
|
@ -1268,6 +1268,11 @@ fn translated_prototype_wire(
|
||||||
center[axis] += delta_f32[axis];
|
center[axis] += delta_f32[axis];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TangentGeom::Arc { center, .. } => {
|
||||||
|
for axis in 0..3 {
|
||||||
|
center[axis] += delta[axis];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !wire.text_verts.is_empty() {
|
if !wire.text_verts.is_empty() {
|
||||||
|
|
@ -2194,9 +2199,9 @@ fn emit_wire(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for tg in &lw.tangent_geoms {
|
for tg in &lw.tangent_geoms {
|
||||||
entry
|
if let Some(tangent) = transform_tangent(tg, accum_xform) {
|
||||||
.tangent_geoms
|
entry.tangent_geoms.push(tangent);
|
||||||
.push(transform_tangent(tg, accum_xform));
|
}
|
||||||
}
|
}
|
||||||
// Per the WireModel contract an empty `fill_tris_low` means "all-zero low
|
// Per the WireModel contract an empty `fill_tris_low` means "all-zero low
|
||||||
// half" (e.g. a Leader / dimension arrowhead fill, which the tessellator
|
// half" (e.g. a Leader / dimension arrowhead fill, which the tessellator
|
||||||
|
|
@ -2299,7 +2304,7 @@ fn emit_wire(
|
||||||
fn transform_tangent(
|
fn transform_tangent(
|
||||||
tg: &TangentGeom,
|
tg: &TangentGeom,
|
||||||
t: &Transform,
|
t: &Transform,
|
||||||
) -> TangentGeom {
|
) -> Option<TangentGeom> {
|
||||||
match tg {
|
match tg {
|
||||||
TangentGeom::Line { p1, p2 } => {
|
TangentGeom::Line { p1, p2 } => {
|
||||||
let q1 = t.apply(Vector3::new(
|
let q1 = t.apply(Vector3::new(
|
||||||
|
|
@ -2312,10 +2317,10 @@ fn transform_tangent(
|
||||||
p2[1] as f64,
|
p2[1] as f64,
|
||||||
p2[2] as f64,
|
p2[2] as f64,
|
||||||
));
|
));
|
||||||
TangentGeom::Line {
|
Some(TangentGeom::Line {
|
||||||
p1: [(q1.x) as f32, (q1.y) as f32, (q1.z) as f32],
|
p1: [(q1.x) as f32, (q1.y) as f32, (q1.z) as f32],
|
||||||
p2: [(q2.x) as f32, (q2.y) as f32, (q2.z) as f32],
|
p2: [(q2.x) as f32, (q2.y) as f32, (q2.z) as f32],
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
TangentGeom::Circle { center, radius } => {
|
TangentGeom::Circle { center, radius } => {
|
||||||
let c = t.apply(Vector3::new(
|
let c = t.apply(Vector3::new(
|
||||||
|
|
@ -2327,10 +2332,44 @@ fn transform_tangent(
|
||||||
let sx = ((m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) as f64).sqrt();
|
let sx = ((m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) as f64).sqrt();
|
||||||
let sy = ((m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]) as f64).sqrt();
|
let sy = ((m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]) as f64).sqrt();
|
||||||
let s = ((sx + sy) * 0.5) as f32;
|
let s = ((sx + sy) * 0.5) as f32;
|
||||||
TangentGeom::Circle {
|
Some(TangentGeom::Circle {
|
||||||
center: [(c.x) as f32, (c.y) as f32, (c.z) as f32],
|
center: [(c.x) as f32, (c.y) as f32, (c.z) as f32],
|
||||||
radius: radius * s,
|
radius: radius * s,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TangentGeom::Arc {
|
||||||
|
center,
|
||||||
|
axis_x,
|
||||||
|
axis_y,
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
} => {
|
||||||
|
let c = t.apply(Vector3::new(center[0], center[1], center[2]));
|
||||||
|
let x = t.apply_rotation(Vector3::new(axis_x[0], axis_x[1], axis_x[2]));
|
||||||
|
let y = t.apply_rotation(Vector3::new(axis_y[0], axis_y[1], axis_y[2]));
|
||||||
|
let sx = x.length();
|
||||||
|
let sy = y.length();
|
||||||
|
let scale = sx.max(sy);
|
||||||
|
if !scale.is_finite()
|
||||||
|
|| scale <= 1.0e-12
|
||||||
|
|| (sx - sy).abs() > scale * 1.0e-9
|
||||||
|
{
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
let x = x / sx;
|
||||||
|
let y = y / sy;
|
||||||
|
if x.dot(&y).abs() > 1.0e-9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(TangentGeom::Arc {
|
||||||
|
center: [c.x, c.y, c.z],
|
||||||
|
axis_x: [x.x, x.y, x.z],
|
||||||
|
axis_y: [y.x, y.y, y.z],
|
||||||
|
radius: radius * ((sx + sy) * 0.5),
|
||||||
|
start_angle: *start_angle,
|
||||||
|
end_angle: *end_angle,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,17 @@ pub enum SnapHint {
|
||||||
pub enum TangentGeom {
|
pub enum TangentGeom {
|
||||||
/// Infinite line through these two world-space points.
|
/// Infinite line through these two world-space points.
|
||||||
Line { p1: [f32; 3], p2: [f32; 3] },
|
Line { p1: [f32; 3], p2: [f32; 3] },
|
||||||
/// Circle/arc.
|
/// Complete circle.
|
||||||
Circle { center: [f32; 3], radius: f32 },
|
Circle { center: [f32; 3], radius: f32 },
|
||||||
|
/// Bounded circular arc in its world-space plane.
|
||||||
|
Arc {
|
||||||
|
center: [f64; 3],
|
||||||
|
axis_x: [f64; 3],
|
||||||
|
axis_y: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
start_angle: f64,
|
||||||
|
end_angle: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A 1-D entity (line, arc, polyline) represented as an ordered set of
|
/// A 1-D entity (line, arc, polyline) represented as an ordered set of
|
||||||
|
|
|
||||||
172
src/snap.rs
172
src/snap.rs
|
|
@ -1607,7 +1607,7 @@ impl Snapper {
|
||||||
let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
|
let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
|
||||||
let t = t_on_segment(cursor_screen, sp0, sp1);
|
let t = t_on_segment(cursor_screen, sp0, sp1);
|
||||||
let w = Vec3::from(*p1) + t * (Vec3::from(*p2) - Vec3::from(*p1));
|
let w = Vec3::from(*p1) + t * (Vec3::from(*p2) - Vec3::from(*p1));
|
||||||
(w, d2)
|
(w.as_dvec3(), d2)
|
||||||
}
|
}
|
||||||
TangentGeom::Circle { center, radius } => {
|
TangentGeom::Circle { center, radius } => {
|
||||||
let cv = Vec3::from(*center);
|
let cv = Vec3::from(*center);
|
||||||
|
|
@ -1621,52 +1621,83 @@ impl Snapper {
|
||||||
);
|
);
|
||||||
let sr = dist2(sc, rim).sqrt();
|
let sr = dist2(sc, rim).sqrt();
|
||||||
let dc = dist2(cursor_screen, sc).sqrt();
|
let dc = dist2(cursor_screen, sc).sqrt();
|
||||||
// Trigger by proximity to the circle EDGE (hovering
|
|
||||||
// the circle), independent of where the tangent point
|
|
||||||
// lands.
|
|
||||||
let edge_d = (dc - sr).abs();
|
let edge_d = (dc - sr).abs();
|
||||||
// A TRUE tangent: from the point the command draws
|
let w = if let Some(from) = self.from_point {
|
||||||
// from (P), the tangent point T on the circle has the
|
let Some((t0, t1)) = circle_tangent_points(from, cv, r) else {
|
||||||
// radius CT perpendicular to PT, so T lies at the
|
continue;
|
||||||
// half-angle acos(r/|CP|) either side of the C→P
|
};
|
||||||
// direction. Two solutions — take the one nearer the
|
let s0 = world_to_screen(t0.as_dvec3(), view_rot, eye, bounds);
|
||||||
// cursor. Without P (a deferred tangent picked first)
|
let s1 = world_to_screen(t1.as_dvec3(), view_rot, eye, bounds);
|
||||||
// or with P inside the circle, fall back to the circle
|
if dist2(s0, cursor_screen) <= dist2(s1, cursor_screen) {
|
||||||
// point facing the cursor. Previously this always used
|
t0
|
||||||
// the facing point, so Tangent behaved like Nearest
|
} else {
|
||||||
// (#274).
|
t1
|
||||||
let w = self
|
}
|
||||||
.from_point
|
} else {
|
||||||
.and_then(|p| circle_tangent_points(p, cv, r))
|
let dx = cursor_screen.x - sc.x;
|
||||||
.map(|(t0, t1)| {
|
let dy = cursor_screen.y - sc.y;
|
||||||
// Two tangents — take the one nearer the cursor.
|
let dl = (dx * dx + dy * dy).sqrt();
|
||||||
let s0 = world_to_screen(t0.as_dvec3(), view_rot, eye, bounds);
|
let (nx, ny) = if dl > 1e-6 {
|
||||||
let s1 = world_to_screen(t1.as_dvec3(), view_rot, eye, bounds);
|
(dx / dl, -dy / dl)
|
||||||
if dist2(s0, cursor_screen) <= dist2(s1, cursor_screen) {
|
} else {
|
||||||
t0
|
(1.0, 0.0)
|
||||||
} else {
|
};
|
||||||
t1
|
Vec3::new(cv.x + r * nx, cv.y + r * ny, cv.z)
|
||||||
}
|
};
|
||||||
|
(w.as_dvec3(), edge_d * edge_d)
|
||||||
|
}
|
||||||
|
TangentGeom::Arc {
|
||||||
|
center,
|
||||||
|
axis_x,
|
||||||
|
axis_y,
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
} => {
|
||||||
|
let Some(from) = self.from_point else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(world) = arc_tangent_points(
|
||||||
|
from.as_dvec3(),
|
||||||
|
*center,
|
||||||
|
*axis_x,
|
||||||
|
*axis_y,
|
||||||
|
*radius,
|
||||||
|
*start_angle,
|
||||||
|
*end_angle,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.min_by(|a, b| {
|
||||||
|
let sa = world_to_screen(*a, view_rot, eye, bounds);
|
||||||
|
let sb = world_to_screen(*b, view_rot, eye, bounds);
|
||||||
|
dist2(sa, cursor_screen).total_cmp(&dist2(sb, cursor_screen))
|
||||||
|
}) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let edge_d2 = wire
|
||||||
|
.points
|
||||||
|
.windows(2)
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, _)| {
|
||||||
|
let a = wp_f64(wire, index);
|
||||||
|
let b = wp_f64(wire, index + 1);
|
||||||
|
(a.is_finite() && b.is_finite()).then(|| {
|
||||||
|
dist2_to_segment(
|
||||||
|
cursor_screen,
|
||||||
|
world_to_screen(a, view_rot, eye, bounds),
|
||||||
|
world_to_screen(b, view_rot, eye, bounds),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.fold(f32::INFINITY, f32::min);
|
||||||
let dx = cursor_screen.x - sc.x;
|
(world, edge_d2)
|
||||||
let dy = cursor_screen.y - sc.y;
|
|
||||||
let dl = (dx * dx + dy * dy).sqrt();
|
|
||||||
let (nx, ny) = if dl > 1e-6 {
|
|
||||||
(dx / dl, -dy / dl)
|
|
||||||
} else {
|
|
||||||
(1.0, 0.0)
|
|
||||||
};
|
|
||||||
Vec3::new(cv.x + r * nx, cv.y + r * ny, cv.z)
|
|
||||||
});
|
|
||||||
(w, edge_d * edge_d)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (tier, sub) = (
|
let (tier, sub) = (
|
||||||
snap_tier(SnapType::Tangent),
|
snap_tier(SnapType::Tangent),
|
||||||
snap_priority(SnapType::Tangent),
|
snap_priority(SnapType::Tangent),
|
||||||
);
|
);
|
||||||
let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
|
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds);
|
||||||
if d2 < radius2
|
if d2 < radius2
|
||||||
&& in_bounds(screen_pt)
|
&& in_bounds(screen_pt)
|
||||||
&& snap_better(tier, d2, sub, (best_rank, best_d2, best_sub))
|
&& snap_better(tier, d2, sub, (best_rank, best_d2, best_sub))
|
||||||
|
|
@ -1687,9 +1718,13 @@ impl Snapper {
|
||||||
),
|
),
|
||||||
radius: *radius as f64,
|
radius: *radius as f64,
|
||||||
},
|
},
|
||||||
|
TangentGeom::Arc { center, radius, .. } => TangentObject::Circle {
|
||||||
|
center: glam::DVec3::from_array(*center),
|
||||||
|
radius: *radius,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
best = Some(SnapResult {
|
best = Some(SnapResult {
|
||||||
world: world_pt.as_dvec3(),
|
world: world_pt,
|
||||||
screen: screen_pt,
|
screen: screen_pt,
|
||||||
snap_type: SnapType::Tangent,
|
snap_type: SnapType::Tangent,
|
||||||
tangent_obj: Some(tangent_obj),
|
tangent_obj: Some(tangent_obj),
|
||||||
|
|
@ -2249,30 +2284,45 @@ fn seg_intersect_2d(a0: Point, a1: Point, b0: Point, b1: Point) -> Option<(f32,
|
||||||
Some((t, s))
|
Some((t, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The two points on a circle (`center`, `radius`, in its XY plane) where a
|
/// Returns the two external tangent points on an XY circle.
|
||||||
/// line drawn from `p` touches tangentially, or `None` when `p` is inside or on
|
|
||||||
/// the circle (no external tangent). Each tangent point's radius is
|
|
||||||
/// perpendicular to the line from `p`, so it sits at the half-angle
|
|
||||||
/// `acos(radius / |center→p|)` on either side of the center→p direction. This
|
|
||||||
/// is the real Tangent osnap; snapping to the circle point merely facing the
|
|
||||||
/// cursor made Tangent behave like Nearest (#274).
|
|
||||||
fn circle_tangent_points(p: Vec3, center: Vec3, radius: f32) -> Option<(Vec3, Vec3)> {
|
fn circle_tangent_points(p: Vec3, center: Vec3, radius: f32) -> Option<(Vec3, Vec3)> {
|
||||||
let vx = p.x - center.x;
|
let curve = cadkernel::geom2d::Curve::Circle(cadkernel::geom2d::Circle {
|
||||||
let vy = p.y - center.y;
|
centre: [center.x as f64, center.y as f64],
|
||||||
let d = (vx * vx + vy * vy).sqrt();
|
radius: radius as f64,
|
||||||
if d <= radius + 1e-6 {
|
});
|
||||||
|
let points = cadkernel::geom2d::tangent_from(&curve, [p.x as f64, p.y as f64]);
|
||||||
|
let [first, second] = points.as_slice() else {
|
||||||
return None;
|
return None;
|
||||||
}
|
|
||||||
let base = vy.atan2(vx);
|
|
||||||
let off = (radius / d).acos();
|
|
||||||
let at = |a: f32| {
|
|
||||||
Vec3::new(
|
|
||||||
center.x + radius * a.cos(),
|
|
||||||
center.y + radius * a.sin(),
|
|
||||||
center.z,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
Some((at(base + off), at(base - off)))
|
Some((
|
||||||
|
Vec3::new(first.point[0] as f32, first.point[1] as f32, center.z),
|
||||||
|
Vec3::new(second.point[0] as f32, second.point[1] as f32, center.z),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_tangent_points(
|
||||||
|
from: DVec3,
|
||||||
|
center: [f64; 3],
|
||||||
|
axis_x: [f64; 3],
|
||||||
|
axis_y: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
start_angle: f64,
|
||||||
|
end_angle: f64,
|
||||||
|
) -> Vec<DVec3> {
|
||||||
|
let plane = cadkernel::space::Plane::from_axes(center, axis_x, axis_y);
|
||||||
|
let Some(from) = plane.project(from.to_array()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let curve = cadkernel::geom2d::Curve::Arc(cadkernel::geom2d::Arc {
|
||||||
|
centre: [0.0, 0.0],
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
});
|
||||||
|
cadkernel::geom2d::tangent_from(&curve, from)
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| DVec3::from_array(plane.point_at(point.point)))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snap to the extension of a ray beyond `origin` in `dir` direction.
|
/// Snap to the extension of a ray beyond `origin` in `dir` direction.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue