forked from 1b-agent/opencadstudio-upstream
Merge pull request #917 from ramox81/codex/fix-3dface-command-properties
Complete 3D face creation and editing
This commit is contained in:
9 files changed
+665
-206
No files matched your search
Generated
+1
-1
@@ -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=1283cce45439abecdfd49828c7d6b84cb8ba0181#1283cce45439abecdfd49828c7d6b84cb8ba0181"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=44fb4783b3e847a6ac35d9e0cf468886cb5a06d1#44fb4783b3e847a6ac35d9e0cf468886cb5a06d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ 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/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "9a332bb3c48eca90c3eefaa17aed514da0b8694a", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1283cce45439abecdfd49828c7d6b84cb8ba0181", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "44fb4783b3e847a6ac35d9e0cf468886cb5a06d1", 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"] }
|
||||||
|
|||||||
@@ -454,6 +454,31 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"3DFACE" => {
|
||||||
|
use crate::modules::draw::draw::face3d::Face3dCommand;
|
||||||
|
let new_cmd = Face3dCommand::new();
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
"EDGE" => {
|
||||||
|
use crate::modules::draw::draw::face3d::FaceEdgeCommand;
|
||||||
|
let faces = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
acadrust::EntityType::Face3D(face) => {
|
||||||
|
Some((face.common.handle, face.clone()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let new_cmd = FaceEdgeCommand::new(faces);
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
|
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
|
||||||
// currently the shaded-display toggle (token collision tracked).
|
// currently the shaded-display toggle (token collision tracked).
|
||||||
"SOLID" | "SOLID2D" => {
|
"SOLID" | "SOLID2D" => {
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ impl OpenCADStudio {
|
|||||||
crate::entities::polyline::polyline3d_control_vertex_count(polyline),
|
crate::entities::polyline::polyline3d_control_vertex_count(polyline),
|
||||||
),
|
),
|
||||||
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()),
|
||||||
|
acadrust::EntityType::Face3D(face) => {
|
||||||
|
Some(if face.is_triangle() { 3 } else { 4 })
|
||||||
|
}
|
||||||
acadrust::EntityType::Spline(spline) => {
|
acadrust::EntityType::Spline(spline) => {
|
||||||
Some(if crate::entities::spline::shows_fit_points(spline) {
|
Some(if crate::entities::spline::shows_fit_points(spline) {
|
||||||
spline.fit_points.len()
|
spline.fit_points.len()
|
||||||
|
|||||||
@@ -4922,11 +4922,14 @@ impl OpenCADStudio {
|
|||||||
Message::PropVertexStep(delta) => {
|
Message::PropVertexStep(delta) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
// Vertex navigation applies to a single selected polyline.
|
// Navigate the active vertex or table cell.
|
||||||
let n = if handles.len() == 1 {
|
let n = if handles.len() == 1 {
|
||||||
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
||||||
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
||||||
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
||||||
|
Some(acadrust::EntityType::Face3D(face)) => {
|
||||||
|
if face.is_triangle() { 3 } else { 4 }
|
||||||
|
}
|
||||||
Some(acadrust::EntityType::Polyline3D(p)) => {
|
Some(acadrust::EntityType::Polyline3D(p)) => {
|
||||||
crate::entities::polyline::polyline3d_control_vertex_count(p)
|
crate::entities::polyline::polyline3d_control_vertex_count(p)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -433,7 +433,8 @@ impl OpenCADStudio {
|
|||||||
Some(acadrust::EntityType::LwPolyline(_))
|
Some(acadrust::EntityType::LwPolyline(_))
|
||||||
| Some(acadrust::EntityType::Polyline2D(_))
|
| Some(acadrust::EntityType::Polyline2D(_))
|
||||||
| Some(acadrust::EntityType::Polyline3D(_))
|
| Some(acadrust::EntityType::Polyline3D(_))
|
||||||
| Some(acadrust::EntityType::Spline(_)) => true,
|
| Some(acadrust::EntityType::Spline(_))
|
||||||
|
| Some(acadrust::EntityType::Face3D(_)) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
indexed.then_some(tab.properties.prop_vertex)
|
indexed.then_some(tab.properties.prop_vertex)
|
||||||
|
|||||||
+128
-202
@@ -2,141 +2,56 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface
|
|||||||
use cadkernel::geom2d::{triangulate, Tolerance};
|
use cadkernel::geom2d::{triangulate, Tolerance};
|
||||||
use cadkernel::space::{polygon, Plane, Vec3 as KernelVec3};
|
use cadkernel::space::{polygon, Plane, Vec3 as KernelVec3};
|
||||||
use glam::Vec3;
|
use glam::Vec3;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{parse_f64, ro_prop as ro, square_grip};
|
use crate::entities::common::{
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
edit_prop as edit, parse_f64, ro_prop as ro, stepper_prop as stepper, square_grip,
|
||||||
|
};
|
||||||
|
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
/// Triangulate a planar (possibly concave) polygon into a flat triangle-soup
|
/// Triangulate a planar polygon through the kernel.
|
||||||
/// (3 vertices per triangle), preserving the polygon's winding. A simple fan
|
|
||||||
/// from vertex 0 is only valid for convex faces — a concave face (e.g. an
|
|
||||||
/// L-shaped mesh face) fans into triangles that spill outside the outline. Ear
|
|
||||||
/// clipping handles both. Falls back to a fan when the polygon is degenerate.
|
|
||||||
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
||||||
let n = poly.len();
|
if poly.len() < 3 {
|
||||||
if n < 3 {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
if n == 3 {
|
let origin = KernelVec3::from(poly[0]);
|
||||||
return vec![poly[0], poly[1], poly[2]];
|
let tolerance = Tolerance::default().linear();
|
||||||
}
|
let Some(axis) = poly[1..]
|
||||||
let cross = |a: [f64; 3], b: [f64; 3]| {
|
.iter()
|
||||||
[
|
.map(|point| KernelVec3::from(*point) - origin)
|
||||||
a[1] * b[2] - a[2] * b[1],
|
.find(|axis| axis.length() > tolerance)
|
||||||
a[2] * b[0] - a[0] * b[2],
|
else {
|
||||||
a[0] * b[1] - a[1] * b[0],
|
return Vec::new();
|
||||||
]
|
|
||||||
};
|
};
|
||||||
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
let Some(normal) = polygon::normal(poly) else {
|
||||||
let fan = || {
|
return Vec::new();
|
||||||
let mut out = Vec::new();
|
|
||||||
for i in 1..n - 1 {
|
|
||||||
out.push(poly[0]);
|
|
||||||
out.push(poly[i]);
|
|
||||||
out.push(poly[i + 1]);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
};
|
};
|
||||||
// Face normal via Newell's method (robust for near-planar polygons).
|
let Some(plane) = Plane::orthonormal(poly[0], axis.to_array(), normal) else {
|
||||||
let mut normal = [0.0f64; 3];
|
return Vec::new();
|
||||||
for i in 0..n {
|
|
||||||
let a = poly[i];
|
|
||||||
let b = poly[(i + 1) % n];
|
|
||||||
normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
|
|
||||||
normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
|
|
||||||
normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
|
|
||||||
}
|
|
||||||
let nlen = dot(normal, normal).sqrt();
|
|
||||||
if nlen < 1e-12 {
|
|
||||||
return fan();
|
|
||||||
}
|
|
||||||
let normal = [normal[0] / nlen, normal[1] / nlen, normal[2] / nlen];
|
|
||||||
// Orthonormal in-plane basis.
|
|
||||||
let seed = if normal[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
|
||||||
let mut u = cross(seed, normal);
|
|
||||||
let ul = dot(u, u).sqrt();
|
|
||||||
if ul < 1e-12 {
|
|
||||||
return fan();
|
|
||||||
}
|
|
||||||
u = [u[0] / ul, u[1] / ul, u[2] / ul];
|
|
||||||
let v = cross(normal, u);
|
|
||||||
let p2: Vec<[f64; 2]> = poly.iter().map(|&p| [dot(p, u), dot(p, v)]).collect();
|
|
||||||
// Signed area → winding (CCW when positive in the (u, v) frame).
|
|
||||||
let mut area = 0.0;
|
|
||||||
for i in 0..n {
|
|
||||||
let a = p2[i];
|
|
||||||
let b = p2[(i + 1) % n];
|
|
||||||
area += a[0] * b[1] - b[0] * a[1];
|
|
||||||
}
|
|
||||||
let ccw = area > 0.0;
|
|
||||||
let tri_area2 = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
|
||||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
||||||
};
|
};
|
||||||
let in_tri = |p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
let projected: Vec<[f64; 2]> = poly
|
||||||
let d1 = tri_area2(a, b, p);
|
.iter()
|
||||||
let d2 = tri_area2(b, c, p);
|
.filter_map(|&point| plane.project(point))
|
||||||
let d3 = tri_area2(c, a, p);
|
.collect();
|
||||||
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
if projected.len() != poly.len() {
|
||||||
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
return Vec::new();
|
||||||
!(neg && pos)
|
|
||||||
};
|
|
||||||
let mut idx: Vec<usize> = (0..n).collect();
|
|
||||||
let mut out: Vec<[f64; 3]> = Vec::with_capacity((n - 2) * 3);
|
|
||||||
let mut guard = 0usize;
|
|
||||||
while idx.len() > 3 && guard < n * n {
|
|
||||||
guard += 1;
|
|
||||||
let m = idx.len();
|
|
||||||
let mut clipped = false;
|
|
||||||
for k in 0..m {
|
|
||||||
let i0 = idx[(k + m - 1) % m];
|
|
||||||
let i1 = idx[k];
|
|
||||||
let i2 = idx[(k + 1) % m];
|
|
||||||
let (a, b, c) = (p2[i0], p2[i1], p2[i2]);
|
|
||||||
let convex = if ccw { tri_area2(a, b, c) > 0.0 } else { tri_area2(a, b, c) < 0.0 };
|
|
||||||
if !convex {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut contains = false;
|
|
||||||
for &j in &idx {
|
|
||||||
if j == i0 || j == i1 || j == i2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if in_tri(p2[j], a, b, c) {
|
|
||||||
contains = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if contains {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(poly[i0]);
|
|
||||||
out.push(poly[i1]);
|
|
||||||
out.push(poly[i2]);
|
|
||||||
idx.remove(k);
|
|
||||||
clipped = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !clipped {
|
|
||||||
// No ear found (self-intersecting / numerically degenerate) — bail
|
|
||||||
// to a fan of the remainder rather than loop forever.
|
|
||||||
for i in 1..idx.len() - 1 {
|
|
||||||
out.push(poly[idx[0]]);
|
|
||||||
out.push(poly[idx[i]]);
|
|
||||||
out.push(poly[idx[i + 1]]);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if idx.len() == 3 {
|
let (points, triangles) = triangulate(&projected, &[]);
|
||||||
out.push(poly[idx[0]]);
|
let source: Vec<usize> = points
|
||||||
out.push(poly[idx[1]]);
|
.iter()
|
||||||
out.push(poly[idx[2]]);
|
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
||||||
|
.collect();
|
||||||
|
if source.len() != points.len() {
|
||||||
|
return Vec::new();
|
||||||
}
|
}
|
||||||
out
|
triangles
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|triangle| triangle.map(|index| poly[source[index]]))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Face3D ────────────────────────────────────────────────────────────────────
|
// ── Face3D ────────────────────────────────────────────────────────────────────
|
||||||
@@ -174,32 +89,7 @@ fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(normal) = polygon::normal(&ring) else {
|
triangulate_planar(&ring)
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let axis = (KernelVec3::from(ring[1]) - KernelVec3::from(ring[0])).to_array();
|
|
||||||
let Some(plane) = Plane::orthonormal(ring[0], axis, normal) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let projected: Vec<[f64; 2]> = ring
|
|
||||||
.iter()
|
|
||||||
.filter_map(|&point| plane.project(point))
|
|
||||||
.collect();
|
|
||||||
if projected.len() != ring.len() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let (points, triangles) = triangulate(&projected, &[]);
|
|
||||||
let source: Vec<usize> = points
|
|
||||||
.iter()
|
|
||||||
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
|
||||||
.collect();
|
|
||||||
if source.len() != points.len() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
triangles
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|triangle| triangle.map(|index| ring[source[index]]))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderConvertible for Face3D {
|
impl RenderConvertible for Face3D {
|
||||||
@@ -238,15 +128,6 @@ impl RenderConvertible for Face3D {
|
|||||||
add_edge(p3, p0);
|
add_edge(p3, p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if pts.is_empty() {
|
|
||||||
// All edges invisible — show a tiny cross at centroid.
|
|
||||||
let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0;
|
|
||||||
let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0;
|
|
||||||
let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0;
|
|
||||||
let s = 0.1_f64;
|
|
||||||
pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]];
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(RenderEntity {
|
Some(RenderEntity {
|
||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
object: RenderObject::Lines(pts),
|
object: RenderObject::Lines(pts),
|
||||||
@@ -265,15 +146,22 @@ impl RenderConvertible for Face3D {
|
|||||||
|
|
||||||
impl Grippable for Face3D {
|
impl Grippable for Face3D {
|
||||||
fn grips(&self) -> Vec<GripDef> {
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
vec![
|
let mut grips = vec![
|
||||||
square_grip(0, dvec3(&self.first_corner)),
|
square_grip(0, dvec3(&self.first_corner)),
|
||||||
square_grip(1, dvec3(&self.second_corner)),
|
square_grip(1, dvec3(&self.second_corner)),
|
||||||
square_grip(2, dvec3(&self.third_corner)),
|
square_grip(2, dvec3(&self.third_corner)),
|
||||||
square_grip(3, dvec3(&self.fourth_corner)),
|
];
|
||||||
]
|
if !self.is_triangle() {
|
||||||
|
grips.push(square_grip(3, dvec3(&self.fourth_corner)));
|
||||||
|
}
|
||||||
|
grips
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
|
let was_triangle = self.is_triangle();
|
||||||
|
if was_triangle && grip_id == 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let corner = match grip_id {
|
let corner = match grip_id {
|
||||||
0 => &mut self.first_corner,
|
0 => &mut self.first_corner,
|
||||||
1 => &mut self.second_corner,
|
1 => &mut self.second_corner,
|
||||||
@@ -281,69 +169,107 @@ impl Grippable for Face3D {
|
|||||||
3 => &mut self.fourth_corner,
|
3 => &mut self.fourth_corner,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
match apply {
|
let current = dvec3(corner);
|
||||||
GripApply::Translate(d) => {
|
let updated = match apply {
|
||||||
corner.x += d.x as f64;
|
GripApply::Translate(delta) => current + delta,
|
||||||
corner.y += d.y as f64;
|
GripApply::Absolute(position) => position,
|
||||||
corner.z += d.z as f64;
|
};
|
||||||
}
|
if !updated.is_finite() {
|
||||||
GripApply::Absolute(p) => {
|
return;
|
||||||
corner.x = p.x as f64;
|
}
|
||||||
corner.y = p.y as f64;
|
corner.x = updated.x;
|
||||||
corner.z = p.z as f64;
|
corner.y = updated.y;
|
||||||
}
|
corner.z = updated.z;
|
||||||
|
if was_triangle && grip_id == 2 {
|
||||||
|
self.fourth_corner = self.third_corner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Face3D {
|
impl PropertyEditable for Face3D {
|
||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
use crate::entities::common::edit_prop as edit;
|
let was_triangle = self.is_triangle();
|
||||||
|
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||||
|
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||||
|
.min(vertex_count - 1);
|
||||||
|
let corner = match current {
|
||||||
|
0 => &self.first_corner,
|
||||||
|
1 => &self.second_corner,
|
||||||
|
2 => &self.third_corner,
|
||||||
|
_ => &self.fourth_corner,
|
||||||
|
};
|
||||||
let inv = self.invisible_edges;
|
let inv = self.invisible_edges;
|
||||||
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
|
let edge = |label: &str, field: &'static str, hidden: bool| Property {
|
||||||
|
label: label.to_string(),
|
||||||
|
field,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: if hidden {
|
||||||
|
t!("Invisible").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("Visible").into_owned()
|
||||||
|
},
|
||||||
|
options: vec![t!("Visible").into_owned(), t!("Invisible").into_owned()],
|
||||||
|
},
|
||||||
|
};
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro(t!("Current vertex").as_ref(), "f3_current", String::new()),
|
stepper(
|
||||||
edit(t!("Vertex 1 X").as_ref(), "f3_p1x", self.first_corner.x),
|
t!("Current vertex").as_ref(),
|
||||||
edit(t!("Vertex 1 Y").as_ref(), "f3_p1y", self.first_corner.y),
|
"current_vertex",
|
||||||
edit(t!("Vertex 1 Z").as_ref(), "f3_p1z", self.first_corner.z),
|
format!("{}", current + 1),
|
||||||
edit(t!("Vertex 2 X").as_ref(), "f3_p2x", self.second_corner.x),
|
),
|
||||||
edit(t!("Vertex 2 Y").as_ref(), "f3_p2y", self.second_corner.y),
|
edit(t!("Vertex X").as_ref(), "f3_vertex_x", corner.x),
|
||||||
edit(t!("Vertex 2 Z").as_ref(), "f3_p2z", self.second_corner.z),
|
edit(t!("Vertex Y").as_ref(), "f3_vertex_y", corner.y),
|
||||||
edit(t!("Vertex 3 X").as_ref(), "f3_p3x", self.third_corner.x),
|
edit(t!("Vertex Z").as_ref(), "f3_vertex_z", corner.z),
|
||||||
edit(t!("Vertex 3 Y").as_ref(), "f3_p3y", self.third_corner.y),
|
edge(t!("Edge 1").as_ref(), "f3_edge1", inv.is_first_invisible()),
|
||||||
edit(t!("Vertex 3 Z").as_ref(), "f3_p3z", self.third_corner.z),
|
edge(t!("Edge 2").as_ref(), "f3_edge2", inv.is_second_invisible()),
|
||||||
edit(t!("Vertex 4 X").as_ref(), "f3_p4x", self.fourth_corner.x),
|
edge(t!("Edge 3").as_ref(), "f3_edge3", inv.is_third_invisible()),
|
||||||
edit(t!("Vertex 4 Y").as_ref(), "f3_p4y", self.fourth_corner.y),
|
edge(t!("Edge 4").as_ref(), "f3_edge4", inv.is_fourth_invisible()),
|
||||||
edit(t!("Vertex 4 Z").as_ref(), "f3_p4z", self.fourth_corner.z),
|
|
||||||
ro(t!("Edge 1").as_ref(), "f3_edge1", edge(inv.is_first_invisible())),
|
|
||||||
ro(t!("Edge 2").as_ref(), "f3_edge2", edge(inv.is_second_invisible())),
|
|
||||||
ro(t!("Edge 3").as_ref(), "f3_edge3", edge(inv.is_third_invisible())),
|
|
||||||
ro(t!("Edge 4").as_ref(), "f3_edge4", edge(inv.is_fourth_invisible())),
|
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
let Ok(v) = value.trim().parse::<f64>() else {
|
if field.starts_with("f3_edge") {
|
||||||
|
let invisible = if value == t!("Invisible").as_ref() {
|
||||||
|
true
|
||||||
|
} else if value == t!("Visible").as_ref() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match field {
|
||||||
|
"f3_edge1" => self.invisible_edges.set_first_invisible(invisible),
|
||||||
|
"f3_edge2" => self.invisible_edges.set_second_invisible(invisible),
|
||||||
|
"f3_edge3" => self.invisible_edges.set_third_invisible(invisible),
|
||||||
|
"f3_edge4" => self.invisible_edges.set_fourth_invisible(invisible),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(value) = parse_f64(value).filter(|value| value.is_finite()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let was_triangle = self.is_triangle();
|
||||||
|
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||||
|
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||||
|
.min(vertex_count - 1);
|
||||||
|
let corner = match current {
|
||||||
|
0 => &mut self.first_corner,
|
||||||
|
1 => &mut self.second_corner,
|
||||||
|
2 => &mut self.third_corner,
|
||||||
|
_ => &mut self.fourth_corner,
|
||||||
|
};
|
||||||
match field {
|
match field {
|
||||||
"f3_p1x" => self.first_corner.x = v,
|
"f3_vertex_x" => corner.x = value,
|
||||||
"f3_p1y" => self.first_corner.y = v,
|
"f3_vertex_y" => corner.y = value,
|
||||||
"f3_p1z" => self.first_corner.z = v,
|
"f3_vertex_z" => corner.z = value,
|
||||||
"f3_p2x" => self.second_corner.x = v,
|
|
||||||
"f3_p2y" => self.second_corner.y = v,
|
|
||||||
"f3_p2z" => self.second_corner.z = v,
|
|
||||||
"f3_p3x" => self.third_corner.x = v,
|
|
||||||
"f3_p3y" => self.third_corner.y = v,
|
|
||||||
"f3_p3z" => self.third_corner.z = v,
|
|
||||||
"f3_p4x" => self.fourth_corner.x = v,
|
|
||||||
"f3_p4y" => self.fourth_corner.y = v,
|
|
||||||
"f3_p4z" => self.fourth_corner.z = v,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if was_triangle && current == 2 {
|
||||||
|
self.fourth_corner = self.third_corner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,500 @@
|
|||||||
|
use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D};
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use acadrust::{EntityType, Handle};
|
||||||
|
use cadkernel::geom2d::Tolerance;
|
||||||
|
use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
|
||||||
|
use glam::DVec3;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||||
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
|
fn vector(point: DVec3) -> Vector3 {
|
||||||
|
Vector3::new(point.x, point.y, point.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(value: Vector3) -> DVec3 {
|
||||||
|
DVec3::new(value.x, value.y, value.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_edge_invisible(flags: &mut InvisibleEdgeFlags, edge: usize, invisible: bool) {
|
||||||
|
match edge {
|
||||||
|
0 => flags.set_first_invisible(invisible),
|
||||||
|
1 => flags.set_second_invisible(invisible),
|
||||||
|
2 => flags.set_third_invisible(invisible),
|
||||||
|
3 => flags.set_fourth_invisible(invisible),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge_is_invisible(flags: InvisibleEdgeFlags, edge: usize) -> bool {
|
||||||
|
match edge {
|
||||||
|
0 => flags.is_first_invisible(),
|
||||||
|
1 => flags.is_second_invisible(),
|
||||||
|
2 => flags.is_third_invisible(),
|
||||||
|
3 => flags.is_fourth_invisible(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_segment(points: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) {
|
||||||
|
if !points.is_empty() {
|
||||||
|
points.push([f64::NAN; 3]);
|
||||||
|
}
|
||||||
|
points.push(start);
|
||||||
|
points.push(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Face3dCommand {
|
||||||
|
points: Vec<DVec3>,
|
||||||
|
invisible: [bool; 4],
|
||||||
|
pending_invisible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Face3dCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
points: Vec::new(),
|
||||||
|
invisible: [false; 4],
|
||||||
|
pending_invisible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self, fourth: DVec3) -> Option<EntityType> {
|
||||||
|
if self.points.len() != 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut face = Face3D::new(
|
||||||
|
vector(self.points[0]),
|
||||||
|
vector(self.points[1]),
|
||||||
|
vector(self.points[2]),
|
||||||
|
vector(fourth),
|
||||||
|
);
|
||||||
|
for edge in 0..4 {
|
||||||
|
set_edge_invisible(&mut face.invisible_edges, edge, self.invisible[edge]);
|
||||||
|
}
|
||||||
|
Some(EntityType::Face3D(face))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn continue_from_last_edge(&mut self, fourth: DVec3) {
|
||||||
|
let third = self.points[2];
|
||||||
|
let shared_edge_invisible = self.invisible[2];
|
||||||
|
self.points.clear();
|
||||||
|
self.points.push(third);
|
||||||
|
self.points.push(fourth);
|
||||||
|
self.invisible = [shared_edge_invisible, false, false, false];
|
||||||
|
self.pending_invisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn continue_from_triangle(&mut self) {
|
||||||
|
let second = self.points[1];
|
||||||
|
let third = self.points[2];
|
||||||
|
let shared_edge_invisible = self.invisible[1];
|
||||||
|
self.points.clear();
|
||||||
|
self.points.push(second);
|
||||||
|
self.points.push(third);
|
||||||
|
self.invisible = [shared_edge_invisible, false, false, false];
|
||||||
|
self.pending_invisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for Face3dCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"3DFACE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.points.len() {
|
||||||
|
0 => t!("3DFACE Specify first point or [Invisible]:").into_owned(),
|
||||||
|
1 => t!("3DFACE Specify second point or [Invisible]:").into_owned(),
|
||||||
|
2 => t!("3DFACE Specify third point or [Invisible] <exit>:").into_owned(),
|
||||||
|
_ => t!("3DFACE Specify fourth point or [Invisible] <create three-sided face>:")
|
||||||
|
.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
vec![CmdOption::new(t!("Invisible").as_ref(), "I")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, picked: DVec3) -> CmdResult {
|
||||||
|
let point_index = self.points.len();
|
||||||
|
if self.pending_invisible && point_index < 4 {
|
||||||
|
self.invisible[point_index] = true;
|
||||||
|
}
|
||||||
|
self.pending_invisible = false;
|
||||||
|
if self.points.len() == 3 {
|
||||||
|
let entity = self.build(picked);
|
||||||
|
self.continue_from_last_edge(picked);
|
||||||
|
return entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity);
|
||||||
|
}
|
||||||
|
self.points.push(picked);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.points.len() != 3 {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
|
if self.pending_invisible {
|
||||||
|
self.invisible[3] = true;
|
||||||
|
}
|
||||||
|
let third = self.points[2];
|
||||||
|
let entity = self.build(third);
|
||||||
|
self.continue_from_triangle();
|
||||||
|
entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||||
|
match value.trim().to_ascii_uppercase().as_str() {
|
||||||
|
"I" | "INVISIBLE" => {
|
||||||
|
self.pending_invisible = true;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, cursor: DVec3) -> Option<WireModel> {
|
||||||
|
if self.points.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut preview = Vec::new();
|
||||||
|
for edge in 0..self.points.len().saturating_sub(1) {
|
||||||
|
if !self.invisible[edge] {
|
||||||
|
append_segment(
|
||||||
|
&mut preview,
|
||||||
|
self.points[edge].to_array(),
|
||||||
|
self.points[edge + 1].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pending_edge = self.points.len() - 1;
|
||||||
|
if !self.invisible[pending_edge] {
|
||||||
|
append_segment(
|
||||||
|
&mut preview,
|
||||||
|
self.points.last()?.to_array(),
|
||||||
|
cursor.to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.points.len() == 3 && !(self.invisible[3] || self.pending_invisible) {
|
||||||
|
append_segment(&mut preview, cursor.to_array(), self.points[0].to_array());
|
||||||
|
}
|
||||||
|
Some(WireModel::solid_f64(
|
||||||
|
"face3d_preview".to_string(),
|
||||||
|
preview,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EdgeMode {
|
||||||
|
Toggle,
|
||||||
|
Display,
|
||||||
|
DisplaySelect,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FaceEdgeCommand {
|
||||||
|
faces: Vec<(Handle, Face3D)>,
|
||||||
|
mode: EdgeMode,
|
||||||
|
show_all_hidden: bool,
|
||||||
|
displayed_faces: HashSet<Handle>,
|
||||||
|
pending_replacements: HashMap<Handle, Face3D>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FaceEdgeCommand {
|
||||||
|
pub fn new(faces: Vec<(Handle, Face3D)>) -> Self {
|
||||||
|
Self {
|
||||||
|
faces,
|
||||||
|
mode: EdgeMode::Toggle,
|
||||||
|
show_all_hidden: false,
|
||||||
|
displayed_faces: HashSet::new(),
|
||||||
|
pending_replacements: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corners(face: &Face3D) -> [KernelVec3; 4] {
|
||||||
|
[
|
||||||
|
KernelVec3::from(point(face.first_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.second_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.third_corner).to_array()),
|
||||||
|
KernelVec3::from(point(face.fourth_corner).to_array()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearest_edge(face: &Face3D, picked: KernelVec3) -> Option<usize> {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
let tolerance = Tolerance::default().linear();
|
||||||
|
let mut best = None;
|
||||||
|
for edge in 0..4 {
|
||||||
|
let start = corners[edge];
|
||||||
|
let end = corners[(edge + 1) % 4];
|
||||||
|
if start.distance(end) <= tolerance {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let distance = picked.distance_to_segment(start, end);
|
||||||
|
if best.is_none_or(|(_, best_distance)| distance < best_distance) {
|
||||||
|
best = Some((edge, distance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(edge, _)| edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge_wire(face: &Face3D, edge: usize, name: &str) -> WireModel {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
WireModel::solid_f64(
|
||||||
|
name.to_string(),
|
||||||
|
vec![corners[edge].to_array(), corners[(edge + 1) % 4].to_array()],
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collinear_replacements(
|
||||||
|
&mut self,
|
||||||
|
selected_face: usize,
|
||||||
|
selected_edge: usize,
|
||||||
|
) -> Vec<(Handle, Vec<EntityType>)> {
|
||||||
|
let selected_corners = Self::corners(&self.faces[selected_face].1);
|
||||||
|
let selected_start = selected_corners[selected_edge];
|
||||||
|
let selected_end = selected_corners[(selected_edge + 1) % 4];
|
||||||
|
let make_invisible = !edge_is_invisible(
|
||||||
|
self.faces[selected_face].1.invisible_edges,
|
||||||
|
selected_edge,
|
||||||
|
);
|
||||||
|
let mut replacements = Vec::new();
|
||||||
|
self.pending_replacements.clear();
|
||||||
|
for (handle, face) in &self.faces {
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
let mut updated = face.clone();
|
||||||
|
let mut changed = false;
|
||||||
|
for edge in 0..4 {
|
||||||
|
if segments_overlap_collinearly(
|
||||||
|
selected_start.to_array(),
|
||||||
|
selected_end.to_array(),
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
Tolerance::default().linear(),
|
||||||
|
) && edge_is_invisible(updated.invisible_edges, edge) != make_invisible
|
||||||
|
{
|
||||||
|
set_edge_invisible(&mut updated.invisible_edges, edge, make_invisible);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
self.pending_replacements.insert(*handle, updated.clone());
|
||||||
|
replacements.push((*handle, vec![EntityType::Face3D(updated)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacements
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden_edges_wire(&self) -> WireModel {
|
||||||
|
let mut points = Vec::new();
|
||||||
|
for (handle, face) in &self.faces {
|
||||||
|
if !self.show_all_hidden && !self.displayed_faces.contains(handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
for edge in 0..4 {
|
||||||
|
if edge_is_invisible(face.invisible_edges, edge)
|
||||||
|
&& corners[edge].distance(corners[(edge + 1) % 4])
|
||||||
|
> Tolerance::default().linear()
|
||||||
|
{
|
||||||
|
append_segment(
|
||||||
|
&mut points,
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WireModel::solid_f64(
|
||||||
|
"face3d_hidden_edges".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for FaceEdgeCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"EDGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle => {
|
||||||
|
t!("EDGE Select edge of 3D face to toggle visibility or [Display]:").into_owned()
|
||||||
|
}
|
||||||
|
EdgeMode::Display => {
|
||||||
|
t!("EDGE Display invisible edges [All/Select] <Select>:").into_owned()
|
||||||
|
}
|
||||||
|
EdgeMode::DisplaySelect => {
|
||||||
|
t!("EDGE Select 3D faces to display invisible edges <done>:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle => vec![CmdOption::new(t!("Display").as_ref(), "D")],
|
||||||
|
EdgeMode::Display => vec![
|
||||||
|
CmdOption::new(t!("All").as_ref(), "A"),
|
||||||
|
CmdOption::new(t!("Select").as_ref(), "S"),
|
||||||
|
],
|
||||||
|
EdgeMode::DisplaySelect => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
matches!(self.mode, EdgeMode::Toggle | EdgeMode::DisplaySelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, picked: DVec3) -> CmdResult {
|
||||||
|
let Some(index) = self.faces.iter().position(|(candidate, _)| *candidate == handle) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if self.mode == EdgeMode::DisplaySelect {
|
||||||
|
self.displayed_faces.insert(handle);
|
||||||
|
return CmdResult::Preview(self.hidden_edges_wire());
|
||||||
|
}
|
||||||
|
let picked = KernelVec3::from(picked.to_array());
|
||||||
|
let Some(edge) = Self::nearest_edge(&self.faces[index].1, picked) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let replacements = self.collinear_replacements(index, edge);
|
||||||
|
if replacements.is_empty() {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
CmdResult::ReplaceManyContinue(replacements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover_entity(&mut self, handle: Handle, picked: DVec3) -> Vec<WireModel> {
|
||||||
|
let Some((_, face)) = self.faces.iter().find(|(candidate, _)| *candidate == handle) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
if self.mode == EdgeMode::DisplaySelect
|
||||||
|
|| self.show_all_hidden
|
||||||
|
|| self.displayed_faces.contains(&handle)
|
||||||
|
{
|
||||||
|
let mut points = Vec::new();
|
||||||
|
let corners = Self::corners(face);
|
||||||
|
for edge in 0..4 {
|
||||||
|
if corners[edge].distance(corners[(edge + 1) % 4])
|
||||||
|
> Tolerance::default().linear()
|
||||||
|
{
|
||||||
|
append_segment(
|
||||||
|
&mut points,
|
||||||
|
corners[edge].to_array(),
|
||||||
|
corners[(edge + 1) % 4].to_array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vec![WireModel::solid_f64(
|
||||||
|
"face3d_edges".to_string(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
Self::nearest_edge(face, KernelVec3::from(picked.to_array()))
|
||||||
|
.map(|edge| vec![Self::edge_wire(face, edge, "face3d_edge")])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||||
|
let keyword = value.trim().to_ascii_uppercase();
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Toggle if matches!(keyword.as_str(), "D" | "DISPLAY") => {
|
||||||
|
self.mode = EdgeMode::Display;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
EdgeMode::Display if matches!(keyword.as_str(), "A" | "ALL") => {
|
||||||
|
self.mode = EdgeMode::Toggle;
|
||||||
|
self.show_all_hidden = true;
|
||||||
|
Some(CmdResult::Preview(self.hidden_edges_wire()))
|
||||||
|
}
|
||||||
|
EdgeMode::Display if matches!(keyword.as_str(), "S" | "SELECT") => {
|
||||||
|
self.mode = EdgeMode::DisplaySelect;
|
||||||
|
self.show_all_hidden = false;
|
||||||
|
self.displayed_faces.clear();
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => Some(CmdResult::NeedPoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.mode {
|
||||||
|
EdgeMode::Display => {
|
||||||
|
self.mode = EdgeMode::DisplaySelect;
|
||||||
|
self.show_all_hidden = false;
|
||||||
|
self.displayed_faces.clear();
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
EdgeMode::DisplaySelect => {
|
||||||
|
self.mode = EdgeMode::Toggle;
|
||||||
|
CmdResult::Preview(self.hidden_edges_wire())
|
||||||
|
}
|
||||||
|
EdgeMode::Toggle => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[Handle]) {
|
||||||
|
if let Some(new_handle) = new_handles.first().copied() {
|
||||||
|
if let Some((handle, face)) =
|
||||||
|
self.faces.iter_mut().find(|(handle, _)| *handle == old)
|
||||||
|
{
|
||||||
|
if let Some(updated) = self.pending_replacements.remove(&old) {
|
||||||
|
*face = updated;
|
||||||
|
}
|
||||||
|
*handle = new_handle;
|
||||||
|
}
|
||||||
|
if self.displayed_faces.remove(&old) {
|
||||||
|
self.displayed_faces.insert(new_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["3DFACE", "EDGE"]
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ pub mod circle;
|
|||||||
pub mod dimcenter;
|
pub mod dimcenter;
|
||||||
pub mod donut;
|
pub mod donut;
|
||||||
pub mod ellipse;
|
pub mod ellipse;
|
||||||
|
pub mod face3d;
|
||||||
pub mod hatch;
|
pub mod hatch;
|
||||||
pub mod hatchedit;
|
pub mod hatchedit;
|
||||||
pub mod helix;
|
pub mod helix;
|
||||||
|
|||||||
Reference in new issue
Block a user