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
9 changed files with 57 additions and 121 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"
+3 -17
View File
@@ -8,9 +8,6 @@ use cadcraft_geom::{Line, Vec2};
pub const MAX_OBJECTS: usize = 200;
/// Most candidates returned.
pub const MAX_CANDIDATES: usize = 2000;
/// Residual-evaluation work all the solves of one AUTOCONSTRAIN may make together (four
/// full-size solves; a few thousand small ones).
const MAX_AUTO_WORK: f64 = 4.0 * crate::MAX_EVAL_WORK;
/// Inference options.
#[derive(Clone, Debug)]
@@ -312,22 +309,11 @@ fn on_arc(d: &Drawing, h: Handle, c: Vec2, p: Vec2) -> bool {
/// Infer and apply constraints over `handles`; returns the ids added. Redundant or conflicting
/// candidates are skipped.
pub fn autoconstrain(d: &mut Drawing, handles: &[Handle], o: &InferOptions) -> Vec<u32> {
autoconstrain_report(d, handles, o).0
}
/// [`autoconstrain`], and whether it stopped early because the solver-work allowance of the command
/// was spent (the remaining candidates were not tried).
pub fn autoconstrain_report(d: &mut Drawing, handles: &[Handle], o: &InferOptions) -> (Vec<u32>, bool) {
let mut added = Vec::new();
// One allowance of solver work for all the candidates: each is a full solve of its component.
let mut work = MAX_AUTO_WORK;
for c in infer(d, handles, o) {
match crate::add_within(d, c, &crate::SolveOptions::default(), &mut work) {
Ok(id) => added.push(id),
// The allowance is spent: what was added stays, the remaining candidates are not tried.
Err(crate::SolveError::TooLarge) if work < 0.0 => return (added, true),
Err(_) => {}
if let Ok(id) = crate::add(d, c, &crate::SolveOptions::default()) {
added.push(id);
}
}
(added, false)
added
}
+4 -43
View File
@@ -36,7 +36,7 @@ use cadcraft_doc::{Constraint, ConstraintKind, Drawing, GeomRef, Handle};
use cadcraft_geom::Vec2;
use serde::Serialize;
pub use auto::{INFER_TYPES, InferOptions, autoconstrain, autoconstrain_report, infer, infer_placed};
pub use auto::{INFER_TYPES, InferOptions, autoconstrain, infer, infer_placed};
pub use dof::{DofReport, DofStatus, ObjectDof, dof};
pub use expr::{Env, ExprError};
pub use model::{ref_points, rigid_points, supported};
@@ -53,14 +53,6 @@ const MAX_ITER: usize = 200;
/// Largest residual-evaluation work (including spline rebuilds) one numerical Jacobian may take,
/// shared by solving, redundancy checks and DOF analysis.
pub(crate) const MAX_EVAL_WORK: f64 = 2e9;
/// Most multiply-adds (rows x columns squared) of the dense rank factorisations one Fix makes:
/// about a thousand parameters locked at once is 1e9; 2e10 is a few seconds.
const MAX_QR_WORK: f64 = 2e10;
/// Whether a dense rank factorisation of a `rows` x `cols` matrix passes [`MAX_QR_WORK`].
fn qr_too_large(rows: usize, cols: usize) -> bool {
(rows as f64) * (cols as f64) * (cols as f64) > MAX_QR_WORK
}
/// Residual evaluations a solver iteration makes besides its Jacobian (damped trial steps).
const TRIAL_EVALS: f64 = 14.0;
@@ -343,14 +335,8 @@ impl Comp {
}
/// Constraint ids in conflict (each one whose removal makes the rest solvable).
fn conflicts(&self, x_failed: &[f64]) -> Vec<u32> {
self.conflicts_probing(x_failed, true)
}
/// [`Comp::conflicts`]; `probe` false skips the re-solves that look for the constraints whose
/// removal lets the rest converge.
fn conflicts_probing(&self, x_failed: &[f64], probe: bool) -> Vec<u32> {
let mut ids = Vec::new();
if probe && self.built.len() <= MAX_CONFLICT_PROBES {
if self.built.len() <= MAX_CONFLICT_PROBES {
for i in 0..self.built.len() {
if let Ok(o) = self.run(Some(i))
&& o.converged
@@ -755,14 +741,7 @@ pub fn purge_invalid(d: &mut Drawing) -> Vec<u32> {
/// Add a constraint: assigns its id (and name/expression for dimensional constraints when
/// empty), refuses redundant or conflicting constraints, solves and writes the geometry back.
/// On error the drawing is unchanged.
pub fn add(d: &mut Drawing, c: Constraint, opts: &SolveOptions) -> Result<u32, SolveError> {
let mut unlimited = f64::INFINITY;
add_within(d, c, opts, &mut unlimited)
}
/// [`add`] charging the solve's residual-evaluation work to `work`: `TooLarge` (nothing changed)
/// once it would take more than is left.
pub(crate) fn add_within(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions, work: &mut f64) -> Result<u32, SolveError> {
pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u32, SolveError> {
if d.constraints.len() >= MAX_CONSTRAINTS {
return Err(SolveError::Other("too many constraints".into()));
}
@@ -798,24 +777,10 @@ pub(crate) fn add_within(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions
let Some(comp) = comps.into_iter().find(|k| k.built.iter().any(|b| b.id == id)) else {
return Err(SolveError::Invalid { id, msg: "could not be interpreted".into() });
};
*work -= comp.eval_work(&comp.free(None), None);
if *work < 0.0 {
return Err(SolveError::TooLarge);
}
let o = comp.run(None)?;
if !o.converged {
// The new constraint first, then the existing ones it fights with.
// Each probe is another solve (up to one per constraint): they are charged too, and skipped
// when the allowance cannot pay for them.
let probes = if comp.built.len() <= MAX_CONFLICT_PROBES { comp.built.len() as f64 } else { 0.0 };
let cost = probes * comp.eval_work(&comp.free(None), None);
// Paid for before it is spent: when the allowance cannot cover the probes they are skipped and
// it is left as it was for the candidates still to try.
let affordable = cost <= *work;
if affordable {
*work -= cost;
}
let mut ids = comp.conflicts_probing(&o.x, affordable);
let mut ids = comp.conflicts(&o.x);
ids.retain(|i| *i != id);
ids.insert(0, id);
return Err(SolveError::Conflict { ids });
@@ -854,10 +819,6 @@ pub(crate) fn add_within(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions
let locked: Vec<usize> = before.iter().enumerate().filter(|(_, j)| !after.contains(j)).map(|(k, _)| k).collect();
let rows_old = comp.rows(Some(pos));
let nf = before.len();
// The synthetic lock rows enlarge the matrix after the solver's own limits were checked.
if qr_too_large(rows_old + locked.len(), nf) {
return Err(SolveError::TooLarge);
}
let f = |x: &[f64], out: &mut Vec<f64>| comp.eval(x, out, Some(pos));
let mut jac = lm::jacobian(&f, &o.x, &before, rows_old);
let r_old = lm::rank_qr(&jac, rows_old, nf);
-38
View File
@@ -516,41 +516,3 @@ fn next_name_skips_names_in_use() {
});
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d2");
}
#[test]
fn a_fix_over_a_huge_component_is_refused_before_its_dense_factorisation() {
assert!(!super::qr_too_large(1_000, 1_000));
assert!(super::qr_too_large(3_000, 3_000));
assert!(super::qr_too_large(usize::MAX, 2));
}
#[test]
fn a_spent_work_allowance_refuses_a_constraint_and_leaves_the_drawing() {
let mut d = Drawing::default();
let l = line(&mut d, (0.0, 0.0), (10.0, 0.01));
let before = d.clone();
let c = con(ConstraintKind::Horizontal, &[w(l)]);
assert!(matches!(add_within(&mut d, c.clone(), &SolveOptions::default(), &mut 0.0), Err(SolveError::TooLarge)));
assert_eq!(d, before);
let mut plenty = 1e9;
assert!(add_within(&mut d, c, &SolveOptions::default(), &mut plenty).is_ok());
assert!(plenty < 1e9, "the solve was charged");
let (_, stopped) = autoconstrain_report(&mut d, &[l], &InferOptions::default());
assert!(!stopped, "a small command never spends its allowance");
}
#[test]
fn skipped_conflict_probes_do_not_spend_the_allowance() {
let mut d = Drawing::default();
let b = line(&mut d, (0.0, 0.0), (3.0, 4.0));
ok(&mut d, con(ConstraintKind::Fix, &[w(b)]));
let candidate = || con(ConstraintKind::Vertical, &[w(b)]);
// The whole cost of the conflicting candidate: its solve and its probes.
let mut rich = 1e12;
assert!(matches!(add_within(&mut d.clone(), candidate(), &SolveOptions::default(), &mut rich), Err(SolveError::Conflict { .. })));
let total = 1e12 - rich;
// Enough for the solve but not the probes: they are skipped, and what is left stays positive.
let mut work = total * 0.6;
assert!(matches!(add_within(&mut d, candidate(), &SolveOptions::default(), &mut work), Err(SolveError::Conflict { .. })));
assert!(work > 0.0 && work < total * 0.6, "{work} of {total}");
}
+2 -6
View File
@@ -821,15 +821,11 @@ fn run_autoconstrain(s: &mut Session, p: &Value) -> Result<Value> {
o.types = t.iter().filter_map(Value::as_str).map(|n| kind_from_name(n).map_or_else(|| n.to_string(), |k| k.name().to_string())).collect();
}
let mut d = s.doc()?.clone();
let (ids, stopped) = cs::autoconstrain_report(&mut d, &hs, &o);
let ids = cs::autoconstrain(&mut d, &hs, &o);
if !ids.is_empty() {
*s.doc_mut()? = d;
}
let mut message = format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len());
if stopped {
message.push_str("; stopped early: the rest need more solver work than one command may spend, so run AUTOCONSTRAIN on fewer objects");
}
Ok(json!({ "added": ids.len(), "ids": ids, "stopped": stopped, "message": message }))
Ok(json!({ "added": ids.len(), "ids": ids, "message": format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len()) }))
}
fn run_delconstraint(s: &mut Session, p: &Value) -> Result<Value> {
+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