mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 06:16:46 -06:00
Merge pull request #1425 from Silvertree2010/fix/h3041-node-set-opacity-1-gives-full-op
Treat a % suffix on node.set opacity as a percentage
This commit is contained in:
@@ -20,6 +20,13 @@ pub fn f64_of(v: &Value) -> Option<f64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 0-1 fraction: a `%` suffix or a number above 1 is a percentage ("1%" is 0.01, 50 is 0.5).
|
||||
pub fn fraction_of(v: &Value) -> Option<f64> {
|
||||
let f = f64_of(v)?;
|
||||
let percent = v.as_str().is_some_and(|s| s.trim().ends_with('%'));
|
||||
Some(if percent || f > 1.0 { f / 100.0 } else { f })
|
||||
}
|
||||
|
||||
pub fn num(p: &Value, key: &str) -> Option<f64> {
|
||||
get(p, key).and_then(f64_of)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use serde_json::{Map, Value, json};
|
||||
|
||||
use super::{CommandSpec, ok};
|
||||
use crate::params::{
|
||||
blend, boolean, color, get, id, id_of, num, num_or, opt_index, paint_of, parse, req_id, req_index, req_num, req_string, req_targets, string,
|
||||
targets,
|
||||
blend, boolean, color, fraction_of, get, id, id_of, num, num_or, opt_index, paint_of, parse, req_id, req_index, req_num, req_string, req_targets,
|
||||
string, targets,
|
||||
};
|
||||
use crate::{BatchStep, EngineError, Result, Session, bad, spec};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
BTreeMap, Effect, EngineError, Map, Node, NodeId, NodeKind, Result, Session, Value, bad, blend, boolean, color, get, json, num, ok, opt_index,
|
||||
paint_of, parse, req_id, req_index, req_string, req_targets, roots, string, targets, z_sorted,
|
||||
BTreeMap, Effect, EngineError, Map, Node, NodeId, NodeKind, Result, Session, Value, bad, blend, boolean, color, fraction_of, get, json, num, ok,
|
||||
opt_index, paint_of, parse, req_id, req_index, req_string, req_targets, roots, string, targets, z_sorted,
|
||||
};
|
||||
|
||||
pub(super) fn batch_rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -312,8 +312,8 @@ pub(super) fn paint_set(s: &mut Session, p: &Value, fills: bool) -> Result<Value
|
||||
slot.color_alpha = c.a;
|
||||
}
|
||||
}
|
||||
if let Some(o) = num(p, "opacity") {
|
||||
slot.opacity = (if o > 1.0 { o / 100.0 } else { o }).clamp(0.0, 1.0) as f32;
|
||||
if let Some(o) = get(p, "opacity").and_then(fraction_of) {
|
||||
slot.opacity = o.clamp(0.0, 1.0) as f32;
|
||||
slot.color_alpha = 1.0;
|
||||
}
|
||||
if let Some(v) = boolean(p, "visible") {
|
||||
|
||||
@@ -9,14 +9,14 @@ use figcraft_doc::{
|
||||
use figcraft_geom::{Affine, Point};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::params::{bool_of, color_of, f64_of, get, paints_of};
|
||||
use crate::params::{bool_of, color_of, f64_of, fraction_of, get, paints_of};
|
||||
use crate::{Result, bad};
|
||||
|
||||
/// The per-side stroke weight keys, in `StrokeProps::sides` order (top, right, bottom, left).
|
||||
pub const STROKE_SIDES: [&str; 4] = ["strokeTopWeight", "strokeRightWeight", "strokeBottomWeight", "strokeLeftWeight"];
|
||||
|
||||
/// Keys `node.set` understands (documented for agents).
|
||||
pub const KEYS: &str = "name, visible, locked, opacity (0-1), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), \
|
||||
pub const KEYS: &str = "name, visible, locked, opacity (0-1 or \"50%\"), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), \
|
||||
fills (paints or colours; gradients as {type: linearGradient|radialGradient|angularGradient|diamondGradient, stops: [\"#hex\", ...], from?, to?} too), fill (colour, or \"none\"/null: no fill), strokes, stroke (colour, or \"none\"/null: no stroke), strokeWeight, strokeAlign (inside|center|outside), strokeSides [t,r,b,l], strokeTopWeight / strokeRightWeight / strokeBottomWeight / strokeLeftWeight (one side; turns on individual strokes), \
|
||||
strokeCap / startCap / endCap, strokeJoin, miterAngle (degrees: sharper joins bevel; or miterLimit as a ratio), dashes [..], widthProfile (preset uniform|wedge|taper|eye|rounded|pinch, [[t, scale], ...] or null), brush (preset id, {preset, spacing, sizeJitter, rotationJitter, scatter, seed} or null), cornerRadius (n or [tl,tr,br,bl]), cornerSmoothing (0-1), effects, \
|
||||
clip, isMask, layout (null|horizontal|vertical|grid|{...}), gap, crossGap, padding (n or [t,r,b,l]), paddingTop/Right/Bottom/Left, \
|
||||
@@ -487,8 +487,7 @@ fn apply_look(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) ->
|
||||
"visible" => doc.get_mut(id)?.visible = v.as_bool().ok_or_else(|| bad(cmd, "`visible` must be a boolean"))?,
|
||||
"locked" => doc.get_mut(id)?.locked = v.as_bool().ok_or_else(|| bad(cmd, "`locked` must be a boolean"))?,
|
||||
"opacity" => {
|
||||
let o = f64_of(v).ok_or_else(|| bad(cmd, "`opacity` must be a number"))?;
|
||||
let o = if o > 1.0 { o / 100.0 } else { o };
|
||||
let o = fraction_of(v).ok_or_else(|| bad(cmd, "`opacity` must be a number"))?;
|
||||
doc.get_mut(id)?.opacity = o.clamp(0.0, 1.0) as f32;
|
||||
}
|
||||
"blend" | "blendMode" => {
|
||||
@@ -888,20 +887,20 @@ fn apply_shape(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) ->
|
||||
}
|
||||
}
|
||||
"innerRadius" | "ratio" => {
|
||||
let r = f64_of(v).unwrap_or(0.38);
|
||||
let r = if r > 1.0 { r / 100.0 } else { r };
|
||||
let r = fraction_of(v).unwrap_or(0.38);
|
||||
match &mut doc.get_mut(id)?.kind {
|
||||
NodeKind::Star { inner_ratio, .. } => *inner_ratio = r.clamp(0.0, 1.0),
|
||||
_ => return Err(bad(cmd, "innerRadius applies to stars")),
|
||||
}
|
||||
}
|
||||
"arcStart" | "arcSweep" | "arcInner" => {
|
||||
let val = f64_of(v).ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?;
|
||||
let val = if k == "arcInner" { fraction_of(v) } else { f64_of(v) };
|
||||
let val = val.ok_or_else(|| bad(cmd, format!("`{k}` must be a number")))?;
|
||||
match &mut doc.get_mut(id)?.kind {
|
||||
NodeKind::Ellipse { start, sweep, inner } => match k {
|
||||
"arcStart" => *start = val.to_radians(),
|
||||
"arcSweep" => *sweep = val.clamp(-360.0, 360.0).to_radians(),
|
||||
_ => *inner = (if val > 1.0 { val / 100.0 } else { val }).clamp(0.0, 0.99),
|
||||
_ => *inner = val.clamp(0.0, 0.99),
|
||||
},
|
||||
_ => return Err(bad(cmd, "arc applies to ellipses")),
|
||||
}
|
||||
|
||||
@@ -475,3 +475,29 @@ fn page_navigation_skips_dividers() {
|
||||
ex(&mut s, "view.nextPage", json!({}));
|
||||
assert_eq!(s.page().unwrap(), later, "Next page stops on the last page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_percent_strings_always_divide_by_100() {
|
||||
let mut s = Session::new();
|
||||
let r = rect(&mut s, "R", 0.0, 0.0, None);
|
||||
for (v, want) in [(json!("1%"), 0.01), (json!("0.5%"), 0.005), (json!("50%"), 0.5), (json!(1), 1.0), (json!(50), 0.5), (json!(0.25), 0.25)] {
|
||||
ex(&mut s, "node.set", json!({"id": r, "props": {"opacity": v.clone()}}));
|
||||
let o = s.doc().unwrap().node(r).unwrap().opacity;
|
||||
assert!((o - want).abs() < 1e-6, "opacity {v} gave {o}, want {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_opacity_star_ratio_and_arc_ratio_read_a_percent_suffix() {
|
||||
let mut s = Session::new();
|
||||
let r = rect(&mut s, "R", 0.0, 0.0, None);
|
||||
ex(&mut s, "fill.set", json!({"ids": [r], "color": "#ff0000", "opacity": "1%"}));
|
||||
let fill = s.doc().unwrap().node(r).unwrap().fills[0].opacity;
|
||||
assert!((fill - 0.01).abs() < 1e-6, "fill opacity \"1%\" gave {fill}");
|
||||
let star = id(&ex(&mut s, "shape.star", json!({"x": 0, "y": 0, "width": 100, "height": 100, "props": {"innerRadius": "1%"}})));
|
||||
let figcraft_doc::NodeKind::Star { inner_ratio, .. } = s.doc().unwrap().node(star).unwrap().kind else { panic!("star") };
|
||||
assert!((inner_ratio - 0.01).abs() < 1e-9, "innerRadius \"1%\" gave {inner_ratio}");
|
||||
let arc = id(&ex(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 100, "height": 100, "props": {"arcInner": "1%"}})));
|
||||
let figcraft_doc::NodeKind::Ellipse { inner, .. } = s.doc().unwrap().node(arc).unwrap().kind else { panic!("ellipse") };
|
||||
assert!((inner - 0.01).abs() < 1e-9, "arcInner \"1%\" gave {inner}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user