View › Rulers (⌘R) now writes unitsAndRulers.showRulers, which is applied at launch and whenever the preference changes, so new and opened documents show the rulers as last set, as in Photoshop. The Preferences dialog doesn't list it: the View menu sets it. Guides are Vec<f64> (doc, New Guide/Move Guide, guide layouts, artboard and canvas geometry, snapping, PSD resource 1032), so a guide placed in ruler units or read from a PSD's 1/32 px positions keeps its exact place. Scorecard UI-217-20 done (part of #217). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2d2e4993a1
commit
1a54384d80
|
|
@ -716,10 +716,13 @@ impl Layer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ruler guides in document pixels. f64 like the rest of the view geometry: a guide placed in
|
||||
/// ruler units (1 cm at 300 ppi is 118.110236… px) or read from a PSD's 1/32 px positions keeps
|
||||
/// its exact place.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Guides {
|
||||
pub horizontal: Vec<f32>,
|
||||
pub vertical: Vec<f32>,
|
||||
pub horizontal: Vec<f64>,
|
||||
pub vertical: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Which areas an alpha channel's overlay colour marks (Channel Options › Color Indicates).
|
||||
|
|
|
|||
|
|
@ -367,8 +367,8 @@ fn clear_artboard_guides(s: &mut Session) -> Result<Value> {
|
|||
let d = s.active().ok_or(EngineError::NoDocument)?;
|
||||
let id = active_artboard(s).ok_or(EngineError::Other("no artboard selected".into()))?;
|
||||
let r = d.doc.layer(id).and_then(Layer::artboard).map(|a| a.rect).ok_or(EngineError::NoLayer(id))?;
|
||||
let inside_x = |x: &f32| f64::from(*x) > f64::from(r.x0) && f64::from(*x) < f64::from(r.x1);
|
||||
let inside_y = |y: &f32| f64::from(*y) > f64::from(r.y0) && f64::from(*y) < f64::from(r.y1);
|
||||
let inside_x = |x: &f64| *x > f64::from(r.x0) && *x < f64::from(r.x1);
|
||||
let inside_y = |y: &f64| *y > f64::from(r.y0) && *y < f64::from(r.y1);
|
||||
let n = d.doc.guides.vertical.iter().filter(|x| inside_x(x)).count() + d.doc.guides.horizontal.iter().filter(|y| inside_y(y)).count();
|
||||
if n == 0 {
|
||||
return Ok(json!({"cleared": 0}));
|
||||
|
|
@ -405,7 +405,7 @@ pub fn artboard_document(doc: &Document, id: LayerId) -> Result<Option<Document>
|
|||
one.layer_comps.clear();
|
||||
one.last_applied_comp = None;
|
||||
one.last_document_state = None;
|
||||
let shift = |v: &[f32], o: i32, len: u32| v.iter().map(|g| g - o as f32).filter(|g| *g > 0.0 && *g < len as f32).collect::<Vec<_>>();
|
||||
let shift = |v: &[f64], o: i32, len: u32| v.iter().map(|g| g - f64::from(o)).filter(|g| *g > 0.0 && *g < f64::from(len)).collect::<Vec<_>>();
|
||||
one.guides.vertical = shift(&doc.guides.vertical, r.x0, r.width());
|
||||
one.guides.horizontal = shift(&doc.guides.horizontal, r.y0, r.height());
|
||||
Ok(Some(one))
|
||||
|
|
|
|||
|
|
@ -148,12 +148,12 @@ pub(crate) fn transform_doc_marks(doc: &mut Document, a: &Affine) {
|
|||
let [m0, m1, m2, m3, tx, ty] = a.m;
|
||||
let g = &mut doc.guides;
|
||||
if m1.abs() < EPS && m2.abs() < EPS {
|
||||
g.horizontal.iter_mut().for_each(|y| *y = (m3 * f64::from(*y) + ty) as f32);
|
||||
g.vertical.iter_mut().for_each(|x| *x = (m0 * f64::from(*x) + tx) as f32);
|
||||
g.horizontal.iter_mut().for_each(|y| *y = m3 * *y + ty);
|
||||
g.vertical.iter_mut().for_each(|x| *x = m0 * *x + tx);
|
||||
} else if m0.abs() < EPS && m3.abs() < EPS {
|
||||
// A horizontal guide at y becomes a vertical one at x' = m2·y + tx, and vice versa.
|
||||
let v: Vec<f32> = g.horizontal.iter().map(|y| (m2 * f64::from(*y) + tx) as f32).collect();
|
||||
let h: Vec<f32> = g.vertical.iter().map(|x| (m1 * f64::from(*x) + ty) as f32).collect();
|
||||
let v: Vec<f64> = g.horizontal.iter().map(|y| m2 * y + tx).collect();
|
||||
let h: Vec<f64> = g.vertical.iter().map(|x| m1 * x + ty).collect();
|
||||
g.horizontal = h;
|
||||
g.vertical = v;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ fn turns_map_guides_paths_slices_and_live_shapes() {
|
|||
// A vertical guide at x=10 becomes a horizontal one at y=10; a horizontal one at y=5 becomes
|
||||
// a vertical one at x = H − 5.
|
||||
assert_eq!(d.guides.horizontal, vec![10.0]);
|
||||
assert_eq!(d.guides.vertical, vec![(H - 5) as f32]);
|
||||
assert_eq!(d.guides.vertical, vec![f64::from(H - 5)]);
|
||||
// Slice [1,2 10×6] → x' = H − y: [H−8, 1, 6×10].
|
||||
let sl = &d.slices.list[0];
|
||||
assert_eq!(sl.rect, Rect::new(H as i32 - 8, 1, H as i32 - 2, 11));
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@ fn transform_again_params(s: &Session) -> Result<Value> {
|
|||
/// View › New Guide / guide moves (undoable, like Photoshop's "New Guide"/"Move Guide" states).
|
||||
fn guide_cmd(s: &mut Session, p: &Value, op: &str) -> Result<Value> {
|
||||
let vertical = p.get("orientation").and_then(Value::as_str).unwrap_or("horizontal") == "vertical";
|
||||
let pos = p.get("position").and_then(Value::as_f64).map(|v| v as f32);
|
||||
let pos = p.get("position").and_then(Value::as_f64);
|
||||
let index = p.get("index").and_then(Value::as_u64).map(|v| v as usize);
|
||||
let label = match op {
|
||||
"new" => "New Guide",
|
||||
|
|
@ -1406,4 +1406,25 @@ mod tests {
|
|||
assert!(s.active().unwrap().doc.guides.vertical.is_empty());
|
||||
assert!(s.execute("view.moveGuide", json!({"orientation": "vertical", "index": 3, "position": 1})).is_err());
|
||||
}
|
||||
|
||||
/// Guides keep their exact place (#2918): 1 cm at 300 ppi through New Guide and .pcraft, and
|
||||
/// a 1/32 px PSD position past 2^19 px, where f32 has no 1/32 steps left.
|
||||
#[test]
|
||||
fn guides_keep_sub_pixel_positions_through_pcraft_and_psd() {
|
||||
let (cm, far) = (300.0 / 2.54, 600_000.031_25);
|
||||
let mut s = session();
|
||||
s.execute("view.newGuide", json!({"orientation": "horizontal", "position": cm})).unwrap();
|
||||
s.execute("view.newGuide", json!({"orientation": "vertical", "position": far})).unwrap();
|
||||
fn wide(v: impl Into<f64>) -> f64 {
|
||||
v.into()
|
||||
}
|
||||
let exact = |g: &photocraft_doc::Guides| (wide(g.horizontal[0]), wide(g.vertical[0]));
|
||||
let doc = &s.active().unwrap().doc;
|
||||
assert_eq!(exact(&doc.guides), (cm, far));
|
||||
let pcraft = photocraft_format::save_to_bytes(doc, &Default::default()).unwrap();
|
||||
assert_eq!(exact(&photocraft_format::load_from_bytes(&pcraft).unwrap().guides), (cm, far));
|
||||
let psd = photocraft_io::export(doc, "psd", &Default::default()).unwrap().bytes;
|
||||
let back = photocraft_io::import("g.psd", &psd).unwrap().document.guides;
|
||||
assert_eq!(wide(back.vertical[0]), far, "1032 stores 1/32 px");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1270,8 +1270,7 @@ fn color_lookup_tables(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
// ---------- guides ----------
|
||||
|
||||
fn push_unique(v: &mut Vec<f32>, x: f64) {
|
||||
let x = x as f32;
|
||||
fn push_unique(v: &mut Vec<f64>, x: f64) {
|
||||
if !v.iter().any(|g| (g - x).abs() < 0.01) {
|
||||
v.push(x);
|
||||
}
|
||||
|
|
@ -1304,9 +1303,9 @@ fn clear_canvas_guides(s: &mut Session) -> Result<Value> {
|
|||
// Guides inside an artboard belong to it (View › Clear Selected Artboard Guides); the rest
|
||||
// are canvas guides. Without artboards every guide is a canvas guide.
|
||||
let boards: Vec<Rect> = d.doc.artboards().iter().map(|b| b.2.rect).collect();
|
||||
let in_x = move |x: f32| boards.iter().any(|r| f64::from(x) > f64::from(r.x0) && f64::from(x) < f64::from(r.x1));
|
||||
let in_x = move |x: f64| boards.iter().any(|r| x > f64::from(r.x0) && x < f64::from(r.x1));
|
||||
let boards_y: Vec<Rect> = d.doc.artboards().iter().map(|b| b.2.rect).collect();
|
||||
let in_y = move |y: f32| boards_y.iter().any(|r| f64::from(y) > f64::from(r.y0) && f64::from(y) < f64::from(r.y1));
|
||||
let in_y = move |y: f64| boards_y.iter().any(|r| y > f64::from(r.y0) && y < f64::from(r.y1));
|
||||
let n = d.doc.guides.vertical.iter().filter(|x| !in_x(**x)).count() + d.doc.guides.horizontal.iter().filter(|y| !in_y(**y)).count();
|
||||
if n == 0 {
|
||||
return Ok(json!({"cleared": 0}));
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ fn targets(s: &Session, p: &Value) -> Result<Vec<Rect>> {
|
|||
}
|
||||
}
|
||||
|
||||
fn push_unique(out: &mut Vec<f32>, value: f64) -> Result<()> {
|
||||
let value = value as f32;
|
||||
fn push_unique(out: &mut Vec<f64>, value: f64) -> Result<()> {
|
||||
if !value.is_finite() {
|
||||
return Err(bad("guide coordinates exceed the supported range"));
|
||||
}
|
||||
|
|
@ -80,7 +79,7 @@ fn push_unique(out: &mut Vec<f32>, value: f64) -> Result<()> {
|
|||
|
||||
/// Guide positions for `count` columns (or rows) across `[start, end]`, Photoshop-style: both
|
||||
/// edges of every column; a fixed `width` packs columns from `start` (or centres them).
|
||||
fn axis(start: f64, end: f64, count: u64, fixed: f64, gap: f64, center: bool, out: &mut Vec<f32>) -> Result<()> {
|
||||
fn axis(start: f64, end: f64, count: u64, fixed: f64, gap: f64, center: bool, out: &mut Vec<f64>) -> Result<()> {
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -158,17 +157,17 @@ pub fn calculate(s: &Session, p: &Value) -> Result<Layout> {
|
|||
if clear {
|
||||
if p.get("target").and_then(Value::as_str) == Some("selectedArtboards") {
|
||||
// Guides are stored as document-wide axes, matching Clear Selected Artboard Guides.
|
||||
guides.vertical.retain(|v| !bounds.iter().any(|r| f64::from(*v) >= f64::from(r.x0) && f64::from(*v) <= f64::from(r.x1)));
|
||||
guides.horizontal.retain(|v| !bounds.iter().any(|r| f64::from(*v) >= f64::from(r.y0) && f64::from(*v) <= f64::from(r.y1)));
|
||||
guides.vertical.retain(|v| !bounds.iter().any(|r| *v >= f64::from(r.x0) && *v <= f64::from(r.x1)));
|
||||
guides.horizontal.retain(|v| !bounds.iter().any(|r| *v >= f64::from(r.y0) && *v <= f64::from(r.y1)));
|
||||
} else {
|
||||
guides = Guides::default();
|
||||
}
|
||||
}
|
||||
for x in &added.vertical {
|
||||
push_unique(&mut guides.vertical, f64::from(*x))?;
|
||||
push_unique(&mut guides.vertical, *x)?;
|
||||
}
|
||||
for y in &added.horizontal {
|
||||
push_unique(&mut guides.horizontal, f64::from(*y))?;
|
||||
push_unique(&mut guides.horizontal, *y)?;
|
||||
}
|
||||
Ok(Layout { added, guides })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -697,6 +697,10 @@ pub struct UnitsAndRulers {
|
|||
pub print_resolution: f64,
|
||||
pub screen_resolution: f64,
|
||||
pub point_size: PointSize,
|
||||
/// View › Rulers as last set: written when the user toggles the rulers and applied at
|
||||
/// launch, so new and opened documents show them as Photoshop does. Set from the View
|
||||
/// menu (⌘R), not from the Preferences dialog.
|
||||
pub show_rulers: bool,
|
||||
}
|
||||
|
||||
impl Default for UnitsAndRulers {
|
||||
|
|
@ -709,6 +713,7 @@ impl Default for UnitsAndRulers {
|
|||
print_resolution: 300.0,
|
||||
screen_resolution: 72.0,
|
||||
point_size: PointSize::PostScript,
|
||||
show_rulers: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,8 +283,8 @@ fn from_guides(s: &mut Session) -> Result<Value> {
|
|||
let b = d.doc.bounds();
|
||||
let mut xs: Vec<i32> = vec![b.x0, b.x1];
|
||||
let mut ys: Vec<i32> = vec![b.y0, b.y1];
|
||||
xs.extend(d.doc.guides.vertical.iter().map(|g| (*g as f64).round() as i32).filter(|x| *x > b.x0 && *x < b.x1));
|
||||
ys.extend(d.doc.guides.horizontal.iter().map(|g| (*g as f64).round() as i32).filter(|y| *y > b.y0 && *y < b.y1));
|
||||
xs.extend(d.doc.guides.vertical.iter().map(|g| g.round() as i32).filter(|x| *x > b.x0 && *x < b.x1));
|
||||
ys.extend(d.doc.guides.horizontal.iter().map(|g| g.round() as i32).filter(|y| *y > b.y0 && *y < b.y1));
|
||||
if xs.len() == 2 && ys.len() == 2 {
|
||||
return Err(EngineError::Other("the document has no guides inside the canvas".into()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ impl SnapTargets {
|
|||
}
|
||||
if opts.guides {
|
||||
for g in &doc.guides.vertical {
|
||||
t.x.push(Target { pos: *g as f64, kind: SnapKind::Guide, span: (0.0, h) });
|
||||
t.x.push(Target { pos: *g, kind: SnapKind::Guide, span: (0.0, h) });
|
||||
}
|
||||
for g in &doc.guides.horizontal {
|
||||
t.y.push(Target { pos: *g as f64, kind: SnapKind::Guide, span: (0.0, w) });
|
||||
t.y.push(Target { pos: *g, kind: SnapKind::Guide, span: (0.0, w) });
|
||||
}
|
||||
}
|
||||
if opts.grid && opts.grid_step.iter().all(|step| step.is_finite() && *step > 0.0) {
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ pub fn write_artboard(a: &Artboard, guides: &[i32]) -> Vec<u8> {
|
|||
/// Indices of the document guides (vertical first, then horizontal, as in resource 1032) that
|
||||
/// lie inside `r`.
|
||||
pub fn guides_in(guides: &Guides, r: Rect) -> Vec<i32> {
|
||||
let v = guides.vertical.iter().map(|x| (*x as f64) > f64::from(r.x0) && (*x as f64) < f64::from(r.x1));
|
||||
let h = guides.horizontal.iter().map(|y| (*y as f64) > f64::from(r.y0) && (*y as f64) < f64::from(r.y1));
|
||||
let v = guides.vertical.iter().map(|x| *x > f64::from(r.x0) && *x < f64::from(r.x1));
|
||||
let h = guides.horizontal.iter().map(|y| *y > f64::from(r.y0) && *y < f64::from(r.y1));
|
||||
v.chain(h).enumerate().filter(|(_, inside)| *inside).map(|(i, _)| i as i32).collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ fn parse_guides(data: &[u8], doc: &mut Document) {
|
|||
for i in 0..count as usize {
|
||||
let at = 16 + i * 5;
|
||||
let (Some(loc), Some(&dir)) = (rd(at), data.get(at + 4)) else { break };
|
||||
let pos = loc as i32 as f32 / 32.0;
|
||||
let pos = f64::from(loc as i32) / 32.0;
|
||||
if dir == 1 {
|
||||
doc.guides.horizontal.push(pos);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2562,3 +2562,4 @@ cameraData Lens Objektiv
|
|||
Snap to Origin Přichytit k počátku
|
||||
Search settings… Hledat nastavení…
|
||||
No matching settings Žádná odpovídající nastavení
|
||||
Show rulers Zobrazit pravítka
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2593,3 +2593,4 @@ cameraData Lens Objektiv
|
|||
Snap to Origin An Ursprung ausrichten
|
||||
Search settings… Einstellungen durchsuchen…
|
||||
No matching settings Keine passenden Einstellungen
|
||||
Show rulers Lineale anzeigen
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3654,3 +3654,4 @@ cameraData Lens Φακός
|
|||
Snap to Origin Προσκόλληση στην αρχή
|
||||
Search settings… Αναζήτηση ρυθμίσεων…
|
||||
No matching settings Δεν βρέθηκαν ρυθμίσεις
|
||||
Show rulers Εμφάνιση χαράκων
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -2634,3 +2634,4 @@ cameraData Lens Objetivo
|
|||
Snap to Origin Ajustar a origen
|
||||
Search settings… Buscar ajustes…
|
||||
No matching settings No hay ajustes que coincidan
|
||||
Show rulers Mostrar reglas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2971,3 +2971,4 @@ cameraData Lens Objectif
|
|||
Snap to Origin Aligner sur l'origine
|
||||
Search settings… Rechercher des réglages…
|
||||
No matching settings Aucun réglage correspondant
|
||||
Show rulers Afficher les règles
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2958,3 +2958,4 @@ cameraData Lens Lensa
|
|||
Snap to Origin Jepret ke Titik Asal
|
||||
Search settings… Cari pengaturan…
|
||||
No matching settings Tidak ada pengaturan yang cocok
|
||||
Show rulers Tampilkan Penggaris
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2625,3 +2625,4 @@ cameraData Lens Obiettivo
|
|||
Snap to Origin Aggancia all'origine
|
||||
Search settings… Cerca impostazioni…
|
||||
No matching settings Nessuna impostazione corrispondente
|
||||
Show rulers Mostra righelli
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 16.
|
|
|
@ -3568,3 +3568,4 @@ cameraData Lens レンズ
|
|||
Snap to Origin 原点にスナップ
|
||||
Search settings… 設定を検索…
|
||||
No matching settings 一致する設定はありません
|
||||
Show rulers 定規を表示
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3137,3 +3137,4 @@ cameraData Lens 렌즈
|
|||
Snap to Origin 원점에 스냅
|
||||
Search settings… 설정 검색…
|
||||
No matching settings 일치하는 설정 없음
|
||||
Show rulers 눈금자 표시
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 11.
|
|
|
@ -3714,6 +3714,7 @@ cameraData Lens Lens
|
|||
Search settings… Instellingen zoeken…
|
||||
No matching settings Geen overeenkomende instellingen
|
||||
PhotoCraft — Screen Color Picker PhotoCraft — Schermkleurkiezer
|
||||
Show rulers Linialen tonen
|
||||
Frame Tool Kadergereedschap
|
||||
Create a new rectangular frame Nieuw rechthoekig kader maken
|
||||
Create a new elliptical frame Nieuw ovaal kader maken
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
|
|
@ -3569,3 +3569,4 @@ cameraData Lens Obiektyw
|
|||
Snap to Origin Przyciągnij do początku
|
||||
Search settings… Szukaj ustawień…
|
||||
No matching settings Brak pasujących ustawień
|
||||
Show rulers Pokaż linijki
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2978,3 +2978,4 @@ cameraData Lens Lente
|
|||
Snap to Origin Ajustar à origem
|
||||
Search settings… Pesquisar configurações…
|
||||
No matching settings Nenhuma configuração encontrada
|
||||
Show rulers Mostrar réguas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2894,6 +2894,7 @@ cameraData Lens Объектив
|
|||
Rasterize Layer Style Растрировать стиль слоя
|
||||
Search settings… Поиск настроек…
|
||||
No matching settings Нет подходящих настроек
|
||||
Show rulers Показать линейки
|
||||
New Crop Preset… Новый набор параметров кадрирования…
|
||||
Delete Crop Preset… Удалить набор параметров кадрирования…
|
||||
Select the crop presets to delete: Выберите наборы параметров кадрирования для удаления:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3644,6 +3644,7 @@ cameraData Lens Об'єктив
|
|||
Search settings… Пошук налаштувань…
|
||||
No matching settings Немає відповідних налаштувань
|
||||
PhotoCraft — Screen Color Picker PhotoCraft — Піпетка кольору екрана
|
||||
Show rulers Показати лінійки
|
||||
Frame Tool Інструмент «Рамка»
|
||||
Create a new rectangular frame Створити прямокутну рамку
|
||||
Create a new elliptical frame Створити еліптичну рамку
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -2563,6 +2563,7 @@ cameraData Lens 镜头
|
|||
Snap to Origin 贴紧原点
|
||||
Search settings… 搜索设置…
|
||||
No matching settings 没有匹配的设置
|
||||
Show rulers 显示标尺
|
||||
New Crop Preset… 新建裁剪预设…
|
||||
Delete Crop Preset… 删除裁剪预设…
|
||||
Select the crop presets to delete: 选择要删除的裁剪预设:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 15.
|
|
|
@ -2533,6 +2533,7 @@ cameraData Lens 鏡頭
|
|||
Snap to Origin 靠齊原點
|
||||
Search settings… 搜尋設定…
|
||||
No matching settings 沒有符合的設定
|
||||
Show rulers 顯示尺標
|
||||
New Crop Preset… 新增裁切預設集…
|
||||
Delete Crop Preset… 刪除裁切預設集…
|
||||
Select the crop presets to delete: 選取要刪除的裁切預設集:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -502,7 +502,11 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
|||
_ => &mut e.lock_guides,
|
||||
};
|
||||
*slot = !*slot;
|
||||
Ok(json!(*slot))
|
||||
let on = *slot;
|
||||
if id == "view.rulers" {
|
||||
crate::prefs_ui::remember_rulers(app, on);
|
||||
}
|
||||
Ok(json!(on))
|
||||
}
|
||||
"edit.freeTransform"
|
||||
| "edit.transform.scale"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ pub struct Runtime {
|
|||
pub(crate) checker_key: Option<[[u8; 3]; 2]>,
|
||||
/// Style last sent to the GPU canvas.
|
||||
pub(crate) gpu_style: Option<crate::gpu_canvas::CanvasStyle>,
|
||||
/// `unitsAndRulers.showRulers` as last applied to View › Rulers.
|
||||
show_rulers: Option<bool>,
|
||||
/// Tool options as last remembered in the preferences (see `tool_memory`).
|
||||
pub(crate) tool_options: Option<crate::state::ToolOptions>,
|
||||
}
|
||||
|
|
@ -180,6 +182,24 @@ fn system_theme(app: &PhotocraftApp, ctx: &egui::Context) -> Option<egui::Theme>
|
|||
app.services.system_theme.as_ref().and_then(|read| read(ctx)).or_else(|| ctx.system_theme())
|
||||
}
|
||||
|
||||
/// View › Rulers follows `unitsAndRulers.showRulers` when the preference changes (at launch, from
|
||||
/// `prefs.set` or a reset), so documents open with the rulers as last set.
|
||||
fn sync_rulers(app: &mut PhotocraftApp) {
|
||||
let want = app.session.prefs().units_and_rulers.show_rulers;
|
||||
if app.prefs_rt.show_rulers != Some(want) {
|
||||
app.ui.extras.rulers = want;
|
||||
app.prefs_rt.show_rulers = Some(want);
|
||||
}
|
||||
}
|
||||
|
||||
/// View › Rulers was toggled: remember it for the next launch (Photoshop keeps the last state).
|
||||
pub(crate) fn remember_rulers(app: &mut PhotocraftApp, on: bool) {
|
||||
if app.session.prefs().units_and_rulers.show_rulers != on {
|
||||
app.session.prefs.edit(|p| p.units_and_rulers.show_rulers = on);
|
||||
}
|
||||
app.prefs_rt.show_rulers = Some(on);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ lifecycle
|
||||
|
||||
/// Load saved preferences once, when the app is created. Recovery starts during frame upkeep.
|
||||
|
|
@ -195,6 +215,7 @@ pub fn load(app: &mut PhotocraftApp) {
|
|||
}
|
||||
crate::dock::restore(app);
|
||||
crate::brush_picker::restore(app);
|
||||
sync_rulers(app);
|
||||
crate::tool_memory::restore(app);
|
||||
app.sync_recent();
|
||||
app.prefs_rt.saved_rev = app.session.prefs.rev();
|
||||
|
|
@ -375,6 +396,7 @@ pub fn tick(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
|||
load(app);
|
||||
}
|
||||
recovery(app);
|
||||
sync_rulers(app);
|
||||
sync_display_scale(app, ctx);
|
||||
sync_appearance(app, ctx);
|
||||
crate::theme::set_ui_font_size(ctx, app.session.prefs().interface.ui_font_size);
|
||||
|
|
@ -1425,6 +1447,8 @@ fn section_fields(
|
|||
// Settings nothing reads yet stay out of the dialog (issue #204); their stored values
|
||||
// pass through untouched.
|
||||
if (section == "interface" && matches!(k.as_str(), "theme" | "appearanceMode" | "darkTheme" | "lightTheme" | "favoriteThemes"))
|
||||
// View › Rulers (⌘R) sets it, as in Photoshop.
|
||||
|| path == "unitsAndRulers.showRulers"
|
||||
|| prefs::is_hidden(&path)
|
||||
|| (section == "performance" && matches!(k.as_str(), "useGpu" | "gpuBackend" | "renderingMode"))
|
||||
|| (section == "export" && !export_field_visible(obj, &k))
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ pub fn draw_guides(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXform,
|
|||
if drag.is_some_and(|d| d.vertical == vertical && d.index == Some(i)) {
|
||||
continue;
|
||||
}
|
||||
line(vertical, *p as f64, guide);
|
||||
line(vertical, *p, guide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -196,12 +196,12 @@ pub fn guide_at(app: &PhotocraftApp, x: f64, y: f64) -> Option<(bool, usize)> {
|
|||
let doc = &app.session.active()?.doc;
|
||||
let tol = 4.0 / app.point_zoom().max(0.01) as f64;
|
||||
for (i, g) in doc.guides.vertical.iter().enumerate() {
|
||||
if (*g as f64 - x).abs() <= tol {
|
||||
if (*g - x).abs() <= tol {
|
||||
return Some((true, i));
|
||||
}
|
||||
}
|
||||
for (i, g) in doc.guides.horizontal.iter().enumerate() {
|
||||
if (*g as f64 - y).abs() <= tol {
|
||||
if (*g - y).abs() <= tol {
|
||||
return Some((false, i));
|
||||
}
|
||||
}
|
||||
|
|
@ -467,6 +467,37 @@ mod tests {
|
|||
assert_eq!(app.session.prefs().units_and_rulers.rulers, Unit::Centimeters);
|
||||
}
|
||||
|
||||
/// View › Rulers is remembered: the next launch, and every document created or opened in it,
|
||||
/// shows the rulers as last set, on or off (#2918).
|
||||
#[test]
|
||||
fn rulers_open_as_last_set_after_a_restart() {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
let saved = Rc::new(RefCell::new(None::<String>));
|
||||
let launch = |saved: &Rc<RefCell<Option<String>>>| {
|
||||
let (load, store) = (saved.clone(), saved.clone());
|
||||
PhotocraftApp::new(
|
||||
photocraft_engine::Session::new(),
|
||||
crate::Services {
|
||||
load_prefs: Some(Box::new(move || load.borrow().clone())),
|
||||
save_prefs: Some(Box::new(move |text| {
|
||||
*store.borrow_mut() = Some(text.to_string());
|
||||
Ok(())
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
};
|
||||
for on in [true, false] {
|
||||
let mut app = launch(&saved);
|
||||
assert_eq!(crate::menus::invoke(&mut app, &egui::Context::default(), "view.rulers", json!({})).unwrap(), json!(on));
|
||||
crate::prefs_ui::save_preferences(&mut app).unwrap();
|
||||
let mut next = launch(&saved);
|
||||
next.session.execute("file.new", json!({"width": 20, "height": 10})).unwrap();
|
||||
next.sync_views();
|
||||
assert_eq!(next.ui.extras.rulers, on, "a new document after a restart shows the rulers as last set");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guide_drags_create_move_and_delete() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
|
||||
|
|
|
|||
|
|
@ -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, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 6 | 12 | 9 | 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) | 25 | 16 | 2 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 8 | 8 | 7 | settings that do nothing: 33 of 153 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 9 | 8 | 6 | settings that do nothing: 33 of 154 (target 0) |
|
||||
| [Type](#type) | 1 | 4 | 4 | |
|
||||
| [Automation](#automation) | 3 | 2 | 6 | 630 of 630 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
|
||||
|
|
@ -176,7 +176,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
|||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 33 of 153** (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: 33 of 154** (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.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`, `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`, `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_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `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`.
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_w
|
|||
| UI-217-17 | Color panel: wheel plus RGB/HSB/CMYK/Lab/Gray/Web slider modes; resizes with its group | missing | [#217](https://github.com/storytold/photocraft/issues/217) | panels.rs:1837 single field + hex/RGB readout |
|
||||
| UI-217-18 | Alt+wheel zooms in gentle ~5 % steps; zoom controls uncramped | missing | [#217](https://github.com/storytold/photocraft/issues/217) | canvas.rs:1107-1125 no Alt handling |
|
||||
| UI-217-19 | Shortcut hints rendered per platform; Reveal label per OS | done | [#217](https://github.com/storytold/photocraft/issues/217) | hints and tooltips go through shortcuts::pretty (6266b7a9, #262; pretty_uses_the_platform_notation); menus::reveal_label names Reveal in Finder per OS (Show in Explorer / Show in Folder) in the Smart Objects menu and the tab menu; reveal_label_names_the_platform_file_manager + smart_objects_reveal_item_names_the_platform_file_manager |
|
||||
| UI-217-20 | 'Show rulers in new documents' preference; guides stored as f64 | missing | [#217](https://github.com/storytold/photocraft/issues/217) | No rulers pref; doc lib.rs:523 guides are Vec<f32> |
|
||||
| UI-217-20 | 'Show rulers in new documents' preference; guides stored as f64 | done | [#217](https://github.com/storytold/photocraft/issues/217) | View › Rulers is remembered in unitsAndRulers.showRulers and applied at launch and on change (#2918; rulers_open_as_last_set_after_a_restart); doc Guides are Vec<f64> through New Guide, .pcraft and PSD 1032 (guides_keep_sub_pixel_positions_through_pcraft_and_psd) |
|
||||
| UI-217-21 | One Preset Manager for every preset kind with folders, search, reorder, load/save sets | partial | [#217](https://github.com/storytold/photocraft/issues/217) | edit_menu_cmds.rs:714 covers brushes, custom shapes, patterns with reorder |
|
||||
| UI-217-22 | Stamp Visible into a new layer keeping originals; Alt+Merge keeps originals | done | [#217](https://github.com/storytold/photocraft/issues/217) | stamp_cmds.rs layer.stampVisible (Cmd+Alt+Shift+E) merges every visible layer into a new layer above the active one and keeps the originals; layer.stampDown (Cmd+Alt+E) too. As in Photoshop they have no menu item of their own: Alt + Layer > Merge Visible / Merge Down / Merge Layers run them (stamp_cmds::alt_variant, menus.rs); stamp_tests.rs alt_click_on_merge_items_stamps and cmd_alt_shift_e_stamps_visible_and_can_be_rebound |
|
||||
| UI-217-23 | Layer clipboard: copy/paste whole layers (masks, styles, type) between documents | partial | [#217](https://github.com/storytold/photocraft/issues/217) | layer.copyToDocument copies layers into another open document keeping names, content, masks, styles and blending, converting to the destination's profile and depth, as one history step (layer_copy_cmds.rs, #589). It is the command a drag between documents dispatches; there is still no copy/paste layer clipboard |
|
||||
|
|
|
|||
|
|
@ -141,9 +141,9 @@ note = "hints and tooltips go through shortcuts::pretty (6266b7a9, #262; pretty_
|
|||
[[item]]
|
||||
id = "UI-217-20"
|
||||
target = "'Show rulers in new documents' preference; guides stored as f64"
|
||||
status = "missing"
|
||||
status = "done"
|
||||
issue = 217
|
||||
note = "No rulers pref; doc lib.rs:523 guides are Vec<f32>"
|
||||
note = "View › Rulers is remembered in unitsAndRulers.showRulers and applied at launch and on change (#2918; rulers_open_as_last_set_after_a_restart); doc Guides are Vec<f64> through New Guide, .pcraft and PSD 1032 (guides_keep_sub_pixel_positions_through_pcraft_and_psd)"
|
||||
|
||||
[[item]]
|
||||
id = "UI-217-21"
|
||||
|
|
|
|||
Loading…
Reference in New Issue