mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 22:02:38 +00:00
Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
7c6bc49e9f
35 files changed
+1259
-72
No files matched your search
@@ -33,6 +33,7 @@ pub fn field_type(kind: &StyleKind, key: &str) -> Option<VariableType> {
|
||||
(EffectKind::DropShadow(_) | EffectKind::InnerShadow(_), "color") => Some(Color),
|
||||
(EffectKind::DropShadow(_) | EffectKind::InnerShadow(_), "radius" | "spread" | "offsetX" | "offsetY") => Some(Number),
|
||||
(EffectKind::LayerBlur(_) | EffectKind::BackgroundBlur(_), "radius" | "startRadius") => Some(Number),
|
||||
(EffectKind::Glass(_), f) if figcraft_doc::Glass::BOUND_FIELDS.contains(&f) => Some(Number),
|
||||
_ => None,
|
||||
},
|
||||
(StyleKind::Grid { grids }, Some((BoundList::Grids, i, "size" | "count" | "gutter" | "offset"))) => grids.get(i).map(|_| Number),
|
||||
|
||||
@@ -98,6 +98,41 @@ fn effect_moves_and_removals_reject_junk() {
|
||||
assert_eq!(s.doc().unwrap().node(r).unwrap().effects.len(), 1);
|
||||
}
|
||||
|
||||
fn glass(s: &Session, r: NodeId) -> figcraft_doc::Glass {
|
||||
match s.doc().unwrap().node(r).unwrap().effects.first().map(|e| &e.kind) {
|
||||
Some(EffectKind::Glass(g)) => g.clone(),
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_fields_take_number_variables() {
|
||||
let (mut s, r) = setup();
|
||||
ex(&mut s, "effect.add", json!({"ids": [r], "type": "glass"}));
|
||||
// Percent fields read the variable as the Design panel shows them; `radius` is the frost.
|
||||
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/splay", "variable": "large"}));
|
||||
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/radius", "variable": "small"}));
|
||||
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/depth", "variable": "large"}));
|
||||
let g = glass(&s, r);
|
||||
assert_eq!((g.splay, g.frost, g.depth), (0.3, 2.0, 30.0));
|
||||
// A new value follows, clamped to the field's range (frost stops at 100).
|
||||
ex(&mut s, "variable.set", json!({"variable": "small", "value": 150}));
|
||||
ex(&mut s, "variable.set", json!({"variable": "large", "value": 75}));
|
||||
let g = glass(&s, r);
|
||||
assert_eq!((g.splay, g.frost, g.depth), (0.75, figcraft_doc::Glass::MAX_FROST, 75.0));
|
||||
// A colour variable doesn't fit a glass field.
|
||||
assert!(s.execute("variable.bind", &json!({"ids": [r], "field": "effects/0/refraction", "variable": "brand"})).is_err());
|
||||
// An effect style binds them too, and every layer using it follows.
|
||||
let fx = ex_value(&mut s, "style.create", json!({"type": "effect", "name": "Pane", "from": r}))["id"].as_str().unwrap().to_string();
|
||||
ex(&mut s, "style.bind", json!({"style": fx, "field": "effects/0/refraction", "variable": "large"}));
|
||||
assert_eq!(glass(&s, r).refraction, 0.75);
|
||||
assert!(s.execute("style.bind", &json!({"style": fx, "field": "effects/0/spread", "variable": "large"})).is_err(), "glass has no spread");
|
||||
}
|
||||
|
||||
fn ex_value(s: &mut Session, cmd: &str, p: serde_json::Value) -> serde_json::Value {
|
||||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
|
||||
}
|
||||
|
||||
fn fill_hex(s: &Session, r: NodeId) -> Vec<String> {
|
||||
s.doc().unwrap().node(r).unwrap().fills.iter().filter_map(|p| p.solid_color()).map(|c| c.to_hex()).collect()
|
||||
}
|
||||
|
||||
@@ -465,7 +465,8 @@ pub fn scale_node(doc: &mut Document, id: NodeId, k: f64) -> Result<()> {
|
||||
}
|
||||
EffectKind::Glass(g) => {
|
||||
g.depth *= k;
|
||||
g.frost *= k;
|
||||
// From the frost it draws with (an older file's past 100 draws at 100).
|
||||
g.frost = (g.sanitized().frost * k).min(figcraft_doc::Glass::MAX_FROST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"style.bind",
|
||||
"Bind variable in style",
|
||||
"{style, field, variable? (id or name; none: unbind)} bind a variable to a field of the style's value: a paint style's `fills/<i>/color` (or `fill`: its first paint) and `fills/<i>/stops/<j>/color`, an effect style's `effects/<i>/<color|radius|spread|offsetX|offsetY|startRadius>`, a grid style's `layoutGrids/<i>/<size|count|gutter|offset>`, a text style's `fontSize|lineHeight|letterSpacing` (numbers, px) and `fontFamily|fontStyle` (strings). Every layer using the style shows the field resolved in its own modes; the style keeps it resolved in the default modes",
|
||||
"{style, field, variable? (id or name; none: unbind)} bind a variable to a field of the style's value: a paint style's `fills/<i>/color` (or `fill`: its first paint) and `fills/<i>/stops/<j>/color`, an effect style's `effects/<i>/<color|radius|spread|offsetX|offsetY|startRadius>` (glass: `lightAngle|lightIntensity|refraction|depth|dispersion|radius|splay`), a grid style's `layoutGrids/<i>/<size|count|gutter|offset>`, a text style's `fontSize|lineHeight|letterSpacing` (numbers, px) and `fontFamily|fontStyle` (strings). Every layer using the style shows the field resolved in its own modes; the style keeps it resolved in the default modes",
|
||||
true,
|
||||
bind_style
|
||||
),
|
||||
@@ -196,7 +196,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"variable.bind",
|
||||
"Bind variable",
|
||||
"{ids?, field (fill|stroke (first paint)|fills/<i>/color|strokes/<i>/color|fills/<i>/stops/<j>/color (gradient stops)|effects/<i>/<offsetX|offsetY|radius|startRadius|spread|color>|layoutGrids/<i>/<size|count|gutter|offset>|componentProperties/<name> (instances: boolean/text properties; variant properties take string variables naming a value)|width|height|opacity (percent)|cornerRadius|gap|paddingHorizontal|characters|visible|fontSize…; list indices bottom first), variable (null unbinds)}",
|
||||
"{ids?, field (fill|stroke (first paint)|fills/<i>/color|strokes/<i>/color|fills/<i>/stops/<j>/color (gradient stops)|effects/<i>/<offsetX|offsetY|radius|startRadius|spread|color|lightAngle|lightIntensity|refraction|depth|dispersion|splay> (glass: `radius` is its frost; intensity, refraction, dispersion and splay in percent)|layoutGrids/<i>/<size|count|gutter|offset>|componentProperties/<name> (instances: boolean/text properties; variant properties take string variables naming a value)|width|height|opacity (percent)|cornerRadius|gap|paddingHorizontal|characters|visible|fontSize…; list indices bottom first), variable (null unbinds)}",
|
||||
true,
|
||||
bind
|
||||
),
|
||||
@@ -1304,6 +1304,9 @@ pub(crate) fn set_effect_field(effects: &mut [Effect], i: usize, prop: &str, val
|
||||
(EffectKind::DropShadow(s) | EffectKind::InnerShadow(s), "color", _, VariableValue::Color(c)) => s.color = *c,
|
||||
(EffectKind::LayerBlur(b) | EffectKind::BackgroundBlur(b), "radius", Some(x), _) => b.radius = x.max(0.0),
|
||||
(EffectKind::LayerBlur(b) | EffectKind::BackgroundBlur(b), "startRadius", Some(x), _) => b.start_radius = x.max(0.0),
|
||||
(EffectKind::Glass(g), field, Some(x), _) => {
|
||||
g.set_bound(field, x);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ pub fn properties_with(doc: &Document, id: NodeId, fmt: ColorFormat) -> Vec<Sect
|
||||
pct(g.refraction),
|
||||
num(g.depth),
|
||||
pct(g.dispersion),
|
||||
num(g.frost),
|
||||
num(g.sanitized().frost),
|
||||
pct(g.splay)
|
||||
),
|
||||
),
|
||||
|
||||
@@ -95,6 +95,32 @@ fn scaling_scales_grain_and_glass() {
|
||||
assert_eq!((g.refraction, nz.density), (d.refraction, 0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaling_a_frost_from_an_older_file_starts_from_what_it_draws() {
|
||||
// A frost of 150, saved before the 0–100 range, draws at 100: halved, it draws at 50.
|
||||
let mut s = Session::new();
|
||||
let r = rect(&mut s);
|
||||
s.execute("effect.add", &json!({"ids": [r], "type": "glass"})).unwrap();
|
||||
s.execute("effect.set", &json!({"ids": [r], "index": 0, "effect": {"type": "glass", "frost": 150}})).unwrap();
|
||||
s.execute("transform.scale", &json!({"ids": [r], "factor": 0.5})).unwrap();
|
||||
let EffectKind::Glass(g) = &s.doc().unwrap().node(r).unwrap().effects[0].kind else { panic!() };
|
||||
assert_eq!(g.frost, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_shows_a_frost_from_an_older_file_as_it_draws() {
|
||||
// A frost of 150, saved before the 0–100 range, draws at 100: Dev Mode says 100 as CSS does.
|
||||
let mut s = Session::new();
|
||||
let r = rect(&mut s);
|
||||
s.execute("effect.add", &json!({"ids": [r], "type": "glass"})).unwrap();
|
||||
s.execute("effect.set", &json!({"ids": [r], "index": 0, "effect": {"type": "glass", "frost": 150}})).unwrap();
|
||||
let doc = s.doc().unwrap();
|
||||
let sections = crate::devinspect::properties(doc, r);
|
||||
let glass = §ions.iter().find(|s| s.title == "Effects").unwrap().rows[0];
|
||||
assert!(glass.value.contains(" frost 100 "), "{}", glass.value);
|
||||
assert!(code_for(doc, r, CodeLang::Css).contains("backdrop-filter: blur(50px);"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_and_css_show_the_new_effects() {
|
||||
let mut s = Session::new();
|
||||
|
||||
Reference in new issue
Block a user