mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
Drawings: a drawing's views are worked out beside the window, so it keeps responding (#4892) (#11126)
* Drawings: views drawn beside the window (#4892 B7) `drawing_scene::begin` asks for the first view out of date, `Job::run` works its lines out on a copy of the session (pending moves cleared: the design's placements, as dimensions use), on any thread, and `finish` takes them only for the latest request while the revision, the model's world state and the view as it resolves are as they were; `cancel` forgets the request. Request numbers come from one counter for every session, so a drawing closed and opened again never takes an old job's lines. A job carries what `step` draws in one swap: the view's lines, what they show, and the view's dimensions through its new frame. `step` and `begin` start from one helper, so both keep the views that show the same and both draw nothing while a deferred drawing holds. `step` is the same three run at once. The window's hook is `drawing_jobs::tick` (the shape of `save_job`): no caller yet, no UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawings: a built sheet leaves uncaptured moves out too (#4892 B7) `build` (every export) draws the parts where the design places them, as the provider's jobs do: one rule for lines, dimensions and files, and once nothing waits the provider's scene is still the built one. Exports change only while an occurrence move isn't captured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawings: the window reads a sheet's dimensions too, once no view is waiting (#4892 B7) Sheets now read their dimensions after their views (`drawing_scene::step`). Drawing views beside the window went through `begin`, which asks only for views, so with every view drawn the dimensions out of date were never read. With no view waiting, `tick` now steps on the window as before; reading dimensions is quick. The tick test adds a dimension and ends with none out of date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,14 @@
|
||||
//! [`hit`] says which of a scene's objects is under a point of the sheet, the one rule for picking
|
||||
//! there: the sheet page's hover and `drawing.scene.hit`.
|
||||
//!
|
||||
//! A view can also be drawn beside the window (#4892 U3): [`begin`] asks for the first view out of
|
||||
//! date, [`Job::run`] works its lines out on a copy of the session (on a worker thread), and
|
||||
//! [`finish`] takes them only for the latest request, while nothing it was asked from has changed.
|
||||
//! A result that comes too late changes nothing. [`step`] is the same three, run at once.
|
||||
//!
|
||||
//! Lines stand where the design places its parts: an occurrence move not captured yet is left out,
|
||||
//! as dimensions leave it out.
|
||||
//!
|
||||
//! [`SheetLayout::paper`]: solidcraft_geom::drawing_sheet::SheetLayout::paper
|
||||
//! [`groups`]: crate::drawing_pictures::groups
|
||||
//! [`TitleBlockLayout::paper`]: solidcraft_geom::title_block::TitleBlockLayout::paper
|
||||
@@ -71,6 +79,10 @@ pub const CMD: &str = "drawing.scene";
|
||||
/// for two scenes, not even across closing and opening its drawing.
|
||||
static GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Numbers every view asked of [`begin`], in every session, so a result is only ever taken by the
|
||||
/// cache that asked for it: not by the one a drawing gets when it is closed and opened again.
|
||||
static REQUEST: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// What became of one view of a sheet scene.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Status {
|
||||
@@ -275,6 +287,9 @@ pub fn sheet_id(drawing: &Drawing, named: &str, cmd: &str) -> Result<u64> {
|
||||
pub fn build(s: &Session, drawing: &Drawing, sheet: u64) -> Result<SheetScene> {
|
||||
let record = sheet_of(drawing, sheet)?;
|
||||
let on = views_on(drawing, sheet)?;
|
||||
// Where the design places its parts, as the provider draws them.
|
||||
let copy = (!s.pending_moves.is_empty()).then(|| designed(s));
|
||||
let s = copy.as_ref().unwrap_or(s);
|
||||
// Worked out once for the sheet: every view draws from the same one. Each view's lines are
|
||||
// placed as they are drawn, so at most one view's are held beside the scene.
|
||||
let shown = crate::drawing_view::shown(s);
|
||||
@@ -661,6 +676,8 @@ pub struct Cache {
|
||||
serial: u64,
|
||||
/// The views' lines of each drawing closed, for when it opens again. They outlive the rest.
|
||||
kept: KeptLines,
|
||||
/// The latest view asked of [`begin`]: the one result [`finish`] may take.
|
||||
requested: Option<u64>,
|
||||
/// Views drawn, for tests: a move draws none.
|
||||
#[cfg(test)]
|
||||
projections: usize,
|
||||
@@ -889,39 +906,146 @@ pub fn extract(s: &mut Session, sheet: u64) -> Result<SheetView> {
|
||||
/// out of date of one view, and says whether more are waiting: a short piece of work for the UI's
|
||||
/// tick, one view's projection or one read of a view's dimensions.
|
||||
pub fn step(s: &mut Session) -> bool {
|
||||
forget_closed(s);
|
||||
let (Ok(d), Some(sheet)) = (s.drawing(CMD).cloned(), s.drawing_scenes.sheet) else { return false };
|
||||
let Ok(on) = views_on(&d, sheet) else { return false };
|
||||
let Ok(state) = s.world_state() else { return false };
|
||||
keep_what_shows_the_same(s, &on, &state);
|
||||
if s.drawing_scenes.holds(&d, &state) {
|
||||
return false;
|
||||
}
|
||||
let Some((d, on, state)) = drawable(s) else { return false };
|
||||
let units = s.doc.drawing_units(&d).to_string();
|
||||
let waiting: Vec<(u64, ResolvedView)> =
|
||||
on.iter().filter(|(id, view, _)| !s.drawing_scenes.fresh(*id, &state, view)).map(|(id, view, _)| (*id, view.clone())).collect();
|
||||
let Some((id, view)) = waiting.first().cloned() else { return step_dimensions(s, &d, &on, &state, units) };
|
||||
let shown = match s.drawing_scenes.shown.take() {
|
||||
Some((was, shown)) if Arc::ptr_eq(&was, &state) => shown,
|
||||
_ => crate::drawing_view::shown(s),
|
||||
};
|
||||
let drawn = draw(s, &shown, id, &view);
|
||||
let shows = SourceKey::of(&shown);
|
||||
// The view's dimensions through its new frame, in the same swap as its lines.
|
||||
let frame = drawn.frame.map(|frame| (frame, view.line_group.value));
|
||||
let these = dimensions_on(&d, std::iter::once(id));
|
||||
let lines = draw_dimensions(s, &d, &these, |_| frame);
|
||||
let done = job_for(s, &d, id, view, state.clone()).run();
|
||||
finish(s, done);
|
||||
waiting.len() > 1 || !s.drawing_scenes.stale_dimensions(&d, &on, &state, &units).is_empty()
|
||||
}
|
||||
|
||||
/// What [`step`] and [`begin`] draw from: the drawing open, the views of the sheet last extracted
|
||||
/// and the world state, once each view that shows the same has taken it. None while a deferred
|
||||
/// drawing holds, or with nothing to draw from.
|
||||
fn drawable(s: &mut Session) -> Option<(Arc<Drawing>, Vec<(u64, ResolvedView, Vec2)>, Arc<ModelState>)> {
|
||||
forget_closed(s);
|
||||
let (Ok(d), Some(sheet)) = (s.drawing(CMD).cloned(), s.drawing_scenes.sheet) else { return None };
|
||||
let on = views_on(&d, sheet).ok()?;
|
||||
let state = s.world_state().ok()?;
|
||||
keep_what_shows_the_same(s, &on, &state);
|
||||
(!s.drawing_scenes.holds(&d, &state)).then_some((d, on, state))
|
||||
}
|
||||
|
||||
/// A shown set, and the world state it was worked out for.
|
||||
type ShownFor = (Arc<ModelState>, std::result::Result<ShownSet, Unmeasured>);
|
||||
|
||||
/// One view's lines, worked out beside the window: [`begin`] asks for them, [`Job::run`] works them
|
||||
/// out anywhere, and [`finish`] takes them.
|
||||
pub struct Job {
|
||||
/// The session as the design places its parts.
|
||||
copy: Session,
|
||||
/// The drawing open when the job was asked for: its records of the view's dimensions.
|
||||
drawing: Arc<Drawing>,
|
||||
ticket: Ticket,
|
||||
/// The cache's shown set, reused when it is for the copy's world state.
|
||||
shown: Option<ShownFor>,
|
||||
}
|
||||
|
||||
/// What a job was asked from: its request, the view as it resolved, and the session's world state,
|
||||
/// revision and drawing units then.
|
||||
struct Ticket {
|
||||
request: u64,
|
||||
view: u64,
|
||||
resolved: ResolvedView,
|
||||
state: Arc<ModelState>,
|
||||
revision: u64,
|
||||
units: String,
|
||||
}
|
||||
|
||||
/// A job's work, for [`finish`]: the view's lines and what they show, its dimensions through its new
|
||||
/// frame, and the shown set they were drawn from.
|
||||
pub struct Done {
|
||||
ticket: Ticket,
|
||||
drawn: Drawn,
|
||||
shows: Option<Arc<SourceKey>>,
|
||||
dimensions: Vec<Arc<Dimension>>,
|
||||
lines: Vec<DrawnDimension>,
|
||||
shown: ShownFor,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Works the view's lines and dimensions out: the one long piece of work, on any thread.
|
||||
pub fn run(self) -> Done {
|
||||
let Job { copy, drawing, ticket, shown } = self;
|
||||
// A design whose parts can't be placed draws its views as placeholders (`shown` says why).
|
||||
let state = copy.world_state().unwrap_or_else(|_| copy.model.state());
|
||||
let shown = match shown {
|
||||
Some((was, shown)) if Arc::ptr_eq(&was, &state) => shown,
|
||||
_ => crate::drawing_view::shown(©),
|
||||
};
|
||||
let drawn = draw(©, &shown, ticket.view, &ticket.resolved);
|
||||
let shows = SourceKey::of(&shown);
|
||||
// The view's dimensions through its new frame, taken in the same swap as its lines.
|
||||
let frame = drawn.frame.map(|frame| (frame, ticket.resolved.line_group.value));
|
||||
let dimensions = dimensions_on(&drawing, std::iter::once(ticket.view));
|
||||
let lines = draw_dimensions(©, &drawing, &dimensions, |_| frame);
|
||||
Done { ticket, drawn, shows, dimensions, lines, shown: (state, shown) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks for the first view out of date on the sheet last extracted, to draw beside the window, as
|
||||
/// [`step`] would draw it: none when nothing is waiting, or while a deferred drawing holds. One at a
|
||||
/// time: asking again replaces the earlier request.
|
||||
pub fn begin(s: &mut Session) -> Option<Job> {
|
||||
let (d, on, state) = drawable(s)?;
|
||||
let (id, view, _) = on.into_iter().find(|(id, view, _)| !s.drawing_scenes.fresh(*id, &state, view))?;
|
||||
Some(job_for(s, &d, id, view, state))
|
||||
}
|
||||
|
||||
/// Asks for view `id` of drawing `d`, resolved as `view`, of the world `state`: the latest request
|
||||
/// from now on.
|
||||
fn job_for(s: &mut Session, d: &Arc<Drawing>, id: u64, view: ResolvedView, state: Arc<ModelState>) -> Job {
|
||||
let request = REQUEST.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
|
||||
s.drawing_scenes.requested = Some(request);
|
||||
let units = s.doc.drawing_units(d).to_string();
|
||||
let ticket = Ticket { request, view: id, resolved: view, state, revision: s.revision, units };
|
||||
Job { copy: designed(s), drawing: d.clone(), ticket, shown: s.drawing_scenes.shown.take() }
|
||||
}
|
||||
|
||||
/// Takes a job's work into the cache when it is for its latest request and nothing it was asked from
|
||||
/// has changed since: the drawing, the model and the view as it resolves. Otherwise it is dropped and
|
||||
/// nothing changes. Says whether it was taken.
|
||||
pub fn finish(s: &mut Session, done: Done) -> bool {
|
||||
forget_closed(s);
|
||||
let Done { ticket, drawn, shows, dimensions, lines, shown } = done;
|
||||
if s.drawing_scenes.requested != Some(ticket.request) {
|
||||
return false;
|
||||
}
|
||||
s.drawing_scenes.requested = None;
|
||||
// Any change to the document since, even one undone, moves the revision: so the drawing, its
|
||||
// dimensions' records and its units are as they were.
|
||||
let holds = s.revision == ticket.revision
|
||||
&& s.world_state().is_ok_and(|state| Arc::ptr_eq(&state, &ticket.state))
|
||||
&& s.drawing(CMD).ok().and_then(|d| d.resolve_view(ticket.view).ok()).is_some_and(|view| view == ticket.resolved);
|
||||
if !holds {
|
||||
return false;
|
||||
}
|
||||
let cache = &mut s.drawing_scenes;
|
||||
cache.shown = Some((state.clone(), shown));
|
||||
cache.shown = Some(shown);
|
||||
cache.serial += 1;
|
||||
let serial = cache.serial;
|
||||
cache.views.insert(id, Entry { state: state.clone(), shows, view, drawn, serial });
|
||||
cache.keep_dimensions(these, lines, &state, serial, &units);
|
||||
let entry = Entry { state: ticket.state.clone(), shows, view: ticket.resolved, drawn, serial };
|
||||
cache.views.insert(ticket.view, entry);
|
||||
cache.keep_dimensions(dimensions, lines, &ticket.state, serial, &ticket.units);
|
||||
#[cfg(test)]
|
||||
{
|
||||
cache.projections += 1;
|
||||
}
|
||||
waiting.len() > 1 || !cache.stale_dimensions(&d, &on, &state, &units).is_empty()
|
||||
true
|
||||
}
|
||||
|
||||
/// Forgets the request [`begin`] made: its lines, when they come, are dropped.
|
||||
pub fn cancel(s: &mut Session) {
|
||||
s.drawing_scenes.requested = None;
|
||||
}
|
||||
|
||||
/// A copy of `s` as the design places its parts: an occurrence move not captured yet is left out.
|
||||
fn designed(s: &Session) -> Session {
|
||||
let mut copy = s.scratch();
|
||||
copy.pending_moves.clear();
|
||||
copy
|
||||
}
|
||||
|
||||
/// Reads the dimensions out of date of the first view that has any, through its frame, and says
|
||||
|
||||
@@ -512,7 +512,8 @@ fn a_change_that_shows_the_same_draws_no_view() {
|
||||
run(&mut s, "drawing.open", drawing);
|
||||
assert_eq!(extract(&mut s, sheet).unwrap().stale.len(), 2, "40 tall shows otherwise");
|
||||
run(&mut s, "edit.restore_checkpoint", json!({"checkpoint": checkpoint}));
|
||||
// A step straight after the change, with no extract between, finds nothing to draw either.
|
||||
// A step or a job straight after the change, with no extract between, finds nothing to draw either.
|
||||
assert!(begin(&mut s).is_none());
|
||||
assert!(!step(&mut s));
|
||||
let back = extract(&mut s, sheet).unwrap();
|
||||
assert!(back.stale.is_empty(), "{:?}", back.stale);
|
||||
@@ -559,6 +560,7 @@ fn a_deferred_drawing_keeps_its_lines_until_it_is_updated() {
|
||||
let checkpoint = run(&mut s, "edit.checkpoint", json!({}))["checkpoint"].clone();
|
||||
extract(&mut s, sheet).unwrap();
|
||||
assert!(!step(&mut s), "nothing is drawn before an update");
|
||||
assert!(begin(&mut s).is_none(), "nor asked for beside the window");
|
||||
assert_eq!(run(&mut s, "drawing.update", json!({}))["waiting"], 2);
|
||||
let before = settled(&mut s, sheet);
|
||||
assert!(before.stale.is_empty());
|
||||
@@ -1767,3 +1769,205 @@ fn a_views_boundary_in_the_scene_is_its_boundary_in_every_style() {
|
||||
assert!((boundary.min - corner("min")).len() < 1e-9 && (boundary.max - corner("max")).len() < 1e-9, "{style}: {boundary:?} {inspected}");
|
||||
}
|
||||
}
|
||||
|
||||
/// What a dropped result must leave as it was: the provider's scene and its number, the design,
|
||||
/// its undo and redo steps, the session's revision and whether it needs saving.
|
||||
fn untouched(s: &mut Session, sheet: u64) -> (String, u64, String, [usize; 2], u64, bool) {
|
||||
let shown = extract(s, sheet).unwrap();
|
||||
(bytes(&shown.scene), shown.generation, s.doc.to_json(), [s.undo.len(), s.redo.len()], s.revision, s.is_dirty())
|
||||
}
|
||||
|
||||
/// The block with a 10 mm cube on it, in a component of its own, and [`front_and_top`]'s drawing of
|
||||
/// them, open: the session, the sheet and the cube's occurrence.
|
||||
fn cube_on_block() -> (Session, u64, Value) {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 100, "width": 50, "height": 20, "body_name": "Block"}));
|
||||
let cube = run(&mut s, "component.create", json!({"name": "Cube", "grounded": false}))["occurrence"].clone();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [0, 0, 20]}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
let (s, sheet, _) = front_and_top_of(s);
|
||||
(s, sheet, cube)
|
||||
}
|
||||
|
||||
/// Runs model command `id` with the drawing closed, as the model can only be changed then, and
|
||||
/// opens the drawing again.
|
||||
fn in_the_model(s: &mut Session, id: &str, p: Value) {
|
||||
let drawing = ids::encode(s.open_drawing.unwrap());
|
||||
run(s, "drawing.close", json!({}));
|
||||
run(s, id, p);
|
||||
run(s, "drawing.open", json!({"drawing": drawing}));
|
||||
}
|
||||
|
||||
/// Lines worked out beside the window (#4892 B7) on another thread are taken once they come back,
|
||||
/// a view at a time, until the provider's scene is the built one, byte for byte; then nothing is
|
||||
/// asked for. Taking them changes nothing authored and adds no undo step.
|
||||
#[test]
|
||||
fn a_job_beside_the_window_draws_as_a_step_does() {
|
||||
let (mut s, sheet, _) = front_and_top();
|
||||
extract(&mut s, sheet).unwrap();
|
||||
let (doc, undo, revision) = (s.doc.to_json(), s.undo.len(), s.revision);
|
||||
while let Some(job) = begin(&mut s) {
|
||||
let done = std::thread::spawn(move || job.run()).join().unwrap();
|
||||
assert!(finish(&mut s, done));
|
||||
}
|
||||
let end = extract(&mut s, sheet).unwrap();
|
||||
assert_eq!((end.stale.len(), bytes(&end.scene), s.drawing_scenes.projections), (0, bytes(&built(&s, sheet).scene), 2));
|
||||
assert_eq!((s.doc.to_json(), s.undo.len(), s.revision), (doc, undo, revision));
|
||||
}
|
||||
|
||||
/// Only the latest request's lines are taken: asking again leaves the earlier result to be
|
||||
/// dropped, whichever comes back first.
|
||||
#[test]
|
||||
fn only_the_latest_request_is_taken() {
|
||||
for latest_first in [false, true] {
|
||||
let (mut s, sheet, _) = front_and_top();
|
||||
extract(&mut s, sheet).unwrap();
|
||||
let earlier = begin(&mut s).unwrap().run();
|
||||
let latest = begin(&mut s).unwrap().run();
|
||||
let was = untouched(&mut s, sheet);
|
||||
if latest_first {
|
||||
assert!(finish(&mut s, latest));
|
||||
assert!(!finish(&mut s, earlier));
|
||||
} else {
|
||||
assert!(!finish(&mut s, earlier));
|
||||
assert_eq!(untouched(&mut s, sheet), was);
|
||||
assert!(finish(&mut s, latest));
|
||||
}
|
||||
assert_eq!((extract(&mut s, sheet).unwrap().stale.len(), s.drawing_scenes.projections), (1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// @0xlillith's four orders at the point a job's lines come back (#4892 W14): after an edit of the
|
||||
/// drawing or of the model, after an edit and its undo (the view as it was, the design not), after
|
||||
/// the drawing is closed and opened again, and after a cancel, they are dropped. Dropped, they
|
||||
/// change nothing on the sheet or in the design, and the next request draws the view as it is now.
|
||||
#[test]
|
||||
fn a_job_is_dropped_after_an_edit_an_undo_a_reopen_or_a_cancel() {
|
||||
let scale = |s: &mut Session, set: &str| {
|
||||
let front = s.drawing("test").unwrap().views[0].id;
|
||||
drop(run(s, "drawing.view.edit", json!({"view": ids::encode(front), "set": {"scale": set}})));
|
||||
};
|
||||
let reopen = |s: &mut Session| {
|
||||
let drawing = ids::encode(s.open_drawing.unwrap());
|
||||
run(s, "drawing.close", json!({}));
|
||||
drop(run(s, "drawing.open", json!({"drawing": drawing})));
|
||||
};
|
||||
// Each starts from the block with a tower added since `checkpoint`.
|
||||
let orders: [(&str, &dyn Fn(&mut Session, &Value)); 5] = [
|
||||
("an edit of the drawing", &|s, _| scale(s, "1:5")),
|
||||
("an edit of the model", &|s, checkpoint| drop(run(s, "edit.restore_checkpoint", json!({"checkpoint": checkpoint})))),
|
||||
("an edit and its undo", &|s, _| {
|
||||
scale(s, "1:5");
|
||||
drop(run(s, "edit.undo", json!({})));
|
||||
}),
|
||||
("closing and opening the drawing", &|s, _| reopen(s)),
|
||||
("a cancel", &|s, _| cancel(s)),
|
||||
];
|
||||
for (order, after) in orders {
|
||||
let (mut s, sheet, _) = front_and_top();
|
||||
let checkpoint = run(&mut s, "edit.checkpoint", json!({}))["checkpoint"].clone();
|
||||
let drawing = ids::encode(s.open_drawing.unwrap());
|
||||
run(&mut s, "drawing.close", json!({}));
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 40, "corner": [40, 60, 0], "body_name": "Tower", "operation": "new"}));
|
||||
run(&mut s, "drawing.open", json!({"drawing": drawing}));
|
||||
settled(&mut s, sheet);
|
||||
scale(&mut s, "1:1");
|
||||
let done = begin(&mut s).unwrap().run();
|
||||
after(&mut s, &checkpoint);
|
||||
let was = untouched(&mut s, sheet);
|
||||
assert!(!finish(&mut s, done), "{order}");
|
||||
assert_eq!(untouched(&mut s, sheet), was, "{order}");
|
||||
assert_eq!(bytes(&settled(&mut s, sheet).scene), bytes(&built(&s, sheet).scene), "{order}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A result is dropped too when what it was asked from changes without the session's revision
|
||||
/// moving: the view as it resolves now (a drawing put in place whole), or the model's world state
|
||||
/// (a move not captured yet, cleared directly).
|
||||
#[test]
|
||||
fn a_job_is_dropped_when_its_view_or_the_model_changed_unannounced() {
|
||||
let (mut s, sheet, [front, _]) = front_and_top();
|
||||
extract(&mut s, sheet).unwrap();
|
||||
let done = begin(&mut s).unwrap().run();
|
||||
let mut d = (**s.drawing("test").unwrap()).clone();
|
||||
let at = d.views.iter().position(|v| v.id == front).unwrap();
|
||||
let mut view = (*d.views[at]).clone();
|
||||
view.settings.scale = Some("1:5".parse().unwrap());
|
||||
d.views[at] = Arc::new(view);
|
||||
s.doc_mut().put_drawing(Arc::new(d));
|
||||
let revision = s.revision;
|
||||
assert!(!finish(&mut s, done));
|
||||
assert_eq!((s.revision, s.drawing_scenes.projections), (revision, 0), "the view");
|
||||
|
||||
let (mut s, sheet, cube) = cube_on_block();
|
||||
in_the_model(&mut s, "occurrence.move", json!({"occurrence": cube, "translate": [30, 0, 0], "capture": false}));
|
||||
extract(&mut s, sheet).unwrap();
|
||||
let done = begin(&mut s).unwrap().run();
|
||||
s.pending_moves.clear();
|
||||
let revision = s.revision;
|
||||
assert!(!finish(&mut s, done));
|
||||
assert_eq!((s.revision, s.drawing_scenes.projections), (revision, 0), "the model");
|
||||
}
|
||||
|
||||
/// An occurrence move not captured yet is left out, as dimensions leave it out: a sheet built for
|
||||
/// an export and the provider's scene both draw the cube on the block where the design places it,
|
||||
/// as before the move. Once the move is captured, both draw it.
|
||||
#[test]
|
||||
fn an_uncaptured_move_is_drawn_where_the_design_places_the_part() {
|
||||
let (mut s, sheet, cube) = cube_on_block();
|
||||
let placed = settled(&mut s, sheet);
|
||||
in_the_model(&mut s, "occurrence.move", json!({"occurrence": cube, "translate": [30, 0, 0], "capture": false}));
|
||||
assert_eq!(s.pending_moves.len(), 1);
|
||||
assert_eq!(bytes(&built(&s, sheet).scene), bytes(&placed.scene));
|
||||
assert_eq!(bytes(&settled(&mut s, sheet).scene), bytes(&placed.scene));
|
||||
in_the_model(&mut s, "component.capture_position", json!({}));
|
||||
let captured = settled(&mut s, sheet);
|
||||
assert_ne!(bytes(&captured.scene), bytes(&placed.scene));
|
||||
assert_eq!(bytes(&captured.scene), bytes(&built(&s, sheet).scene));
|
||||
}
|
||||
|
||||
/// Seeded mixes of edits, undos, redos, steps and jobs begun, run, cancelled and taken in any
|
||||
/// order: once nothing is waiting, the provider's scene is always the built one, byte for byte.
|
||||
#[test]
|
||||
fn seeded_edits_steps_and_jobs_always_end_at_the_built_scene() {
|
||||
let pick = |seed: u64, salt: u64, n: u64| ((seed ^ salt).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 33) % n;
|
||||
for seed in 1..=24u64 {
|
||||
let (mut s, sheet, [front, top]) = front_and_top();
|
||||
let edit =
|
||||
|s: &mut Session, view: u64, set: Value| s.execute("drawing.view.edit", &json!({"view": ids::encode(view), "set": set})).map(|_| ());
|
||||
let mut running = Vec::new();
|
||||
for i in 0..16u64 {
|
||||
let scale = ["1:1", "1:2", "1:5"][pick(seed, 200 + i, 3) as usize];
|
||||
let _ = match pick(seed, i, 9) {
|
||||
0 => edit(&mut s, front, json!({"scale": scale})),
|
||||
1 => edit(&mut s, top, json!({"place": {"aligned": 200.0 + 10.0 * pick(seed, 100 + i, 5) as f64}})),
|
||||
2 => s.execute("edit.undo", &json!({})).map(|_| ()),
|
||||
3 => s.execute("edit.redo", &json!({})).map(|_| ()),
|
||||
4 => {
|
||||
step(&mut s);
|
||||
Ok(())
|
||||
}
|
||||
5 => extract(&mut s, sheet).map(drop),
|
||||
6 => {
|
||||
running.extend(begin(&mut s).map(Job::run));
|
||||
Ok(())
|
||||
}
|
||||
7 if !running.is_empty() => {
|
||||
let done = running.remove(pick(seed, 300 + i, running.len() as u64) as usize);
|
||||
finish(&mut s, done);
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
cancel(&mut s);
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
for done in running {
|
||||
finish(&mut s, done);
|
||||
}
|
||||
let end = settled(&mut s, sheet);
|
||||
assert!(end.stale.is_empty(), "seed {seed}");
|
||||
assert_eq!(bytes(&end.scene), bytes(&built(&s, sheet).scene), "seed {seed}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! A drawing's views drawn off the window's thread (#4892 B7): working a view's lines out takes long
|
||||
//! enough on a large design that the window must keep drawing meanwhile. Each view is worked out
|
||||
//! on a worker from a copy of the session, and its lines are taken up only when nothing they were
|
||||
//! asked from has changed (`drawing_scene::finish`). The drawing page calls [`tick`] each frame.
|
||||
|
||||
use solidcraft_engine::drawing_scene::{self, Done};
|
||||
|
||||
use crate::SolveApp;
|
||||
|
||||
/// A view being drawn on a worker thread.
|
||||
pub struct Drawing {
|
||||
done: std::sync::mpsc::Receiver<Done>,
|
||||
}
|
||||
|
||||
/// Takes up a view a worker has drawn, then starts on the next one out of date, and says whether
|
||||
/// any is still being drawn. In the browser, or when no thread can start, it draws one view here.
|
||||
pub fn tick(app: &mut SolveApp) -> bool {
|
||||
if let Some(drawing) = &app.drawing_job {
|
||||
match drawing.done.try_recv() {
|
||||
Ok(done) => {
|
||||
drawing_scene::finish(&mut app.session, done);
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return true,
|
||||
// The worker stopped without lines: the view is asked for again below.
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {}
|
||||
}
|
||||
app.drawing_job = None;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// No view waiting: the dimensions out of date are read here (they are quick), as `step` does.
|
||||
let Some(job) = drawing_scene::begin(&mut app.session) else { return drawing_scene::step(&mut app.session) };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let worker = std::thread::Builder::new().name("drawing view".into()).spawn(move || {
|
||||
let _ = tx.send(job.run());
|
||||
});
|
||||
if worker.is_ok() {
|
||||
app.drawing_job = Some(Drawing { done: rx });
|
||||
return true;
|
||||
}
|
||||
// No thread to draw on: draw here, as before.
|
||||
drawing_scene::cancel(&mut app.session);
|
||||
}
|
||||
drawing_scene::step(&mut app.session)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::doc::drawing::view::{View, ViewKind, ViewPlace};
|
||||
use solidcraft_engine::doc::drawing_view::{ViewDirection, ViewOverrides};
|
||||
use solidcraft_engine::drawing_scene;
|
||||
use solidcraft_engine::geom::drawing_view::NamedView;
|
||||
|
||||
/// Ticked until nothing is being drawn, the window ends at the sheet built at once, byte for
|
||||
/// byte, its dimension read too, with the design and its undo steps as they were.
|
||||
#[test]
|
||||
fn ticks_draw_the_sheet_beside_the_window() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.box", json!({"length": 100, "width": 80, "height": 20, "body_name": "Plate"})).unwrap();
|
||||
h.app.run("drawing.create", json!({})).unwrap();
|
||||
let mut d = (**h.app.session.drawing("test").unwrap()).clone();
|
||||
let mut ids = solidcraft_engine::doc::ids::IdSource::seeded(4892);
|
||||
let (sheet, source) = (d.sheets[0].id, d.sources[0].id);
|
||||
let mut front = 0;
|
||||
for (at, view) in [([120.0, 150.0], NamedView::Front), ([280.0, 150.0], NamedView::Right)] {
|
||||
let id = d.allocate_id(&mut ids).unwrap();
|
||||
if view == NamedView::Front {
|
||||
front = id;
|
||||
}
|
||||
let kind = ViewKind::Base { source, view: ViewDirection::Named(view) };
|
||||
d.add_views([Arc::new(View {
|
||||
id,
|
||||
sheet,
|
||||
kind,
|
||||
place: ViewPlace::At(at),
|
||||
settings: ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() },
|
||||
reference_shown_percent: 0.0,
|
||||
})]);
|
||||
}
|
||||
h.app.session.doc_mut().put_drawing(Arc::new(d));
|
||||
// The plate's top front edge, read once no view is waiting.
|
||||
let view = solidcraft_engine::doc::ids::encode(front);
|
||||
h.app.run("drawing.dimension.add", json!({"view": view, "kind": {"length": {"edge": [50, 0, 20]}}})).unwrap();
|
||||
let (doc, undo) = (h.app.session.doc.to_json(), h.app.session.undo.len());
|
||||
drawing_scene::extract(&mut h.app.session, sheet).unwrap();
|
||||
for _ in 0..10_000 {
|
||||
if !super::tick(&mut h.app) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
let shown = drawing_scene::extract(&mut h.app.session, sheet).unwrap();
|
||||
let d = h.app.session.drawing("test").unwrap().clone();
|
||||
let built = drawing_scene::build(&h.app.session, &d, sheet).unwrap();
|
||||
assert!(shown.stale.is_empty() && shown.stale_dimensions.is_empty() && h.app.drawing_job.is_none());
|
||||
assert_eq!(serde_json::to_string(&*shown.scene).unwrap(), serde_json::to_string(&built.scene).unwrap());
|
||||
assert_eq!((h.app.session.doc.to_json(), h.app.session.undo.len()), (doc, undo));
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,7 @@ mod blend_dialog_tests;
|
||||
#[cfg(test)]
|
||||
mod boundary_dialog_tests;
|
||||
pub(crate) mod display_lod;
|
||||
pub mod drawing_jobs;
|
||||
pub mod drawing_workspace;
|
||||
#[cfg(test)]
|
||||
mod file_load_ui_tests;
|
||||
@@ -552,6 +553,8 @@ pub struct SolveApp {
|
||||
pub(crate) easyprint: easyprint_ui::State,
|
||||
/// A Save or Save As being written on a worker (see [`save_job`]).
|
||||
saving: Option<save_job::Saving>,
|
||||
/// A drawing view being drawn on a worker (see [`drawing_jobs`]).
|
||||
drawing_job: Option<drawing_jobs::Drawing>,
|
||||
pub viewport: viewport::ViewportState,
|
||||
pub render_workspace: render_workspace::State,
|
||||
pub drawing_workspace: drawing_workspace::State,
|
||||
@@ -817,6 +820,7 @@ impl SolveApp {
|
||||
slicer_wait: None,
|
||||
easyprint: Default::default(),
|
||||
saving: None,
|
||||
drawing_job: None,
|
||||
viewport: viewport::ViewportState::default(),
|
||||
render_workspace,
|
||||
drawing_workspace: Default::default(),
|
||||
|
||||
Reference in New Issue
Block a user