mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-19 19:44:18 +00:00
feat: generate resource symbol svg in server and improve viewBox (#2109)
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run
- feat: move symbol svg generation logic from client to client, and directly display svg elements instead of mask-image. This significantly boosts loading speed. - fix: use font ascender and descender to bound view box, avoid clipping.
This commit is contained in:
parent
6799951ee3
commit
4c8d34cdb2
8 changed files with 312 additions and 465 deletions
|
|
@ -5,7 +5,7 @@ mod prelude {
|
|||
|
||||
pub use std::collections::HashMap;
|
||||
|
||||
pub use reflexo_vec2svg::ir::{GlyphItem, GlyphRef};
|
||||
pub use reflexo_vec2svg::ir::GlyphItem;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use serde_json::Value as JsonValue;
|
||||
pub use sync_ls::*;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ use reflexo_typst::TypstPagedDocument;
|
|||
use reflexo_typst::{vector::font::GlyphId, TypstFont};
|
||||
use reflexo_vec2svg::SvgGlyphBuilder;
|
||||
use sync_ls::LspResult;
|
||||
use tinymist_std::typst::TypstDocument;
|
||||
use typst::foundations::Bytes;
|
||||
use typst::{syntax::VirtualPath, World};
|
||||
|
||||
|
|
@ -16,16 +15,23 @@ use crate::world::{base::ShadowApi, EntryState, TaskInputs};
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResourceSymbolResponse {
|
||||
symbols: BTreeMap<String, ResourceSymbolItem>,
|
||||
font_selects: Vec<FontItem>,
|
||||
glyph_defs: HashMap<String, String>,
|
||||
symbols: Vec<ResourceSymbolItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResourceSymbolItem {
|
||||
id: String,
|
||||
category: SymCategory,
|
||||
unicode: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
glyph: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SymbolItem {
|
||||
category: SymCategory,
|
||||
unicode: u32,
|
||||
glyphs: Vec<ResourceGlyphDesc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
|
|
@ -60,32 +66,7 @@ enum SymCategory {
|
|||
DoubleStruck,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResourceGlyphDesc {
|
||||
font_index: u32,
|
||||
x_advance: Option<u16>,
|
||||
y_advance: Option<u16>,
|
||||
x_min: Option<i16>,
|
||||
x_max: Option<i16>,
|
||||
y_min: Option<i16>,
|
||||
y_max: Option<i16>,
|
||||
name: Option<String>,
|
||||
shape: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FontItem {
|
||||
family: String,
|
||||
cap_height: f32,
|
||||
ascender: f32,
|
||||
descender: f32,
|
||||
units_per_em: f32,
|
||||
// vertical: bool,
|
||||
}
|
||||
|
||||
type ResourceSymbolMap = BTreeMap<String, ResourceSymbolItem>;
|
||||
type ResourceSymbolMap = BTreeMap<String, SymbolItem>;
|
||||
|
||||
static CAT_MAP: LazyLock<HashMap<&str, SymCategory>> = LazyLock::new(|| {
|
||||
use SymCategory::*;
|
||||
|
|
@ -951,155 +932,13 @@ static CAT_MAP: LazyLock<HashMap<&str, SymCategory>> = LazyLock::new(|| {
|
|||
impl ServerState {
|
||||
/// Get the all valid symbols
|
||||
pub async fn get_symbol_resources(snap: LspComputeGraph) -> LspResult<JsonValue> {
|
||||
let mut symbols = ResourceSymbolMap::new();
|
||||
let symbols = collect_symbols(&snap)?;
|
||||
|
||||
let std = snap
|
||||
.library()
|
||||
.std
|
||||
.read()
|
||||
.scope()
|
||||
.ok_or_else(|| internal_error("cannot get std scope"))?;
|
||||
let sym = std
|
||||
.get("sym")
|
||||
.ok_or_else(|| internal_error("cannot get sym"))?;
|
||||
let glyph_mapping = render_symbols(&snap, &symbols)?;
|
||||
|
||||
if let Some(scope) = sym.read().scope() {
|
||||
populate_scope(scope, "sym", SymCategory::Misc, &mut symbols);
|
||||
}
|
||||
// todo: disabling emoji module, as there is performant issue on emojis
|
||||
// let _ = emoji;
|
||||
// populate_scope(emoji().scope(), "emoji", SymCategory::Emoji, &mut symbols);
|
||||
let symbols = render_glyphs(&symbols, &glyph_mapping)?;
|
||||
|
||||
const PRELUDE: &str = r#"#show math.equation: set text(font: (
|
||||
"New Computer Modern Math",
|
||||
"Latin Modern Math",
|
||||
"STIX Two Math",
|
||||
"Cambria Math",
|
||||
"New Computer Modern",
|
||||
"Cambria",
|
||||
))
|
||||
"#;
|
||||
|
||||
let math_shaping_text = symbols.iter().fold(PRELUDE.to_owned(), |mut o, (k, e)| {
|
||||
use std::fmt::Write;
|
||||
writeln!(o, "$#{k}$/* {} */#pagebreak()", e.unicode).ok();
|
||||
o
|
||||
});
|
||||
log::debug!("math shaping text: {math_shaping_text}");
|
||||
|
||||
let symbols_ref = symbols.keys().cloned().collect::<Vec<_>>();
|
||||
|
||||
let font = {
|
||||
let entry_path: Arc<Path> = Path::new("/._sym_.typ").into();
|
||||
|
||||
let new_entry = EntryState::new_rootless(VirtualPath::new(&entry_path));
|
||||
|
||||
let mut forked = snap.world().task(TaskInputs {
|
||||
entry: Some(new_entry),
|
||||
..TaskInputs::default()
|
||||
});
|
||||
forked
|
||||
.map_shadow_by_id(forked.main(), Bytes::from_string(math_shaping_text))
|
||||
.map_err(|e| error_once!("cannot map shadow", err: e))
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let sym_doc = typst::compile::<TypstPagedDocument>(&forked)
|
||||
.output
|
||||
.map_err(|e| error_once!("cannot compile symbols", err: format!("{e:?}")))
|
||||
.map_err(internal_error)?;
|
||||
|
||||
log::debug!("sym doc: {sym_doc:?}");
|
||||
Some(trait_symbol_fonts(
|
||||
&TypstDocument::Paged(Arc::new(sym_doc)),
|
||||
&symbols_ref,
|
||||
))
|
||||
};
|
||||
|
||||
let mut glyph_defs = HashMap::new();
|
||||
|
||||
let mut collected_fonts = None;
|
||||
|
||||
if let Some(glyph_mapping) = font.clone() {
|
||||
let glyph_provider = reflexo_vec2svg::GlyphProvider::default();
|
||||
let glyph_pass =
|
||||
reflexo_typst::vector::pass::ConvertInnerImpl::new(glyph_provider, false);
|
||||
|
||||
let mut glyphs = vec![];
|
||||
|
||||
let font_collected = glyph_mapping
|
||||
.values()
|
||||
.map(|e| e.0.clone())
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut render_sym = |u| {
|
||||
let (font, id) = glyph_mapping.get(u)?.clone();
|
||||
let font_index = font_collected.iter().position(|e| e == &font).unwrap() as u32;
|
||||
|
||||
let width = font.ttf().glyph_hor_advance(id);
|
||||
let height = font.ttf().glyph_ver_advance(id);
|
||||
let bbox = font.ttf().glyph_bounding_box(id);
|
||||
|
||||
let glyph = glyph_pass.must_flat_glyph(&GlyphItem::Raw(font.clone(), id))?;
|
||||
|
||||
let g_ref = GlyphRef {
|
||||
font_hash: font_index,
|
||||
glyph_idx: id.0 as u32,
|
||||
};
|
||||
|
||||
glyphs.push((g_ref, glyph));
|
||||
|
||||
Some(ResourceGlyphDesc {
|
||||
font_index,
|
||||
x_advance: width,
|
||||
y_advance: height,
|
||||
x_min: bbox.map(|e| e.x_min),
|
||||
x_max: bbox.map(|e| e.x_max),
|
||||
y_min: bbox.map(|e| e.y_min),
|
||||
y_max: bbox.map(|e| e.y_max),
|
||||
name: font.ttf().glyph_name(id).map(|e| e.to_owned()),
|
||||
shape: Some(g_ref.as_svg_id("g")),
|
||||
})
|
||||
};
|
||||
|
||||
for (k, v) in symbols.iter_mut() {
|
||||
let Some(desc) = render_sym(k) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
v.glyphs.push(desc);
|
||||
}
|
||||
|
||||
let mut builder = SvgGlyphBuilder::new();
|
||||
glyph_defs = glyphs
|
||||
.iter()
|
||||
.map(|(id, item)| {
|
||||
let glyph_id = id.as_svg_id("g");
|
||||
let rendered = builder.render_glyph("", item).unwrap_or_default();
|
||||
(glyph_id, rendered)
|
||||
})
|
||||
.collect();
|
||||
|
||||
collected_fonts = Some(font_collected);
|
||||
}
|
||||
|
||||
let resp = ResourceSymbolResponse {
|
||||
symbols,
|
||||
font_selects: collected_fonts
|
||||
.map(|e| e.into_iter())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|e| FontItem {
|
||||
family: e.info().family.clone(),
|
||||
cap_height: e.metrics().cap_height.get() as f32,
|
||||
ascender: e.metrics().ascender.get() as f32,
|
||||
descender: e.metrics().descender.get() as f32,
|
||||
units_per_em: e.metrics().units_per_em as f32,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
glyph_defs,
|
||||
};
|
||||
let resp = ResourceSymbolResponse { symbols };
|
||||
|
||||
serde_json::to_value(resp)
|
||||
.context("cannot serialize response")
|
||||
|
|
@ -1107,82 +946,42 @@ impl ServerState {
|
|||
}
|
||||
}
|
||||
|
||||
fn trait_symbol_fonts(
|
||||
doc: &TypstDocument,
|
||||
symbols: &[String],
|
||||
) -> HashMap<String, (TypstFont, GlyphId)> {
|
||||
use typst::layout::Frame;
|
||||
use typst::layout::FrameItem;
|
||||
fn collect_symbols(snap: &LspComputeGraph) -> LspResult<BTreeMap<String, SymbolItem>> {
|
||||
let mut symbols = ResourceSymbolMap::new();
|
||||
|
||||
let mut worker = Worker {
|
||||
symbols,
|
||||
active: "",
|
||||
res: HashMap::new(),
|
||||
};
|
||||
worker.work(doc);
|
||||
let res = worker.res;
|
||||
let std = snap
|
||||
.library()
|
||||
.std
|
||||
.read()
|
||||
.scope()
|
||||
.ok_or_else(|| internal_error("cannot get std scope"))?;
|
||||
let sym = std
|
||||
.get("sym")
|
||||
.ok_or_else(|| internal_error("cannot get sym"))?;
|
||||
|
||||
struct Worker<'a> {
|
||||
symbols: &'a [String],
|
||||
active: &'a str,
|
||||
res: HashMap<String, (TypstFont, GlyphId)>,
|
||||
if let Some(scope) = sym.read().scope() {
|
||||
populate_scope(scope, "sym", SymCategory::Misc, &mut symbols);
|
||||
}
|
||||
// todo: disabling emoji module, as there is performant issue on emojis
|
||||
// let _ = emoji;
|
||||
// populate_scope(emoji().scope(), "emoji", SymCategory::Emoji, &mut symbols);
|
||||
|
||||
impl Worker<'_> {
|
||||
fn work(&mut self, doc: &TypstDocument) {
|
||||
match doc {
|
||||
TypstDocument::Paged(paged_doc) => {
|
||||
for (pg, s) in paged_doc.pages.iter().zip(self.symbols.iter()) {
|
||||
self.active = s;
|
||||
self.work_frame(&pg.frame);
|
||||
}
|
||||
}
|
||||
// todo: handle html
|
||||
TypstDocument::Html(..) => {}
|
||||
}
|
||||
}
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn work_frame(&mut self, k: &Frame) {
|
||||
for (_, item) in k.items() {
|
||||
let text = match item {
|
||||
FrameItem::Group(g) => {
|
||||
self.work_frame(&g.frame);
|
||||
continue;
|
||||
}
|
||||
FrameItem::Text(text) => text,
|
||||
FrameItem::Shape(_, _)
|
||||
| FrameItem::Image(_, _, _)
|
||||
| FrameItem::Link(_, _)
|
||||
| FrameItem::Tag(_) => continue,
|
||||
};
|
||||
fn populate_scope(
|
||||
sym: &Scope,
|
||||
mod_name: &str,
|
||||
fallback_cat: SymCategory,
|
||||
out: &mut ResourceSymbolMap,
|
||||
) {
|
||||
for (k, b) in sym.iter() {
|
||||
let Value::Symbol(sym) = b.read() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let font = text.font.clone();
|
||||
for g in &text.glyphs {
|
||||
let g_text = &text.text[g.range()];
|
||||
let chars_count = g_text.chars().count();
|
||||
if chars_count > 1 {
|
||||
log::warn!("multi char glyph: {g_text}");
|
||||
continue;
|
||||
}
|
||||
let Some(ch) = g_text.chars().next() else {
|
||||
continue;
|
||||
};
|
||||
if ch.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
log::debug!(
|
||||
"glyph: {active} => {ch} ({chc:x})",
|
||||
active = self.active,
|
||||
chc = ch as u32
|
||||
);
|
||||
self.res
|
||||
.insert(self.active.to_owned(), (font.clone(), GlyphId(g.id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
populate(sym, mod_name, k, fallback_cat, out)
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn populate(
|
||||
|
|
@ -1208,26 +1007,188 @@ fn populate(
|
|||
let category = CAT_MAP.get(name.as_str()).cloned().unwrap_or(fallback_cat);
|
||||
out.insert(
|
||||
name,
|
||||
ResourceSymbolItem {
|
||||
SymbolItem {
|
||||
category,
|
||||
unicode: ch as u32,
|
||||
glyphs: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_scope(
|
||||
sym: &Scope,
|
||||
mod_name: &str,
|
||||
fallback_cat: SymCategory,
|
||||
out: &mut ResourceSymbolMap,
|
||||
) {
|
||||
for (k, b) in sym.iter() {
|
||||
let Value::Symbol(sym) = b.read() else {
|
||||
continue;
|
||||
};
|
||||
fn render_symbols(
|
||||
snap: &LspComputeGraph,
|
||||
symbols: &BTreeMap<String, SymbolItem>,
|
||||
) -> LspResult<HashMap<String, (TypstFont, GlyphId)>> {
|
||||
const PRELUDE: &str = r#"#show math.equation: set text(font: (
|
||||
"New Computer Modern Math",
|
||||
"Latin Modern Math",
|
||||
"STIX Two Math",
|
||||
"Cambria Math",
|
||||
"New Computer Modern",
|
||||
"Cambria",
|
||||
))
|
||||
"#;
|
||||
|
||||
populate(sym, mod_name, k, fallback_cat, out)
|
||||
}
|
||||
let math_shaping_text = symbols.iter().fold(PRELUDE.to_owned(), |mut o, (k, e)| {
|
||||
use std::fmt::Write;
|
||||
writeln!(o, "$#{k}$/* {} */#pagebreak()", e.unicode).ok();
|
||||
o
|
||||
});
|
||||
log::debug!("math shaping text: {math_shaping_text}");
|
||||
|
||||
let entry_path: Arc<Path> = Path::new("/._sym_.typ").into();
|
||||
|
||||
let new_entry = EntryState::new_rootless(VirtualPath::new(&entry_path));
|
||||
|
||||
let mut forked = snap.world().task(TaskInputs {
|
||||
entry: Some(new_entry),
|
||||
..TaskInputs::default()
|
||||
});
|
||||
forked
|
||||
.map_shadow_by_id(forked.main(), Bytes::from_string(math_shaping_text))
|
||||
.map_err(|e| error_once!("cannot map shadow", err: e))
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let sym_doc = typst::compile::<TypstPagedDocument>(&forked)
|
||||
.output
|
||||
.map_err(|e| error_once!("cannot compile symbols", err: format!("{e:?}")))
|
||||
.map_err(internal_error)?;
|
||||
|
||||
log::debug!("sym doc: {sym_doc:?}");
|
||||
|
||||
let res = extract_rendered_symbols(&sym_doc, symbols.keys());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn extract_rendered_symbols<'a>(
|
||||
doc: &TypstPagedDocument,
|
||||
symbols: impl Iterator<Item = &'a String>,
|
||||
) -> HashMap<String, (TypstFont, GlyphId)> {
|
||||
use typst::layout::Frame;
|
||||
use typst::layout::FrameItem;
|
||||
|
||||
struct Worker {
|
||||
res: HashMap<String, (TypstFont, GlyphId)>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
fn work<'a>(
|
||||
&mut self,
|
||||
paged_doc: &TypstPagedDocument,
|
||||
symbols: impl Iterator<Item = &'a String>,
|
||||
) {
|
||||
for (pg, s) in paged_doc.pages.iter().zip(symbols) {
|
||||
self.work_frame(&pg.frame, s);
|
||||
}
|
||||
}
|
||||
|
||||
fn work_frame(&mut self, k: &Frame, active: &str) {
|
||||
for (_, item) in k.items() {
|
||||
let text = match item {
|
||||
FrameItem::Group(g) => {
|
||||
self.work_frame(&g.frame, active);
|
||||
continue;
|
||||
}
|
||||
FrameItem::Text(text) => text,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let font = text.font.clone();
|
||||
for g in &text.glyphs {
|
||||
let g_text = &text.text[g.range()];
|
||||
let chars_count = g_text.chars().count();
|
||||
if chars_count > 1 {
|
||||
log::warn!("multi char glyph: {g_text}");
|
||||
continue;
|
||||
}
|
||||
let Some(ch) = g_text.chars().next() else {
|
||||
continue;
|
||||
};
|
||||
if ch.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
log::debug!("glyph: {active} => {ch} ({chc:x})", chc = ch as u32);
|
||||
self.res
|
||||
.insert(active.to_owned(), (font.clone(), GlyphId(g.id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut worker = Worker {
|
||||
res: HashMap::new(),
|
||||
};
|
||||
worker.work(doc, symbols);
|
||||
worker.res
|
||||
}
|
||||
|
||||
fn render_glyphs(
|
||||
symbols: &BTreeMap<String, SymbolItem>,
|
||||
glyph_mapping: &HashMap<String, (TypstFont, GlyphId)>,
|
||||
) -> LspResult<Vec<ResourceSymbolItem>> {
|
||||
let glyph_provider = reflexo_vec2svg::GlyphProvider::default();
|
||||
let glyph_pass = reflexo_typst::vector::pass::ConvertInnerImpl::new(glyph_provider, false);
|
||||
|
||||
let mut builder = SvgGlyphBuilder::new();
|
||||
|
||||
let mut render_sym = |u| {
|
||||
let (font, id) = glyph_mapping.get(u)?.clone();
|
||||
|
||||
let glyph = glyph_pass.must_flat_glyph(&GlyphItem::Raw(font.clone(), id))?;
|
||||
|
||||
let rendered = builder.render_glyph("", &glyph)?; // the glyph_id does not matter here
|
||||
|
||||
Some(create_display_svg(&font, id, &rendered))
|
||||
};
|
||||
|
||||
let rendered_symbols = symbols
|
||||
.iter()
|
||||
.map(|(k, v)| ResourceSymbolItem {
|
||||
id: k.clone(),
|
||||
category: v.category,
|
||||
unicode: v.unicode,
|
||||
glyph: render_sym(k),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(rendered_symbols)
|
||||
}
|
||||
|
||||
fn create_display_svg(font: &TypstFont, gid: GlyphId, svg_path: &str) -> String {
|
||||
let face = font.ttf();
|
||||
|
||||
let (x_min, x_max) = face
|
||||
.glyph_bounding_box(gid)
|
||||
.map(|bbox| (bbox.x_min as f32, bbox.x_max as f32))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Font-wide metrics
|
||||
let units_per_em = font.metrics().units_per_em as f32;
|
||||
let ascender = font.metrics().ascender.get() as f32 * units_per_em;
|
||||
let descender = font.metrics().descender.get() as f32 * units_per_em; // usually negative
|
||||
|
||||
// Horizontal advance (fallback to em)
|
||||
let x_advance = face
|
||||
.glyph_hor_advance(gid)
|
||||
.map(f32::from)
|
||||
.unwrap_or(units_per_em);
|
||||
|
||||
// Start viewBox.x at left-most ink or 0, whichever is smaller (to include left overhang)
|
||||
let view_x = x_min.min(0.0);
|
||||
|
||||
// Start view width as the advance; enlarge if ink extends past that
|
||||
let view_w = x_advance.max(x_max - view_x);
|
||||
|
||||
// Vertical viewBox uses font ascender/descent so baseline is at y=0
|
||||
let view_y = -ascender;
|
||||
let view_h = ascender - descender; // ascender - (negative descender) -> total height
|
||||
|
||||
let svg_content = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{view_x} {view_y} {view_w} {view_h}" preserveAspectRatio="xMidYMid meet">
|
||||
<g transform="scale(1 -1)">{svg_path}</g>
|
||||
</svg>"#
|
||||
);
|
||||
|
||||
svg_content
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const SymbolCell = (sym: SymbolItem) => {
|
|||
|
||||
const fallback = () => {
|
||||
const key = stripSymPrefix(sym.id);
|
||||
return span(NOPRINT_SYMBOLS[key] ?? key);
|
||||
return span({ class: "symbol-glyph" }, NOPRINT_SYMBOLS[key] ?? key);
|
||||
};
|
||||
|
||||
const symbolName = stripSymPrefix(sym.id);
|
||||
|
|
@ -50,7 +50,7 @@ export const SymbolCell = (sym: SymbolItem) => {
|
|||
title: `Click to insert: ${symbolName}`,
|
||||
onclick: handleClick,
|
||||
},
|
||||
div({ class: "symbol-glyph" }, sym.rendered ?? fallback()),
|
||||
(sym.glyph && div({ class: "symbol-glyph", innerHTML: sym.glyph })) ?? fallback(),
|
||||
div(
|
||||
{ class: "symbol-details" },
|
||||
div(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { CanvasPanel } from "./components/canvas-panel";
|
|||
import { SymbolPicker } from "./components/symbol-picker";
|
||||
import { SearchBar, ViewModeToggle } from "./components/toolbox";
|
||||
import { useDetypifyFilter } from "./detypify-filter";
|
||||
import { prerenderSymbols } from "./render";
|
||||
import { useSymbolSearch } from "./search-filter";
|
||||
import type { SymbolItem, SymbolResource } from "./symbols";
|
||||
|
||||
|
|
@ -18,15 +17,14 @@ function useSymbolResource() {
|
|||
const symbols = van.state<SymbolItem[]>(
|
||||
symbolInformationData.startsWith(":")
|
||||
? []
|
||||
: prerenderSymbols(JSON.parse(base64Decode(symbolInformationData))),
|
||||
: (JSON.parse(base64Decode(symbolInformationData)) as SymbolResource).symbols,
|
||||
);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// Dynamically import mock data in development mode if no real data is present
|
||||
import("./mock-data.json").then((json) => {
|
||||
const symbolResource = json as SymbolResource;
|
||||
symbols.val = prerenderSymbols(symbolResource);
|
||||
console.log("symbols", symbols.val);
|
||||
symbols.val = symbolResource.symbols;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,148 +1,117 @@
|
|||
{
|
||||
"fontSelects": [
|
||||
"symbols": [
|
||||
{
|
||||
"ascender": 0.72802734375,
|
||||
"capHeight": 0.7001953125,
|
||||
"descender": -0.21044921875,
|
||||
"family": "Segoe UI Symbol",
|
||||
"unitsPerEm": 2048
|
||||
},
|
||||
{
|
||||
"ascender": 0.8059999942779541,
|
||||
"capHeight": 0.6830000281333923,
|
||||
"descender": -0.1940000057220459,
|
||||
"family": "New Computer Modern Math",
|
||||
"unitsPerEm": 1000
|
||||
}
|
||||
],
|
||||
"glyphDefs": {
|
||||
"gAQAAAHU": "<path id=\"gAQAAAHU\" class=\"outline_glyph\" d=\"M 336 698 C 324 698 313 692 304 680 L 187 531 L 208 509 L 356 628 C 368 637 374 648 374 660 C 374 680 356 698 336 698 Z \"/>",
|
||||
"gAQAAABQD": "<path id=\"gAQAAABQD\" class=\"outline_glyph\" d=\"M 377 536 C 432 565 484 604 534 651 L 519 673 C 480 636 446 618 419 618 C 406 618 389 623 368 633 C 295 668 244 686 215 686 C 156 686 109 650 76 578 C 58 540 42 502 27 464 L 51 449 C 69 499 91 537 117 564 C 137 584 162 594 193 594 C 210 594 228 590 248 583 Z M 462 510 C 475 533 494 555 517 576 L 498 587 C 452 558 405 517 357 463 C 356 448 355 436 355 428 C 355 407 358 375 363 333 C 370 282 373 240 373 208 C 373 148 356 82 330 56 C 310 35 289 25 266 25 C 241 25 219 34 201 53 C 181 86 166 114 155 137 L 128 109 C 87 67 57 32 37 4 L 45 -19 L 99 19 C 134 -12 165 -27 193 -27 C 268 -27 339 7 406 74 C 445 114 465 171 465 246 C 465 293 461 344 453 399 C 451 412 450 434 450 463 C 450 482 454 497 462 510 Z \"/>",
|
||||
"gAQAAAGw": "<path id=\"gAQAAAGw\" class=\"outline_glyph\" d=\"M 430 0 C 439 0 444 5 444 14 L 444 16 C 425 96 395 171 353 242 C 396 313 426 388 444 467 L 444 469 C 444 478 439 483 430 483 L 428 483 C 421 483 417 480 416 474 C 386 388 340 314 278 252 C 276 250 275 247 274 242 C 274 237 276 233 279 232 C 341 169 387 95 416 10 C 418 3 422 0 428 0 Z M 267 0 C 276 0 281 5 281 14 L 281 16 C 262 96 232 171 190 242 C 233 313 263 388 281 467 L 281 469 C 281 478 276 483 267 483 L 265 483 C 259 483 255 480 253 474 C 223 389 177 315 115 252 C 113 250 112 247 111 242 C 111 237 113 233 116 232 C 178 169 224 95 253 10 C 255 3 259 0 265 0 Z \"/>"
|
||||
},
|
||||
"symbols": {
|
||||
"sym.acute": {
|
||||
"category": "accent",
|
||||
"glyphs": [
|
||||
{
|
||||
"fontIndex": 1,
|
||||
"name": "acute",
|
||||
"shape": "gAQAAAHU",
|
||||
"xAdvance": 500,
|
||||
"xMax": 374,
|
||||
"xMin": 187,
|
||||
"yAdvance": null,
|
||||
"yMax": 698,
|
||||
"yMin": 509
|
||||
}
|
||||
],
|
||||
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 500 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 336 698 C 324 698 313 692 304 680 L 187 531 L 208 509 L 356 628 C 368 637 374 648 374 660 C 374 680 356 698 336 698 Z \"/></g>\n</svg>",
|
||||
"id": "sym.acute",
|
||||
"unicode": 180
|
||||
},
|
||||
"sym.Im": {
|
||||
{
|
||||
"category": "misc",
|
||||
"glyphs": [
|
||||
{
|
||||
"fontIndex": 1,
|
||||
"name": "uni2111",
|
||||
"shape": "gAQAAABQD",
|
||||
"xAdvance": 554,
|
||||
"xMax": 534,
|
||||
"xMin": 27,
|
||||
"yAdvance": null,
|
||||
"yMax": 686,
|
||||
"yMin": -27
|
||||
}
|
||||
],
|
||||
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 554 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 377 536 C 432 565 484 604 534 651 L 519 673 C 480 636 446 618 419 618 C 406 618 389 623 368 633 C 295 668 244 686 215 686 C 156 686 109 650 76 578 C 58 540 42 502 27 464 L 51 449 C 69 499 91 537 117 564 C 137 584 162 594 193 594 C 210 594 228 590 248 583 Z M 462 510 C 475 533 494 555 517 576 L 498 587 C 452 558 405 517 357 463 C 356 448 355 436 355 428 C 355 407 358 375 363 333 C 370 282 373 240 373 208 C 373 148 356 82 330 56 C 310 35 289 25 266 25 C 241 25 219 34 201 53 C 181 86 166 114 155 137 L 128 109 C 87 67 57 32 37 4 L 45 -19 L 99 19 C 134 -12 165 -27 193 -27 C 268 -27 339 7 406 74 C 445 114 465 171 465 246 C 465 293 461 344 453 399 C 451 412 450 434 450 463 C 450 482 454 497 462 510 Z \"/></g>\n</svg>",
|
||||
"id": "sym.Im",
|
||||
"unicode": 8465
|
||||
},
|
||||
"sym.quote.angle.l.double": {
|
||||
{
|
||||
"category": "quote",
|
||||
"glyphs": [
|
||||
{
|
||||
"fontIndex": 1,
|
||||
"name": "guillemotleft",
|
||||
"shape": "gAQAAAGw",
|
||||
"xAdvance": 556,
|
||||
"xMax": 444,
|
||||
"xMin": 111,
|
||||
"yAdvance": null,
|
||||
"yMax": 483,
|
||||
"yMin": 0
|
||||
}
|
||||
],
|
||||
"glyph": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -806 556 1000\" preserveAspectRatio=\"xMidYMid meet\">\n<g transform=\"scale(1 -1)\"><path id=\"\" class=\"outline_glyph\" d=\"M 430 0 C 439 0 444 5 444 14 L 444 16 C 425 96 395 171 353 242 C 396 313 426 388 444 467 L 444 469 C 444 478 439 483 430 483 L 428 483 C 421 483 417 480 416 474 C 386 388 340 314 278 252 C 276 250 275 247 274 242 C 274 237 276 233 279 232 C 341 169 387 95 416 10 C 418 3 422 0 428 0 Z M 267 0 C 276 0 281 5 281 14 L 281 16 C 262 96 232 171 190 242 C 233 313 263 388 281 467 L 281 469 C 281 478 276 483 267 483 L 265 483 C 259 483 255 480 253 474 C 223 389 177 315 115 252 C 113 250 112 247 111 242 C 111 237 113 233 116 232 C 178 169 224 95 253 10 C 255 3 259 0 265 0 Z \"/></g>\n</svg>",
|
||||
"id": "sym.quote.angle.l.double",
|
||||
"unicode": 171
|
||||
},
|
||||
|
||||
"sym.lrm": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.lrm",
|
||||
"unicode": 8206
|
||||
},
|
||||
"sym.rlm": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.rlm",
|
||||
"unicode": 8207
|
||||
},
|
||||
"sym.wj": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.wj",
|
||||
"unicode": 8288
|
||||
},
|
||||
"sym.zwj": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.zwj",
|
||||
"unicode": 8205
|
||||
},
|
||||
"sym.zwnj": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.zwnj",
|
||||
"unicode": 8204
|
||||
},
|
||||
"sym.zws": {
|
||||
{
|
||||
"category": "control",
|
||||
"glyphs": [],
|
||||
"id": "sym.zws",
|
||||
"unicode": 8203
|
||||
},
|
||||
|
||||
"sym.space": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space",
|
||||
"unicode": 32
|
||||
},
|
||||
"sym.space.en": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.en",
|
||||
"unicode": 8194
|
||||
},
|
||||
"sym.space.med": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.fig",
|
||||
"unicode": 8199
|
||||
},
|
||||
{
|
||||
"category": "space",
|
||||
"id": "sym.space.hair",
|
||||
"unicode": 8202
|
||||
},
|
||||
{
|
||||
"category": "space",
|
||||
"id": "sym.space.med",
|
||||
"unicode": 8287
|
||||
},
|
||||
"sym.space.nobreak": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.nobreak",
|
||||
"unicode": 160
|
||||
},
|
||||
"sym.space.nobreak.narrow": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.nobreak.narrow",
|
||||
"unicode": 8239
|
||||
},
|
||||
"sym.space.quad": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.punct",
|
||||
"unicode": 8200
|
||||
},
|
||||
{
|
||||
"category": "space",
|
||||
"id": "sym.space.quad",
|
||||
"unicode": 8195
|
||||
},
|
||||
"sym.space.quarter": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.quarter",
|
||||
"unicode": 8197
|
||||
},
|
||||
"sym.space.thin": {
|
||||
{
|
||||
"category": "space",
|
||||
"glyphs": [],
|
||||
"id": "sym.space.sixth",
|
||||
"unicode": 8198
|
||||
},
|
||||
{
|
||||
"category": "space",
|
||||
"id": "sym.space.thin",
|
||||
"unicode": 8201
|
||||
},
|
||||
{
|
||||
"category": "space",
|
||||
"id": "sym.space.third",
|
||||
"unicode": 8196
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import van from "vanjs-core";
|
||||
import type { FontItem, GlyphDesc, SymbolItem, SymbolResource } from "./symbols";
|
||||
|
||||
const { div } = van.tags;
|
||||
|
||||
function renderSymbol(
|
||||
mask: HTMLElement,
|
||||
primaryGlyph: GlyphDesc,
|
||||
fontSelected: FontItem,
|
||||
path: string,
|
||||
) {
|
||||
const diff = (min?: number | null, max?: number | null) => {
|
||||
return Math.abs((max ?? 0) - (min ?? 0));
|
||||
};
|
||||
|
||||
const bboxXWidth = diff(primaryGlyph.xMin, primaryGlyph.xMax);
|
||||
const xWidth = Math.max(bboxXWidth, primaryGlyph.xAdvance ?? fontSelected.unitsPerEm);
|
||||
|
||||
const yReal = diff(primaryGlyph.yMin, primaryGlyph.yMax);
|
||||
const yGlobal = primaryGlyph.yAdvance ?? fontSelected.unitsPerEm;
|
||||
const yWidth = Math.max(yReal, yGlobal);
|
||||
|
||||
// keep viewBox in glyph units
|
||||
const viewBox = `0 0 ${xWidth} ${yWidth}`;
|
||||
|
||||
const yShift =
|
||||
yReal >= yGlobal
|
||||
? Math.abs(primaryGlyph.yMax ?? 0)
|
||||
: (Math.abs(primaryGlyph.yMax ?? 0) + yWidth) / 2;
|
||||
|
||||
// Center the symbol horizontally
|
||||
const xShift = -(primaryGlyph.xMin ?? 0) + (xWidth - bboxXWidth) / 2;
|
||||
|
||||
const imageData = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" preserveAspectRatio="xMidYMid meet">
|
||||
<g transform="translate(${xShift}, ${yShift}) scale(1, -1)">${path}</g>
|
||||
</svg>`;
|
||||
|
||||
mask.style.maskImage = `url('data:image/svg+xml;utf8,${encodeURIComponent(imageData)}')`;
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
export function prerenderSymbols(symRes: SymbolResource): SymbolItem[] {
|
||||
return Object.entries(symRes.symbols).map(([id, sym]) => {
|
||||
const primaryGlyph = sym.glyphs[0];
|
||||
const mask = div();
|
||||
const renderedSym: SymbolItem = {
|
||||
id,
|
||||
category: sym.category,
|
||||
unicode: sym.unicode,
|
||||
rendered: primaryGlyph ? mask : undefined,
|
||||
};
|
||||
if (primaryGlyph?.fontIndex && primaryGlyph?.shape) {
|
||||
const fontSelected = symRes.fontSelects[primaryGlyph.fontIndex];
|
||||
if (fontSelected) {
|
||||
const glyphPath = (primaryGlyph.shape && symRes.glyphDefs[primaryGlyph.shape]) ?? "";
|
||||
setTimeout(() => {
|
||||
renderSymbol(mask, sym.glyphs[0], fontSelected, glyphPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
return renderedSym;
|
||||
});
|
||||
}
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
background: var(--vscode-list-inactiveSelectionBackground, rgba(201, 209, 217, 0.15));
|
||||
|
|
@ -35,6 +35,10 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.symbol-cell:hover {
|
||||
overflow: visible
|
||||
}
|
||||
|
||||
.symbol-picker.detailed .symbol-cell {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
|
|
@ -62,21 +66,28 @@
|
|||
}
|
||||
|
||||
/* Symbol glyph mask */
|
||||
.symbol-glyph > div {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: currentColor;
|
||||
div.symbol-glyph svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
fill: currentColor;
|
||||
transition: fill 200ms;
|
||||
}
|
||||
|
||||
.symbol-cell:hover div.symbol-glyph svg {
|
||||
fill: var(--vscode-button-hoverBackground, #1177bb);
|
||||
}
|
||||
|
||||
/* Symbol glyph fallback text */
|
||||
.symbol-glyph > span {
|
||||
span.symbol-glyph {
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.symbol-cell:hover span.symbol-glyph {
|
||||
color: var(--vscode-button-hoverBackground, #1177bb);
|
||||
}
|
||||
|
||||
/* Symbol details */
|
||||
.symbol-details {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const CATEGORY_NAMES = {
|
|||
|
||||
export type SymbolCategory = keyof typeof CATEGORY_NAMES;
|
||||
|
||||
export const NOPRINT_SYMBOLS: { [key: string]: string } = {
|
||||
export const NOPRINT_SYMBOLS: Record<string, string> = {
|
||||
space: "␣",
|
||||
"space.en": "ensp",
|
||||
"space.quad": "emsp",
|
||||
|
|
@ -50,45 +50,17 @@ export const NOPRINT_SYMBOLS: { [key: string]: string } = {
|
|||
zws: "zwsp",
|
||||
};
|
||||
|
||||
export interface GlyphDesc {
|
||||
fontIndex: number | null;
|
||||
xAdvance?: number | null;
|
||||
yAdvance?: number | null;
|
||||
xMin?: number | null;
|
||||
yMin?: number | null;
|
||||
xMax?: number | null;
|
||||
yMax?: number | null;
|
||||
name?: string | null;
|
||||
shape?: string | null;
|
||||
}
|
||||
|
||||
export type SymbolId = string;
|
||||
|
||||
export interface SymbolItem {
|
||||
id: SymbolId;
|
||||
category: SymbolCategory;
|
||||
unicode: number;
|
||||
rendered?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface FontItem {
|
||||
family: string;
|
||||
capHeight: number;
|
||||
ascender: number;
|
||||
descender: number;
|
||||
unitsPerEm: number;
|
||||
}
|
||||
|
||||
export interface RawSymbolItem {
|
||||
category: SymbolCategory;
|
||||
unicode: number;
|
||||
glyphs: GlyphDesc[];
|
||||
glyph?: string;
|
||||
}
|
||||
|
||||
export interface SymbolResource {
|
||||
symbols: Record<string, RawSymbolItem>;
|
||||
fontSelects: FontItem[];
|
||||
glyphDefs: Record<string, string>;
|
||||
symbols: SymbolItem[];
|
||||
}
|
||||
|
||||
export function stripSymPrefix(name: string): string {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue