mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-12 00:45:16 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910278bf4d | ||
|
|
886f6b66b4 | ||
|
|
b15c5a023e |
@@ -19,6 +19,21 @@ const MAX_TABLE_ROWS: usize = 2000;
|
||||
const MAX_TABLE_COLS: usize = 200;
|
||||
/// Attribute columns kept at most.
|
||||
const MAX_TAGS: usize = 190;
|
||||
/// Attribute text one extraction may hold across all its references (each row owns a copy of its
|
||||
/// values, constants included). Far above any real schedule; an error, never a silent cut.
|
||||
const MAX_VALUE_BYTES: usize = 1 << 30;
|
||||
|
||||
/// Add `add` bytes to `used`; an error past `limit`.
|
||||
fn charge(used: &mut usize, add: usize, limit: usize) -> Result<()> {
|
||||
*used = used.saturating_add(add);
|
||||
if *used > limit {
|
||||
return Err(bad(
|
||||
"dataextraction",
|
||||
format!("the attribute text of the block references passes {} MiB; narrow `blocks`, `tags` or `handles`", limit >> 20),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const PARAMS: &str = "{blocks?: \"A*,B\" (block names, wildcards, default *), tags?: attribute tags to include (wildcards, default *), handles? (default: the block references in model space and every layout), combine?: bool (default true: identical rows once, with a Count column; false: one row per reference with handle, layer, position and rotation), file?: path (.csv; .txt is tab-separated), at?: [x,y] (insert a table there), title?: table title} → {columns, rows, file?, table?}";
|
||||
|
||||
@@ -167,6 +182,8 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
|
||||
};
|
||||
let selected = selected_tags(&ents, d, o);
|
||||
let mut out = Vec::new();
|
||||
let mut used = 0usize;
|
||||
let mut const_sizes: HashMap<String, Vec<(String, usize)>> = HashMap::new();
|
||||
let mut consts: HashMap<String, Vec<(String, String)>> = HashMap::new();
|
||||
for e in &ents {
|
||||
let EntityKind::Insert(ins) = &e.kind else { continue };
|
||||
@@ -175,18 +192,47 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
|
||||
continue;
|
||||
}
|
||||
let block = d.block(&ins.block).map(|b| b.name.clone()).unwrap_or_else(|| ins.block.clone());
|
||||
// Charged from the lengths, before any text is copied.
|
||||
let selected_bytes: usize =
|
||||
ins.attribs.iter().filter(|a| selected.contains(&a.tag.to_ascii_uppercase())).map(|a| a.text.value.len() + a.tag.len()).sum();
|
||||
// The constants this row copies (those its own attributes do not override), from the lengths.
|
||||
let first_of_block = !const_sizes.contains_key(&block);
|
||||
let consts_sizes = const_sizes.entry(block.clone()).or_insert_with(|| {
|
||||
d.block(&ins.block).map_or_else(Vec::new, |b| {
|
||||
b.entities
|
||||
.iter()
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::AttDef(a) if a.constant && selected.iter().any(|t| t == &a.tag.to_ascii_uppercase()) => {
|
||||
Some((a.tag.to_ascii_uppercase(), a.tag.len() + a.text.value.len()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
});
|
||||
if first_of_block {
|
||||
// The per-block cache of constants is one more copy of them, made once.
|
||||
charge(&mut used, consts_sizes.iter().map(|(_, n)| *n).fold(0, usize::saturating_add), MAX_VALUE_BYTES)?;
|
||||
}
|
||||
let const_bytes: usize = consts_sizes
|
||||
.iter()
|
||||
.filter(|(tag, _)| !ins.attribs.iter().any(|a| a.tag.eq_ignore_ascii_case(tag)))
|
||||
.map(|(_, n)| *n)
|
||||
.fold(0, usize::saturating_add);
|
||||
charge(&mut used, selected_bytes.saturating_add(const_bytes), MAX_VALUE_BYTES)?;
|
||||
let values = values(
|
||||
ins,
|
||||
// Keyed by the resolved definition's exact name: `Part` and `PART` can both exist.
|
||||
consts.entry(block.clone()).or_insert_with(|| d.block(&ins.block).map(|b| constants(b, &selected)).unwrap_or_default()),
|
||||
&selected,
|
||||
);
|
||||
out.push(Found {
|
||||
handle: e.handle.hex(),
|
||||
layer: e.common.layer.clone(),
|
||||
at: ins.insert.xy(),
|
||||
rotation: ins.rotation.to_degrees(),
|
||||
block: block.clone(),
|
||||
values: values(
|
||||
ins,
|
||||
// Keyed by the resolved definition's exact name: `Part` and `PART` can both exist.
|
||||
consts.entry(block.clone()).or_insert_with(|| d.block(&ins.block).map(|b| constants(b, &selected)).unwrap_or_default()),
|
||||
&selected,
|
||||
),
|
||||
values,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
@@ -449,6 +495,15 @@ impl Interactive for ExtractM {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn attribute_text_is_charged_against_one_budget() {
|
||||
let mut used = 0;
|
||||
assert!(super::charge(&mut used, 60, 100).is_ok());
|
||||
assert!(super::charge(&mut used, 40, 100).is_ok());
|
||||
assert!(super::charge(&mut used, 1, 100).is_err());
|
||||
assert!(super::charge(&mut used, usize::MAX, 100).is_err());
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! of nested xrefs (`xref|nested|name`) bind with every `|` written `$0$`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityStore, MAX_BLOCK_DEPTH, XrefStatus};
|
||||
@@ -20,6 +20,16 @@ fn split(name: &str) -> Option<(&str, &str)> {
|
||||
|
||||
/// Whether table `sym` of `d` has a symbol called `name`.
|
||||
fn taken(d: &Drawing, sym: Sym, name: &str) -> bool {
|
||||
taken_with(d, sym, name, None)
|
||||
}
|
||||
|
||||
/// [`taken`], answering for blocks from `blocks` (the upper-cased names of `d`'s blocks, kept
|
||||
/// current by the caller) when given: a lookup that misses scans every block, which a bind
|
||||
/// naming thousands of blocks would repeat for each.
|
||||
fn taken_with(d: &Drawing, sym: Sym, name: &str, blocks: Option<&HashSet<String>>) -> bool {
|
||||
if let (Sym::Block, Some(set)) = (sym, blocks) {
|
||||
return set.contains(&name.to_ascii_uppercase());
|
||||
}
|
||||
match sym {
|
||||
Sym::Layer => d.layer(name).is_some(),
|
||||
Sym::Linetype => d.linetype(name).is_some(),
|
||||
@@ -32,12 +42,12 @@ fn taken(d: &Drawing, sym: Sym, name: &str) -> bool {
|
||||
}
|
||||
|
||||
/// The bound name of `key|rest`: `key$n$rest` with the first free `n`, or `rest` (Insert type).
|
||||
fn bound_name(d: &Drawing, sym: Sym, key: &str, rest: &str, insert: bool) -> String {
|
||||
fn bound_name(d: &Drawing, sym: Sym, key: &str, rest: &str, insert: bool, blocks: Option<&HashSet<String>>) -> String {
|
||||
let rest = rest.replace('|', "$0$");
|
||||
if insert {
|
||||
return rest;
|
||||
}
|
||||
(0..10_000).map(|n| format!("{key}${n}${rest}")).find(|c| !taken(d, sym, c)).unwrap_or_else(|| format!("{key}$${rest}"))
|
||||
(0..10_000).map(|n| format!("{key}${n}${rest}")).find(|c| !taken_with(d, sym, c, blocks)).unwrap_or_else(|| format!("{key}$${rest}"))
|
||||
}
|
||||
|
||||
/// Every table's symbol names, for one table at a time.
|
||||
@@ -53,9 +63,8 @@ fn names(d: &Drawing, sym: Sym) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename symbol `old` of table `sym` to `new`, or drop it (`new` already exists: Insert type).
|
||||
fn rename_symbol(d: &mut Drawing, sym: Sym, old: &str, new: &str) {
|
||||
let merge = !new.eq_ignore_ascii_case(old) && taken(d, sym, new);
|
||||
/// Rename symbol `old` of table `sym` to `new`, or drop it (`merge`: `new` already exists, Insert type).
|
||||
fn rename_symbol(d: &mut Drawing, sym: Sym, old: &str, new: &str, merge: bool) {
|
||||
macro_rules! table {
|
||||
($v:expr) => {{
|
||||
if merge {
|
||||
@@ -101,12 +110,17 @@ pub(crate) fn bind(d: &mut Drawing, key: &str, insert: bool) -> Result<(), Strin
|
||||
let mut map: HashMap<(Sym, String), String> = HashMap::new();
|
||||
let mut bound_blocks = vec![key.clone()];
|
||||
for sym in TABLES {
|
||||
let mut blocks: Option<HashSet<String>> = (sym == Sym::Block).then(|| d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect());
|
||||
for old in names(d, sym).into_iter().filter(|n| has_prefix(n, &prefix)) {
|
||||
let rest = old.get(prefix.len()..).unwrap_or_default();
|
||||
let new = bound_name(d, sym, &key, rest, insert);
|
||||
let new = bound_name(d, sym, &key, rest, insert, blocks.as_ref());
|
||||
// Insert type: a host symbol of the bound name is used instead of the xref's.
|
||||
let merged = taken(d, sym, &new);
|
||||
rename_symbol(d, sym, &old, &new);
|
||||
let merged = taken_with(d, sym, &new, blocks.as_ref());
|
||||
rename_symbol(d, sym, &old, &new, !new.eq_ignore_ascii_case(&old) && merged);
|
||||
if let Some(set) = &mut blocks {
|
||||
set.remove(&old.to_ascii_uppercase());
|
||||
set.insert(new.to_ascii_uppercase());
|
||||
}
|
||||
if sym == Sym::Block && !merged {
|
||||
bound_blocks.push(new.clone());
|
||||
}
|
||||
@@ -242,14 +256,27 @@ fn xbind_in(d: &mut Drawing, sym: Sym, name: &str, depth: usize) -> Result<Strin
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_block_name_set_answers_as_the_scan_does() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
for n in ["SITE$0$Plan", "Other"] {
|
||||
d.blocks.insert(n.into(), Arc::new(cadcraft_doc::Block::new(n)));
|
||||
}
|
||||
let set: HashSet<String> = d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect();
|
||||
for n in ["site$0$plan", "SITE$0$Plan", "OTHER", "SITE$1$Plan", "nothing"] {
|
||||
assert_eq!(taken_with(&d, Sym::Block, n, Some(&set)), taken(&d, Sym::Block, n), "{n}");
|
||||
}
|
||||
assert_eq!(bound_name(&d, Sym::Block, "SITE", "plan", false, Some(&set)), "SITE$1$plan");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_names() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", false), "SITE$0$Walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", false, None), "SITE$0$Walls");
|
||||
d.ensure_layer("SITE$0$Walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "walls", false), "SITE$1$walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", true), "Walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "TREES|Oak", false), "SITE$0$TREES$0$Oak");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "walls", false, None), "SITE$1$walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "Walls", true, None), "Walls");
|
||||
assert_eq!(bound_name(&d, Sym::Layer, "SITE", "TREES|Oak", false, None), "SITE$0$TREES$0$Oak");
|
||||
assert_eq!(split("A|b|c"), Some(("A", "b|c")));
|
||||
assert_eq!(split("|b"), None);
|
||||
}
|
||||
|
||||
@@ -497,6 +497,30 @@ fn frame_books(d: &Drawing, frame_height: f64, unit: f64) -> (Vec<Vec<Frame>>, u
|
||||
(books, skipped)
|
||||
}
|
||||
|
||||
/// Most page-by-entity renderings one PDFBOOK may ask for: every page draws the whole model through
|
||||
/// its window, so the work is the frames times the model's objects (1,000 sheets of a 500,000-object
|
||||
/// drawing).
|
||||
const MAX_BOOK_WORK: u64 = 500_000_000;
|
||||
|
||||
/// Objects one page draws: the model's objects, and the contents of each block reference (once per
|
||||
/// array cell, and the reference's own attributes; nested blocks count as their own entries).
|
||||
fn model_visits(d: &Drawing) -> usize {
|
||||
d.model
|
||||
.iter()
|
||||
.map(|e| match &e.kind {
|
||||
EntityKind::Insert(i) => {
|
||||
d.block(&i.block).map_or(1, |b| 1 + b.entities.len().saturating_mul((i.rows as usize).saturating_mul(i.cols as usize)))
|
||||
}
|
||||
_ => 1,
|
||||
})
|
||||
.fold(0usize, usize::saturating_add)
|
||||
}
|
||||
|
||||
/// Whether `frames` pages of a model with `entities` objects stay within [`MAX_BOOK_WORK`].
|
||||
fn book_work_fits(frames: usize, entities: usize) -> bool {
|
||||
(frames as u64).saturating_mul(entities as u64) <= MAX_BOOK_WORK
|
||||
}
|
||||
|
||||
fn run_pdfbook(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if io().and_then(|h| h.plot).is_none() {
|
||||
return Err(bad("pdfbook", "plotting is not available in this build"));
|
||||
@@ -507,6 +531,13 @@ fn run_pdfbook(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if books.is_empty() {
|
||||
return Err(bad("pdfbook", format!("no closed polylines {frame_height} mm high (sheet frames) in model space")));
|
||||
}
|
||||
let sheets: usize = books.iter().map(Vec::len).sum();
|
||||
if !book_work_fits(sheets, model_visits(s.doc()?)) {
|
||||
return Err(bad(
|
||||
"pdfbook",
|
||||
format!("{sheets} sheets of a model this large are too much work for one command; plot part of the drawing at a time"),
|
||||
));
|
||||
}
|
||||
let target = str_param(p, "path").map(str::to_string).or_else(|| s.state().ok().and_then(|st| st.path.clone()));
|
||||
let pb = target.as_deref().map(std::path::Path::new);
|
||||
let dir = pb.and_then(std::path::Path::parent).map(std::path::Path::to_path_buf).unwrap_or_default();
|
||||
@@ -561,6 +592,13 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_book_is_bounded_by_pages_times_objects() {
|
||||
assert!(book_work_fits(1_000, 500_000));
|
||||
assert!(!book_work_fits(100_000, 100_000));
|
||||
assert!(!book_work_fits(usize::MAX, usize::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_export_delivers_a_download_instead_of_writing_a_path() {
|
||||
deliver_file("export", "Drawing1.svg", b"<svg/>", Some(record)).unwrap();
|
||||
|
||||
@@ -7,16 +7,32 @@ use super::{
|
||||
// NCOPY
|
||||
// =====================================================================================
|
||||
|
||||
/// The leaf objects of a block reference in world coordinates (nested references expanded).
|
||||
fn nested_leaves(d: &cadcraft_doc::Drawing, ins: &cadcraft_doc::Insert, m: &Mat3, depth: usize, out: &mut Vec<(Common, EntityKind)>) {
|
||||
/// Block entities one NCOPY may visit in all, leaves or not: references to shared, empty or
|
||||
/// repeated sub-blocks multiply the visits (not the leaves) with each level of nesting.
|
||||
const MAX_VISITS: usize = 1_000_000;
|
||||
|
||||
/// The leaf objects of a block reference in world coordinates (nested references expanded). An
|
||||
/// error when the nesting needs more than [`MAX_VISITS`] visits.
|
||||
fn nested_leaves(
|
||||
d: &cadcraft_doc::Drawing,
|
||||
ins: &cadcraft_doc::Insert,
|
||||
m: &Mat3,
|
||||
depth: usize,
|
||||
visits: &mut usize,
|
||||
out: &mut Vec<(Common, EntityKind)>,
|
||||
) -> Result<()> {
|
||||
if depth >= MAX_BLOCK_DEPTH || out.len() >= MAX_GEN {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
let Some(b) = d.block(&ins.block) else { return };
|
||||
let Some(b) = d.block(&ins.block) else { return Ok(()) };
|
||||
let mm = m.then_before(ins.transform(b.base.xy()));
|
||||
for e in b.entities.iter() {
|
||||
*visits += 1;
|
||||
if *visits > MAX_VISITS {
|
||||
return Err(other("The block reference nests too many objects to copy."));
|
||||
}
|
||||
match &e.kind {
|
||||
EntityKind::Insert(inner) => nested_leaves(d, inner, &mm, depth + 1, out),
|
||||
EntityKind::Insert(inner) => nested_leaves(d, inner, &mm, depth + 1, visits, out)?,
|
||||
EntityKind::AttDef(_) => {}
|
||||
k => {
|
||||
let mut k = k.clone();
|
||||
@@ -25,13 +41,14 @@ fn nested_leaves(d: &cadcraft_doc::Drawing, ins: &cadcraft_doc::Insert, m: &Mat3
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn leaves_of(s: &Session, h: Handle) -> Result<Vec<(Common, EntityKind)>> {
|
||||
fn leaves_of(s: &Session, h: Handle, visits: &mut usize) -> Result<Vec<(Common, EntityKind)>> {
|
||||
let e = curves::entity(s, h)?;
|
||||
let EntityKind::Insert(ins) = &e.kind else { return Err(other("Select a block reference.")) };
|
||||
let mut out = Vec::new();
|
||||
nested_leaves(s.doc()?, ins, &Mat3::IDENTITY, 0, &mut out);
|
||||
nested_leaves(s.doc()?, ins, &Mat3::IDENTITY, 0, visits, &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -53,7 +70,7 @@ fn place_copies(s: &mut Session, items: &[(Common, EntityKind)], delta: Vec2) ->
|
||||
|
||||
pub(super) fn run_ncopy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("ncopy", "`handle` (block reference) is required"))?;
|
||||
let leaves = leaves_of(s, h)?;
|
||||
let leaves = leaves_of(s, h, &mut 0)?;
|
||||
let items: Vec<(Common, EntityKind)> = match point_param(p, "pick") {
|
||||
Some(q) => nearest_leaf(leaves, q).into_iter().collect(),
|
||||
None => leaves,
|
||||
@@ -74,6 +91,8 @@ pub(super) fn run_ncopy(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub(super) struct NcopyM {
|
||||
items: Vec<(Common, EntityKind)>,
|
||||
selecting_done: bool,
|
||||
/// Block entities visited by every selection so far: one allowance for the whole command.
|
||||
visits: usize,
|
||||
base: Option<Vec2>,
|
||||
}
|
||||
|
||||
@@ -97,7 +116,7 @@ impl Interactive for NcopyM {
|
||||
return match i {
|
||||
Input::Point(p) => {
|
||||
let h = curves::pick_at(s, p).ok_or_else(|| other("*Invalid selection*"))?;
|
||||
let leaf = nearest_leaf(leaves_of(s, h)?, p).ok_or_else(|| other("Nothing nested there."))?;
|
||||
let leaf = nearest_leaf(leaves_of(s, h, &mut self.visits)?, p).ok_or_else(|| other("Nothing nested there."))?;
|
||||
s.echo(format!("Nested object: {}", leaf.1.type_name()));
|
||||
if self.items.len() < MAX_GEN {
|
||||
self.items.push(leaf);
|
||||
|
||||
@@ -777,3 +777,25 @@ fn offset_elliptical_arc_follows_picked_side() {
|
||||
let outer = max_x(json!([20, 20]));
|
||||
assert!((outer - 11.0).abs() < 0.05, "outer side went inward: max x = {outer}");
|
||||
}
|
||||
|
||||
/// Nested references multiply visits even when no leaf results; NCOPY refuses past its visit budget.
|
||||
#[test]
|
||||
fn ncopy_charges_every_nested_visit() {
|
||||
use cadcraft_doc::{Block, Common, Entity, Insert};
|
||||
let mut s = Session::new();
|
||||
let d = s.doc_mut().unwrap();
|
||||
let mut n = 1;
|
||||
for level in 0..15 {
|
||||
let mut b = Block::new(&format!("B{level}"));
|
||||
for _ in 0..100 {
|
||||
let ins = Insert { block: format!("B{}", level + 1), ..Insert::default() };
|
||||
b.entities.push(Entity::new(Handle(n), EntityKind::Insert(ins)));
|
||||
n += 1;
|
||||
}
|
||||
d.blocks.insert(b.name.clone(), std::sync::Arc::new(b));
|
||||
}
|
||||
d.blocks.insert("B15".into(), std::sync::Arc::new(Block::new("B15")));
|
||||
let h = d.add(&Space::Model, Common::default(), EntityKind::Insert(Insert { block: "B0".into(), ..Insert::default() })).unwrap();
|
||||
let err = s.execute("ncopy", &json!({ "handle": h.hex() })).unwrap_err().to_string();
|
||||
assert!(err.contains("nests too many objects"), "{err}");
|
||||
}
|
||||
|
||||
@@ -697,7 +697,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub(crate) use drawing::from_json as drawing_from_json;
|
||||
pub(crate) use drawing::load_source;
|
||||
pub(crate) use drawing::{load_source, support_file};
|
||||
|
||||
impl Session {
|
||||
/// A file APPLOAD loaded, by its name (with or without `.lsp`, any case).
|
||||
|
||||
@@ -235,7 +235,8 @@ pub(super) fn autoload(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispErr
|
||||
return Err(LispError::bad_arg("command name", &cmd));
|
||||
}
|
||||
// The stub forgets itself, loads the file and calls what it defined.
|
||||
let file_lit = to_text(&Value::Str(file.clone()), true);
|
||||
// A file in the support folder is loaded from there, never from a same-named file of the working folder.
|
||||
let file_lit = to_text(&Value::Str(crate::lisp::support_file(ctx.s, &file).map_or_else(|| file.clone(), |p| p.into())), true);
|
||||
let src = format!(
|
||||
"(lambda () (setq C:{name} nil) (load {file_lit}) (if C:{name} (C:{name}) (princ \"\\n; error: {} did not define {name}\")) (princ))",
|
||||
file.replace(['"', '\\'], "/")
|
||||
|
||||
@@ -1128,7 +1128,9 @@ pub fn trans(_ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
|
||||
/// What `(load name)` reads: a file loaded through APPLOAD (the web), or one on disk, with
|
||||
/// `.lsp` added when the name has no extension.
|
||||
pub fn load_source(s: &Session, name: &str) -> Option<String> {
|
||||
if let Some(t) = s.lisp_file(name) {
|
||||
// An absolute path names a file, not an APPLOAD entry that happens to share its base name.
|
||||
let absolute = std::path::Path::new(name).is_absolute();
|
||||
if !absolute && let Some(t) = s.lisp_file(name) {
|
||||
return Some(t);
|
||||
}
|
||||
let candidates =
|
||||
@@ -1139,10 +1141,37 @@ pub fn load_source(s: &Session, name: &str) -> Option<String> {
|
||||
candidates.iter().chain(&support).find_map(super::builtins::read_text_file)
|
||||
}
|
||||
|
||||
/// The file `name` names in the support folder, when it is there: what an autoload registration
|
||||
/// loads, so a same-named file in the working folder cannot take its place. `None` for an absolute
|
||||
/// path, no support folder or no such file there (the general search then applies, as before).
|
||||
pub fn support_file(s: &Session, name: &str) -> Option<String> {
|
||||
if std::path::Path::new(name).is_absolute() {
|
||||
return None;
|
||||
}
|
||||
let dir = s.lisp_support_dir()?;
|
||||
let candidates =
|
||||
if std::path::Path::new(name).extension().is_none() { vec![format!("{name}.lsp"), name.to_string()] } else { vec![name.to_string()] };
|
||||
candidates.iter().map(|c| dir.join(c)).find(|p| p.is_file()).map(|p| std::path::absolute(&p).unwrap_or(p).to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod budget_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn autoload_prefers_the_support_folder_file() {
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-lisp-support-file-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("lazy.lsp"), "").unwrap();
|
||||
let mut s = Session::new();
|
||||
assert_eq!(support_file(&s, "lazy"), None);
|
||||
s.set_lisp_support_dir(Some(dir.clone()));
|
||||
assert_eq!(support_file(&s, "lazy"), Some(dir.join("lazy.lsp").to_string_lossy().into_owned()));
|
||||
assert_eq!(support_file(&s, "other"), None);
|
||||
assert_eq!(support_file(&s, dir.join("lazy.lsp").to_str().unwrap()), None);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// One 64 KB string occurring many times is charged by its length each time.
|
||||
#[test]
|
||||
fn shared_strings_spend_the_conversion_budgets() {
|
||||
|
||||
@@ -12,17 +12,16 @@ pub(crate) fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> {
|
||||
};
|
||||
let lw = match e.common.lineweight {
|
||||
Lineweight::ByBlock => ctx.block_lw,
|
||||
Lineweight::ByLayer => ctx.layer(layer.as_deref().unwrap_or("0")).map(|l| l.lineweight).unwrap_or(Lineweight::Default),
|
||||
Lineweight::ByLayer => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.lineweight).unwrap_or(Lineweight::Default),
|
||||
x => x,
|
||||
};
|
||||
let lt = match e.common.linetype.to_ascii_lowercase().as_str() {
|
||||
"byblock" => ctx.block_ltype.clone(),
|
||||
"bylayer" => ctx.layer(layer.as_deref().unwrap_or("0")).map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
|
||||
"bylayer" => ctx.d.layer(layer.as_deref().unwrap_or("0")).map(|l| l.linetype.clone()).unwrap_or_else(|| "Continuous".into()),
|
||||
_ => e.common.linetype.clone(),
|
||||
};
|
||||
Ctx {
|
||||
d: ctx.d,
|
||||
layers: ctx.layers,
|
||||
xf: ctx.xf.then_before(m),
|
||||
block_color: color,
|
||||
block_layer: layer,
|
||||
|
||||
@@ -256,16 +256,8 @@ pub struct PolyClip {
|
||||
band_h: f64,
|
||||
/// The region as triangles (for clipping fills), with each triangle's bounds.
|
||||
tris: Vec<([Vec2; 3], Bounds2)>,
|
||||
/// Indices into `tris` of the triangles whose y range meets each band (at most
|
||||
/// [`TRI_BAND_SPAN`] bands of it), so a fill triangle meets only the clip triangles near it.
|
||||
tri_bands: Vec<Vec<u32>>,
|
||||
/// Indices of the triangles spanning more bands than that: tested against every fill triangle.
|
||||
tall_tris: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Most bands one clip triangle is registered in; a taller one goes to [`PolyClip::tall_tris`].
|
||||
const TRI_BAND_SPAN: usize = 8;
|
||||
|
||||
impl PolyClip {
|
||||
/// A clip region for `poly` (see [`clean_polygon`]); `None` for a degenerate outline.
|
||||
pub fn new(poly: &[Vec2]) -> Option<PolyClip> {
|
||||
@@ -274,15 +266,7 @@ impl PolyClip {
|
||||
let band_h = bounds.height() / BANDS as f64;
|
||||
let mut bands: Vec<Vec<u32>> = vec![Vec::new(); BANDS];
|
||||
let n = poly.len();
|
||||
let mut clip = PolyClip {
|
||||
poly: Vec::new(),
|
||||
bounds,
|
||||
bands: Vec::new(),
|
||||
band_h,
|
||||
tris: Vec::new(),
|
||||
tri_bands: vec![Vec::new(); BANDS],
|
||||
tall_tris: Vec::new(),
|
||||
};
|
||||
let mut clip = PolyClip { poly: Vec::new(), bounds, bands: Vec::new(), band_h, tris: Vec::new() };
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (poly.get(i), poly.get((i + 1) % n)) else { continue };
|
||||
let (lo, hi) = (clip.band(a.y.min(b.y)), clip.band(a.y.max(b.y)));
|
||||
@@ -298,17 +282,6 @@ impl PolyClip {
|
||||
.map(|t| (*t, Bounds2::from_points(t.iter().copied())))
|
||||
.filter(|(_, b)| b.width() > 0.0 && b.height() > 0.0)
|
||||
.collect();
|
||||
for k in 0..clip.tris.len() {
|
||||
let Some((_, tb)) = clip.tris.get(k) else { continue };
|
||||
let (lo, hi) = (clip.band(tb.min.y), clip.band(tb.max.y));
|
||||
if hi - lo >= TRI_BAND_SPAN {
|
||||
clip.tall_tris.push(k as u32);
|
||||
} else {
|
||||
for band in clip.tri_bands.iter_mut().take(hi + 1).skip(lo) {
|
||||
band.push(k as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
clip.poly = poly;
|
||||
clip.bands = bands;
|
||||
Some(clip)
|
||||
@@ -431,15 +404,7 @@ impl PolyClip {
|
||||
out.extend_from_slice(t);
|
||||
continue;
|
||||
}
|
||||
// The clip triangles near `t`, in clip order (the same output as a scan of all of them).
|
||||
let (lo, hi) = (self.band(tb.min.y), self.band(tb.max.y));
|
||||
let mut near: Vec<u32> = self.tall_tris.clone();
|
||||
for band in self.tri_bands.iter().take(hi + 1).skip(lo) {
|
||||
near.extend_from_slice(band);
|
||||
}
|
||||
near.sort_unstable();
|
||||
near.dedup();
|
||||
for (c, cb) in near.iter().filter_map(|&k| self.tris.get(k as usize)) {
|
||||
for (c, cb) in &self.tris {
|
||||
if !cb.intersects(&tb) {
|
||||
continue;
|
||||
}
|
||||
@@ -625,39 +590,6 @@ mod tests {
|
||||
assert!(t.iter().all(|p| !(p.x > 5.0 + 1e-9 && p.y > 5.0 + 1e-9)));
|
||||
}
|
||||
|
||||
/// A fill clipped through the band index equals the same fill against a plain scan of every clip
|
||||
/// triangle (the unindexed result), for a boundary with tall and short triangles.
|
||||
#[test]
|
||||
fn indexed_triangle_clipping_equals_a_full_scan() {
|
||||
// A comb: tall teeth over a base, so its triangles span many bands.
|
||||
let mut poly = vec![Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0)];
|
||||
for i in (0..20).rev() {
|
||||
let x = i as f64 * 2.0;
|
||||
poly.extend([
|
||||
Vec2::new(x + 2.0, 20.0),
|
||||
Vec2::new(x + 1.0, 20.0),
|
||||
Vec2::new(x + 1.0, 2.0),
|
||||
Vec2::new(x + 0.5, 2.0),
|
||||
Vec2::new(x + 0.5, 20.0),
|
||||
Vec2::new(x, 20.0),
|
||||
]);
|
||||
}
|
||||
let c = PolyClip::new(&poly).unwrap();
|
||||
assert!(!c.tall_tris.is_empty());
|
||||
let fill: Vec<Vec2> = (0..30)
|
||||
.flat_map(|i| {
|
||||
let (x, y) = (i as f64 * 1.3, (i % 7) as f64 * 2.5);
|
||||
[Vec2::new(x, y), Vec2::new(x + 6.0, y), Vec2::new(x + 2.0, y + 9.0)]
|
||||
})
|
||||
.collect();
|
||||
let got = c.clip_triangles(&fill);
|
||||
let mut scan = c.clone();
|
||||
scan.tri_bands = vec![(0..scan.tris.len() as u32).collect(); BANDS];
|
||||
scan.tall_tris.clear();
|
||||
assert_eq!(got, scan.clip_triangles(&fill));
|
||||
assert!(!got.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_and_hostile_outlines() {
|
||||
assert!(PolyClip::new(&[Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)]).is_none());
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn dimension(
|
||||
let dim_rgb = ink(style.dim_line_color, layer_color, own);
|
||||
let ext_rgb = ink(style.ext_line_color, layer_color, own);
|
||||
let txt_rgb = ink(style.text_color, layer_color, own);
|
||||
let layer = ctx.layer(layer_name);
|
||||
let layer = d.layer(layer_name);
|
||||
let weight = |v: i16| -> f32 {
|
||||
if !b.opts.lineweights {
|
||||
return 0.0;
|
||||
|
||||
+15
-118
@@ -1,109 +1,40 @@
|
||||
//! Polygon filling: even-odd regions to triangles (slab decomposition), wide polylines.
|
||||
|
||||
use cadcraft_doc::LwPolyline;
|
||||
use cadcraft_geom::{Bounds2, Polyline, Vec2};
|
||||
use cadcraft_geom::{Polyline, Vec2};
|
||||
|
||||
/// Triangulate an even-odd region bounded by closed loops (holes allowed) by cutting it into
|
||||
/// horizontal slabs at every vertex y and emitting trapezoids. Two edges that cross inside a slab
|
||||
/// (a bowtie, two loops crossing each other) swap order there, so such a slab is cut again at the
|
||||
/// crossing and each part is paired on its own. Robust for any input; output size is
|
||||
/// O(slabs × crossings), capped for hostile inputs. Groups of loops whose bounding boxes are
|
||||
/// separated (by a vertical or a horizontal gap, recursively) cannot affect each other's even-odd
|
||||
/// parity, so each group is cut on its own: disjoint outlines do not multiply each other's slabs.
|
||||
/// O(slabs × crossings), capped for hostile inputs.
|
||||
pub fn triangulate_evenodd(loops: &[Vec<Vec2>]) -> Vec<Vec2> {
|
||||
// (original index, bounds, edges) per loop.
|
||||
let mut items: Vec<(usize, Bounds2, Vec<(Vec2, Vec2)>)> = Vec::new();
|
||||
let mut total = 0usize;
|
||||
for (k, l) in loops.iter().enumerate() {
|
||||
let mut edges: Vec<(Vec2, Vec2)> = Vec::new();
|
||||
for l in loops {
|
||||
let n = l.len();
|
||||
if n < 3 {
|
||||
continue;
|
||||
}
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..n {
|
||||
let (Some(&a), Some(&b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
|
||||
if (a.y - b.y).abs() > 1e-15 && a.is_finite() && b.is_finite() {
|
||||
edges.push(if a.y < b.y { (a, b) } else { (b, a) });
|
||||
}
|
||||
}
|
||||
total += edges.len();
|
||||
if total > 200_000 {
|
||||
return Vec::new();
|
||||
}
|
||||
if !edges.is_empty() {
|
||||
let bounds = Bounds2::from_points(edges.iter().flat_map(|(a, b)| [*a, *b]));
|
||||
items.push((k, bounds, edges));
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
// Slabs created by cutting at crossings, over the whole region; past this the remaining
|
||||
// crossings are left unsplit rather than letting a hostile input run away.
|
||||
let mut cuts_left = MAX_CROSSING_CUTS;
|
||||
// Loop visits all the partitioning may make: an input that peels one loop off a big group at a
|
||||
// time would rescan the rest each time, so past this a group is cut as it stands (as before partitioning existed).
|
||||
let mut split_work = 64 * items.len() + 4_000_000;
|
||||
let mut stack = vec![items];
|
||||
while let Some(mut group) = stack.pop() {
|
||||
if group.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let parts = match split_work.checked_sub(2 * group.len()) {
|
||||
Some(rest) => {
|
||||
split_work = rest;
|
||||
let parts = split_groups(&mut group, true);
|
||||
if parts.len() > 1 { parts } else { split_groups(&mut group, false) }
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
if parts.len() > 1 {
|
||||
// Lowest-first when popped.
|
||||
stack.extend(parts.into_iter().rev());
|
||||
continue;
|
||||
}
|
||||
group.sort_by_key(|g| g.0);
|
||||
let edges: Vec<(Vec2, Vec2)> = group.into_iter().flat_map(|g| g.2).collect();
|
||||
if slabs(edges, &mut out, &mut cuts_left) {
|
||||
break;
|
||||
}
|
||||
if edges.is_empty() || edges.len() > 200_000 {
|
||||
return Vec::new();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
type Item = (usize, Bounds2, Vec<(Vec2, Vec2)>);
|
||||
|
||||
/// `group` cut into the chains of loops whose extents along x (or y) overlap, in order along it;
|
||||
/// one chain when nothing separates them (then `group` is left as it was, but for its order).
|
||||
fn split_groups(group: &mut Vec<Item>, along_x: bool) -> Vec<Vec<Item>> {
|
||||
let span = |b: &Bounds2| if along_x { (b.min.x, b.max.x) } else { (b.min.y, b.max.y) };
|
||||
group.sort_by(|a, b| span(&a.1).0.total_cmp(&span(&b.1).0));
|
||||
let mut parts: Vec<Vec<Item>> = Vec::new();
|
||||
let mut reach = f64::NEG_INFINITY;
|
||||
for item in std::mem::take(group) {
|
||||
match parts.last_mut() {
|
||||
Some(part) if span(&item.1).0 <= reach => {
|
||||
reach = reach.max(span(&item.1).1);
|
||||
part.push(item);
|
||||
}
|
||||
_ => {
|
||||
reach = span(&item.1).1;
|
||||
parts.push(vec![item]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if parts.len() == 1 {
|
||||
*group = parts.pop().unwrap_or_default();
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
/// The slab decomposition of `edges` appended to `out`; true once `out` passes its cap.
|
||||
fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usize) -> bool {
|
||||
let mut ys: Vec<f64> = edges.iter().flat_map(|(a, b)| [a.y, b.y]).collect();
|
||||
ys.sort_by(f64::total_cmp);
|
||||
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
|
||||
edges.sort_by(|a, b| a.0.y.total_cmp(&b.0.y));
|
||||
let mut out = Vec::new();
|
||||
let mut active: Vec<(Vec2, Vec2)> = Vec::new();
|
||||
let mut next = 0usize;
|
||||
// Slabs created by cutting at crossings, over the whole region; past this the remaining
|
||||
// crossings are left unsplit rather than letting a hostile input run away.
|
||||
let mut cuts_left = MAX_CROSSING_CUTS;
|
||||
let mut pending: Vec<(f64, f64)> = Vec::new();
|
||||
for w in ys.windows(2) {
|
||||
let (Some(&y0), Some(&y1)) = (w.first(), w.get(1)) else { continue };
|
||||
@@ -123,7 +54,7 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
|
||||
let ymid = (ya + yb) / 2.0;
|
||||
let mut xs: Vec<(f64, f64, f64)> = active.iter().map(|e| (x_at(e, ymid), x_at(e, ya), x_at(e, yb))).collect();
|
||||
xs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
if *cuts_left > 0 {
|
||||
if cuts_left > 0 {
|
||||
let mut cuts: Vec<f64> = Vec::new();
|
||||
for pair in xs.windows(2) {
|
||||
let (Some(l), Some(r)) = (pair.first(), pair.get(1)) else { continue };
|
||||
@@ -134,8 +65,8 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
|
||||
if !cuts.is_empty() {
|
||||
cuts.sort_by(f64::total_cmp);
|
||||
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
|
||||
cuts.truncate(*cuts_left);
|
||||
*cuts_left -= cuts.len();
|
||||
cuts.truncate(cuts_left);
|
||||
cuts_left -= cuts.len();
|
||||
// Push the parts top-down so the lowest is processed first.
|
||||
let mut top = yb;
|
||||
for &c in cuts.iter().rev() {
|
||||
@@ -153,11 +84,11 @@ fn slabs(mut edges: Vec<(Vec2, Vec2)>, out: &mut Vec<Vec2>, cuts_left: &mut usiz
|
||||
}
|
||||
}
|
||||
if out.len() > 6_000_000 {
|
||||
return true;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
out
|
||||
}
|
||||
|
||||
/// How many extra slab boundaries crossings may add over one region.
|
||||
@@ -233,40 +164,6 @@ mod tests {
|
||||
assert!((area(&t) - 96.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A thousand disjoint squares side by side (every slab of one would otherwise be cut at the
|
||||
/// vertex heights of all the others) triangulate in linear output.
|
||||
#[test]
|
||||
fn disjoint_outlines_do_not_multiply_each_others_slabs() {
|
||||
let n = 1_000;
|
||||
let loops: Vec<Vec<Vec2>> = (0..n)
|
||||
.map(|i| {
|
||||
let (x, jitter) = (i as f64 * 3.0, i as f64 * 0.001);
|
||||
vec![Vec2::new(x, jitter), Vec2::new(x + 2.0, jitter), Vec2::new(x + 2.0, 2.0 + jitter), Vec2::new(x, 2.0 + jitter)]
|
||||
})
|
||||
.collect();
|
||||
let t = triangulate_evenodd(&loops);
|
||||
assert!((area(&t) - 4.0 * n as f64).abs() < 1e-6);
|
||||
// Two triangles per square: no slab was cut at another square's height.
|
||||
assert_eq!(t.len(), 6 * n);
|
||||
}
|
||||
|
||||
/// Rectangles that peel off one at a time along alternating axes (each split isolates a single loop
|
||||
/// from a large group) still triangulate correctly, with the partition work bounded.
|
||||
#[test]
|
||||
fn alternating_peeling_is_bounded_and_correct() {
|
||||
let m = 1_000;
|
||||
let (mf, mut loops) = (m as f64, Vec::new());
|
||||
let rect = |x0: f64, x1: f64, y0: f64, y1: f64| vec![Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)];
|
||||
for i in 0..m {
|
||||
let i = i as f64;
|
||||
loops.push(rect(2.0 * i, 2.0 * i + 1.0, 2.0 * i, 2.0 * mf));
|
||||
loops.push(rect(2.0 * i + 2.0, 2.0 * mf + 1.0, 2.0 * i, 2.0 * i + 1.0));
|
||||
}
|
||||
let expected: f64 = (0..m).map(|i| (2.0 * mf - 2.0 * i as f64) + (2.0 * mf - 2.0 * i as f64 - 1.0)).sum();
|
||||
let t = triangulate_evenodd(&loops);
|
||||
assert!((area(&t) - expected).abs() < 1e-6 * expected, "{} vs {expected}", area(&t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangle_area() {
|
||||
let t = triangulate_evenodd(&[vec![Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(0.0, 3.0)]]);
|
||||
|
||||
@@ -78,32 +78,10 @@ fn max_scale(m: &Mat3) -> f64 {
|
||||
if s.is_finite() { s } else { 0.0 }
|
||||
}
|
||||
|
||||
/// A drawing's layers by lower-cased name (the first of a duplicated name wins, as the table's own
|
||||
/// search does), built once per display list: a lookup per object over the layer vector multiplies
|
||||
/// the object count by the layer count.
|
||||
struct LayerIndex(Option<std::collections::HashMap<String, usize>>);
|
||||
|
||||
impl LayerIndex {
|
||||
fn new(d: &Drawing) -> Self {
|
||||
let mut m = std::collections::HashMap::with_capacity(d.layers.len());
|
||||
for (i, l) in d.layers.iter().enumerate() {
|
||||
m.entry(l.name.to_ascii_lowercase()).or_insert(i);
|
||||
}
|
||||
LayerIndex(Some(m))
|
||||
}
|
||||
|
||||
/// No index: lookups scan the layer vector, as `Drawing::layer` does. For a few objects (a
|
||||
/// rubber band redrawn at every pointer move), where building an index would cost more.
|
||||
fn linear() -> Self {
|
||||
LayerIndex(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Graphics state inherited through block references.
|
||||
#[derive(Clone)]
|
||||
struct Ctx<'a> {
|
||||
d: &'a Drawing,
|
||||
layers: &'a LayerIndex,
|
||||
xf: Mat3,
|
||||
block_color: Color,
|
||||
block_layer: Option<String>,
|
||||
@@ -125,19 +103,12 @@ struct Ctx<'a> {
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
/// The layer called `name` (any case), as [`Drawing::layer`] finds it.
|
||||
fn layer(&self, name: &str) -> Option<&cadcraft_doc::Layer> {
|
||||
let Some(map) = &self.layers.0 else { return self.d.layer(name) };
|
||||
let i = if name.bytes().any(|b| b.is_ascii_uppercase()) { map.get(&name.to_ascii_lowercase()) } else { map.get(name) };
|
||||
i.and_then(|i| self.d.layers.get(*i))
|
||||
}
|
||||
|
||||
/// A layer's colour, honouring the viewport's override.
|
||||
fn layer_color(&self, name: &str) -> Option<Color> {
|
||||
if let Some((_, c)) = self.vp_colors.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)) {
|
||||
return Some(*c);
|
||||
}
|
||||
self.layer(name).map(|l| l.color)
|
||||
self.d.layer(name).map(|l| l.color)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +125,6 @@ struct Builder<'a> {
|
||||
shared_budget: Option<&'a std::sync::atomic::AtomicUsize>,
|
||||
/// Transparency (percent) of the entity being drawn.
|
||||
transparency: u8,
|
||||
/// The drawing's layers by name.
|
||||
layers: &'a LayerIndex,
|
||||
}
|
||||
|
||||
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
|
||||
@@ -371,7 +340,6 @@ impl<'a> Builder<'a> {
|
||||
named: self.named,
|
||||
shared_budget: self.shared_budget,
|
||||
transparency: self.transparency,
|
||||
layers: self.layers,
|
||||
}
|
||||
}
|
||||
/// Take one unit of the block-expansion budget; false once it is spent.
|
||||
|
||||
@@ -3,9 +3,7 @@ use super::display::append;
|
||||
use super::display::{DisplayList, Kind, NO_LAYER, PlotText, VIEWPORT_CONTENT};
|
||||
use super::draw::entity;
|
||||
use super::viewports::{MAX_VIEWPORTS, viewport};
|
||||
use super::{
|
||||
Bounds2, Builder, Color, Ctx, Drawing, Entity, EntityKind, Handle, LayerIndex, Lineweight, Mat3, Space, Vec2, paper, pen, plotstyle, point,
|
||||
};
|
||||
use super::{Bounds2, Builder, Color, Ctx, Drawing, Entity, EntityKind, Handle, Lineweight, Mat3, Space, Vec2, paper, pen, plotstyle, point};
|
||||
|
||||
/// Build options.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -82,17 +80,9 @@ pub fn build_plot(d: &Drawing, space: &Space, opts: &Options) -> DisplayList {
|
||||
build_space(d, space, opts, true)
|
||||
}
|
||||
|
||||
pub(crate) fn top_ctx<'a>(
|
||||
d: &'a Drawing,
|
||||
layers: &'a LayerIndex,
|
||||
xf: Mat3,
|
||||
top: Handle,
|
||||
frozen: &'a [String],
|
||||
vp_colors: &'a [(String, Color)],
|
||||
) -> Ctx<'a> {
|
||||
pub(crate) fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_colors: &'a [(String, Color)]) -> Ctx<'a> {
|
||||
Ctx {
|
||||
d,
|
||||
layers,
|
||||
xf,
|
||||
block_color: Color::Index(7),
|
||||
block_layer: None,
|
||||
@@ -111,17 +101,7 @@ pub(crate) fn top_ctx<'a>(
|
||||
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
|
||||
let opts = &with_point_view(d, space, opts);
|
||||
let styler = style_table(d, space, opts, plotting).map(|t| plotstyle::Styler::new(d, t));
|
||||
let layers = LayerIndex::new(d);
|
||||
let mut b = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts,
|
||||
plotting,
|
||||
styles: styler.as_ref(),
|
||||
named: None,
|
||||
shared_budget: None,
|
||||
transparency: 0,
|
||||
layers: &layers,
|
||||
};
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
|
||||
if let Space::Paper(name) = space {
|
||||
b.list.sheet = paper::sheet(d, name);
|
||||
}
|
||||
@@ -144,7 +124,7 @@ fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> Di
|
||||
}
|
||||
continue;
|
||||
}
|
||||
entity(&mut b, &top_ctx(d, &layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
}
|
||||
// A layout is drawn in paper units, so its pens become geometry now.
|
||||
@@ -168,22 +148,14 @@ fn build_parallel(b: &mut Builder, d: &Drawing, store: &cadcraft_doc::EntityStor
|
||||
use rayon::prelude::*;
|
||||
let ents: Vec<&std::sync::Arc<Entity>> = store.iter().collect();
|
||||
let budget = std::sync::atomic::AtomicUsize::new(0);
|
||||
let (opts, plotting, layers) = (b.opts, b.plotting, b.layers);
|
||||
let (opts, plotting) = (b.opts, b.plotting);
|
||||
let parts: Vec<DisplayList> = ents
|
||||
.par_chunks(PARALLEL_CHUNK)
|
||||
.map(|chunk| {
|
||||
let mut part = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts,
|
||||
plotting,
|
||||
styles: None,
|
||||
named: None,
|
||||
shared_budget: Some(&budget),
|
||||
transparency: 0,
|
||||
layers,
|
||||
};
|
||||
let mut part =
|
||||
Builder { list: DisplayList::default(), opts, plotting, styles: None, named: None, shared_budget: Some(&budget), transparency: 0 };
|
||||
for e in chunk {
|
||||
entity(&mut part, &top_ctx(d, layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
entity(&mut part, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
part.list
|
||||
})
|
||||
@@ -222,10 +194,6 @@ pub(crate) fn with_point_view(d: &Drawing, space: &Space, opts: &Options) -> Opt
|
||||
o
|
||||
}
|
||||
|
||||
/// Loose entity lists shorter than this, with no block reference (whose contents expand to many
|
||||
/// objects), look layers up by scanning rather than building an index.
|
||||
const SMALL_LIST: usize = 64;
|
||||
|
||||
/// Build the display list for a set of loose entities (previews, rubber bands).
|
||||
pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents: I, opts: &Options) -> DisplayList {
|
||||
let ents: Vec<&Entity> = ents.into_iter().collect();
|
||||
@@ -237,23 +205,9 @@ pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents:
|
||||
} else {
|
||||
opts
|
||||
};
|
||||
let layers = if ents.len() < SMALL_LIST && !ents.iter().any(|e| matches!(e.kind, EntityKind::Insert(_))) {
|
||||
LayerIndex::linear()
|
||||
} else {
|
||||
LayerIndex::new(d)
|
||||
};
|
||||
let mut b = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts,
|
||||
plotting: false,
|
||||
styles: None,
|
||||
named: None,
|
||||
shared_budget: None,
|
||||
transparency: 0,
|
||||
layers: &layers,
|
||||
};
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None, shared_budget: None, transparency: 0 };
|
||||
for e in ents {
|
||||
entity(&mut b, &top_ctx(d, &layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
b.list
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Styler {
|
||||
let normal = || by_name(cadcraft_doc::NORMAL_STYLE);
|
||||
let p = c.plot_style.trim();
|
||||
if p.is_empty() || p.eq_ignore_ascii_case("bylayer") {
|
||||
ctx.layer(layer_name).and_then(|l| by_name(&l.plot_style)).or_else(normal)
|
||||
ctx.d.layer(layer_name).and_then(|l| by_name(&l.plot_style)).or_else(normal)
|
||||
} else if p.eq_ignore_ascii_case("byblock") {
|
||||
ctx.block_pstyle.or_else(normal)
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@ pub(crate) fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool
|
||||
let d = ctx.d;
|
||||
// Layer "0" inside a block takes the insert's layer.
|
||||
let layer_name = if c.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { c.layer.as_str() };
|
||||
let layer = ctx.layer(layer_name);
|
||||
let layer = d.layer(layer_name);
|
||||
let visible =
|
||||
c.visible && layer.is_none_or(|l| l.visible() && (!plotting || l.plot)) && !ctx.frozen.iter().any(|f| f.eq_ignore_ascii_case(layer_name));
|
||||
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
|
||||
|
||||
@@ -1217,20 +1217,3 @@ fn rotated_and_reversed_copies_of_a_curved_loop_fill_once() {
|
||||
});
|
||||
assert_eq!(loops_of(&h).len(), 2);
|
||||
}
|
||||
|
||||
/// The per-list layer index finds what `Drawing::layer` finds: any case, the first of a duplicate.
|
||||
#[test]
|
||||
fn the_layer_index_agrees_with_the_layer_table() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut first = cadcraft_doc::Layer::new("Walls");
|
||||
first.color = Color::Index(1);
|
||||
let mut dup = cadcraft_doc::Layer::new("WALLS");
|
||||
dup.color = Color::Index(2);
|
||||
d.layers.extend([first, dup, cadcraft_doc::Layer::new("Doors")]);
|
||||
for index in [LayerIndex::new(&d), LayerIndex::linear()] {
|
||||
let top = crate::pipeline::top_ctx(&d, &index, cadcraft_geom::Mat3::IDENTITY, cadcraft_doc::Handle(1), &[], &[]);
|
||||
for name in ["Walls", "walls", "WALLS", "doors", "0", "Missing"] {
|
||||
assert_eq!(top.layer(name).map(|l| (&l.name, l.color)), d.layer(name).map(|l| (&l.name, l.color)), "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ use super::draw::entity;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use super::pipeline::PARALLEL_CHUNK;
|
||||
use super::pipeline::{Options, style_table, top_ctx, with_point_view};
|
||||
use super::{
|
||||
Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, LayerIndex, MAX_BLOCK_EXPANSION, Mat3, Pen, Space, Vec2, clip, paper, pen, plotstyle,
|
||||
};
|
||||
use super::{Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, MAX_BLOCK_EXPANSION, Mat3, Pen, Space, Vec2, clip, paper, pen, plotstyle};
|
||||
|
||||
/// Upper bound on viewports drawn per layout (hostile files).
|
||||
pub(crate) const MAX_VIEWPORTS: usize = 256;
|
||||
@@ -23,7 +21,7 @@ pub(crate) fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_d
|
||||
let poly = viewport_clip(d, e.handle, vp);
|
||||
// Border (on the viewport's layer; layer off hides only the border).
|
||||
if poly.is_none() {
|
||||
entity(b, &top_ctx(d, b.layers, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
entity(b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
if b.opts.skip_viewport == Some(e.handle) {
|
||||
return;
|
||||
@@ -69,17 +67,8 @@ pub fn viewport_content(d: &Drawing, space: &Space, handle: Handle, area: Bounds
|
||||
}
|
||||
let opts = &with_point_view(d, space, opts);
|
||||
let styler = style_table(d, space, opts, false).map(|t| plotstyle::Styler::new(d, t));
|
||||
let layers = LayerIndex::new(d);
|
||||
let mut b = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts,
|
||||
plotting: false,
|
||||
styles: styler.as_ref(),
|
||||
named: None,
|
||||
shared_budget: None,
|
||||
transparency: 0,
|
||||
layers: &layers,
|
||||
};
|
||||
let mut b =
|
||||
Builder { list: DisplayList::default(), opts, plotting: false, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
|
||||
contents(&mut b, d, vp, area, &Clip::Rect(area), paper::unit_mm_of(d, handle));
|
||||
b.list
|
||||
}
|
||||
@@ -108,8 +97,7 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
|
||||
// paper space, whatever the viewport scale.
|
||||
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
|
||||
let layers = b.layers;
|
||||
let ctx = || Ctx { lt_factor, ..top_ctx(d, layers, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) };
|
||||
let ctx = || Ctx { lt_factor, ..top_ctx(d, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) };
|
||||
let seen = |me: &Entity| {
|
||||
if matches!(me.kind, EntityKind::Viewport(_)) {
|
||||
return false;
|
||||
@@ -132,7 +120,6 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
named: None,
|
||||
shared_budget: Some(&budget),
|
||||
transparency: 0,
|
||||
layers,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let parallel = !b.plotting && b.styles.is_none() && d.model.len() >= 2 * PARALLEL_CHUNK;
|
||||
@@ -156,7 +143,6 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
named: None,
|
||||
shared_budget: Some(&budget),
|
||||
transparency: 0,
|
||||
layers,
|
||||
};
|
||||
let c = ctx();
|
||||
for me in chunk.iter().filter(|me| seen(me)) {
|
||||
|
||||
Reference in New Issue
Block a user