mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
Merge pull request #6514 from ruchirguitar/fix/5820-general-units-for-unitless-files
#5820: files that record no units open in Preferences › Units › General
This commit is contained in:
@@ -75,6 +75,7 @@ impl Document {
|
||||
dimensions,
|
||||
retired_artboard_id: _,
|
||||
opened_artboard: _,
|
||||
units_from_file: _,
|
||||
index: _,
|
||||
} = self;
|
||||
*version == other.version
|
||||
@@ -257,6 +258,7 @@ impl Document {
|
||||
dimensions: false,
|
||||
retired_artboard_id: Default::default(),
|
||||
opened_artboard: None,
|
||||
units_from_file: false,
|
||||
index: Default::default(),
|
||||
};
|
||||
let id = d.alloc_id();
|
||||
|
||||
@@ -339,6 +339,11 @@ pub struct Document {
|
||||
/// names it as the file's active artboard. `None`: the first. Native files do not keep it.
|
||||
#[serde(skip)]
|
||||
pub opened_artboard: Option<u32>,
|
||||
/// The file this document was read from recorded its units (a `.ai` file's
|
||||
/// `%AI5_RulerUnits`): it opens in them. A file that records none opens in Preferences ›
|
||||
/// Units › General (#5820). Native files do not keep it: they always keep their units.
|
||||
#[serde(skip)]
|
||||
pub units_from_file: bool,
|
||||
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
|
||||
/// is set, so documents without any pay nothing for them.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
|
||||
@@ -43,6 +43,22 @@ impl Loaded {
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// The file in the units of Preferences › Units › General when it records none of its own: a
|
||||
/// PDF, an EPS or `.ai` without ruler units, a raster image, a metafile, an Affinity file or
|
||||
/// what an importer plug-in reads, as the reference app opens such files (#5820). A native
|
||||
/// document (also one that an SVG, PDF, `.ai` or EPS file carries, `restored`), an SVG (the
|
||||
/// units of its root's size, pixels when it gives none), a DXF drawing (the unit it was read
|
||||
/// in) and a `.ai` or EPS with ruler units keep theirs. Only the units the document shows
|
||||
/// change, not its art.
|
||||
pub(crate) fn with_unit_prefs(mut self, prefs: &Prefs) -> Self {
|
||||
let records_units =
|
||||
self.restored || self.doc.units_from_file || matches!(self.format.id, "vectorcraft" | "template" | "svg" | "svgz" | "dxf");
|
||||
if !records_units {
|
||||
self.doc.units = crate::units::pref_unit(&prefs.units_general);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// An image ready to embed: PNG/JPEG/GIF/WebP keep their bytes, other formats are stored as PNG
|
||||
@@ -610,7 +626,8 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
/// Make a loaded file the new active document; `links` is what became of its linked files, and
|
||||
/// `opts` are the options it was read with.
|
||||
fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
|
||||
let Loaded { mut doc, format, warnings, restored, converted, native_pdf_compatible, .. } = loaded.with_grid_prefs(&s.prefs);
|
||||
let Loaded { mut doc, format, warnings, restored, converted, native_pdf_compatible, .. } =
|
||||
loaded.with_grid_prefs(&s.prefs).with_unit_prefs(&s.prefs);
|
||||
// EPS now has an ordinary Save path. Its preview, partial-program and damaged-editing
|
||||
// fallbacks must ask before overwriting the richer source they could not fully read.
|
||||
let losses: Vec<String> = warnings
|
||||
|
||||
@@ -1051,7 +1051,7 @@ fn can_revert(s: &Session) -> std::result::Result<(), String> {
|
||||
fn revert(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let index = s.active_index().ok_or(crate::EngineError::NoDocument)?;
|
||||
let path = s.doc()?.path.clone().ok_or_else(|| bad("file.revert", "the document has never been saved"))?;
|
||||
let Loaded { mut doc, native_pdf_compatible, .. } = load(&path, &read_file(&path)?)?.with_grid_prefs(&s.prefs);
|
||||
let Loaded { mut doc, native_pdf_compatible, .. } = load(&path, &read_file(&path)?)?.with_grid_prefs(&s.prefs).with_unit_prefs(&s.prefs);
|
||||
doc.template = false;
|
||||
// Back to the saved file: its recovery copy has nothing left to recover.
|
||||
let uid = s.doc()?.uid;
|
||||
|
||||
@@ -199,3 +199,78 @@ fn east_asian_type_unit_leaves_the_document_in_points() {
|
||||
assert!((lead.unwrap() - Unit::Ha.to_pt(20.0)).abs() < 1e-9, "{lead:?}");
|
||||
assert!((sz - 17.0 * 72.0 / 101.6).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A session whose Preferences › Units › General is `unit`, with no document open.
|
||||
fn general_in(unit: &str) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", &json!({"key": "unitsGeneral", "value": unit})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// `bytes` opened as `name` in `s` → the opened document's units.
|
||||
fn opened_units(s: &mut Session, name: &str, bytes: &[u8]) -> Unit {
|
||||
s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
|
||||
units(s)
|
||||
}
|
||||
|
||||
/// The active document of a session in `unit`, exported as `format`.
|
||||
fn exported(unit: &str, format: &str) -> Vec<u8> {
|
||||
let mut s = session();
|
||||
s.execute("document.setUnits", &json!({"units": unit})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 30})).unwrap();
|
||||
let data = s.execute("document.export", &json!({"format": format})).unwrap()["dataBase64"].clone();
|
||||
vectorcraft_format::base64_decode(data.as_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// A PostScript `.ai` with one empty layer and `header` among its comments.
|
||||
fn postscript_ai(header: &str) -> Vec<u8> {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0\n%%Creator: test\n%AI5_FileFormat 14.0\n{header}%AI3_Cropmarks: 0 0 200 100\n%%EndComments\n%%BeginProlog\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n(L) Ln\n0 A\n0 Xw\n\nLB\n%AI5_EndLayer--\n%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_that_record_no_units_open_in_the_general_unit() {
|
||||
let pdf = vectorcraft_testkit::pdf::pdf(&[vectorcraft_testkit::pdf::PdfPage::new(120.0, 60.0, "0 0 0 rg 10 10 50 30 re f")], None);
|
||||
let png = exported("Inches", "png");
|
||||
let ai = postscript_ai("");
|
||||
let mut s = general_in("Millimeters");
|
||||
assert_eq!(opened_units(&mut s, "plain.pdf", &pdf), Unit::Millimeters, "a PDF records no units (#5820)");
|
||||
assert_eq!(opened_units(&mut s, "image.png", &png), Unit::Millimeters, "nor does an image");
|
||||
assert_eq!(opened_units(&mut s, "old.ai", &ai), Unit::Millimeters, "nor a .ai without ruler units");
|
||||
// Only the units change: the page keeps its size in points.
|
||||
s.execute("document.open", &json!({"name": "plain.pdf", "dataBase64": vectorcraft_format::base64_encode(&pdf)})).unwrap();
|
||||
let board = s.doc().unwrap().doc.artboards[0].rect;
|
||||
assert!((board.width() - 120.0).abs() < 1e-6 && (board.height() - 60.0).abs() < 1e-6, "{board:?}");
|
||||
// With the preference left at points, they open in points as before.
|
||||
let mut p = general_in("Points");
|
||||
assert_eq!(opened_units(&mut p, "plain.pdf", &pdf), Unit::Points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_that_record_units_keep_them_whatever_the_general_unit() {
|
||||
let mut s = general_in("Millimeters");
|
||||
assert_eq!(opened_units(&mut s, "inches.ai", &postscript_ai("%AI5_RulerUnits: 0\n")), Unit::Inches, "a .ai's ruler units");
|
||||
assert_eq!(opened_units(&mut s, "points.ai", &postscript_ai("%AI5_RulerUnits: 2\n")), Unit::Points, "ruler units in points are kept too");
|
||||
assert_eq!(opened_units(&mut s, "saved.vectorcraft", &exported("Inches", "vectorcraft")), Unit::Inches, "a native file");
|
||||
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="2in" height="1in" viewBox="0 0 144 72"><rect width="10" height="10"/></svg>"#;
|
||||
assert_eq!(opened_units(&mut s, "inches.svg", svg.as_bytes()), Unit::Inches, "an SVG's root size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revert_reopens_a_file_without_units_in_the_general_unit() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-5820-revert-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("plain.pdf");
|
||||
std::fs::write(&path, vectorcraft_testkit::pdf::pdf(&[vectorcraft_testkit::pdf::PdfPage::new(120.0, 60.0, "0 0 0 rg 10 10 50 30 re f")], None))
|
||||
.unwrap();
|
||||
let mut s = general_in("Millimeters");
|
||||
s.execute("document.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(units(&s), Unit::Millimeters);
|
||||
s.execute("shape.rectangle", &json!({"x": 1, "y": 1, "width": 5, "height": 5})).unwrap();
|
||||
s.execute("document.setUnits", &json!({"units": "Inches"})).unwrap();
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(units(&s), Unit::Millimeters, "as the file opens");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Measure {
|
||||
}
|
||||
|
||||
/// The unit a Units preference value names (points when it names none).
|
||||
fn pref_unit(value: &str) -> Unit {
|
||||
pub(crate) fn pref_unit(value: &str) -> Unit {
|
||||
Unit::named(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
|
||||
@@ -648,6 +648,10 @@ impl<'a> Reader<'a> {
|
||||
if self.author.is_none() {
|
||||
self.author = outer.author;
|
||||
}
|
||||
// Ruler units the editing data doesn't give: the file still records them (#5820).
|
||||
if self.ruler_units.is_none() {
|
||||
self.ruler_units = outer.ruler_units;
|
||||
}
|
||||
self.names.absorb(outer.names);
|
||||
for n in outer.custom_colors {
|
||||
if self.custom_colors.len() < super::directives::MAX_CUSTOM_COLORS && !self.custom_colors.iter().any(|c| c.name == n.name) {
|
||||
@@ -1255,6 +1259,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
if let Some(u) = self.ruler_units.and_then(ruler_unit) {
|
||||
doc.units = u;
|
||||
doc.units_from_file = true;
|
||||
}
|
||||
// The file's own swatches in place of the defaults, in its order and colour groups.
|
||||
let own_palette = match self.palette_swatches(&doc.patterns) {
|
||||
|
||||
@@ -77,6 +77,26 @@ fn names(d: &Document) -> Vec<String> {
|
||||
d.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_units_in_the_program_header_alone_are_kept() {
|
||||
let art = layer("Art", true, &square(10, 10));
|
||||
let native = |units: &str| {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n{units}%AI5_NumLayers: 1\n{art}%%Trailer\n"
|
||||
)
|
||||
};
|
||||
let with_header = |data: Vec<u8>| {
|
||||
let at = data.windows(13).position(|w| w == b"%%EndComments").unwrap();
|
||||
[&data[..at], &b"%AI5_RulerUnits: 0\n"[..], &data[at..]].concat()
|
||||
};
|
||||
let r = import(&with_header(eps(&page_square(10, 10), &native("")))).unwrap();
|
||||
assert_eq!((r.document.units, r.document.units_from_file), (Unit::Inches, true), "the program's header gives them");
|
||||
let r = import(&with_header(eps(&page_square(10, 10), &native("%AI5_RulerUnits: 1\n")))).unwrap();
|
||||
assert_eq!(r.document.units, Unit::Millimeters, "the editing data's own come first");
|
||||
let r = import(&eps(&page_square(10, 10), &native(""))).unwrap();
|
||||
assert_eq!((r.document.units, r.document.units_from_file), (Unit::Points, false), "none recorded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_import_keeps_ruler_units_from_editing_data() {
|
||||
let art = layer("Art", true, &square(10, 10));
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
>
|
||||
> Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369).
|
||||
>
|
||||
@@ -282,7 +282,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Numeric spinner arrows step once on a click and keep repeating while held with the mouse | done | #6307; headless test `a_held_spinner_arrow_repeats` holds past egui's click timeout and checks release. |
|
||||
| Arithmetic in fields (`+ - * /`, and `%` of the value before it: `3cm*50%`, `50pt+25%`) and unit suffixes (`3 mm`, `1in`, `2p6`) | done | `Unit::parse`; mixed units in one expression not verified |
|
||||
| Numeric fields: focus click selects the whole value; dragging selects from the pressed character | partial | #1326; select-all waits for click release; px/pt/plain/optional fields, both directions and replacement text tested; native API input, 17 px partial replacement, Undo/Redo, saved-file round trip and screenshots verified; Tab, double-click, arrows and wheel preserved; short movements still classified as clicks can select all and remain open |
|
||||
| Units: pt, pc, in, mm, cm, px, ft-in, m, yd, ft; General/Stroke/Type units | done | units preference reverting to points on SVG output reported (#864) |
|
||||
| Units: pt, pc, in, mm, cm, px, ft-in, m, yd, ft; General/Stroke/Type units | done | units preference reverting to points on SVG output reported (#864). A file that records no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files) opens and reverts in General units; native, SVG and DXF files and a `.ai` or EPS with ruler units keep theirs, as a [Community Expert's answer](https://community.adobe.com/questions-652/change-units-from-points-to-inches-won-t-stick-765372) describes (#5820) |
|
||||
| Units › East Asian Type (Q and Ha) | done | #394: `unitsAsianType` offers every unit and Q and Ha, a quarter of a millimetre each ([Scripting Guide's Measurement Units](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/): 0.709 pt per Q; [InDesign's unit notation](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html): `q`, `h`); only it lists Q and Ha. The public pages do not say which fields use it, so no field uses it yet and that waits for a source. Tests `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` |
|
||||
| Field precision: 3 decimals for steps; values kept as f64 | done | |
|
||||
| Scrubby labels (beyond Illustrator) | done | #400; the labels that are links (the Control bar's Stroke: and Opacity:, the Properties panel's Stroke) scrub too, and a click on them still opens their panel (#4088) |
|
||||
@@ -507,6 +507,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-11 | minor | Files that record no units (PDF, EPS or `.ai` without ruler units, raster images, metafiles, Affinity files, importer plug-ins) open and revert in Preferences › Units › General instead of points; native, SVG and DXF files and `.ai`/EPS ruler units are kept (#5820). Scores and estimates unchanged. |
|
||||
| 2026-11-10 | minor | Held numeric spinner arrows keep stepping past the click timeout and stop on release (#6307). Scores and estimates unchanged. |
|
||||
| 2026-11-09 | minor | Interim macOS canvas cursor delivery (#2292): existing artwork in aligned 1×/2× AppKit representations, visible system fallback and host-owned restoration. Headless routing, bitmap and ownership coverage; native timing, menus, focus/handback, mixed displays, Spotlight and full-screen Spaces remain unverified. Scores and estimates unchanged. |
|
||||
| 2026-11-08 | minor | Floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part); tab groups remain intact. Scores and estimates unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user