diff --git a/crates/doc/src/drawing/balloon_tests.rs b/crates/doc/src/drawing/balloon_tests.rs index 73fcdbb63e..9eccbeea06 100644 --- a/crates/doc/src/drawing/balloon_tests.rs +++ b/crates/doc/src/drawing/balloon_tests.rs @@ -34,7 +34,7 @@ fn ballooned() -> (Drawing, [u64; 2]) { kind, place: ViewPlace::At([150.0, 150.0]), settings: ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() }, - reference_margin_mm: 0.0, + reference_shown_percent: 0.0, })]); let mut added = [0; 2]; for ((path, at), id) in [(vec![3], [40.0, 25.0]), (vec![3, 7], [-35.5, 60.0])].into_iter().zip(&mut added) { diff --git a/crates/doc/src/drawing/dimension_tests.rs b/crates/doc/src/drawing/dimension_tests.rs index e7c2a5ebdc..9335876b28 100644 --- a/crates/doc/src/drawing/dimension_tests.rs +++ b/crates/doc/src/drawing/dimension_tests.rs @@ -37,7 +37,7 @@ fn measured() -> (Drawing, [u64; 3]) { let (view, sheet) = (d.allocate_id(&mut id_source).unwrap(), d.sheets[0].id); let kind = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) }; let settings = ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() }; - d.add_views([Arc::new(View { id: view, sheet, kind, place: ViewPlace::At([150.0, 150.0]), settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id: view, sheet, kind, place: ViewPlace::At([150.0, 150.0]), settings, reference_shown_percent: 0.0 })]); let kinds = [ DimensionKind::Linear { a: corner(&["F1:front|F1:bottom", "F1:front|F1:left"]), diff --git a/crates/doc/src/drawing/view.rs b/crates/doc/src/drawing/view.rs index d57abb0ed7..101428b8da 100644 --- a/crates/doc/src/drawing/view.rs +++ b/crates/doc/src/drawing/view.rs @@ -23,7 +23,7 @@ use solidcraft_geom::{Plane, Vec2}; use solidcraft_render::VisualStyle; use super::{Drawing, add_by_id, in_id_order, require}; -use crate::drawing_view::{MAX_REFERENCE_MARGIN_MM, TangentEdges, ViewDirection, ViewOverrides, ViewSettings}; +use crate::drawing_view::{MAX_REFERENCE_SHOWN_PERCENT, TangentEdges, ViewDirection, ViewOverrides, ViewSettings}; use crate::{DocError, Result, ids}; /// The most views a drawing may have. @@ -46,10 +46,10 @@ pub struct View { /// What it sets itself; each setting left out follows its parent, then the default. #[serde(default, skip_serializing_if = "ViewOverrides::is_empty")] pub settings: ViewOverrides, - /// Model millimetres added on every side of its extents, so reference parts around the model - /// show inside its boundary: 0 to [`MAX_REFERENCE_MARGIN_MM`], each view its own. + /// How far its boundary grows from the model's box towards the box that also holds its reference + /// parts, each side on its own: 0 to [`MAX_REFERENCE_SHOWN_PERCENT`], each view its own. #[serde(default, skip_serializing_if = "is_zero")] - pub reference_margin_mm: f64, + pub reference_shown_percent: f64, } /// What a view draws, and from where. @@ -109,7 +109,7 @@ pub struct ResolvedView { pub reference_style: Setting, pub reference_hides_model: Setting, pub tangent_edges: Setting, - pub reference_margin_mm: f64, + pub reference_shown_percent: f64, } fn is_zero(v: &f64) -> bool { @@ -187,9 +187,9 @@ impl Drawing { fn check_view(&self, view: &View) -> Result<()> { let on = |sheet: u64| format!("it is on sheet {}, which the drawing doesn't have", ids::encode(sheet)); require(self.sheets.iter().any(|s| s.id == view.sheet), || on(view.sheet))?; - let margin = view.reference_margin_mm; - require(margin.is_finite() && (0.0..=MAX_REFERENCE_MARGIN_MM).contains(&margin), || { - format!("its reference margin is 0 to {MAX_REFERENCE_MARGIN_MM} model millimetres") + let percent = view.reference_shown_percent; + require(percent.is_finite() && (0.0..=MAX_REFERENCE_SHOWN_PERCENT).contains(&percent), || { + format!("its reference_shown_percent is 0 to {MAX_REFERENCE_SHOWN_PERCENT}") })?; let place = match view.place { ViewPlace::At([x, y]) => is_paper_coordinate(x) && is_paper_coordinate(y), @@ -237,7 +237,7 @@ impl Drawing { reference_style: or_default(&chain, |s| s.reference_style, defaults.reference_style), reference_hides_model: or_default(&chain, |s| s.reference_hides_model, defaults.reference_hides_model), tangent_edges: or_default(&chain, |s| s.tangent_edges, defaults.tangent_edges), - reference_margin_mm: chain.first().map_or(0.0, |view| view.reference_margin_mm), + reference_shown_percent: chain.first().map_or(0.0, |view| view.reference_shown_percent), }) } diff --git a/crates/doc/src/drawing/view_tests.rs b/crates/doc/src/drawing/view_tests.rs index 6a5a817137..04657e0156 100644 --- a/crates/doc/src/drawing/view_tests.rs +++ b/crates/doc/src/drawing/view_tests.rs @@ -27,11 +27,11 @@ fn front() -> (Drawing, IdSource, u64) { (d, id_source, id) } -/// A view on the drawing's first sheet, with no reference margin. +/// A view on the drawing's first sheet, showing no more for its reference parts. fn add(d: &mut Drawing, id_source: &mut IdSource, kind: ViewKind, place: ViewPlace, settings: ViewOverrides) -> u64 { let id = d.allocate_id(id_source).unwrap(); let sheet = d.sheets[0].id; - d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_shown_percent: 0.0 })]); id } @@ -192,15 +192,15 @@ fn the_views_rules_are_kept_on_reading() { rule(&right_id, "a view lined up with its parent is on its parent's sheet") ); assert_eq!(refused(&d, |v| v["views"][1]["sheet"] = json!(other_id)), "read"); - // Its place and its reference margin. + // Its place and its reference_shown_percent. let off_paper = "its place is at most 5000 mm from the sheet's origin"; assert_eq!(refused(&d, |v| v["views"][1]["place"] = json!({"at": [5000.0, -5000.0]})), "read"); assert_eq!(refused(&d, |v| v["views"][1]["place"] = json!({"at": [5000.001, 0.0]})), rule(&top_id, off_paper)); assert_eq!(refused(&d, |v| v["views"][2]["place"] = json!({"aligned": -5000.001})), rule(&right_id, off_paper)); - for (margin, read) in [(0.0, true), (100_000.0, true), (-0.001, false), (100_000.001, false)] { - let why = refused(&d, |v| v["views"][1]["reference_margin_mm"] = json!(margin)); - let expected = if read { "read".to_string() } else { rule(&top_id, "its reference margin is 0 to 100000 model millimetres") }; - assert_eq!(why, expected, "{margin}"); + for (percent, read) in [(0.0, true), (100.0, true), (-0.001, false), (100.001, false)] { + let why = refused(&d, |v| v["views"][1]["reference_shown_percent"] = json!(percent)); + let expected = if read { "read".to_string() } else { rule(&top_id, "its reference_shown_percent is 0 to 100") }; + assert_eq!(why, expected, "{percent}"); } // Its settings and its fields, only those it knows; a resolution is the sheet's, not a view's. let unknown: [fn(&mut Value); 5] = [ @@ -254,13 +254,13 @@ fn a_chain_of_views_is_at_most_its_depth() { /// A view resolves through its chain of parents: its basis turned down the chain in the drawing's /// projection, each setting from the nearest view that sets it, then the default, saying who gave -/// it, and its own reference margin. +/// it, and its own reference_shown_percent. #[test] fn a_view_resolves_through_its_chain() { let (mut d, _, [front, right, top]) = three(); let view = view_mut(&mut d, top); view.settings.tangent_edges = Some(TangentEdges::Show); - view.reference_margin_mm = 2.0; + view.reference_shown_percent = 2.0; let resolved = d.resolve_view(top).unwrap(); assert_eq!(resolved.source, d.sources[0].id); let right_basis = projected_basis(&NamedView::Front.basis(), ProjectedSide::Right, Projection::Third).unwrap(); @@ -270,7 +270,7 @@ fn a_view_resolves_through_its_chain() { assert_eq!(resolved.tangent_edges, Setting { value: TangentEdges::Show, set_by: SetBy::This }); assert_eq!(resolved.reference_style, Setting { value: VisualStyle::Visible, set_by: SetBy::Default }); assert_eq!(resolved.reference_hides_model, Setting { value: false, set_by: SetBy::Default }); - assert_eq!(resolved.reference_margin_mm, 2.0); + assert_eq!(resolved.reference_shown_percent, 2.0); // The base view gives its own; its projected views follow the drawing's projection. let base = d.resolve_view(front).unwrap(); assert_eq!((base.basis, base.scale.set_by, base.display_style.set_by), (NamedView::Front.basis(), SetBy::This, SetBy::Default)); diff --git a/crates/doc/src/drawing_tests.rs b/crates/doc/src/drawing_tests.rs index 5dd462be07..53c105a9a6 100644 --- a/crates/doc/src/drawing_tests.rs +++ b/crates/doc/src/drawing_tests.rs @@ -24,7 +24,7 @@ fn own_border() -> DrawingBorderProfile { /// the second (A4 portrait, a border of its own, 600 dpi, left out of printing) the design itself. /// On the first sheet, the front view at 1:2, its right view lined up with it and drawing hidden /// lines too, and the right view's top view placed apart; on the second, an iso view at 1:1, -/// tangent edges shown, with a 5 mm reference margin. On the front view, a dimension of each kind: +/// tangent edges shown, with reference_shown_percent 5. On the front view, a dimension of each kind: /// between two corners, a placed copy's edge length along a direction, and an angle with its text /// given; and a balloon on the placed copy's top edge. fn plate() -> Drawing { @@ -48,9 +48,9 @@ fn plate() -> Drawing { Arc::make_mut(&mut d.properties).set(crate::metadata::Owner::Design, "author", json!("Jane Doe")).unwrap(); d.units = Units::Fixed { unit: "in".into() }; let (assembly, bolt_sheet) = (d.sheets[0].id, d.sheets[1].id); - let mut add = |d: &mut Drawing, sheet, kind, place, settings, reference_margin_mm| { + let mut add = |d: &mut Drawing, sheet, kind, place, settings, reference_shown_percent| { let id = d.allocate_id(&mut id_source).unwrap(); - d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_margin_mm })]); + d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_shown_percent })]); id }; let half = ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() }; diff --git a/crates/doc/src/drawing_view.rs b/crates/doc/src/drawing_view.rs index 8f63bf9f6c..b45e7ca297 100644 --- a/crates/doc/src/drawing_view.rs +++ b/crates/doc/src/drawing_view.rs @@ -7,8 +7,8 @@ use solidcraft_geom::Plane; use solidcraft_geom::drawing_view::{Invalid, NamedView, Scale, basis_looking}; use solidcraft_render::VisualStyle; -/// The largest reference margin, in model millimetres. -pub const MAX_REFERENCE_MARGIN_MM: f64 = 1e5; +/// The most of its reference parts a view shows, as a percent. +pub const MAX_REFERENCE_SHOWN_PERCENT: f64 = 100.0; /// Which way a view looks: by name, or along any direction with an up. #[derive(Clone, Copy, Debug, PartialEq, Serialize)] @@ -86,9 +86,9 @@ pub struct ViewSettings { pub reference_style: VisualStyle, /// Whether reference parts hide the model's lines behind them. pub reference_hides_model: bool, - /// Model millimetres added on every side of the view's extents, so reference parts around the - /// model show inside its boundary: 0 to [`MAX_REFERENCE_MARGIN_MM`]. - pub reference_margin_mm: f64, + /// How far the view's boundary grows from the model's box towards the box that also holds its + /// reference parts, each side on its own: 0 to [`MAX_REFERENCE_SHOWN_PERCENT`]. + pub reference_shown_percent: f64, pub tangent_edges: TangentEdges, } @@ -117,11 +117,11 @@ impl ViewOverrides { } impl ViewSettings { - /// What reading alone can't check: the margin's range and the view's basis. + /// What reading alone can't check: the percent's range and the view's basis. pub fn validate(&self) -> Result { - let margin = self.reference_margin_mm; - if !(margin.is_finite() && (0.0..=MAX_REFERENCE_MARGIN_MM).contains(&margin)) { - return Err(Invalid("reference_margin_mm is a number of model millimetres from 0 to 100000")); + let percent = self.reference_shown_percent; + if !(percent.is_finite() && (0.0..=MAX_REFERENCE_SHOWN_PERCENT).contains(&percent)) { + return Err(Invalid("reference_shown_percent is a number from 0 to 100")); } self.view.basis() } diff --git a/crates/doc/src/drawing_view_tests.rs b/crates/doc/src/drawing_view_tests.rs index 00f3d5c85f..e1b632150a 100644 --- a/crates/doc/src/drawing_view_tests.rs +++ b/crates/doc/src/drawing_view_tests.rs @@ -15,7 +15,7 @@ fn view_settings_default_to_a_front_view_that_fits_its_page() { let full = json!({ "view": {"direction": [0.0, 1.0, 0.0], "up": [0.0, 0.0, 1.0], "origin": [1.0, 2.0, 3.0]}, "scale": "1:2", "display_style": "visible_hidden", "reference_style": "shaded", - "reference_hides_model": true, "reference_margin_mm": 5.0, "tangent_edges": "show", + "reference_hides_model": true, "reference_shown_percent": 5.0, "tangent_edges": "show", }); let read: ViewSettings = serde_json::from_value(full.clone()).unwrap(); assert_eq!(serde_json::to_value(read).unwrap(), full); @@ -43,13 +43,13 @@ fn view_settings_refuse_what_they_cant_draw() { let refused = [ json!({"view": {"direction": [0, 0, 0], "up": [0, 0, 1]}}), json!({"view": {"direction": [0, 0, 1], "up": [0, 0, -1]}}), - json!({"reference_margin_mm": -0.001}), - json!({"reference_margin_mm": 100000.001}), + json!({"reference_shown_percent": -0.001}), + json!({"reference_shown_percent": 100.001}), ]; for settings in refused { let read: ViewSettings = serde_json::from_value(settings.clone()).unwrap(); assert!(read.validate().is_err(), "{settings}"); } - let edge: ViewSettings = serde_json::from_value(json!({"reference_margin_mm": 100000.0})).unwrap(); + let edge: ViewSettings = serde_json::from_value(json!({"reference_shown_percent": 100.0})).unwrap(); assert!(edge.validate().is_ok()); } diff --git a/crates/engine/src/cmd/drawing_balloon_tests.rs b/crates/engine/src/cmd/drawing_balloon_tests.rs index e854b6bc84..76bcb17b1f 100644 --- a/crates/engine/src/cmd/drawing_balloon_tests.rs +++ b/crates/engine/src/cmd/drawing_balloon_tests.rs @@ -38,7 +38,7 @@ fn placed() -> (Session, u64, [u64; 2]) { let (sheet, source) = (d.sheets[0].id, d.sources[0].id); let kind = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) }; let settings = ViewOverrides { scale: Some("1:1".parse().unwrap()), ..ViewOverrides::default() }; - d.add_views([Arc::new(View { id, sheet, kind, place: ViewPlace::At([200.0, 150.0]), settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id, sheet, kind, place: ViewPlace::At([200.0, 150.0]), settings, reference_shown_percent: 0.0 })]); d.validate().unwrap(); s.doc_mut().put_drawing(Arc::new(d)); (s, id, bracket) diff --git a/crates/engine/src/cmd/drawing_dimension_tests.rs b/crates/engine/src/cmd/drawing_dimension_tests.rs index 620292814d..0531079e55 100644 --- a/crates/engine/src/cmd/drawing_dimension_tests.rs +++ b/crates/engine/src/cmd/drawing_dimension_tests.rs @@ -36,7 +36,7 @@ fn drawn_from_the_front(mut s: Session) -> (Session, u64) { let (sheet, source) = (d.sheets[0].id, d.sources[0].id); let kind = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) }; let settings = ViewOverrides { scale: Some("1:1".parse().unwrap()), ..ViewOverrides::default() }; - d.add_views([Arc::new(View { id, sheet, kind, place: ViewPlace::At([200.0, 150.0]), settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id, sheet, kind, place: ViewPlace::At([200.0, 150.0]), settings, reference_shown_percent: 0.0 })]); d.validate().unwrap(); s.doc_mut().put_drawing(Arc::new(d)); (s, id) diff --git a/crates/engine/src/cmd/drawing_view.rs b/crates/engine/src/cmd/drawing_view.rs index 5658b84d8c..96ad233d1c 100644 --- a/crates/engine/src/cmd/drawing_view.rs +++ b/crates/engine/src/cmd/drawing_view.rs @@ -76,7 +76,7 @@ pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new(CMD, "Drawing View Snap .noundo() .contract(&CONTRACT) .output(output_schema_for::) - .params("view?: front (default), back, top, bottom, left, right, iso (= iso_front_right), iso_front_left, iso_back_right, iso_back_left, or {direction, up, origin?}; scale?: \"paper:model\" such as \"1:2\" (default: the largest ISO 5455 scale that fits the sheet's drawing area); position_mm?: [x, y] for the view's centre on the sheet (default the sheet's centre); sheet?: {size: {standard: a3} or {custom: {sides_mm: [w, h]}}, orientation?: landscape|portrait, dpi?: 25..2400} (default A3 landscape, 300 dpi); display_style?, reference_style?: visible (default), visible_hidden or shaded (drawn as a picture, without its own lines, and not with scene; shaded_visible and shaded_visible_hidden aren't supported yet); reference_hides_model?: false; reference_margin_mm?: 0..100000 model mm on every side; tangent_edges?: hide (default) or show; chord_mm?: 0.001..1 paper mm the lines are sampled at (default 0.01); max_error_mm?: the accuracy the lines must certify on paper (refused while they come from the mesh); scene?: true adds the lines as a paper scene of the sheet (visible lines only for now); source?: {occurrences: [[ids…]]}, the occurrence subtrees drawn ([] the design's own bodies; default every body); hide?: [[ids…]], subtrees left out; at most 1000 paths in each and 4096 ids in all. Every body the design places, whatever the browser shows. Returns {view, scale: {text, factor, fitted}, position_mm, sheet, extents (model mm on the view's plane), boundary_mm (on the sheet), lines: [{occurrence, body, reference, source: {kind: edge, edge, tangent} or {kind: silhouette, face}, pieces: [{visibility, path: [paper mm lines, and arcs for circles facing the view], accuracy}]}], status: {lines: approximate, provenance: mesh_projection, chord_mm, model_chord_mm, uncertain_pieces, covered_pieces}, stats, scene (with scene: true: {size_mm, groups: [{object: 0, kind: view, items: [{kind: stroke, path, closed, width_mm}]}]}), picture? ({pixels: [w, h], dpi, rect_mm} when something is shaded: the boundary's pixel grid at sheet.dpi or 1200, the lower, which dpi gives), source}. Read-only: saves nothing."), + .params("view?: front (default), back, top, bottom, left, right, iso (= iso_front_right), iso_front_left, iso_back_right, iso_back_left, or {direction, up, origin?}; scale?: \"paper:model\" such as \"1:2\" (default: the largest ISO 5455 scale that fits the sheet's drawing area); position_mm?: [x, y] for the view's centre on the sheet (default the sheet's centre); sheet?: {size: {standard: a3} or {custom: {sides_mm: [w, h]}}, orientation?: landscape|portrait, dpi?: 25..2400} (default A3 landscape, 300 dpi); display_style?, reference_style?: visible (default), visible_hidden or shaded (drawn as a picture, without its own lines, and not with scene; shaded_visible and shaded_visible_hidden aren't supported yet); reference_hides_model?: false; reference_shown_percent?: 0..100, how far the box grows from the model's towards holding its reference parts, each side on its own (0, the default: the model's box; 100: holds them all); tangent_edges?: hide (default) or show; chord_mm?: 0.001..1 paper mm the lines are sampled at (default 0.01); max_error_mm?: the accuracy the lines must certify on paper (refused while they come from the mesh); scene?: true adds the lines as a paper scene of the sheet (visible lines only for now); source?: {occurrences: [[ids…]]}, the occurrence subtrees drawn ([] the design's own bodies; default every body); hide?: [[ids…]], subtrees left out; at most 1000 paths in each and 4096 ids in all. Every body the design places, whatever the browser shows. Returns {view, scale: {text, factor, fitted}, position_mm, sheet, extents (model mm on the view's plane), boundary_mm (on the sheet), lines: [{occurrence, body, reference, source: {kind: edge, edge, tangent} or {kind: silhouette, face}, pieces: [{visibility, path: [paper mm lines, and arcs for circles facing the view], accuracy}]}], status: {lines: approximate, provenance: mesh_projection, chord_mm, model_chord_mm, uncertain_pieces, covered_pieces}, stats, scene (with scene: true: {size_mm, groups: [{object: 0, kind: view, items: [{kind: stroke, path, closed, width_mm}]}]}), picture? ({pixels: [w, h], dpi, rect_mm} when something is shaded: the boundary's pixel grid at sheet.dpi or 1200, the lower, which dpi gives), source}. Read-only: saves nothing."), CommandSpec::new(INSPECT, "Inspect Drawing View", crate::checked!("drawing.view.inspect", inspect)) .icon("inspect") .in_drawing() @@ -84,14 +84,14 @@ pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new(CMD, "Drawing View Snap .doc_only() .contract(&INSPECT_CONTRACT) .output(output_schema_for::) - .params("view: a saved view's id → {view, sheet, kind: base|projected, source, parent?, side?, direction, place: {at: [x, y]} or {aligned: n}, settings: {scale, display_style, reference_style, reference_hides_model, tangent_edges: {value, from: this|parent|default, parent?, set}}, reference_margin_mm, boundary_mm: {min, max} on the sheet (a 20 x 20 mm placeholder round the view's centre when there's nothing to measure), extents: {min, max, error_mm, status: approximate|placeholder, provenance} in model mm on the view's plane, linked? (a projected view's display settings: true, false or \"partly\"), dimensions: [its dimensions' ids, in id order], balloons: [its balloons' ids, in id order], choices: {display_style, scale}}. Read-only; no model is evaluated."), + .params("view: a saved view's id → {view, sheet, kind: base|projected, source, parent?, side?, direction, place: {at: [x, y]} or {aligned: n}, settings: {scale, display_style, reference_style, reference_hides_model, tangent_edges: {value, from: this|parent|default, parent?, set}}, reference_shown_percent, boundary_mm: {min, max} on the sheet (a 20 x 20 mm placeholder round the view's centre when there's nothing to measure), extents: {min, max, error_mm, status: approximate|placeholder, provenance} in model mm on the view's plane, linked? (a projected view's display settings: true, false or \"partly\"), dimensions: [its dimensions' ids, in id order], balloons: [its balloons' ids, in id order], choices: {display_style, scale}}. Read-only; no model is evaluated."), CommandSpec::new(EDIT, "Edit Drawing View", crate::checked!("drawing.view.edit", edit)) .icon("edit") .in_drawing() .doc_only() .contract(&EDIT_CONTRACT) .output(output_schema_for::) - .params("view: a saved view's id; set: {scale?, display_style?, reference_style?, reference_hides_model?, tangent_edges?: each a value, or null to follow the parent (a base view keeps a scale); reference_margin_mm?; view?: a base view's direction; place?: {at: [x, y]} or {aligned: n}; linked?: a projected view's display settings all following its parent (true) or all its own, as they are now (false)} → {view, changed: [fields], followers: [the views projected from it whose settings changed with it]}. One undo step; no model is evaluated."), + .params("view: a saved view's id; set: {scale?, display_style?, reference_style?, reference_hides_model?, tangent_edges?: each a value, or null to follow the parent (a base view keeps a scale); reference_shown_percent?; view?: a base view's direction; place?: {at: [x, y]} or {aligned: n}; linked?: a projected view's display settings all following its parent (true) or all its own, as they are now (false)} → {view, changed: [fields], followers: [the views projected from it whose settings changed with it]}. One undo step; no model is evaluated."), CommandSpec::new(MOVE, "Move View to Sheet", crate::checked!("drawing.view.move_to_sheet", move_to_sheet)) .icon("move") .in_drawing() @@ -112,7 +112,7 @@ pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new(CMD, "Drawing View Snap .doc_only() .contract(&BASE_CONTRACT) .output(output_schema_for::) - .params("sheet: id or name; view?: as drawing.view.snapshot's (front by default); scale?: \"paper:model\" (default: the largest ISO 5455 preset at which it lies inside the sheet's drawing space, centred where it goes, or 1:1 with nothing to measure; saved either way); position_mm?: [x, y] for its centre (default the sheet's centre); display_style?, reference_style?, reference_hides_model?, reference_margin_mm?, tangent_edges?: settings it sets itself (each left out follows the default) → {view, sheet, scale: {text, factor, fitted}, boundary_mm, outside?: [it, when its boundary leaves the sheet's drawing space]}. A view of the drawing's design. One undo step; no model is evaluated."), + .params("sheet: id or name; view?: as drawing.view.snapshot's (front by default); scale?: \"paper:model\" (default: the largest ISO 5455 preset at which it lies inside the sheet's drawing space, centred where it goes, or 1:1 with nothing to measure; saved either way); position_mm?: [x, y] for its centre (default the sheet's centre); display_style?, reference_style?, reference_hides_model?, reference_shown_percent?, tangent_edges?: settings it sets itself (each left out follows the default) → {view, sheet, scale: {text, factor, fitted}, boundary_mm, outside?: [it, when its boundary leaves the sheet's drawing space]}. A view of the drawing's design. One undo step; no model is evaluated."), CommandSpec::new(PROJECTED, "Projected View", crate::checked!("drawing.view.projected", projected)) .icon("new") .in_drawing() @@ -150,13 +150,13 @@ fn snapshot(s: &mut Session, p: &Value) -> Result { return Err(bad(CMD, "the source and hide leave no bodies to draw")); } let extents = crate::drawing_view::measure(&set, &basis).map_err(|why| bad(CMD, why.to_string()))?; - let shown = extents.model.inflated(settings.reference_margin_mm); + let shown = crate::drawing_view::shows(&set, &basis, &extents, settings.reference_shown_percent); let position = request.position_mm.map_or(paper * 0.5, |p| p.0); let (scale, fitted) = match settings.scale { Some(scale) => (scale, false), - None => (fit(shown.size(), position, border, CMD)?, true), + None => (fit(&extents, shown, position, border, CMD)?, true), }; - let frame = crate::drawing_view::frame(basis, scale, &shown, position, CMD)?; + let frame = crate::drawing_view::frame(basis, scale, &extents.model, position, CMD)?; let boundary = frame.boundary(&shown).map_err(|e| bad(CMD, e.to_string()))?; let on_paper = [boundary.min, boundary.max].iter().all(|c| c.is_finite() && c.x.abs() <= MAX_COORDINATE_MM && c.y.abs() <= MAX_COORDINATE_MM); if !on_paper { @@ -326,7 +326,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result { reference_hides_model: setting(resolved.reference_hides_model, own.reference_hides_model, |b| b), tangent_edges: setting(resolved.tangent_edges, own.tangent_edges, snake_name), }, - reference_margin_mm: view.reference_margin_mm, + reference_shown_percent: view.reference_shown_percent, boundary_mm: BoxResult { min: placed.boundary.min.into(), max: placed.boundary.max.into() }, extents: extents_result(&placed.extents, placed.unmeasured), linked: matches!(view.kind, ViewKind::Projected { .. }).then(|| match display_set(&own) { @@ -432,8 +432,8 @@ fn change(d: &mut Drawing, id: u64, set: &ViewSet) -> Result> { if let Some(edges) = set.tangent_edges { own.tangent_edges = edges.map(|e| e.0); } - if let Some(margin) = set.reference_margin_mm { - view.reference_margin_mm = margin; + if let Some(percent) = set.reference_shown_percent { + view.reference_shown_percent = percent; } if let Some(direction) = set.view { let ViewKind::Base { view: looking, .. } = &mut view.kind else { @@ -459,7 +459,7 @@ fn changed_fields(a: &View, b: &View) -> Vec { ("reference_style", x.reference_style != y.reference_style), ("reference_hides_model", x.reference_hides_model != y.reference_hides_model), ("tangent_edges", x.tangent_edges != y.tangent_edges), - ("reference_margin_mm", a.reference_margin_mm != b.reference_margin_mm), + ("reference_shown_percent", a.reference_shown_percent != b.reference_shown_percent), ("view", a.kind != b.kind), ("place", a.place != b.place), ]; @@ -648,10 +648,10 @@ fn base(s: &mut Session, p: &Value) -> Result { }; let place = ViewPlace::At([position.x, position.y]); let kind = ViewKind::Base { source, view: direction }; - let mut view = View { id, sheet, kind, place, settings, reference_margin_mm: request.reference_margin_mm.unwrap_or(0.0) }; + let mut view = View { id, sheet, kind, place, settings, reference_shown_percent: request.reference_shown_percent.unwrap_or(0.0) }; let mut fitted = false; if view.settings.scale.is_none() { - // The record's rules first, at 1:1, so a bad direction, margin or place is refused as the + // The record's rules first, at 1:1, so a bad direction, percent or place is refused as the // record says; then the fitted scale, which is saved. With nothing to measure, 1:1. let one = Scale::new(1.0, 1.0).map_err(|e| bad(BASE, e.to_string()))?; let mut staged = Drawing::clone(&record); @@ -659,11 +659,14 @@ fn base(s: &mut Session, p: &Value) -> Result { staged.validate().map_err(|e| bad(BASE, e.to_string()))?; let space = crate::drawing_view::sheet(&staged, sheet, BASE)?.drawing_space().map_err(|e| bad(BASE, e.to_string()))?; let basis = staged.resolve_view(id).map_err(|e| bad(BASE, e.to_string()))?.basis; - let measured = crate::drawing_view::shown(s).and_then(|set| crate::drawing_view::measure(&set, &basis)).ok(); - fitted = measured.is_some(); + let measured = crate::drawing_view::shown(s).and_then(|set| { + let extents = crate::drawing_view::measure(&set, &basis)?; + Ok((extents, crate::drawing_view::shows(&set, &basis, &extents, view.reference_shown_percent))) + }); + fitted = measured.is_ok(); view.settings.scale = Some(match measured { - Some(extents) => fit(extents.model.inflated(view.reference_margin_mm).size(), position, space, BASE)?, - None => one, + Ok((extents, shown)) => fit(&extents, shown, position, space, BASE)?, + Err(_) => one, }); } s.edit_drawing(BASE, |d| { @@ -686,7 +689,7 @@ fn projected(s: &mut Session, p: &Value) -> Result { None => ViewPlace::Aligned(side.along(parent_centre)), }; let kind = ViewKind::Projected { parent, side }; - let mut view = View { id, sheet, kind, place, settings: ViewOverrides::default(), reference_margin_mm: 0.0 }; + let mut view = View { id, sheet, kind, place, settings: ViewOverrides::default(), reference_shown_percent: 0.0 }; if request.place.is_none() { // Staged on its parent's centre to measure it, then moved along the free axis until its // boundary is the gap past its parent's. @@ -765,10 +768,12 @@ fn direction(basis: &Plane) -> ViewResult { } } -/// The largest preset scale at which a view `size` (model mm), centred at `position`, lies inside -/// the drawing border: the room is twice the distance from the position to the nearer side, -/// each way. -fn fit(size: Vec2, position: Vec2, border: Aabb2, cmd: &str) -> Result { +/// The largest preset scale at which what a view shows (model mm), placed by the middle of its model at +/// `position`, lies inside the drawing border: the room is twice the distance from the position to +/// the nearer side, each way, for the box round the model's middle that holds what shows. +fn fit(extents: &Extents, shown: Aabb2, position: Vec2, border: Aabb2, cmd: &str) -> Result { + let off = shown.center() - extents.model.center(); + let size = shown.size() + Vec2::new(off.x.abs(), off.y.abs()) * 2.0; let room = Vec2::new(2.0 * (position.x - border.min.x).min(border.max.x - position.x), 2.0 * (position.y - border.min.y).min(border.max.y - position.y)); Scale::fit(size, room).ok_or_else(|| { diff --git a/crates/engine/src/cmd/drawing_view_tests.rs b/crates/engine/src/cmd/drawing_view_tests.rs index 6139de47db..c7e035ea2c 100644 --- a/crates/engine/src/cmd/drawing_view_tests.rs +++ b/crates/engine/src/cmd/drawing_view_tests.rs @@ -61,7 +61,7 @@ fn three_views_of(mut s: Session) -> (Session, [u64; 3]) { let (sheet, source) = (d.sheets[0].id, d.sources[0].id); let mut add = |d: &mut Drawing, kind: ViewKind, place: ViewPlace, settings: ViewOverrides| { let id = d.allocate_id(&mut id_source).unwrap(); - d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_shown_percent: 0.0 })]); id }; let front_settings = @@ -194,9 +194,11 @@ fn an_edit_is_one_undo_step_and_names_its_followers() { assert_eq!(turned["changed"], json!(["scale", "view"])); assert_eq!(turned["followers"], json!([ids::encode(top), ids::encode(right)]), "by id"); assert_eq!(edit_view(&mut s, right, json!({"place": {"at": [320.0, 150.0]}}))["changed"], json!(["place"])); - assert_eq!(edit_view(&mut s, top, json!({"reference_margin_mm": 5.0}))["changed"], json!(["reference_margin_mm"])); - assert_eq!(inspect_view(&mut s, top)["reference_margin_mm"], 5.0); - assert!(edit_refused(&mut s, top, json!({"reference_margin_mm": -1.0})).contains("reference margin")); + assert_eq!(edit_view(&mut s, top, json!({"reference_shown_percent": 5.0}))["changed"], json!(["reference_shown_percent"])); + assert_eq!(inspect_view(&mut s, top)["reference_shown_percent"], 5.0); + for percent in [-1.0, 100.001, 1e308] { + assert!(edit_refused(&mut s, top, json!({"reference_shown_percent": percent})).contains("reference_shown_percent"), "{percent}"); + } // Nothing changed, no step. let steps = s.undo.len(); assert_eq!(edit_view(&mut s, front, json!({"scale": "1:5"}))["changed"], json!([])); @@ -396,7 +398,7 @@ fn block_drawing() -> Session { } /// A base view with no scale takes the largest preset at which it lies inside the sheet's drawing -/// space (not the paper), centred where it goes, its margin included: the 100 x 20 front view on A3 +/// space (not the paper), centred where it goes: the 100 x 20 front view on A3 /// at 2:1, as the snapshot fits it; that scale is saved as the view's own. A scale given is kept, /// the settings given are the view's own, each view is one undo step ([`create`]), and with nothing /// to measure the view takes 1:1. @@ -414,18 +416,16 @@ fn a_base_view_fits_its_sheet_and_saves_the_scale() { assert_eq!(inspect_view(&mut s, front)["settings"]["scale"], json!({"value": "2:1", "from": "this", "set": "2:1"})); let near_the_left = create(&mut s, BASE, json!({"sheet": "Sheet 1", "position_mm": [108, 148.5]})).0; assert_eq!(near_the_left["scale"]["text"], "1:1", "98 mm to the drawing border: 196 mm of room (the paper's edge would give 216)"); - let with_margin = create(&mut s, BASE, json!({"sheet": "Sheet 1", "reference_margin_mm": 60})).0; - assert_eq!(with_margin["scale"]["text"], "1:1", "220 x 140 with the margin: 440 mm at 2:1"); - let p = json!({"sheet": "Sheet 1", "view": "top", "scale": "1:5", "position_mm": [100, 100], "display_style": "visible_hidden", "reference_margin_mm": 5}); + let p = json!({"sheet": "Sheet 1", "view": "top", "scale": "1:5", "position_mm": [100, 100], "display_style": "visible_hidden", "reference_shown_percent": 50}); let (given, top) = create(&mut s, BASE, p); assert_eq!((given["scale"]["text"].as_str(), given["scale"]["fitted"].as_bool()), (Some("1:5"), Some(false))); - assert_box(&given, [89.0, 94.0], [111.0, 106.0]); + assert_box(&given, [90.0, 95.0], [110.0, 105.0]); let view = saved(&s, top); assert_eq!( view.settings, ViewOverrides { scale: Some("1:5".parse().unwrap()), display_style: Some(VisualStyle::VisibleHidden), ..ViewOverrides::default() } ); - assert_eq!(view.reference_margin_mm, 5.0); + assert_eq!(view.reference_shown_percent, 50.0, "it has no reference parts, so its box doesn't grow"); let before = s.drawing("test").unwrap().views.len(); run(&mut s, "edit.undo", json!({})); assert_eq!(s.drawing("test").unwrap().views.len(), before - 1, "one undo takes the view away"); @@ -486,9 +486,27 @@ fn a_projected_view_lines_up_a_gap_past_its_parent() { assert_eq!((beside["sheet"].clone(), saved(&s, beside_id).sheet), (json!(ids::encode(second)), second)); } +/// A base view with no scale fits what its percent shows, the scale the snapshot gives for the same +/// percent, and stays inside the drawing space on the side the box grows on. +#[test] +fn a_base_view_fits_what_its_percent_shows() { + let (mut s, [_, block]) = plate_and_block(5.0); + run(&mut s, "occurrence.move", json!({"occurrence": block, "translate": [3000, 10, 5]})); + let snapshot = |s: &mut Session, percent: f64| snap(s, json!({"view": "top", "reference_shown_percent": percent}))["scale"]["text"].clone(); + let (near, far) = (snapshot(&mut s, 0.0), snapshot(&mut s, 100.0)); + assert_ne!(near, far); + run(&mut s, "drawing.create", json!({})); + for (percent, scale) in [(0.0, near), (100.0, far)] { + let (made, id) = create(&mut s, BASE, json!({"sheet": "Sheet 1", "view": "top", "reference_shown_percent": percent})); + assert_eq!((&made["scale"]["text"], made["scale"]["fitted"].as_bool()), (&scale, Some(true)), "{made}"); + assert!(made.get("outside").is_none(), "{made}"); + assert_eq!(saved(&s, id).reference_shown_percent, percent); + } +} + /// What view creation refuses, each with the drawing and the undo steps unchanged: no drawing, a /// drawing that draws no design, unknown or badly given sheets, parents and sides, places and -/// margins past their limits, a scale that doesn't fit, unknown fields, and a 33rd view in a chain. +/// percents past their limits, a scale that doesn't fit, unknown fields, and a 33rd view in a chain. #[test] fn hostile_view_creation_is_refused() { let mut s = block(); @@ -508,8 +526,8 @@ fn hostile_view_creation_is_refused() { base(json!({"scale": "1:1", "position_mm": [1e300, 0]})), base(json!({"position_mm": [1e300, 0]})), base(json!({"position_mm": [5, 148.5]})), - base(json!({"reference_margin_mm": 1e6})), - base(json!({"reference_margin_mm": -1})), + base(json!({"reference_shown_percent": 100.001})), + base(json!({"reference_shown_percent": -1})), base(json!({"display_style": "glossy"})), base(json!({"colour": "red"})), base(json!({"position_mm": null})), @@ -610,18 +628,16 @@ fn without_a_scale_the_view_fits_and_centres_on_its_sheet() { assert_eq!(a4["sheet"], json!({"size": {"standard": "a4"}, "orientation": "portrait", "dpi": 300.0}), "the sheet, its defaults filled in"); } -/// Scale and position only scale and move the boundary; the margin grows each side by the -/// margin times the scale; the browser's eye counts for nothing; nothing in the design changes. +/// Scale and position only scale and move the boundary; the browser's eye counts for nothing; +/// nothing in the design changes. #[test] -fn scale_position_and_margin_set_the_boundary_and_the_design_is_untouched() { +fn scale_and_position_set_the_boundary_and_the_design_is_untouched() { let mut s = block(); let (undo, doc) = (s.undo.len(), s.doc.clone()); let dirty = s.is_dirty(); let one = boundary(&snap(&mut s, json!({"scale": "1:1", "position_mm": [0, 0]}))); let half = boundary(&snap(&mut s, json!({"scale": "1:2", "position_mm": [0, 0]}))); assert!((one.0 - half.0 * 2.0).len() < EPS && one.1.len() < EPS && half.1.len() < EPS); - let margin = boundary(&snap(&mut s, json!({"scale": "1:2", "position_mm": [0, 0], "reference_margin_mm": 5}))); - assert!((margin.0 - (half.0 + Vec2::new(5.0, 5.0))).len() < EPS, "2.5 mm a side at 1:2: {margin:?}"); // The same call gives the same answer, apart from what working it out took. let answer = |mut v: Value| { if let Some(reply) = v.as_object_mut() { @@ -718,7 +734,7 @@ fn hostile_snapshots_are_refused() { (json!({"sheet": {"dpi": 24.9}}), "dpi"), (json!({"sheet": {"dpi": 2400.1}}), "dpi"), (json!({"sheet": {"size": {"custom": {"sides_mm": [0, 10]}}}}), "sheet"), - (json!({"reference_margin_mm": -1}), "reference_margin_mm"), + (json!({"reference_shown_percent": -1}), "reference_shown_percent"), (json!({"position_mm": [1e308, 0]}), "point"), (json!({"position_mm": [5000.1, 0], "scale": "1:1"}), "position"), (json!({"scael": "1:2"}), "scael"), @@ -1198,7 +1214,7 @@ fn assert_lengths(v: &Value, reference: bool, visible: f64, hidden: f64) { assert!((got.0 - visible).abs() < 1e-6 && (got.1 - hidden).abs() < 1e-6, "{reference}: {got:?}, not ({visible}, {hidden})"); } -/// Worked by hand, top view at 1:1 with a margin taking in all of the block. The plate's outline is +/// Worked by hand, top view at 1:1 showing all of the block. The plate's outline is /// 160 mm, 20 of it (x = 40, y 10..30) under the block when the block is above; the block's is 80, /// and when it is below, its parts at x 30..40 are under the plate: 40 hidden and 40 visible. /// Reference parts cover the model only when told, and the plate always covers the block. @@ -1206,7 +1222,7 @@ fn assert_lengths(v: &Value, reference: bool, visible: f64, hidden: f64) { fn reference_lines_are_their_own_and_cover_the_model_only_when_told() { let view = |hides: bool| { json!({"view": "top", "scale": "1:1", "display_style": "visible_hidden", "reference_style": "visible_hidden", - "reference_hides_model": hides, "reference_margin_mm": 20}) + "reference_hides_model": hides, "reference_shown_percent": 100}) }; let (mut above, _) = plate_and_block(5.0); let v = snap(&mut above, view(false)); @@ -1228,28 +1244,95 @@ fn reference_lines_are_their_own_and_cover_the_model_only_when_told() { #[test] fn each_part_of_a_view_draws_hidden_lines_by_its_own_style() { let (mut above, _) = plate_and_block(5.0); - let hides = json!({"view": "top", "scale": "1:1", "reference_hides_model": true, "reference_margin_mm": 20}); + let hides = json!({"view": "top", "scale": "1:1", "reference_hides_model": true, "reference_shown_percent": 100}); let v = snap(&mut above, hides.clone()); assert_lengths(&v, false, 140.0, 0.0); let (mut below, _) = plate_and_block(-10.0); let v = snap(&mut below, hides); assert_lengths(&v, true, 40.0, 0.0); - let v = snap(&mut below, json!({"view": "top", "scale": "1:1", "reference_style": "visible_hidden", "reference_margin_mm": 20})); + let v = snap(&mut below, json!({"view": "top", "scale": "1:1", "reference_style": "visible_hidden", "reference_shown_percent": 100})); assert_lengths(&v, true, 40.0, 40.0); } -/// With model parts there, the boundary is theirs and the margin's, and reference lines stop at it: -/// with no margin the block keeps x 30..40 (20 + 10 + 10); with 5 mm, x 30..45 (20 + 15 + 15). +/// With model parts there, the boundary starts at theirs and reference lines stop at it. The block +/// passes only the plate's right side, so that side alone grows, to x 50 at 100%: the block keeps +/// x 30..40 at 0% (20 + 10 + 10), x 30..45 at 50% (20 + 15 + 15) and all of itself at 100%. The model +/// stays where it is throughout, so a view stays lined up with its projected views. #[test] -fn reference_parts_stop_at_the_boundary_of_the_model_and_its_margin() { +fn reference_parts_grow_the_boundary_on_their_own_side_as_far_as_asked() { let (mut s, _) = plate_and_block(5.0); - let v = snap(&mut s, json!({"view": "top", "scale": "1:1"})); - assert_box(&v, [190.0, 128.5], [230.0, 168.5]); - assert_eq!(v["source"]["bodies"], 2, "{v}"); - assert_lengths(&v, true, 40.0, 0.0); - let v = snap(&mut s, json!({"view": "top", "scale": "1:1", "reference_margin_mm": 5})); - assert_box(&v, [185.0, 123.5], [235.0, 173.5]); - assert_lengths(&v, true, 50.0, 0.0); + let at = |s: &mut Session, percent: f64| snap(s, json!({"view": "top", "scale": "1:1", "reference_shown_percent": percent})); + let model_lines = |v: &Value| v["lines"].as_array().unwrap().iter().filter(|l| l["reference"] == false).cloned().collect::>(); + let none = at(&mut s, 0.0); + assert_box(&none, [190.0, 128.5], [230.0, 168.5]); + assert_eq!(none["source"]["bodies"], 2, "{none}"); + assert_lengths(&none, true, 40.0, 0.0); + let half = at(&mut s, 50.0); + assert_box(&half, [190.0, 128.5], [235.0, 168.5]); + assert_lengths(&half, true, 50.0, 0.0); + let all = at(&mut s, 100.0); + assert_box(&all, [190.0, 128.5], [240.0, 168.5]); + assert_lengths(&all, true, 80.0, 0.0); + assert_eq!(model_lines(&none), model_lines(&half)); + assert_eq!(model_lines(&none), model_lines(&all)); + // A side no reference part passes is the model's, exactly, whatever the percent, and the side it + // passes only ever grows. + let mut reach = f64::MIN; + for percent in [0.0, 1e-9, 0.1, 33.3, 50.0, 99.999, 100.0] { + let v = at(&mut s, percent); + assert_eq!(none["boundary_mm"]["min"], v["boundary_mm"]["min"], "{percent}: {v}"); + let right = v["boundary_mm"]["max"][0].as_f64().unwrap(); + assert!(right >= reach && right <= 240.0, "{percent}: {v}"); + reach = right; + } +} + +/// Left out and 0 are one picture, reply for reply, apart from what working it out took. +#[test] +fn no_reference_shown_is_the_same_picture_as_not_asking() { + let (mut s, _) = plate_and_block(5.0); + let reply = |s: &mut Session, p: Value| { + let mut v = snap(s, p); + v.as_object_mut().unwrap().remove("stats"); + v + }; + let plain = reply(&mut s, json!({"view": "top", "scale": "1:1", "scene": true})); + assert_eq!(plain, reply(&mut s, json!({"view": "top", "scale": "1:1", "scene": true, "reference_shown_percent": 0}))); +} + +/// A part many times the model's size: at 100% the box holds it all and a fitted scale takes it in, +/// and a scale that can't holds it is refused by name, while 0% still draws. A view with only +/// reference parts is measured by them already, so the percent changes nothing. +#[test] +fn a_reference_part_much_bigger_than_the_model_is_held_or_refused() { + let mut s = Session::default(); + let add = |s: &mut Session, name: &str, size: f64, body: &str| { + let occurrence = run(s, "component.create", json!({"name": name}))["occurrence"].as_u64().unwrap(); + run(s, "solid.box", json!({"length": size, "width": size, "height": 5, "body_name": body, "operation": "new"})); + run(s, "component.activate", json!({"component": 0})); + occurrence + }; + add(&mut s, "PlatePart", 10.0, "Plate"); + let big = add(&mut s, "BigPart", 20000.0, "Big"); + run(&mut s, "occurrence.bom_type", json!({"occurrence": big, "bom_type": "reference"})); + let fitted = |s: &mut Session, percent: f64| snap(s, json!({"view": "top", "reference_shown_percent": percent})); + let (near, far) = (fitted(&mut s, 0.0), fitted(&mut s, 100.0)); + assert_ne!(near["scale"], far["scale"], "{far}"); + assert!(role_length(&far, true, "visible") > role_length(&near, true, "visible"), "{far}"); + // Fitted round the model's middle, the side it grows on still stays on the sheet. + let position = (point(&near["boundary_mm"]["min"]) + point(&near["boundary_mm"]["max"])) * 0.5; + let (low, high) = (point(&far["boundary_mm"]["min"]), point(&far["boundary_mm"]["max"])); + assert!(low.x >= 0.0 && low.y >= 0.0 && high.x <= position.x * 2.0 && high.y <= position.y * 2.0, "{far}"); + let refused = + s.execute("drawing.view.snapshot", &json!({"view": "top", "scale": "100:1", "reference_shown_percent": 100})).unwrap_err().to_string(); + assert!(refused.contains("reaches past"), "{refused}"); + snap(&mut s, json!({"view": "top", "scale": "100:1"})); + + let (mut only, [plate, _]) = plate_and_block(5.0); + run(&mut only, "occurrence.bom_type", json!({"occurrence": plate, "bom_type": "reference"})); + let at = |s: &mut Session, percent: f64| snap(s, json!({"view": "top", "scale": "1:1", "reference_shown_percent": percent})); + let (none, all) = (at(&mut only, 0.0), at(&mut only, 100.0)); + assert_eq!((&none["boundary_mm"], &none["lines"]), (&all["boundary_mm"], &all["lines"])); } /// Reference lines test against everything: with both parts reference, the block hides 20 mm of the @@ -1343,7 +1426,7 @@ fn no_reference_parts_means_the_setting_changes_nothing() { #[test] fn a_scene_draws_reference_parts_thin_under_the_model() { let (mut s, _) = plate_and_block(5.0); - let v = snap(&mut s, json!({"view": "top", "scale": "1:1", "scene": true, "reference_margin_mm": 20})); + let v = snap(&mut s, json!({"view": "top", "scale": "1:1", "scene": true, "reference_shown_percent": 100})); let widths: Vec = strokes(&v).iter().map(|(_, _, width)| *width).collect(); let thin = widths.iter().take_while(|w| **w == crate::drawing_view::REFERENCE_MM).count(); assert!(thin > 0 && widths[thin..].iter().all(|w| *w == crate::drawing_view::VISIBLE_MM), "{widths:?}"); @@ -1468,26 +1551,19 @@ fn a_saved_views_boundary_is_its_extents_at_its_scale_round_its_centre() { /// The right view follows the front view up and down when it moves, and keeps its own place /// across; at its own scale (1:1, 50 x 20) it is still lined up through its centre, and the top -/// view projected from it takes that scale too. The top view's 5 mm reference margin grows it by -/// 2.5 mm a side at 1:2, its extents unchanged. The largest margin at the largest scale still -/// gives a finite box. +/// view projected from it takes that scale too. The largest scale still gives a finite box. #[test] -fn a_lined_up_view_follows_its_parent_and_a_margin_grows_the_boundary() { +fn a_lined_up_view_follows_its_parent() { let (mut s, [front, right, top]) = three_views(); edit_view(&mut s, front, json!({"place": {"at": [160.0, 140.0]}})); assert_box(&inspect_view(&mut s, front), [135.0, 135.0], [185.0, 145.0]); assert_box(&inspect_view(&mut s, right), [287.5, 135.0], [312.5, 145.0]); - edit_view(&mut s, top, json!({"reference_margin_mm": 5.0})); - let margin = inspect_view(&mut s, top); - assert_box(&margin, [285.0, 222.5], [315.0, 277.5]); - let size = point(&margin["extents"]["max"]) - point(&margin["extents"]["min"]); - assert!((size - Vec2::new(50.0, 100.0)).len() < EPS, "{margin}"); edit_view(&mut s, right, json!({"scale": "1:1"})); assert_box(&inspect_view(&mut s, right), [275.0, 130.0], [325.0, 150.0]); - assert_box(&inspect_view(&mut s, top), [270.0, 195.0], [330.0, 305.0]); - edit_view(&mut s, top, json!({"reference_margin_mm": 100000.0, "scale": "50:1"})); + assert_box(&inspect_view(&mut s, top), [275.0, 200.0], [325.0, 300.0]); + edit_view(&mut s, top, json!({"scale": "1000:1"})); let (huge, centre) = boundary(&inspect_view(&mut s, top)); - assert!(huge.x.is_finite() && huge.x > 1e7 && (centre - Vec2::new(300.0, 250.0)).len() < 1e-6, "{huge:?} {centre:?}"); + assert!(huge.x.is_finite() && huge.x > 1e4 && (centre - Vec2::new(300.0, 250.0)).len() < 1e-6, "{huge:?} {centre:?}"); } /// With nothing to measure a view gets a 20 x 20 mm placeholder round its centre, and says why: diff --git a/crates/engine/src/drawing_dimension_tests.rs b/crates/engine/src/drawing_dimension_tests.rs index 5df21aa5f9..11a51aaa43 100644 --- a/crates/engine/src/drawing_dimension_tests.rs +++ b/crates/engine/src/drawing_dimension_tests.rs @@ -57,14 +57,14 @@ fn boxed() -> Fixture { let base = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) }; let side = ViewKind::Projected { parent: front, side: ProjectedSide::Right }; d.add_views([ - Arc::new(View { id: front, sheet, kind: base, place: ViewPlace::At([150.0, 150.0]), settings: half, reference_margin_mm: 0.0 }), + Arc::new(View { id: front, sheet, kind: base, place: ViewPlace::At([150.0, 150.0]), settings: half, reference_shown_percent: 0.0 }), Arc::new(View { id: right, sheet, kind: side, place: ViewPlace::Aligned(300.0), settings: ViewOverrides::default(), - reference_margin_mm: 0.0, + reference_shown_percent: 0.0, }), ]); d.validate().unwrap(); diff --git a/crates/engine/src/drawing_scene.rs b/crates/engine/src/drawing_scene.rs index 3ab1b7a502..27afc3bc19 100644 --- a/crates/engine/src/drawing_scene.rs +++ b/crates/engine/src/drawing_scene.rs @@ -140,13 +140,13 @@ fn draw(s: &Session, shown: &std::result::Result, id: u64, Ok(measured) => measured, Err(why) => return none(Status::Placeholder(why)), }; - let around = extents.model.inflated(view.reference_margin_mm); + let around = crate::drawing_view::shows(set, &view.basis, &extents, view.reference_shown_percent); let tangent_edges = view.tangent_edges.value == TangentEdges::Show; // The margin keeps the mesh's chord at or above its floor once divided back by the factor. let chord_mm = CHORD_MM.max(MIN_MODEL_CHORD_MM * view.scale.value.factor() * (1.0 + 1e-9)); let reference_hides_model = view.reference_hides_model.value; let request = LineRequest { shows: around, hidden: false, reference_hidden: false, reference_hides_model, tangent_edges, chord_mm }; - let lines = crate::drawing_view::frame(view.basis, view.scale.value, &around, Vec2::ZERO, CMD) + let lines = crate::drawing_view::frame(view.basis, view.scale.value, &extents.model, Vec2::ZERO, CMD) .and_then(|frame| crate::drawing_view::lines(s, set, &frame, &request, CMD)); match lines { Ok(lines) => { diff --git a/crates/engine/src/drawing_scene_tests.rs b/crates/engine/src/drawing_scene_tests.rs index d2ce880a97..f1818fc91b 100644 --- a/crates/engine/src/drawing_scene_tests.rs +++ b/crates/engine/src/drawing_scene_tests.rs @@ -34,7 +34,7 @@ fn front_and_top_of(mut s: Session) -> (Session, u64, [u64; 2]) { let (sheet, source) = (d.sheets[0].id, d.sources[0].id); let mut add = |d: &mut Drawing, kind: ViewKind, place: ViewPlace, settings: ViewOverrides| { let id = d.allocate_id(&mut id_source).unwrap(); - d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_margin_mm: 0.0 })]); + d.add_views([Arc::new(View { id, sheet, kind, place, settings, reference_shown_percent: 0.0 })]); id }; let half = ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() }; @@ -500,3 +500,45 @@ fn text_moves_with_its_group() { let Item::Text(placed) = &moved(&group, Vec2::new(10.0, 20.0)).items[0] else { panic!("a run") }; assert_eq!(placed, &TextRun { origin: Vec2::new(11.0, 22.0), ..run }); } + +/// A saved view's reference_shown_percent redraws that view alone, grows its boundary on the reference +/// part's side, leaves the model's strokes where they were, and goes back with an undo. +#[test] +fn a_saved_views_reference_shown_percent_grows_only_what_it_should() { + let mut design = Session::default(); + let mut part = |name: &str, body: &str| { + let occurrence = run(&mut design, "component.create", json!({"name": name}))["occurrence"].as_u64().unwrap(); + run(&mut design, "solid.box", json!({"length": 20, "width": 20, "height": 10, "body_name": body, "operation": "new"})); + run(&mut design, "component.activate", json!({"component": 0})); + occurrence + }; + part("Under", "BodyUnder"); + let over = part("Over", "BodyOver"); + run(&mut design, "occurrence.move", json!({"occurrence": over, "translate": [10, 0, 10]})); + run(&mut design, "occurrence.bom_type", json!({"occurrence": over, "bom_type": "reference"})); + let (mut s, sheet, [_, top]) = front_and_top_of(design); + let strokes = |scene: &Scene, reference: bool| { + let items = scene.groups.iter().filter(|g| g.kind == ObjectKind::View).flat_map(|g| &g.items); + let thin = |i: &&Item| matches!(i, Item::Stroke { width_mm, .. } if *width_mm == crate::drawing_view::REFERENCE_MM); + let mut drawn: Vec = items.filter(|i| matches!(i, Item::Stroke { .. }) && thin(i) == reference).map(|i| format!("{i:?}")).collect(); + drawn.sort(); + drawn + }; + let before = settled(&mut s, sheet); + let boundary = |s: &mut Session| run(s, "drawing.view.inspect", json!({"view": ids::encode(top)}))["boundary_mm"].clone(); + let (min, max) = { + let b = boundary(&mut s); + (b["min"].clone(), b["max"].clone()) + }; + run(&mut s, "drawing.view.edit", json!({"view": ids::encode(top), "set": {"reference_shown_percent": 100}})); + assert_eq!(extract(&mut s, sheet).unwrap().stale, [top]); + let after = settled(&mut s, sheet); + assert_eq!(strokes(&before.scene, false), strokes(&after.scene, false), "the model stays where it is"); + assert_ne!(strokes(&before.scene, true), strokes(&after.scene, true)); + let grown = boundary(&mut s); + assert_eq!(grown["min"], min, "the side it doesn't pass stays"); + assert_ne!(grown["max"], max); + assert_eq!(bytes(&built(&s, sheet).scene), bytes(&after.scene), "the provider and a build draw one thing"); + run(&mut s, "edit.undo", json!({})); + assert_eq!(bytes(&settled(&mut s, sheet).scene), bytes(&before.scene)); +} diff --git a/crates/engine/src/drawing_view.rs b/crates/engine/src/drawing_view.rs index 79a907755b..dce8e78cab 100644 --- a/crates/engine/src/drawing_view.rs +++ b/crates/engine/src/drawing_view.rs @@ -189,35 +189,51 @@ pub fn measure(shown: &ShownSet, basis: &Plane) -> std::result::Result Result { - ViewFrame::new(basis, scale, shown.center(), centre).map_err(|e| bad(cmd, e.to_string())) +/// What a view of `shown` shows on `basis`: its `extents`, each side grown on its own towards the box +/// that also holds its reference parts, `percent` of the way. A side no reference part passes stays +/// exactly where it is, and at 0 no reference part is measured. +pub fn shows(shown: &ShownSet, basis: &Plane, extents: &Extents, percent: f64) -> Aabb2 { + if percent <= 0.0 { + return extents.model; + } + let references = shown.bodies.iter().filter(|b| b.reference).filter_map(|b| shown.body(b)); + let all = references.fold(extents.model, |all, body| all.union(&body.mesh_bounds_in(basis))); + let part = percent / 100.0; + let side = |from: f64, to: f64| if from == to { from } else { from * (1.0 - part) + to * part }; + let model = extents.model; + Aabb2 { + min: Vec2::new(side(model.min.x, all.min.x), side(model.min.y, all.min.y)), + max: Vec2::new(side(model.max.x, all.max.x), side(model.max.y, all.max.y)), + } } -/// The boundary on the sheet of [`frame`]'s view: the box round `shown` there. -pub fn boundary(basis: Plane, scale: Scale, shown: &Aabb2, centre: Vec2, cmd: &str) -> Result { - frame(basis, scale, shown, centre, cmd)?.boundary(shown).map_err(|e| bad(cmd, e.to_string())) +/// The view through `basis` at `scale` on paper, with the middle of the model's box (model mm on its +/// plane) at `centre`, however far reference parts grow what it shows, so it stays lined up with its +/// projected views: the one rule the snapshot and saved views are placed by. +pub fn frame(basis: Plane, scale: Scale, model: &Aabb2, centre: Vec2, cmd: &str) -> Result { + ViewFrame::new(basis, scale, model.center(), centre).map_err(|e| bad(cmd, e.to_string())) } /// Saved view `id` of `drawing`, one of `s`'s design's drawings, on its sheet: the design's extents -/// on its plane (a view's source is the design the drawing is in), grown by its reference margin, -/// at its scale, with their middle on its centre ([`Drawing::view_centre`]); with nothing to +/// on its plane (a view's source is the design the drawing is in), grown by [`shows`], at its +/// scale, with the model's middle on its centre ([`Drawing::view_centre`]); with nothing to /// measure, a placeholder [`PLACEHOLDER_MM`] each way round its centre. It reads the model as it /// is evaluated: no model is evaluated. pub fn on_sheet(s: &Session, drawing: &Drawing, id: u64, cmd: &str) -> Result { let view = drawing.resolve_view(id).map_err(|e| bad(cmd, e.to_string()))?; let centre = drawing.view_centre(id).map_err(|e| bad(cmd, e.to_string()))?; let scale = view.scale.value; - let (extents, shown, unmeasured) = match shown(s).and_then(|set| measure(&set, &view.basis)) { - Ok(extents) => (extents, extents.model.inflated(view.reference_margin_mm), None), + let measured = shown(s).and_then(|set| measure(&set, &view.basis).map(|e| (e, shows(&set, &view.basis, &e, view.reference_shown_percent)))); + let (extents, shows, unmeasured) = match measured { + Ok((extents, shows)) => (extents, shows, None), Err(why) => { let half = PLACEHOLDER_MM / 2.0 / scale.factor(); let model = Aabb2 { min: Vec2::new(-half, -half), max: Vec2::new(half, half) }; (Extents { model, error_mm: 0.0, bodies: 0 }, model, Some(why)) } }; - Ok(OnSheet { boundary: boundary(view.basis, scale, &shown, centre, cmd)?, extents, unmeasured }) + let boundary = frame(view.basis, scale, &extents.model, centre, cmd)?.boundary(&shows).map_err(|e| bad(cmd, e.to_string()))?; + Ok(OnSheet { boundary, extents, unmeasured }) } /// Each of `views` of `drawing` (one of `s`'s design's) whose boundary leaves its sheet's drawing diff --git a/crates/engine/src/schema/drawing_view.rs b/crates/engine/src/schema/drawing_view.rs index 642b86a31f..28caf07ee5 100644 --- a/crates/engine/src/schema/drawing_view.rs +++ b/crates/engine/src/schema/drawing_view.rs @@ -40,9 +40,9 @@ pub struct SnapshotParams { pub reference_style: StyleParam, /// Whether reference parts hide the model's lines behind them (false when not given). pub reference_hides_model: bool, - /// Model millimetres added on every side of the view's extents, so reference parts around - /// the model show inside its boundary: 0 to 100000 (0 when not given). - pub reference_margin_mm: f64, + /// How far the view's boundary grows from the model's box towards the box that also holds its + /// reference parts, each side on its own: 0 to 100 (0 when not given). + pub reference_shown_percent: f64, pub tangent_edges: TangentEdgesParam, pub sheet: SheetParam, /// Where the view's centre goes on the sheet, in paper millimetres from its lower-left @@ -84,7 +84,7 @@ impl SnapshotParams { display_style: self.display_style.0, reference_style: self.reference_style.0, reference_hides_model: self.reference_hides_model, - reference_margin_mm: self.reference_margin_mm, + reference_shown_percent: self.reference_shown_percent, tangent_edges: self.tangent_edges.0, } } @@ -298,10 +298,11 @@ pub struct ViewSet { pub reference_hides_model: Option>, #[serde(deserialize_with = "super::present")] pub tangent_edges: Option>, - /// Model millimetres added on every side of the view's extents: 0 to 100000. + /// How far the view's boundary grows from the model's box towards the box that also holds its + /// reference parts, each side on its own: 0 to 100. #[serde(deserialize_with = "super::present")] #[schemars(with = "f64")] - pub reference_margin_mm: Option, + pub reference_shown_percent: Option, /// A base view's direction. #[serde(deserialize_with = "super::present")] #[schemars(with = "ViewParam")] @@ -372,9 +373,9 @@ pub struct InspectResult { pub direction: ViewResult, pub place: PlaceParam, pub settings: SettingsResult, - pub reference_margin_mm: f64, - /// Where it sits on its sheet: its extents grown by its reference margin, at its scale, round - /// its centre; a placeholder 20 mm each way when there's nothing to measure. + pub reference_shown_percent: f64, + /// Where it sits on its sheet: its extents grown by `reference_shown_percent`, at its scale, + /// with the model's middle on its centre; a placeholder 20 mm each way when there's nothing to measure. pub boundary_mm: BoxResult, /// Its design's extents on its plane. pub extents: ExtentsResult, @@ -474,10 +475,11 @@ pub struct BaseParams { #[serde(default, deserialize_with = "super::present", skip_serializing_if = "Option::is_none")] #[schemars(with = "bool")] pub reference_hides_model: Option, - /// Model millimetres added on every side of its extents: 0 to 100000 (0 when not given). + /// How far the view's boundary grows from the model's box towards the box that also holds its + /// reference parts, each side on its own: 0 to 100 (0 when not given). #[serde(default, deserialize_with = "super::present", skip_serializing_if = "Option::is_none")] #[schemars(with = "f64")] - pub reference_margin_mm: Option, + pub reference_shown_percent: Option, #[serde(default, deserialize_with = "super::present", skip_serializing_if = "Option::is_none")] #[schemars(with = "TangentEdgesParam")] pub tangent_edges: Option, diff --git a/crates/engine/src/schema/drawing_view_tests.rs b/crates/engine/src/schema/drawing_view_tests.rs index 9182145e4f..cd89cc6dc2 100644 --- a/crates/engine/src/schema/drawing_view_tests.rs +++ b/crates/engine/src/schema/drawing_view_tests.rs @@ -14,7 +14,7 @@ fn schemas_and_the_key_check_agree() { "max_error_mm", "position_mm", "reference_hides_model", - "reference_margin_mm", + "reference_shown_percent", "reference_style", "scale", "scene", @@ -28,7 +28,7 @@ fn schemas_and_the_key_check_agree() { "display_style", "position_mm", "reference_hides_model", - "reference_margin_mm", + "reference_shown_percent", "reference_style", "scale", "sheet", @@ -56,7 +56,7 @@ fn the_edit_contract_lists_a_saved_views_settings() { }; let own = serde_json::to_value(all).unwrap_or_default(); let mut theirs: Vec = own.as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default(); - theirs.extend(["linked", "place", "reference_margin_mm", "view"].map(String::from)); + theirs.extend(["linked", "place", "reference_shown_percent", "view"].map(String::from)); theirs.sort(); let schema = input_schema_for::(); let mut ours: Vec = schema.as_value()["properties"].as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default(); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index dc1c88b108..977dec3680 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -873,7 +873,7 @@ fn hostile_params_never_panic() { "display_style", "reference_style", "reference_hides_model", - "reference_margin_mm", + "reference_shown_percent", "tangent_edges", "sheet", "position_mm",