1 parent
bd17eeef0c
commit
987f0be21e
27 files changed
+435
-28
No files matched your search
@@ -781,6 +781,20 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
if !curves.is_empty() {
|
||||
v.push(cmd(app, "sketch.construction", "Normal / Construction").with(json!({ "curves": curves })));
|
||||
}
|
||||
// Infinite lines (#1359): the selected lines switch together.
|
||||
let lines: Vec<String> = curves
|
||||
.iter()
|
||||
.filter(|id| {
|
||||
ss.sketch
|
||||
.curve_index(id)
|
||||
.and_then(|i| ss.sketch.curves.get(i))
|
||||
.is_some_and(|c| matches!(c.kind, solidcraft_engine::sketch::CurveKind::Line { .. }))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if !lines.is_empty() {
|
||||
v.push(cmd(app, "sketch.infinite", "Infinite Line").with(json!({ "curves": lines })));
|
||||
}
|
||||
if curves.len() == 1 && ss.sketch.freeform_offsets.iter().any(|offset| offset.output == curves[0]) {
|
||||
v.push(cmd(app, "sketch.offset.break", "Break Offset Association").with(json!({"curve":curves[0]})));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! An infinite line (#1359) is drawn across the whole view and picked along its whole line,
|
||||
//! and the Sketch Palette's Infinite mode draws one.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::scenario::Harness;
|
||||
use crate::viewport::{Hit, pick, projection};
|
||||
|
||||
/// An XY sketch with a short line from (0, 0) to (2, 0) made infinite, the view zoomed out.
|
||||
fn short_infinite_line() -> Harness {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points": [[0, 0], [2, 0]], "ids": ["inf"]})).unwrap();
|
||||
h.app.run("sketch.infinite", json!({"curves": ["inf"]})).unwrap();
|
||||
h.app.cam.distance = 400.0;
|
||||
h.settle();
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_is_drawn_from_edge_to_edge_of_the_view() {
|
||||
let h = short_infinite_line();
|
||||
let rect = h.app.viewport.rect.unwrap();
|
||||
let proj = projection(&h.app, rect);
|
||||
let st = h.app.session.world_state();
|
||||
let ss = st.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
let i = ss.sketch.curve_index("inf").unwrap();
|
||||
let parts = super::curve_parts(&ss.sketch, i, &ss.plane, &proj, 1.0);
|
||||
let xs: Vec<f32> = parts.iter().flatten().filter_map(|p| proj.to_screen(*p)).map(|p| p.x).collect();
|
||||
let (lo, hi) = xs.iter().fold((f32::MAX, f32::MIN), |(lo, hi), x| (lo.min(*x), hi.max(*x)));
|
||||
// Its two points are a few pixels apart; drawn, it spans the view.
|
||||
assert!(lo <= rect.left() + 2.0 && hi >= rect.right() - 2.0, "drawn from {lo} to {hi} in {rect:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_is_picked_far_beyond_its_two_points() {
|
||||
let h = short_infinite_line();
|
||||
let rect = h.app.viewport.rect.unwrap();
|
||||
let proj = projection(&h.app, rect);
|
||||
let ss_plane = h.app.session.world_state().sketch(h.app.session.active_sketch.unwrap()).unwrap().plane;
|
||||
let far = proj.to_screen(ss_plane.to_world(solidcraft_engine::geom::Vec2::new(150.0, 0.0))).unwrap();
|
||||
assert!(rect.contains(far));
|
||||
let hits = pick(&h.app, &proj, far);
|
||||
assert!(hits.iter().any(|h| matches!(h, Hit::SketchCurve { id, .. } if id == "inf")), "{hits:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infinite_mode_draws_infinite_lines() {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.settle();
|
||||
crate::sketch_palette::set_mode(&mut h.app, 3);
|
||||
let r = h.app.run("sketch.line", json!({"points": [[0, 0], [10, 5]]})).unwrap();
|
||||
crate::sketch_palette::apply_mode(&mut h.app, "sketch.line", &r);
|
||||
let id = r["curves"][0].as_str().unwrap().to_string();
|
||||
let sk = h.app.session.doc.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
let c = sk.curves.get(sk.curve_index(&id).unwrap()).unwrap();
|
||||
assert!(c.infinite && c.construction, "{c:?}");
|
||||
}
|
||||
@@ -26,8 +26,35 @@ pub fn pixel_view(proj: &Proj, ppp: f32) -> PixelCurveView {
|
||||
PixelCurveView { vp: proj.vp, width_px: f64::from(proj.rect.width() * ppp), height_px: f64::from(proj.rect.height() * ppp) }
|
||||
}
|
||||
|
||||
/// The pieces a curve is drawn and picked from. An infinite line (#1359) reaches across the whole
|
||||
/// view: through the point of its line nearest the view's centre, far past the view each way
|
||||
/// (the sampler clips it to the view). Only the display changes: the line keeps its two points.
|
||||
fn display_segs(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj) -> Vec<solidcraft_engine::geom::Seg2> {
|
||||
use solidcraft_engine::geom::Seg2;
|
||||
let segs = sketch.exact_segs(index).unwrap_or_else(|| sketch.segs(index));
|
||||
if !sketch.curves.get(index).is_some_and(|c| c.infinite) {
|
||||
return segs;
|
||||
}
|
||||
let target = proj.cam.target - plane.origin;
|
||||
let centre = Vec2::new(target.dot(plane.x), target.dot(plane.y));
|
||||
let reach = 4.0 * (proj.cam.distance.abs() + proj.cam.half_height().abs()) + 1.0;
|
||||
segs.into_iter()
|
||||
.map(|s| match s {
|
||||
Seg2::Line { a, b } => match (b - a).normalized() {
|
||||
Some(dir) => {
|
||||
let mid = a + dir * (centre - a).dot(dir);
|
||||
let reach = reach + mid.dist(centre);
|
||||
Seg2::Line { a: mid - dir * reach, b: mid + dir * reach }
|
||||
}
|
||||
None => s,
|
||||
},
|
||||
other => other,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn curve_with_budget(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj, ppp: f32, budget: &mut SamplingBudget) -> SampledPath {
|
||||
let segments = sketch.exact_segs(index).unwrap_or_else(|| sketch.segs(index));
|
||||
let segments = display_segs(sketch, index, plane, proj);
|
||||
let mut local = SamplingBudget::new(budget.remaining_vertices.min(4096), budget.remaining_nodes.min(65_536));
|
||||
let initial = local.clone();
|
||||
let mut out = SampledPath { parts: Vec::new(), quality: SamplingQuality::Complete, vertex_count: 0 };
|
||||
@@ -47,7 +74,7 @@ pub fn curve_with_budget(sketch: &Sketch, index: usize, plane: &Plane, proj: &Pr
|
||||
pub fn curve_unclipped(sketch: &Sketch, index: usize, plane: &Plane, proj: &Proj, ppp: f32) -> SampledPath {
|
||||
let mut budget = SamplingBudget::new(4096, 65_536);
|
||||
let mut out = SampledPath { parts: Vec::new(), quality: SamplingQuality::Complete, vertex_count: 0 };
|
||||
for segment in sketch.exact_segs(index).unwrap_or_else(|| sketch.segs(index)) {
|
||||
for segment in display_segs(sketch, index, plane, proj) {
|
||||
let sampled = sample_segment_unclipped(&segment, plane, &pixel_view(proj, ppp), &mut budget);
|
||||
if sampled.quality != SamplingQuality::Complete {
|
||||
out.quality = sampled.quality;
|
||||
@@ -244,3 +271,7 @@ pub fn curve_sample(app: &SolveApp, sketch: u64, index: usize, plane: &Plane, pr
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "infinite_line_display_tests.rs"]
|
||||
mod infinite_line_display_tests;
|
||||
@@ -83,7 +83,7 @@ pub fn dof_text(dof: usize, ok: bool) -> String {
|
||||
}
|
||||
|
||||
/// The line type new curves get in the sketch being edited (0 normal, 1 construction,
|
||||
/// 2 centerline); a new sketch starts normal.
|
||||
/// 2 centerline, 3 infinite); a new sketch starts normal.
|
||||
pub fn mode(app: &SolveApp) -> u8 {
|
||||
let (m, sk) = MODE.with(Cell::get);
|
||||
if sk.is_some() && sk == app.session.active_sketch { m } else { 0 }
|
||||
@@ -95,6 +95,7 @@ pub fn set_mode(app: &mut SolveApp, m: u8) {
|
||||
let hint = match m {
|
||||
1 => "Construction mode: new curves are construction geometry (X or the palette's Normal to leave)",
|
||||
2 => "Centerline mode: new lines are centerlines",
|
||||
3 => "Infinite mode: new lines are infinite construction lines",
|
||||
_ => "Normal mode",
|
||||
};
|
||||
app.set_status(hint, false);
|
||||
@@ -124,7 +125,7 @@ pub fn apply_mode(app: &mut SolveApp, id: &str, result: &Value) {
|
||||
if curves.is_empty() {
|
||||
return;
|
||||
}
|
||||
let lines: Vec<String> = if m == 2 {
|
||||
let lines: Vec<String> = if m == 2 || m == 3 {
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
|
||||
curves
|
||||
@@ -144,7 +145,8 @@ pub fn apply_mode(app: &mut SolveApp, id: &str, result: &Value) {
|
||||
// Part of the drawing's own undo step.
|
||||
let depth = app.session.undo.len();
|
||||
if !lines.is_empty() {
|
||||
let _ = app.session.execute("sketch.centerline", &json!({ "curves": lines, "value": true }));
|
||||
let line_type = if m == 3 { "sketch.infinite" } else { "sketch.centerline" };
|
||||
let _ = app.session.execute(line_type, &json!({ "curves": lines, "value": true }));
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
let _ = app.session.execute("sketch.construction", &json!({ "curves": rest, "value": true }));
|
||||
@@ -158,9 +160,10 @@ fn selected_curves(app: &SolveApp) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// The line types among the selected curves of the active sketch, indexed like the palette's
|
||||
/// buttons (0 normal, 1 construction, 2 centerline); a mixed selection marks each of its types.
|
||||
fn selected_linetypes(app: &SolveApp) -> [bool; 3] {
|
||||
let mut types = [false; 3];
|
||||
/// buttons (0 normal, 1 construction, 2 centerline, 3 infinite); a mixed selection marks each of
|
||||
/// its types.
|
||||
fn selected_linetypes(app: &SolveApp) -> [bool; 4] {
|
||||
let mut types = [false; 4];
|
||||
let curves = selected_curves(app);
|
||||
if curves.is_empty() {
|
||||
return types;
|
||||
@@ -168,7 +171,9 @@ fn selected_linetypes(app: &SolveApp) -> [bool; 3] {
|
||||
let st = app.session.world_state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return types };
|
||||
for c in curves.iter().filter_map(|id| ss.sketch.curve_index(id).and_then(|i| ss.sketch.curves.get(i))) {
|
||||
let k = if c.centerline {
|
||||
let k = if c.infinite {
|
||||
3
|
||||
} else if c.centerline {
|
||||
2
|
||||
} else if c.construction {
|
||||
1
|
||||
@@ -182,7 +187,8 @@ fn selected_linetypes(app: &SolveApp) -> [bool; 3] {
|
||||
types
|
||||
}
|
||||
|
||||
/// Set the line type of the selected curves: 0 normal, 1 construction, 2 centerline (lines).
|
||||
/// Set the line type of the selected curves: 0 normal, 1 construction, 2 centerline, 3 infinite
|
||||
/// (the last two for lines).
|
||||
pub fn set_linetype(app: &mut SolveApp, kind: u8) {
|
||||
let curves = selected_curves(app);
|
||||
if curves.is_empty() {
|
||||
@@ -210,7 +216,14 @@ pub fn set_linetype(app: &mut SolveApp, kind: u8) {
|
||||
}
|
||||
app.run("sketch.construction", json!({ "curves": curves, "value": false }))
|
||||
}
|
||||
1 => app.run("sketch.construction", json!({ "curves": curves, "value": true })),
|
||||
1 => {
|
||||
if !lines.is_empty() {
|
||||
let _ = app.run("sketch.infinite", json!({ "curves": lines, "value": false }));
|
||||
}
|
||||
app.run("sketch.construction", json!({ "curves": curves, "value": true }))
|
||||
}
|
||||
3 if lines.is_empty() => Err("an infinite line must be a line".to_string()),
|
||||
3 => app.run("sketch.infinite", json!({ "curves": lines, "value": true })),
|
||||
_ if lines.is_empty() => Err("a centerline must be a line".to_string()),
|
||||
_ => app.run("sketch.centerline", json!({ "curves": lines, "value": true })),
|
||||
};
|
||||
@@ -430,7 +443,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// Centerline need a round dot and a clear gap to read apart. 30 by
|
||||
// 20 px left too little square room for either.
|
||||
let btn_w = 36.0;
|
||||
let group_w = 3.0 * btn_w + 2.0 * ui.spacing().item_spacing.x;
|
||||
let group_w = 4.0 * btn_w + 3.0 * ui.spacing().item_spacing.x;
|
||||
crate::frame::right_aligned(ui, width, group_w, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// With a selection: its line type, and a click converts it;
|
||||
@@ -442,10 +455,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// construction orange. Normal's icon never changes with the
|
||||
// accent colour. It stays neutral in every case.
|
||||
let current = mode(app);
|
||||
for (i, (label, icon, tip)) in
|
||||
[("—", "normal", "Normal"), ("- -", "construction", "Construction"), ("-·-", "centerline", "Centerline")]
|
||||
.iter()
|
||||
.enumerate()
|
||||
for (i, (label, icon, tip)) in [
|
||||
("—", "normal", "Normal"),
|
||||
("- -", "construction", "Construction"),
|
||||
("-·-", "centerline", "Centerline"),
|
||||
("↔", "infinite", "Infinite"),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let on = if has_curves { selected_types.get(i).copied().unwrap_or(false) } else { current == i as u8 };
|
||||
let tr_tip = app.tr_label(tip);
|
||||
|
||||
@@ -533,7 +533,9 @@ pub(crate) fn paint_preview_curve(
|
||||
mode: u8,
|
||||
) {
|
||||
for curve in &mut sketch.curves {
|
||||
curve.centerline = mode == 2 && matches!(curve.kind, solidcraft_engine::sketch::CurveKind::Line { .. });
|
||||
let line = matches!(curve.kind, solidcraft_engine::sketch::CurveKind::Line { .. });
|
||||
curve.centerline = mode == 2 && line;
|
||||
curve.infinite = mode == 3 && line;
|
||||
curve.construction = mode != 0 && !curve.centerline;
|
||||
}
|
||||
paint_curve(&sketch, 0, plane, painter, proj, stroke, &mut solidcraft_engine::render::SamplingBudget::new(4096, 65_536));
|
||||
|
||||
@@ -807,7 +807,8 @@ fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &std::sync::Arc<solidcra
|
||||
/// Sketch line types share the same world-space dash phase in the scene and overlays.
|
||||
pub(crate) fn sketch_segments(pts: &[Vec3], mode: u8, unit: f64) -> Vec<(Vec3, Vec3)> {
|
||||
match mode {
|
||||
1 => dashes(pts, &[0.3 * unit, 0.2 * unit]),
|
||||
// Construction, and infinite lines (#1359), which are construction too.
|
||||
1 | 3 => dashes(pts, &[0.3 * unit, 0.2 * unit]),
|
||||
2 => dashes(pts, &[0.6 * unit, 0.15 * unit, 0.05 * unit, 0.15 * unit]),
|
||||
_ => pts.windows(2).map(|w| (w[0], w[1])).collect(),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user