forked from 1b-agent/opencadstudio-upstream
fix(model): correct primitive creation and grips
This commit is contained in:
1 parent
59ca0f693f
commit
c4dfa7567d
13 files changed
+353
-84
No files matched your search
+104
-2
@@ -35,6 +35,64 @@ fn pt_pt_d2(a: Point, b: Point) -> f32 {
|
||||
(a.x - b.x).powi(2) + (a.y - b.y).powi(2)
|
||||
}
|
||||
|
||||
fn cursor_on_projected_axis(
|
||||
cursor: Point,
|
||||
bounds: iced::Rectangle,
|
||||
view: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
origin: glam::DVec3,
|
||||
direction: glam::DVec3,
|
||||
) -> Option<glam::DVec3> {
|
||||
let direction = direction.normalize_or_zero();
|
||||
if direction.length_squared() <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let relative = (origin - eye).as_vec3();
|
||||
let start = view * relative.extend(1.0);
|
||||
let next = view * (relative + direction.as_vec3()).extend(1.0);
|
||||
if start.w.abs() <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let delta = next - start;
|
||||
let ndc = start.truncate() / start.w;
|
||||
let derivative = glam::Vec2::new(
|
||||
(delta.x * start.w - start.x * delta.w) / start.w.powi(2),
|
||||
(delta.y * start.w - start.y * delta.w) / start.w.powi(2),
|
||||
);
|
||||
let screen_origin = glam::Vec2::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
);
|
||||
let screen_direction = glam::Vec2::new(
|
||||
derivative.x * 0.5 * bounds.width,
|
||||
-derivative.y * 0.5 * bounds.height,
|
||||
);
|
||||
let length_squared = screen_direction.length_squared();
|
||||
if length_squared <= 1e-8 {
|
||||
return None;
|
||||
}
|
||||
let cursor = glam::Vec2::new(cursor.x, cursor.y);
|
||||
let screen = screen_origin
|
||||
+ screen_direction * (cursor - screen_origin).dot(screen_direction) / length_squared;
|
||||
let target = glam::Vec2::new(
|
||||
screen.x / bounds.width * 2.0 - 1.0,
|
||||
1.0 - screen.y / bounds.height * 2.0,
|
||||
);
|
||||
let x_denominator = target.x * delta.w - delta.x;
|
||||
let y_denominator = target.y * delta.w - delta.y;
|
||||
if x_denominator.abs().max(y_denominator.abs()) <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let distance = if x_denominator.abs() >= y_denominator.abs() {
|
||||
(start.x - target.x * start.w) / x_denominator
|
||||
} else {
|
||||
(start.y - target.y * start.w) / y_denominator
|
||||
};
|
||||
distance
|
||||
.is_finite()
|
||||
.then_some(origin + direction * distance as f64)
|
||||
}
|
||||
|
||||
fn is_added_polyline_vertex(
|
||||
original: &AcadEntityType,
|
||||
current: &AcadEntityType,
|
||||
@@ -422,9 +480,18 @@ impl OpenCADStudio {
|
||||
is_translate: bool,
|
||||
world: glam::DVec3,
|
||||
) -> GripEdit {
|
||||
let axis = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, grip)| *owner == handle && grip.id == grip_id)
|
||||
.and_then(|(_, grip)| grip.axis);
|
||||
if !self.tabs[i].hot_grips.contains(&(handle, grip_id)) {
|
||||
self.tabs[i].hot_grips.clear();
|
||||
return GripEdit::single(handle, grip_id, is_translate, world);
|
||||
let mut edit = GripEdit::single(handle, grip_id, is_translate, world);
|
||||
edit.axis = axis;
|
||||
return edit;
|
||||
}
|
||||
|
||||
let mut targets: Vec<GripTarget> = self.tabs[i]
|
||||
@@ -459,14 +526,18 @@ impl OpenCADStudio {
|
||||
}
|
||||
});
|
||||
if targets.is_empty() {
|
||||
return GripEdit::single(handle, grip_id, is_translate, world);
|
||||
let mut edit = GripEdit::single(handle, grip_id, is_translate, world);
|
||||
edit.axis = axis;
|
||||
return edit;
|
||||
}
|
||||
let axis = (targets.len() == 1).then_some(axis).flatten();
|
||||
GripEdit {
|
||||
handle,
|
||||
grip_id,
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
mode: GripEditMode::Stretch,
|
||||
axis,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
@@ -1244,6 +1315,18 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(axis) = grip.axis {
|
||||
snapped = cursor_on_projected_axis(
|
||||
p,
|
||||
bounds,
|
||||
view_rot,
|
||||
eye,
|
||||
grip.origin_world,
|
||||
axis,
|
||||
)
|
||||
.unwrap_or(snapped);
|
||||
}
|
||||
|
||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
// The overlay builds the active tracking guide from `otrack_active.base`
|
||||
// to `last_cursor_world`. Keep it synchronized with the actual point used
|
||||
@@ -1734,6 +1817,14 @@ impl OpenCADStudio {
|
||||
}
|
||||
pt
|
||||
};
|
||||
let effective = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.cursor_axis())
|
||||
.and_then(|(origin, direction)| {
|
||||
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
|
||||
})
|
||||
.unwrap_or(effective);
|
||||
// Dynamic-input locked fields constrain the preview point
|
||||
// (#356): a typed angle pins the direction, a typed
|
||||
// distance pins the radius — the same resolution the Enter
|
||||
@@ -2737,6 +2828,9 @@ impl OpenCADStudio {
|
||||
let history_originals = std::mem::take(&mut self.grip_history_originals);
|
||||
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||
if !handles.is_empty() {
|
||||
for &handle in &handles {
|
||||
let _ = self.tabs[i].scene.finalize_solid_history(handle);
|
||||
}
|
||||
if !originals.is_empty() {
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
@@ -2998,6 +3092,14 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
pt = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.cursor_axis())
|
||||
.and_then(|(origin, direction)| {
|
||||
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
|
||||
})
|
||||
.unwrap_or(pt);
|
||||
// A click while dynamic-input fields hold typed values
|
||||
// commits the CONSTRAINED point — the same resolution
|
||||
// the preview shows and Enter would commit (#356).
|
||||
|
||||
@@ -151,6 +151,7 @@ impl OpenCADStudio {
|
||||
is_midpoint: false,
|
||||
shape: GripShape::Triangle,
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
self.tabs[i].selected_grip_handles.push(handle);
|
||||
self.tabs[i].visibility_grip = Some(VisibilityGrip {
|
||||
|
||||
@@ -1621,6 +1621,11 @@ pub trait CadCommand: Send {
|
||||
/// during TRIM and trims during EXTEND, #336). Default no-op.
|
||||
fn set_shift(&mut self, _shift: bool) {}
|
||||
|
||||
/// Constrain the cursor to a construction axis for this step.
|
||||
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Mid-command Ctrl+Z: a multi-point drawing command can take the undo
|
||||
/// itself (PLINE pops its last vertex) instead of the document undo
|
||||
/// swallowing the whole in-progress object. `None` (default) lets the
|
||||
|
||||
@@ -423,6 +423,7 @@ pub fn square_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||
is_midpoint: false,
|
||||
shape: GripShape::Square,
|
||||
dir: None,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +436,7 @@ pub fn center_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||
is_midpoint: true,
|
||||
shape: GripShape::Square,
|
||||
dir: None,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +449,7 @@ pub fn circle_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||
is_midpoint: true,
|
||||
shape: GripShape::Circle,
|
||||
dir: None,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +463,7 @@ pub fn rectangle_grip(id: usize, world: glam::DVec3, dir: [f32; 2]) -> GripDef {
|
||||
is_midpoint: true,
|
||||
shape: GripShape::Rectangle,
|
||||
dir: Some(dir),
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +475,7 @@ pub fn triangle_grip(id: usize, world: glam::DVec3) -> GripDef {
|
||||
is_midpoint: false,
|
||||
shape: GripShape::Triangle,
|
||||
dir: None,
|
||||
axis: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,8 +102,7 @@ impl PrimitiveCommand {
|
||||
}
|
||||
|
||||
fn cursor_height(&self, point: DVec3) -> f64 {
|
||||
(self.plane.to_local(point) - self.pts[0])
|
||||
.length()
|
||||
(self.plane.to_local(point).z - self.pts[0].z)
|
||||
.max(1e-6)
|
||||
}
|
||||
|
||||
@@ -218,11 +217,15 @@ impl PrimitiveCommand {
|
||||
}
|
||||
Shape::Torus => {
|
||||
let c = self.pts[0];
|
||||
let major = (self.pts[1] - c).length();
|
||||
let minor = (self.pts[2] - self.pts[1]).length();
|
||||
if major < 1e-6 || minor < 1e-6 {
|
||||
let first = (self.pts[1] - c).length();
|
||||
let second = (self.pts[2] - c).length();
|
||||
let outer = first.max(second);
|
||||
let inner = first.min(second);
|
||||
if inner < 1e-6 || outer - inner < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let major = (outer + inner) * 0.5;
|
||||
let minor = (outer - inner) * 0.5;
|
||||
let center = [c.x, c.y, c.z];
|
||||
(
|
||||
primitives::build_torus(center, major, minor),
|
||||
@@ -238,9 +241,6 @@ impl PrimitiveCommand {
|
||||
let solid = solid?;
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.set_sat_document(&doc);
|
||||
// Edge wires make the solid click-pickable and draw a wireframe over
|
||||
// the shaded mesh.
|
||||
s3d.wires = solid_model::edge_wires(&solid);
|
||||
Some((EntityType::Solid3D(s3d), solid, history))
|
||||
}
|
||||
|
||||
@@ -280,6 +280,15 @@ impl CadCommand for PrimitiveCommand {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||
self.height_step.then(|| {
|
||||
(
|
||||
self.plane.to_world(self.pts[0]),
|
||||
self.plane.z.normalize_or_zero(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
self.shape.name()
|
||||
}
|
||||
@@ -289,12 +298,18 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.height_step {
|
||||
return t!("%{n} Specify height <Enter for default>:", n = n).into_owned();
|
||||
}
|
||||
match (self.shape.radial(), self.pts.len()) {
|
||||
(false, 0) => t!("%{n} Specify first corner:", n = n).into_owned(),
|
||||
(false, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(),
|
||||
(true, 0) => t!("%{n} Specify center point:", n = n).into_owned(),
|
||||
(true, 1) => t!("%{n} Specify radius:", n = n).into_owned(),
|
||||
(true, _) => t!("%{n} Specify tube radius:", n = n).into_owned(),
|
||||
match (self.shape, self.pts.len()) {
|
||||
(Shape::Torus, 0) => t!("%{n} Specify center point:", n = n).into_owned(),
|
||||
(Shape::Torus, 1) => t!("%{n} Specify outer radius:", n = n).into_owned(),
|
||||
(Shape::Torus, _) => t!("%{n} Specify inner radius:", n = n).into_owned(),
|
||||
(shape, 0) if shape.radial() => {
|
||||
t!("%{n} Specify center point:", n = n).into_owned()
|
||||
}
|
||||
(shape, _) if shape.radial() => {
|
||||
t!("%{n} Specify radius:", n = n).into_owned()
|
||||
}
|
||||
(_, 0) => t!("%{n} Specify first corner:", n = n).into_owned(),
|
||||
(_, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,10 +395,9 @@ fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
|
||||
let r = (pts[1] - c).length();
|
||||
circle_points(&mut points, c, r);
|
||||
if shape == Shape::Torus && pts.len() >= 3 {
|
||||
// outer ring at major + minor for a quick torus hint
|
||||
let minor = (pts[2] - pts[1]).length();
|
||||
let inner = (pts[2] - c).length();
|
||||
points.push([f32::NAN; 3]);
|
||||
circle_points(&mut points, c, r + minor);
|
||||
circle_points(&mut points, c, inner);
|
||||
}
|
||||
} else {
|
||||
let (a, b) = (pts[0], pts[1]);
|
||||
@@ -423,7 +437,7 @@ fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel {
|
||||
let low = [
|
||||
DVec3::new(x0, y0, a.z),
|
||||
DVec3::new(x1, y0, a.z),
|
||||
DVec3::new(x1, y0, a.z + height),
|
||||
DVec3::new(x0, y0, a.z + height),
|
||||
];
|
||||
let high = low.map(|point| DVec3::new(point.x, y1, point.z));
|
||||
push_loop(&mut points, &low);
|
||||
|
||||
+14
-14
@@ -2673,30 +2673,28 @@ impl Scene {
|
||||
})
|
||||
}
|
||||
|
||||
/// Re-evaluate every cached mesh's color through `render_style` so a
|
||||
/// Register a Model-tab solid: cache its B-rep (for boolean ops and the
|
||||
/// exact-geometry save path) and tessellate it into the shaded mesh
|
||||
/// pipeline under `handle`. The body is in the same offset-relative frame
|
||||
/// the mesh pipeline uses, so the mesh is stored as-is (Model-tab geometry
|
||||
/// is authored at world_offset 0).
|
||||
/// Cache and display a Model-tab kernel solid.
|
||||
pub fn register_solid_model(&mut self, handle: Handle, solid: cadkernel::brep::Body) {
|
||||
if let Some(center) = crate::scene::model::solid_model::centre(&solid) {
|
||||
let color = self
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|e| self.render_style(e).0)
|
||||
.unwrap_or([0.8, 0.8, 0.85, 1.0]);
|
||||
if let Some((mut set, wires, center)) =
|
||||
crate::scene::model::solid_model::display_from_solid(&solid, color)
|
||||
{
|
||||
if let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) {
|
||||
entity.point_of_reference = acadrust::types::Vector3::new(
|
||||
center[0], center[1], center[2],
|
||||
);
|
||||
entity.wires = wires;
|
||||
entity.silhouettes.clear();
|
||||
}
|
||||
}
|
||||
let entity = self.document.get_entity(handle);
|
||||
let color = entity
|
||||
.map(|e| self.render_style(e).0)
|
||||
.unwrap_or([0.8, 0.8, 0.85, 1.0]);
|
||||
if let Some(mut set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) {
|
||||
let name = handle.value().to_string();
|
||||
for mesh in &mut set.lods {
|
||||
mesh.name = name.clone();
|
||||
}
|
||||
if let Some(entity) = entity {
|
||||
if let Some(entity) = self.document.get_entity(handle) {
|
||||
crate::scene::model::material_model::resolve_material_with_base(
|
||||
&self.document,
|
||||
entity,
|
||||
@@ -2716,6 +2714,8 @@ impl Scene {
|
||||
);
|
||||
}
|
||||
self.meshes.insert(handle, set);
|
||||
} else {
|
||||
self.meshes.remove(&handle);
|
||||
}
|
||||
self.solid_models.insert(handle, solid);
|
||||
// Only this solid's mesh changed — report just its handle so the mesh /
|
||||
|
||||
@@ -105,6 +105,8 @@ pub struct GripDef {
|
||||
/// along its segment. `None` for shapes that don't need rotation
|
||||
/// (Square, Triangle in non-directional contexts).
|
||||
pub dir: Option<[f32; 2]>,
|
||||
/// World-space axis that constrains this grip's drag.
|
||||
pub axis: Option<glam::DVec3>,
|
||||
}
|
||||
|
||||
/// How to apply a grip drag result.
|
||||
|
||||
@@ -18,16 +18,16 @@ pub const GRIP_LENGTH: usize = 10_001;
|
||||
pub const GRIP_WIDTH: usize = 10_002;
|
||||
pub const GRIP_HEIGHT: usize = 10_003;
|
||||
pub const GRIP_RADIUS: usize = 10_004;
|
||||
pub const GRIP_MAJOR_RADIUS: usize = 10_005;
|
||||
pub const GRIP_MINOR_RADIUS: usize = 10_006;
|
||||
pub const GRIP_OUTER_RADIUS: usize = 10_005;
|
||||
pub const GRIP_INNER_RADIUS: usize = 10_006;
|
||||
pub const GRIP_SIDES: usize = 10_007;
|
||||
|
||||
pub const PROP_LENGTH: &str = "solid_history_length";
|
||||
pub const PROP_WIDTH: &str = "solid_history_width";
|
||||
pub const PROP_HEIGHT: &str = "solid_history_height";
|
||||
pub const PROP_RADIUS: &str = "solid_history_radius";
|
||||
pub const PROP_MAJOR_RADIUS: &str = "solid_history_major_radius";
|
||||
pub const PROP_MINOR_RADIUS: &str = "solid_history_minor_radius";
|
||||
pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius";
|
||||
pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius";
|
||||
pub const PROP_SIDES: &str = "solid_history_sides";
|
||||
|
||||
fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property {
|
||||
@@ -62,14 +62,14 @@ pub fn primitive_properties(
|
||||
)],
|
||||
SolidHistoryOperation::Torus(value) => vec![
|
||||
history_prop(
|
||||
t!("Major Radius").as_ref(),
|
||||
PROP_MAJOR_RADIUS,
|
||||
value.major_radius,
|
||||
t!("Outer Radius").as_ref(),
|
||||
PROP_OUTER_RADIUS,
|
||||
value.major_radius + value.minor_radius,
|
||||
),
|
||||
history_prop(
|
||||
t!("Minor Radius").as_ref(),
|
||||
PROP_MINOR_RADIUS,
|
||||
value.minor_radius,
|
||||
t!("Inner Radius").as_ref(),
|
||||
PROP_INNER_RADIUS,
|
||||
(value.major_radius - value.minor_radius).max(0.0),
|
||||
),
|
||||
],
|
||||
SolidHistoryOperation::Pyramid(value) => vec![
|
||||
@@ -92,8 +92,8 @@ pub fn is_primitive_property(field: &str) -> bool {
|
||||
| PROP_WIDTH
|
||||
| PROP_HEIGHT
|
||||
| PROP_RADIUS
|
||||
| PROP_MAJOR_RADIUS
|
||||
| PROP_MINOR_RADIUS
|
||||
| PROP_OUTER_RADIUS
|
||||
| PROP_INNER_RADIUS
|
||||
| PROP_SIDES
|
||||
)
|
||||
}
|
||||
@@ -132,11 +132,33 @@ pub fn apply_primitive_property(
|
||||
SolidHistoryOperation::Sphere(value) if field == PROP_RADIUS => {
|
||||
value.radius = positive().unwrap_or(value.radius);
|
||||
}
|
||||
SolidHistoryOperation::Torus(value) => match field {
|
||||
PROP_MAJOR_RADIUS => value.major_radius = positive().unwrap_or(value.major_radius),
|
||||
PROP_MINOR_RADIUS => value.minor_radius = positive().unwrap_or(value.minor_radius),
|
||||
_ => return false,
|
||||
},
|
||||
SolidHistoryOperation::Torus(value) => {
|
||||
let outer = value.major_radius + value.minor_radius;
|
||||
let inner = (value.major_radius - value.minor_radius).max(0.0);
|
||||
match field {
|
||||
PROP_OUTER_RADIUS => {
|
||||
let Some(next_outer) = positive() else {
|
||||
return false;
|
||||
};
|
||||
if next_outer <= inner {
|
||||
return false;
|
||||
}
|
||||
value.major_radius = (next_outer + inner) * 0.5;
|
||||
value.minor_radius = (next_outer - inner) * 0.5;
|
||||
}
|
||||
PROP_INNER_RADIUS => {
|
||||
let Some(next_inner) = positive() else {
|
||||
return false;
|
||||
};
|
||||
if next_inner >= outer {
|
||||
return false;
|
||||
}
|
||||
value.major_radius = (outer + next_inner) * 0.5;
|
||||
value.minor_radius = (outer - next_inner) * 0.5;
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
SolidHistoryOperation::Pyramid(value) => match field {
|
||||
PROP_RADIUS => value.radius = positive().unwrap_or(value.radius),
|
||||
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
||||
@@ -232,17 +254,28 @@ fn world_point(transform: [f64; 16], point: [f64; 3]) -> Option<glam::DVec3> {
|
||||
Some(matrix(transform)?.transform_point3(glam::DVec3::from_array(point)))
|
||||
}
|
||||
|
||||
fn world_vector(transform: [f64; 16], vector: [f64; 3]) -> Option<glam::DVec3> {
|
||||
let vector = matrix(transform)?.transform_vector3(glam::DVec3::from_array(vector));
|
||||
(vector.length_squared() > 1e-12).then(|| vector.normalize())
|
||||
}
|
||||
|
||||
fn local_point(transform: [f64; 16], point: glam::DVec3) -> Option<glam::DVec3> {
|
||||
Some(matrix(transform)?.inverse().transform_point3(point))
|
||||
}
|
||||
|
||||
fn grip(id: usize, world: glam::DVec3, shape: GripShape) -> GripDef {
|
||||
fn grip(
|
||||
id: usize,
|
||||
world: glam::DVec3,
|
||||
shape: GripShape,
|
||||
axis: Option<glam::DVec3>,
|
||||
) -> GripDef {
|
||||
GripDef {
|
||||
id,
|
||||
world,
|
||||
is_midpoint: false,
|
||||
shape,
|
||||
dir: None,
|
||||
axis,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,9 +287,14 @@ pub fn primitive_grips(
|
||||
return Vec::new();
|
||||
};
|
||||
let mut grips = Vec::new();
|
||||
let mut add = |id, transform, point, shape| {
|
||||
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
||||
if let Some(world) = world_point(transform, point) {
|
||||
grips.push(grip(id, world, shape));
|
||||
grips.push(grip(
|
||||
id,
|
||||
world,
|
||||
shape,
|
||||
axis.and_then(|vector| world_vector(transform, vector)),
|
||||
));
|
||||
}
|
||||
};
|
||||
match operation {
|
||||
@@ -266,18 +304,21 @@ pub fn primitive_grips(
|
||||
value.base.transform,
|
||||
[value.length, value.width * 0.5, 0.0],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_WIDTH,
|
||||
value.base.transform,
|
||||
[value.length * 0.5, value.width, 0.0],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_HEIGHT,
|
||||
value.base.transform,
|
||||
[value.length * 0.5, value.width * 0.5, value.height],
|
||||
GripShape::Square,
|
||||
Some([0.0, 0.0, 1.0]),
|
||||
);
|
||||
}
|
||||
SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => {
|
||||
@@ -286,12 +327,14 @@ pub fn primitive_grips(
|
||||
value.base.transform,
|
||||
[value.major_radius, 0.0, value.height * 0.5],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_HEIGHT,
|
||||
value.base.transform,
|
||||
[0.0, 0.0, value.height],
|
||||
GripShape::Square,
|
||||
Some([0.0, 0.0, 1.0]),
|
||||
);
|
||||
}
|
||||
SolidHistoryOperation::Sphere(value) => add(
|
||||
@@ -299,19 +342,26 @@ pub fn primitive_grips(
|
||||
value.base.transform,
|
||||
[value.radius, 0.0, 0.0],
|
||||
GripShape::Square,
|
||||
None,
|
||||
),
|
||||
SolidHistoryOperation::Torus(value) => {
|
||||
add(
|
||||
GRIP_MAJOR_RADIUS,
|
||||
value.base.transform,
|
||||
[value.major_radius, 0.0, 0.0],
|
||||
GripShape::Square,
|
||||
);
|
||||
add(
|
||||
GRIP_MINOR_RADIUS,
|
||||
GRIP_OUTER_RADIUS,
|
||||
value.base.transform,
|
||||
[value.major_radius + value.minor_radius, 0.0, 0.0],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_INNER_RADIUS,
|
||||
value.base.transform,
|
||||
[
|
||||
(value.major_radius - value.minor_radius).max(0.0),
|
||||
0.0,
|
||||
0.0,
|
||||
],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
}
|
||||
SolidHistoryOperation::Pyramid(value) => {
|
||||
@@ -320,12 +370,14 @@ pub fn primitive_grips(
|
||||
value.base.transform,
|
||||
[value.radius, 0.0, 0.0],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_HEIGHT,
|
||||
value.base.transform,
|
||||
[0.0, 0.0, value.height],
|
||||
GripShape::Square,
|
||||
Some([0.0, 0.0, 1.0]),
|
||||
);
|
||||
let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians();
|
||||
add(
|
||||
@@ -333,6 +385,7 @@ pub fn primitive_grips(
|
||||
value.base.transform,
|
||||
[value.radius * angle.cos(), value.radius * angle.sin(), 0.0],
|
||||
GripShape::Triangle,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
@@ -360,7 +413,7 @@ pub fn apply_primitive_grip(
|
||||
match grip_id {
|
||||
GRIP_LENGTH => value.length = positive(local.x),
|
||||
GRIP_WIDTH => value.width = positive(local.y),
|
||||
GRIP_HEIGHT => value.height = positive(local.z),
|
||||
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
@@ -372,7 +425,7 @@ pub fn apply_primitive_grip(
|
||||
value.minor_radius = radius;
|
||||
value.x_radius = radius;
|
||||
}
|
||||
GRIP_HEIGHT => value.height = positive(local.z),
|
||||
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
@@ -380,19 +433,29 @@ pub fn apply_primitive_grip(
|
||||
value.radius = local.length().max(1e-6);
|
||||
}
|
||||
SolidHistoryOperation::Torus(value) => match grip_id {
|
||||
GRIP_MAJOR_RADIUS => {
|
||||
value.major_radius = local.x.hypot(local.y).max(1e-6)
|
||||
GRIP_OUTER_RADIUS => {
|
||||
let outer = local.x.hypot(local.y).max(1e-6);
|
||||
let inner = (value.major_radius - value.minor_radius).max(1e-6);
|
||||
if outer <= inner {
|
||||
return false;
|
||||
}
|
||||
value.major_radius = (outer + inner) * 0.5;
|
||||
value.minor_radius = (outer - inner) * 0.5;
|
||||
}
|
||||
GRIP_MINOR_RADIUS => {
|
||||
value.minor_radius = (local.x.hypot(local.y) - value.major_radius)
|
||||
.abs()
|
||||
.max(1e-6)
|
||||
GRIP_INNER_RADIUS => {
|
||||
let inner = local.x.hypot(local.y).max(1e-6);
|
||||
let outer = value.major_radius + value.minor_radius;
|
||||
if inner >= outer {
|
||||
return false;
|
||||
}
|
||||
value.major_radius = (outer + inner) * 0.5;
|
||||
value.minor_radius = (outer - inner) * 0.5;
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
SolidHistoryOperation::Pyramid(value) => match grip_id {
|
||||
GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6),
|
||||
GRIP_HEIGHT => value.height = positive(local.z),
|
||||
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||
GRIP_SIDES => {
|
||||
let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU);
|
||||
value.sides = (angle.to_degrees() / 5.0).round() as i32;
|
||||
|
||||
@@ -211,11 +211,11 @@ pub fn edge_wires(body: &Body) -> Vec<acadrust::entities::Wire> {
|
||||
use acadrust::types::Vector3;
|
||||
tessellation(body)
|
||||
.edges
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|edge| {
|
||||
acadrust::entities::Wire::from_points(
|
||||
edge.positions
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p[0], p[1], p[2]))
|
||||
.collect(),
|
||||
)
|
||||
@@ -254,7 +254,13 @@ pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> {
|
||||
/// Tessellate a `Body` into a single-LOD `MeshLodSet` (world-space, before
|
||||
/// world_offset is applied by the caller).
|
||||
pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
let tessellation = tessellation(body);
|
||||
mesh_from_tessellation(tessellation(body), color)
|
||||
}
|
||||
|
||||
fn mesh_from_tessellation(
|
||||
tessellation: brep::mesh::BodyMesh,
|
||||
color: [f32; 4],
|
||||
) -> Option<MeshLodSet> {
|
||||
let silhouette = tessellation.silhouette_source();
|
||||
let mesh = tessellation.mesh;
|
||||
if mesh.is_empty() {
|
||||
@@ -313,12 +319,37 @@ pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
Some(set)
|
||||
}
|
||||
|
||||
pub fn display_from_solid(
|
||||
body: &Body,
|
||||
color: [f32; 4],
|
||||
) -> Option<(MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3])> {
|
||||
use acadrust::types::Vector3;
|
||||
let tessellation = tessellation(body);
|
||||
let center = mesh_center(&tessellation.mesh)?;
|
||||
let wires = tessellation
|
||||
.edges
|
||||
.iter()
|
||||
.map(|edge| {
|
||||
acadrust::entities::Wire::from_points(
|
||||
edge.positions
|
||||
.iter()
|
||||
.map(|point| Vector3::new(point[0], point[1], point[2]))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some((mesh_from_tessellation(tessellation, color)?, wires, center))
|
||||
}
|
||||
|
||||
/// The middle of a body, for a caller needing a point to turn or scale about.
|
||||
///
|
||||
/// Read off the mesh rather than `body_bounds`, which refuses a face that
|
||||
/// wraps a closed surface — a sphere is one such face and has no box at all.
|
||||
pub fn centre(body: &Body) -> Option<[f64; 3]> {
|
||||
let mesh = tessellation(body).mesh;
|
||||
mesh_center(&tessellation(body).mesh)
|
||||
}
|
||||
|
||||
fn mesh_center(mesh: &brep::mesh::Mesh) -> Option<[f64; 3]> {
|
||||
if mesh.positions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
+51
-8
@@ -728,7 +728,6 @@ impl Scene {
|
||||
let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else {
|
||||
return false;
|
||||
};
|
||||
let wires = crate::scene::model::solid_model::edge_wires(&body);
|
||||
self.record_solid_history_before(handle);
|
||||
if self
|
||||
.document
|
||||
@@ -741,8 +740,46 @@ impl Scene {
|
||||
return false;
|
||||
};
|
||||
entity.set_sat_document(&document);
|
||||
entity.wires = wires;
|
||||
entity.silhouettes.clear();
|
||||
self.register_solid_model(handle, body);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn finalize_solid_history(&mut self, handle: Handle) -> bool {
|
||||
let Some(operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
||||
return false;
|
||||
};
|
||||
let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else {
|
||||
return false;
|
||||
};
|
||||
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
|
||||
return false;
|
||||
};
|
||||
entity.set_sat_document(&document);
|
||||
self.register_solid_model(handle, body);
|
||||
true
|
||||
}
|
||||
|
||||
fn preview_solid_history(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
operation: acadrust::objects::SolidHistoryOperation,
|
||||
) -> bool {
|
||||
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
||||
return false;
|
||||
};
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(EntityType::Solid3D(_)) = self.document.get_entity(handle) else {
|
||||
return false;
|
||||
};
|
||||
self.register_solid_model(handle, body);
|
||||
true
|
||||
}
|
||||
@@ -761,12 +798,18 @@ impl Scene {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if self.rebuild_solid_history(handle, operation.clone()) {
|
||||
if let EntityTransform::Translate(delta) = transform {
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.translate_solid_geometry(handle, delta.to_array());
|
||||
return true;
|
||||
}
|
||||
self.record_solid_history_before(handle);
|
||||
let _ = self.document.update_solid_history(handle, operation);
|
||||
false
|
||||
self.rebuild_solid_history(handle, operation)
|
||||
}
|
||||
|
||||
fn apply_solid_history_grip(
|
||||
@@ -785,7 +828,7 @@ impl Scene {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
self.rebuild_solid_history(handle, operation)
|
||||
self.preview_solid_history(handle, operation)
|
||||
}
|
||||
|
||||
pub fn apply_solid_history_property(
|
||||
|
||||
@@ -26,6 +26,8 @@ pub struct GripEdit {
|
||||
pub last_world: DVec3,
|
||||
/// How cursor movement modifies the selected grip.
|
||||
pub mode: GripEditMode,
|
||||
/// Optional world-space drag axis.
|
||||
pub axis: Option<DVec3>,
|
||||
/// Every hot grip moved by this edit. A normal grip edit contains one target.
|
||||
pub targets: Vec<GripTarget>,
|
||||
}
|
||||
@@ -57,6 +59,7 @@ impl GripEdit {
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
mode: GripEditMode::Stretch,
|
||||
axis: None,
|
||||
targets: vec![GripTarget {
|
||||
handle,
|
||||
grip_id,
|
||||
|
||||
Reference in new issue
Block a user