mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 04:06:54 -06:00
Place: with no frame selected, the graphic loads into the place cursor
File › Place (native picker, browser import, Place with Import Options and the PDF page dialog) passes loadCursor to file.place. With no empty or graphic frame selected and no x/y given, an image is added to the document and loaded into the place cursor instead of landing in a new frame on the first spread; a click places it at its natural size, a drag draws its frame. The loaded graphic keeps the chosen PDF page and crop box. Escape or choosing another tool unloads the cursor. Dropped files and file.place without loadCursor behave as before.
This commit is contained in:
@@ -100,7 +100,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Place…",
|
||||
["File"],
|
||||
Some("Cmd+D"),
|
||||
"{path?|base64?, name?, frame?: id (place into), spread?, x?, y?, width?, pdfPage?: n (1-based, Image Import Options), pdfCrop?: crop|trim|bleed|art|media, layoutPage?: n (IDML / .designcraft: that page's objects as a group)} — places an image (into the selected empty frame if any); text files (.txt, .docx, .rtf, .md) and Excel workbooks (.xlsx, as a table) go into the insertion point, the selected frame or a new frame on `page`/`rect` — {autoflow?: adds pages with threaded frames until the text fits, removeStyles?, styleMap?: {imported name: document style}, styleConflicts?: useExisting|redefine|autoRename}",
|
||||
"{path?|base64?, name?, frame?: id (place into), spread?, x?, y?, width?, pdfPage?: n (1-based, Image Import Options), pdfCrop?: crop|trim|bleed|art|media, loadCursor?: with no frame to fill and no x/y, an image loads into the place cursor (File › Place), layoutPage?: n (IDML / .designcraft: that page's objects as a group)} — places an image (into the selected empty frame if any); text files (.txt, .docx, .rtf, .md) and Excel workbooks (.xlsx, as a table) go into the insertion point, the selected frame or a new frame on `page`/`rect` — {autoflow?: adds pages with threaded frames until the text fits, removeStyles?, styleMap?: {imported name: document style}, styleConflicts?: useExisting|redefine|autoRename}",
|
||||
has_doc,
|
||||
file_place
|
||||
),
|
||||
@@ -336,6 +336,11 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let px = p.get("x").and_then(Value::as_f64);
|
||||
let py = p.get("y").and_then(Value::as_f64);
|
||||
let want_w = p.get("width").and_then(Value::as_f64);
|
||||
if target_frame.is_none() && px.is_none() && py.is_none() && p.get("loadCursor").and_then(Value::as_bool) == Some(true) {
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
let asset = Asset { page: pdf_page, id: AssetId(0), name, mime, link, data: Arc::new(bytes), pixels: Some((pw, ph)) };
|
||||
return load_cursor(s, asset, (nw, nh), crop_box);
|
||||
}
|
||||
s.edit(|d, sel| {
|
||||
let fresh = AssetId(d.alloc());
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
@@ -442,18 +447,32 @@ fn place_load(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (bytes, name, link) = read_source(p)?;
|
||||
let (pw, ph) = designcraft_render::image_size(&bytes).ok_or_else(|| bad("place.load", "unsupported or corrupt image"))?;
|
||||
let natural = designcraft_render::natural_size(&bytes, 0).unwrap_or((pw as f64, ph as f64));
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
let asset = Asset { page: 0, id: AssetId(0), name, mime, link, data: Arc::new(bytes), pixels: Some((pw, ph)) };
|
||||
load_cursor(s, asset, natural, None)
|
||||
}
|
||||
|
||||
/// Adds `asset` to the document (with a fresh id) and loads it into the place cursor.
|
||||
fn load_cursor(s: &mut Session, mut asset: Asset, size: (f64, f64), crop: Option<(f64, f64, f64, f64)>) -> Result<Value> {
|
||||
let name = asset.name.clone();
|
||||
let (pw, ph) = asset.pixels.unwrap_or_default();
|
||||
let aid = s.edit(|d, _| {
|
||||
let fresh = AssetId(d.alloc());
|
||||
let mime = designcraft_render::image_mime(&bytes).to_string();
|
||||
Ok(d.add_asset(Asset { page: 0, id: fresh, name: name.clone(), mime, link: link.clone(), data: Arc::new(bytes), pixels: Some((pw, ph)) }))
|
||||
asset.id = AssetId(d.alloc());
|
||||
Ok(d.add_asset(asset))
|
||||
})?;
|
||||
s.loaded = Some((aid, natural));
|
||||
s.loaded = Some(crate::LoadedGraphic { asset: aid, size, crop });
|
||||
s.set_tool("placeGun");
|
||||
Ok(json!({"asset": aid.0, "name": name, "width": pw, "height": ph}))
|
||||
Ok(json!({"asset": aid.0, "name": name, "width": pw, "height": ph, "loaded": true}))
|
||||
}
|
||||
|
||||
fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (aid, (nw, nh)) = s.loaded.ok_or_else(|| bad("place.drop", "nothing loaded in the place cursor"))?;
|
||||
let crate::LoadedGraphic { asset: aid, size: (nw, nh), crop } =
|
||||
s.loaded.ok_or_else(|| bad("place.drop", "nothing loaded in the place cursor"))?;
|
||||
// Undo can remove the loaded graphic's asset.
|
||||
if !s.doc()?.doc.assets.contains_key(&aid) {
|
||||
s.loaded = None;
|
||||
return Err(bad("place.drop", "the loaded graphic is no longer in the document"));
|
||||
}
|
||||
let sr = super::spread_param(p, "spread");
|
||||
let lid = s.doc()?.active_layer;
|
||||
let frame = super::id_param(p, "frame");
|
||||
@@ -477,12 +496,14 @@ fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => {
|
||||
// Drag: fit proportionally into the dragged rect; click: actual size at the point.
|
||||
// The frame shows the crop box when one was chosen.
|
||||
let (bx, by, bw, bh) = crop.unwrap_or((0.0, 0.0, nw, nh));
|
||||
let (frame_r, k) = match rect {
|
||||
Some(r) if r.width() > 2.0 && r.height() > 2.0 => {
|
||||
let k = (r.width() / nw).min(r.height() / nh);
|
||||
(Rect::new(r.x0, r.y0, r.x0 + nw * k, r.y0 + nh * k), k)
|
||||
let k = (r.width() / bw).min(r.height() / bh);
|
||||
(Rect::new(r.x0, r.y0, r.x0 + bw * k, r.y0 + bh * k), k)
|
||||
}
|
||||
_ => (Rect::new(x, y, x + nw, y + nh), 1.0),
|
||||
_ => (Rect::new(x, y, x + bw, y + bh), 1.0),
|
||||
};
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it = Item::new(id, lid, Shape::Rectangle, shapes::rectangle(frame_r));
|
||||
@@ -490,7 +511,7 @@ fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
it.content = Content::Graphic(Graphic {
|
||||
asset: aid,
|
||||
size: (nw, nh),
|
||||
xf: Affine::translate((frame_r.x0, frame_r.y0)) * Affine::scale(k),
|
||||
xf: Affine::translate((frame_r.x0 - bx * k, frame_r.y0 - by * k)) * Affine::scale(k),
|
||||
auto_fit: Default::default(),
|
||||
fit_align: 4,
|
||||
crop: [0.0; 4],
|
||||
|
||||
@@ -105,6 +105,16 @@ pub struct DocState {
|
||||
#[derive(Debug)]
|
||||
pub struct FontScope(pub u32);
|
||||
|
||||
/// A graphic loaded in the place cursor (`place.load`, File › Place with no frame to fill).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LoadedGraphic {
|
||||
pub asset: designcraft_doc::AssetId,
|
||||
/// Natural size in points.
|
||||
pub size: (f64, f64),
|
||||
/// Image Import Options › Crop to: the box of the graphic a new frame shows (x, y, w, h).
|
||||
pub crop: Option<(f64, f64, f64, f64)>,
|
||||
}
|
||||
|
||||
impl Drop for FontScope {
|
||||
fn drop(&mut self) {
|
||||
designcraft_fonts::FontDb::global().close_scope(self.0);
|
||||
@@ -258,8 +268,8 @@ pub struct Session {
|
||||
pub text_clipboard: Option<(Arc<designcraft_doc::Story>, String)>,
|
||||
/// Requests for the UI (dialogs, view changes) produced by tools/commands.
|
||||
pub ui_requests: Vec<UiRequest>,
|
||||
/// Graphic loaded in the place cursor: (asset, natural size in points).
|
||||
pub loaded: Option<(designcraft_doc::AssetId, (f64, f64))>,
|
||||
/// Graphic loaded in the place cursor.
|
||||
pub loaded: Option<LoadedGraphic>,
|
||||
pub(crate) untitled: u32,
|
||||
/// Transforms applied to the current selection, oldest first (Transform Again / Sequence
|
||||
/// Again), with the selection they were applied to and whether the last one came from a
|
||||
|
||||
@@ -59,6 +59,10 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
self.tool = designcraft_tools::create(id);
|
||||
// Any other tool unloads the place cursor.
|
||||
if id != "placeGun" {
|
||||
self.loaded = None;
|
||||
}
|
||||
// Leaving the Type tool keeps the text frame selected.
|
||||
if id != "type"
|
||||
&& let Some(st) = self.active_mut()
|
||||
|
||||
@@ -157,6 +157,13 @@ impl Tool for PlaceGun {
|
||||
None => vec![],
|
||||
}
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
// Escape unloads the place cursor.
|
||||
crate::ToolKey::Escape => vec![Action::SwitchTool("selection".into())],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::LoadedGraphic
|
||||
}
|
||||
|
||||
@@ -1921,7 +1921,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
if let Some(target) = d.fields.get("target").and_then(Value::as_u64) {
|
||||
app.activate_import_target(&crate::ImportRequest { purpose: "place".into(), target: Some(target) })?;
|
||||
}
|
||||
app.open_file("file.place", json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }}))
|
||||
app.place_file(json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }}))
|
||||
}
|
||||
"print" => {
|
||||
let pages = if d.s("range") == "all" { Value::Null } else { json!(d.s("pages")) };
|
||||
|
||||
@@ -622,7 +622,6 @@ impl DesignApp {
|
||||
/// command right away; asynchronous ones (web) deliver the file through the inbox.
|
||||
pub fn pick_and_open(&mut self, purpose: &str) -> Result<Value, String> {
|
||||
let target = self.session.active().map(|d| d.uid);
|
||||
let cmd = if purpose == "place" { "file.place" } else { "file.open" };
|
||||
if let Some(pick) = self.services.pick_open.as_mut() {
|
||||
return match pick(purpose) {
|
||||
// Image Import Options: a multi-page PDF asks which page.
|
||||
@@ -634,10 +633,11 @@ impl DesignApp {
|
||||
Some(dialogs::Dialog::new("pdfImport", json!({"path": path, "page": "1", "pages": pages, "target": target})));
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
self.open_file(cmd, json!({"path": path}))
|
||||
self.place_file(json!({"path": path}))
|
||||
}
|
||||
}
|
||||
Some(path) => self.open_file(cmd, json!({"path": path})),
|
||||
Some(path) if purpose == "place" => self.place_file(json!({"path": path})),
|
||||
Some(path) => self.open_file("file.open", json!({"path": path})),
|
||||
None => Ok(Value::Null),
|
||||
};
|
||||
}
|
||||
@@ -669,10 +669,8 @@ impl DesignApp {
|
||||
pub(crate) fn confirm_pdf_import(&mut self, page: u64, crop: String) -> Result<Value, String> {
|
||||
let file = self.pending_pdf.take().ok_or("PDF import cancelled: no pending file")?;
|
||||
self.activate_import_target(&file.request)?;
|
||||
self.open_file(
|
||||
"file.place",
|
||||
json!({"name": file.name, "base64": designcraft_engine::cmd::base64_encode(&file.bytes), "pdfPage": page, "pdfCrop": crop}),
|
||||
)
|
||||
let params = json!({"name": file.name, "base64": designcraft_engine::cmd::base64_encode(&file.bytes), "pdfPage": page, "pdfCrop": crop});
|
||||
if file.request.purpose == "place" { self.place_file(params) } else { self.open_file("file.place", params) }
|
||||
}
|
||||
|
||||
/// A single unresolved PDF owns its bytes. Later deliveries cancel explicitly rather than
|
||||
@@ -726,6 +724,8 @@ impl DesignApp {
|
||||
let b64 = designcraft_engine::cmd::base64_encode(&file.bytes);
|
||||
if lower.ends_with(".ase") || file.request.purpose == "swatches" {
|
||||
self.run("swatch.load", json!({"base64": b64}))
|
||||
} else if file.request.purpose == "place" {
|
||||
self.place_file(json!({"name": file.name, "base64": b64}))
|
||||
} else if place {
|
||||
self.open_file("file.place", json!({"name": file.name, "base64": b64}))
|
||||
} else {
|
||||
@@ -810,6 +810,15 @@ impl DesignApp {
|
||||
self.open_file(cmd, json!({"path": path}))
|
||||
}
|
||||
|
||||
/// File › Place: the file fills the selected empty or graphic frame; with none, an image loads
|
||||
/// into the place cursor (click to place at its natural size, drag to draw the frame).
|
||||
pub(crate) fn place_file(&mut self, mut params: Value) -> Result<Value, String> {
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
o.insert("loadCursor".into(), json!(true));
|
||||
}
|
||||
self.open_file("file.place", params)
|
||||
}
|
||||
|
||||
/// Open (`file.open`, `file.openBytes`) or place (`file.place`) a file the user chose. A file
|
||||
/// that can't be opened or placed gets an alert with the reason, as well as the status line.
|
||||
/// A successful import with warnings keeps the document and shows the warnings in an alert.
|
||||
@@ -1625,6 +1634,7 @@ mod browser_import_tests {
|
||||
assert_eq!(*asset.data, bytes);
|
||||
assert!(app.pending_pdf.is_none());
|
||||
assert!(app.session.documents()[1].doc.assets.is_empty());
|
||||
app.run("place.drop", json!({"x": 36, "y": 36})).unwrap();
|
||||
let exported = app.run("file.exportPdf", json!({})).unwrap();
|
||||
let exported = designcraft_engine::cmd::base64_decode(exported["base64"].as_str().unwrap());
|
||||
let pixels = designcraft_render::render_pdf_page(&exported, 0, 400).unwrap();
|
||||
@@ -1632,6 +1642,38 @@ mod browser_import_tests {
|
||||
assert_eq!(pixels.data().iter().filter(|p| p.r > 240 && p.g < 10 && p.b < 10).count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_loads_the_cursor_unless_a_frame_can_take_the_graphic() {
|
||||
let mut app = app();
|
||||
let items = |app: &DesignApp| app.session.doc().unwrap().doc.spreads[0].items.len();
|
||||
// Nothing selected: the graphic waits in the place cursor; Escape unloads it.
|
||||
let request = app.import_request("place");
|
||||
deliver(&mut app, request, "red.svg", image());
|
||||
assert_eq!(app.session.tool_id(), "placeGun");
|
||||
assert!(app.session.loaded.is_some());
|
||||
assert_eq!(items(&app), 0);
|
||||
let mods = designcraft_tools::Mods::default();
|
||||
app.session.tool_key(designcraft_tools::ToolKey::Escape, mods, designcraft_engine::ViewInfo::at_zoom(1.0)).unwrap();
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
assert!(app.session.loaded.is_none());
|
||||
// A click with the loaded cursor places the graphic at its natural size.
|
||||
let request = app.import_request("place");
|
||||
deliver(&mut app, request, "red.svg", image());
|
||||
app.run("place.drop", json!({"x": 100, "y": 100})).unwrap();
|
||||
let it = app.session.doc().unwrap().doc.spreads[0].items.last().unwrap();
|
||||
assert_eq!(it.bounds(), designcraft_geom::Rect::new(100.0, 100.0, 107.5, 107.5), "10 CSS px = 7.5 pt");
|
||||
// An empty graphic frame selected: the graphic fills it.
|
||||
let f = app.run("frame.create", json!({"rect": [200, 200, 300, 300], "content": "graphic"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"ids": [f]})).unwrap();
|
||||
let request = app.import_request("place");
|
||||
deliver(&mut app, request, "red.svg", image());
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
assert!(app.session.loaded.is_none());
|
||||
assert_eq!(items(&app), 2);
|
||||
let frame = app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(f)).unwrap();
|
||||
assert!(matches!(frame.content, designcraft_doc::Content::Graphic(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_options_reject_oversized_page_without_editing() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -743,7 +743,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("place")) else { return Some(Ok(Value::Null)) };
|
||||
let l = path.to_lowercase();
|
||||
if !(l.ends_with(".docx") || l.ends_with(".rtf")) {
|
||||
return Some(app.open_file("file.place", json!({"path": path})));
|
||||
return Some(app.place_file(json!({"path": path})));
|
||||
}
|
||||
let info = match app.run("place.styles", json!({"path": path})) {
|
||||
Ok(v) => v,
|
||||
|
||||
Reference in New Issue
Block a user