Merge pull request #6912 from dotCooCoo/fix/new-document-side-limit

New Document: file.new and artboard.new take the same largest artboard side
This commit is contained in:
BFlat
2026-11-12 23:54:11 +07:00
committed by GitHub
7 changed files with 86 additions and 11 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New…",
["File"],
None,
"{preset?: a name from file.newPresets (start from it; the other params change it), name?|title?: document name (default Untitled-N), width?: pt=612|\"210 mm\", height?: pt=792, units?: \"Pixels\"|\"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Feet\"|\"Yards\"|\"Meters\"|\"Feet & Inches\" (default: prefs unitsGeneral, as print presets; screen presets: Pixels), orientation?: \"portrait\"|\"landscape\" (swaps width and height to match), artboards?: n (1–1000), artboardLayout?: {layout?: \"gridByRow\"|\"gridByColumn\"|\"row\"|\"column\", columns?: n (default: all in one row), spacing?: pt=20, rightToLeft?: bool}, bleed?: pt|[top, bottom, left, right]|{top?, …} (0–72 pt), backgroundContents?: \"transparent\"|\"white\" (a white artboard background, not an object), colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK), rasterEffectsPpi?: 72|150|300 (1–2400), previewMode?: \"default\"|\"pixel\"|\"overprint\" (overprint turns Overprint Preview on, pixel Pixel Preview in the app; default leaves both), alignNewObjectsToPixelGrid?: bool (Advanced Options › Align New Objects to Pixel Grid: paths drawn in the document are pixel-aligned; default on for the Mobile and Web presets, else off; document.setup changes it later), created?: Unix seconds|null (File Info's created date; default now, recorded in the journal so a replay matches), size?: a preset's size label (a preset file.newPresets lists can be passed back; ignored)} → {index, previewMode}; the size is remembered in file.newPresets' Recent",
"{preset?: a name from file.newPresets (start from it; the other params change it), name?|title?: document name (default Untitled-N), width?: pt=612|\"210 mm\", height?: pt=792 (each more than 0 and at most 3,999,856 pt, as artboard.new takes them), units?: \"Pixels\"|\"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Feet\"|\"Yards\"|\"Meters\"|\"Feet & Inches\" (default: prefs unitsGeneral, as print presets; screen presets: Pixels), orientation?: \"portrait\"|\"landscape\" (swaps width and height to match), artboards?: n (1–1000), artboardLayout?: {layout?: \"gridByRow\"|\"gridByColumn\"|\"row\"|\"column\", columns?: n (default: all in one row), spacing?: pt=20, rightToLeft?: bool}, bleed?: pt|[top, bottom, left, right]|{top?, …} (0–72 pt), backgroundContents?: \"transparent\"|\"white\" (a white artboard background, not an object), colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK), rasterEffectsPpi?: 72|150|300 (1–2400), previewMode?: \"default\"|\"pixel\"|\"overprint\" (overprint turns Overprint Preview on, pixel Pixel Preview in the app; default leaves both), alignNewObjectsToPixelGrid?: bool (Advanced Options › Align New Objects to Pixel Grid: paths drawn in the document are pixel-aligned; default on for the Mobile and Web presets, else off; document.setup changes it later), created?: Unix seconds|null (File Info's created date; default now, recorded in the journal so a replay matches), size?: a preset's size label (a preset file.newPresets lists can be passed back; ignored)} → {index, previewMode}; the size is remembered in file.newPresets' Recent",
always,
super::newdoc::file_new
),
+8 -1
View File
@@ -128,7 +128,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Artboard",
["Window", "Artboards"],
None,
"{x?, y?, width?, height?, name?, color?: color (its background color, as artboard.setProps takes it, #rrggbbaa and #rgba refused; default: none), guideLabel?: name | \"#rrggbb\" | null (its guide label, as artboard.setProps takes it; default: none), showCenterMark?: bool, showCrossHairs?: bool, showVideoSafeAreas?: bool (as artboard.setProps takes them; default false), videoRulerPixelAspectRatio?: number 0.1–10 (as artboard.setProps takes it; default 1), side?: \"left\"|\"right\"|\"top\"|\"bottom\" (the size of artboard `beside`, 20 pt from that side of it; refused with x, y, width or height), beside?: n (the artboard `side` places it by; default the last)} (default place: right of the last)",
"{x?, y?, width?, height? (each more than 0 and at most 3,999,856 pt, as file.new takes them), name?, color?: color (its background color, as artboard.setProps takes it, #rrggbbaa and #rgba refused; default: none), guideLabel?: name | \"#rrggbb\" | null (its guide label, as artboard.setProps takes it; default: none), showCenterMark?: bool, showCrossHairs?: bool, showVideoSafeAreas?: bool (as artboard.setProps takes them; default false), videoRulerPixelAspectRatio?: number 0.1–10 (as artboard.setProps takes it; default 1), side?: \"left\"|\"right\"|\"top\"|\"bottom\" (the size of artboard `beside`, 20 pt from that side of it; refused with x, y, width or height), beside?: n (the artboard `side` places it by; default the last)} (default place: right of the last)",
has_doc,
artboard_new
),
@@ -926,6 +926,9 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result<Value> {
let guide_label = artboard_guide_label(p, "artboard.new")?.flatten();
check_switches(p, &["showCenterMark", "showCrossHairs", "showVideoSafeAreas"], "artboard.new")?;
check_numbers(p, &["x", "y", "width", "height"], "artboard.new")?;
if ["width", "height"].iter().filter_map(|k| p.get(*k).and_then(Value::as_f64)).any(|side| !super::newdoc::side_in_range(side)) {
return Err(super::newdoc::side_error("artboard.new", d0.units));
}
let r = match artboard_side(p, "artboard.new")? {
Some(side) => {
if ["x", "y", "width", "height"].iter().any(|k| p.get(*k).is_some_and(|v| !v.is_null())) {
@@ -941,6 +944,10 @@ fn artboard_new(s: &mut Session, p: &Value) -> Result<Value> {
None if p.get("beside").is_some_and(|v| !v.is_null()) => return Err(bad("artboard.new", "`beside` needs `side`")),
None => rect_from(p, def),
};
// A size taken from another artboard (`side`, or width or height left out) keeps to the limit too.
if !super::newdoc::side_in_range(r.width()) || !super::newdoc::side_in_range(r.height()) {
return Err(super::newdoc::side_error("artboard.new", d0.units));
}
let given = artboard_str(p, "name", "artboard.new")?.map(str::trim).filter(|n| !n.is_empty());
let (show_center_mark, show_cross_hairs) = (bool_or(p, "showCenterMark", false), bool_or(p, "showCrossHairs", false));
let show_video_safe_areas = bool_or(p, "showVideoSafeAreas", false);
+25 -4
View File
@@ -241,8 +241,8 @@ impl DocSettings {
if let Some(h) = length("height", self.units)? {
self.height = h;
}
if !(self.width > 0.0 && self.height > 0.0) || self.width > MAX_SIDE || self.height > MAX_SIDE {
return Err(bad(cmd, "width/height must be positive and within the canvas"));
if !side_in_range(self.width) || !side_in_range(self.height) {
return Err(side_error(cmd, self.units));
}
match p.get("orientation").and_then(Value::as_str) {
None => {}
@@ -359,8 +359,29 @@ impl DocSettings {
}
}
/// Largest artboard side `file.new` accepts, points.
const MAX_SIDE: f64 = 16383.0 * 10.0;
/// Largest artboard side `file.new` and `artboard.new` accept, points: the span of the canvas
/// ([`crate::MAX_COORD`] from the origin) less the largest bleed on both sides, 3,999,856 pt.
pub(crate) const MAX_SIDE: f64 = crate::MAX_COORD - 2.0 * vectorcraft_doc::setup::MAX_BLEED;
/// Is `side` an artboard side `file.new` and `artboard.new` accept: more than 0 and at most [`MAX_SIDE`]?
pub(crate) fn side_in_range(side: f64) -> bool {
side > 0.0 && side <= MAX_SIDE
}
/// The error of an artboard side outside [`side_in_range`]: the limit in points, and in `units` as
/// well when they aren't points, rounded down to the decimals [`Unit::number`] shows so the value
/// shown is one the commands take.
pub(crate) fn side_error(cmd: &str, units: Unit) -> crate::EngineError {
let limit = match units {
Unit::Points => format!("{MAX_SIDE} pt"),
u => {
let step = if matches!(u, Unit::Pixels | Unit::Picas | Unit::Q | Unit::Ha) { 1e3 } else { 1e4 };
let shown = (u.from_pt(MAX_SIDE) * step).floor() / step;
format!("{MAX_SIDE} pt ({} {})", u.number(u.to_pt(shown)), u.suffix())
}
};
bad(cmd, format!("width and height must be more than 0 and at most {limit}"))
}
/// Largest artboard spacing, points.
const MAX_SPACING: f64 = 1.0e5;
+45 -2
View File
@@ -233,7 +233,7 @@ fn a_saved_preset_with_values_out_of_range_reads_in_range() {
saved,
[
(1.0, 1.0, 1.0, 0.0, vec![0.0, 0.0, 0.0, 9.0]),
(163830.0, 163830.0, 1.0, 1.0e5, vec![72.0, 72.0, 72.0, 0.0]),
(3_999_856.0, 200000.0, 1.0, 1.0e5, vec![72.0, 72.0, 72.0, 0.0]),
(10.0, 10.0, 1000.0, 0.0, vec![0.0; 4]),
]
);
@@ -242,7 +242,7 @@ fn a_saved_preset_with_values_out_of_range_reads_in_range() {
s.execute("file.new", &json!({"preset": "Low"})).unwrap();
assert_eq!((boards(&s), s.doc().unwrap().doc.setup.bleed), (vec![Rect::new(0.0, 0.0, 1.0, 1.0)], [0.0, 0.0, 0.0, 9.0]));
s.execute("file.new", &json!({"preset": "High"})).unwrap();
assert_eq!((boards(&s), s.doc().unwrap().doc.setup.bleed), (vec![Rect::new(0.0, 0.0, 163830.0, 163830.0)], [72.0, 72.0, 72.0, 0.0]));
assert_eq!((boards(&s), s.doc().unwrap().doc.setup.bleed), (vec![Rect::new(0.0, 0.0, 3_999_856.0, 200000.0)], [72.0, 72.0, 72.0, 0.0]));
s.execute("file.new", &json!({"preset": "Many"})).unwrap();
assert_eq!(boards(&s).len(), 1000);
let custom = presets(&mut s, "Recent").into_iter().find(|p| p["name"] == "Custom").unwrap();
@@ -250,6 +250,49 @@ fn a_saved_preset_with_values_out_of_range_reads_in_range() {
assert_eq!(boards(&s).iter().map(|r| r.x0).collect::<Vec<_>>(), [0.0, 100.0, 200.0], "side by side, not overlapping");
}
/// `file.new` and `artboard.new` take the same largest artboard side (the canvas less the largest
/// bleed on both sides) and refuse a side just past it, or of 0 or less, with an error that names
/// the limit in points and, when the document's units aren't points, in those units too.
#[test]
fn file_new_and_artboard_new_take_the_same_largest_side() {
let max = 3_999_856.0;
let in_meters = "at most 3999856 pt (1411.0603 m)";
let s = new_doc(json!({"width": max, "height": 170078}));
assert_eq!(boards(&s), [Rect::new(0.0, 0.0, max, 170078.0)]);
let mut s = Session::new();
let err = s.execute("file.new", &json!({"width": max + 1.0, "height": 100})).unwrap_err().to_string();
assert!(err.contains("width and height must be more than 0 and at most 3999856 pt") && !err.contains('('), "{err}");
let err = s.execute("file.new", &json!({"width": 100, "height": "1412 m", "units": "Meters"})).unwrap_err().to_string();
assert!(err.contains(in_meters), "{err}");
// The value in other units is rounded down, so it is one file.new takes.
let err = s.execute("file.new", &json!({"width": 100, "height": "56000 in", "units": "Inches"})).unwrap_err().to_string();
assert!(err.contains("at most 3999856 pt (55553.5555 in)"), "{err}");
assert!(s.execute("file.new", &json!({"width": 0, "height": 100})).is_err());
assert!(s.doc().is_err(), "no document was made");
s.execute("file.new", &json!({"width": "55553.5555 in", "height": 100, "units": "Inches"})).unwrap();
let mut s = new_doc(json!({"units": "Meters"}));
s.execute("artboard.new", &json!({"x": -2_000_000, "y": 0, "width": max, "height": 100})).unwrap();
assert_eq!(boards(&s)[1], Rect::new(-2_000_000.0, 0.0, max - 2_000_000.0, 100.0));
for p in [
json!({"x": -2_000_000, "y": 0, "width": max + 1.0, "height": 100}),
json!({"x": 0, "y": 2000, "width": 100, "height": max + 1.0}),
json!({"width": 0}),
json!({"height": -1}),
] {
let err = s.execute("artboard.new", &p).unwrap_err().to_string();
assert!(err.contains(in_meters), "{p}: {err}");
}
assert_eq!(boards(&s).len(), 2, "the refused artboards were not added");
// A size taken from a wider artboard is refused too.
s.execute("artboard.setProps", &json!({"index": 1, "x": -2_000_000, "y": 0, "width": max + 1.0, "height": 100})).unwrap();
for p in [json!({}), json!({"x": 0, "y": 5000, "height": 100}), json!({"side": "bottom", "beside": 1})] {
let err = s.execute("artboard.new", &p).unwrap_err().to_string();
assert!(err.contains(in_meters), "{p}: {err}");
}
assert_eq!(boards(&s).len(), 2);
}
#[test]
fn saved_presets_live_in_the_preferences() {
let mut s = Session::new();
+1 -1
View File
@@ -8144,7 +8144,7 @@ antiAlias Sharp शार्प
@msg width must be positive width: चौड़ाई सकारात्मक होना चाहिए
@msg width out of range सीमा से बाहर चौड़ाई
@msg width/height must be > 0 चौड़ाई / ऊंचाई होना चाहिए > 0
@msg width/height must be positive and within the canvas चौड़ाई / ऊंचाई सकारात्मक होना चाहिए और कैनवास के भीतर होना चाहिए
@msg width and height must be more than 0 and at most {limit} चौड़ाई और ऊंचाई 0 से अधिक और अधिकतम {limit} होनी चाहिए
@msg workspace name can't be empty कार्यक्षेत्र का नाम खाली नहीं हो सकता है
@msg x and y must be finite x और y को परिमित होना चाहिए
@msg x and y must be finite numbers x और y होना चाहिए परिमित संख्या
Can't render this file because it is too large.
+1 -1
View File
@@ -8394,7 +8394,7 @@ digits Hindi Hindi
@msg width must be positive width måste vara positiv
@msg width out of range bredden ligger utanför tillåtet omfång
@msg width/height must be > 0 bredd/höjd måste vara > 0
@msg width/height must be positive and within the canvas bredd/höjd måste vara positiva och inom arbetsytan
@msg width and height must be more than 0 and at most {limit} bredd och höjd måste vara större än 0 och högst {limit}
@msg x and y must be finite numbers x och y måste vara ändliga tal
@msg x and y must be given together x och y måste anges tillsammans
@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone behöver ett ankare: docka bredvid en panel med anchor eller i en ny dockkolumn med side (Start eller End)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+5 -1
View File
@@ -5496,7 +5496,11 @@ value outside that range is read as the nearer end), `previewMode` and `alignNew
`file.newPresets {category?}` lists the categories and presets (Recent: the last sizes used; Saved: the user's presets,
kept in the preferences by `file.newPresets.save` and removed by `file.newPresets.delete`). A listed preset can be
passed straight back to `file.new`. Print presets and sizes without `units` start in `unitsGeneral`; screen presets
(mobile, web, video, social) in Pixels. A saved or Recent preset whose size (up to 163,830 pt), `artboards` (1 to
(mobile, web, video, social) in Pixels. `file.new` and `artboard.new` take the same artboard sizes: each side more than 0
and at most 3,999,856 pt (the 4,000,000 pt the canvas reaches from the origin, less a 72 pt bleed on both sides). A side outside that range is
refused with `width and height must be more than 0 and at most 3999856 pt`, followed by the limit in the document's
units in parentheses when they are not points, such as `(1411.0603 m)`, and no document or artboard is made.
A saved or Recent preset whose size (up to 3,999,856 pt), `artboards` (1 to
1,000), spacing (0 to 100,000 pt) or bleed (0 to 72 pt a side) lies outside those ranges is read with the nearer end of
each range, and a size of 0 or less is read as 1 pt.
The desktop app's `file.newDialog` (Cmd/Ctrl+N) opens the New Document dialog on the first Recent entry, or on the