forked from 1b-agent/opencadstudio-upstream
fix(3d): seat axis steps on 3D snaps; raised bases follow the cursor (#1030)
Height and distance steps (PUSHPULL, CYLINDER, CONE, PYRAMID, BOX, EXTRUDE) replaced the point with the cursor's screen mapping onto the command axis, discarding an acquired 3D object snap. A genuine snap now supplies its own coordinate along the axis, as axis grips already do. Base steps after the first base point (BOX/WEDGE opposite corner and cube size, CYLINDER/CONE/PYRAMID radius) read the cursor on the base plane through that point. A base started on a raised face intersected the ray with world XY instead, so in oblique views the footprint ran far past the mouse. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
89512c2f20
commit
eb84bae779
3 files changed
+61
-2
No files matched your search
@@ -87,6 +87,28 @@ pub(in crate::app) fn snap_keeps_elevation(hit: Option<crate::snap::SnapType>) -
|
|||||||
matches!(hit, Some(t) if t != crate::snap::SnapType::Grid)
|
matches!(hit, Some(t) if t != crate::snap::SnapType::Grid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Point on a command's construction axis (height / distance steps). A
|
||||||
|
/// genuine object snap supplies its own coordinate along the axis, so a
|
||||||
|
/// height or push distance seats on existing geometry — as axis grips do.
|
||||||
|
/// Otherwise the cursor maps onto the axis on screen.
|
||||||
|
fn command_axis_point(
|
||||||
|
snap: Option<crate::snap::SnapResult>,
|
||||||
|
cursor: Point,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
view: glam::Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
|
origin: glam::DVec3,
|
||||||
|
direction: glam::DVec3,
|
||||||
|
) -> Option<glam::DVec3> {
|
||||||
|
match snap.filter(|hit| snap_keeps_elevation(Some(hit.snap_type))) {
|
||||||
|
Some(hit) => {
|
||||||
|
let axis = direction.try_normalize()?;
|
||||||
|
Some(origin + axis * (hit.world - origin).dot(axis))
|
||||||
|
}
|
||||||
|
None => cursor_on_projected_axis(cursor, bounds, view, eye, origin, direction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cursor_on_projected_axis(
|
fn cursor_on_projected_axis(
|
||||||
cursor: Point,
|
cursor: Point,
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
@@ -2393,7 +2415,15 @@ impl OpenCADStudio {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|command| command.cursor_axis())
|
.and_then(|command| command.cursor_axis())
|
||||||
.and_then(|(origin, direction)| {
|
.and_then(|(origin, direction)| {
|
||||||
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
|
command_axis_point(
|
||||||
|
self.tabs[i].snap_result,
|
||||||
|
p,
|
||||||
|
bounds,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or(effective)
|
.unwrap_or(effective)
|
||||||
};
|
};
|
||||||
@@ -3955,7 +3985,7 @@ impl OpenCADStudio {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|command| command.cursor_axis())
|
.and_then(|command| command.cursor_axis())
|
||||||
.and_then(|(origin, direction)| {
|
.and_then(|(origin, direction)| {
|
||||||
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
|
command_axis_point(snap_hit, p, bounds, view_rot, eye, origin, direction)
|
||||||
})
|
})
|
||||||
.unwrap_or(pt);
|
.unwrap_or(pt);
|
||||||
// A click while dynamic-input fields hold typed values
|
// A click while dynamic-input fields hold typed values
|
||||||
|
|||||||
@@ -462,6 +462,16 @@ impl CadCommand for CylinderCommand {
|
|||||||
self.plane = plane;
|
self.plane = plane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The radius step reads the cursor on the base plane through the
|
||||||
|
/// centre, so a base started on a raised face follows the mouse.
|
||||||
|
fn cursor_plane(&self) -> Option<(DVec3, DVec3)> {
|
||||||
|
if !matches!(self.step, Step::BaseRadius | Step::BaseDiameter) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let frame = self.frame?;
|
||||||
|
Some((frame.z.normalize_or_zero(), frame.origin))
|
||||||
|
}
|
||||||
|
|
||||||
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||||
matches!(self.step, Step::Height).then(|| {
|
matches!(self.step, Step::Height).then(|| {
|
||||||
let frame = self.frame.unwrap_or(self.plane);
|
let frame = self.frame.unwrap_or(self.plane);
|
||||||
|
|||||||
@@ -2098,6 +2098,25 @@ impl CadCommand for PrimitiveCommand {
|
|||||||
self.plane = plane;
|
self.plane = plane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Base steps after the first base point read the cursor on the base
|
||||||
|
/// plane through that point, so a base started on a raised face follows
|
||||||
|
/// the mouse instead of the ray's hit on the working plane below.
|
||||||
|
fn cursor_plane(&self) -> Option<(DVec3, DVec3)> {
|
||||||
|
let origin = if self.shape == Shape::Cone {
|
||||||
|
matches!(self.cone_step, ConeStep::BaseRadius | ConeStep::BaseDiameter)
|
||||||
|
.then_some(self.cone_frame?.origin)
|
||||||
|
} else if self.shape == Shape::Pyramid {
|
||||||
|
(self.pyramid_step == PyramidStep::BaseRadius).then_some(self.pyramid_frame?.origin)
|
||||||
|
} else if self.shape.rectangular() {
|
||||||
|
matches!(self.box_step, BoxStep::OppositeCorner | BoxStep::CubeSize)
|
||||||
|
.then(|| self.pts.first().map(|&first| self.plane.to_world(first)))
|
||||||
|
.flatten()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}?;
|
||||||
|
Some((self.plane.z.normalize_or_zero(), origin))
|
||||||
|
}
|
||||||
|
|
||||||
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||||
if self.shape == Shape::Cone {
|
if self.shape == Shape::Cone {
|
||||||
return matches!(self.cone_step, ConeStep::Height | ConeStep::HeightAfterTopRadius)
|
return matches!(self.cone_step, ConeStep::Height | ConeStep::HeightAfterTopRadius)
|
||||||
|
|||||||
Reference in new issue
Block a user