mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
render: a display list past the block-expansion budget says what it left out (#1278)
* render: a display list past the block-expansion budget says what it left out * Report viewport truncation only after the expansion budget is exceeded * fmt: keep ui-egui control.rs rustfmt-clean
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
//! Nested, self-referencing and arrayed block references in a hostile DXF must not hang drawing
|
||||
//! or extents (issue #255): the expansion is bounded, not only its depth.
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Space};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Block, Common, Drawing, Entity, EntityKind, Handle, Insert, Line, Space, Viewport};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use cadcraft_render::{MAX_BLOCK_EXPANSION, Options, build};
|
||||
|
||||
fn read(text: &str) -> Drawing {
|
||||
@@ -63,3 +66,63 @@ fn ordinary_nested_and_arrayed_blocks_are_drawn_whole() {
|
||||
assert!(matches!(d.model.iter().next().map(|e| &e.kind), Some(EntityKind::Insert(_))));
|
||||
assert_eq!(build(&d, &Space::Model, &Options::default()).prims.len(), cadcraft_doc::MAX_BLOCK_DEPTH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_build_that_spends_the_expansion_budget_says_so() {
|
||||
// 10,000 x 10,000 copies of a one-line block: far past the budget. The list stops at the
|
||||
// budget and says so, so a screen, plot or export missing contents is not silent (#1070).
|
||||
let d = read(&minsert(10_000, 10_000));
|
||||
let list = build(&d, &Space::Model, &Options::default());
|
||||
assert!(list.truncated, "block contents were left out without a word");
|
||||
assert!(list.prims.len() <= MAX_BLOCK_EXPANSION, "{}", list.prims.len());
|
||||
// A drawing within the budget is whole and says nothing.
|
||||
let d = read(&minsert(3, 2));
|
||||
let list = build(&d, &Space::Model, &Options::default());
|
||||
assert!(!list.truncated);
|
||||
assert_eq!(list.prims.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_followup_viewport_does_not_mark_an_exact_budget_truncated() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.ensure_layer("FROZEN");
|
||||
let mut block = Block::new("A");
|
||||
block.entities.push(Entity {
|
||||
handle: Handle(0x200),
|
||||
common: Common { layer: "FROZEN".into(), ..Common::default() },
|
||||
kind: EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(1.0, 1.0, 0.0) }),
|
||||
});
|
||||
d.blocks.insert(block.name.clone(), Arc::new(block));
|
||||
let insert = Insert {
|
||||
block: "A".into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: Vec::new(),
|
||||
cols: 1_000,
|
||||
rows: 1_000,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Insert(insert)).unwrap();
|
||||
let viewport = |center: Vec3, view_center: Vec2, frozen_layers| Viewport {
|
||||
center,
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
view_center,
|
||||
view_height: 100.0,
|
||||
id: 2,
|
||||
locked: false,
|
||||
frozen_layers,
|
||||
layer_colors: Vec::new(),
|
||||
clip: None,
|
||||
on: true,
|
||||
};
|
||||
let paper = Space::Paper("Layout1".into());
|
||||
d.add(&paper, Common::default(), EntityKind::Viewport(viewport(Vec3::new(50.0, 50.0, 0.0), Vec2::ZERO, vec!["FROZEN".into()]))).unwrap();
|
||||
d.add(&paper, Common::default(), EntityKind::Viewport(viewport(Vec3::new(200.0, 50.0, 0.0), Vec2::new(10_000.0, 10_000.0), Vec::new()))).unwrap();
|
||||
|
||||
let list = build(&d, &paper, &Options::default());
|
||||
assert_eq!(list.expanded, MAX_BLOCK_EXPANSION);
|
||||
assert!(!list.truncated, "the first viewport consumed the budget exactly and the second saw no model content");
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ impl Backend for Headless {
|
||||
if let Some(path) = str_p("path") {
|
||||
std::fs::write(&path, &png).map_err(|e| format!("{path}: {e}"))?;
|
||||
}
|
||||
Ok(json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(&png), "width": w, "height": h}))
|
||||
Ok(json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(&png), "width": w, "height": h, "truncated": list.truncated}))
|
||||
}
|
||||
"app.open" => {
|
||||
let path = str_p("path").ok_or("missing path")?;
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct DisplayList {
|
||||
pub sheet: Option<Sheet>,
|
||||
/// Block contents expanded while building (see [`MAX_BLOCK_EXPANSION`]).
|
||||
pub expanded: usize,
|
||||
/// The expansion budget was spent: block contents past it are not in the list. The screen,
|
||||
/// plots and exports draw what is here; the CLI and the control channel report this, so an
|
||||
/// output that leaves contents out is never silent (#1070).
|
||||
pub truncated: bool,
|
||||
/// Plot style end, join and fill style per primitive (parallel to `prims`; missing entries
|
||||
/// are "use object"), turned into geometry by [`pen::apply`].
|
||||
pub pens: Vec<Pen>,
|
||||
|
||||
@@ -291,10 +291,14 @@ impl Builder<'_> {
|
||||
/// Take one unit of the block-expansion budget; false once it is spent.
|
||||
fn expand(&mut self) -> bool {
|
||||
self.list.expanded = self.list.expanded.saturating_add(1);
|
||||
match self.shared_budget {
|
||||
let ok = match self.shared_budget {
|
||||
Some(spent) => spent.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < MAX_BLOCK_EXPANSION,
|
||||
None => self.list.expanded <= MAX_BLOCK_EXPANSION,
|
||||
};
|
||||
if !ok {
|
||||
self.list.truncated = true;
|
||||
}
|
||||
ok
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
// The contents are in paper units now: draw their pens before clipping.
|
||||
pen::apply(&mut sub.list, unit_mm);
|
||||
b.list.expanded = b.list.expanded.saturating_add(sub.list.expanded);
|
||||
b.list.truncated |= sub.list.truncated;
|
||||
append_clipped(&mut b.list, &sub.list, clip);
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ pub fn inspect(app: &CadApp, ctx: &egui::Context) -> Value {
|
||||
"snap": app.canvas.snap.map(|s| json!({"point": [s.point.x, s.point.y], "mode": s.name})),
|
||||
"session": app.session.summary(),
|
||||
"closePrompt": app.close_prompt(),
|
||||
"perf": {"frameMs": app.frame_ms, "buildMs": app.canvas.build_ms, "drawMs": app.canvas.draw_ms, "meshMs": app.canvas.mesh_ms, "building": app.canvas.building(), "renderer": if app.canvas.gpu.is_some() { "gpu" } else { "cpu" }, "prims": app.canvas.list.as_ref().map(|l| l.prims.len()), "uiFrames": app.ui_frames, "passIndex": ctx.current_pass_index()},
|
||||
"perf": {"frameMs": app.frame_ms, "buildMs": app.canvas.build_ms, "drawMs": app.canvas.draw_ms, "meshMs": app.canvas.mesh_ms, "building": app.canvas.building(), "renderer": if app.canvas.gpu.is_some() { "gpu" } else { "cpu" }, "prims": app.canvas.list.as_ref().map(|l| l.prims.len()), "truncated": app.canvas.list.as_ref().map(|l| l.truncated), "uiFrames": app.ui_frames, "passIndex": ctx.current_pass_index()},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -492,9 +492,9 @@ pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Ou
|
||||
if let Err(e) = std::fs::write(path, &png) {
|
||||
return err(e);
|
||||
}
|
||||
return ok(render_reply(&png, w, h, Some(path)));
|
||||
return ok(render_reply(&png, w, h, Some(path), list.truncated));
|
||||
}
|
||||
ok(render_reply(&png, w, h, None))
|
||||
ok(render_reply(&png, w, h, None, list.truncated))
|
||||
}
|
||||
"app.open" => {
|
||||
let Some(path) = s("path") else { return err("missing path") };
|
||||
@@ -537,9 +537,11 @@ fn render_view(bounds: &cadcraft_geom::Bounds2, center: Vec2, height: f64, fit:
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ui.render` reply, matching the headless backend (`pngBase64`, `width`, `height`), plus `bytes` and `path`.
|
||||
fn render_reply(png: &[u8], w: u32, h: u32, path: Option<&str>) -> Value {
|
||||
let mut v = json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(png), "width": w, "height": h, "bytes": png.len()});
|
||||
/// The `ui.render` reply, matching the headless backend (`pngBase64`, `width`, `height`), plus
|
||||
/// `bytes`, `path` and `truncated` (the build left block contents out, see #1070).
|
||||
fn render_reply(png: &[u8], w: u32, h: u32, path: Option<&str>, truncated: bool) -> Value {
|
||||
let mut v =
|
||||
json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(png), "width": w, "height": h, "bytes": png.len(), "truncated": truncated});
|
||||
if let (Some(path), Some(o)) = (path, v.as_object_mut()) {
|
||||
o.insert("path".to_string(), json!(path));
|
||||
}
|
||||
@@ -816,12 +818,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn render_reply_has_base64_width_height_and_keeps_old_keys() {
|
||||
let r = render_reply(b"abc", 7, 5, Some("x.png"));
|
||||
let r = render_reply(b"abc", 7, 5, Some("x.png"), false);
|
||||
assert_eq!(r["pngBase64"], "YWJj");
|
||||
assert_eq!(r["width"], 7);
|
||||
assert_eq!(r["height"], 5);
|
||||
assert_eq!(r["bytes"], 3);
|
||||
assert_eq!(r["path"], "x.png");
|
||||
assert!(render_reply(b"abc", 7, 5, None).get("path").is_none());
|
||||
assert_eq!(r["truncated"], false);
|
||||
assert!(render_reply(b"abc", 7, 5, None, true).get("path").is_none());
|
||||
assert_eq!(render_reply(b"abc", 7, 5, None, true)["truncated"], true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user