Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 59468ec6e5 test: avoid unchecked option parsing in Flatpak regression 2026-11-11 14:08:46 -05:00
Dylan M. Taylor f9dd53a77e Release 0.20.1: fix Flatpak update permissions 2026-11-11 14:04:24 -05:00
FredApps 28241abb8f Merge main into release for CADCraft v0.20.0 2026-11-11 16:38:54 +01:00
FredAppsand.CooCoo 51f5bbcc21 Fonts: the MTEXT strip test expects a short \U+ escape to keep its digits (#2954) (#3024)
strips_formatting expected `\U+12` to strip to nothing. plain_mtext
dropped every unknown code up to the next `;` when that assertion was
written. #1036 limits this to the codes in takes_arg, and `\U` is not
one of them, so plain_mtext returns `+12`. The layout and paragraph
paths give the same text, and
unicode_escapes_are_decoded_in_every_parser already expects it.

The test has failed on main since both changes merged, and the failure
stops the gate at its test step. The test module now also checks the
short escape in both layout paths.

(cherry picked from commit 6f0f1fefa8)

Co-authored-by: .CooCoo <RobertLeeLW@gmail.com>
2026-11-10 19:49:24 +01:00
Dylan M. Taylor 227e3c385b Release 0.19.0 2026-11-09 06:29:44 -05:00
15 changed files with 86 additions and 336 deletions
Generated
+16 -16
View File
@@ -614,7 +614,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadcraft"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -645,7 +645,7 @@ dependencies = [
[[package]]
name = "cadcraft-cli"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -661,14 +661,14 @@ dependencies = [
[[package]]
name = "cadcraft-color"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-constraints"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-doc",
"cadcraft-geom",
@@ -678,7 +678,7 @@ dependencies = [
[[package]]
name = "cadcraft-doc"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-geom",
@@ -689,7 +689,7 @@ dependencies = [
[[package]]
name = "cadcraft-dwg"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"acadrust",
"log",
@@ -697,7 +697,7 @@ dependencies = [
[[package]]
name = "cadcraft-dxf"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"encoding_rs",
"log",
@@ -706,7 +706,7 @@ dependencies = [
[[package]]
name = "cadcraft-engine"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-constraints",
@@ -727,7 +727,7 @@ dependencies = [
[[package]]
name = "cadcraft-fonts"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-dxf",
"cadcraft-geom",
@@ -740,14 +740,14 @@ dependencies = [
[[package]]
name = "cadcraft-geom"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-io"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"acadrust",
"cadcraft-color",
@@ -769,7 +769,7 @@ dependencies = [
[[package]]
name = "cadcraft-mcp"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -784,7 +784,7 @@ dependencies = [
[[package]]
name = "cadcraft-render"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -798,7 +798,7 @@ dependencies = [
[[package]]
name = "cadcraft-ui-egui"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -825,7 +825,7 @@ dependencies = [
[[package]]
name = "cadcraft-web"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"cadcraft-engine",
"cadcraft-fonts",
@@ -5993,7 +5993,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "0.20.0"
version = "0.20.1"
dependencies = [
"chrono",
"serde_json",
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
# default-members
[workspace.package]
version = "0.20.0"
version = "0.20.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
+2 -3
View File
@@ -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,
+2 -70
View File
@@ -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());
+1 -1
View File
@@ -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
View File
@@ -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)]]);
+1 -33
View File
@@ -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.
+10 -56
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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));
-17
View File
@@ -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 -19
View File
@@ -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)) {
+27
View File
@@ -0,0 +1,27 @@
//! Release and source Flatpaks must allow release checks and host update helpers.
fn finish_args(manifest: &str) -> Vec<&str> {
manifest
.split("finish-args:")
.nth(1)
.unwrap_or_default()
.lines()
.take_while(|line| line.is_empty() || line.starts_with(' ') || line.starts_with('#'))
.filter_map(|line| line.trim().strip_prefix("- "))
.map(|arg| arg.trim_matches(['\'', '"']))
.collect()
}
#[test]
fn flatpak_update_permissions() {
let manifests = [
include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.bundle.yml"),
include_str!("../../../packaging/linux/flatpak/ai.storyteller.cadcraft.yml"),
];
let args = manifests.map(finish_args);
assert_eq!(args[0], args[1], "source and release permissions must agree");
for permissions in args {
for required in ["--share=network", "--talk-name=org.freedesktop.Flatpak"] {
assert!(permissions.contains(&required), "missing update permission: {required}");
}
}
}
@@ -17,6 +17,8 @@ runtime-version: '26.08'
sdk: org.freedesktop.Sdk
command: cadcraft
finish-args:
# The shared updater inspects and updates this installation via flatpak-spawn --host.
- --talk-name=org.freedesktop.Flatpak
- --filesystem=xdg-data/ArtCraft/versions:ro
# Display: Wayland first; X11 only when no Wayland compositor is running.
- --socket=wayland
@@ -18,6 +18,8 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: cadcraft
finish-args:
# The shared updater inspects and updates this installation via flatpak-spawn --host.
- --talk-name=org.freedesktop.Flatpak
- --filesystem=xdg-data/ArtCraft/versions:ro
# Display: Wayland first; X11 only when no Wayland compositor is running.
- --socket=wayland