//! Essential Graphics Font and uniform Scale controls, Composition ▸ Open in Essential Graphics. use effectcraft_keyframe::Value as KV; use effectcraft_project::essential::{self, ControlType}; use effectcraft_time::Tick; use serde_json::json; use crate::{Event, Session}; #[test] fn font_and_scale_controls_override_per_instance() { let mut s = Session::default(); s.execute("comp.new", json!({"name": "Title", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap(); let title = s.active_comp_id().unwrap(); let text = s.execute("layer.newText", json!({"text": "Hello"})).unwrap()["layer"].as_u64().unwrap(); // Source Text: as text and as font; Scale as one slider. let t = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText"})).unwrap()["controls"][0].as_u64().unwrap(); let f = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap()["controls"][0].as_u64().unwrap(); let sc = s.execute("essential.addProperty", json!({"layer": text, "path": "transform/scale", "as": "scale"})).unwrap()["controls"][0].as_u64().unwrap(); // Adding the same font control again adds nothing with `mirror: false` (else a mirror of // it); opacity can't be a font. assert!( s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font", "mirror": false})).unwrap()["controls"] .as_array() .unwrap() .is_empty() ); let m = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap(); assert_eq!(m["mirrors"].as_array().unwrap().len(), 1); let ml = s.execute("essential.list", json!({})).unwrap(); let mirror = ml["controls"].as_array().unwrap().iter().find(|c| c["kind"] == "mirror").unwrap().clone(); assert_eq!((mirror["of"].as_u64(), mirror["type"].as_str()), (Some(f), Some("font"))); s.execute("edit.undo", json!({})).unwrap(); assert!(s.execute("essential.addProperty", json!({"layer": text, "path": "transform/opacity", "as": "font"})).is_err()); assert_eq!(s.execute("essential.canAdd", json!({"layer": text, "path": "transform/opacity", "as": "font"})).unwrap()["ok"], false); assert_eq!(s.execute("essential.canAdd", json!({"layer": text, "path": "transform/scale", "as": "scale"})).unwrap()["type"], "scale"); let l = s.execute("essential.list", json!({})).unwrap(); let types: Vec<&str> = l["controls"].as_array().unwrap().iter().map(|c| c["type"].as_str().unwrap()).collect(); assert_eq!(types, ["text", "font", "scale"]); assert!(l["controls"][1]["value"]["size"].as_f64().unwrap() > 0.0); let eg = s.project.comp(title).unwrap().essential.clone().unwrap(); assert_eq!(eg.find(f).unwrap().as_type, Some(ControlType::Font)); // Survives save / load (serde). let j = serde_json::to_value(&eg).unwrap(); assert_eq!(serde_json::from_value::(j).unwrap(), eg); // An instance: change the text, then the font and the scale. s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap(); let inst = s.execute("layer.addItem", json!({"item": title.0})).unwrap()["layer"].as_u64().unwrap(); s.execute("layer.select", json!({"layers": [inst]})).unwrap(); s.execute("essential.set", json!({"layer": inst, "control": t, "value": "Bye"})).unwrap(); s.execute("essential.set", json!({"layer": inst, "control": f, "value": {"font": "Serif Test", "size": 40}})).unwrap(); s.execute("essential.set", json!({"layer": inst, "control": sc, "value": 50})).unwrap(); let i = s.execute("essential.instance", json!({"layer": inst})).unwrap(); assert_eq!(i["controls"][1]["value"]["font"], "Serif Test"); assert_eq!(i["controls"][1]["value"]["size"], 40.0); assert_eq!(i["controls"][1]["overridden"], true); assert_eq!(i["controls"][2]["type"], "scale"); assert_eq!(i["controls"][2]["value"][0], 50.0); assert_eq!(i["controls"][2]["value"][1], 50.0); // What the instance renders: the overridden text with the overridden font, scaled. let layer = s.active_comp().unwrap().layer(effectcraft_project::LayerId(inst)).unwrap().clone(); let over: Vec = { let g = essential::group(&layer).unwrap(); let mut v = vec![]; g.walk("", &mut |_, p| { if let Some(c) = essential::control_of(&p.match_id) { v.push(essential::Override { control: c, value: p.value.clone() }); } }); v }; let p = essential::with_overrides(&s.project, title, &over).unwrap(); let src = p.comp(title).unwrap().layer(effectcraft_project::LayerId(text)).unwrap(); match src.props.prop("text/sourceText").map(|x| &x.value) { Some(KV::Text(d)) => assert_eq!((d.text.as_str(), d.font.as_str(), d.size), ("Bye", "Serif Test", 40.0)), o => panic!("{o:?}"), } // Push to Comp with only the font: the source keeps its own text. s.execute("essential.revert", json!({"layer": inst, "control": t})).unwrap(); s.execute("essential.pushToComp", json!({"layer": inst, "control": f})).unwrap(); let srcl = s.project.comp(title).unwrap().layer(effectcraft_project::LayerId(text)).unwrap(); match srcl.props.prop("text/sourceText").map(|x| &x.value) { Some(KV::Text(d)) => assert_eq!((d.text.as_str(), d.font.as_str()), ("Hello", "Serif Test")), o => panic!("{o:?}"), } } #[test] fn open_in_essential_graphics_sets_the_primary_comp() { let mut s = Session::default(); s.execute("comp.new", json!({"name": "A", "width": 32, "height": 32, "duration": 1})).unwrap(); let a = s.active_comp_id().unwrap(); s.execute("comp.new", json!({"name": "B", "width": 32, "height": 32, "duration": 1})).unwrap(); s.drain_events(); let r = s.execute("comp.openInEssentialGraphics", json!({"comp": a.0})).unwrap(); assert_eq!(r["name"], "A"); assert_eq!(s.state.essential_primary, Some(a)); assert!( s.drain_events() .iter() .any(|e| matches!(e, Event::Frontend { command, params } if command == "window.panel" && params["panel"] == "essentialGraphics")) ); assert_eq!(s.execute("essential.list", json!({})).unwrap()["name"], "A"); } /// #1747: through a collapsed precomp, a Text override and a Font override of the same Source /// Text both apply: the Font merges into the overridden text, as `essential::with_overrides` /// does on the flattened path, instead of one entry replacing the other. #[test] fn collapsed_instance_merges_text_and_font_overrides() { let mut s = Session::default(); s.execute("comp.new", json!({"name": "Title", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap(); let title = s.active_comp_id().unwrap(); let text = s.execute("layer.newText", json!({"text": "Hello"})).unwrap()["layer"].as_u64().unwrap(); let body = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText"})).unwrap()["controls"][0].as_u64().unwrap(); let font = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap()["controls"][0].as_u64().unwrap(); s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 1})).unwrap(); let main = s.active_comp_id().unwrap(); let inst = s.execute("layer.addItem", json!({"item": title.0})).unwrap()["layer"].as_u64().unwrap(); s.execute("layer.setSwitch", json!({"layers": [inst], "switch": "collapse", "value": true})).unwrap(); s.execute("essential.set", json!({"layer": inst, "control": body, "value": "Bye"})).unwrap(); s.execute("essential.set", json!({"layer": inst, "control": font, "value": {"font": "Serif Test", "size": 40}})).unwrap(); // Evaluate the nested text in the collapsed instance's context, as the renderer does. let r = effectcraft_render::Renderer::new(&s.project, &*s.footage, crate::render::RenderOpts::default()); let ctx = effectcraft_render::EvalCtx::new(&s.project, main, s.project.comp(main).unwrap(), Tick::ZERO); let layer = ctx.comp.layer(effectcraft_project::LayerId(inst)).unwrap(); let collapsed = r.collapsed(&ctx, layer).expect("the instance collapses"); let (_, nctx) = r.collapse_into(&ctx, layer, &collapsed, 1.0).expect("a nested context"); let src = nctx.comp.layer(effectcraft_project::LayerId(text)).unwrap(); match nctx.value(src, src.props.prop("text/sourceText").unwrap()) { KV::Text(d) => assert_eq!((d.text.as_str(), d.font.as_str(), d.size), ("Bye", "Serif Test", 40.0)), o => panic!("{o:?}"), } }