mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 15:49:42 +00:00
Properties: Anchor Point(s) with Convert Points and Delete Point(s)
Points selected with Direct Selection title Properties "Anchor Point(s)" and show Transform, Appearance, Convert Points and the Delete Point(s) quick action, as InDesign does. path.deletePoints removes the selected (or given) points of several paths in one undo step. The Convert Point glyphs are redrawn after InDesign's: the anchor on a corner or a curve with its handles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
061c4faaf0
commit
5e1fd65dfc
16 files changed
+226
-15
No files matched your search
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-21** · Properties for points selected with Direct Selection, as InDesign's: the title is "Anchor Point(s)", and the panel shows Transform, Appearance, Convert Points (Plain, Corner, Smooth, Symmetrical) and the Delete Point(s) quick action (`path.deletePoints`, one undo step). Found in Zero2Graphic's *How to Page Layout design in Adobe InDesign 2022 CC*. No readiness percentage change.
|
||||
- **2026-10-20** · Direct Selection selects anchor points: a marquee selects the points inside it on every path it reaches, a click selects one point and Shift+click adds or removes one, selected points show solid, and dragging one (Shift: at 45° steps) or pressing the arrow keys moves them all in one step (`selection.set {anchors}`, `path.moveAnchors {points}`). No readiness percentage change.
|
||||
- **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change.
|
||||
- **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change.
|
||||
|
||||
@@ -66,6 +66,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
join_paths
|
||||
),
|
||||
cmd!(
|
||||
"path.deletePoints",
|
||||
"Delete Points",
|
||||
[],
|
||||
None,
|
||||
"{points?: [[id, subpath, index]]} — the Direct Selection tool's selected points by default, in one undo step (Properties › Quick Actions › Delete Point(s)); a subpath left with one point goes, and an object with none is removed → {deleted, removed: [id]}",
|
||||
has_selection,
|
||||
delete_points
|
||||
),
|
||||
cmd!(
|
||||
"path.convertPoints",
|
||||
"Convert Point",
|
||||
@@ -152,6 +161,66 @@ fn join_paths(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete Point(s): removes the given points, or the ones selected with Direct Selection.
|
||||
fn delete_points(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "path.deletePoints";
|
||||
let points: Vec<(ItemId, usize, usize)> = match p.get("points") {
|
||||
Some(v) => serde_json::from_value::<Vec<(u64, usize, usize)>>(v.clone())
|
||||
.map_err(|e| bad(ID, format!("points: [[id, subpath, index], …] ({e})")))?
|
||||
.into_iter()
|
||||
.map(|(id, si, ai)| (ItemId(id), si, ai))
|
||||
.collect(),
|
||||
None => s.doc()?.selection.anchors.clone(),
|
||||
};
|
||||
if points.is_empty() {
|
||||
return Err(bad(ID, "no points selected"));
|
||||
}
|
||||
// Each subpath's points, worked from the last subpath and the last point back, so the indices
|
||||
// still to delete keep their place.
|
||||
let mut groups: Vec<((ItemId, usize), Vec<usize>)> = Vec::new();
|
||||
for (id, si, ai) in points {
|
||||
match groups.iter_mut().find(|(k, _)| *k == (id, si)) {
|
||||
Some((_, v)) => v.push(ai),
|
||||
None => groups.push(((id, si), vec![ai])),
|
||||
}
|
||||
}
|
||||
groups.sort_by_key(|((id, si), _)| std::cmp::Reverse((id.0, *si)));
|
||||
s.edit(|d, sel| {
|
||||
let (mut deleted, mut removed) = (0usize, Vec::new());
|
||||
for ((id, si), mut ais) in groups {
|
||||
if d.item(id).is_none() {
|
||||
continue;
|
||||
}
|
||||
path_editable(d, id, ID)?;
|
||||
let Some(it) = d.item_mut(id) else { continue };
|
||||
let Some(sp) = it.path.subpaths.get_mut(si) else { continue };
|
||||
ais.sort_unstable();
|
||||
ais.dedup();
|
||||
for ai in ais.into_iter().rev() {
|
||||
if ai < sp.anchors.len() {
|
||||
sp.anchors.remove(ai);
|
||||
deleted += 1;
|
||||
}
|
||||
}
|
||||
// A subpath needs two points (three to stay closed).
|
||||
let (left, closed) = (sp.anchors.len(), sp.closed);
|
||||
if left < 2 {
|
||||
it.path.subpaths.remove(si);
|
||||
} else if closed && left < 3 {
|
||||
sp.closed = false;
|
||||
}
|
||||
as_path(&mut it.shape);
|
||||
if it.path.subpaths.is_empty() {
|
||||
d.remove_item(id)?;
|
||||
sel.items.retain(|i| *i != id);
|
||||
removed.push(id.0);
|
||||
}
|
||||
}
|
||||
sel.anchors.clear();
|
||||
Ok(json!({"deleted": deleted, "removed": removed}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert Point: the selected points (or every point of the selected paths) to a point type.
|
||||
fn convert_points(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let kind = super::str_param(p, "kind").unwrap_or("plain").to_string();
|
||||
@@ -444,6 +513,35 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Convert Point turns the selected points (or a whole path's) plain, corner, smooth or symmetrical.
|
||||
/// Delete Point(s) removes the selected points of several paths in one step; a triangle left
|
||||
/// with two points opens, and Undo brings every point back.
|
||||
#[test]
|
||||
fn delete_points_removes_the_selected_points_of_several_paths() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("frame.create", &json!({"rect": [200, 72, 300, 172], "shape": "polygon", "sides": 3})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("selection.set", &json!({"anchors": [[a, 0, 0], [a, 0, 2], [b, 0, 1]]})).unwrap();
|
||||
let r = s.execute("path.deletePoints", &json!({})).unwrap();
|
||||
assert_eq!(r["deleted"], 3);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let pa = &d.item(ItemId(a)).unwrap().path.subpaths[0];
|
||||
assert_eq!(pa.anchors.len(), 2, "two of the square's four points are left");
|
||||
assert!(!pa.closed, "two points can't stay closed");
|
||||
assert_eq!(d.item(ItemId(b)).unwrap().path.subpaths[0].anchors.len(), 2);
|
||||
assert!(s.doc().unwrap().selection.anchors.is_empty());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.item(ItemId(a)).unwrap().path.subpaths[0].anchors.len(), 4, "one undo step");
|
||||
assert_eq!(d.item(ItemId(b)).unwrap().path.subpaths[0].anchors.len(), 3);
|
||||
// A line's two points: the line goes.
|
||||
let l = s.execute("path.create", &json!({"anchors": [{"p": [72, 300]}, {"p": [172, 300]}]})).unwrap()["id"].as_u64().unwrap();
|
||||
let r = s.execute("path.deletePoints", &json!({"points": [[l, 0, 0]]})).unwrap();
|
||||
assert_eq!(r["removed"], json!([l]));
|
||||
assert!(s.doc().unwrap().doc.item(ItemId(l)).is_none());
|
||||
assert!(s.execute("path.deletePoints", &json!({"points": []})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_point_kinds() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -251,6 +251,9 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Pathfinder:", ["Pathfinder:", "Pathfinder :", "Buscatrazos:", "パスファインダー:", "路径查找器:"]),
|
||||
("Convert Shape:", ["Form konvertieren:", "Convertir la forme :", "Convertir forma:", "シェイプを変換:", "转换形状:"]),
|
||||
("Convert Point:", ["Punkt konvertieren:", "Convertir le point :", "Convertir punto:", "ポイントを変換:", "转换点:"]),
|
||||
("Anchor Point(s)", ["Ankerpunkt(e)", "Point(s) d’ancrage", "Punto(s) de ancla", "アンカーポイント", "锚点"]),
|
||||
("Convert Points", ["Punkte konvertieren", "Convertir les points", "Convertir puntos", "ポイントを変換", "转换点"]),
|
||||
("Delete Point(s)", ["Punkt(e) löschen", "Supprimer le(s) point(s)", "Eliminar punto(s)", "ポイントを削除", "删除点"]),
|
||||
("Join Path", ["Pfad zusammenfügen", "Joindre le tracé", "Unir trazado", "パスを連結", "连接路径"]),
|
||||
("Open Path", ["Pfad öffnen", "Ouvrir le tracé", "Abrir trazado", "パスをオープン", "开放路径"]),
|
||||
("Plain", ["Einfach", "Simple", "Normal", "プレーン", "普通"]),
|
||||
|
||||
@@ -30,6 +30,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "قص"),
|
||||
("Copy", "نسخ"),
|
||||
("Paste", "لصق"),
|
||||
("Anchor Point(s)", "نقطة (نقاط) الارتكاز"),
|
||||
("Convert Points", "تحويل النقاط"),
|
||||
("Delete Point(s)", "حذف النقطة (النقاط)"),
|
||||
("Paste Vector Art", "لصق رسم متجه"),
|
||||
("Baseline Options", "خيارات خط الأساس"),
|
||||
("Fixed Number", "عدد ثابت"),
|
||||
|
||||
@@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Vyjmout"),
|
||||
("Copy", "Kopírovat"),
|
||||
("Paste", "Vložit"),
|
||||
("Anchor Point(s)", "Kotevní body"),
|
||||
("Convert Points", "Převést body"),
|
||||
("Delete Point(s)", "Odstranit body"),
|
||||
("Reverse Path", "Obrátit cestu"),
|
||||
("Set Anchor", "Nastavit kotvu"),
|
||||
("Clear", "Vymazat"),
|
||||
|
||||
@@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Ausschneiden"),
|
||||
("Copy", "Kopieren"),
|
||||
("Paste", "Einfügen"),
|
||||
("Anchor Point(s)", "Ankerpunkt(e)"),
|
||||
("Convert Points", "Punkte konvertieren"),
|
||||
("Delete Point(s)", "Punkt(e) löschen"),
|
||||
("Inches Decimal", "Zoll dezimal"),
|
||||
("Back", "Zurück"),
|
||||
("Warning", "Warnung"),
|
||||
|
||||
@@ -28,6 +28,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Taglia"),
|
||||
("Copy", "Copia"),
|
||||
("Paste", "Incolla"),
|
||||
("Anchor Point(s)", "Punti di ancoraggio"),
|
||||
("Convert Points", "Converti punti"),
|
||||
("Delete Point(s)", "Elimina punti"),
|
||||
("Paste Vector Art", "Incolla grafica vettoriale"),
|
||||
("Baseline Options", "Opzioni linea di base"),
|
||||
("Fixed Number", "Numero fisso"),
|
||||
|
||||
@@ -30,6 +30,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "カット"),
|
||||
("Copy", "コピー"),
|
||||
("Paste", "ペースト"),
|
||||
("Anchor Point(s)", "アンカーポイント"),
|
||||
("Convert Points", "ポイントを変換"),
|
||||
("Delete Point(s)", "ポイントを削除"),
|
||||
("Paste Vector Art", "ベクトルアートをペースト"),
|
||||
("Baseline Options", "ベースラインオプション"),
|
||||
("Column Rules", "段落境界線"),
|
||||
|
||||
@@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "ამოჭრა"),
|
||||
("Copy", "კოპირება"),
|
||||
("Paste", "ჩასმა"),
|
||||
("Anchor Point(s)", "საყრდენი წერტილ(ებ)ი"),
|
||||
("Convert Points", "წერტილების გარდაქმნა"),
|
||||
("Delete Point(s)", "წერტილ(ებ)ის წაშლა"),
|
||||
("Inches Decimal", "ინჩები (ათობითი)"),
|
||||
("Back", "უკან"),
|
||||
("Warning", "გაფრთხილება"),
|
||||
|
||||
@@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Wytnij"),
|
||||
("Copy", "Kopiuj"),
|
||||
("Paste", "Wklej"),
|
||||
("Anchor Point(s)", "Punkty kontrolne"),
|
||||
("Convert Points", "Konwertuj punkty"),
|
||||
("Delete Point(s)", "Usuń punkty"),
|
||||
("Clear", "Wyczyść"),
|
||||
("Duplicate", "Powiel"),
|
||||
("Select All", "Zaznacz wszystko"),
|
||||
|
||||
@@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Recortar"),
|
||||
("Copy", "Copiar"),
|
||||
("Paste", "Colar"),
|
||||
("Anchor Point(s)", "Ponto(s) de ancoragem"),
|
||||
("Convert Points", "Converter pontos"),
|
||||
("Delete Point(s)", "Excluir ponto(s)"),
|
||||
("Paste Vector Art", "Colar arte vetorial"),
|
||||
("Baseline Options", "Opções de linha de base"),
|
||||
("Fixed Number", "Número fixo"),
|
||||
|
||||
@@ -33,6 +33,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Вырезать"),
|
||||
("Copy", "Копировать"),
|
||||
("Paste", "Вставить"),
|
||||
("Anchor Point(s)", "Опорные точки"),
|
||||
("Convert Points", "Преобразовать точки"),
|
||||
("Delete Point(s)", "Удалить точки"),
|
||||
("Clear", "Очистить"),
|
||||
("Duplicate", "Дублировать"),
|
||||
("Select All", "Выделить все"),
|
||||
|
||||
@@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Вирізати"),
|
||||
("Copy", "Копіювати"),
|
||||
("Paste", "Вставити"),
|
||||
("Anchor Point(s)", "Опорні точки"),
|
||||
("Convert Points", "Перетворити точки"),
|
||||
("Delete Point(s)", "Видалити точки"),
|
||||
("Paste Vector Art", "Вставити векторну графіку"),
|
||||
("Baseline Options", "Параметри базової лінії"),
|
||||
("Fixed Number", "Фіксована кількість"),
|
||||
|
||||
+31
-14
@@ -720,23 +720,40 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) {
|
||||
pen.line(&[(10.0, 3.0), (10.0, 17.0)]);
|
||||
pen.line(&[(3.0, 10.0), (17.0, 10.0)]);
|
||||
}
|
||||
// Pathfinder panel › Convert Point: a point on a path, with its direction handles.
|
||||
"point-plain" | "point-corner" | "point-smooth" | "point-symmetrical" => {
|
||||
match name {
|
||||
"point-plain" | "point-corner" => pen.line(&[(3.0, 16.0), (10.0, 8.0), (17.0, 16.0)]),
|
||||
_ => pen.line(&[(3.0, 16.0), (5.5, 11.5), (10.0, 8.0), (14.5, 11.5), (17.0, 16.0)]),
|
||||
// Convert Point (Pathfinder panel, Properties › Convert Points): the anchor square on its
|
||||
// path. Plain and Corner sit on a corner, Corner with a knob on each arm; Smooth and
|
||||
// Symmetrical sit on a curve with a straight handle through them, its arms equal for
|
||||
// Symmetrical.
|
||||
"point-plain" | "point-corner" => {
|
||||
pen.line(&[(17.0, 6.0), (6.0, 6.0), (6.0, 17.0)]);
|
||||
if name == "point-corner" {
|
||||
pen.fcircle(13.0, 6.0, 1.8);
|
||||
pen.fcircle(6.0, 13.0, 1.8);
|
||||
}
|
||||
let handles: &[(f32, f32)] = match name {
|
||||
"point-corner" => &[(4.5, 4.0), (16.0, 5.0)],
|
||||
"point-smooth" => &[(5.0, 8.0), (17.5, 8.0)],
|
||||
"point-symmetrical" => &[(4.0, 8.0), (16.0, 8.0)],
|
||||
_ => &[],
|
||||
pen.frect(4.0, 4.0, 8.0, 8.0);
|
||||
}
|
||||
"point-smooth" | "point-symmetrical" => {
|
||||
// A curve through the anchor (a quadratic sampled as a polyline), and the handle.
|
||||
let ((p0, p2), (h0, h1)) = if name == "point-smooth" {
|
||||
(((3.0, 6.5), (12.5, 18.0)), ((5.0, 4.0), (15.5, 15.5)))
|
||||
} else {
|
||||
(((7.0, 18.0), (17.5, 6.0)), ((5.0, 15.0), (15.0, 5.0)))
|
||||
};
|
||||
for &(hx, hy) in handles {
|
||||
pen.line(&[(10.0, 8.0), (hx, hy)]);
|
||||
pen.fcircle(hx, hy, 1.4);
|
||||
let m = (10.0f32, 10.0f32);
|
||||
let c = (2.0 * m.0 - (p0.0 + p2.0) / 2.0, 2.0 * m.1 - (p0.1 + p2.1) / 2.0);
|
||||
let curve: Vec<(f32, f32)> = (0..=12)
|
||||
.map(|k| {
|
||||
let t = k as f32 / 12.0;
|
||||
let u = 1.0 - t;
|
||||
(u * u * p0.0 + 2.0 * u * t * c.0 + t * t * p2.0, u * u * p0.1 + 2.0 * u * t * c.1 + t * t * p2.1)
|
||||
})
|
||||
.collect();
|
||||
pen.line(&curve);
|
||||
pen.line(&[h0, h1]);
|
||||
for (x, y) in [h0, h1] {
|
||||
pen.frect(x - 1.4, y - 1.4, x + 1.4, y + 1.4);
|
||||
}
|
||||
pen.frect(8.5, 6.5, 11.5, 9.5);
|
||||
pen.frect(8.2, 8.2, 11.8, 11.8);
|
||||
}
|
||||
// ---- Properties panel glyphs (original drawings)
|
||||
"clock" => {
|
||||
|
||||
@@ -59,6 +59,9 @@ fn selection_title(app: &DesignApp, info: &Option<SelInfo>) -> String {
|
||||
if st.selection.text.is_some() {
|
||||
return crate::i18n::tr(&app.ui.language, "Characters").into();
|
||||
}
|
||||
if !st.selection.anchors.is_empty() {
|
||||
return crate::i18n::tr(&app.ui.language, "Anchor Point(s)").into();
|
||||
}
|
||||
match info {
|
||||
None => crate::i18n::tr(&app.ui.language, "No Selection").into(),
|
||||
Some(i) if i.count > 1 => crate::i18n::tr(&app.ui.language, "Multiple Objects").into(),
|
||||
@@ -118,6 +121,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
document_sections(app, ui);
|
||||
return;
|
||||
};
|
||||
if app.session.active().is_some_and(|st| !st.selection.anchors.is_empty()) {
|
||||
anchor_point_sections(app, ui, &i);
|
||||
return;
|
||||
}
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true);
|
||||
transform_section(app, ui, &i);
|
||||
divider(ui);
|
||||
@@ -2214,6 +2221,37 @@ const CONVERT_SHAPE: [(&str, &str, &str); 9] = [
|
||||
("shape-orthogonal", "orthogonalLine", "Orthogonal Line"),
|
||||
];
|
||||
|
||||
/// Points selected with Direct Selection, as InDesign's Properties shows them: Transform,
|
||||
/// Appearance, Convert Points (the point type of the selected points) and Delete Point(s).
|
||||
fn anchor_point_sections(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true);
|
||||
transform_section(app, ui, i);
|
||||
divider(ui);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Appearance"), true);
|
||||
appearance_section(app, ui, i);
|
||||
divider(ui);
|
||||
let mut kind = None;
|
||||
let lang = app.ui.language.clone();
|
||||
section_row(ui, crate::i18n::tr(&lang, "Convert Points"), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 14.0;
|
||||
// Laid out from the right: last first, so the row reads Plain … Symmetrical.
|
||||
for (icon, k, tip) in CONVERT_POINT.iter().rev() {
|
||||
if icon_toggle(ui, icon, false, crate::i18n::tr(&lang, tip)).clicked() {
|
||||
kind = Some(k);
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(k) = kind {
|
||||
let _ = app.run("path.convertPoints", json!({"kind": k}));
|
||||
}
|
||||
rule(ui);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Quick Actions"), true);
|
||||
let fw = full_width(ui);
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Delete Point(s)"), fw).clicked() {
|
||||
let _ = app.run("path.deletePoints", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Pathfinder panel › Convert Point: (icon, kind, tooltip).
|
||||
const CONVERT_POINT: [(&str, &str, &str); 4] = [
|
||||
("point-plain", "plain", "Plain"),
|
||||
@@ -3419,6 +3457,30 @@ mod tests {
|
||||
assert_eq!(doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch, "[None]", "the frame isn't");
|
||||
}
|
||||
|
||||
/// Points selected with Direct Selection: InDesign's "Anchor Point(s)" panel, with Convert
|
||||
/// Points and the Delete Point(s) quick action, as in the Zero2Graphic tutorial (1:25).
|
||||
#[test]
|
||||
fn selected_points_show_convert_points_and_delete_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("frame.create", json!({"rect": [200, 72, 300, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0], [b, 0, 0]]})).unwrap();
|
||||
assert_eq!(selection_title(&app, &super::super::sel_info(&app)), "Anchor Point(s)");
|
||||
let mut h = panel(app);
|
||||
// The four point types (Appearance has a Corner row too).
|
||||
for tip in ["Plain", "Corner", "Smooth", "Symmetrical"] {
|
||||
assert!(h.get_all_by_label(tip).next().is_some(), "{tip}");
|
||||
}
|
||||
assert!(h.query_by_label("Align").is_none(), "only the sections InDesign shows for points");
|
||||
h.get_by_label("Delete Point(s)").click();
|
||||
h.run_steps(3);
|
||||
let d = &h.state().session.active().unwrap().doc;
|
||||
for id in [a, b] {
|
||||
assert_eq!(d.item(designcraft_doc::ids::ItemId(id)).unwrap().path.subpaths[0].anchors.len(), 3, "one point gone from each");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_flips_and_rotates_from_properties() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -435,7 +435,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd/.psb composite in its colour mode — Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK, Lab, 8/16-bit — with merged transparency; extra alpha and spot channels don't change the image), BMP, and Illustrator files saved with PDF compatibility (.ai). A swatch on a placed greyscale or bitmap image (IDML `<Image FillColor>`) colours it on screen and in PDF. 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).
|
||||
- OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes.
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`.
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`. Points selected with Direct Selection title Properties "Anchor Point(s)", with Convert Points and the Delete Point(s) quick action (`path.deletePoints`).
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel (InDesign's layout: thumbnail, Type, Location, Angle, Reverse) sets type, angle, reverse and edits stops and midpoints on a ramp (drag, add, drag off to remove, location; drop a swatch from the Swatches panel on a stop or the ramp; stops keep their CMYK), and follows the Fill/Stroke proxy. Edited gradients are unnamed swatches until the Swatches panel's New Swatch names one "New Gradient Swatch" (`swatch.new`, which also duplicates a named swatch); `swatch.create {gradient}` builds one from colour swatches; `object.gradient` (`target: stroke` for a stroke's gradient, drawn across the path's box on screen and in PDF); the vector round-trips through IDML (`GradientFill…`/`GradientStroke…`). A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF). Text takes a gradient swatch as its fill or stroke, laid across its frame's box as in InDesign (screen and PDF).
|
||||
- New Document (⌘N) as in InDesign: Recent, Saved, Print, Web and Mobile tabs of page cards (double-click creates) beside the preset's details: document name, size with arrows, units, orientation, pages, start page #, facing pages, primary text frame, columns, margins, bleed and slug per edge (linked or not); Save Document Preset (`file.savePreset`, `file.deletePreset`; saved presets are kept with the preferences, listed by `file.presets` and made by `file.new {preset: name}`); `file.new` takes `intent`, `units`, `startPage` and per-edge `bleed`/`slug`. Preview shows a document with the window's settings in the workspace while it is open; Create keeps it, Close removes it.
|
||||
- Document Setup (⌥⌘P), laid out like InDesign's: intent, number of pages, start page # (an even start begins with a left page), facing pages, binding, Page Size (presets keeping the orientation, width and height with arrows, orientation), margins for every page and parent page, bleed and slug per edge (each group linked or not), and Adjust Layout (objects follow the new page size and margins), all in one undo step; measures are read in the document's units, and a value that can't be read keeps the dialog open; `layout.documentSetup {}` reports the setup, margins included.
|
||||
|
||||
Reference in new issue
Block a user