mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
Merge main into type-3d-redraw
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
//! `removeWord`, `changeWord`), which every document's check takes as right; Ignore All
|
||||
//! (`text.spelling.ignoreAll`) skips a word in the active document only.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Range;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use craft_proof::{IssueKind, Options, ProofLang};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, NodeId, NodeKind};
|
||||
use vectorcraft_doc::{Document, NodeId, NodeKind, TextObject};
|
||||
|
||||
use super::menucmds::whole_or;
|
||||
use super::typemenu::{all_texts, text_ids};
|
||||
@@ -106,56 +108,96 @@ fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => all_texts(&s.doc()?.doc),
|
||||
};
|
||||
let st = s.doc()?;
|
||||
let ignored = &st.spelling_ignored;
|
||||
let opts = Options::default();
|
||||
let mut words = vec![];
|
||||
let mut more = false;
|
||||
'texts: for id in ids {
|
||||
for id in ids {
|
||||
let Some(NodeKind::Text(t)) = st.doc.node(id).map(|n| &n.kind) else { continue };
|
||||
let text = t.plain_text();
|
||||
// Each run in its own language, at its bytes of the plain text.
|
||||
let mut spans: Vec<(Range<usize>, ProofLang)> = Vec::with_capacity(t.runs.len());
|
||||
let mut at = 0;
|
||||
for r in &t.runs {
|
||||
let end = at + r.text.len();
|
||||
spans.push((at..end, ProofLang::from_tag(r.style.language.as_deref())));
|
||||
at = end;
|
||||
let (found, cut) = misspellings(t, &st.spelling_ignored, MAX_WORDS - words.len());
|
||||
for m in found {
|
||||
let Some(word) = text.get(m.range.clone()) else { continue };
|
||||
words.push(json!({ "id": id.0, "start": m.range.start, "end": m.range.end, "word": word, "lang": m.lang.tag() }));
|
||||
}
|
||||
// craft-proof checks one paragraph at a time (its language detection reads the
|
||||
// paragraph), and the check stops at the paragraph where the list fills up rather than
|
||||
// proofing the rest of the text.
|
||||
let mut para_start = 0;
|
||||
for para in text.split('\n') {
|
||||
let para_end = para_start + para.len();
|
||||
let para_spans: Vec<(Range<usize>, ProofLang)> = spans
|
||||
.iter()
|
||||
.filter_map(|(r, lang)| {
|
||||
let (a, b) = (r.start.max(para_start), r.end.min(para_end));
|
||||
(a < b).then(|| (a - para_start..b - para_start, *lang))
|
||||
})
|
||||
.collect();
|
||||
for issue in craft_proof::check_text_with(para, ¶_spans, &opts, false, false) {
|
||||
if issue.kind != IssueKind::Spelling {
|
||||
continue;
|
||||
}
|
||||
let (start, end) = (para_start + issue.start, para_start + issue.end);
|
||||
let Some(word) = text.get(start..end) else { continue };
|
||||
if craft_proof::custom_word(word).is_some_and(|w| ignored.contains(&w)) {
|
||||
continue;
|
||||
}
|
||||
if words.len() == MAX_WORDS {
|
||||
more = true;
|
||||
break 'texts;
|
||||
}
|
||||
words.push(json!({ "id": id.0, "start": start, "end": end, "word": word, "lang": issue.lang.tag() }));
|
||||
}
|
||||
// Past the paragraph and its `\n`.
|
||||
para_start = para_end + 1;
|
||||
if cut {
|
||||
more = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(json!({ "words": words, "more": more }))
|
||||
}
|
||||
|
||||
/// One word of a type object its dictionaries don't know ([`misspellings`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Misspelling {
|
||||
/// The word's bytes in the object's [`TextObject::plain_text`].
|
||||
pub range: Range<usize>,
|
||||
/// The language it was checked in.
|
||||
pub lang: ProofLang,
|
||||
}
|
||||
|
||||
/// The words of `t` its dictionaries don't know, in text order: each run checked in its
|
||||
/// language, each paragraph on its own, leaving out the words in `ignored` (a document's Ignore
|
||||
/// All words). At most `max` of them; the flag says the list was cut short there. What
|
||||
/// `text.spelling.check` lists for one object, and what Auto Spell Check underlines.
|
||||
pub fn misspellings(t: &TextObject, ignored: &BTreeSet<String>, max: usize) -> (Vec<Misspelling>, bool) {
|
||||
let text = t.plain_text();
|
||||
// Each run in its own language, at its bytes of the plain text.
|
||||
let mut spans: Vec<(Range<usize>, ProofLang)> = Vec::with_capacity(t.runs.len());
|
||||
let mut at = 0;
|
||||
for r in &t.runs {
|
||||
let end = at + r.text.len();
|
||||
spans.push((at..end, ProofLang::from_tag(r.style.language.as_deref())));
|
||||
at = end;
|
||||
}
|
||||
let opts = Options::default();
|
||||
let mut found = vec![];
|
||||
// craft-proof checks one paragraph at a time (its language detection reads the paragraph),
|
||||
// and the check stops at the paragraph where the list fills up rather than proofing the rest
|
||||
// of the text.
|
||||
let mut para_start = 0;
|
||||
for para in text.split('\n') {
|
||||
let para_end = para_start + para.len();
|
||||
let para_spans: Vec<(Range<usize>, ProofLang)> = spans
|
||||
.iter()
|
||||
.filter_map(|(r, lang)| {
|
||||
let (a, b) = (r.start.max(para_start), r.end.min(para_end));
|
||||
(a < b).then(|| (a - para_start..b - para_start, *lang))
|
||||
})
|
||||
.collect();
|
||||
for issue in craft_proof::check_text_with(para, ¶_spans, &opts, false, false) {
|
||||
if issue.kind != IssueKind::Spelling {
|
||||
continue;
|
||||
}
|
||||
let (start, end) = (para_start + issue.start, para_start + issue.end);
|
||||
let Some(word) = text.get(start..end) else { continue };
|
||||
if craft_proof::custom_word(word).is_some_and(|w| ignored.contains(&w)) {
|
||||
continue;
|
||||
}
|
||||
if found.len() == max {
|
||||
return (found, true);
|
||||
}
|
||||
found.push(Misspelling { range: start..end, lang: issue.lang });
|
||||
}
|
||||
// Past the paragraph and its `\n`.
|
||||
para_start = para_end + 1;
|
||||
}
|
||||
(found, false)
|
||||
}
|
||||
|
||||
/// Goes up each time the custom dictionary may have changed: an add, delete or change (saved or
|
||||
/// not, since a save takes in the words another window saved), and a dictionary file attached.
|
||||
static DICTIONARY_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// The custom dictionary's generation: what [`misspellings`] found before it last changed may be
|
||||
/// out of date (Auto Spell Check checks the type again).
|
||||
pub fn dictionary_generation() -> u64 {
|
||||
DICTIONARY_GENERATION.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn dictionary_changed() {
|
||||
DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// `ids` sorted back to front, as the document paints them: by their index paths, which are in
|
||||
/// paint order.
|
||||
fn in_paint_order(d: &Document, mut ids: Vec<NodeId>) -> Vec<NodeId> {
|
||||
@@ -246,7 +288,9 @@ fn add_word(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let word = word_param(p, "word", C)?;
|
||||
let added = !craft_proof::user_dictionary().contains(&word);
|
||||
craft_proof::add_word(&word);
|
||||
save_dictionary(s, &[], C).inspect_err(|_| {
|
||||
let saved = save_dictionary(s, &[], C);
|
||||
dictionary_changed();
|
||||
saved.inspect_err(|_| {
|
||||
// Only this command's word: words the save took in from the file stay.
|
||||
if added {
|
||||
craft_proof::remove_word(&word);
|
||||
@@ -263,7 +307,9 @@ fn remove_word(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !craft_proof::remove_word(&word) {
|
||||
return Err(bad(C, format!("`{word}` isn't in the custom dictionary")));
|
||||
}
|
||||
save_dictionary(s, std::slice::from_ref(&word), C).inspect_err(|_| craft_proof::add_word(&word))?;
|
||||
let saved = save_dictionary(s, std::slice::from_ref(&word), C);
|
||||
dictionary_changed();
|
||||
saved.inspect_err(|_| craft_proof::add_word(&word))?;
|
||||
Ok(json!({ "word": word }))
|
||||
}
|
||||
|
||||
@@ -276,13 +322,17 @@ fn change_word(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, format!("`{word}` isn't in the custom dictionary")));
|
||||
}
|
||||
if word == to {
|
||||
save_dictionary(s, &[], C)?;
|
||||
let saved = save_dictionary(s, &[], C);
|
||||
dictionary_changed();
|
||||
saved?;
|
||||
return Ok(json!({ "word": to }));
|
||||
}
|
||||
let to_is_new = !craft_proof::user_dictionary().contains(&to);
|
||||
craft_proof::remove_word(&word);
|
||||
craft_proof::add_word(&to);
|
||||
save_dictionary(s, std::slice::from_ref(&word), C).inspect_err(|_| {
|
||||
let saved = save_dictionary(s, std::slice::from_ref(&word), C);
|
||||
dictionary_changed();
|
||||
saved.inspect_err(|_| {
|
||||
if to_is_new {
|
||||
craft_proof::remove_word(&to);
|
||||
retake_file_words(s);
|
||||
@@ -315,5 +365,6 @@ impl Session {
|
||||
Err(why) => log::warn!("not reading the custom dictionary {}: {why}; it is left as it is", path.display()),
|
||||
}
|
||||
self.dictionary_file = Some(path);
|
||||
dictionary_changed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! of symbols the library panel shows. A library is a document holding the symbols with the
|
||||
//! resources they use (nested symbols, patterns, swatches, brushes, graphic styles, images) and
|
||||
//! their Symbol Options; saved, it is a native `.vectorcraft` file, which File › Open also opens.
|
||||
//! The built-in library is generated here ([`BUILTIN`]); User Defined ones are the `.vectorcraft`
|
||||
//! The built-in libraries are generated here ([`BUILTIN`]); User Defined ones are the `.vectorcraft`
|
||||
//! files in the user library folder ([`Libraries`]), and Other Library… loads the symbols of any
|
||||
//! document VectorCraft opens. `symbol.addFromLibrary` copies symbols into the document;
|
||||
//! `symbol.saveLibrary` writes the document's.
|
||||
@@ -12,7 +12,7 @@ use std::sync::{Arc, LazyLock};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Symbol};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Symbol, SymbolNineSlice};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, shapes};
|
||||
|
||||
use super::brushsym::{CURRENT_SYMBOL, natural_size, normalize, place_xf, set_natural_size};
|
||||
@@ -154,7 +154,10 @@ pub struct BuiltinSymbolLibrary {
|
||||
}
|
||||
|
||||
/// The built-in symbol libraries, in menu order: original art generated in code.
|
||||
pub const BUILTIN: &[BuiltinSymbolLibrary] = &[BuiltinSymbolLibrary { id: "basic-shapes", name: "Basic Shapes", make: basic_shapes }];
|
||||
pub const BUILTIN: &[BuiltinSymbolLibrary] = &[
|
||||
BuiltinSymbolLibrary { id: "basic-shapes", name: "Basic Shapes", make: basic_shapes },
|
||||
BuiltinSymbolLibrary { id: "ui-frames", name: "UI Frames", make: ui_frames },
|
||||
];
|
||||
|
||||
/// Built-in library `id`, made once.
|
||||
fn builtin_library(id: &str) -> Option<Arc<SymbolLibrary>> {
|
||||
@@ -194,6 +197,30 @@ fn basic_shapes() -> SymbolLibrary {
|
||||
SymbolLibrary { name: "Basic Shapes".into(), doc }
|
||||
}
|
||||
|
||||
/// UI Frames: panel, button and bar HUD chrome with default 9-slice guides (original art).
|
||||
fn ui_frames() -> SymbolLibrary {
|
||||
// Heights keep default 9-slice (0.25) past each corner radius so a vertical resize
|
||||
// stretches only the flat middle, not the curves.
|
||||
let frames = [
|
||||
("Panel", Rect::new(-48.0, -36.0, 48.0, 36.0), 10.0, "#2b3340", "#8aa0b8"),
|
||||
("Button", Rect::new(-36.0, -18.0, 36.0, 18.0), 8.0, "#3a6ea5", "#d7e6f5"),
|
||||
("Bar", Rect::new(-56.0, -24.0, 56.0, 24.0), 10.0, "#1f6b4a", "#b7e0c9"),
|
||||
];
|
||||
let mut doc = Document::new(100.0, 100.0);
|
||||
for (name, rect, radius, fill_hex, edge_hex) in frames {
|
||||
let fill = Color::from_hex(fill_hex).unwrap_or(Color::BLACK);
|
||||
let edge = Color::from_hex(edge_hex).unwrap_or(Color::BLACK);
|
||||
let id = doc.alloc_id();
|
||||
let art = Node::path(id, shapes::rounded_rectangle(rect, radius), Appearance::basic(Paint::solid(fill), Paint::solid(edge), 1.5));
|
||||
let Ok((art, size, _)) = normalize(art) else { continue };
|
||||
doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
|
||||
set_natural_size(&mut doc, name, Some(size));
|
||||
doc.set_symbol_nine_slice(name, Some(SymbolNineSlice::default()));
|
||||
}
|
||||
doc.title = "UI Frames".into();
|
||||
SymbolLibrary { name: "UI Frames".into(), doc }
|
||||
}
|
||||
|
||||
/// An instance of symbol `name` (what the resource gathering follows).
|
||||
fn instance(name: &str) -> Node {
|
||||
Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name.into(), xf: Affine::IDENTITY })
|
||||
|
||||
Reference in New Issue
Block a user