* feat(engine): Resize Image During Place drives the implicit Place fit (#204) * Merge origin/main; regenerate docs/scorecard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a27d717597
commit
a3d2a6d486
|
|
@ -400,9 +400,9 @@ fn open_as(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
/// History label of an embedded place.
|
||||
pub const PLACE_EMBEDDED: &str = "Place Embedded";
|
||||
|
||||
/// Place a file's bytes as a smart object layer, centred and (when larger than the canvas)
|
||||
/// scaled down to fit, like Photoshop's Place with "Resize Image During Place". `linked` makes it
|
||||
/// a linked smart object that refers to that path instead of embedding the bytes.
|
||||
/// Place a file's bytes as a smart object layer, centred, and (with Preferences ▸ General ▸
|
||||
/// Resize Image During Place, on by default) scaled down to fit when larger than the canvas.
|
||||
/// `linked` makes it a linked smart object that refers to that path instead of embedding the bytes.
|
||||
pub fn place_bytes(s: &mut Session, name: &str, bytes: Vec<u8>, linked: Option<String>, p: &Value) -> Result<Value> {
|
||||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
let (cw, ch) = (d.doc.size.width as f64, d.doc.size.height as f64);
|
||||
|
|
@ -417,9 +417,11 @@ pub fn place_bytes(s: &mut Session, name: &str, bytes: Vec<u8>, linked: Option<S
|
|||
}
|
||||
let src = imported.document;
|
||||
let (w, h) = (src.size.width as f64, src.size.height as f64);
|
||||
// The implicit `fit` is Preferences ▸ General ▸ Resize Image During Place (on by default).
|
||||
let fit = p.get("fit").and_then(Value::as_bool).unwrap_or(s.prefs().general.resize_image_during_place);
|
||||
let scale = match f64_param(p, "scale") {
|
||||
Some(k) => (k / 100.0).max(1e-4),
|
||||
None if p.get("fit").and_then(Value::as_bool) != Some(false) && (w > cw || h > ch) => (cw / w).min(ch / h),
|
||||
None if fit && (w > cw || h > ch) => (cw / w).min(ch / h),
|
||||
None => 1.0,
|
||||
};
|
||||
let center = match p.get("center").and_then(Value::as_array) {
|
||||
|
|
@ -1302,13 +1304,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Place Embedded…",
|
||||
&["File"],
|
||||
None,
|
||||
r##"{"path":str,"scale":%? (default: fit when larger than the canvas),"fit":bool=true,"center":[x,y]?}"##,
|
||||
r##"{"path":str,"scale":%? (default: fit when larger than the canvas),"fit":bool? (default: Preferences > General > Resize Image During Place),"center":[x,y]?}"##,
|
||||
native_doc,
|
||||
|s, p| place(s, p, false)
|
||||
),
|
||||
spec!("file.placeLinked", "Place Linked…", &["File"], None, r##"{"path":str,"scale":%?,"fit":bool=true,"center":[x,y]?}"##, native_doc, |s, p| place(
|
||||
s, p, true
|
||||
)),
|
||||
spec!(
|
||||
"file.placeLinked",
|
||||
"Place Linked…",
|
||||
&["File"],
|
||||
None,
|
||||
r##"{"path":str,"scale":%?,"fit":bool? (default: Preferences > General > Resize Image During Place),"center":[x,y]?}"##,
|
||||
native_doc,
|
||||
|s, p| place(s, p, true)
|
||||
),
|
||||
spec!(
|
||||
"file.fileInfo",
|
||||
"File Info…",
|
||||
|
|
|
|||
|
|
@ -180,6 +180,23 @@ fn place_embedded_centres_fits_and_embeds() {
|
|||
}
|
||||
}
|
||||
|
||||
/// Preferences ▸ General ▸ Resize Image During Place: off, a larger image keeps its natural size
|
||||
/// instead of being fitted to the canvas; an explicit `"fit"` still wins.
|
||||
#[test]
|
||||
fn resize_image_during_place_preference_controls_the_fit() {
|
||||
let dir = tmp("place-fit-pref");
|
||||
let big = png(&dir, "big.png", 200, 100, "#ff0000");
|
||||
let mut s = session(100, 100, 8);
|
||||
s.execute("prefs.set", json!({"path": "general.resizeImageDuringPlace", "value": false})).unwrap();
|
||||
let r = s.execute("file.placeEmbedded", json!({"path": big})).unwrap();
|
||||
assert!((r["scale"].as_f64().unwrap() - 100.0).abs() < 1e-9, "natural size: {r}");
|
||||
let b = r["bounds"].as_array().unwrap();
|
||||
assert!(b[2].as_f64().unwrap() > 100.0, "wider than the canvas: {b:?}");
|
||||
// An explicit "fit": true overrides the preference.
|
||||
let r = s.execute("file.placeEmbedded", json!({"path": big, "fit": true})).unwrap();
|
||||
assert!((r["scale"].as_f64().unwrap() - 50.0).abs() < 1e-9, "fitted on request: {r}");
|
||||
}
|
||||
|
||||
/// A 40×20 JPEG (left half red, right half blue) tagged EXIF Orientation = 6: shown upright it
|
||||
/// is 20×40, red on top.
|
||||
fn rotated_jpeg(dir: &str) -> String {
|
||||
|
|
|
|||
|
|
@ -972,7 +972,6 @@ pub const SECTIONS: [(&str, &str); 18] = [
|
|||
pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
|
||||
"general.colorPicker",
|
||||
"general.beepWhenDone",
|
||||
"general.resizeImageDuringPlace",
|
||||
"general.alwaysCreateSmartObjectsWhenPlacing",
|
||||
"general.animatedZoom",
|
||||
"general.zoomResizesWindows",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
|
|||
| [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 15 not run, 7 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 5 | 11 | 11 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 21 | 15 | 6 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 44 of 149 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 43 of 149 (target 0) |
|
||||
| [Type](#type) | 0 | 3 | 6 | |
|
||||
| [Automation](#automation) | 3 | 2 | 6 | 628 of 628 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
|
||||
|
|
@ -175,9 +175,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
|||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 44 of 149** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
**Settings that do nothing: 43 of 149** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
|
||||
Unread: `general.color_picker`, `general.beep_when_done`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
|
||||
Unread: `general.color_picker`, `general.beep_when_done`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
|
||||
|
||||
| Id | Target | Status | Issue | Note |
|
||||
|---|---|---|---|---|
|
||||
|
|
|
|||
Loading…
Reference in New Issue