mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
M7.5: OpenType as character attributes — per-run features (ligatures, contextual alternates, discretionary ligatures, swash, small caps, ordinals, fractions, figure styles) drive shaping; OpenType panel; SVG font-feature-settings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9df5b0135
commit
d5aa387367
@@ -26,7 +26,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id, start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, underline?, strikethrough?, allCaps?: bool} style a character range (runs are split at the range ends) → {id, runs}",
|
||||
"{id, start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, underline?, strikethrough?, allCaps?: bool, features?: [\"dlig\", \"-liga\", …]} style a character range (runs are split at the range ends) → {id, runs}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -137,6 +137,17 @@ pub(crate) struct CharChange {
|
||||
underline: Option<bool>,
|
||||
strikethrough: Option<bool>,
|
||||
all_caps: Option<bool>,
|
||||
features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// `features: ["dlig", "-liga", …]` → the canonical tag list (differences from the defaults).
|
||||
pub(crate) fn features_param(p: &Value, cmd: &str) -> Result<Option<Vec<String>>> {
|
||||
let Some(v) = p.get("features").filter(|v| !v.is_null()) else { return Ok(None) };
|
||||
let tags: Vec<&str> = v.as_array().ok_or_else(|| bad(cmd, "features must be a list of tags"))?.iter().filter_map(Value::as_str).collect();
|
||||
if let Some(t) = tags.iter().find(|t| !drawcraft_text::OtFeatures::known_tag(t)) {
|
||||
return Err(bad(cmd, format!("unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh; prefix - to turn off)")));
|
||||
}
|
||||
Ok(Some(drawcraft_text::OtFeatures::default().with_tags(tags).to_tags()))
|
||||
}
|
||||
|
||||
fn paint_param(p: &Value, k: &str, cmd: &str) -> Result<Option<Paint>> {
|
||||
@@ -176,6 +187,7 @@ impl CharChange {
|
||||
underline: flag("underline"),
|
||||
strikethrough: flag("strikethrough"),
|
||||
all_caps: flag("allCaps"),
|
||||
features: features_param(p, cmd)?,
|
||||
};
|
||||
if c.size.is_some_and(|v| v <= 0.0) {
|
||||
return Err(bad(cmd, "size must be positive"));
|
||||
@@ -200,6 +212,7 @@ impl CharChange {
|
||||
&& self.underline.is_none()
|
||||
&& self.strikethrough.is_none()
|
||||
&& self.all_caps.is_none()
|
||||
&& self.features.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn apply(&self, st: &mut CharStyle) {
|
||||
@@ -258,6 +271,9 @@ impl CharChange {
|
||||
if let Some(v) = self.all_caps {
|
||||
st.all_caps = v;
|
||||
}
|
||||
if let Some(v) = &self.features {
|
||||
st.features = v.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, font?, style?, size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour}",
|
||||
"{ids?|id?, font?, style?, size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour, features?: [\"dlig\", \"-liga\", …] OpenType}",
|
||||
has_doc,
|
||||
set_style
|
||||
),
|
||||
@@ -188,7 +188,16 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(Value::String(n)) if n.eq_ignore_ascii_case("none") => Some(drawcraft_color::Paint::None),
|
||||
Some(v) => Some(drawcraft_color::Paint::solid(color_value(v).ok_or_else(|| bad(C, "bad fill colour"))?)),
|
||||
};
|
||||
if font.is_none() && style.is_none() && size.is_none() && leading.is_none() && tracking.is_none() && justify.is_none() && fill.is_none() {
|
||||
let features = super::textedit::features_param(p, C)?;
|
||||
if font.is_none()
|
||||
&& style.is_none()
|
||||
&& size.is_none()
|
||||
&& leading.is_none()
|
||||
&& tracking.is_none()
|
||||
&& justify.is_none()
|
||||
&& fill.is_none()
|
||||
&& features.is_none()
|
||||
{
|
||||
return Err(bad(C, "nothing to change"));
|
||||
}
|
||||
let ids = text_targets(s, p, C)?;
|
||||
@@ -215,6 +224,9 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(f) = &fill {
|
||||
st.fill = f.clone();
|
||||
}
|
||||
if let Some(f) = &features {
|
||||
st.features = f.clone();
|
||||
}
|
||||
}
|
||||
if let Some(j) = justify {
|
||||
t.para.justify = j;
|
||||
|
||||
Reference in New Issue
Block a user