mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-30 13:56:46 -06:00
* xtask: the test suite and clippy pass on Windows
Two Windows-only failures stopped `cargo xtask ci` before it got going on
a Windows checkout (CI runs the gate on Linux only, so neither showed):
- parity::tests::civil_dates built a SystemTime 115,740,740 days after
the epoch (year 318857). Windows' SystemTime ends in the year 30828, so
the addition panicked ("overflow when adding duration to `SystemTime`").
That case now checks `civil_from_days` on the day count, which is what
it was about, on every platform.
- stats.rs imported `walk` for a test that only exists on Unix, so the
import was unused on Windows and `clippy -D warnings` refused it. The
Unix test calls `super::walk`, as its neighbour calls `super::walk_inner`.
Checked on Windows 11 x64: `cargo test --workspace` and
`cargo clippy --workspace --all-targets -- -D warnings` both pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refresh attribute baselines after value changes
CC-046: invalidate the derived XY baseline for changed justified attribute
values, including small changes inside the writer's imported-font tolerance.
Preserve no-op placement, alignment anchors and Aligned/Fit endpoints.
Regression reproduced on current upstream; all three focused tests pass.
Full cargo xtask ci passed all seven stages on Windows (2558 passing test
executions, zero failures, one ignored). Release Trunk web build passed.
Browser interaction verification is pending user dismissal of a Windows
Security prompt; do not treat the build as a runtime pass.
Windows validation prerequisite: original upstream PR #2198 commit a2dd814.
Independent of editor/ribbon changes; reverting restores only this setter,
its regression tests and corresponding file-format parity record.
* fix(io): preserve insertion elevation when refreshing text baselines
CC-046 review follow-up to PR #2214 comment 4377706777. Regression reproduced insertion Z 9 becoming 7. Preserve group 10 Z; test DXF independent elevations and document the DWG bridge shared elevation. Full cargo xtask ci passes all seven gates. This separate commit is reversible; reverting it restores elevation loss during baseline recomputation. Existing browser UI verification predates this writer correction.
* test(engine): cover global attribute no-ops and two-point exports
CC-046 review 5653054817: verify equal-value global JSON and scripted
-ATTEDIT preserve imported placement, and Aligned/Fit DXF save/reopen
retain both placement points and justification.
Validation: cargo xtask ci passed all seven gates on this branch;
git diff --check passed. Test-only follow-up, no application change.
Depends on CC-046 and unchanged Windows prerequisite #2198.
Revert this commit independently to remove only these assertions.
Runtime evidence remains separately recorded for the production commits.
---------
Co-authored-by: 2ez4gcx <108074836+2ez4gcx@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
9.2 KiB
Rust
204 lines
9.2 KiB
Rust
//! Small value changes must not export an imported justified attribute's old baseline.
|
|
use cadcraft_doc::{Attrib, Drawing, EntityKind, HAlign, Handle, Text, VAlign};
|
|
use cadcraft_engine::{Input, Session};
|
|
use cadcraft_geom::Vec3;
|
|
use serde_json::json;
|
|
|
|
const BEFORE: &str = "POWER ADAPTER";
|
|
const AFTER: &str = "POWER ADAPTER.";
|
|
|
|
fn fixture() -> (Session, Handle, Handle) {
|
|
let mut s = Session::new();
|
|
for (tag, value, y) in [("T", BEFORE, 15), ("U", "ADPT-01", 0)] {
|
|
s.execute("attdef", &json!({"tag":tag,"default":value,"at":[0,y],"height":3})).unwrap();
|
|
}
|
|
s.execute("line", &json!({"points":[[0,-5],[40,-5]]})).unwrap();
|
|
let handles: Vec<_> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
|
|
s.execute("block", &json!({"name":"Equipment","handles":handles,"base":[0,0],"keep":"delete"})).unwrap();
|
|
let a = s.execute("insert", &json!({"name":"Equipment","at":[100,200],"scale":2,"rotation":30})).unwrap();
|
|
let b = s.execute("insert", &json!({"name":"Equipment","at":[300,200],"attribs":{"T":"Other"}})).unwrap();
|
|
let a = Handle::parse_hex(a["handle"].as_str().unwrap()).unwrap();
|
|
let b = Handle::parse_hex(b["handle"].as_str().unwrap()).unwrap();
|
|
// Set existing document fields directly: this regression does not require Text Options.
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(a, |e| {
|
|
let EntityKind::Insert(i) = &mut e.kind else { panic!("insert") };
|
|
let t = &mut i.attribs[0].text;
|
|
t.insert.z = 7.0;
|
|
t.align_pt = Some(t.insert);
|
|
t.halign = HAlign::Center;
|
|
t.valign = VAlign::Top;
|
|
})
|
|
.unwrap();
|
|
let imported = cadcraft_io::read_dxf(cadcraft_io::write_dxf(s.doc().unwrap()).as_bytes()).unwrap();
|
|
let mut s = Session::empty();
|
|
s.open_drawing(imported, "Imported", None);
|
|
(s, a, b)
|
|
}
|
|
|
|
fn attributes(d: &Drawing, h: Handle) -> Vec<Attrib> {
|
|
let EntityKind::Insert(i) = &d.entity(h).unwrap().kind else { panic!("insert") };
|
|
i.attribs.clone()
|
|
}
|
|
|
|
fn baseline(d: &Drawing, t: &Text) -> cadcraft_geom::Vec2 {
|
|
let style = d.text_style(&t.style).unwrap();
|
|
let (h, v) = cadcraft_render::text_align(t.halign, t.valign);
|
|
cadcraft_fonts::baseline_start(
|
|
&cadcraft_fonts::TextFont::resolve(&style.font),
|
|
&t.value,
|
|
&cadcraft_fonts::TextParams {
|
|
insert: t.insert.xy(),
|
|
align_pt: t.align_pt.map(|p| p.xy()),
|
|
height: t.height,
|
|
rotation: t.rotation,
|
|
width_factor: t.width_factor,
|
|
oblique: t.oblique,
|
|
h,
|
|
v,
|
|
backwards: style.backwards,
|
|
upside_down: style.upside_down,
|
|
},
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn all_attribute_value_paths_refresh_small_justified_export_changes() {
|
|
for path in ["json", "interactive", "global-json", "global-interactive"] {
|
|
let (mut s, a, b) = fixture();
|
|
let before = attributes(s.doc().unwrap(), a);
|
|
let other = attributes(s.doc().unwrap(), b);
|
|
let definition = s.doc().unwrap().block("Equipment").unwrap().clone();
|
|
match path {
|
|
"json" => {
|
|
s.execute("attedit", &json!({"handle":a.hex(),"values":{"t":AFTER}})).unwrap();
|
|
}
|
|
"interactive" => {
|
|
s.start("attedit").unwrap();
|
|
s.input(Input::Pick(vec![a])).unwrap();
|
|
s.input(Input::Text(AFTER.into())).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
assert!(s.running.is_none());
|
|
}
|
|
"global-json" => {
|
|
s.execute("-attedit", &json!({"handles":[a.hex()],"tag":"T","find":"ADAPTER","replace":"ADAPTER."})).unwrap();
|
|
}
|
|
"global-interactive" => {
|
|
s.script("-attedit\nn\nn\nEquipment\nT\n\nADAPTER\nADAPTER.\n").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
}
|
|
_ => panic!("path"),
|
|
}
|
|
let edited = attributes(s.doc().unwrap(), a);
|
|
assert_eq!(edited[0].text.value, AFTER, "{path}");
|
|
assert_eq!(edited[0].text.align_pt, before[0].text.align_pt);
|
|
assert_eq!(edited[0].text.insert.z, 7.0);
|
|
assert_eq!(edited[1], before[1]);
|
|
assert_eq!(attributes(s.doc().unwrap(), b), other);
|
|
assert_eq!(s.doc().unwrap().block("Equipment").unwrap(), &definition);
|
|
let expected = baseline(s.doc().unwrap(), &edited[0].text);
|
|
assert!(expected.dist(before[0].text.insert.xy()) > 0.01, "fixture must change width");
|
|
for name in ["edited.dxf", "edited.dwg"] {
|
|
let reopened = cadcraft_io::read(&cadcraft_io::write(s.doc().unwrap(), name).unwrap(), name).unwrap();
|
|
let restored = attributes(&reopened, a);
|
|
assert!(
|
|
restored[0].text.insert.xy().dist(expected) < 1e-8,
|
|
"{path} {name}: stale {:?}, expected {expected:?}",
|
|
restored[0].text.insert.xy()
|
|
);
|
|
assert_eq!(restored[0].text.align_pt, before[0].text.align_pt);
|
|
assert_eq!(restored[0].text.value, AFTER);
|
|
assert_eq!(restored[0].text.insert.z, 7.0);
|
|
}
|
|
s.undo().unwrap();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), before, "{path} undo");
|
|
s.redo().unwrap();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), edited, "{path} redo");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn equal_attribute_values_preserve_an_imported_font_baseline() {
|
|
let (mut s, a, _) = fixture();
|
|
// A small imported-font offset must survive a no-op edit (writer tolerance is deliberate).
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(a, |e| {
|
|
let EntityKind::Insert(i) = &mut e.kind else { panic!("insert") };
|
|
i.attribs[0].text.insert.x += 0.1;
|
|
})
|
|
.unwrap();
|
|
let before = attributes(s.doc().unwrap(), a);
|
|
s.execute("attedit", &json!({"handle":a.hex(),"values":{"T":BEFORE}})).unwrap();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), before);
|
|
s.start("attedit").unwrap();
|
|
s.input(Input::Pick(vec![a])).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), before);
|
|
s.execute("-attedit", &json!({"handles":[a.hex()],"tag":"T","find":"ADAPTER","replace":"ADAPTER"})).unwrap();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), before, "global JSON no-op");
|
|
s.script("-attedit\nn\nn\nEquipment\nT\n\nADAPTER\nADAPTER\n").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(attributes(s.doc().unwrap(), a), before, "global interactive no-op");
|
|
}
|
|
|
|
/// Recomputing a justified baseline must not replace its independent insertion elevation.
|
|
#[test]
|
|
fn value_edits_preserve_distinct_insertion_and_alignment_elevations_on_export() {
|
|
let (mut s, a, _) = fixture();
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(a, |e| {
|
|
let EntityKind::Insert(i) = &mut e.kind else { panic!("insert") };
|
|
i.attribs[0].text.insert.z = 9.0;
|
|
})
|
|
.unwrap();
|
|
let anchor = attributes(s.doc().unwrap(), a)[0].text.align_pt;
|
|
s.execute("attedit", &json!({"handle":a.hex(),"values":{"T":AFTER}})).unwrap();
|
|
for name in ["elevations.dxf", "elevations.dwg"] {
|
|
let reopened = cadcraft_io::read(&cadcraft_io::write(s.doc().unwrap(), name).unwrap(), name).unwrap();
|
|
let restored = attributes(&reopened, a);
|
|
assert_eq!(restored[0].text.insert.z, 9.0, "{name}: insertion elevation");
|
|
assert_eq!(restored[0].text.align_pt.map(|p| p.xy()), anchor.map(|p| p.xy()), "{name}: alignment XY");
|
|
if name.ends_with(".dxf") {
|
|
assert_eq!(restored[0].text.align_pt, anchor, "DXF retains independent alignment elevation");
|
|
} else {
|
|
// The DWG bridge uses one text elevation for both points.
|
|
assert_eq!(restored[0].text.align_pt.unwrap().z, 9.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn value_edits_preserve_left_baseline_and_two_point_placements() {
|
|
for horizontal in [HAlign::Left, HAlign::Aligned, HAlign::Fit] {
|
|
let (mut s, a, _) = fixture();
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(a, |e| {
|
|
let EntityKind::Insert(i) = &mut e.kind else { panic!("insert") };
|
|
let t = &mut i.attribs[0].text;
|
|
t.halign = horizontal;
|
|
t.valign = VAlign::Baseline;
|
|
t.insert = Vec3::new(10.0, 20.0, 7.0);
|
|
t.align_pt = (horizontal != HAlign::Left).then_some(Vec3::new(40.0, 30.0, 9.0));
|
|
})
|
|
.unwrap();
|
|
let before = attributes(s.doc().unwrap(), a);
|
|
s.execute("attedit", &json!({"handle":a.hex(),"values":{"T":AFTER}})).unwrap();
|
|
let mut expected = before;
|
|
expected[0].text.value = AFTER.into();
|
|
assert_eq!(attributes(s.doc().unwrap(), a), expected);
|
|
if horizontal != HAlign::Left {
|
|
let reopened = cadcraft_io::read_dxf(cadcraft_io::write_dxf(s.doc().unwrap()).as_bytes()).unwrap();
|
|
let restored = attributes(&reopened, a);
|
|
assert_eq!(restored[0].text.insert, expected[0].text.insert);
|
|
assert_eq!(restored[0].text.align_pt, expected[0].text.align_pt);
|
|
assert_eq!(restored[0].text.halign, horizontal);
|
|
assert_eq!(restored[0].text.value, AFTER);
|
|
}
|
|
}
|
|
}
|