mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 12:16:53 -06:00
O: Transform Again family, Clear Transformations, layer merge/unused/others, Break Link to Style
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4b2417a8c0
commit
571e1043f7
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Transform Again / Individually / Sequence Again (⌥⌘3; a moved copy repeats like Step and Repeat), Clear Transformations; Layers: Merge, Delete Unused, Hide/Lock Others, Show/Unlock All; Break Link to Style (paragraph and character).
|
||||
- Floating panels: tear a panel off the dock (its float button, or drag its header away) into a movable, resizable window; Dock puts it back; positions persist; `window.floatPanel` / `window.dockPanel`.
|
||||
- Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd composite), BMP, and Illustrator files saved with PDF compatibility (.ai). New `designcraft-images` crate (L2) for sniffing, sizes and decoding. EPS is still missing.
|
||||
- View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough).
|
||||
|
||||
@@ -353,6 +353,88 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
ok()
|
||||
})
|
||||
}),
|
||||
cmd!(
|
||||
"layer.merge",
|
||||
"Merge Layers",
|
||||
[],
|
||||
None,
|
||||
"{ids: [layer ids], into?: layer id (default: the first)} — their objects move to `into`, the other layers are deleted",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let ids: Vec<LayerId> =
|
||||
p.get("ids").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(LayerId).collect()).unwrap_or_default();
|
||||
let into = p
|
||||
.get("into")
|
||||
.and_then(Value::as_u64)
|
||||
.map(LayerId)
|
||||
.or_else(|| ids.first().copied())
|
||||
.ok_or_else(|| bad("layer.merge", "no layers"))?;
|
||||
s.edit(|d, _| {
|
||||
if d.layer(into).is_none() || ids.iter().any(|l| d.layer(*l).is_none()) {
|
||||
return Err(bad("layer.merge", "no such layer"));
|
||||
}
|
||||
let gone: Vec<LayerId> = ids.iter().copied().filter(|l| *l != into).collect();
|
||||
for sp in d.spreads.iter_mut().chain(d.parents.iter_mut()) {
|
||||
if !sp.items.iter().any(|i| gone.contains(&i.layer)) {
|
||||
continue;
|
||||
}
|
||||
for it in &mut std::sync::Arc::make_mut(sp).items {
|
||||
if gone.contains(&it.layer) {
|
||||
std::sync::Arc::make_mut(it).layer = into;
|
||||
}
|
||||
}
|
||||
}
|
||||
d.layers.retain(|l| !gone.contains(&l.id));
|
||||
Ok(json!({"merged": gone.len()}))
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!("layer.deleteUnused", "Delete Unused Layers", [], None, "{} — layers without objects (one layer always stays)", has_doc, |s, _| {
|
||||
s.edit(|d, _| {
|
||||
let used: std::collections::HashSet<LayerId> =
|
||||
d.spreads.iter().chain(d.parents.iter()).flat_map(|sp| sp.items.iter().map(|i| i.layer)).collect();
|
||||
let before = d.layers.len();
|
||||
if used.is_empty() {
|
||||
// Nothing anywhere: keep the first layer.
|
||||
d.layers.truncate(1);
|
||||
} else {
|
||||
d.layers.retain(|l| used.contains(&l.id));
|
||||
}
|
||||
Ok(json!({"deleted": before - d.layers.len()}))
|
||||
})
|
||||
}),
|
||||
cmd!(
|
||||
"layer.others",
|
||||
"Hide/Lock Others",
|
||||
[],
|
||||
None,
|
||||
"{id, hide?: bool, lock?: bool, show?: true (Show All Layers), unlock?: true (Unlock All Layers)} — Hide Others / Lock Others act on every layer but `id`",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let id = LayerId(p.get("id").and_then(Value::as_u64).unwrap_or(0));
|
||||
let f = |k: &str| p.get(k).and_then(Value::as_bool).unwrap_or(false);
|
||||
let (hide, lock, show, unlock) = (f("hide"), f("lock"), f("show"), f("unlock"));
|
||||
s.edit(|d, _| {
|
||||
for l in &mut d.layers {
|
||||
if show {
|
||||
l.visible = true;
|
||||
}
|
||||
if unlock {
|
||||
l.locked = false;
|
||||
}
|
||||
if l.id != id {
|
||||
if hide {
|
||||
l.visible = false;
|
||||
}
|
||||
if lock {
|
||||
l.locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!(noundo "layer.activate", "Set Active Layer", [], None, "{id}", has_doc, |s, p| {
|
||||
let id = LayerId(p.get("id").and_then(Value::as_u64).unwrap_or(0));
|
||||
let st = s.doc_mut()?;
|
||||
@@ -413,3 +495,32 @@ mod setup_tests {
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layer_tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn merge_delete_unused_hide_and_lock_others() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let l1 = s.doc().unwrap().doc.layers[0].id.0;
|
||||
let l2 = s.execute("layer.new", &json!({"name": "Art"})).unwrap()["id"].as_u64().unwrap();
|
||||
let l3 = s.execute("layer.new", &json!({"name": "Empty"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("layer.activate", &json!({"id": l2})).unwrap();
|
||||
let f = s.execute("frame.create", &json!({"rect": [0, 0, 10, 10]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("layer.others", &json!({"id": l2, "hide": true, "lock": true})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
assert!(d.layers.iter().all(|l| (l.id.0 == l2) == l.visible && (l.id.0 != l2) == l.locked));
|
||||
s.execute("layer.others", &json!({"id": l2, "show": true, "unlock": true})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.layers.iter().all(|l| l.visible && !l.locked));
|
||||
let r = s.execute("layer.deleteUnused", &json!({})).unwrap();
|
||||
assert_eq!(r["deleted"], 2, "the empty default layer and Empty");
|
||||
let _ = (l1, l3);
|
||||
let l4 = s.execute("layer.new", &json!({"name": "Into"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("layer.merge", &json!({"ids": [l4, l2]})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.layers.len(), 1);
|
||||
assert_eq!(d.item(designcraft_doc::ItemId(f)).unwrap().layer.0, l4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
ok()
|
||||
})
|
||||
}),
|
||||
cmd!(
|
||||
"transform.again",
|
||||
"Transform Again",
|
||||
["Object", "Transform Again"],
|
||||
Some("Cmd+Alt+3"),
|
||||
"{individually?: bool, sequence?: bool, ids?} — repeat the last transform (or the whole sequence applied to this selection), on the selection as a whole or on each object about its own centre",
|
||||
has_selection,
|
||||
transform_again
|
||||
),
|
||||
cmd!(
|
||||
"transform.clear",
|
||||
"Clear Transformations",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{ids?} — remove rotation, shear and scaling (the object keeps its centre)",
|
||||
has_selection,
|
||||
clear_transformations
|
||||
),
|
||||
cmd!("transform.shear", "Shear", ["Object", "Transform"], None, "{angle (degrees), ids?}", has_selection, |s, p| {
|
||||
let a = f64_or(p, "angle", 0.0).clamp(-85.0, 85.0).to_radians().tan();
|
||||
apply_about_center(s, p, Affine::new([1.0, 0.0, a, 1.0, 0.0, 0.0]))
|
||||
@@ -670,6 +688,48 @@ pub(crate) fn scale_item(it: &mut Item, m: Affine, k: f64, strokes: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_again(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
let (list, _, _) = &s.transforms;
|
||||
if list.is_empty() {
|
||||
return Err(bad("transform.again", "no transform to repeat"));
|
||||
}
|
||||
let steps: Vec<(String, Value)> = if bool_or(p, "sequence", false) { list.clone() } else { vec![list.last().cloned().expect("non-empty")] };
|
||||
let mut groups: Vec<Vec<ItemId>> = if bool_or(p, "individually", false) { ids.iter().map(|i| vec![*i]).collect() } else { vec![ids.clone()] };
|
||||
// Run the steps directly (no recording, one undo step for the whole repeat).
|
||||
let saved = s.transforms.clone();
|
||||
for (cmd, params) in &steps {
|
||||
let spec = super::find_command(cmd).ok_or_else(|| bad("transform.again", "unknown transform"))?;
|
||||
for g in &groups {
|
||||
let mut q = params.clone();
|
||||
q["ids"] = json!(g.iter().map(|i| i.0).collect::<Vec<_>>());
|
||||
(spec.run)(s, &q)?;
|
||||
}
|
||||
// Moving a copy selects the copies: later steps (and the next repeat) act on them.
|
||||
if bool_or(params, "copy", false) {
|
||||
groups = vec![s.doc()?.selection.items.clone()];
|
||||
}
|
||||
}
|
||||
s.transforms = saved;
|
||||
Ok(json!({"repeated": steps.len()}))
|
||||
}
|
||||
|
||||
fn clear_transformations(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
s.edit(|d, _| {
|
||||
for id in &ids {
|
||||
let Some(it) = d.item_mut(*id) else { continue };
|
||||
if matches!(it.content, Content::Group { .. }) && it.shape == Shape::Group {
|
||||
continue;
|
||||
}
|
||||
let c = it.bounds().center();
|
||||
let inner = it.inner_bounds().center();
|
||||
it.xf = Affine::translate(c - inner);
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_about_center(s: &mut Session, p: &Value, a: Affine) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
s.edit(|d, _| {
|
||||
@@ -1311,3 +1371,40 @@ mod gradient_tests {
|
||||
assert_eq!(fill(&s).gradient_angle, Some(90.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod again_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transform_again_sequence_and_clear() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = ItemId(s.execute("frame.create", &json!({"rect": [100, 100, 200, 150]})).unwrap()["id"].as_u64().unwrap());
|
||||
let b = |s: &Session| s.doc().unwrap().doc.item(id).unwrap().bounds();
|
||||
assert!(s.execute("transform.again", &json!({})).is_err());
|
||||
s.execute("transform.move", &json!({"dx": 10, "dy": 0})).unwrap();
|
||||
s.execute("transform.again", &json!({})).unwrap();
|
||||
assert_eq!(b(&s).x0, 120.0);
|
||||
s.execute("transform.rotate", &json!({"angle": 90})).unwrap();
|
||||
// Sequence: move 10 then rotate 90 again.
|
||||
s.execute("transform.again", &json!({"sequence": true})).unwrap();
|
||||
// The 100×50 frame is now 180° round from where it started: still 100 wide.
|
||||
assert!((b(&s).width() - 100.0).abs() < 1e-6, "{:?}", b(&s));
|
||||
// Clear Transformations: 180° in total → upright again, same centre.
|
||||
let c = b(&s).center();
|
||||
s.execute("transform.clear", &json!({})).unwrap();
|
||||
let it = s.doc().unwrap().doc.item(id).unwrap().clone();
|
||||
assert_eq!(it.xf.as_coeffs()[..4], [1.0, 0.0, 0.0, 1.0]);
|
||||
assert!((it.bounds().center() - c).hypot() < 1e-6);
|
||||
assert!((it.bounds().width() - 100.0).abs() < 1e-6);
|
||||
// Step and repeat via Transform Again on a moved copy.
|
||||
let n = s.doc().unwrap().doc.spreads[0].items.len();
|
||||
s.execute("transform.move", &json!({"dx": 0, "dy": 60, "copy": true})).unwrap();
|
||||
s.execute("transform.again", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.spreads[0].items.len(), n + 2);
|
||||
let ys: Vec<f64> = d.spreads[0].items[n..].iter().map(|i| i.bounds().y0.round()).collect();
|
||||
assert_eq!(ys[1] - ys[0], 60.0, "{ys:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_text_or_frames,
|
||||
open_type
|
||||
),
|
||||
cmd!(
|
||||
"style.breakLink",
|
||||
"Break Link to Style",
|
||||
[],
|
||||
None,
|
||||
"{kind: paragraph|character} — the selected text keeps its look as local formatting, with [No Paragraph Style] / [None]",
|
||||
has_text_or_frames,
|
||||
break_link
|
||||
),
|
||||
cmd!(query "type.selectionAttrs", "Selection Attributes", [], None, "{} → resolved character/paragraph attributes at the text selection", has_doc, selection_attrs),
|
||||
]
|
||||
}
|
||||
@@ -520,6 +529,45 @@ fn step_size(s: &mut Session, delta: f64) -> Result<Value> {
|
||||
format_chars(s, &json!({"size": (size + delta).clamp(0.1, 1296.0)}))
|
||||
}
|
||||
|
||||
fn break_link(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use designcraft_doc::{CharFormat, CharProps, ParaProps};
|
||||
let para = match p.get("kind").and_then(Value::as_str).unwrap_or("paragraph") {
|
||||
"paragraph" => true,
|
||||
"character" => false,
|
||||
k => return Err(bad("style.breakLink", format!("unknown kind `{k}`"))),
|
||||
};
|
||||
let targets = format_targets(s);
|
||||
s.edit(|d, _| {
|
||||
let styles = d.styles.clone();
|
||||
for t in &targets {
|
||||
let Some(st) = d.text_story_mut(t.story, t.cell) else { continue };
|
||||
let r = t.range.clone();
|
||||
if para {
|
||||
st.format_paras(r, |pf| {
|
||||
let (pp, pc) = styles.resolve_para(pf);
|
||||
pf.style = designcraft_doc::styles::NO_PARA_STYLE.into();
|
||||
pf.para = ParaProps::default().delta(&pp);
|
||||
pf.chars = CharProps::default().delta(&pc);
|
||||
});
|
||||
} else {
|
||||
let ranges = st.para_ranges();
|
||||
for (pi, pr) in ranges.into_iter().enumerate() {
|
||||
let (a, b) = (pr.start.max(r.start), pr.end.min(r.end));
|
||||
if a >= b {
|
||||
continue;
|
||||
}
|
||||
let (_, pc) = styles.resolve_para(&st.paras[pi]);
|
||||
st.format_chars(a..b, |f| {
|
||||
let cp = styles.resolve_char(&pc, f);
|
||||
*f = CharFormat { style: designcraft_doc::story::NO_CHAR_STYLE.into(), over: pc.delta(&cp) };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn open_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use designcraft_doc::otf;
|
||||
let cur = selection_attrs(s, &json!({}))?;
|
||||
@@ -706,6 +754,30 @@ mod open_type_tests {
|
||||
assert!(s.execute("type.openType", &json!({"feature": "bad tag!"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_link_to_paragraph_and_character_styles() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Head", "para": {"spaceBefore": 6}, "chars": {"size": 24}})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Em", "chars": {"fontStyle": "Italic"}})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Title here"})).unwrap();
|
||||
let sid = r["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 10})).unwrap();
|
||||
s.execute("style.paragraph.apply", &json!({"name": "Head"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 5})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Em"})).unwrap();
|
||||
let before = s.execute("type.selectionAttrs", &json!({})).unwrap();
|
||||
s.execute("style.breakLink", &json!({"kind": "character"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 10})).unwrap();
|
||||
s.execute("style.breakLink", &json!({"kind": "paragraph"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 5})).unwrap();
|
||||
let after = s.execute("type.selectionAttrs", &json!({})).unwrap();
|
||||
assert_eq!(after["paragraphStyle"], "[No Paragraph Style]");
|
||||
assert_eq!(after["characterStyle"], "[None]");
|
||||
assert_eq!(after["chars"], before["chars"], "same look");
|
||||
assert_eq!(after["para"], before["para"]);
|
||||
}
|
||||
|
||||
/// Old-style figures shape to other glyphs than the default lining ones (Source Serif 4).
|
||||
#[test]
|
||||
fn oldstyle_figures_change_the_glyphs() {
|
||||
|
||||
@@ -163,6 +163,10 @@ pub struct Session {
|
||||
/// Graphic loaded in the place cursor: (asset, natural size in points).
|
||||
pub loaded: Option<(designcraft_doc::AssetId, (f64, f64))>,
|
||||
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
|
||||
/// tool interaction (its previews replace each other).
|
||||
pub(crate) transforms: (Vec<(String, Value)>, Vec<designcraft_doc::ItemId>, bool),
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -185,6 +189,7 @@ impl Session {
|
||||
text_clipboard: None,
|
||||
ui_requests: vec![],
|
||||
loaded: None,
|
||||
transforms: Default::default(),
|
||||
untitled: 0,
|
||||
}
|
||||
}
|
||||
@@ -234,6 +239,7 @@ impl Session {
|
||||
(spec.enabled)(self).map_err(|e| EngineError::Disabled(id.into(), e))?;
|
||||
let before = self.active().map(|d| (d.uid, d.doc.clone()));
|
||||
let r = (spec.run)(self, params)?;
|
||||
self.record_transform(id, params);
|
||||
// Record undo if the document changed (and we're not previewing an interaction).
|
||||
if let (Some((uid, old)), Some(st)) = (before, self.active_mut())
|
||||
&& st.uid == uid
|
||||
@@ -250,6 +256,30 @@ impl Session {
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Remember a move/rotate/scale/shear/flip for Transform Again.
|
||||
fn record_transform(&mut self, id: &str, params: &Value) {
|
||||
if !matches!(id, "transform.move" | "transform.rotate" | "transform.scale" | "transform.shear" | "transform.flip") {
|
||||
return;
|
||||
}
|
||||
let Some(st) = self.active() else { return };
|
||||
let sel = st.selection.items.clone();
|
||||
let live = st.interaction.is_some();
|
||||
let mut p = params.clone();
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.remove("ids");
|
||||
o.remove("id");
|
||||
}
|
||||
let (list, on, was_live) = &mut self.transforms;
|
||||
if *on != sel {
|
||||
list.clear();
|
||||
*on = sel;
|
||||
} else if *was_live && live && list.last().is_some_and(|l| l.0 == id) {
|
||||
list.pop();
|
||||
}
|
||||
list.push((id.to_string(), p));
|
||||
*was_live = live;
|
||||
}
|
||||
|
||||
pub fn commands(&self) -> Vec<CommandInfo> {
|
||||
command_specs().iter().map(|c| c.info(self)).collect()
|
||||
}
|
||||
|
||||
@@ -152,6 +152,8 @@ impl Session {
|
||||
if let Some((c, p)) = it.preview {
|
||||
self.journal.push((c, p));
|
||||
}
|
||||
// The interaction's last preview is the transform to repeat; the next one starts afresh.
|
||||
self.transforms.2 = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -162,6 +164,10 @@ impl Session {
|
||||
st.doc = it.doc;
|
||||
st.selection = it.selection;
|
||||
st.revision += 1;
|
||||
if self.transforms.2 {
|
||||
self.transforms.0.pop();
|
||||
self.transforms.2 = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,14 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"cmd:transform.shear",
|
||||
"-",
|
||||
"cmd:transform.flip",
|
||||
"-",
|
||||
"cmd:transform.clear",
|
||||
"<",
|
||||
">Transform Again",
|
||||
"cmd:transform.again",
|
||||
"cmd:transform.again|Transform Again Individually|{\"individually\": true}",
|
||||
"cmd:transform.again|Transform Sequence Again|{\"sequence\": true}",
|
||||
"cmd:transform.again|Transform Sequence Again Individually|{\"sequence\": true, \"individually\": true}",
|
||||
"<",
|
||||
">Arrange",
|
||||
"cmd:object.bringToFront",
|
||||
|
||||
@@ -149,6 +149,29 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
|
||||
let _ = app.run("object.setLayer", json!({"layer": l.id.0}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let active = app.session.active().map(|d| d.active_layer);
|
||||
if let Some(a) = active.filter(|a| *a != l.id)
|
||||
&& ui.button("Merge into Active Layer").clicked()
|
||||
{
|
||||
let _ = app.run("layer.merge", json!({"ids": [a.0, l.id.0], "into": a.0}));
|
||||
ui.close();
|
||||
}
|
||||
for (label, params) in [
|
||||
("Hide Others", json!({"id": l.id.0, "hide": true})),
|
||||
("Lock Others", json!({"id": l.id.0, "lock": true})),
|
||||
("Show All Layers", json!({"id": l.id.0, "show": true})),
|
||||
("Unlock All Layers", json!({"id": l.id.0, "unlock": true})),
|
||||
] {
|
||||
if ui.button(label).clicked() {
|
||||
let _ = app.run("layer.others", params);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.button("Delete Unused Layers").clicked() {
|
||||
let _ = app.run("layer.deleteUnused", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if !open {
|
||||
continue;
|
||||
|
||||
@@ -43,6 +43,17 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
|
||||
let clear = ui.input(|i| i.modifiers.alt);
|
||||
let _ = app.run(cmd, json!({"name": n, "clearOverrides": clear}));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button(format!("Apply \"{n}\"")).clicked() {
|
||||
let cmd = if para { "style.paragraph.apply" } else { "style.character.apply" };
|
||||
let _ = app.run(cmd, json!({"name": n}));
|
||||
ui.close();
|
||||
}
|
||||
if current.as_deref() == Some(n.as_str()) && ui.button("Break Link to Style").clicked() {
|
||||
let _ = app.run("style.breakLink", json!({"kind": if para { "paragraph" } else { "character" }}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
+10
-10
@@ -5,27 +5,27 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of
|
||||
Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded.
|
||||
|
||||
<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 70%** over 275 features.
|
||||
**Weighted breadth parity: 72%** over 275 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 33 | 23 | 41 | 46% |
|
||||
| P1 | 97 | 38 | 23 | 36 | 51% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
| Application shell & workspace | 69% |
|
||||
| Documents, pages, spreads | 71% |
|
||||
| Layers | 74% |
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 76% |
|
||||
| Transform | 78% |
|
||||
| Transform | 93% |
|
||||
| Fill, stroke, colour | 69% |
|
||||
| Effects & transparency | 50% |
|
||||
| Placing & links | 66% |
|
||||
| Type & text frames | 77% |
|
||||
| Typography | 79% |
|
||||
| Styles | 61% |
|
||||
| Styles | 71% |
|
||||
| Tables | 73% |
|
||||
| Long documents | 58% |
|
||||
| Interactivity & digital | 30% |
|
||||
@@ -89,8 +89,8 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Layers | Layers panel | P0 | D |
|
||||
| Layers | Layer options | P0 | D |
|
||||
| Layers | Move items between layers | P0 | D |
|
||||
| Layers | Merge layers, delete unused | P1 | M |
|
||||
| Layers | Hide/Lock others | P1 | M |
|
||||
| Layers | Merge layers, delete unused | P1 | D |
|
||||
| Layers | Hide/Lock others | P1 | D |
|
||||
| Layers | Page item names in Layers panel | P1 | D |
|
||||
| Layers | Layer colour drives selection colour | P0 | D |
|
||||
| Layers | Object Layer Options for placed files | P2 | M |
|
||||
@@ -132,8 +132,8 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Transform | Transform panel / Control | P0 | D |
|
||||
| Transform | Free Transform tool | P0 | D |
|
||||
| Transform | Rotate, Scale, Shear tools + dialogs | P0 | D |
|
||||
| Transform | Transform Again family | P1 | M |
|
||||
| Transform | Clear Transformations | P1 | M |
|
||||
| Transform | Transform Again family | P1 | D |
|
||||
| Transform | Clear Transformations | P1 | D |
|
||||
| Transform | Transformations are Totals pref | P1 | M |
|
||||
| Transform | Scale strokes / content options | P0 | D |
|
||||
| Transform | Align & Distribute | P0 | D |
|
||||
@@ -242,7 +242,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Styles | Table & cell styles | P1 | M |
|
||||
| Styles | Style groups | P1 | M |
|
||||
| Styles | Quick Apply | P1 | D |
|
||||
| Styles | Break link to style | P1 | M |
|
||||
| Styles | Break link to style | P1 | D |
|
||||
| Styles | Style mapping on import | P1 | P |
|
||||
| Styles | Style export tagging | P2 | P |
|
||||
| Tables | Create / Insert Table | P1 | D |
|
||||
|
||||
Reference in New Issue
Block a user