mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 13:05:01 +00:00
* M6: .fig export writes a page's prototype background (#2326) The export writes a page's prototype background as prototypeBackgroundColor (field 141, a plain colour) when it differs from the default, as Figma does for a page whose background was set. The device-settings warning now fires only for the device, rotation, colour, custom size and responsive settings. Measured on a synthetic file saved from Figma's web app. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 measured and wrote at medium * M6: the .fig export writes a prototype background opaque and warns for its opacity, and its tests read the written record (#2326) The colour is written with alpha 1, as the measured record has it and as the import reads it, and a background that isn't opaque warns that its opacity was left out. The tests now read the written record, and each device setting alone is checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Fable 5.2 reviewed; Sonnet 5.5 wrote at medium * M6: the .fig export writes every page's prototype background, so Figma shows the same colour (#2326) A page at FigCraft's default background used to write no field, so Figma showed its own default instead. Every page now writes its colour, opaque. The test for a default page reads the written record and the import. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
7b34dbab00
commit
fb9df220b1
@@ -38,6 +38,8 @@ mod paste_style_tests;
|
||||
#[cfg(test)]
|
||||
mod prop_value_tests;
|
||||
#[cfg(test)]
|
||||
mod proto_background_tests;
|
||||
#[cfg(test)]
|
||||
mod scale_factor_tests;
|
||||
#[cfg(test)]
|
||||
mod slot_tests;
|
||||
|
||||
@@ -190,7 +190,7 @@ message GridTrackSizingFunction GridTrackSizingType type=1, float value=2
|
||||
message GridTrackSize GridTrackSizingFunction minSizing=1, GridTrackSizingFunction maxSizing=2
|
||||
message GUIDGridTrackSizeMapEntry GUID id=1, GridTrackSize trackSize=2
|
||||
message GUIDGridTrackSizeMap GUIDGridTrackSizeMapEntry[] entries=1
|
||||
message NodeChange GUID guid=1, NodePhase phase=2, ParentIndex parentIndex=3, NodeType type=4, string name=5, bool visible=6, bool locked=7, float opacity=8, BlendMode blendMode=9, Vector size=11, Matrix transform=12, bool mask=16, MaskType maskType=317, float backgroundOpacity=19, bool backgroundEnabled=15, bool internalOnly=142, string description=318, StyleType styleType=163, string sortPosition=320, StyleId styleIdForFill=332, StyleId styleIdForStrokeFill=333, StyleId styleIdForText=334, StyleId styleIdForEffect=335, StyleId styleIdForGrid=336, VariableDataMap variableConsumptionMap=307, VariableModeBySetMap variableModeBySetMap=316, VariableSetMode[] variableSetModes=312, VariableSetID variableSetID=313, VariableResolvedDataType variableResolvedType=314, VariableDataValues variableDataValues=315, VariableScope[] variableScopes=353, DocumentColorProfile documentColorProfile=349, Paint[] fillPaints=38, Paint[] strokePaints=39, float strokeWeight=26, StrokeAlign strokeAlign=29, StrokeJoin strokeJoin=31, StrokeCap strokeCap=30, float[] dashPattern=13, float miterLimit=25, bool borderStrokeWeightsIndependent=299, float borderTopWeight=295, float borderRightWeight=298, float borderBottomWeight=296, float borderLeftWeight=297, Effect[] effects=43, float cornerRadius=20, float cornerSmoothing=160, bool rectangleCornerRadiiIndependent=149, float rectangleTopLeftCornerRadius=145, float rectangleTopRightCornerRadius=146, float rectangleBottomRightCornerRadius=148, float rectangleBottomLeftCornerRadius=147, ConstraintType horizontalConstraint=28, ConstraintType verticalConstraint=37, bool frameMaskDisabled=115, bool resizeToFit=117, bool isStateGroup=225, StateGroupPropertyValueOrder[] stateGroupPropertyValueOrders=238, bool sectionContentsHidden=283, Color backgroundColor=50, StackMode stackMode=105, float stackSpacing=107, float stackCounterSpacing=324, StackWrap stackWrap=323, StackCounterAlignContent stackCounterAlignContent=343, float stackHorizontalPadding=209, float stackVerticalPadding=210, float stackPaddingRight=233, float stackPaddingBottom=234, StackJustify stackPrimaryAlignItems=230, StackAlign stackCounterAlignItems=231, StackSize stackPrimarySizing=229, StackSize stackCounterSizing=221, bool bordersTakeSpace=294, bool stackReverseZIndex=271, float gridColumnGap=438, float gridRowGap=437, GUIDPositionMap gridColumns=436, GUIDPositionMap gridRows=435, GUIDGridTrackSizeMap gridColumnsSizing=474, GUIDGridTrackSizeMap gridRowsSizing=475, float stackChildPrimaryGrow=232, StackCounterAlign stackChildAlignSelf=236, StackPositioning stackPositioning=269, OptionalVector minSize=325, OptionalVector maxSize=326, GUID gridColumnAnchor=440, GUID gridRowAnchor=439, uint gridColumnSpan=442, uint gridRowSpan=441, LayoutGrid[] layoutGrids=47, ExportSettings[] exportSettings=45, TextData textData=42, FontName fontName=41, float fontSize=21, Number lineHeight=40, Number letterSpacing=165, TextCase textCase=34, TextDecoration textDecoration=35, TextAlignHorizontal textAlignHorizontal=32, TextAlignVertical textAlignVertical=33, TextAutoResize textAutoResize=46, TextTruncation textTruncation=280, int maxLines=351, float paragraphSpacing=23, float paragraphIndent=22, float listSpacing=264, LeadingTrim leadingTrim=322, bool hangingPunctuation=337, bool hangingList=339, Hyperlink hyperlink=223, bool autoRename=14, bool fontVariantCommonLigatures=127, bool fontVariantContextualLigatures=128, string fontVersion=202, uint textUserLayoutVersion=203, uint textExplicitLayoutVersion=396, uint textBidiVersion=279, float textTracking=27, bool textDecorationSkipInk=412, EmojiImageSet emojiImageSet=391, uint styleID=49, Path[] fillGeometry=51, Path[] strokeGeometry=52, VectorData vectorData=48, VectorMirror handleMirroring=44, uint count=10, float starInnerScale=24, ArcData arcData=195, BooleanOperation booleanOperation=36, string symbolDescription=144, VariableWidthPoint[] variableWidthPoints=447, PluginData[] pluginData=204, SymbolData symbolData=113, NodeChange[] derivedSymbolData=125, GUID overriddenSymbolID=143, ComponentPropDef[] componentPropDefs=266, ComponentPropRef[] componentPropRefs=267, ComponentPropAssignment[] componentPropAssignments=268, VariantPropSpec[] variantPropSpecs=483, VariableDataMap parameterConsumptionMap=445, GUIDPath guidPath=111, PrototypeInteraction[] prototypeInteractions=226, PrototypeStartingPoint prototypeStartingPoint=249, ScrollDirection scrollDirection=159, ScrollBehavior scrollBehavior=186, OverlayPositionType overlayPositionType=198, OverlayBackgroundInteraction overlayBackgroundInteraction=200, OverlayBackgroundAppearance overlayBackgroundAppearance=201
|
||||
message NodeChange GUID guid=1, NodePhase phase=2, ParentIndex parentIndex=3, NodeType type=4, string name=5, bool visible=6, bool locked=7, float opacity=8, BlendMode blendMode=9, Vector size=11, Matrix transform=12, bool mask=16, MaskType maskType=317, float backgroundOpacity=19, bool backgroundEnabled=15, bool internalOnly=142, string description=318, StyleType styleType=163, string sortPosition=320, StyleId styleIdForFill=332, StyleId styleIdForStrokeFill=333, StyleId styleIdForText=334, StyleId styleIdForEffect=335, StyleId styleIdForGrid=336, VariableDataMap variableConsumptionMap=307, VariableModeBySetMap variableModeBySetMap=316, VariableSetMode[] variableSetModes=312, VariableSetID variableSetID=313, VariableResolvedDataType variableResolvedType=314, VariableDataValues variableDataValues=315, VariableScope[] variableScopes=353, DocumentColorProfile documentColorProfile=349, Paint[] fillPaints=38, Paint[] strokePaints=39, float strokeWeight=26, StrokeAlign strokeAlign=29, StrokeJoin strokeJoin=31, StrokeCap strokeCap=30, float[] dashPattern=13, float miterLimit=25, bool borderStrokeWeightsIndependent=299, float borderTopWeight=295, float borderRightWeight=298, float borderBottomWeight=296, float borderLeftWeight=297, Effect[] effects=43, float cornerRadius=20, float cornerSmoothing=160, bool rectangleCornerRadiiIndependent=149, float rectangleTopLeftCornerRadius=145, float rectangleTopRightCornerRadius=146, float rectangleBottomRightCornerRadius=148, float rectangleBottomLeftCornerRadius=147, ConstraintType horizontalConstraint=28, ConstraintType verticalConstraint=37, bool frameMaskDisabled=115, bool resizeToFit=117, bool isStateGroup=225, StateGroupPropertyValueOrder[] stateGroupPropertyValueOrders=238, bool sectionContentsHidden=283, Color backgroundColor=50, StackMode stackMode=105, float stackSpacing=107, float stackCounterSpacing=324, StackWrap stackWrap=323, StackCounterAlignContent stackCounterAlignContent=343, float stackHorizontalPadding=209, float stackVerticalPadding=210, float stackPaddingRight=233, float stackPaddingBottom=234, StackJustify stackPrimaryAlignItems=230, StackAlign stackCounterAlignItems=231, StackSize stackPrimarySizing=229, StackSize stackCounterSizing=221, bool bordersTakeSpace=294, bool stackReverseZIndex=271, float gridColumnGap=438, float gridRowGap=437, GUIDPositionMap gridColumns=436, GUIDPositionMap gridRows=435, GUIDGridTrackSizeMap gridColumnsSizing=474, GUIDGridTrackSizeMap gridRowsSizing=475, float stackChildPrimaryGrow=232, StackCounterAlign stackChildAlignSelf=236, StackPositioning stackPositioning=269, OptionalVector minSize=325, OptionalVector maxSize=326, GUID gridColumnAnchor=440, GUID gridRowAnchor=439, uint gridColumnSpan=442, uint gridRowSpan=441, LayoutGrid[] layoutGrids=47, ExportSettings[] exportSettings=45, TextData textData=42, FontName fontName=41, float fontSize=21, Number lineHeight=40, Number letterSpacing=165, TextCase textCase=34, TextDecoration textDecoration=35, TextAlignHorizontal textAlignHorizontal=32, TextAlignVertical textAlignVertical=33, TextAutoResize textAutoResize=46, TextTruncation textTruncation=280, int maxLines=351, float paragraphSpacing=23, float paragraphIndent=22, float listSpacing=264, LeadingTrim leadingTrim=322, bool hangingPunctuation=337, bool hangingList=339, Hyperlink hyperlink=223, bool autoRename=14, bool fontVariantCommonLigatures=127, bool fontVariantContextualLigatures=128, string fontVersion=202, uint textUserLayoutVersion=203, uint textExplicitLayoutVersion=396, uint textBidiVersion=279, float textTracking=27, bool textDecorationSkipInk=412, EmojiImageSet emojiImageSet=391, uint styleID=49, Path[] fillGeometry=51, Path[] strokeGeometry=52, VectorData vectorData=48, VectorMirror handleMirroring=44, uint count=10, float starInnerScale=24, ArcData arcData=195, BooleanOperation booleanOperation=36, string symbolDescription=144, VariableWidthPoint[] variableWidthPoints=447, PluginData[] pluginData=204, SymbolData symbolData=113, NodeChange[] derivedSymbolData=125, GUID overriddenSymbolID=143, ComponentPropDef[] componentPropDefs=266, ComponentPropRef[] componentPropRefs=267, ComponentPropAssignment[] componentPropAssignments=268, VariantPropSpec[] variantPropSpecs=483, VariableDataMap parameterConsumptionMap=445, GUIDPath guidPath=111, PrototypeInteraction[] prototypeInteractions=226, PrototypeStartingPoint prototypeStartingPoint=249, ScrollDirection scrollDirection=159, ScrollBehavior scrollBehavior=186, OverlayPositionType overlayPositionType=198, OverlayBackgroundInteraction overlayBackgroundInteraction=200, OverlayBackgroundAppearance overlayBackgroundAppearance=201, Color prototypeBackgroundColor=141
|
||||
struct Blob byte[] bytes
|
||||
message Message MessageType type=1, uint sessionID=2, NodeChange[] nodeChanges=4, Blob[] blobs=6
|
||||
";
|
||||
|
||||
@@ -26,7 +26,15 @@ impl<'d> Exporter<'d> {
|
||||
if !n.guides.is_empty() {
|
||||
self.warn("ruler guides were left out");
|
||||
}
|
||||
if *prototype != Default::default() {
|
||||
let defaults = figcraft_doc::proto::PrototypeSettings::default();
|
||||
// Every page writes its colour, so Figma shows the same one (a page without the field shows Figma's own default).
|
||||
// The colour is written opaque: the record measured has alpha 1, and the import reads the colour as opaque and
|
||||
// an alpha of 0 as no background.
|
||||
f.push(("prototypeBackgroundColor", color(prototype.background.with_alpha(1.0))));
|
||||
if prototype.background.a < 1.0 {
|
||||
self.warn("a prototype background's opacity was left out");
|
||||
}
|
||||
if (figcraft_doc::proto::PrototypeSettings { background: defaults.background, ..prototype.clone() }) != defaults {
|
||||
self.warn("prototype device settings were left out");
|
||||
}
|
||||
self.modes(n, &mut f);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! A page's prototype background in `.fig` exports.
|
||||
|
||||
use figcraft_doc::color::Color;
|
||||
use figcraft_doc::proto::{DeviceRotation, PrototypeSettings};
|
||||
use figcraft_doc::{Document, NodeKind};
|
||||
|
||||
use super::kiwi::Kv;
|
||||
use super::{Archive, decode_canvas, export, import};
|
||||
|
||||
fn blue() -> Color {
|
||||
Color::from_rgb8_const(0x33, 0x66, 0x99)
|
||||
}
|
||||
|
||||
/// The background of the first page, and its device, after exporting and importing `doc`.
|
||||
fn back(doc: &Document) -> (Color, String, Vec<String>) {
|
||||
let (bytes, warnings) = export(doc, None).unwrap();
|
||||
let (imported, _) = import(&bytes, "x").unwrap();
|
||||
let page = imported.first_page().and_then(|p| imported.node(p)).unwrap();
|
||||
let NodeKind::Page { prototype, .. } = &page.kind else { panic!("not a page") };
|
||||
(prototype.background, prototype.device.clone(), warnings)
|
||||
}
|
||||
|
||||
/// The `prototypeBackgroundColor` (r, g, b, a) in the record the export writes for the page of `doc`.
|
||||
fn written(doc: &Document) -> Option<[f32; 4]> {
|
||||
let (bytes, _) = export(doc, None).unwrap();
|
||||
let archive = Archive::open(&bytes).unwrap();
|
||||
let (schema, message) = decode_canvas(&archive.canvas).unwrap();
|
||||
let msg = schema.decode_message(schema.find("Message").unwrap(), &message).unwrap();
|
||||
let page = msg.get("nodeChanges").unwrap().list().iter().find(|n| n.get("type").and_then(Kv::str) == Some("CANVAS")).unwrap();
|
||||
page.get("prototypeBackgroundColor").map(|c| ["r", "g", "b", "a"].map(|k| c.get(k).and_then(Kv::f64).unwrap() as f32))
|
||||
}
|
||||
|
||||
fn with_prototype(set: impl FnOnce(&mut figcraft_doc::proto::PrototypeSettings)) -> Document {
|
||||
let mut doc = Document::new("t");
|
||||
let page = doc.first_page().unwrap();
|
||||
if let Some(NodeKind::Page { prototype, .. }) = doc.node_mut(page).map(|n| &mut n.kind) {
|
||||
set(prototype);
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pages_prototype_background_survives_a_fig_export() {
|
||||
let doc = with_prototype(|p| p.background = blue());
|
||||
assert_eq!(written(&doc), Some([blue().r, blue().g, blue().b, 1.0]));
|
||||
let (background, _, warnings) = back(&doc);
|
||||
assert_eq!(background, blue());
|
||||
assert!(warnings.iter().all(|w| !w.contains("prototype")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_with_a_device_keeps_its_background_and_warns_for_the_device() {
|
||||
let doc = with_prototype(|p| {
|
||||
p.background = blue();
|
||||
p.device = "iPhone 17".into();
|
||||
});
|
||||
let (background, _, warnings) = back(&doc);
|
||||
assert_eq!(background.to_hex(), blue().to_hex());
|
||||
assert!(warnings.iter().any(|w| w.contains("prototype device settings were left out")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_default_page_writes_its_prototype_background_so_figma_shows_the_same_colour() {
|
||||
let doc = with_prototype(|_| {});
|
||||
let default = PrototypeSettings::default().background;
|
||||
assert_eq!(default.to_hex(), "1E1E1E");
|
||||
assert_eq!(written(&doc), Some([default.r, default.g, default.b, 1.0]));
|
||||
let (background, _, warnings) = back(&doc);
|
||||
assert_eq!(background, default);
|
||||
assert!(warnings.iter().all(|w| !w.contains("prototype")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_schema_numbers_the_prototype_background() {
|
||||
let s = super::kiwi::Schema::parse(super::export::SCHEMA).unwrap();
|
||||
let def = |n: &str| s.defs.iter().find(|d| d.name == n).unwrap();
|
||||
let id = |f: &str| def("NodeChange").fields.iter().find(|x| x.name == f).unwrap().value;
|
||||
assert_eq!(id("prototypeBackgroundColor"), 141);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_device_setting_alone_warns_and_keeps_the_default_background() {
|
||||
let settings: [fn(&mut PrototypeSettings); 5] = [
|
||||
|p| p.device = "iPhone 17".into(),
|
||||
|p| p.rotation = DeviceRotation::Landscape,
|
||||
|p| p.color = "White".into(),
|
||||
|p| p.custom_size = [320.0, 480.0],
|
||||
|p| p.responsive = true,
|
||||
];
|
||||
for (i, set) in settings.into_iter().enumerate() {
|
||||
let doc = with_prototype(set);
|
||||
let default = PrototypeSettings::default().background;
|
||||
assert_eq!(written(&doc), Some([default.r, default.g, default.b, 1.0]), "setting {i}");
|
||||
let (_, _, warnings) = back(&doc);
|
||||
assert!(warnings.iter().any(|w| w == "prototype device settings were left out"), "setting {i}: {warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prototype_background_is_written_opaque() {
|
||||
// The import reads an alpha of 0 as no background, and any other alpha as 1.
|
||||
for alpha in [0.0, 0.5] {
|
||||
let doc = with_prototype(|p| p.background = blue().with_alpha(alpha));
|
||||
assert_eq!(written(&doc), Some([blue().r, blue().g, blue().b, 1.0]), "alpha {alpha}");
|
||||
let (background, _, warnings) = back(&doc);
|
||||
assert_eq!(background, blue(), "alpha {alpha}");
|
||||
assert!(warnings.iter().any(|w| w == "a prototype background's opacity was left out"), "alpha {alpha}: {warnings:?}");
|
||||
}
|
||||
// The default colour is written opaque too.
|
||||
let doc = with_prototype(|p| p.background = p.background.with_alpha(0.5));
|
||||
let default = PrototypeSettings::default().background;
|
||||
assert_eq!(written(&doc), Some([default.r, default.g, default.b, 1.0]));
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user