mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 03:01:11 +00:00
Merge main into #2324
This commit is contained in:
@@ -383,3 +383,41 @@ fn the_caps_refuse_what_is_too_far_and_cut_what_is_too_long() {
|
||||
assert_eq!(text_data(&s, id(&out, "id")).characters.chars().count(), 500);
|
||||
assert_eq!(text_data(&s, id(&out["children"][0], "id")).characters.chars().count(), 500);
|
||||
}
|
||||
|
||||
/// Where a node's own origin (the place `x`/`y` name) is on the page.
|
||||
fn origin(s: &Session, n: NodeId) -> Point {
|
||||
s.doc().unwrap().world_transform(n) * Point::new(0.0, 0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mind_map_in_a_section_without_a_place_is_centred_on_the_view() {
|
||||
let mut s = board();
|
||||
let (vx, vy) = {
|
||||
let v = &s.ds().unwrap().view;
|
||||
(v.cx, v.cy)
|
||||
};
|
||||
let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 1000, "y": 500, "width": 4000, "height": 4000})).unwrap(), "id");
|
||||
let out = s.execute("board.mindMap", &json!({"parent": sec})).unwrap();
|
||||
let c = origin(&s, id(&out, "id"));
|
||||
assert!(same(c.x, vx) && same(c.y, vy), "the root's place {c:?} is the view's centre ({vx}, {vy})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mind_map_in_a_turned_frame_without_a_place_is_centred_on_the_view_and_a_given_place_is_the_frames() {
|
||||
let mut s = board();
|
||||
let (vx, vy) = {
|
||||
let v = &s.ds().unwrap().view;
|
||||
(v.cx, v.cy)
|
||||
};
|
||||
let fr = id(&s.execute("node.create", &json!({"type": "frame", "x": 300, "y": 200, "width": 2000, "height": 2000})).unwrap(), "id");
|
||||
s.execute("node.set", &json!({"id": fr, "props": {"rotation": 30}})).unwrap();
|
||||
let out = s.execute("board.mindMap", &json!({"parent": fr})).unwrap();
|
||||
let c = origin(&s, id(&out, "id"));
|
||||
assert!(same(c.x, vx) && same(c.y, vy), "the root's place {c:?} is the view's centre ({vx}, {vy})");
|
||||
|
||||
// A given place is the frame's own, as on the page: (10, 20) of the frame, carried through it.
|
||||
let given = id(&s.execute("board.mindMap", &json!({"parent": fr, "x": 10, "y": 20})).unwrap(), "id");
|
||||
let want = s.doc().unwrap().world_transform(fr) * Point::new(10.0, 20.0);
|
||||
let got = origin(&s, given);
|
||||
assert!(same(got.x, want.x) && same(got.y, want.y), "{got:?} against {want:?}");
|
||||
}
|
||||
|
||||
@@ -168,9 +168,13 @@ pub(super) fn mind_map_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "board.mindMap";
|
||||
require(s, cmd)?;
|
||||
let parent = parent_of(cmd, s, p)?;
|
||||
// The view's centre is on the page, and `x`/`y` are in the parent: a map in a section or frame
|
||||
// takes the centre in that parent's coordinates (a parent that can't be inverted gives a
|
||||
// non-finite centre, which `coord` refuses).
|
||||
let (vx, vy) = {
|
||||
let v = &s.ds()?.view;
|
||||
(v.cx, v.cy)
|
||||
let ds = s.ds()?;
|
||||
let c = ds.doc.world_transform(parent).inverse() * figcraft_geom::Point::new(ds.view.cx, ds.view.cy);
|
||||
(c.x, c.y)
|
||||
};
|
||||
let (x, y) = (coord(cmd, p, "x", vx)?, coord(cmd, p, "y", vy)?);
|
||||
let texts = starters(cmd, p)?;
|
||||
|
||||
@@ -142,7 +142,8 @@ message StyleId GUID guid=1
|
||||
message VariableID GUID guid=1
|
||||
message VariableSetID GUID guid=1
|
||||
message ColorStopVar Color color=1, VariableData colorVar=2, float position=3
|
||||
message Paint PaintType type=1, Color color=2, float opacity=3, bool visible=4, BlendMode blendMode=5, ColorStop[] stops=6, Matrix transform=7, Image image=8, ImageScaleMode imageScaleMode=10, float rotation=11, float scale=12, VariableData colorVar=21, ColorStopVar[] stopsVar=23
|
||||
message PaintFilterMessage float tint=1, float shadows=2, float highlights=3, float exposure=5, float temperature=7, float vibrance=8, float contrast=9
|
||||
message Paint PaintType type=1, Color color=2, float opacity=3, bool visible=4, BlendMode blendMode=5, ColorStop[] stops=6, Matrix transform=7, Image image=8, ImageScaleMode imageScaleMode=10, float rotation=11, float scale=12, PaintFilterMessage paintFilter=14, VariableData colorVar=21, ColorStopVar[] stopsVar=23
|
||||
message Effect EffectType type=1, Color color=2, Vector offset=3, float radius=4, bool visible=5, BlendMode blendMode=6, float spread=7, bool showShadowBehindNode=8, BlurOpType blurOpType=19, float startRadius=21, Vector startOffset=20, Vector endOffset=28, NoiseType noiseType=26, Vector noiseSize=22, float density=25, float opacity=27, Color secondaryColor=24, bool clipToShape=23, VariableData radiusVar=9, VariableData colorVar=10, VariableData spreadVar=11, VariableData xVar=12, VariableData yVar=13, VariableData startRadiusVar=45
|
||||
struct FontName string family, string style, string postscript
|
||||
struct Number float value, NumberUnits units
|
||||
|
||||
@@ -76,8 +76,21 @@ impl<'d> Exporter<'d> {
|
||||
if let (ImageScaleMode::Crop, Some(c)) = (img.scale_mode, img.crop) {
|
||||
f.push(("transform", matrix(Affine::new(c))));
|
||||
}
|
||||
if img.filters != Default::default() {
|
||||
self.warn("image adjustments (exposure, contrast, …) were left out");
|
||||
let x = img.filters.sanitized();
|
||||
if !x.is_identity() {
|
||||
// The file's tint is positive towards magenta, ours towards green; `0.0 -` keeps zero positive.
|
||||
f.push((
|
||||
"paintFilter",
|
||||
o(vec![
|
||||
("exposure", fl(f64::from(x.exposure))),
|
||||
("contrast", fl(f64::from(x.contrast))),
|
||||
("vibrance", fl(f64::from(x.saturation))),
|
||||
("temperature", fl(f64::from(x.temperature))),
|
||||
("tint", fl(0.0 - f64::from(x.tint))),
|
||||
("highlights", fl(f64::from(x.highlights))),
|
||||
("shadows", fl(f64::from(x.shadows))),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
PaintKind::Pattern(_) => {
|
||||
|
||||
@@ -16,8 +16,8 @@ fn image_filter_schema() -> Schema {
|
||||
Schema::parse(&text).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
/// A file with one image rectangle per filter case (current container, legacy, both, neutral, unknown fields).
|
||||
fn image_filter_fixture() -> Vec<u8> {
|
||||
let filter = |fields: Vec<(&'static str, Kv<'static>)>| o(fields);
|
||||
let image_paint = |id: u64, name: &'static str, mut extra: Vec<(&'static str, Kv<'static>)>| {
|
||||
let mut paint = vec![("type", e("IMAGE")), ("image", o(vec![("hash", Kv::Bytes(HASH.to_vec()))]))];
|
||||
@@ -54,7 +54,12 @@ fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
vec![("paintFilter", filter(vec![("brightness", f(0.4)), ("detail", f(-0.2)), ("vignette", f(0.3)), ("mystery", f(0.1))]))],
|
||||
),
|
||||
];
|
||||
let bytes = zip_file(&canvas(&image_filter_schema(), &o(vec![("nodeChanges", arr(nodes))])));
|
||||
zip_file(&canvas(&image_filter_schema(), &o(vec![("nodeChanges", arr(nodes))])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_filters_import_from_current_container_and_skip_ambiguous_data() {
|
||||
let bytes = image_filter_fixture();
|
||||
let (doc, warnings) = import(&bytes, "ImageFilters.fig").unwrap();
|
||||
let get = |name: &str| {
|
||||
let node = doc.nodes().find(|node| node.name == name).unwrap();
|
||||
@@ -102,3 +107,76 @@ fn warnings_naming_unknown_filter_fields_are_capped() {
|
||||
let named = warnings.iter().filter(|w| w.contains("unsupported image filter")).count();
|
||||
assert!((100..=crate::rest::MAX_WARNINGS).contains(&named), "{named} of {} warnings", warnings.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_adjustments_survive_a_fig_export() {
|
||||
let (mut doc, _) = import(&image_filter_fixture(), "ImageFilters.fig").unwrap();
|
||||
let filters_of = |doc: &figcraft_doc::Document, name: &str| {
|
||||
let node = doc.nodes().find(|node| node.name == name).unwrap();
|
||||
let figcraft_doc::color::PaintKind::Image(image) = &node.fills[0].kind else { panic!("{name} is not an image fill") };
|
||||
image.filters
|
||||
};
|
||||
// Values a document can hold and the file can't: out of range, and not a number.
|
||||
let ids: Vec<figcraft_doc::NodeId> = doc.ids().collect();
|
||||
for id in ids {
|
||||
let Some(node) = doc.node_mut(id) else { continue };
|
||||
let name = node.name.clone();
|
||||
let Some(figcraft_doc::color::PaintKind::Image(image)) = node.fills.first_mut().map(|p| &mut p.kind) else { continue };
|
||||
match name.as_str() {
|
||||
"Legacy" => image.filters.exposure = 5.0,
|
||||
"Explicit neutral" => image.filters.contrast = f32::NAN,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (bytes, warnings) = export::export(&doc, None).unwrap();
|
||||
assert!(!warnings.iter().any(|w| w.contains("image adjustments")), "{warnings:?}");
|
||||
let (back, _) = import(&bytes, "Back.fig").unwrap();
|
||||
for name in ["Current", "Negative tint", "Neutral"] {
|
||||
let (a, b) = (filters_of(&doc, name), filters_of(&back, name));
|
||||
for (x, y) in [
|
||||
(a.exposure, b.exposure),
|
||||
(a.contrast, b.contrast),
|
||||
(a.saturation, b.saturation),
|
||||
(a.temperature, b.temperature),
|
||||
(a.tint, b.tint),
|
||||
(a.highlights, b.highlights),
|
||||
(a.shadows, b.shadows),
|
||||
] {
|
||||
assert!((x - y).abs() < 1e-6, "{name}: {a:?} came back as {b:?}");
|
||||
}
|
||||
}
|
||||
assert!(filters_of(&back, "Neutral").is_identity());
|
||||
assert!(!filters_of(&back, "Current").is_identity());
|
||||
|
||||
// What is written: the file's names and signs.
|
||||
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 fill_of = |name: &str| {
|
||||
let nc = msg.get("nodeChanges").unwrap().list().iter().find(|nc| nc.get("name").and_then(Kv::str) == Some(name)).unwrap();
|
||||
nc.get("fillPaints").unwrap().list()[0].clone()
|
||||
};
|
||||
let current = fill_of("Current");
|
||||
let written = |paint: &Kv, field: &str| paint.get("paintFilter").and_then(|f| f.get(field)).and_then(Kv::f64);
|
||||
assert!((written(¤t, "vibrance").unwrap() + 0.4).abs() < 1e-6);
|
||||
assert!((written(¤t, "tint").unwrap() - 1.0).abs() < 1e-6);
|
||||
assert!((written(¤t, "shadows").unwrap() - 0.7).abs() < 1e-6);
|
||||
assert!(fill_of("Neutral").get("paintFilter").is_none());
|
||||
assert!((written(&fill_of("Legacy"), "exposure").unwrap() - 1.0).abs() < 1e-6, "written within the file's range");
|
||||
assert!(fill_of("Explicit neutral").get("paintFilter").is_none(), "not a number is no adjustment");
|
||||
let partly = fill_of("Negative tint");
|
||||
for field in ["exposure", "contrast", "vibrance", "temperature", "tint", "highlights", "shadows"] {
|
||||
assert!(written(&partly, field).is_some(), "{field} is written");
|
||||
}
|
||||
assert!((written(&partly, "tint").unwrap() + 1.0).abs() < 1e-6);
|
||||
|
||||
// The numbers the format uses.
|
||||
let s = Schema::parse(export::SCHEMA).unwrap();
|
||||
let def = |n: &str| s.defs.iter().find(|d| d.name == n).unwrap();
|
||||
let id = |d: &str, f: &str| def(d).fields.iter().find(|x| x.name == f).unwrap().value;
|
||||
assert_eq!(id("Paint", "paintFilter"), 14);
|
||||
assert_eq!(
|
||||
["tint", "shadows", "highlights", "exposure", "temperature", "vibrance", "contrast"].map(|f| id("PaintFilterMessage", f)),
|
||||
[1, 2, 3, 5, 7, 8, 9]
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -498,7 +498,7 @@ one undo step and returns the ids it made:
|
||||
in the app does both).
|
||||
- `board.mindMap {x?, y?, text?, children?, parent?, edit?}` → `{id, text, children: [{id, text, connector}]}`:
|
||||
a mind map: a first node (`mindNode`, bold 24, no box; `text` defaults to `Topic`, `x`/`y` to the
|
||||
middle of the view) with up to 20 `children` (texts; default `Idea 1`, `Idea 2`, `Idea 3`)
|
||||
view's centre, in the parent's coordinates) with up to 20 `children` (texts; default `Idea 1`, `Idea 2`, `Idea 3`)
|
||||
on its right, each joined to it by a curved branch (a connector with `branch: true`: stroke 4,
|
||||
`#757575`, round caps). The map lays itself out: children stand in one column 88 units right of
|
||||
the widest node of their parent's column, 48 apart, centred on their parent; a node is its text,
|
||||
|
||||
Reference in New Issue
Block a user