Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b426859b15 | ||
|
|
61d372bb7f |
No files matched your search
+20
-5
@@ -86,6 +86,19 @@ pub fn objects_at(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeI
|
||||
|
||||
/// [`hit_test`] passing over the nodes `skip` names (and their contents).
|
||||
fn hit_test_skipping(doc: &Document, p: Point, opt: HitOptions, skip: &dyn Fn(NodeId) -> bool) -> Option<Hit> {
|
||||
let finish = |mut h: Hit| {
|
||||
h.contents_of = h.ancestry.iter().rev().copied().find(|a| doc.node(*a).is_some_and(edits_contents));
|
||||
h.layers = h.ancestry.iter().take_while(|a| doc.node(**a).is_some_and(Node::is_layer)).count();
|
||||
h
|
||||
};
|
||||
// Isolation mode: only what is inside the isolated container hits. The art around it is
|
||||
// locked, and clicks pass through it (#833).
|
||||
if let Some(scope) = opt.scope
|
||||
&& let Some(container) = doc.node(scope).filter(|n| n.is_container())
|
||||
&& let Some(mut chain) = doc.ancestry(scope)
|
||||
{
|
||||
return hit_children(container, p, opt, skip, &mut chain).map(finish);
|
||||
}
|
||||
let mut chain = Vec::new();
|
||||
for layer in doc.layers.iter().rev() {
|
||||
if !layer.visible || layer.locked {
|
||||
@@ -100,10 +113,8 @@ fn hit_test_skipping(doc: &Document, p: Point, opt: HitOptions, skip: &dyn Fn(No
|
||||
}
|
||||
chain.push(layer.id);
|
||||
let opt = if matches!(layer.kind, NodeKind::Layer { preview: false, .. }) { HitOptions { outline: true, ..opt } } else { opt };
|
||||
if let Some(mut h) = hit_children(layer, p, opt, skip, &mut chain) {
|
||||
h.contents_of = h.ancestry.iter().rev().copied().find(|a| doc.node(*a).is_some_and(edits_contents));
|
||||
h.layers = h.ancestry.iter().take_while(|a| doc.node(**a).is_some_and(Node::is_layer)).count();
|
||||
return Some(h);
|
||||
if let Some(h) = hit_children(layer, p, opt, skip, &mut chain) {
|
||||
return Some(finish(h));
|
||||
}
|
||||
chain.pop();
|
||||
}
|
||||
@@ -560,7 +571,11 @@ world",
|
||||
d.insert(Some(l), 9, far).unwrap();
|
||||
let at = |x: f64, scope| objects_at(&d, Point::new(x, 25.0), HitOptions::default(), scope);
|
||||
assert_eq!(at(30.0, None), vec![g, a_id]);
|
||||
assert_eq!(at(30.0, Some(g))[..2], [c_id, b_id], "isolated: the group's own objects");
|
||||
assert_eq!(at(30.0, Some(g)), [c_id, b_id], "isolated: the group's own objects");
|
||||
// Isolated, the art around the group is locked: clicks pass through it (#833).
|
||||
assert!(at(5.0, Some(g)).is_empty());
|
||||
assert!(at(130.0, Some(g)).is_empty());
|
||||
assert!(hit_test(&d, Point::new(5.0, 25.0), HitOptions { scope: Some(g), ..HitOptions::default() }).is_none());
|
||||
assert_eq!(at(5.0, None), vec![a_id]);
|
||||
assert!(at(90.0, None).is_empty());
|
||||
}
|
||||
|
||||
@@ -107,8 +107,14 @@ pub struct RenderOptions {
|
||||
/// Images are sampled smoothly when scaled or rotated; off, each pixel takes its nearest image
|
||||
/// pixel (Pixel Preview with File Handling › Display Bitmaps as Anti-aliased Images off).
|
||||
pub smooth_images: bool,
|
||||
/// Screen view in isolation mode: the isolated group or layer. Everything around it draws
|
||||
/// dimmed ([`ISOLATION_DIM`]).
|
||||
pub isolated: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// The opacity of the art around an isolated group or layer.
|
||||
pub const ISOLATION_DIM: f32 = 0.5;
|
||||
|
||||
/// How edges are rasterized (raster export option).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum AntiAlias {
|
||||
@@ -174,6 +180,7 @@ impl Default for RenderOptions {
|
||||
progressive_placed: false,
|
||||
trace_views: false,
|
||||
smooth_images: true,
|
||||
isolated: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +362,11 @@ pub struct Renderer {
|
||||
/// Inline graphics being drawn inside inline graphics (a symbol whose art holds text showing
|
||||
/// it): drawing stops at [`MAX_INLINE_DEPTH`].
|
||||
inline_depth: u32,
|
||||
/// [`RenderOptions::isolated`]'s layers and groups, from the top layer down to it, for the
|
||||
/// frame (`stamp`) and document they were found in.
|
||||
isolation: Option<(u64, usize, NodeId, Vec<NodeId>)>,
|
||||
/// Drawing inside the isolated container or a dimmed object: nothing further dims.
|
||||
isolation_settled: bool,
|
||||
}
|
||||
|
||||
/// How deep inline graphics nest (text in a symbol shown inline in text…) before they draw nothing.
|
||||
@@ -441,6 +453,8 @@ impl Renderer {
|
||||
adjusted: Default::default(),
|
||||
dim_images: None,
|
||||
inline_depth: 0,
|
||||
isolation: None,
|
||||
isolation_settled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,6 +890,46 @@ impl Renderer {
|
||||
|
||||
/// [`Self::draw_node`] after culling and Layer Options.
|
||||
fn draw_layer_node(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
|
||||
// Isolation mode: the art around the isolated container draws dimmed (#833).
|
||||
if let Some((isolated, path)) = self.isolation_path(f) {
|
||||
// A container on the way down to it draws as usual: its members decide.
|
||||
if path.contains(&n.id) && n.id != isolated {
|
||||
return self.draw_layer_node_now(ctx, f, n);
|
||||
}
|
||||
self.isolation_settled = true;
|
||||
if n.id == isolated {
|
||||
self.draw_layer_node_now(ctx, f, n);
|
||||
} else {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| r.draw_layer_node_now(c, fr, n));
|
||||
}
|
||||
self.isolation_settled = false;
|
||||
return;
|
||||
}
|
||||
self.draw_layer_node_now(ctx, f, n);
|
||||
}
|
||||
|
||||
/// In isolation mode, while drawing outside the isolated container and the art dimmed around
|
||||
/// it: the isolated container and its layers and groups, from the top layer down to it.
|
||||
fn isolation_path(&mut self, f: &Frame) -> Option<(NodeId, Vec<NodeId>)> {
|
||||
let isolated = f.opts.isolated.filter(|_| !self.isolation_settled)?;
|
||||
let doc = std::ptr::from_ref(f.doc) as usize;
|
||||
if !self.isolation.as_ref().is_some_and(|(stamp, d, id, _)| (*stamp, *d, *id) == (self.stamp, doc, isolated)) {
|
||||
self.isolation = Some((self.stamp, doc, isolated, f.doc.ancestry(isolated).unwrap_or_default()));
|
||||
}
|
||||
let path = self.isolation.as_ref().map(|(.., path)| path.clone()).filter(|p| !p.is_empty())?;
|
||||
Some((isolated, path))
|
||||
}
|
||||
|
||||
/// `draw` at [`ISOLATION_DIM`], with nothing inside dimmed again.
|
||||
fn dimmed(&mut self, ctx: &mut RenderContext, f: &Frame, draw: &mut group::Content) {
|
||||
let settled = std::mem::replace(&mut self.isolation_settled, true);
|
||||
let comp = Composite { opacity: ISOLATION_DIM, ..Default::default() };
|
||||
self.group(ctx, f, comp, draw);
|
||||
self.isolation_settled = settled;
|
||||
}
|
||||
|
||||
/// [`Self::draw_layer_node`] past isolation mode's dimming.
|
||||
fn draw_layer_node_now(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
|
||||
if fx::has_object_fx(n) {
|
||||
return self.draw_object_fx(ctx, f, &Arc::new(n.clone()), false);
|
||||
}
|
||||
@@ -998,6 +1052,22 @@ impl Renderer {
|
||||
fn draw_children(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], knockout: bool) {
|
||||
if knockout {
|
||||
self.draw_knockout(ctx, f, children);
|
||||
} else if let Some((_, path)) = self.isolation_path(f) {
|
||||
// Isolation mode, in a container on the way down to the isolated one: each run of
|
||||
// members around it draws dimmed as one (#833).
|
||||
let mut rest = children;
|
||||
while let Some(i) = rest.iter().position(|c| path.contains(&c.id)) {
|
||||
let (around, from) = rest.split_at(i);
|
||||
if !around.is_empty() {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| around.iter().for_each(|n| r.draw_arc(c, fr, n)));
|
||||
}
|
||||
let Some((on_path, after)) = from.split_first() else { break };
|
||||
self.draw_arc(ctx, f, on_path);
|
||||
rest = after;
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| rest.iter().for_each(|n| r.draw_arc(c, fr, n)));
|
||||
}
|
||||
} else {
|
||||
for c in children {
|
||||
self.draw_arc(ctx, f, c);
|
||||
|
||||
@@ -218,3 +218,30 @@ fn copying_the_backdrop_changes_nothing_without_blending() {
|
||||
let worst = a.iter().zip(&b).map(|(x, y)| (*x as i32 - *y as i32).abs()).max().unwrap();
|
||||
assert!(worst <= 1, "differs by {worst}/255");
|
||||
}
|
||||
|
||||
/// Isolation mode (#833): the art around the isolated group draws at half opacity, above it as
|
||||
/// well as below; the group's own art as usual.
|
||||
#[test]
|
||||
fn isolation_mode_dims_the_art_around_the_isolated_group() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let below = rect(&mut d, Rect::new(0.0, 0.0, 40.0, 40.0), [0.0, 0.0, 0.0]);
|
||||
let inside = rect(&mut d, Rect::new(60.0, 60.0, 100.0, 100.0), [0.0, 0.0, 0.0]);
|
||||
let above = rect(&mut d, Rect::new(60.0, 0.0, 100.0, 40.0), [0.0, 0.0, 0.0]);
|
||||
let g = group(&mut d, vec![inside], 1.0, Blend::Normal, false);
|
||||
let gid = g.id;
|
||||
for n in [below, g, above] {
|
||||
d.insert(Some(l), usize::MAX, n).unwrap();
|
||||
}
|
||||
let alpha = |img: &Rendered, x: usize, y: usize| img.pixels[(y * 100 + x) * 4 + 3];
|
||||
let plain = render(&d);
|
||||
let isolated = |id| render_with(&mut Renderer::new(), &d, &RenderOptions { isolated: Some(id), ..Default::default() });
|
||||
let dimmed = isolated(gid);
|
||||
assert_eq!(alpha(&dimmed, 80, 80), 255, "the isolated group's art");
|
||||
for (x, y) in [(20, 20), (80, 20)] {
|
||||
assert_eq!(alpha(&plain, x, y), 255);
|
||||
assert!((120..=135).contains(&alpha(&dimmed, x, y)), "({x}, {y}): {}", alpha(&dimmed, x, y));
|
||||
}
|
||||
// An object that isn't in the document dims nothing.
|
||||
assert_eq!(isolated(NodeId(u64::MAX)).pixels, plain.pixels);
|
||||
}
|
||||
@@ -249,6 +249,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
placed: vectorcraft_render::placed_document::generation(),
|
||||
pixel,
|
||||
smooth_images,
|
||||
isolated: st.isolation.map(|id| id.0),
|
||||
};
|
||||
// Placed documents' bitmaps are being made: draw again when they are ready.
|
||||
if vectorcraft_render::placed_document::busy() {
|
||||
@@ -298,6 +299,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
progressive_placed: true,
|
||||
trace_views: true,
|
||||
smooth_images,
|
||||
// Isolation mode: the art around the isolated group or layer is dimmed (#833).
|
||||
isolated: key.isolated.map(vectorcraft_doc::NodeId),
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
|
||||
@@ -1498,6 +1498,7 @@
|
||||
More Más
|
||||
More Pathfinder options Más opciones de Buscatrazos
|
||||
More Align options Más opciones de alineación
|
||||
More Paragraph options Más opciones de párrafo
|
||||
More Settings Más ajustes
|
||||
Move Mover
|
||||
Move All Objects Mover todos los objetos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1497,6 +1497,7 @@
|
||||
More Plus
|
||||
More Pathfinder options Plus d’options Pathfinder
|
||||
More Align options Plus d’options d’alignement
|
||||
More Paragraph options Plus d’options de paragraphe
|
||||
More Settings Plus de réglages
|
||||
Move Déplacer
|
||||
Move All Objects Déplacer tous les objets
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1498,6 +1498,7 @@
|
||||
More Altro
|
||||
More Pathfinder options Altre opzioni di Elaborazione tracciati
|
||||
More Align options Altre opzioni di allineamento
|
||||
More Paragraph options Altre opzioni di paragrafo
|
||||
More Settings Altre impostazioni
|
||||
Move Sposta
|
||||
Move All Objects Sposta tutti gli oggetti
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1640,6 +1640,7 @@
|
||||
More 多く
|
||||
More Pathfinder options その他のパスファインダーオプション
|
||||
More Align options その他の整列オプション
|
||||
More Paragraph options その他の段落オプション
|
||||
More Settings その他の設定
|
||||
Move 移動
|
||||
Move Anchors アンカーポイントを移動
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1314,6 +1314,7 @@
|
||||
More Mais
|
||||
More Pathfinder options Mais opções de Localizador de Caminhos
|
||||
More Align options Mais opções de alinhamento
|
||||
More Paragraph options Mais opções de parágrafo
|
||||
More Settings Mais Configurações
|
||||
Move Mover
|
||||
Move Anchors Mover Âncoras
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1496,6 +1496,7 @@
|
||||
More Больше
|
||||
More Pathfinder options Дополнительные параметры обработки контуров
|
||||
More Align options Дополнительные параметры выравнивания
|
||||
More Paragraph options Дополнительные параметры абзаца
|
||||
More Settings Дополнительные настройки
|
||||
Move Переместить
|
||||
Move All Objects Переместить все объекты
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1492,6 +1492,7 @@
|
||||
More Більше
|
||||
More Pathfinder options Додаткові параметри обробки контурів
|
||||
More Align options Додаткові параметри вирівнювання
|
||||
More Paragraph options Додаткові параметри абзацу
|
||||
More Settings Додаткові налаштування
|
||||
Move Перемістити
|
||||
Move All Objects Перемістити всі об’єкти
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 632 and column 40.
|
@@ -1456,6 +1456,7 @@
|
||||
More 较多
|
||||
More Pathfinder options 更多路径查找器选项
|
||||
More Align options 更多对齐选项
|
||||
More Paragraph options 更多段落选项
|
||||
More Settings 更多设置
|
||||
Move 移动
|
||||
Move All Objects 移动所有对象
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1498,6 +1498,7 @@
|
||||
More 更多
|
||||
More Pathfinder options 更多路徑管理員選項
|
||||
More Align options 更多對齊選項
|
||||
More Paragraph options 更多段落選項
|
||||
More Settings 更多設定
|
||||
Move 移動
|
||||
Move All Objects 移動所有物件
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -293,6 +293,8 @@ pub struct CacheKey {
|
||||
pub pixel: Option<[i64; 4]>,
|
||||
/// Images sampled smoothly ([`vectorcraft_render::RenderOptions::smooth_images`]).
|
||||
pub smooth_images: bool,
|
||||
/// Isolation mode's group or layer ([`vectorcraft_render::RenderOptions::isolated`]).
|
||||
pub isolated: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
|
||||
@@ -60,6 +60,24 @@ fn shown_alignment(justify: Justify, rtl: bool) -> Justify {
|
||||
}
|
||||
}
|
||||
|
||||
/// The seven alignment buttons, `size` points square, the one of `para`'s alignment on (the
|
||||
/// Paragraph panel's and the Properties panel's).
|
||||
pub fn alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32) {
|
||||
let shown = shown_alignment(para.justify, resolved_rtl(app, para));
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, size).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Hyphenate checkbox of `para`'s paragraphs.
|
||||
pub fn hyphenate_check(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle) {
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some((_, para)) = text_style(app) else {
|
||||
@@ -67,14 +85,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
};
|
||||
let rtl = resolved_rtl(app, ¶);
|
||||
let shown = shown_alignment(para.justify, rtl);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, 28.0).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
alignment_buttons(app, ui, ¶, 28.0);
|
||||
// Paragraph direction, with the Middle Eastern (Indic) options.
|
||||
if app.session.prefs.show_indic_options {
|
||||
ui.add_space(6.0);
|
||||
@@ -127,9 +140,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if pstate::<bool>(ui.ctx(), "pa-hide-options") {
|
||||
return;
|
||||
}
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
hyphenate_check(app, ui, ¶);
|
||||
// Japanese composition: how punctuation is spaced (JLREQ 3.1), with the East Asian options.
|
||||
if app.session.prefs.show_east_asian_options {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -523,13 +523,17 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
super::character::font_pickers(app, ui, &s, ("font", "font-style"), (w - 4.0, w - 4.0));
|
||||
super::character::metrics_grid(app, ui, "props-char", &s, w, false);
|
||||
section_header(ui, tl!("Paragraph"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, j) in [("align-start-vertical", "left"), ("align-center-vertical", "center"), ("align-end-vertical", "right")] {
|
||||
if widgets::icon_button(ui, icon, j, false, 24.0).clicked() {
|
||||
app.run("text.setStyle", json!({"justify": j})).ok();
|
||||
// As in the Paragraph panel: the seven alignments and Hyphenate (#775); the rest is there.
|
||||
if let Some((_, para)) = super::character::text_style(app) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
super::paragraph::alignment_buttons(app, ui, ¶, 24.0);
|
||||
if widgets::icon_button(ui, "ellipsis", tl!("More Paragraph options"), false, 24.0).clicked() {
|
||||
app.ui.open_panel = Some("paragraph".into());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
super::paragraph::hyphenate_check(app, ui, ¶);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -592,6 +596,20 @@ mod tests {
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
/// #775: type's Paragraph section has Hyphenate, and its checkbox turns it on.
|
||||
#[test]
|
||||
fn type_paragraph_section_hyphenates() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
app.run("text.create", json!({"x": 10, "y": 50, "text": "Paragraph"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Hyphenate");
|
||||
let hyphenate = |app: &VectorcraftApp| crate::panels::character::text_style(app).unwrap().1.hyphenate;
|
||||
assert!(hyphenate(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_artboards_adds_artboards_and_exits() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
Reference in new issue
Block a user