mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 19:36:46 -06:00
#801: keep a component set's outline as its real stroke (reverts the export change)
This commit is contained in:
@@ -565,14 +565,14 @@ impl Node {
|
||||
NodeKind::Text(_) => vec![Paint::solid(Color::BLACK)],
|
||||
_ => vec![],
|
||||
};
|
||||
// A component set's purple dashed frame is editor chrome: the canvas draws it, and it is
|
||||
// never a stroke in the document, so exports don't show it.
|
||||
let strokes = match &kind {
|
||||
NodeKind::Line => vec![Paint::solid(Color::BLACK)],
|
||||
NodeKind::ComponentSet { .. } => vec![Paint::solid(Color::from_rgb8_const(0x97, 0x47, 0xFF))],
|
||||
_ => vec![],
|
||||
};
|
||||
let stroke = match &kind {
|
||||
NodeKind::Line | NodeKind::Vector { .. } => StrokeProps { align: StrokeAlign::Center, ..Default::default() },
|
||||
NodeKind::ComponentSet { .. } => StrokeProps { dashes: vec![10.0, 5.0], ..Default::default() },
|
||||
_ => StrokeProps::default(),
|
||||
};
|
||||
let blend = if kind.is_container() { BlendMode::PassThrough } else { BlendMode::Normal };
|
||||
|
||||
@@ -317,20 +317,6 @@ fn svg_export_options() {
|
||||
assert!(svg.contains("stroke-width=\"4\""), "an inside stroke on a rectangle simplifies too (the rectangle inset by half the weight)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_component_sets_editor_outline_is_not_exported() {
|
||||
let mut s = Session::new();
|
||||
let a = id(&ex(&mut s, "node.create", json!({"type": "component", "x": 0, "y": 0, "width": 100, "height": 40})));
|
||||
let b = id(&ex(&mut s, "node.create", json!({"type": "component", "x": 300, "y": 0, "width": 100, "height": 40})));
|
||||
let set = id(&ex(&mut s, "component.combineVariants", json!({"ids": [a, b]})));
|
||||
assert!(s.doc().unwrap().node(set).unwrap().strokes.is_empty(), "the purple dashed frame is editor chrome, not a stroke");
|
||||
let svg = text(&export(&mut s, json!({"ids": [set], "format": "svg"})));
|
||||
assert!(!svg.to_ascii_lowercase().contains("9747ff") && !svg.contains("stroke-dasharray"), "{svg}");
|
||||
let png = pixels(&export(&mut s, json!({"ids": [set]})));
|
||||
let purple = png.pixels().filter(|p| p[3] > 0 && p[0].abs_diff(0x97) < 24 && p[1].abs_diff(0x47) < 24 && p[2].abs_diff(0xFF) < 24).count();
|
||||
assert_eq!(purple, 0, "the exported set has no purple outline pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_as_png_svg_and_code() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -186,36 +186,6 @@ fn files_with_every_field_spelled_out_still_open() {
|
||||
assert_same_doc(&d, &again);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_component_sets_old_editor_outline_is_dropped_only_without_the_marker() {
|
||||
let mut d = Document::new("Sets");
|
||||
let page = d.first_page().unwrap();
|
||||
let old = d.create(page, NodeKind::ComponentSet { description: String::new() }, "Button", 0.0, 0.0, 100.0, 40.0).unwrap();
|
||||
let custom = d.create(page, NodeKind::ComponentSet { description: String::new() }, "Chip", 0.0, 80.0, 100.0, 40.0).unwrap();
|
||||
// As 0.6.0 wrote every set: the canvas's purple dashed outline stored as a real stroke.
|
||||
let n = d.get_mut(old).unwrap();
|
||||
n.strokes = vec![Paint::solid(Color::from_rgb8_const(0x97, 0x47, 0xFF))];
|
||||
n.stroke.dashes = vec![10.0, 5.0];
|
||||
// A stroke someone chose is design content and stays.
|
||||
let n = d.get_mut(custom).unwrap();
|
||||
n.strokes = vec![Paint::solid(Color::from_rgb8_const(0x11, 0x22, 0x33))];
|
||||
n.stroke.dashes = vec![1.0, 2.0];
|
||||
// A file this version writes carries the marker: the same stroke is a choice, not decoration.
|
||||
let text = serde_json::to_string(&to_json(&d).unwrap()).unwrap();
|
||||
let (current, _) = from_json(&text).unwrap();
|
||||
assert_eq!(current.node(old).unwrap().strokes.len(), 1, "a marked file keeps a stroke someone chose");
|
||||
assert_eq!(current.node(custom).unwrap().strokes.len(), 1);
|
||||
// A file without the marker is from a version that stored the decoration; drop exactly that.
|
||||
let mut legacy = to_json(&d).unwrap();
|
||||
legacy.as_object_mut().unwrap().remove("editorOutlines");
|
||||
let (back, _) = from_json(&serde_json::to_string(&legacy).unwrap()).unwrap();
|
||||
let old = back.node(old).unwrap();
|
||||
assert!(old.strokes.is_empty() && old.stroke.dashes.is_empty(), "the decoration is canvas chrome, not a document stroke");
|
||||
let custom = back.node(custom).unwrap();
|
||||
assert_eq!(custom.strokes.len(), 1, "a real stroke is not the decoration");
|
||||
assert_eq!(custom.stroke.dashes, vec![1.0, 2.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_header_is_read_on_its_own() {
|
||||
// A newer file is refused before its document is looked at, whatever the version's size.
|
||||
|
||||
@@ -28,8 +28,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use figcraft_doc::color::{Color, Paint};
|
||||
use figcraft_doc::{Document, ImageData, NodeKind};
|
||||
use figcraft_doc::{Document, ImageData};
|
||||
use serde::de::{Deserializer, IgnoredAny, MapAccess, Visitor};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::{Value, json};
|
||||
@@ -232,18 +231,13 @@ struct DocumentJson<'a> {
|
||||
version: u32,
|
||||
#[serde(rename = "imageIndex", serialize_with = "image_index")]
|
||||
images: &'a BTreeMap<String, ImageData>,
|
||||
/// The editor draws component-set outlines itself instead of storing them as strokes. Files
|
||||
/// without this marker were written by a version that stored the purple dashed outline as a
|
||||
/// real stroke (see [`drop_component_set_decoration`]).
|
||||
#[serde(rename = "editorOutlines")]
|
||||
editor_outlines: bool,
|
||||
#[serde(flatten)]
|
||||
doc: &'a Document,
|
||||
}
|
||||
|
||||
impl<'a> DocumentJson<'a> {
|
||||
fn new(doc: &'a Document) -> Self {
|
||||
Self { format: FORMAT, version: VERSION, images: &doc.images, editor_outlines: true, doc }
|
||||
Self { format: FORMAT, version: VERSION, images: &doc.images, doc }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,8 +276,6 @@ type ImageIndex = Option<BTreeSet<String>>;
|
||||
struct Header {
|
||||
version: Option<u64>,
|
||||
images: ImageIndex,
|
||||
/// Whether the writer drew component-set outlines outside the document.
|
||||
editor_outlines: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Header {
|
||||
@@ -299,7 +291,6 @@ impl<'de> Deserialize<'de> for Header {
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
match key.as_str() {
|
||||
"version" => h.version = map.next_value::<Value>()?.as_u64(),
|
||||
"editorOutlines" => h.editor_outlines = map.next_value::<Value>()?.as_bool().unwrap_or(false),
|
||||
"imageIndex" => {
|
||||
h.images = map
|
||||
.next_value::<Value>()?
|
||||
@@ -318,21 +309,6 @@ impl<'de> Deserialize<'de> for Header {
|
||||
}
|
||||
}
|
||||
|
||||
/// Earlier versions gave every component set the purple dashed outline of the canvas as a real
|
||||
/// stroke, so it exported with the set. New sets carry no paint; files written back then lose
|
||||
/// exactly that decoration when opened, and the canvas draws the outline itself.
|
||||
fn drop_component_set_decoration(doc: &mut Document) {
|
||||
let decoration = Paint::solid(Color::from_rgb8_const(0x97, 0x47, 0xFF));
|
||||
let ids: Vec<_> = doc.nodes().filter(|n| matches!(n.kind, NodeKind::ComponentSet { .. })).map(|n| n.id).collect();
|
||||
for id in ids {
|
||||
let Some(n) = doc.node_mut(id) else { continue };
|
||||
if n.strokes.len() == 1 && n.strokes.first() == Some(&decoration) && n.stroke.dashes.as_slice() == [10.0, 5.0] {
|
||||
n.strokes.clear();
|
||||
n.stroke.dashes.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `document.json` content; also returns its [`ImageIndex`]. The header is checked first
|
||||
/// (a newer file is refused before its document is read), then the document is deserialized
|
||||
/// straight from the text.
|
||||
@@ -345,11 +321,6 @@ fn parse_json(text: &str) -> Result<(Document, Vec<String>, ImageIndex), FormatE
|
||||
return Err(FormatError::Newer(u32::try_from(ver).unwrap_or(u32::MAX)));
|
||||
}
|
||||
let mut doc: Document = serde_json::from_str(text).map_err(corrupt)?;
|
||||
// Only files from before the marker can carry the old stored decoration; a file this version
|
||||
// wrote keeps a stroke someone chose, even one that looks exactly like it.
|
||||
if !header.editor_outlines {
|
||||
drop_component_set_decoration(&mut doc);
|
||||
}
|
||||
let issues = doc.validate();
|
||||
if !issues.is_empty() {
|
||||
doc.repair();
|
||||
|
||||
@@ -936,13 +936,6 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
|
||||
if labelled && let Some(q) = crate::frame_labels::draw(app, painter, t, &doc, *c, sel.contains(c), ds.hover == Some(*c)) {
|
||||
labels.push((*c, q.map(|p| app.to_page(p))));
|
||||
}
|
||||
// Component sets keep Figma's purple dashed frame on the canvas: editor chrome, not a
|
||||
// document paint, so it never shows in an export.
|
||||
if matches!(n.kind, NodeKind::ComponentSet { .. })
|
||||
&& let Some(q) = node_quad(app, &doc, *c)
|
||||
{
|
||||
painter.add(Shape::dashed_line(&[q[0], q[1], q[2], q[3], q[0]], stroke(COMPONENT, 1.0), 4.0, 3.0));
|
||||
}
|
||||
}
|
||||
// Prototype connections.
|
||||
crate::proto_canvas::draw(app, painter);
|
||||
|
||||
@@ -51,20 +51,6 @@ fn frame_outlines_and_slices_draw_when_shown() {
|
||||
assert!(!outlined(&shapes, quad(&u, inner)) && dashes_along(&shapes, sq[0], sq[1]) == 0, "both turn off again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_sets_draw_their_editor_outline() {
|
||||
let mut s = Session::new();
|
||||
let a = id(&s.execute("node.create", &json!({"type": "component", "x": 0, "y": 0, "width": 100, "height": 40})).unwrap());
|
||||
let b = id(&s.execute("node.create", &json!({"type": "component", "x": 200, "y": 0, "width": 100, "height": 40})).unwrap());
|
||||
let set = id(&s.execute("component.combineVariants", &json!({"ids": [a, b]})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": []})).unwrap();
|
||||
assert!(s.doc().unwrap().node(set).unwrap().strokes.is_empty(), "the outline is not a document stroke");
|
||||
let mut u = Ui::new(s);
|
||||
let q = crate::canvas::node_quad(&u.app, u.app.session.doc().unwrap(), set).unwrap();
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
assert!(dashes_along(&shapes, q[0], q[1]) > 3, "the set shows its dashed outline");
|
||||
}
|
||||
|
||||
/// A job a few hundred milliseconds of rendering big.
|
||||
fn big_job(doc: &Arc<figcraft_doc::Document>, page: figcraft_doc::NodeId, scale: f64) -> crate::render_worker::Job {
|
||||
crate::render_worker::Job {
|
||||
|
||||
@@ -76,7 +76,6 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | minor | Component sets: the purple dashed frame is canvas chrome again. A new set carries no stroke, the canvas draws the outline, and exports and screenshots leave it out; a file written by 0.6.0 loses exactly that stored decoration when opened. A stroke someone chose stays. Numbers unchanged |
|
||||
| 2026-10-18 | minor | Variables import (#790): a `variables` map outside `meta` (or `meta.variables` next to top-level collections) is read, and an import that finds no variables, or fewer than the collections' `variableIds` list, says so in `warnings` instead of succeeding silently |
|
||||
| 2026-10-17 | minor | Variant properties reorder by dragging them in the Properties section, one undo step (#679; was "no drag in the panel yet"); a library file saved again right after it was read is read again, so a quick republish isn't missed (#652); the Hidden from publishing row no longer says the Hide when publishing item is missing (it landed 2026-10-12). Numbers unchanged |
|
||||
| 2026-10-17 | minor | Size and resizing overrides of layers inside instances (#731): a layer in an instance can hug, fill or keep a size of its own (group `resizing`: hug/fill/fixed with min and max; `size`), from the Layout panel, the canvas handles and `node.set`; they were refused, and imported ones were dropped at the first relayout. Kept through main edits of other properties, rebuilds, undo, save, `.fig` and REST import (sizes a `.fig` instance gives its layers and the REST `overrides` fields), `.fig` export and detach; reset per group and with Reset all changes (`instance_resizing_tests`). The row that listed them as not overridable was wrong. Numbers unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user