mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Merge pull request #702 from storytold/pr/land-698-699
Land #698, #699: a saved multi-artboard .ai reopens as its document; FreeBSD hexagon test
This commit is contained in:
@@ -185,7 +185,12 @@ fn polygon_rotation(corners: &[Point], step: f64) -> f64 {
|
||||
})
|
||||
.sum::<f64>()
|
||||
};
|
||||
[0.0, 90.0, 180.0, 270.0].into_iter().filter(|r| r % step == 0.0).min_by(|a, b| error(*a).total_cmp(&error(*b))).unwrap_or(0.0)
|
||||
// A candidate a whole number of periods from an earlier one is the same polygon (a hexagon at 270°
|
||||
// is the one at 90°) and scores the same up to rounding, so it is dropped: otherwise the platform's
|
||||
// `atan2` rounding picks between them (FreeBSD's libm gave 270° where Linux gives 90°).
|
||||
let candidates = [0.0, 90.0, 180.0, 270.0].into_iter().filter(|r| r % step == 0.0);
|
||||
let distinct = candidates.clone().enumerate().filter(|&(i, r)| !candidates.clone().take(i).any(|q| (r - q) % period == 0.0));
|
||||
distinct.map(|(_, r)| r).min_by(|a, b| error(*a).total_cmp(&error(*b))).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn rectangle_rotation(corners: &[Point]) -> f64 {
|
||||
@@ -533,6 +538,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hexagon_rotation_never_depends_on_rounding_between_equivalent_angles() {
|
||||
// 0° and 180° (and 90° and 270°) are the same hexagon, so only 0° and 90° may come back, whatever
|
||||
// rounding does to the tied scores: on FreeBSD the tilted test above got 270°.
|
||||
for direction in [0.0_f64, 90.0] {
|
||||
for tilt in [-9.0_f64, -7.0, -3.0, 3.0, 7.0, 9.0] {
|
||||
for offset in [0.0, 1e-7, 0.1, 1e3, 1e6] {
|
||||
let corners: Vec<Point> = (0..6)
|
||||
.map(|i| {
|
||||
let a = (direction + tilt - 90.0).to_radians() + std::f64::consts::TAU * i as f64 / 6.0;
|
||||
Point::new(offset + 70.0 * a.cos(), offset + 70.0 * a.sin())
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(polygon_rotation(&corners, 90.0), direction, "tilt {tilt}, offset {offset}");
|
||||
let mut shifted = corners.clone();
|
||||
shifted.rotate_left(3);
|
||||
assert_eq!(polygon_rotation(&shifted, 90.0), direction, "tilt {tilt}, offset {offset}, from the opposite corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Triangles have no such twin among the candidates: 0° and 180° stay distinct.
|
||||
let down: Vec<Point> = (0..3).map(|i| Point::new(0.0, 0.0) + Vec2::from_angle((90.0_f64 + 120.0 * i as f64).to_radians()) * 50.0).collect();
|
||||
assert_eq!(polygon_rotation(&down, 180.0), 180.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_fit_ignores_sampling_density_and_document_offset() {
|
||||
let mut stroke = wobbly(&[Point::new(0.0, 0.0), Point::new(100.0, 8.0), Point::new(94.0, 100.0), Point::new(10.0, 91.0)], 24, 4.0);
|
||||
|
||||
@@ -114,11 +114,17 @@ pub(super) fn show_preview(ui: &mut egui::Ui, tex: Option<&egui::TextureHandle>,
|
||||
|
||||
/// Open `bytes` (or place them with the `file.place` params `place`) through the dialog when the
|
||||
/// file is a PDF (or `.ai`/`.ait`) with several pages or a password; false when there's nothing to
|
||||
/// ask (the caller goes on).
|
||||
/// ask (the caller goes on), including a file that carries its VectorCraft document when opening.
|
||||
pub fn offer(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<String>, place: Option<&Value>) -> bool {
|
||||
if !fileio::detect(name, bytes).is_some_and(|f| matches!(f.id, "pdf" | "ai" | "ait")) {
|
||||
return false;
|
||||
}
|
||||
// A file carrying its VectorCraft document (a saved .ai, or a PDF saved with Preserve Editing)
|
||||
// opens as that document, every artboard and layer: there are no pages to pick. Placing still
|
||||
// asks, since a place takes one page.
|
||||
if place.is_none() && vectorcraft_pdf::editing_with(bytes, None).is_some() {
|
||||
return false;
|
||||
}
|
||||
let (pages, locked) = match vectorcraft_pdf::info(bytes, None) {
|
||||
Ok(i) if i.pages.len() > 1 => (i.pages.len(), false),
|
||||
Err(PdfError::NeedsPassword) => (0, true),
|
||||
|
||||
@@ -165,3 +165,35 @@ fn placing_a_pdf_page_asks_for_the_page_and_crop_box() {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(placed(&app).1.width(), 80.0);
|
||||
}
|
||||
|
||||
/// A saved `.ai` with several artboards carries its VectorCraft document: opening it restores that
|
||||
/// document (artboards, layers) instead of asking which pages to import.
|
||||
#[test]
|
||||
fn opening_a_saved_ai_with_several_artboards_restores_it_without_asking() {
|
||||
let mut app = app();
|
||||
app.run("artboard.new", json!({"width": 200, "height": 100, "name": "Second"})).unwrap();
|
||||
app.run("layer.new", json!({"name": "Front"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let saved = app.run("document.save", json!({"format": "ai"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
|
||||
assert!(vectorcraft_pdf::info(&bytes, None).unwrap().pages.len() > 1, "a page per artboard");
|
||||
|
||||
io::open_bytes(&mut app, "two.ai", &bytes, Some("/tmp/two.ai".into())).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "no Import PDF dialog");
|
||||
assert_eq!(app.session.documents().len(), 2);
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
assert_eq!(doc.artboards.len(), 2);
|
||||
assert_eq!(doc.artboards[1].name, "Second");
|
||||
assert!(doc.layers.iter().any(|l| l.name.as_deref() == Some("Front")), "layers come back");
|
||||
}
|
||||
|
||||
/// A PDF without a VectorCraft document still asks, and placing a saved `.ai` still asks for the page.
|
||||
#[test]
|
||||
fn placing_a_saved_ai_still_asks_for_the_page() {
|
||||
let mut app = app();
|
||||
app.run("artboard.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let saved = app.run("document.save", json!({"format": "ai"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
|
||||
assert!(import_pdf::offer(&mut app, "two.ai", &bytes, None, Some(&json!({}))));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("mode"), "place");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user