mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:33:54 +00:00
Merge main into fix/save-screens-unknown-options
This commit is contained in:
@@ -71,24 +71,39 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
/// other open documents (or an earlier active document) defined. Run after each command and when
|
||||
/// another document takes the front, so an undo, a revert, an opened or closed file or a document
|
||||
/// switch draws type in the active document's own composite fonts, and type naming one it does
|
||||
/// not define shows as missing. Whether a composite font was added, changed or removed.
|
||||
pub(crate) fn sync(s: &mut Session) -> bool {
|
||||
/// not define shows as missing. The lowercased names of the composite fonts added, changed or
|
||||
/// removed.
|
||||
pub(crate) fn sync(s: &mut Session) -> Vec<String> {
|
||||
let db = FontDb::global();
|
||||
let mut names = Vec::new();
|
||||
let mut changed = false;
|
||||
let mut changed = Vec::new();
|
||||
for f in s.active().map(|d| d.doc.composite_fonts.to_vec()).unwrap_or_default() {
|
||||
changed |= db.set_composite_font(&f);
|
||||
names.push(f.name.to_lowercase());
|
||||
let name = f.name.to_lowercase();
|
||||
if db.set_composite_font(&f) {
|
||||
changed.push(name.clone());
|
||||
}
|
||||
names.push(name);
|
||||
}
|
||||
let others = s.docs.iter().flat_map(|d| d.doc.composite_fonts.iter().map(|f| f.name.to_lowercase()));
|
||||
let stale: Vec<String> = s.composite_names.iter().cloned().chain(others).filter(|n| !names.contains(n)).collect();
|
||||
for gone in stale {
|
||||
changed |= db.remove_composite_font(&gone);
|
||||
if db.remove_composite_font(&gone) {
|
||||
changed.push(gone);
|
||||
}
|
||||
}
|
||||
s.composite_names_registered.extend(names.iter().chain(&changed).cloned());
|
||||
s.composite_names = names;
|
||||
changed
|
||||
}
|
||||
|
||||
/// The lowercased names of the composite fonts the open documents define, the font database holds
|
||||
/// and the session has registered before: the names type may have been laid out in.
|
||||
pub(crate) fn names_in_use(s: &Session) -> std::collections::HashSet<String> {
|
||||
let defined = s.docs.iter().flat_map(|d| d.doc.composite_fonts.iter().map(|f| f.name.to_lowercase()));
|
||||
let held = FontDb::global().composite_font_names().into_iter().map(|n| n.to_lowercase());
|
||||
defined.chain(held).chain(s.composite_names_registered.iter().cloned()).collect()
|
||||
}
|
||||
|
||||
/// Leave out a file's unusable composite fonts before registering and measuring its type.
|
||||
pub(crate) fn filter_loaded(doc: &mut vectorcraft_doc::Document) {
|
||||
let db = FontDb::global();
|
||||
@@ -172,6 +187,9 @@ fn replace(s: &mut Session, label: &str, after: Vec<CompositeFont>) -> Result<()
|
||||
}
|
||||
let changed: Vec<String> =
|
||||
before.iter().filter(|b| !after.contains(b)).chain(after.iter().filter(|a| !before.contains(a))).map(|f| f.name.to_lowercase()).collect();
|
||||
// A refused or panicked edit leaves the document without the new fonts: `sync` forgets a name
|
||||
// listed here that the document doesn't define.
|
||||
s.composite_names.extend(after.iter().map(|f| f.name.to_lowercase()));
|
||||
let db = FontDb::global();
|
||||
for f in &before {
|
||||
if !after.iter().any(|a| CompositeFont::same_name(&a.name, &f.name)) {
|
||||
|
||||
@@ -395,6 +395,150 @@ fn opened_document_measures_missing_composite_font_as_missing() {
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// The native file of a document whose composite font `name` sets Roman at full size, with type
|
||||
/// "Ab12" set in it → (the file, the type's id, its width).
|
||||
fn full_roman_file(name: &str) -> (Vec<u8>, u64, f64) {
|
||||
let mut s = session();
|
||||
let mut full = font(name);
|
||||
full["parts"]["roman"]["size"] = json!(100);
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [full]})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": name})).unwrap()["id"].as_u64().unwrap();
|
||||
let width = text(&s, id).cached_bounds.unwrap().width();
|
||||
(vectorcraft_format::save_file(&s.doc().unwrap().doc), id, width)
|
||||
}
|
||||
|
||||
/// Type in a missing font that no composite font is named after keeps the bounds it was measured
|
||||
/// with when its file was read: opening the document does not lay it out again.
|
||||
#[test]
|
||||
fn opening_leaves_type_in_an_unrelated_missing_font_as_measured() {
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Unrelated")]})).unwrap();
|
||||
let id =
|
||||
s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab", "size": 20, "font": "No Such Family CF"})).unwrap()["id"].as_u64().unwrap();
|
||||
let mut doc = (*s.doc().unwrap().doc).clone();
|
||||
let kept = vectorcraft_geom::Rect::new(1.0, 2.0, 3.0, 4.0);
|
||||
if let NodeKind::Text(t) = &mut doc.node_mut(NodeId(id)).unwrap().kind {
|
||||
t.cached_bounds = Some(kept);
|
||||
}
|
||||
s.add_document(doc, None);
|
||||
assert_eq!(text(&s, id).cached_bounds, Some(kept));
|
||||
}
|
||||
|
||||
/// Text holding an inline graphic whose symbol's art is type in a composite font is measured with
|
||||
/// the graphic at the size the opened document's own definition gives it.
|
||||
#[test]
|
||||
fn opening_measures_inline_graphics_of_composite_type_in_its_own_definition() {
|
||||
let mut s = session();
|
||||
let mut full = font("CF Inline");
|
||||
full["parts"]["roman"]["size"] = json!(100);
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [full]})).unwrap();
|
||||
let art = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": "CF Inline"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "CF Inline Mark", "ids": [art]})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 150, "text": "x", "size": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": id, "at": 0, "symbol": "CF Inline Mark"})).unwrap();
|
||||
let width = text(&s, id).cached_bounds.unwrap().width();
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Inline")]})).unwrap();
|
||||
let file = crate::cmd::fileio::read_for_open("inline.vectorcraft", &bytes, None, &json!({}), false, false).unwrap();
|
||||
crate::cmd::fileio::open_read(&mut s, file).unwrap();
|
||||
assert!((text(&s, id).cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Type read for opening while a composite font of its name was registered is measured again when
|
||||
/// it opens, even after the document that defined the font has closed in between.
|
||||
#[test]
|
||||
fn opening_measures_type_read_in_a_composite_font_closed_since() {
|
||||
let mut s = session();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": "CF Gone"})).unwrap()["id"].as_u64().unwrap();
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Gone")]})).unwrap();
|
||||
let file = crate::cmd::fileio::read_for_open("gone.vectorcraft", &bytes, None, &json!({}), false, false).unwrap();
|
||||
assert!(s.close_document(1));
|
||||
crate::cmd::fileio::open_read(&mut s, file).unwrap();
|
||||
let actual = text(&s, id);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &actual).bounds;
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// A file read for opening lays out its type in the composite fonts registered then (the front
|
||||
/// document's, which defines the name with Roman at half size); once the file opens, its type is
|
||||
/// measured again in its own.
|
||||
#[test]
|
||||
fn an_opened_file_measures_its_type_in_its_own_composite_fonts() {
|
||||
let (bytes, id, width) = full_roman_file("CF Opened");
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Opened")]})).unwrap();
|
||||
let file = crate::cmd::fileio::read_for_open("opened.vectorcraft", &bytes, None, &json!({}), false, false).unwrap();
|
||||
crate::cmd::fileio::open_read(&mut s, file).unwrap();
|
||||
let actual = text(&s, id);
|
||||
assert!((actual.cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &actual).bounds;
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// A document replaced while in the back (File › Revert) is measured in its own composite fonts
|
||||
/// when it comes to the front, not in the front document's.
|
||||
#[test]
|
||||
fn a_document_replaced_in_the_back_is_measured_in_its_own_composite_fonts() {
|
||||
let (bytes, id, width) = full_roman_file("CF Replaced");
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Replaced")]})).unwrap();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Replaced")]})).unwrap();
|
||||
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
|
||||
assert!(s.set_active(0));
|
||||
let actual = text(&s, id);
|
||||
assert!((actual.cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &actual).bounds;
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// So is one that comes to the front because the front document closes.
|
||||
#[test]
|
||||
fn a_document_replaced_in_the_back_is_measured_when_the_front_one_closes() {
|
||||
let (bytes, id, width) = full_roman_file("CF Closed");
|
||||
let mut s = session();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Closed")]})).unwrap();
|
||||
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
|
||||
assert!(s.close_document(1));
|
||||
let actual = text(&s, id);
|
||||
assert!((actual.cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &actual).bounds;
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// A failed batch takes back the composite fonts it set, in the font database too.
|
||||
#[test]
|
||||
fn a_failed_batch_takes_back_its_composite_fonts() {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let mut s = session();
|
||||
let steps = json!([
|
||||
{"command": "type.compositeFonts.set", "params": {"fonts": [font("CF Failed Batch")]}},
|
||||
{"command": "no.such.command"},
|
||||
]);
|
||||
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
|
||||
assert!(names(&s).is_empty());
|
||||
assert!(db.composite_font("CF Failed Batch").is_none());
|
||||
}
|
||||
|
||||
/// A composite font whose edit is refused, because it makes selected type too wide for the canvas,
|
||||
/// is not left in the font database.
|
||||
#[test]
|
||||
fn a_refused_edit_takes_back_its_new_composite_font() {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let mut s = session();
|
||||
let x = crate::MAX_COORD - 1000.0;
|
||||
s.execute("text.create", &json!({"x": x, "y": 50, "text": "MMMMMMMM", "size": 100, "font": "CF Refused Edit"})).unwrap();
|
||||
let wide = json!({"name": "CF Refused Edit", "parts": {"kanji": {"font": "Source Sans 3", "size": 400}}});
|
||||
let e = s.execute("type.compositeFonts.set", &json!({"fonts": [wide]})).unwrap_err().to_string();
|
||||
assert!(e.contains("exceed the canvas"), "{e}");
|
||||
assert!(names(&s).is_empty());
|
||||
assert!(db.composite_font("CF Refused Edit").is_none());
|
||||
}
|
||||
|
||||
/// A loaded composite font cannot replace an installed family used by existing type.
|
||||
#[test]
|
||||
fn loaded_composite_font_cannot_take_an_installed_family_name() {
|
||||
|
||||
@@ -175,6 +175,9 @@ pub struct Session {
|
||||
/// The composite fonts (lowercase names) that the open documents defined when the font
|
||||
/// database was last brought in step with them ([`cmd::compositefonts::sync`]).
|
||||
pub(crate) composite_names: Vec<String>,
|
||||
/// Every composite font (lowercase name) this session has registered in the font database:
|
||||
/// type read for opening may have been laid out in one that is gone by the time it opens.
|
||||
pub(crate) composite_names_registered: std::collections::HashSet<String>,
|
||||
/// While a command that changes nothing in the document runs (a query, or one of
|
||||
/// [`command_sets::JOURNALED_READS`]): it reads the instance of an object selected in a symbol
|
||||
/// instance's art.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use vectorcraft_doc::{Node, TextObject};
|
||||
|
||||
use super::{
|
||||
Arc, Clipboard, Color, CommandInfo, DocState, Document, DrawMode, EngineError, GradientPaint, Journal, NodeKind, PaintDefaults, Prefs, Result,
|
||||
Session, ViewInfo, cmd, command_specs, file_access,
|
||||
@@ -84,6 +88,7 @@ impl Session {
|
||||
batch_steps: None,
|
||||
batch_paint_docs: Vec::new(),
|
||||
composite_names: Vec::new(),
|
||||
composite_names_registered: HashSet::new(),
|
||||
reading: false,
|
||||
place_hand_off: None,
|
||||
queue_hand_off: None,
|
||||
@@ -182,10 +187,8 @@ impl Session {
|
||||
self.reset_tool_for_doc_switch();
|
||||
}
|
||||
self.active = Some(index);
|
||||
// Type is drawn in the composite fonts of the document now in front, and measured again
|
||||
// when they differ from those of the document before.
|
||||
if switched && cmd::compositefonts::sync(self) {
|
||||
self.refresh_document_text_bounds(index);
|
||||
if switched {
|
||||
self.sync_front_composite_fonts();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -193,6 +196,16 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the composite fonts of the document now in front ([`cmd::compositefonts::sync`]),
|
||||
/// after a switch or a close brought it there. Its type set in a composite font that differs
|
||||
/// from the one of that name before (added, changed or removed) is measured again, and type not
|
||||
/// measured yet (in a document replaced while in the back) is measured now.
|
||||
fn sync_front_composite_fonts(&mut self) {
|
||||
let changed: HashSet<String> = cmd::compositefonts::sync(self).into_iter().collect();
|
||||
let Some(index) = self.active else { return };
|
||||
self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() || sets_in_any(t, &changed));
|
||||
}
|
||||
|
||||
/// The name [`Session::next_untitled`] gives next (New Document's Name field).
|
||||
pub fn peek_untitled(&self) -> String {
|
||||
format!("Untitled-{}", self.untitled_counter + 1)
|
||||
@@ -211,19 +224,44 @@ impl Session {
|
||||
pub(crate) fn refresh_text_bounds(doc: &mut Document) {
|
||||
// Inline graphics in text take their size from their symbols' art (not saved either).
|
||||
doc.resolve_inline_art();
|
||||
let trees = doc.layers.iter_mut().chain(doc.symbols.iter_mut().map(|s| &mut s.art));
|
||||
let trees = trees.chain(doc.graph_designs.iter_mut().map(|g| &mut g.art));
|
||||
for tree in trees.chain(doc.patterns.iter_mut().flat_map(|p| p.art.iter_mut())) {
|
||||
refresh_tree_text_bounds(tree);
|
||||
let mut uncached = |t: &TextObject| t.cached_bounds.is_none();
|
||||
for tree in text_trees_mut(doc) {
|
||||
each_text_where(tree, &mut uncached, &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Session::refresh_text_bounds`] for open document `index`, as a change that is not an
|
||||
/// edit: a saved document stays saved, and its undo history keeps its documents as they are.
|
||||
fn refresh_document_text_bounds(&mut self, index: usize) {
|
||||
if let Some(st) = self.docs.get_mut(index) {
|
||||
st.change_unsaved(|doc| Self::refresh_text_bounds(Arc::make_mut(doc)));
|
||||
/// Measure the type in open document `index` that `stale` picks (in its trees as
|
||||
/// [`Session::refresh_text_bounds`] walks them), as a change that is not an edit: a saved
|
||||
/// document stays saved, and its undo history keeps its documents as they are. The document is
|
||||
/// left as it is when `stale` picks nothing.
|
||||
fn refresh_document_text_bounds(&mut self, index: usize, mut stale: impl FnMut(&TextObject) -> bool) {
|
||||
let Some(st) = self.docs.get_mut(index) else { return };
|
||||
if !text_trees(&st.doc).any(|n| has_text_where(n, &mut stale)) {
|
||||
return;
|
||||
}
|
||||
st.change_unsaved(|doc| {
|
||||
let doc = Arc::make_mut(doc);
|
||||
for tree in text_trees_mut(doc) {
|
||||
each_text_where(tree, &mut stale, &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
// Inline graphics take their size from their symbols' art, whose type may just have
|
||||
// been measured again: the text holding one whose size changed is measured again too.
|
||||
if !doc.resolve_inline_art().is_empty() {
|
||||
for tree in text_trees_mut(doc) {
|
||||
each_text_where(tree, &mut |t| t.cached_bounds.is_none(), &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Register the composite fonts of open document `index`, which is in front, and measure its
|
||||
/// type not measured yet and its type set in a font of a name that a composite font of an open
|
||||
/// document or of the font database has, or had before: that type may have been laid out in
|
||||
/// another document's composite fonts (those registered while its file was read for opening).
|
||||
fn sync_opened_composite_fonts(&mut self, index: usize) {
|
||||
let mut names = cmd::compositefonts::names_in_use(self);
|
||||
names.extend(cmd::compositefonts::sync(self));
|
||||
self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() || sets_in_any(t, &names));
|
||||
}
|
||||
|
||||
/// Add a document and make it active. Between `edit.undoGroup.begin` and `edit.undoGroup.end`,
|
||||
@@ -234,6 +272,8 @@ impl Session {
|
||||
// (importers and the native reader have seen to theirs already, with a warning).
|
||||
doc.limit_depth();
|
||||
cmd::compositefonts::filter_loaded(&mut doc);
|
||||
// Inline graphics in text take their size from their symbols' art (not saved).
|
||||
doc.resolve_inline_art();
|
||||
// Guides from files saved before guides had layers go on a layer.
|
||||
doc.adopt_guides();
|
||||
cmd::dimension::conform(&mut doc);
|
||||
@@ -244,8 +284,7 @@ impl Session {
|
||||
self.docs.push(st);
|
||||
let i = self.docs.len() - 1;
|
||||
self.active = Some(i);
|
||||
cmd::compositefonts::sync(self);
|
||||
self.refresh_document_text_bounds(i);
|
||||
self.sync_opened_composite_fonts(i);
|
||||
if self.depth == 0
|
||||
&& let Some(label) = self.explicit_undo_group.clone()
|
||||
{
|
||||
@@ -262,11 +301,26 @@ impl Session {
|
||||
}
|
||||
doc.limit_depth();
|
||||
cmd::compositefonts::filter_loaded(&mut doc);
|
||||
doc.resolve_inline_art();
|
||||
doc.adopt_guides();
|
||||
cmd::dimension::conform(&mut doc);
|
||||
// In the back, type set in a font named like a composite font is measured when the document
|
||||
// comes to the front and its own composite fonts are registered, not in the front
|
||||
// document's.
|
||||
let mut back_names = None;
|
||||
if self.active == Some(index) {
|
||||
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
||||
self.reset_tool_for_doc_switch();
|
||||
} else {
|
||||
let mut names = cmd::compositefonts::names_in_use(self);
|
||||
names.extend(doc.composite_fonts.iter().map(|f| f.name.to_lowercase()));
|
||||
for tree in text_trees_mut(&mut doc) {
|
||||
each_text_where(tree, &mut |t| t.cached_bounds.is_some() && sets_in_any(t, &names), &mut |t| {
|
||||
t.cached_bounds = None;
|
||||
t.cached_baselines.clear();
|
||||
});
|
||||
}
|
||||
back_names = Some(names);
|
||||
}
|
||||
let old = &self.docs[index];
|
||||
let mut st = DocState::new(doc, old.path.clone());
|
||||
@@ -297,12 +351,10 @@ impl Session {
|
||||
if let Some(stash) = &mut self.batch_stash {
|
||||
stash.push(old);
|
||||
}
|
||||
// Type in the composite fonts of a document in the back is measured again when it comes to
|
||||
// the front.
|
||||
if self.active == Some(index) {
|
||||
cmd::compositefonts::sync(self);
|
||||
match back_names {
|
||||
Some(names) => self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() && !sets_in_any(t, &names)),
|
||||
None => self.sync_opened_composite_fonts(index),
|
||||
}
|
||||
self.refresh_document_text_bounds(index);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -319,7 +371,7 @@ impl Session {
|
||||
stash.push(old);
|
||||
}
|
||||
self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) };
|
||||
cmd::compositefonts::sync(self);
|
||||
self.sync_front_composite_fonts();
|
||||
true
|
||||
}
|
||||
|
||||
@@ -351,27 +403,49 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay out the type in tree `n` (opacity mask art included) whose bounds aren't cached. Only the
|
||||
/// nodes on the way to such type are copied.
|
||||
fn refresh_tree_text_bounds(n: &mut Arc<vectorcraft_doc::Node>) {
|
||||
fn stale(n: &vectorcraft_doc::Node) -> bool {
|
||||
matches!(&n.kind, NodeKind::Text(t) if t.cached_bounds.is_none())
|
||||
|| n.extras().mask.as_deref().is_some_and(|m| stale(&m.art))
|
||||
|| n.children().is_some_and(|ch| ch.iter().any(|c| stale(c)))
|
||||
}
|
||||
if !stale(n) {
|
||||
/// Whether some of type `t` is set in a font named (in any case) one of `names`, lowercased.
|
||||
fn sets_in_any(t: &TextObject, names: &HashSet<String>) -> bool {
|
||||
!names.is_empty() && t.runs.iter().any(|r| names.contains(&r.style.font_family.to_lowercase()))
|
||||
}
|
||||
|
||||
/// The trees of `doc` that hold type: its layers, symbol definitions, graph designs and pattern
|
||||
/// tiles.
|
||||
fn text_trees(doc: &Document) -> impl Iterator<Item = &Arc<Node>> {
|
||||
let trees = doc.layers.iter().chain(doc.symbols.iter().map(|s| &s.art));
|
||||
let trees = trees.chain(doc.graph_designs.iter().map(|g| &g.art));
|
||||
trees.chain(doc.patterns.iter().flat_map(|p| p.art.iter()))
|
||||
}
|
||||
|
||||
/// [`text_trees`], to change.
|
||||
fn text_trees_mut(doc: &mut Document) -> impl Iterator<Item = &mut Arc<Node>> {
|
||||
let trees = doc.layers.iter_mut().chain(doc.symbols.iter_mut().map(|s| &mut s.art));
|
||||
let trees = trees.chain(doc.graph_designs.iter_mut().map(|g| &mut g.art));
|
||||
trees.chain(doc.patterns.iter_mut().flat_map(|p| p.art.iter_mut()))
|
||||
}
|
||||
|
||||
/// Whether tree `n` (opacity mask art included) holds type that `pick` picks.
|
||||
fn has_text_where(n: &Node, pick: &mut dyn FnMut(&TextObject) -> bool) -> bool {
|
||||
matches!(&n.kind, NodeKind::Text(t) if pick(t))
|
||||
|| n.extras().mask.as_deref().is_some_and(|m| has_text_where(&m.art, pick))
|
||||
|| n.children().is_some_and(|ch| ch.iter().any(|c| has_text_where(c, pick)))
|
||||
}
|
||||
|
||||
/// Run `act` on the type in tree `n` (opacity mask art included) that `pick` picks. Only the nodes
|
||||
/// on the way to such type are copied.
|
||||
fn each_text_where(n: &mut Arc<Node>, pick: &mut dyn FnMut(&TextObject) -> bool, act: &mut dyn FnMut(&mut TextObject)) {
|
||||
if !has_text_where(n, pick) {
|
||||
return;
|
||||
}
|
||||
let n = Arc::make_mut(n);
|
||||
if let NodeKind::Text(t) = &mut n.kind
|
||||
&& t.cached_bounds.is_none()
|
||||
&& pick(t)
|
||||
{
|
||||
cmd::typecmd::refresh_bounds_for_first_frame(t);
|
||||
act(t);
|
||||
}
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) {
|
||||
refresh_tree_text_bounds(&mut m.art);
|
||||
each_text_where(&mut m.art, pick, act);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
refresh_tree_text_bounds(c);
|
||||
each_text_where(c, pick, act);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@ impl Session {
|
||||
let r = if self.depth == 0 { self.run_guarded(id, run) } else { self.run_nested(run) };
|
||||
if r.is_err() && self.depth == 0 {
|
||||
self.batch_paint_docs.clear();
|
||||
// A failed or panicked command's documents are put back: type is drawn in their
|
||||
// composite fonts again, not in those the command set.
|
||||
cmd::compositefonts::sync(self);
|
||||
}
|
||||
let r = r?;
|
||||
if self.depth == 0 {
|
||||
|
||||
@@ -31,6 +31,13 @@ impl FontDb {
|
||||
{
|
||||
let mut map = self.composites.write().unwrap_or_else(|e| e.into_inner());
|
||||
if map.get(&key).is_some_and(|f| **f == *font) {
|
||||
// The same definition a guard lends: it stays when the guard drops.
|
||||
let mut loans = self.composite_loans.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(loan) = loans.get_mut(&key)
|
||||
&& map.get(&key).is_some_and(|have| Arc::ptr_eq(have, &loan.font))
|
||||
{
|
||||
loan.adopted = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
map.insert(key, Arc::new(font.clone()));
|
||||
@@ -194,7 +201,7 @@ impl FontDb {
|
||||
}
|
||||
let font = Arc::new((*f).clone());
|
||||
let previous = map.insert(key.clone(), font.clone());
|
||||
loans.insert(key.clone(), CompositeLoan { font: font.clone(), previous, guards: 1 });
|
||||
loans.insert(key.clone(), CompositeLoan { font: font.clone(), previous, guards: 1, adopted: false });
|
||||
lent.push((key, font));
|
||||
changed = true;
|
||||
}
|
||||
@@ -300,6 +307,9 @@ pub(super) struct CompositeLoan {
|
||||
previous: Option<Arc<CompositeFont>>,
|
||||
/// How many guards lend it.
|
||||
guards: usize,
|
||||
/// Whether [`FontDb::set_composite_font`] set the same definition while it was lent: the last
|
||||
/// guard to drop then leaves it in place.
|
||||
adopted: bool,
|
||||
}
|
||||
|
||||
/// The composite fonts [`FontDb::lend_composite_fonts`] made available, given back when it drops.
|
||||
@@ -327,9 +337,11 @@ impl Drop for LentCompositeFonts<'_> {
|
||||
continue;
|
||||
}
|
||||
let previous = loan.previous.take();
|
||||
let adopted = loan.adopted;
|
||||
loans.remove(key);
|
||||
// Given back unless a font of that name has been set or removed since.
|
||||
if map.get(key).is_some_and(|have| Arc::ptr_eq(have, font)) {
|
||||
// Given back unless a font of that name has been set (the same definition included)
|
||||
// or removed since.
|
||||
if !adopted && map.get(key).is_some_and(|have| Arc::ptr_eq(have, font)) {
|
||||
match previous {
|
||||
Some(p) => map.insert(key.clone(), p),
|
||||
None => map.remove(key),
|
||||
|
||||
@@ -129,6 +129,13 @@ fn lent_composite_fonts_last_as_long_as_a_guard() {
|
||||
db.set_composite_font(&replaced);
|
||||
drop(lent);
|
||||
assert_eq!(db.composite_font("Lent").unwrap().parts.roman.size, 60.0);
|
||||
// So does the same definition set while lent, after every guard lending it drops.
|
||||
assert!(db.remove_composite_font("Lent"));
|
||||
let (one, two) = (db.lend_composite_fonts(&fonts[..1]), db.lend_composite_fonts(&fonts[..1]));
|
||||
assert!(!db.set_composite_font(&fonts[0]));
|
||||
drop(one);
|
||||
drop(two);
|
||||
assert!(db.composite_font("Lent").is_some());
|
||||
// A guard whose font was removed doesn't give back a later guard's loan of that name.
|
||||
assert!(db.remove_composite_font("Lent"));
|
||||
let old = db.lend_composite_fonts(&fonts[..1]);
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
CMYK Color CMYK ფერი
|
||||
RGB Color RGB ფერი
|
||||
File Info… ფაილის ინფორმაცია…
|
||||
Print… დაბეჭდვა…
|
||||
Print… ბეჭდვა…
|
||||
Edit რედაქტირება
|
||||
Undo დაბრუნება
|
||||
Redo ხელახლა შესრულება
|
||||
@@ -1259,7 +1259,7 @@ lighting Light სინათლე
|
||||
Dashed ტირეებიანი
|
||||
Delete Selected Item მონიშნული ელემენტის წაშლა
|
||||
Double-click to edit the characters' paint ორჯერ დაწკაპეთ სიმბოლოების შეღებვის რედაქტირებისთვის
|
||||
Click to edit დააწკაპეთ რედაქტირებისთვის
|
||||
Click to edit დაწკაპეთ დასარედაქტირებლად
|
||||
Double-click to edit ორჯერ დაწკაპეთ რედაქტირებისთვის
|
||||
Double-click to target the contents ორჯერ დაწკაპეთ შიგთავსის ასარჩევად
|
||||
Drag onto art to apply this appearance ჩაავლეთ ნახატზე ამ იერსახის გამოსაყენებლად
|
||||
@@ -3010,7 +3010,7 @@ theme Appearance იერსახე
|
||||
Reverse Order რიგის შებრუნება
|
||||
Show All Layers ყველა შრის ჩვენება
|
||||
Unlock All Layers ყველა შრის განბლოკვა
|
||||
English: USA English: USA
|
||||
English: USA ინგლისური: აშშ
|
||||
Move გადატანა
|
||||
Character Style სიმბოლოს სტილი
|
||||
Paragraph Style აბზაცის სტილი
|
||||
@@ -3427,7 +3427,7 @@ effect Points წერტილები
|
||||
Discard Empty Type ცარიელი ტექსტის მოცილება
|
||||
Copyright საავტორო უფლება
|
||||
Other სხვა
|
||||
Print დაბეჭდვა
|
||||
Print ბეჭდვა
|
||||
Solid მთლიანი
|
||||
Difference სხვაობა
|
||||
Flatten Transparency გამჭვირვალობის გაბრტყელება
|
||||
@@ -3663,7 +3663,7 @@ font-technology Variable ცვლადი
|
||||
Flip: არეკვლა:
|
||||
Spacing: ინტერვალი:
|
||||
Output გამოტანა
|
||||
Export… ექსპორტი…
|
||||
Export… ექსპორტირება…
|
||||
Import… იმპორტი…
|
||||
Load Profile… პროფილის ჩატვირთვა…
|
||||
Embedded ჩაშენებული
|
||||
@@ -4677,7 +4677,7 @@ font-technology Variable ცვლადი
|
||||
Bicubic ბიკუბური
|
||||
Minimum მინიმალური
|
||||
Run Length Run Length
|
||||
Roman Roman
|
||||
Roman რომაული
|
||||
No conversion გარდაქმნის გარეშე
|
||||
Convert to destination სამიზნეში გარდაქმნა
|
||||
Convert to destination (preserve numbers) სამიზნეში გარდაქმნა (რიცხვების შენარჩუნებით)
|
||||
@@ -6152,7 +6152,7 @@ anchor Points: წერტილები:
|
||||
Close Path: ტრაექტორიის შეკვრა:
|
||||
Keep Handles სახელურების შენარჩუნება
|
||||
Straight სწორი
|
||||
Stretch Curve მრუდის გაჭიმვა
|
||||
Stretch Curve მრუდის გაწელვა
|
||||
@msg direct-select the anchor points to remove პირდაპირ მონიშნეთ მოსაცილებელი საყრდენი წერტილები
|
||||
@msg select segments, or paths, to add points to მონიშნეთ სეგმენტები ან ტრაექტორიები წერტილების დასამატებლად
|
||||
@msg select open paths to close მონიშნეთ შესაკრავი ღია ტრაექტორიები
|
||||
@@ -6297,7 +6297,7 @@ anchor Points: წერტილები:
|
||||
Join Point Text წერტილის ტექსტის შეერთება
|
||||
Remove Breaks წყვეტების წაშლა
|
||||
Split Text at Cursor ტექსტის დაყოფა კურსორთან
|
||||
Set Alignment, Keep Position სწორების დაყენება მდებარეობის შენარჩუნებით
|
||||
Set Alignment, Keep Position სწორების დაყენება პოზიციის შენარჩუნებით
|
||||
Reset Text Transform ტექსტის გარდაქმნის ჩამოყრა
|
||||
Replace Missing Fonts… დაკარგული შრიფტების ჩანაცვლება…
|
||||
Replace Missing Fonts დაკარგული შრიფტების ჩანაცვლება
|
||||
@@ -6308,7 +6308,7 @@ anchor Points: წერტილები:
|
||||
Break Text into Words ტექსტის დაყოფა სიტყვებად
|
||||
Break Text into Glyphs ტექსტის დაყოფა გლიფებად
|
||||
Reset Text Transform and Scale ტექსტის გარდაქმნისა და მასშტაბის ჩამოყრა
|
||||
Set Alignment, Keep Position: სწორების დაყენება მდებარეობის შენარჩუნებით:
|
||||
Set Alignment, Keep Position: სწორების დაყენება პოზიციის შენარჩუნებით:
|
||||
Break Text into: ტექსტის დაყოფა:
|
||||
Split at Cursor დაყოფა კურსორთან
|
||||
Reset Transform გარდაქმნის ჩამოყრა
|
||||
@@ -6317,7 +6317,7 @@ anchor Points: წერტილები:
|
||||
breakText Lines სტრიქონები
|
||||
breakText Words სიტყვები
|
||||
breakText Glyphs გლიფები
|
||||
@msg Click in type with a Type tool to split it დასაყოფად დააწკაპუნეთ ტექსტში ტექსტის ხელსაწყოთი
|
||||
@msg Click in type with a Type tool to split it დასაყოფად დაწკაპეთ ტექსტში ტექსტის ხელსაწყოთი
|
||||
@msg Too many type objects to change at once ერთდროულად შესაცვლელად ძალიან ბევრი ტექსტური ობიექტია
|
||||
@msg Too many pieces to make at once ერთდროულად შესაქმნელად ძალიან ბევრი ნაწილია
|
||||
@msg Select horizontal point or area type whose text fits აირჩიეთ ჰორიზონტალური წერტილის ან არის ტექსტი, რომლის ტექსტიც ეტევა
|
||||
@@ -6417,9 +6417,9 @@ breakText Glyphs გლიფები
|
||||
Fit to Corners and Ends კუთხეებსა და ბოლოებზე მორგება
|
||||
Remove Gaps ღრეჩოების მოცილება
|
||||
@msg that makes too many pieces (at most 100000): make the dashes longer ეს ძალიან ბევრ ნაწილს იძლევა (მაქსიმუმ 100000): გაზარდეთ ტირეების სიგრძე
|
||||
@msg select paths, or segments, to divide აირჩიეთ დასაყოფი კონტურები ან სეგმენტები
|
||||
@msg select paths, or segments, to divide აირჩიეთ დასაყოფი ტრაექტორიები ან სეგმენტები
|
||||
# Path Precision Tools add-on: Expand Dashes into Paths.
|
||||
Expand Dashes into Paths ტირეების კონტურებად გაშლა
|
||||
Expand Dashes into Paths ტირეების ტრაექტორიებად გაშლა
|
||||
@msg select paths with dashed strokes to expand აირჩიეთ გასაშლელი ტირეებიანი ხაზის კონტურები
|
||||
# Document Tools add-on (Object › Plug-ins › Document Tools): Save a Timestamped Copy.
|
||||
Document Tools დოკუმენტის ხელსაწყოები
|
||||
@@ -6519,6 +6519,7 @@ font-script Japanese იაპონური
|
||||
Japanese Double Postcard იაპონური ორმაგი ღია ბარათი
|
||||
Japanese Envelope #3 იაპონური კონვერტი #3
|
||||
Japanese Envelope #4 იაპონური კონვერტი #4
|
||||
@msg image '{key}' has no pixels and was left out გამოსახულებას '{key}' პიქსელები არ აქვს და გამოტოვდა
|
||||
@msg not an RTF file: it doesn't start with {\\rtf RTF ფაილი არ არის: არ იწყება {\\rtf-ით
|
||||
# Type Tools add-on (Object › Plug-ins › Type Tools).
|
||||
Type Tools ტექსტის ხელსაწყოები
|
||||
@@ -6530,6 +6531,54 @@ font-script Japanese იაპონური
|
||||
Bow რკალი
|
||||
Spike პიკი
|
||||
Skip Spaces ჰარების გამოტოვება
|
||||
|
||||
# The strings the report listed as missing for ka.
|
||||
1 Layer შრე: 1
|
||||
Art and Bright Colors მხატვრული და კაშკაშა ფერები
|
||||
Automate ავტომატიზაცია
|
||||
Color Table (4 colors) ფერების ცხრილი (ფერი: 4)
|
||||
Current Colors (0) მიმდინარე ფერები (0)
|
||||
Differences from VectorCraft Default: განსხვავებები VectorCraft-ის ნაგულისხმევთან:
|
||||
Differences from [Default]: განსხვავებები [ნაგულისხმევი]-თან:
|
||||
Dot Gain 15% Dot Gain 15%
|
||||
Expand 1 Selected 1 მონიშნული ობიექტის გაშლა
|
||||
Graphic Styles (4) გრაფიკული სტილები (4)
|
||||
Gray Gamma 2.2 Gray Gamma 2.2
|
||||
Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono, Noto Sans SC (OFL). Icons: Lucide (ISC) + VectorCraft. ArtCraft-ის ნაწილი. MIT OR Apache-2.0. შრიფტები: Source Sans 3, Inter, JetBrains Mono, Noto Sans SC (OFL). ხატულები: Lucide (ISC) + VectorCraft.
|
||||
Phone 375×812 ტელეფონი 375×812
|
||||
Photo Low Fidelity დაბალი სიზუსტის ფოტო
|
||||
Print & Proof ბეჭდვა და კორექტურა
|
||||
Printing & Proofing ბეჭდვა და კორექტურა
|
||||
RECENT (1) ბოლოდროინდელი (1)
|
||||
Reset Automation Automation-ის ჩამოყრა
|
||||
Reset Essentials Classic Essentials Classic-ის ჩამოყრა
|
||||
Reset Layout Layout-ის ჩამოყრა
|
||||
Reset Painting Painting-ის ჩამოყრა
|
||||
Reset Printing and Proofing Printing and Proofing-ის ჩამოყრა
|
||||
Reset Tracing Tracing-ის ჩამოყრა
|
||||
Reset Typography Typography-ის ჩამოყრა
|
||||
Reset Web Web-ის ჩამოყრა
|
||||
Restricted Colors შეზღუდული ფერები
|
||||
Right-click an item to move it into this group. ამ ჯგუფში გადასატანად დაწკაპეთ ელემენტზე მარჯვენა ღილაკით.
|
||||
Scale X მასშტაბი X
|
||||
Scale X: მასშტაბი X:
|
||||
Scale Y მასშტაბი Y
|
||||
Scale Y: მასშტაბი Y:
|
||||
Selected Styles Only (0) მხოლოდ მონიშნული სტილები (0)
|
||||
Selected Swatches Only (0) მხოლოდ მონიშნული ფერის ნიმუშები (0)
|
||||
Spot Colors (0) სპოტ-ფერები (0)
|
||||
Tablet 1024×768 პლანშეტი 1024×768
|
||||
Video 1920×1080 ვიდეო 1920×1080
|
||||
Video 3840×2160 ვიდეო 3840×2160
|
||||
Web 1280×720 ვები 1280×720
|
||||
[High Resolution] [მაღალი გარჩევადობა]
|
||||
[Low Resolution] [დაბალი გარჩევადობა]
|
||||
[Medium Resolution] [საშუალო გარჩევადობა]
|
||||
[PDF/X-1a:2001] [PDF/X-1a:2001]
|
||||
[PDF/X-3:2002] [PDF/X-3:2002]
|
||||
[PDF/X-4:2010] [PDF/X-4:2010]
|
||||
[VectorCraft Default] [VectorCraft-ის ნაგულისხმევი]
|
||||
@msg give corners of the path: anchors between two straight sides მიუთითეთ ტრაექტორიის კუთხეები: საყრდენი წერტილები ორ სწორ გვერდს შორის
|
||||
Release Type on a Path… ტექსტის ტრაექტორიიდან მოხსნა…
|
||||
Release Type on a Path ტექსტის ტრაექტორიიდან მოხსნა
|
||||
Text and Path ტექსტი და ტრაექტორია
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2566,7 +2566,7 @@ fn each_filter_plugin_takes_a_shortcut_of_its_own() {
|
||||
let lines: Vec<&str> = text.lines().filter(|l| l.starts_with("Object › Plug-ins › Same Name")).collect();
|
||||
assert_eq!(lines.len(), 2, "{text}");
|
||||
assert_eq!(lines[0], "Object › Plug-ins › Same Name\t");
|
||||
assert!(lines[1].starts_with("Object › Plug-ins › Same Name…\t") && lines[1].ends_with("+Alt+Shift+F7"), "{}", lines[1]);
|
||||
assert_eq!(lines[1], format!("Object › Plug-ins › Same Name…\t{}", crate::menus::pretty_shortcut(chord)));
|
||||
// A chord an Effect menu item holds is a conflict.
|
||||
crate::keymap::set_override(&mut app.ui.shortcut_overrides, "effect:distort.transform", "Cmd+Alt+Shift+F8");
|
||||
let conflicts = crate::shortcut_editor::conflicts_with(&app.ui.shortcut_overrides, "Cmd+Alt+Shift+F8", &key_one);
|
||||
|
||||
Reference in New Issue
Block a user