fix(model): correct primitive creation and grips

This commit is contained in:
Hakan Seven committed 2026-08-12 00:03:27 +03:00
1 parent 59ca0f693f
commit c4dfa7567d
13 files changed
+353 -84

No files matched your search

+104 -2
View File
@@ -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).
+1
View File
@@ -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 {
+5
View File
@@ -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
+5
View File
@@ -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,
}
}
+32 -18
View File
@@ -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
View File
@@ -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 /
+2
View File
@@ -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.
+99 -36
View File
@@ -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;
+35 -4
View File
@@ -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
View File
@@ -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(
+3
View File
@@ -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,