mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-12 08:49:40 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
776badbc20 | ||
|
|
2d2f5b6fc9 | ||
|
|
8e6b45ad08 |
@@ -27,10 +27,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
|
||||
@@ -27,10 +27,6 @@ jobs:
|
||||
name: Windows installation policy (no app build)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -30,10 +30,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
|
||||
Generated
+17
-18
@@ -614,7 +614,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-io",
|
||||
@@ -645,7 +645,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-cli"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-io",
|
||||
@@ -661,14 +661,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-color"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-constraints"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-doc",
|
||||
"cadcraft-geom",
|
||||
@@ -678,7 +678,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-doc"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-geom",
|
||||
@@ -689,7 +689,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-dwg"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"log",
|
||||
@@ -697,7 +697,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-dxf"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
@@ -706,7 +706,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-engine"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-constraints",
|
||||
@@ -727,7 +727,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-fonts"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-geom",
|
||||
@@ -740,14 +740,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-geom"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-io"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cadcraft-color",
|
||||
@@ -769,7 +769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-mcp"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-io",
|
||||
@@ -784,7 +784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-render"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-doc",
|
||||
@@ -798,7 +798,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-ui-egui"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-color",
|
||||
"cadcraft-doc",
|
||||
@@ -825,7 +825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadcraft-web"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadcraft-engine",
|
||||
"cadcraft-fonts",
|
||||
@@ -1214,7 +1214,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=6e03f65bf45181db77ba6229716e6624db9eb25b#6e03f65bf45181db77ba6229716e6624db9eb25b"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -1224,7 +1224,6 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"plist",
|
||||
"rustix 1.1.5",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5994,7 +5993,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
# default-members
|
||||
|
||||
[workspace.package]
|
||||
version = "0.20.1"
|
||||
version = "0.20.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -71,7 +71,7 @@ egui-wgpu = "0.37"
|
||||
# Monotonic time on native and web (performance.now on wasm).
|
||||
web-time = "1.1"
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] }
|
||||
rfd = "0.17"
|
||||
arboard = { version = "3.6", default-features = false }
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ 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)]
|
||||
@@ -309,11 +312,22 @@ 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) {
|
||||
if let Ok(id) = crate::add(d, c, &crate::SolveOptions::default()) {
|
||||
added.push(id);
|
||||
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(_) => {}
|
||||
}
|
||||
}
|
||||
added
|
||||
(added, false)
|
||||
}
|
||||
|
||||
@@ -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, infer, infer_placed};
|
||||
pub use auto::{INFER_TYPES, InferOptions, autoconstrain, autoconstrain_report, infer, infer_placed};
|
||||
pub use dof::{DofReport, DofStatus, ObjectDof, dof};
|
||||
pub use expr::{Env, ExprError};
|
||||
pub use model::{ref_points, rigid_points, supported};
|
||||
@@ -53,6 +53,14 @@ 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;
|
||||
|
||||
@@ -335,8 +343,14 @@ 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 self.built.len() <= MAX_CONFLICT_PROBES {
|
||||
if probe && self.built.len() <= MAX_CONFLICT_PROBES {
|
||||
for i in 0..self.built.len() {
|
||||
if let Ok(o) = self.run(Some(i))
|
||||
&& o.converged
|
||||
@@ -741,7 +755,14 @@ 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, mut c: Constraint, opts: &SolveOptions) -> Result<u32, SolveError> {
|
||||
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> {
|
||||
if d.constraints.len() >= MAX_CONSTRAINTS {
|
||||
return Err(SolveError::Other("too many constraints".into()));
|
||||
}
|
||||
@@ -777,10 +798,24 @@ pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u3
|
||||
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.
|
||||
let mut ids = comp.conflicts(&o.x);
|
||||
// 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);
|
||||
ids.retain(|i| *i != id);
|
||||
ids.insert(0, id);
|
||||
return Err(SolveError::Conflict { ids });
|
||||
@@ -819,6 +854,10 @@ pub fn add(d: &mut Drawing, mut c: Constraint, opts: &SolveOptions) -> Result<u3
|
||||
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);
|
||||
|
||||
@@ -516,3 +516,41 @@ 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}");
|
||||
}
|
||||
|
||||
@@ -144,10 +144,10 @@ impl BlockEdit {
|
||||
}
|
||||
let host = &self.host.doc;
|
||||
let mut d = editor.clone();
|
||||
// When saving a copy, the source keeps its last saved definition. The editor's own
|
||||
// table entry can still be the one from before BSAVE; the copy gets the edited data.
|
||||
// When the block is saved under another name, the edited block keeps the drawing's
|
||||
// description and block `name` gets the description changed in the editor.
|
||||
if !name.eq_ignore_ascii_case(&self.name) {
|
||||
restore_saved_definition(&mut d, host, &self.name);
|
||||
keep_description(&mut d, host, &self.name);
|
||||
}
|
||||
d.model = host.model.clone();
|
||||
d.layouts = host.layouts.clone();
|
||||
@@ -176,13 +176,16 @@ impl BlockEdit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the source block's saved objects and metadata when the edited objects are saved as a copy.
|
||||
/// Without a saved source, drop the empty definition an editor description may have created.
|
||||
fn restore_saved_definition(d: &mut Drawing, host: &Drawing, name: &str) {
|
||||
/// Give block `name` of `d` the description `host` has for it, or, when `host` has no such block,
|
||||
/// drop the empty definition a description gives a block the editor started and never saved.
|
||||
fn keep_description(d: &mut Drawing, host: &Drawing, name: &str) {
|
||||
match host.block(name) {
|
||||
Some(h) => {
|
||||
let key = d.blocks.keys().find(|key| key.eq_ignore_ascii_case(name)).cloned().unwrap_or_else(|| h.name.clone());
|
||||
d.blocks.insert(key, h.clone());
|
||||
if let Some((_, b)) = d.blocks.iter_mut().find(|(k, _)| k.eq_ignore_ascii_case(name))
|
||||
&& b.description != h.description
|
||||
{
|
||||
Arc::make_mut(b).description = h.description.clone();
|
||||
}
|
||||
}
|
||||
None => d.blocks.retain(|k, b| !(k.eq_ignore_ascii_case(name) && b.entities.is_empty())),
|
||||
}
|
||||
@@ -299,7 +302,7 @@ impl DocState {
|
||||
let was = be.name.clone();
|
||||
let reconcile = |doc: &mut Arc<Drawing>| {
|
||||
let editor = Arc::make_mut(doc);
|
||||
restore_saved_definition(editor, &d, &was);
|
||||
keep_description(editor, &d, &was);
|
||||
editor.blocks.retain(|k, _| !k.eq_ignore_ascii_case(&name));
|
||||
editor.blocks.insert(name.clone(), b.clone());
|
||||
};
|
||||
|
||||
@@ -821,11 +821,15 @@ 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 = cs::autoconstrain(&mut d, &hs, &o);
|
||||
let (ids, stopped) = cs::autoconstrain_report(&mut d, &hs, &o);
|
||||
if !ids.is_empty() {
|
||||
*s.doc_mut()? = d;
|
||||
}
|
||||
Ok(json!({ "added": ids.len(), "ids": ids, "message": format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len()) }))
|
||||
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 }))
|
||||
}
|
||||
|
||||
fn run_delconstraint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
//! Saving an edited block as a copy must retain its source's last saved definition.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
/// Add a third line to a block and BSAVE it, BSAVEAS a copy, then edit and save the copy: the
|
||||
/// source block keeps the definition of its own last save.
|
||||
#[test]
|
||||
fn save_as_preserves_the_last_saved_source_definition() {
|
||||
let mut s = Session::new();
|
||||
let mut handles = Vec::new();
|
||||
for y in [5, 9] {
|
||||
let line = s.execute("line", &json!({ "points": [[5, y], [15, y]] })).unwrap();
|
||||
handles.push(line["handles"][0].clone());
|
||||
}
|
||||
s.execute("block", &json!({ "name": "Frame", "base": [5, 5], "handles": handles, "keep": "delete" })).unwrap();
|
||||
s.execute("insert", &json!({ "name": "Frame", "at": [100, 20] })).unwrap();
|
||||
s.execute("bedit", &json!({ "name": "Frame" })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().model.len(), 2);
|
||||
|
||||
s.execute("line", &json!({ "points": [[0, 3], [3, 3]] })).unwrap();
|
||||
s.execute("bsave", &json!({})).unwrap();
|
||||
let saved_source = s.state().unwrap().drawing().block("Frame").unwrap().as_ref().clone();
|
||||
assert_eq!(saved_source.entities.len(), 3);
|
||||
s.execute("bsaveas", &json!({ "name": "Copy" })).unwrap();
|
||||
assert_eq!(s.state().unwrap().block_editor(), Some("Copy"));
|
||||
assert_eq!(s.state().unwrap().drawing().block("Frame").unwrap().as_ref(), &saved_source, "copy composition keeps the saved source");
|
||||
|
||||
// A later save of Copy must not reintroduce the editor table's older source definition.
|
||||
s.execute("line", &json!({ "points": [[0, 6], [4, 6]] })).unwrap();
|
||||
s.execute("bsave", &json!({})).unwrap();
|
||||
s.execute("bclose", &json!({ "save": false })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().block("Frame").unwrap().as_ref(), &saved_source, "editor reconciliation keeps the saved source");
|
||||
assert_eq!(s.doc().unwrap().block("Copy").unwrap().entities.len(), 4, "the copy keeps its own later edit");
|
||||
s.execute("bedit", &json!({ "name": "Frame" })).unwrap();
|
||||
assert_eq!(s.doc().unwrap().model.len(), 3);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//! 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,8 +17,6 @@ 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,8 +18,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user