Construction axes and points in the viewport: axes dashed in the construction colour across the model (past it either side), points as small stars; axes are picked like the origin axes and work as axes in dialogs (pattern directions, revolve, rotate); hidden like construction planes; timeline icons
Fixes #16 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
cbdc4fe0d8
commit
90fde44c32
4 files changed
+103
-8
No files matched your search
@@ -2286,14 +2286,13 @@ fn curves_sketch(app: &SolveApp, ids: &[String], not: Option<u64>) -> Option<u64
|
||||
/// sketch line).
|
||||
pub fn axis_of(app: &SolveApp, sel: &Sel) -> Option<(Vec3, Vec3)> {
|
||||
match sel {
|
||||
Sel::Axis { name } => Some((
|
||||
Vec3::ZERO,
|
||||
match name.as_str() {
|
||||
"X" => Vec3::X,
|
||||
"Y" => Vec3::Y,
|
||||
_ => Vec3::Z,
|
||||
},
|
||||
)),
|
||||
Sel::Axis { name } => match name.as_str() {
|
||||
"X" => Some((Vec3::ZERO, Vec3::X)),
|
||||
"Y" => Some((Vec3::ZERO, Vec3::Y)),
|
||||
"Z" => Some((Vec3::ZERO, Vec3::Z)),
|
||||
// A construction axis.
|
||||
n => solvecraft_engine::view::construction_axes(&app.session).into_iter().find(|(_, an, _, _)| an == n).map(|(_, _, o, d)| (o, d)),
|
||||
},
|
||||
Sel::SketchCurve { id } => {
|
||||
let st = app.session.world_state();
|
||||
let sid = curves_sketch(app, std::slice::from_ref(id), None)?;
|
||||
|
||||
@@ -60,6 +60,8 @@ pub struct Tokens {
|
||||
pub axis_z: Color32,
|
||||
/// Construction planes.
|
||||
pub construction_plane: Color32,
|
||||
/// Construction axes and points.
|
||||
pub construction_geom: Color32,
|
||||
// ---- bodies and live previews ----
|
||||
pub body: Color32,
|
||||
pub body_edge: Color32,
|
||||
@@ -164,6 +166,7 @@ impl Tokens {
|
||||
axis_y: Color32::from_rgb(0, 255, 0),
|
||||
axis_z: Color32::from_rgb(2, 2, 248),
|
||||
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
|
||||
construction_geom: Color32::from_rgb(232, 140, 48),
|
||||
body: Color32::from_rgb(176, 186, 198),
|
||||
body_edge: Color32::from_rgb(40, 44, 52),
|
||||
manipulator: Color32::from_rgb(40, 110, 220),
|
||||
|
||||
@@ -23,6 +23,8 @@ fn icon_of(k: &FeatureKind) -> &'static str {
|
||||
FeatureKind::Pattern { .. } => "pattern_circ",
|
||||
FeatureKind::Mirror { .. } => "mirror",
|
||||
FeatureKind::ConstructionPlane { .. } => "plane",
|
||||
FeatureKind::ConstructionAxis { .. } => "axis",
|
||||
FeatureKind::ConstructionPoint { .. } => "point",
|
||||
FeatureKind::Hole { .. } => "hole",
|
||||
FeatureKind::Loft { .. } => "loft",
|
||||
FeatureKind::Sweep { .. } => "sweep",
|
||||
|
||||
@@ -404,6 +404,43 @@ pub fn origin_axes(app: &SolveApp) -> Vec<(&'static str, Vec3)> {
|
||||
[("X", Vec3::X), ("Y", Vec3::Y), ("Z", Vec3::Z)].into_iter().filter(|(n, _)| !app.ui.hidden_origin.iter().any(|h| h == n)).collect()
|
||||
}
|
||||
|
||||
/// Construction axes shown, as segments: across the model where the axis passes it (a little
|
||||
/// past it either side), at least the origin axes' length around the axis's origin: (name,
|
||||
/// from, to).
|
||||
pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
|
||||
let h = origin_size(app.cam.half_height()) * AXIS_LEN;
|
||||
let bb = solvecraft_engine::view::bounds(&app.session);
|
||||
solvecraft_engine::view::construction_axes(&app.session)
|
||||
.into_iter()
|
||||
.filter(|(_, n, _, _)| !app.ui.hidden_origin.contains(n))
|
||||
.map(|(_, name, o, d)| {
|
||||
let (mut lo, mut hi) = (-h, h);
|
||||
if !bb.is_empty() {
|
||||
for i in 0..8 {
|
||||
let c = Vec3::new(
|
||||
if i & 1 == 0 { bb.min.x } else { bb.max.x },
|
||||
if i & 2 == 0 { bb.min.y } else { bb.max.y },
|
||||
if i & 4 == 0 { bb.min.z } else { bb.max.z },
|
||||
);
|
||||
let t = (c - o).dot(d);
|
||||
lo = lo.min(t - h * 0.3);
|
||||
hi = hi.max(t + h * 0.3);
|
||||
}
|
||||
}
|
||||
(name, o + d * lo, o + d * hi)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Construction points shown: (name, position).
|
||||
pub fn construction_points(app: &SolveApp) -> Vec<(String, Vec3)> {
|
||||
solvecraft_engine::view::construction_points(&app.session)
|
||||
.into_iter()
|
||||
.filter(|(_, n, _)| !app.ui.hidden_origin.contains(n))
|
||||
.map(|(_, n, p)| (n, p))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Construction planes drawn as squares around their origin: (name, normal, quad).
|
||||
pub fn construction_quads(app: &SolveApp) -> Vec<(String, Vec3, [Vec3; 4])> {
|
||||
let h = origin_size(app.cam.half_height()) * 1.2;
|
||||
@@ -648,6 +685,15 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Construction axes, picked like the origin axes.
|
||||
for (name, a, b) in construction_axis_segments(app) {
|
||||
if let (Some(a), Some(b)) = (proj.to_screen(a), proj.to_screen(b)) {
|
||||
let dd = seg_dist(pos, a, b);
|
||||
if dd < 5.0 && besta.as_ref().is_none_or(|(bd, _)| dd < *bd) {
|
||||
besta = Some((dd, Hit::Axis { name }));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.extend(besta.map(|x| x.1));
|
||||
// Surfaces by depth: profiles (slightly preferred over a face they lie on), faces, planes.
|
||||
let mut surf: Vec<(f64, Hit)> = Vec::new();
|
||||
@@ -971,6 +1017,27 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
a += dash * 2.0;
|
||||
}
|
||||
}
|
||||
// Construction axes (dashed, the length of the origin axes either side) and points (a
|
||||
// small star), in the construction colour.
|
||||
for (name, a, b) in construction_axis_segments(app) {
|
||||
let on = sel.iter().any(|x| matches!(x, Sel::Axis { name: n } if *n == name)) || matches!(hover, Some(Hit::Axis { name: n }) if *n == name);
|
||||
let col = c4(if on { t.sel_edge } else { t.construction_geom });
|
||||
let w = if on { 3.0 } else { 1.6 };
|
||||
let len = a.dist(b);
|
||||
let dash = size * 0.08;
|
||||
let mut s = 0.0;
|
||||
while s < len {
|
||||
let e = (s + dash).min(len);
|
||||
sc.line(a.lerp(b, s / len).to_f32(), a.lerp(b, e / len).to_f32(), col, w, false);
|
||||
s += dash * 1.6;
|
||||
}
|
||||
}
|
||||
for (_, p) in construction_points(app) {
|
||||
let r = size * 0.04;
|
||||
for d in [Vec3::X, Vec3::Y, Vec3::Z] {
|
||||
sc.line((p - d * r).to_f32(), (p + d * r).to_f32(), c4(t.construction_geom), 2.0, true);
|
||||
}
|
||||
}
|
||||
// Selected things.
|
||||
for x in &sel {
|
||||
match x {
|
||||
@@ -2113,6 +2180,30 @@ mod tests {
|
||||
assert_eq!(regions, 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_axes_span_the_model_and_points_show() {
|
||||
let mut s = solvecraft_engine::Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
s.execute("construct.axis.two_points", &json!({"a": {"vertex": [0, 0, 20]}, "b": {"vertex": [40, 30, 20]}})).unwrap();
|
||||
s.execute("construct.point.vertex", &json!({"point": {"vertex": [40, 0, 20]}})).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
let axes = construction_axis_segments(&app);
|
||||
assert_eq!(axes.len(), 1);
|
||||
let (name, a, b) = axes[0].clone();
|
||||
// The segment runs through both points and past them.
|
||||
let d = (b - a).normalized().unwrap();
|
||||
for p in [Vec3::new(0.0, 0.0, 20.0), Vec3::new(40.0, 30.0, 20.0)] {
|
||||
let t = (p - a).dot(d);
|
||||
assert!(t > 0.0 && t < a.dist(b) && (a + d * t).dist(p) < 1e-6, "{p:?} not on {a:?}..{b:?}");
|
||||
}
|
||||
assert_eq!(construction_points(&app).len(), 1);
|
||||
// It is an axis for dialogs (pattern directions, revolve axes), and hiding it hides it.
|
||||
let (o, dir) = crate::dialogs::axis_of(&app, &Sel::Axis { name: name.clone() }).unwrap();
|
||||
assert!(o.dist(Vec3::new(0.0, 0.0, 20.0)) < 1e-6 && dir.dist(d) < 1e-6);
|
||||
app.ui.hidden_origin.push(name);
|
||||
assert!(construction_axis_segments(&app).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_cube_zones() {
|
||||
// A square face 100 px wide, corners in order −−, +−, ++, −+ (screen y down).
|
||||
|
||||
Reference in new issue
Block a user