Guides: ruler guides as layer objects
A ruler guide goes on the current layer (guide.add {layer}), shows,
hides, locks and is deleted with its layer, is listed in the Layers panel
as a row that selects it, and moves to another layer (guide.setLayer).
Guides on a hidden layer neither show nor snap; those on a locked layer
can't be picked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
parent
d7339804e6
commit
02e93902b3
|
|
@ -402,12 +402,17 @@ pub struct Guide {
|
|||
/// and moves, is copied and is deleted with it. None: a canvas guide, across the whole canvas.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artboard: Option<u32>,
|
||||
/// The layer (or sublayer) it is on: it shows, hides and locks with that layer and is listed
|
||||
/// in it in the Layers panel, and goes when the layer is deleted. None: on no layer (guides
|
||||
/// from before guides had layers, and from files that don't say), always there.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub layer: Option<NodeId>,
|
||||
}
|
||||
|
||||
impl Guide {
|
||||
/// A canvas guide.
|
||||
pub fn new(vertical: bool, pos: f64) -> Self {
|
||||
Self { vertical, pos, artboard: None }
|
||||
Self { vertical, pos, artboard: None, layer: None }
|
||||
}
|
||||
|
||||
/// Moved by `d` (a vertical guide across, a horizontal one down).
|
||||
|
|
@ -990,6 +995,19 @@ impl Document {
|
|||
let r = self.artboards.iter().find(|a| Some(a.id) == g.artboard)?.rect;
|
||||
Some(if g.vertical { (r.y0, r.y1) } else { (r.x0, r.x1) })
|
||||
}
|
||||
/// Is ruler guide `g` shown: on no layer, or on a layer that (with the layers around it) is
|
||||
/// visible? A guide whose layer is gone shows as one on no layer.
|
||||
pub fn guide_shown(&self, g: &Guide) -> bool {
|
||||
g.layer.is_none_or(|l| self.node(l).is_none() || self.is_visible(l))
|
||||
}
|
||||
/// Can ruler guide `g` be picked, moved and deleted: shown, and its layer unlocked?
|
||||
pub fn guide_editable(&self, g: &Guide) -> bool {
|
||||
g.layer.is_none_or(|l| self.node(l).is_none() || self.is_editable(l))
|
||||
}
|
||||
/// The ruler guides on layer `layer` (not its sublayers'), by index.
|
||||
pub fn guides_on(&self, layer: NodeId) -> impl Iterator<Item = (usize, &Guide)> + '_ {
|
||||
self.guides.iter().enumerate().filter(move |(_, g)| g.layer == Some(layer))
|
||||
}
|
||||
/// Does ruler guide `g` run past `p` (up to `tol` beyond its ends)?
|
||||
pub fn guide_passes(&self, g: &Guide, p: Point, tol: f64) -> bool {
|
||||
let along = if g.vertical { p.y } else { p.x };
|
||||
|
|
|
|||
|
|
@ -206,6 +206,24 @@ mod tests {
|
|||
assert_eq!(s.guides, vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_show_and_lock_with_their_layer() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer = d.layers[0].id;
|
||||
let g = crate::Guide { layer: Some(layer), ..crate::Guide::new(true, 10.0) };
|
||||
assert!(d.guide_shown(&g) && d.guide_editable(&g));
|
||||
d.node_mut(layer).unwrap().locked = true;
|
||||
assert!(d.guide_shown(&g) && !d.guide_editable(&g), "locked: shown, not editable");
|
||||
d.node_mut(layer).unwrap().visible = false;
|
||||
assert!(!d.guide_shown(&g) && !d.guide_editable(&g), "hidden");
|
||||
// On no layer, or a layer that's gone: always there.
|
||||
assert!(d.guide_shown(&crate::Guide::new(false, 5.0)));
|
||||
let gone = crate::Guide { layer: Some(NodeId(9999)), ..crate::Guide::new(true, 1.0) };
|
||||
assert!(d.guide_shown(&gone) && d.guide_editable(&gone));
|
||||
d.guides.push(g);
|
||||
assert_eq!(d.guides_on(layer).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_changes_clear_the_target() {
|
||||
let mut s = Selection::default();
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Add Guide",
|
||||
[],
|
||||
None,
|
||||
"{vertical: bool, pos: pt (x for vertical, y for horizontal), artboard?: index (an artboard guide: it runs across that artboard only and moves, is copied and is deleted with it; default a canvas guide, across the whole canvas)} → {index}",
|
||||
"{vertical: bool, pos: pt (x for vertical, y for horizontal), artboard?: index (an artboard guide: it runs across that artboard only and moves, is copied and is deleted with it; default a canvas guide, across the whole canvas), layer?: id (the layer it is on, shown, hidden, locked and deleted with it; default the current layer)} → {index}",
|
||||
has_doc,
|
||||
guide_add
|
||||
),
|
||||
|
|
@ -169,7 +169,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Guides",
|
||||
[],
|
||||
None,
|
||||
"{} → [{index, vertical, pos, selected, artboard?: index (an artboard guide's)}…] the ruler guides",
|
||||
"{} → [{index, vertical, pos, selected, artboard?: index (an artboard guide's), layer?: id (the layer it is on), shown, editable (its layer shown and unlocked)}…] the ruler guides",
|
||||
has_doc,
|
||||
guide_list
|
||||
),
|
||||
|
|
@ -200,6 +200,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
guides_unlocked,
|
||||
guide_move
|
||||
),
|
||||
cmd!(
|
||||
"guide.setLayer",
|
||||
"Move Guide to Layer",
|
||||
[],
|
||||
None,
|
||||
"{index?, layer: id} put ruler guide `index`, else the selected guides, on layer `layer` (a layer or sublayer), as for art moved to another layer → {count}",
|
||||
guides_unlocked,
|
||||
guide_set_layer
|
||||
),
|
||||
cmd!("file.closeAll", "Close All", ["File"], Some("Cmd+Alt+W"), "{} → {closed}", has_doc, close_all),
|
||||
cmd!(
|
||||
"file.documentColorMode",
|
||||
|
|
@ -507,8 +516,13 @@ fn guide_add(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
None => None,
|
||||
};
|
||||
let layer = match id_param(p, "layer") {
|
||||
Some(l) if s.doc()?.doc.node(l).is_some_and(Node::is_layer) => Some(l),
|
||||
Some(_) => return Err(bad("guide.add", "layer must name a layer or sublayer")),
|
||||
None => s.doc()?.current_layer(),
|
||||
};
|
||||
let i = s.edit("New Guide", |d, _| {
|
||||
d.guides.push(Guide { artboard, ..Guide::new(vertical, pos) });
|
||||
d.guides.push(Guide { artboard, layer, ..Guide::new(vertical, pos) });
|
||||
Ok(d.guides.len() - 1)
|
||||
})?;
|
||||
Ok(json!({ "index": i }))
|
||||
|
|
@ -521,6 +535,11 @@ fn guide_list(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
if let Some(ab) = st.doc.artboards.iter().position(|a| Some(a.id) == g.artboard) {
|
||||
r["artboard"] = json!(ab);
|
||||
}
|
||||
if let Some(l) = g.layer {
|
||||
r["layer"] = json!(l.0);
|
||||
}
|
||||
r["shown"] = json!(st.doc.guide_shown(g));
|
||||
r["editable"] = json!(st.doc.guide_editable(g));
|
||||
r
|
||||
};
|
||||
Ok(Value::Array(st.doc.guides.iter().enumerate().map(row).collect()))
|
||||
|
|
@ -568,6 +587,28 @@ pub(crate) fn guide_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({ "count": gone.len() }))
|
||||
}
|
||||
|
||||
/// Put ruler guide `index`, else the selected guides, on layer `layer`, in one undo step.
|
||||
fn guide_set_layer(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let layer = id_param(p, "layer").filter(|l| s.doc().is_ok_and(|st| st.doc.node(*l).is_some_and(Node::is_layer)));
|
||||
let layer = layer.ok_or_else(|| bad("guide.setLayer", "layer must name a layer or sublayer"))?;
|
||||
let moving: Vec<usize> = match p.get("index") {
|
||||
Some(v) => vec![guide_index(s, Some(v), "guide.setLayer")?],
|
||||
None => s.doc()?.selection.guides.clone(),
|
||||
};
|
||||
if moving.is_empty() {
|
||||
return Err(bad("guide.setLayer", "no guide selected (or give an index)"));
|
||||
}
|
||||
s.edit("Move Guide to Layer", |d, _| {
|
||||
for i in &moving {
|
||||
if let Some(g) = d.guides.get_mut(*i) {
|
||||
g.layer = Some(layer);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "count": moving.len() }))
|
||||
}
|
||||
|
||||
/// Put ruler guide `index` at `pos`, else move (or copy) the selected guides, in one undo step.
|
||||
pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = p.get("index") {
|
||||
|
|
|
|||
|
|
@ -462,6 +462,9 @@ fn delete_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for id in &ids {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
// The guides on the deleted layers go with them.
|
||||
let orphans: Vec<usize> = d.guides.iter().enumerate().filter(|(_, g)| g.layer.is_some_and(|l| d.node(l).is_none())).map(|(i, _)| i).collect();
|
||||
d.retain_guides(sel, |i, _| !orphans.contains(&i));
|
||||
sel.prune(d);
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -785,7 +785,11 @@ fn ruler_guides_select_move_and_delete() {
|
|||
assert_eq!(s.execute("guide.select", &json!({"indexes": [0]})).unwrap()["selected"], json!([0]));
|
||||
assert!(selected(&s).is_empty());
|
||||
assert_eq!(s.execute("guide.select", &json!({"indexes": [1], "toggle": true})).unwrap()["selected"], json!([0, 1]));
|
||||
assert_eq!(s.execute("guide.list", &json!({})).unwrap()[1], json!({"index": 1, "vertical": false, "pos": 50.0, "selected": true}));
|
||||
let layer = s.doc().unwrap().current_layer().unwrap().0;
|
||||
assert_eq!(
|
||||
s.execute("guide.list", &json!({})).unwrap()[1],
|
||||
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": true, "layer": layer, "shown": true, "editable": true})
|
||||
);
|
||||
assert!(s.execute("guide.select", &json!({"indexes": [7]})).is_err());
|
||||
// A move takes each selected guide along its own axis, in one undo step.
|
||||
let n = undo_len(&s);
|
||||
|
|
@ -866,11 +870,12 @@ fn artboard_guides_go_with_their_artboard() {
|
|||
s.execute("guide.add", &json!({"vertical": true, "pos": 1000, "artboard": 1})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 100})).unwrap();
|
||||
let list = |s: &mut Session| s.execute("guide.list", &json!({})).unwrap();
|
||||
let layer = s.doc().unwrap().current_layer().unwrap().0;
|
||||
assert_eq!(
|
||||
list(&mut s),
|
||||
json!([
|
||||
{"index": 0, "vertical": true, "pos": 1000.0, "selected": false, "artboard": 1},
|
||||
{"index": 1, "vertical": false, "pos": 100.0, "selected": false},
|
||||
{"index": 0, "vertical": true, "pos": 1000.0, "selected": false, "artboard": 1, "layer": layer, "shown": true, "editable": true},
|
||||
{"index": 1, "vertical": false, "pos": 100.0, "selected": false, "layer": layer, "shown": true, "editable": true},
|
||||
])
|
||||
);
|
||||
let spans = |s: &Session| {
|
||||
|
|
@ -951,12 +956,44 @@ fn guides_dragged_out_of_a_ruler_snap_and_take_the_artboard_tool_s_artboard() {
|
|||
s.set_tool_option("active", &json!(1));
|
||||
s.ruler_guide(false, &ev(PointerKind::Drag, 1000.0, 50.0), true, v).unwrap();
|
||||
s.ruler_guide(false, &ev(PointerKind::Up, 1000.0, 50.0), true, v).unwrap();
|
||||
let layer = s.doc().unwrap().current_layer().unwrap().0;
|
||||
assert_eq!(
|
||||
s.execute("guide.list", &json!({})).unwrap()[1],
|
||||
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": false, "artboard": 1})
|
||||
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": false, "artboard": 1, "layer": layer, "shown": true, "editable": true})
|
||||
);
|
||||
}
|
||||
|
||||
/// Ruler guides are layer objects: made on the current layer, hidden, locked and deleted with it,
|
||||
/// and moved to another layer.
|
||||
#[test]
|
||||
fn ruler_guides_live_on_layers() {
|
||||
let mut s = session();
|
||||
let first = s.doc().unwrap().current_layer().unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[0].layer, Some(first), "on the current layer");
|
||||
let second = id_of(&s.execute("layer.new", &json!({"name": "Guides"})).unwrap());
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(second), "the new layer is current");
|
||||
assert!(s.execute("guide.add", &json!({"vertical": true, "pos": 1, "layer": 99999})).is_err());
|
||||
// Hidden or locked with its layer.
|
||||
s.execute("layer.setProps", &json!({"ids": [second.0], "visible": false})).unwrap();
|
||||
assert_eq!(s.execute("guide.list", &json!({})).unwrap()[1]["shown"], false);
|
||||
s.execute("layer.setProps", &json!({"ids": [second.0], "visible": true, "locked": true})).unwrap();
|
||||
let row = s.execute("guide.list", &json!({})).unwrap()[1].clone();
|
||||
assert_eq!((row["shown"].clone(), row["editable"].clone()), (json!(true), json!(false)));
|
||||
s.execute("layer.setProps", &json!({"ids": [second.0], "locked": false})).unwrap();
|
||||
// Moved to the first layer, then back.
|
||||
s.execute("guide.setLayer", &json!({"index": 1, "layer": first.0})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(first));
|
||||
s.execute("guide.setLayer", &json!({"index": 1, "layer": second.0})).unwrap();
|
||||
assert!(s.execute("guide.setLayer", &json!({"index": 1, "layer": 99999})).is_err());
|
||||
// Deleting a layer deletes its guides; undo brings them back.
|
||||
s.execute("layer.delete", &json!({"ids": [second.0]})).unwrap();
|
||||
assert_eq!(guides(&s), [(true, 200.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s).len(), 2);
|
||||
}
|
||||
|
||||
/// #451: art moved with the Selection tool lands flush on the artboard's edges and centre (Smart
|
||||
/// Guides).
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -372,7 +372,13 @@ impl Targets {
|
|||
/// tools): they pull a point into line where they run.
|
||||
fn with_rulers(mut self, cx: &ToolContext) -> Self {
|
||||
if cx.guides {
|
||||
self.rulers = cx.doc.guides.iter().map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) }).collect();
|
||||
self.rulers = cx
|
||||
.doc
|
||||
.guides
|
||||
.iter()
|
||||
.filter(|g| cx.doc.guide_shown(g))
|
||||
.map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) })
|
||||
.collect();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
//! selected guides (Alt copies them). A dragged guide snaps ([`GuideSnap`]): with Shift to the
|
||||
//! ruler's ticks, else to whole pixels, the grid, with Smart Guides the art's and the artboards'
|
||||
//! edges, centres and anchors, or with Snap to Point the anchors. Dropped off the canvas onto its
|
||||
//! ruler, a moved guide is deleted. Hidden or locked guides (View › Guides) can't be picked.
|
||||
//! ruler, a moved guide is deleted. Hidden or locked guides (View › Guides), and guides on a hidden
|
||||
//! or locked layer, can't be picked.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_geom::Point;
|
||||
|
|
@ -24,7 +25,7 @@ pub fn guide_at(cx: &ToolContext, p: Point) -> Option<usize> {
|
|||
.guides
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, g)| cx.doc.guide_passes(g, p, tol))
|
||||
.filter(|(_, g)| cx.doc.guide_editable(g) && cx.doc.guide_passes(g, p, tol))
|
||||
.map(|(i, g)| (i, off(g.pos, g.vertical)))
|
||||
.filter(|(_, d)| *d <= tol)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then(b.0.cmp(&a.0)))
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fn snap(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
|||
cx.doc
|
||||
.guides
|
||||
.iter()
|
||||
.filter(|g| g.vertical == vertical && (g.pos - v).abs() <= tol && cx.doc.guide_passes(g, p, tol))
|
||||
.filter(|g| g.vertical == vertical && (g.pos - v).abs() <= tol && cx.doc.guide_shown(g) && cx.doc.guide_passes(g, p, tol))
|
||||
.min_by(|a, b| (a.pos - v).abs().total_cmp(&(b.pos - v).abs()))
|
||||
};
|
||||
if let Some(g) = nearest(true, p.x) {
|
||||
|
|
|
|||
|
|
@ -378,7 +378,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
// Selected guides in the selection colour.
|
||||
let picked = LineLook { color: t.selection, ..look };
|
||||
let selected: &[usize] = app.session.active().map_or(&[], |st| &st.selection.guides);
|
||||
for (i, g) in doc.guides.iter().enumerate() {
|
||||
// Guides on a hidden layer hide with it.
|
||||
for (i, g) in doc.guides.iter().enumerate().filter(|(_, g)| doc.guide_shown(g)) {
|
||||
let at = |along: f64| xf.to_screen(if g.vertical { Point::new(g.pos, along) } else { Point::new(along, g.pos) });
|
||||
// An artboard guide runs across its artboard, a canvas guide across the window.
|
||||
let (a, b) = match doc.guide_span(g) {
|
||||
|
|
|
|||
|
|
@ -732,3 +732,5 @@ lighting Light Světlo
|
|||
Spatter… Rozstřik…
|
||||
Sprayed Strokes… Stříkané tahy…
|
||||
Sumi-e… Sumi-e…
|
||||
Vertical Guide Svislé vodítko
|
||||
Horizontal Guide Vodorovné vodítko
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -362,6 +362,9 @@
|
|||
Position Panel Above Text Bedienfeld über dem Text positionieren
|
||||
Clear Appearance Aussehen löschen
|
||||
Clear Guides Hilfslinien löschen
|
||||
Vertical Guide Vertikale Hilfslinie
|
||||
Horizontal Guide Horizontale Hilfslinie
|
||||
Move Guide to Layer Hilfslinie auf Ebene verschieben
|
||||
Clear Overrides Abweichungen löschen
|
||||
Clear Recent Files Liste der zuletzt geöffneten Dateien leeren
|
||||
Click Klicken
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -4097,3 +4097,6 @@ lighting Light Luz
|
|||
Stroke Pressure Presión de pincelada
|
||||
Right Diagonal Diagonal derecha
|
||||
Left Diagonal Diagonal izquierda
|
||||
Vertical Guide Guía vertical
|
||||
Horizontal Guide Guía horizontal
|
||||
Move Guide to Layer Mover guía a capa
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -4107,3 +4107,6 @@ lighting Light Lumière
|
|||
Stroke Pressure Pression des traits
|
||||
Right Diagonal Diagonale droite
|
||||
Left Diagonal Diagonale gauche
|
||||
Vertical Guide Repère vertical
|
||||
Horizontal Guide Repère horizontal
|
||||
Move Guide to Layer Déplacer le repère vers le calque
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -4099,3 +4099,6 @@ lighting Light Luce
|
|||
Stroke Pressure Pressione pennellata
|
||||
Right Diagonal Diagonale destra
|
||||
Left Diagonal Diagonale sinistra
|
||||
Vertical Guide Guida verticale
|
||||
Horizontal Guide Guida orizzontale
|
||||
Move Guide to Layer Sposta guida nel livello
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -4092,3 +4092,6 @@ lighting Light ライト
|
|||
Stroke Pressure ストロークの筆圧
|
||||
Right Diagonal 右斜め
|
||||
Left Diagonal 左斜め
|
||||
Vertical Guide 垂直ガイド
|
||||
Horizontal Guide 水平ガイド
|
||||
Move Guide to Layer ガイドをレイヤーへ移動
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2790,3 +2790,5 @@ lighting Light Luz
|
|||
Spatter… Respingo…
|
||||
Sprayed Strokes… Traços Borrifados…
|
||||
Sumi-e… Sumi-e…
|
||||
Vertical Guide Guia vertical
|
||||
Horizontal Guide Guia horizontal
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -4095,3 +4095,6 @@ lighting Light Свет
|
|||
Stroke Pressure Нажим мазка
|
||||
Right Diagonal Диагональ вправо
|
||||
Left Diagonal Диагональ влево
|
||||
Vertical Guide Вертикальная направляющая
|
||||
Horizontal Guide Горизонтальная направляющая
|
||||
Move Guide to Layer Переместить направляющую на слой
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -4078,3 +4078,6 @@ lighting Light Світло
|
|||
Stroke Pressure Натиск мазка
|
||||
Right Diagonal Діагональ праворуч
|
||||
Left Diagonal Діагональ ліворуч
|
||||
Vertical Guide Вертикальна напрямна
|
||||
Horizontal Guide Горизонтальна напрямна
|
||||
Move Guide to Layer Перемістити напрямну на шар
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -3724,3 +3724,6 @@ lighting Light 光源
|
|||
Stroke Pressure 描边压力
|
||||
Right Diagonal 右对角线
|
||||
Left Diagonal 左对角线
|
||||
Vertical Guide 垂直参考线
|
||||
Horizontal Guide 水平参考线
|
||||
Move Guide to Layer 将参考线移至图层
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3505,3 +3505,6 @@ lighting Light 光源
|
|||
Stroke Pressure 筆觸壓力
|
||||
Right Diagonal 右對角線
|
||||
Left Diagonal 左對角線
|
||||
Vertical Guide 垂直參考線
|
||||
Horizontal Guide 水平參考線
|
||||
Move Guide to Layer 將參考線移至圖層
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -220,6 +220,8 @@ struct Row {
|
|||
colour: Color32,
|
||||
/// Drawn dimmed: hidden itself or in a hidden layer or group, or a template layer.
|
||||
dim: bool,
|
||||
/// A ruler guide on the layer `node` (its index in the document's guides): the guide's row.
|
||||
guide: Option<usize>,
|
||||
}
|
||||
|
||||
/// What the rows were listed from: listed again when any of it changes.
|
||||
|
|
@ -239,7 +241,7 @@ fn list_rows(doc: &Document, open: &OpenRows, opts: &PanelOptions, query: &str)
|
|||
let mask_layer = doc.mask_edit.map(|m| m.layer);
|
||||
let mut out = vec![];
|
||||
for l in doc.layers.iter().rev().filter(|l| mask_layer.is_none_or(|m| m == l.id)) {
|
||||
push_rows(l, 0, false, (Color32::PLACEHOLDER, true), (open, opts, query), &mut out);
|
||||
push_rows(l, 0, false, (Color32::PLACEHOLDER, true), (open, opts, query, doc), &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -251,10 +253,10 @@ fn push_rows(
|
|||
depth: usize,
|
||||
clip_path: bool,
|
||||
(colour, shown): (Color32, bool),
|
||||
cx: (&OpenRows, &PanelOptions, &str),
|
||||
cx: (&OpenRows, &PanelOptions, &str, &Document),
|
||||
out: &mut Vec<Row>,
|
||||
) {
|
||||
let (open_rows, opts, query) = cx;
|
||||
let (open_rows, opts, query, doc) = cx;
|
||||
if opts.layers_only && !n.is_layer() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -266,11 +268,18 @@ fn push_rows(
|
|||
_ => colour,
|
||||
};
|
||||
let shown = shown && n.visible;
|
||||
let opens = opens(n, opts);
|
||||
let searching = !query.is_empty();
|
||||
// A layer's ruler guides are rows in it, above its art (not while searching).
|
||||
let guides: Vec<usize> = if n.is_layer() && !opts.layers_only && !searching { doc.guides_on(n.id).map(|(i, _)| i).collect() } else { vec![] };
|
||||
let opens = opens(n, opts) || !guides.is_empty();
|
||||
let open = opens && (searching || open_rows.contains(n.id));
|
||||
let at = out.len();
|
||||
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template() });
|
||||
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template(), guide: None });
|
||||
if open {
|
||||
for i in guides.into_iter().rev() {
|
||||
out.push(Row { node: n.clone(), depth: depth + 1, clip_path: false, opens: false, open: false, colour, dim: !shown, guide: Some(i) });
|
||||
}
|
||||
}
|
||||
if open && let Some(children) = n.children() {
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
push_rows(c, depth + 1, i == 0 && n.clips(), (colour, shown), cx, out);
|
||||
|
|
@ -302,6 +311,8 @@ fn rows_of_doc(ui: &Ui, st: &vectorcraft_engine::DocState, opts: &PanelOptions,
|
|||
struct View<'a> {
|
||||
doc: &'a Document,
|
||||
sel: HashSet<NodeId>,
|
||||
/// The selected ruler guides.
|
||||
guides: Vec<usize>,
|
||||
target: Option<NodeId>,
|
||||
current: Option<NodeId>,
|
||||
rows: Vec<NodeId>,
|
||||
|
|
@ -362,6 +373,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let view = View {
|
||||
doc: &doc,
|
||||
sel: st.selection.objects.iter().copied().collect(),
|
||||
guides: st.selection.guides.clone(),
|
||||
target: st.selection.target,
|
||||
current: st.current_layer(),
|
||||
rows,
|
||||
|
|
@ -542,8 +554,47 @@ fn takes(n: &Node, drag: &LayersDrag, doc: &Document) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// The row of ruler guide `i` on the layer of `item`: a guide mark and its name, the selection
|
||||
/// square when it is selected. A click selects the guide; Shift or Cmd adds it to (or takes it
|
||||
/// from) the selected guides.
|
||||
fn guide_row(ui: &mut Ui, view: &View, item: &Row, i: usize, out: &mut Out) {
|
||||
let (t, h) = (&view.t, view.h);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
|
||||
let Some(g) = view.doc.guides.get(i) else { return };
|
||||
let selected = view.guides.contains(&i);
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover.gamma_multiply(0.6));
|
||||
}
|
||||
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
|
||||
let mut x = r.left() + 2.0 * COLUMN + 2.0;
|
||||
colour_bar(ui, &item.node, r, x, item.colour);
|
||||
x += 6.0 + item.depth as f32 * INDENT + 16.0;
|
||||
// The guide mark: a short line across or down, in the guides' colour.
|
||||
let c = egui::pos2(x + 8.0, r.center().y);
|
||||
let half = if g.vertical { vec2(0.0, 6.0) } else { vec2(6.0, 0.0) };
|
||||
ui.painter().line_segment([c - half, c + half], Stroke::new(1.5, t.accent));
|
||||
x += 22.0;
|
||||
let name = format!("<{}>", if g.vertical { tl!("Vertical Guide") } else { tl!("Horizontal Guide") });
|
||||
let color = if item.dim { t.text_dim } else { t.text };
|
||||
ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(13.0), color);
|
||||
let col_x = r.right() - 43.5;
|
||||
ui.painter().line_segment([egui::pos2(col_x, r.top()), egui::pos2(col_x, r.bottom())], Stroke::new(1.0, t.input_border));
|
||||
if selected {
|
||||
let q = egui::Rect::from_center_size(egui::pos2(r.right() - SQUARE_X, r.center().y), vec2(7.0, 7.0));
|
||||
ui.painter().rect_filled(q, 0.0, item.colour);
|
||||
ui.painter().rect_stroke(q, 0.0, Stroke::new(1.0, t.text), StrokeKind::Inside);
|
||||
}
|
||||
if resp.clicked() {
|
||||
let m = ui.input(|i| i.modifiers);
|
||||
out.actions.push(("guide.select".into(), json!({"indexes": [i], "toggle": m.shift || m.command})));
|
||||
}
|
||||
}
|
||||
|
||||
/// The widgets of row `item`.
|
||||
fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
||||
if let Some(i) = item.guide {
|
||||
return guide_row(ui, view, item, i, out);
|
||||
}
|
||||
let n = &*item.node;
|
||||
let (depth, clip_path, has_children, open, color) = (item.depth, item.clip_path, item.opens, item.open, item.colour);
|
||||
let doc = view.doc;
|
||||
|
|
|
|||
|
|
@ -256,6 +256,11 @@ Constrain acts as Shift held (proportional scaling, moves and rotations by 45°)
|
|||
where it is greyed. The modifier keys still work while dragging (Cmd on a corner distorts it freely, Cmd+Alt+Shift in
|
||||
perspective, Cmd on a side shears).
|
||||
|
||||
Ruler guides are layer objects: `guide.add` puts a guide on the current layer (or `layer`), where it shows, hides,
|
||||
locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it);
|
||||
`guide.setLayer {index?, layer}` moves guides to another layer, and `guide.list` gives each
|
||||
guide's `layer`, `shown` and `editable`.
|
||||
|
||||
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
|
||||
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
|
||||
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;
|
||||
|
|
|
|||
Loading…
Reference in New Issue