mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
The Inspect tab lists the layers in and under the selection that have export settings, with their formats and sizes. Click one to export it, or Export all from the section header (one file through a save dialog, several into a folder). New command inspect.assets. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
5.5 KiB
Rust
105 lines
5.5 KiB
Rust
//! Dev-mode inspection: properties grouped with their styles and variables, positions relative
|
|
//! to the parent, and redlines on hover without Alt.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
use crate::devinspect::{Section, properties};
|
|
use crate::params::id_of;
|
|
use crate::tools::{PointerEvent, PointerKind};
|
|
|
|
fn id(v: Value) -> figcraft_doc::NodeId {
|
|
id_of(&v["id"]).unwrap()
|
|
}
|
|
|
|
fn row<'a>(sections: &'a [Section], title: &str, label: &str) -> &'a crate::devinspect::Prop {
|
|
sections.iter().find(|s| s.title == title).and_then(|s| s.rows.iter().find(|r| r.label == label)).unwrap_or_else(|| panic!("{title}/{label}"))
|
|
}
|
|
|
|
#[test]
|
|
fn properties_group_layout_auto_layout_paints_and_tokens() {
|
|
let mut s = Session::new();
|
|
let f = id(s.execute("frame.create", &json!({"x": 100, "y": 50, "width": 300, "height": 200})).unwrap());
|
|
let r = id(s.execute("shape.rectangle", &json!({"x": 130, "y": 90, "width": 80, "height": 40, "parent": f})).unwrap());
|
|
s.execute("node.set", &json!({"id": r, "props": {"fill": "#0D99FF80", "cornerRadius": 6, "opacity": 0.5}})).unwrap();
|
|
s.execute("variable.createCollection", &json!({"name": "T"})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "T", "name": "radius/sm", "type": "number", "value": 6})).unwrap();
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "cornerRadius", "variable": "radius/sm"})).unwrap();
|
|
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
|
s.execute("style.create", &json!({"type": "paint", "name": "Brand/Blue"})).unwrap();
|
|
s.execute("style.apply", &json!({"ids": [r], "style": "Brand/Blue"})).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
let p = properties(doc, r);
|
|
assert_eq!(row(&p, "Layout", "Width").value, "80");
|
|
// Top/left from the parent frame's corner (the rectangle was placed in the frame's coordinates).
|
|
assert_eq!((row(&p, "Layout", "Top").value.as_str(), row(&p, "Layout", "Left").value.as_str()), ("90", "130"));
|
|
assert_eq!(row(&p, "Layout", "Opacity").value, "50%");
|
|
let radius = row(&p, "Layout", "Radius");
|
|
assert_eq!((radius.value.as_str(), radius.token.as_deref()), ("6", Some("radius/sm")));
|
|
let fill = &p.iter().find(|s| s.title == "Fills").unwrap().rows[0];
|
|
assert_eq!(fill.value, "#0D99FF · 50%");
|
|
assert_eq!(fill.color.as_deref(), Some("#0D99FF80"));
|
|
assert_eq!(fill.token.as_deref(), Some("Brand/Blue"));
|
|
assert!(p.iter().all(|s| s.title != "Auto layout" && s.title != "Typography"));
|
|
|
|
// Auto layout: direction, gap, padding shorthand, alignment.
|
|
s.execute("layout.add", &json!({"ids": [f]})).unwrap();
|
|
s.execute("node.set", &json!({"id": f, "props": {"layoutMode": "vertical", "gap": 12, "padding": [8, 16, 8, 16]}})).unwrap();
|
|
let p = properties(s.doc().unwrap(), f);
|
|
assert_eq!(row(&p, "Auto layout", "Direction").value, "Vertical");
|
|
assert_eq!(row(&p, "Auto layout", "Gap").value, "12");
|
|
assert_eq!(row(&p, "Auto layout", "Padding").value, "8 16");
|
|
// At the top level, positions are from the page origin.
|
|
assert_eq!(row(&p, "Layout", "Left").value, "100");
|
|
|
|
// The command returns the same, for MCP.
|
|
let out = s.execute("inspect.properties", &json!({"id": r})).unwrap();
|
|
assert_eq!(out["sections"][0]["title"], "Layout");
|
|
assert!(s.execute("inspect.properties", &json!({"id": 999_999})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn typography_section_for_text() {
|
|
let mut s = Session::new();
|
|
let t = id(s.execute("text.create", &json!({"text": "Hello", "x": 0, "y": 0})).unwrap());
|
|
s.execute("node.set", &json!({"id": t, "props": {"fontSize": 18, "textAlignHorizontal": "center"}})).unwrap();
|
|
let p = properties(s.doc().unwrap(), t);
|
|
assert_eq!(row(&p, "Typography", "Size").value, "18");
|
|
assert!(p.iter().any(|s| s.title == "Typography"));
|
|
}
|
|
|
|
#[test]
|
|
fn redlines_measure_on_hover_without_alt() {
|
|
let mut s = Session::new();
|
|
let a = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap());
|
|
s.execute("shape.rectangle", &json!({"x": 100, "y": 0, "width": 50, "height": 50})).unwrap();
|
|
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
|
let hover = |s: &mut Session| {
|
|
s.pointer(&PointerEvent::at(PointerKind::Move, 125.0, 25.0, 1.0)).unwrap();
|
|
s.tool.overlay.guides.len()
|
|
};
|
|
assert_eq!(hover(&mut s), 0, "plain hover doesn't measure");
|
|
s.tool.redlines = true;
|
|
assert!(hover(&mut s) > 0, "Inspect measures on hover");
|
|
}
|
|
|
|
#[test]
|
|
fn assets_list_layers_with_export_settings_under_the_selection() {
|
|
let mut s = Session::new();
|
|
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
|
|
let icon = id(s.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 20, "height": 20, "parent": f})).unwrap());
|
|
s.execute("shape.rectangle", &json!({"x": 40, "y": 10, "width": 20, "height": 20, "parent": f})).unwrap();
|
|
s.execute("export.add", &json!({"ids": [icon]})).unwrap();
|
|
s.execute("export.add", &json!({"ids": [icon]})).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
let a = crate::devinspect::assets(doc, &[f]);
|
|
assert_eq!(a.len(), 1, "only the layer with settings");
|
|
assert_eq!(a[0].id, icon);
|
|
assert_eq!(a[0].exports.len(), 2);
|
|
assert!(a[0].exports[0].ends_with("1x"), "{:?}", a[0].exports);
|
|
s.execute("select.set", &json!({"ids": [f]})).unwrap();
|
|
let out = s.execute("inspect.assets", &json!({})).unwrap();
|
|
assert_eq!(out["assets"][0]["name"], "Ellipse");
|
|
assert!(crate::devinspect::assets(s.doc().unwrap(), &[]).is_empty());
|
|
}
|