Reference parts: a view shows them by percent, not by a margin (#5866 V5c) (#10713)

reference_margin_mm becomes reference_shown_percent, 0 to 100. At 0 the
boundary is the model's box, as before, and no reference part is measured.
At 100 it holds the model and all the view's reference parts. Between, each
side moves on its own, so a part on one side grows only that side.

The model's middle now places a view, not the middle of what it shows, so a
view stays lined up with its projected views as its box grows. With nothing
grown the two are the same point and the picture is the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CamWight
2026-11-01 23:36:53 +00:00
committed by GitHub
parent 71edae80df
commit 85be50df0d
18 changed files with 280 additions and 139 deletions
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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"]),
+9 -9
View File
@@ -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<VisualStyle>,
pub reference_hides_model: Setting<bool>,
pub tangent_edges: Setting<TangentEdges>,
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),
})
}
+10 -10
View File
@@ -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));
+3 -3
View File
@@ -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() };
+9 -9
View File
@@ -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<Plane, Invalid> {
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()
}
+4 -4
View File
@@ -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());
}
@@ -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)
@@ -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)
+27 -22
View File
@@ -76,7 +76,7 @@ pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new(CMD, "Drawing View Snap
.noundo()
.contract(&CONTRACT)
.output(output_schema_for::<SnapshotResult>)
.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::<InspectResult>)
.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::<EditResult>)
.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::<CreateResult>)
.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<Value> {
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<Value> {
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<Vec<String>> {
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<String> {
("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<Value> {
};
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<Value> {
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<Value> {
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<Scale> {
/// 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<Scale> {
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(|| {
+122 -46
View File
@@ -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::<Vec<_>>();
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<f64> = 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:
+2 -2
View File
@@ -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();
+2 -2
View File
@@ -140,13 +140,13 @@ fn draw(s: &Session, shown: &std::result::Result<ShownSet, Unmeasured>, 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) => {
+43 -1
View File
@@ -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<String> = 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));
}
+28 -12
View File
@@ -189,35 +189,51 @@ pub fn measure(shown: &ShownSet, basis: &Plane) -> std::result::Result<Extents,
if extents.model.is_empty() { Err(Unmeasured::NoBodies) } else { Ok(extents) }
}
/// The view through `basis` at `scale` on paper, with the middle of `shown` (model mm on its
/// plane) at `centre`: the one rule the snapshot and saved views are placed by.
pub fn frame(basis: Plane, scale: Scale, shown: &Aabb2, centre: Vec2, cmd: &str) -> Result<ViewFrame> {
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<Aabb2> {
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> {
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<OnSheet> {
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
+13 -11
View File
@@ -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<Option<bool>>,
#[serde(deserialize_with = "super::present")]
pub tangent_edges: Option<Option<TangentEdgesParam>>,
/// 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<f64>,
pub reference_shown_percent: Option<f64>,
/// 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<bool>,
/// 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<f64>,
pub reference_shown_percent: Option<f64>,
#[serde(default, deserialize_with = "super::present", skip_serializing_if = "Option::is_none")]
#[schemars(with = "TangentEdgesParam")]
pub tangent_edges: Option<TangentEdgesParam>,
@@ -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<String> = 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::<ViewSet>();
let mut ours: Vec<String> = schema.as_value()["properties"].as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default();
+1 -1
View File
@@ -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",