mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
Fixes from agent reports: hex colours everywhere, camelCase in/out, unique variable ids, strict batch, ui.key modifiers, export rounding, Fit images, inner shadows on all edges
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b36fd771ab
commit
b26b449fe3
@@ -250,7 +250,12 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let label = string(p, "label").unwrap_or_else(|| "Batch".into());
|
||||
let cmds: Vec<(String, Value)> = match get(p, "commands") {
|
||||
Some(Value::Array(a)) => {
|
||||
a.iter().filter_map(|c| Some((c.get("command")?.as_str()?.to_string(), c.get("params").cloned().unwrap_or(json!({}))))).collect()
|
||||
let mut out = Vec::new();
|
||||
for (i, c) in a.iter().enumerate() {
|
||||
let cmd = c.get("command").and_then(Value::as_str).ok_or_else(|| bad("command.batch", format!("step {i} has no `command`")))?;
|
||||
out.push((cmd.to_string(), c.get("params").cloned().unwrap_or(json!({}))));
|
||||
}
|
||||
out
|
||||
}
|
||||
_ => return Err(bad("command.batch", "`commands` must be an array")),
|
||||
};
|
||||
|
||||
@@ -19,6 +19,32 @@ fontSize, lineHeight (auto|px|\"150%\"), letterSpacing (px|\"2%\"), textAlign (l
|
||||
(widthAndHeight|height|none|truncate), textDecoration, textCase, paragraphSpacing, listStyle, pointCount, innerRadius, arcStart, arcSweep, \
|
||||
arcInner, constrainProportions, description, interactions, overflow, pluginData {k: v}";
|
||||
|
||||
/// camelCase object keys → snake_case (accept the inspect/camel forms for serde-backed values).
|
||||
pub fn snake(v: &Value) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => Value::Object(
|
||||
m.iter()
|
||||
.map(|(k, v)| {
|
||||
let mut out = String::new();
|
||||
for c in k.chars() {
|
||||
if c.is_ascii_uppercase() {
|
||||
out.push('_');
|
||||
out.push(c.to_ascii_lowercase());
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
// Paint/effect type tags stay as they are.
|
||||
let key = if matches!(k.as_str(), "type" | "kind") { k.clone() } else { out };
|
||||
(key, if matches!(k.as_str(), "type" | "kind") { v.clone() } else { snake(v) })
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(a) => Value::Array(a.iter().map(snake).collect()),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_of<T: serde::de::DeserializeOwned>(v: &Value) -> Option<T> {
|
||||
let s = v.as_str()?;
|
||||
// Accept camelCase, snake/upper-case and spaced variants.
|
||||
@@ -209,7 +235,7 @@ pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map<String, Valu
|
||||
}
|
||||
}
|
||||
"cornerSmoothing" => doc.get_mut(id)?.corners.smoothing = f64_of(v).unwrap_or(0.0).clamp(0.0, 1.0),
|
||||
"effects" => doc.get_mut(id)?.effects = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("effects: {e}")))?,
|
||||
"effects" => doc.get_mut(id)?.effects = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("effects: {e}")))?,
|
||||
"clip" | "clipsContent" => doc.get_mut(id)?.clip = v.as_bool().ok_or_else(|| bad(cmd, "`clip` must be a boolean"))?,
|
||||
"isMask" => doc.get_mut(id)?.is_mask = v.as_bool().unwrap_or(false),
|
||||
"layout" | "layoutMode" => {
|
||||
@@ -229,7 +255,7 @@ pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map<String, Valu
|
||||
};
|
||||
ensure_layout(n).mode = mode;
|
||||
}
|
||||
Value::Object(_) => n.layout = Some(serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("layout: {e}")))?),
|
||||
Value::Object(_) => n.layout = Some(serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("layout: {e}")))?),
|
||||
_ => return Err(bad(cmd, "layout: none|horizontal|vertical|grid|{...}")),
|
||||
}
|
||||
}
|
||||
@@ -293,9 +319,9 @@ pub fn apply(cmd: &str, doc: &mut Document, id: NodeId, props: &Map<String, Valu
|
||||
n.constraints.vertical = enum_of(vv).ok_or_else(|| bad(cmd, "vertical: top|bottom|topBottom|center|scale"))?;
|
||||
}
|
||||
}
|
||||
"grids" | "layoutGrids" => doc.get_mut(id)?.grids = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("grids: {e}")))?,
|
||||
"grids" | "layoutGrids" => doc.get_mut(id)?.grids = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("grids: {e}")))?,
|
||||
"exports" | "exportSettings" => {
|
||||
doc.get_mut(id)?.exports = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("exports: {e}")))?
|
||||
doc.get_mut(id)?.exports = serde_json::from_value(snake(v)).map_err(|e| bad(cmd, format!("exports: {e}")))?
|
||||
}
|
||||
"interactions" => {
|
||||
doc.get_mut(id)?.interactions = serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("interactions: {e}")))?
|
||||
|
||||
@@ -72,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!("variable.createCollection", "Create variable collection", "{name, modes? [names]}", true, |s, p| {
|
||||
let name = req_string("variable.createCollection", p, "name")?;
|
||||
let doc = s.doc_mut()?;
|
||||
let id = format!("VariableCollectionId:{}", doc.variables.collections.len() + 1);
|
||||
let id = unique_id("VariableCollectionId", doc.variables.collections.iter().map(|c| c.id.as_str()));
|
||||
let modes: Vec<String> = match get(p, "modes") {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(|m| m.as_str().map(str::to_string)).collect(),
|
||||
_ => vec!["Mode 1".into()],
|
||||
@@ -396,7 +396,7 @@ fn var_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for m in &coll.modes {
|
||||
values.entry(m.id.clone()).or_insert_with(|| default.clone());
|
||||
}
|
||||
let id = format!("VariableID:{}", doc.variables.variables.len() + 1);
|
||||
let id = unique_id("VariableID", doc.variables.variables.iter().map(|v| v.id.as_str()));
|
||||
doc.variables.variables.push(Variable {
|
||||
id: id.clone(),
|
||||
name,
|
||||
@@ -455,3 +455,9 @@ pub fn apply_bindings(doc: &mut Document) {
|
||||
let _ = super::props::apply("variables", doc, id, &sets);
|
||||
}
|
||||
}
|
||||
|
||||
/// `prefix:N` with N one more than the largest existing.
|
||||
fn unique_id<'a>(prefix: &str, existing: impl Iterator<Item = &'a str>) -> String {
|
||||
let max = existing.filter_map(|e| e.strip_prefix(prefix)?.strip_prefix(':')?.parse::<u64>().ok()).max().unwrap_or(0);
|
||||
format!("{prefix}:{}", max + 1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user