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:
CamWight
2026-11-06 07:10:50 +00:00
committed by GitHub
parent ecf75d9088
commit f50edc1ed0
4 changed files with 457 additions and 23 deletions
+146 -22
View File
@@ -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(&copy),
};
let drawn = draw(&copy, &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(&copy, &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
+205 -1
View File
@@ -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}");
}
}
+102
View File
@@ -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));
}
}
+4
View File
@@ -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(),