Compare commits

..
19 changed files with 140 additions and 224 deletions
+7 -6
View File
@@ -250,17 +250,18 @@ fn parse_line(bytes: &[u8]) -> Line {
Ok(v) => v,
Err(e) => return Line::Reject(format!("bad JSON ({e}); expected one JSON request object per line; closing the connection")),
};
let Some(obj) = msg.as_object() else {
let Value::Object(mut obj) = msg else {
return Line::Reject("not a control request (expected a JSON object); closing the connection".into());
};
let Some(method) = obj.get("method").and_then(Value::as_str) else {
let Some(Value::String(method)) = obj.remove("method") else {
return Line::Reject("not a control request (missing string `method`); closing the connection".into());
};
// Moved out of the parsed object, not cloned: `params` can be most of the line.
Line::Request {
id: obj.get("id").cloned().unwrap_or(Value::Null),
method: method.to_string(),
params: obj.get("params").cloned().unwrap_or(json!({})),
token: obj.get("token").and_then(Value::as_str).map(str::to_string),
id: obj.remove("id").unwrap_or(Value::Null),
method,
params: obj.remove("params").unwrap_or(json!({})),
token: obj.remove("token").and_then(|t| if let Value::String(t) = t { Some(t) } else { None }),
}
}
+1 -31
View File
@@ -204,23 +204,6 @@ impl ArrayParams {
self.replaced.iter().rev().find(|r| (r.i, r.j, r.level) == (i, j, level)).map_or(self.source.as_str(), |r| r.block.as_str())
}
/// [`block_for`](Self::block_for) for many items: the replacements are indexed once (the last
/// entry for a place wins, as there), so each lookup is O(1) instead of a scan of `replaced`.
pub fn block_lookup<'a>(&'a self) -> impl Fn(u32, u32, u32) -> &'a str + 'a {
// Only places `items()` generates are indexed, so stray out-of-range records cost nothing.
let (cols, rows) = match &self.kind {
ArrayKind::Rect(r) => (r.cols, r.rows),
ArrayKind::Polar(p) => (p.items, p.rows),
};
let index: std::collections::HashMap<(u32, u32, u32), &str> = self
.replaced
.iter()
.filter(|r| r.i < cols && r.j < rows && r.level < self.levels)
.map(|r| ((r.i, r.j, r.level), r.block.as_str()))
.collect();
move |i, j, level| index.get(&(i, j, level)).copied().unwrap_or(self.source.as_str())
}
/// The block references that make up the array block: one per item, on layer 0 with ByBlock
/// properties so they take the array's. Handles are allocated from `d`. `None` when the
/// parameters are not [valid](Self::is_valid).
@@ -234,10 +217,9 @@ impl ArrayParams {
..Common::default()
};
let mut out = Vec::with_capacity(items.len());
let block_for = self.block_lookup();
for it in items {
let ins = Insert {
block: block_for(it.i, it.j, it.level).to_string(),
block: self.block_for(it.i, it.j, it.level).to_string(),
insert: it.insert,
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: it.rotation,
@@ -318,18 +300,6 @@ mod tests {
}
}
#[test]
fn block_lookup_agrees_with_block_for_and_the_last_replacement_wins() {
let mut p = rect(3, 2);
let r = |i, j, level, b: &str| ReplacedItem { i, j, level, block: b.into() };
p.replaced = vec![r(1, 0, 0, "A"), r(1, 0, 0, "B"), r(2, 1, 1, "C")];
let look = p.block_lookup();
for (i, j, level) in [(0, 0, 0), (1, 0, 0), (2, 1, 1), (2, 1, 0)] {
assert_eq!(look(i, j, level), p.block_for(i, j, level));
}
assert_eq!((look(1, 0, 0), look(0, 0, 0)), ("B", "*U1"));
}
#[test]
fn rect_items_go_column_row_level() {
let items = rect(3, 2).items().unwrap_or_default();
-6
View File
@@ -674,10 +674,6 @@ fn apply(
/// Associative hatches of block `b` whose loop is one closed polyline of the block take its
/// outline again (it may have been stretched).
fn refill_hatches(b: &mut Block) {
// Each loop gets its own copy of the outline, so many small loops on one big outline multiply it;
// past this many vertices in all, the remaining loops keep the outline they have.
const MAX_REFILLED_VERTICES: usize = 4_000_000;
let mut budget = MAX_REFILLED_VERTICES;
let outlines: HashMap<Handle, Vec<cadcraft_geom::PolyVertex>> = b
.entities
.iter()
@@ -693,9 +689,7 @@ fn refill_hatches(b: &mut Block) {
for l in &mut hh.loops {
if let [s] = l.sources.as_slice()
&& let Some(vs) = outlines.get(s)
&& let Some(left) = budget.checked_sub(vs.len())
{
budget = left;
l.vertices.clone_from(vs);
}
}
-7
View File
@@ -491,18 +491,11 @@ impl DimStyle {
use serde_json::Value;
let mut rejected = Vec::new();
let Ok(mut cur) = serde_json::to_value(&*self) else { return src.keys().cloned().collect() };
// Spellings of one field (`DIMPOST`, `dimpost`, `textPrefix`…): only the last is applied, so
// the style is checked once per field, not once per spelling (each check copies its strings).
let last_spelling: std::collections::HashMap<&str, &String> =
src.keys().filter_map(|k| DimStyle::field_name(k).filter(|f| *f != "name").map(|f| (f, k))).collect();
for (k, v) in src {
let Some(field) = DimStyle::field_name(k).filter(|f| *f != "name") else {
rejected.push(k.clone());
continue;
};
if last_spelling.get(field) != Some(&k) {
continue;
}
let Some(obj) = cur.as_object_mut() else { break };
let Some(old) = obj.get(field).cloned() else {
rejected.push(k.clone());
-11
View File
@@ -709,14 +709,3 @@ fn space_names_its_tab() {
assert_eq!(p.layout(), Some("Sheet A"));
assert_eq!(p.tab_name(), "Sheet A");
}
/// Several spellings of one DIM field apply once, the last in the map's order, and none is rejected.
#[test]
fn dim_style_spellings_of_one_field_apply_once() {
let mut st = DimStyle::default();
let m = serde_json::json!({ "textHeight": 2.5, "TEXTHEIGHT": 3.5, "DIMTXT": 4.5 });
let m = m.as_object().unwrap();
let want = m.values().next_back().and_then(serde_json::Value::as_f64).unwrap();
assert!(st.apply_fields(m).is_empty());
assert_eq!(st.text_height, want);
}
+14 -1
View File
@@ -177,8 +177,16 @@ fn create(s: &mut Session, objs: &[Handle], base: Vec2, p: ArrayParams, assoc: b
Ok(json!({ "created": n }))
}
/// The most objects EXPLODE of one associative array makes from its source objects (ten times the
/// default MAXARRAY).
const MAX_EXPLODED_OBJECTS: usize = 1_000_000;
/// EXPLODE of an associative array: every item becomes the objects of its block (the source
/// objects, or a replacement block's reference), in world coordinates. `None` for other objects.
///
/// An array that would explode into more than [`MAX_EXPLODED_OBJECTS`] objects (items times the
/// objects of the source block, which a file sets freely) is not expanded here: `None`, so EXPLODE
/// takes the array block's own items.
pub(crate) fn explode_array(d: &Drawing, e: &Entity) -> Option<Vec<Entity>> {
let EntityKind::Insert(ins) = &e.kind else { return None };
let p = d.array_params(e)?;
@@ -192,7 +200,12 @@ pub(crate) fn explode_array(d: &Drawing, e: &Entity) -> Option<Vec<Entity>> {
_ => None,
};
match source {
Some((im, b)) => out.extend(b.entities.iter().map(|se| placed(&item, se, &im))),
Some((im, b)) => {
if out.len().saturating_add(b.entities.len()) > MAX_EXPLODED_OBJECTS {
return None;
}
out.extend(b.entities.iter().map(|se| placed(&item, se, &im)));
}
None => out.push(item),
}
}
+1 -2
View File
@@ -188,9 +188,8 @@ fn item_at(d: &Drawing, h: Handle, at: Vec2, tol: f64) -> Option<(u32, u32, u32)
let mut best: Option<((u32, u32, u32), f64)> = None;
// Each block's own extents, measured once.
let mut extents: HashMap<String, (Vec2, Bounds2)> = HashMap::new();
let block_for = p.block_lookup();
for it in p.items()? {
let name = block_for(it.i, it.j, it.level);
let name = p.block_for(it.i, it.j, it.level);
let (base, local) = match extents.get(name) {
Some(x) => *x,
None => {
+37 -14
View File
@@ -2,7 +2,7 @@
//! block name and values, or one row per reference with its handle, layer, position and
//! rotation — returned, written to a CSV file and/or inserted as a table. Tags match in any case.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use cadcraft_doc::{EntityKind, Insert};
use cadcraft_geom::Vec2;
@@ -88,17 +88,34 @@ fn matches(spec: &str, text: &str) -> bool {
spec.split(',').any(|p| wildcard(p.trim(), text))
}
/// The attribute values of `ins`: its own, then the constant ones of its block.
fn values(d: &cadcraft_doc::Drawing, ins: &Insert) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = ins.attribs.iter().map(|a| (a.tag.to_ascii_uppercase(), a.text.value.clone())).collect();
if let Some(b) = d.block(&ins.block) {
for e in b.entities.iter() {
if let EntityKind::AttDef(a) = &e.kind
&& a.constant
&& !v.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
{
v.push((a.tag.to_ascii_uppercase(), a.text.value.clone()));
}
/// The constant attribute values of a block definition whose tags `tags` selects.
fn constants(b: &cadcraft_doc::Block, tags: &str) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = Vec::new();
for e in b.entities.iter() {
if let EntityKind::AttDef(a) = &e.kind
&& a.constant
&& matches(tags, &a.tag.to_ascii_uppercase())
&& !v.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
{
v.push((a.tag.to_ascii_uppercase(), a.text.value.clone()));
}
}
v
}
/// The attribute values of `ins` that `o.tags` selects: its own, then the constant ones of its
/// block (`consts`, read once per block). Values no column shows are not copied.
fn values(ins: &Insert, consts: &[(String, String)], tags: &str) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = ins
.attribs
.iter()
.map(|a| (a.tag.to_ascii_uppercase(), a))
.filter(|(t, _)| matches(tags, t))
.map(|(t, a)| (t, a.text.value.clone()))
.collect();
for (t, val) in consts {
if !v.iter().any(|(k, _)| k.eq_ignore_ascii_case(t)) {
v.push((t.clone(), val.clone()));
}
}
v
@@ -113,6 +130,7 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).cloned().collect()
};
let mut out = Vec::new();
let mut consts: HashMap<String, Vec<(String, String)>> = HashMap::new();
for e in &ents {
let EntityKind::Insert(ins) = &e.kind else { continue };
let anonymous = ins.block.starts_with('*') || d.block(&ins.block).is_some_and(|b| b.anonymous);
@@ -121,12 +139,17 @@ fn found(s: &Session, p: &Value, o: &Options) -> Result<Vec<Found>> {
}
let block = d.block(&ins.block).map(|b| b.name.clone()).unwrap_or_else(|| ins.block.clone());
out.push(Found {
block,
handle: e.handle.hex(),
layer: e.common.layer.clone(),
at: ins.insert.xy(),
rotation: ins.rotation.to_degrees(),
values: values(d, ins),
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, &o.tags)).unwrap_or_default()),
&o.tags,
),
});
}
Ok(out)
+8 -54
View File
@@ -453,32 +453,14 @@ impl Chain {
}
/// Point and unit tangent at arc length `d` from the start (clamped).
pub fn at_length(&self, d: f64) -> Option<(Vec2, Vec2)> {
self.at_length_in(&self.end_lengths(), d)
}
/// The arc length from the chain's start to the end of each segment, for [`Self::at_length_in`].
pub fn end_lengths(&self) -> Vec<f64> {
self.segs
.iter()
.scan(0.0, |acc, s| {
*acc += s.len();
Some(*acc)
})
.collect()
}
/// [`Self::at_length`] with the segment end lengths computed once (`ends` from
/// [`Self::end_lengths`]), so many samples along one chain each cost a binary search.
pub fn at_length_in(&self, ends: &[f64], d: f64) -> Option<(Vec2, Vec2)> {
let first = ends.partition_point(|e| d > e + 1e-12);
// A segment of no length can't hold the point; the next one that has length does.
// `ends` never decreases, so the first segment past `first` with length is found by search
// (a run of zero-length segments can be long) instead of a scan.
let base = first.checked_sub(1).and_then(|p| ends.get(p)).copied().unwrap_or(0.0);
let i = first + ends.get(first..).unwrap_or_default().partition_point(|e| *e <= base);
let i = (i < self.segs.len()).then_some(i);
if let Some(s) = i.and_then(|i| self.segs.get(i)) {
let start = i.and_then(|i| i.checked_sub(1)).and_then(|p| ends.get(p)).copied().unwrap_or(0.0);
let t = ((d - start) / s.len()).clamp(0.0, 1.0);
return Some((s.at(t), s.tangent(t)));
let mut acc = 0.0;
for s in &self.segs {
let l = s.len();
if d <= acc + l + 1e-12 && l > 0.0 {
let t = ((d - acc) / l).clamp(0.0, 1.0);
return Some((s.at(t), s.tangent(t)));
}
acc += l;
}
self.segs.last().map(|s| (s.end(), s.tangent(1.0)))
}
@@ -658,34 +640,6 @@ pub fn reverse_vertices(vs: &[PolyVertex], closed: bool) -> Vec<PolyVertex> {
mod tests {
use super::*;
/// The indexed lookup gives the point and tangent a scan from the start gives, also past the
/// ends and over a segment of no length.
#[test]
fn at_length_in_matches_a_scan() {
let l = |a: (f64, f64), b: (f64, f64)| Segment::Line(Line::new(Vec2::new(a.0, a.1), Vec2::new(b.0, b.1)));
let c = Chain {
segs: vec![l((0.0, 0.0), (0.0, 0.0)), l((0.0, 0.0), (4.0, 0.0)), l((4.0, 0.0), (4.0, 0.0)), l((4.0, 0.0), (4.0, 3.0))],
closed: false,
};
let ends = c.end_lengths();
for d in [-1.0, 0.0, 1.5, 4.0, 4.0 + 1e-13, 5.0, 7.0, 9.0] {
// The scan this replaced.
let mut acc = 0.0;
let mut want = c.segs.last().map(|s| (s.end(), s.tangent(1.0)));
for s in &c.segs {
let len = s.len();
if d <= acc + len + 1e-12 && len > 0.0 {
let t = ((d - acc) / len).clamp(0.0, 1.0);
want = Some((s.at(t), s.tangent(t)));
break;
}
acc += len;
}
assert_eq!(c.at_length_in(&ends, d), want, "d = {d}");
assert_eq!(c.at_length(d), want);
}
}
#[test]
fn arc_builders() {
let a = arc_sca(Vec2::new(1.0, 0.0), Vec2::ZERO, std::f64::consts::FRAC_PI_2).unwrap();
+2 -4
View File
@@ -36,18 +36,16 @@ fn check_block(s: &Session, b: Option<&str>) -> Result<()> {
pub(crate) fn divide_points(c: &Chain, n: usize) -> Vec<(Vec2, Vec2)> {
let l = c.len();
let range = if c.closed { 0..n } else { 1..n };
let ends = c.end_lengths();
range.filter_map(|i| c.at_length_in(&ends, l * i as f64 / n as f64)).collect()
range.filter_map(|i| c.at_length(l * i as f64 / n as f64)).collect()
}
pub(crate) fn measure_points(c: &Chain, step: f64) -> Vec<(Vec2, Vec2)> {
let l = c.len();
let ends = c.end_lengths();
let mut out = Vec::new();
let mut k = 1usize;
while (k as f64) * step < l - 1e-9 && out.len() < MAX_GEN {
let d = k as f64 * step;
out.extend(c.at_length_in(&ends, d));
out.extend(c.at_length(d));
k += 1;
}
out
+1 -2
View File
@@ -58,9 +58,8 @@ pub(crate) fn cloud_vertices(boundary: &[Vec2], arc_len: f64, per_edge: bool, re
closed: true,
};
let k = ((perim / arc_len).round() as usize).clamp(3, MAX_GEN);
let ends = chain.end_lengths();
for j in 0..k {
out.extend(chain.at_length_in(&ends, perim * j as f64 / k as f64).map(|x| x.0));
out.extend(chain.at_length(perim * j as f64 / k as f64).map(|x| x.0));
}
}
let b = if reverse { -0.5 } else { 0.5 };
+12 -8
View File
@@ -8,7 +8,7 @@ use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, MAX_BLOCK_DEPTH, Space, entity_bounds};
use cadcraft_doc::{BlockBoundsCache, Drawing, Entity, EntityKind, Handle, MAX_BLOCK_DEPTH, Space, entity_bounds_cached};
use cadcraft_geom::{Bounds2, Vec2};
use serde_json::{Value, json};
@@ -375,16 +375,20 @@ fn run_compare(s: &mut Session, p: &Value) -> Result<Value> {
}
let Diff { added, removed, changed, common, truncated } = diff(&cur, &other, &space);
let mut boxes = Vec::new();
let mut bound = |d: &Drawing, h: Handle| {
if let Some(b) = d.entity(h).map(|e| entity_bounds(d, e, 0)).filter(|b| !b.is_empty() && b.min.is_finite() && b.max.is_finite()) {
// One block-bounds cache for each drawing, so a block many differing entities refer to is
// measured once, not once per entity.
let (mut cur_cache, mut other_cache) = (BlockBoundsCache::default(), BlockBoundsCache::default());
fn bound<'a>(boxes: &mut Vec<Bounds2>, d: &'a Drawing, h: Handle, cache: &mut BlockBoundsCache<'a>) {
if let Some(b) = d.entity(h).map(|e| entity_bounds_cached(d, e, 0, cache)).filter(|b| !b.is_empty() && b.min.is_finite() && b.max.is_finite())
{
boxes.push(b);
}
};
added.iter().for_each(|h| bound(&cur, *h));
removed.iter().for_each(|h| bound(&other, *h));
}
added.iter().for_each(|h| bound(&mut boxes, &cur, *h, &mut cur_cache));
removed.iter().for_each(|h| bound(&mut boxes, &other, *h, &mut other_cache));
for (a, b, _, _) in &changed {
bound(&cur, *a);
bound(&other, *b);
bound(&mut boxes, &cur, *a, &mut cur_cache);
bound(&mut boxes, &other, *b, &mut other_cache);
}
let regions = regions(&boxes);
let ch: Vec<Value> = changed
+6
View File
@@ -108,6 +108,12 @@ pub(super) fn run_mocoro(s: &mut Session, p: &Value) -> Result<Value> {
}
// Every option is checked before the drawing changes.
let ops = list.iter().enumerate().map(|(i, v)| parse_op(i, v)).collect::<Result<Vec<_>>>()?;
// Each Copy clones every selected object: the copies together stay within MAXARRAY, as an array's do.
// Erased handles are skipped by `apply`, so only live objects count (none: nothing is copied).
let live = counts(s, &objs)?.0;
if live > 0 {
crate::cmd::array::check_size(s, ops.iter().filter(|o| matches!(o, Op::Copy(_))).count() as u64, live)?;
}
let mut copies = Vec::new();
for op in ops {
copies.extend(apply(s, &objs, &mut base, op)?);
+2 -4
View File
@@ -34,12 +34,10 @@ pub(crate) fn arraypath(
_ => (6, if c.closed { l / 6.0 } else { l / 5.0 }),
};
crate::cmd::array::check_size(s, n as u64, objs.len())?;
// Segment end lengths once: each sample is then a binary search, not a scan from the start.
let ends = c.end_lengths();
let Some((p0, t0)) = c.at_length_in(&ends, 0.0) else { return Ok(Vec::new()) };
let Some((p0, t0)) = c.at_length(0.0) else { return Ok(Vec::new()) };
let mut out = Vec::new();
for i in 1..n {
let Some((p, t)) = c.at_length_in(&ends, step * i as f64) else { continue };
let Some((p, t)) = c.at_length(step * i as f64) else { continue };
let mut m = Mat3::translate(p - p0);
if align {
m = Mat3::rotate_about(p, t.angle() - t0.angle()).then_before(m);
+14
View File
@@ -233,3 +233,17 @@ fn options_on_objects_that_are_gone_record_nothing() {
assert!(lines(&s).is_empty());
assert_eq!(undo_len(&s), undo, "no empty undo step");
}
#[test]
fn json_form_copies_stay_within_maxarray() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
// 5,000 Copy options of 30 objects would be 150,000 copies, over the default MAXARRAY of 100,000.
let others: Vec<String> = (0..29).map(|i| line(&mut s, [0.0, f64::from(i + 1)], [10.0, f64::from(i + 1)])).collect();
let mut hs = vec![l];
hs.extend(others);
let ops: Vec<Value> = (0..5_000).map(|i| json!({"copy": [0, i]})).collect();
let e = s.execute("mocoro", &json!({"handles": hs, "base": [0, 0], "ops": ops})).unwrap_err();
assert!(e.to_string().contains("MAXARRAY"), "{e}");
assert_eq!(lines(&s).len(), 30, "nothing was copied");
}
+17 -3
View File
@@ -50,6 +50,8 @@ const MAX_LAYER_NAME: usize = 1000;
/// Most link annotations in one file, and the longest link target (bytes).
const MAX_LINKS: usize = 10_000;
const MAX_URL: usize = 2048;
/// The most link annotations in one PDF, over all pages (a page holds at most [`MAX_LINKS`]).
const MAX_FILE_ANNOTATIONS: usize = 100_000;
/// Longest title and page label written (characters).
const MAX_TEXT: usize = 1000;
@@ -357,7 +359,10 @@ fn sheet_name(space: &Space) -> String {
/// Write a vector PDF with one page per space (at most [`MAX_PAGES`]), each labelled with its
/// layout name. The options apply to every sheet; layouts use their own page setups.
pub fn pdf_sheets(d: &Drawing, spaces: &[Space], o: &PdfOptions) -> Result<Vec<u8>> {
let items: Vec<(Space, PdfOptions)> = spaces.iter().take(MAX_PAGES + 1).map(|s| (s.clone(), o.clone())).collect();
// Links are file-wide (`pdf_items` reads them from `o`), so a sheet's own options carry none:
// a copy of the list per sheet multiplied it by the sheet count.
let sheet = PdfOptions { links: Vec::new(), ..o.clone() };
let items: Vec<(Space, PdfOptions)> = spaces.iter().take(MAX_PAGES + 1).map(|s| (s.clone(), sheet.clone())).collect();
pdf_items(d, &items, o)
}
@@ -453,8 +458,16 @@ fn write_pdf(d: &Drawing, items: &[(Space, PdfOptions)], o: &PdfOptions, text_as
let mut writer = Writer::new(settings, metadata);
let mut fonts = fonts::Fonts::new(!text_as_geometry);
let mut groups = Groups::default();
// Link annotations of the whole file: each page keeps its own copy of the targets, so the
// pages times the links must not multiply without a bound.
let mut annotations_left = MAX_FILE_ANNOTATIONS;
for (s, po) in items {
let page = sheet(drawn, s, po, &links)?;
// Once the budget is spent no link is built (scanned and cloned) for later pages.
let mut page = sheet(drawn, s, po, if annotations_left == 0 { &links[..0] } else { &links })?;
// Only targets `write_page` accepts count against the budget.
page.links.retain(|(_, url)| uri(url).is_some());
page.links.truncate(annotations_left);
annotations_left -= page.links.len();
let doc = writer.document_mut();
// Groups are added to the document, so they must exist before the page starts.
groups.add(doc, d, &page.layers);
@@ -625,7 +638,8 @@ pub(crate) fn placement(space: &Space, bounds: &Bounds2, fit: bool, sheet: &Shee
/// The hyperlinks to plot: the options' own, then (with [`PdfOptions::hyperlinks`]) those the
/// drawing file gave its entities as `PE_URL` extended data, at most [`MAX_LINKS`] in all.
fn all_links(d: &Drawing, o: &PdfOptions) -> Vec<(Handle, String)> {
let mut out: Vec<(Handle, String)> = o.links.iter().take(MAX_LINKS).cloned().collect();
// A target past MAX_URL is refused at the annotation anyway; it is left out before it is copied.
let mut out: Vec<(Handle, String)> = o.links.iter().filter(|(_, u)| u.len() <= MAX_URL).take(MAX_LINKS).cloned().collect();
if !o.hyperlinks {
return out;
}
+13 -45
View File
@@ -133,20 +133,6 @@ pub fn table_geometry(d: &Drawing, t: &Table) -> TableGeom {
if parts.len() <= 1 {
return layout(d, t);
}
// Every part is laid out and kept, so the cells of all of them count together (a header repeated
// on each part adds to each); past the limit only the table's outline is drawn.
let cols = t.col_widths.len().min(10_000);
let total: usize = parts.iter().map(|p| p.rows.len().min(10_000).saturating_mul(cols)).fold(0, usize::saturating_add);
if total > MAX_LAYOUT_CELLS {
// One outline per part, at the part's origin and as tall as its displayed rows.
let w = edges(&t.col_widths, cols).last().copied().unwrap_or(0.0);
let mut g = TableGeom::default();
for p in parts {
let h: f64 = p.rows.iter().take(10_000).map(|r| t.row_heights.get(*r).copied().filter(|v| v.is_finite()).unwrap_or(0.0)).sum();
outline_box(&mut g, p.origin, w, h);
}
return g;
}
let mut g = TableGeom::default();
for p in parts {
let mut pg = layout(d, &part_table(t, &p.rows, p.origin));
@@ -170,20 +156,16 @@ fn part_table(t: &Table, rows: &[usize], origin: Vec2) -> Table {
// Otherwise `layout` renders an outline, so materialising millions of empty cells here would be
// wasted work an attacker can trigger with a compact dimension array (CWE-789).
if rows.len().min(10_000).saturating_mul(cols) <= MAX_LAYOUT_CELLS {
// `run[i]`: how many rows from part row `i` follow one another in the table, found once so a
// merge is cut by a lookup, not by walking the rows it spans.
let mut run = vec![1u32; rows.len()];
for i in (0..rows.len().saturating_sub(1)).rev() {
if rows.get(i + 1).zip(rows.get(i)).is_some_and(|(next, this)| *next == this + 1) {
run[i] = run.get(i + 1).copied().unwrap_or(0).saturating_add(1);
}
}
for (i, row) in cells.iter_mut().enumerate() {
row.resize_with(cols, Default::default);
for cell in row.iter_mut() {
if let Some((rs, cs)) = cell.merged {
// Rows of the merge that follow in this part.
let n = rs.max(1).min(run.get(i).copied().unwrap_or(1));
let start = rows.get(i).copied().unwrap_or(0);
let mut n = 1u32;
while n < rs && rows.get(i + n as usize).is_some_and(|r| *r == start + n as usize) {
n += 1;
}
cell.merged = (n > 1 || cs > 1).then_some((n, cs));
}
}
@@ -204,27 +186,6 @@ fn part_table(t: &Table, rows: &[usize], origin: Vec2) -> Table {
}
}
/// The table's outline alone, for a table too large to lay out.
fn outline(t: &Table) -> TableGeom {
let rows = t.row_heights.len().min(10_000);
let cols = t.col_widths.len().min(10_000);
let (w, h) = (edges(&t.col_widths, cols).last().copied().unwrap_or(0.0), edges(&t.row_heights, rows).last().copied().unwrap_or(0.0));
let mut g = TableGeom::default();
outline_box(&mut g, t.insert.xy(), w, h);
g
}
/// The four sides of a `w` by `h` box hanging down from `o`.
fn outline_box(g: &mut TableGeom, o: Vec2, w: f64, h: f64) {
let p = |x: f64, y: f64| o + Vec2::new(x, -y);
let corners = [p(0.0, 0.0), p(w, 0.0), p(w, h), p(0.0, h), p(0.0, 0.0)];
for s in corners.windows(2) {
if let [a, b] = s {
g.lines.push(TableLine { a: *a, b: *b, color: Color::ByBlock, lineweight: Lineweight::ByBlock });
}
}
}
/// Lay out one part.
fn layout(d: &Drawing, t: &Table) -> TableGeom {
let st = table_style_of(d, t);
@@ -242,7 +203,14 @@ fn layout(d: &Drawing, t: &Table) -> TableGeom {
}
let cov = coverage(t);
if rows.saturating_mul(cols) > MAX_LAYOUT_CELLS || cov.is_err() {
return outline(t);
let (w, h) = (x_at(cols), y_at(rows));
let corners = [p(0.0, 0.0), p(w, 0.0), p(w, h), p(0.0, h), p(0.0, 0.0)];
for s in corners.windows(2) {
if let [a, b] = s {
g.lines.push(TableLine { a: *a, b: *b, color: Color::ByBlock, lineweight: Lineweight::ByBlock });
}
}
return g;
}
let k = style_scale(&st, t);
// Cell styles by name, scaled to the table.
-22
View File
@@ -1148,25 +1148,3 @@ fn without_linetypes_every_line_is_drawn_whole() {
let drawn = build(&drawing("DASHED"), &Space::Model, &Options::default());
assert!(drawn.prims.len() > 2 * continuous.prims.len(), "drawn with its linetype: {} primitives", drawn.prims.len());
}
/// A broken table whose parts, with their repeated labels, together hold more cells than a table may
/// lay out is drawn as its outline, though the table's own cells are within the limit.
#[test]
fn broken_table_cells_count_across_its_parts() {
let d = Drawing::new_imperial();
let t = cadcraft_doc::Table {
col_widths: vec![1.0; 600],
row_heights: vec![1.0; 1000],
title: true,
header: true,
breaks: Some(cadcraft_doc::TableBreak { enabled: true, height: 3.0, ..Default::default() }),
..Default::default()
};
assert!(t.parts().len() > 500 && 1000 * 600 <= crate::MAX_LAYOUT_CELLS);
let g = crate::table_geometry(&d, &t);
let parts = t.parts();
assert_eq!((g.lines.len(), g.texts.len(), g.fills.len()), (4 * parts.len(), 0, 0));
// Each part's box starts at its own origin.
let top = |i: usize| g.lines[4 * i].a;
assert!((0..parts.len()).all(|i| top(i) == parts[i].origin));
}
+5 -4
View File
@@ -306,7 +306,7 @@ fn point_style(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
enum Field {
Num(&'static str, &'static str, f64),
Bool(&'static str, &'static str, bool),
Choice(&'static str, &'static str, String, Vec<String>),
Choice(&'static str, &'static str, String, std::sync::Arc<[String]>),
}
struct Style {
@@ -371,7 +371,8 @@ fn mleader_style(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Acti
no_drawing(ctx, "Multileader Style", open);
return None;
};
let text_styles: Vec<String> = d.text_styles.iter().map(|s| s.name.clone()).collect();
// One list shared by every row: a copy per style multiplied the two counts.
let text_styles: std::sync::Arc<[String]> = d.text_styles.iter().map(|s| s.name.clone()).collect();
let mut styles: Vec<Style> = d
.mleader_styles
.iter()
@@ -382,7 +383,7 @@ fn mleader_style(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Acti
Field::Num("textHeight", "Text height", s.text_height),
Field::Num("landingGap", "Landing gap", s.landing_gap),
Field::Num("dogleg", "Landing length", s.dogleg),
Field::Choice("textStyle", "Text style", s.text_style.clone(), text_styles.clone()),
Field::Choice("textStyle", "Text style", s.text_style.clone(), std::sync::Arc::clone(&text_styles)),
],
})
.collect();
@@ -451,7 +452,7 @@ fn style_manager(ctx: &egui::Context, open: &mut bool, title: &str, cmd: &'stati
ui.label(*label);
let mut picked = None;
craft_ui::ComboBox::from_id_salt((cmd, *key)).selected_text(v.as_str()).show_ui(ui, |ui| {
for c in choices {
for c in choices.iter() {
if ui.selectable_label(c == v, c).clicked() && c != v {
picked = Some(c.clone());
}