Integrate shared filter parameters with current main

This commit is contained in:
skylar taylor-barrick
2026-10-22 08:30:51 -06:00
parent 5fb854ec91
commit fcb987a8d1
11 changed files with 70 additions and 44 deletions
+2 -1
View File
@@ -318,7 +318,8 @@ pub(crate) fn run_filter(s: &mut Session, id: &str, p: &Value) -> Result<Value>
_ => params.clone(),
};
// Non-destructive: record the filter and re-render the smart object from its source.
let sf = SmartFilter { command: id.clone(), params: params.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
let sf =
SmartFilter { command: id.clone(), params: params.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
return crate::smart_cmds::add_smart_filter(doc, layer, sf, selection.as_ref()).map(|()| fp);
}
_ => return Err(EngineError::Other("not a pixel layer".into())),
+3 -2
View File
@@ -1120,9 +1120,10 @@ fn set_filter_params(s: &mut Session, p: &Value) -> Result<Value> {
if f.command == DISPLACE {
if let Some(map) = &new_map {
f.params = photocraft_io::smart_map::displace_embed_map(&f.params, map)
.ok_or_else(|| bad(CMD, "the displacement map is too large to keep in a Smart Object's filter"))?;
.ok_or_else(|| bad(CMD, "the displacement map is too large to keep in a Smart Object's filter"))?
.into();
} else if new_path {
f.params = photocraft_io::smart_map::displace_without_map(&f.params);
f.params = photocraft_io::smart_map::displace_without_map(&f.params).into();
}
}
// Authorize the underlying command with the complete merged params, including paths
+1 -1
View File
@@ -187,7 +187,7 @@ pub fn embed_map(params: &J, map: &Image) -> Option<J> {
let enumerated = |t: &str, v: &str| Value::Enumerated { type_id: Id::new(t), value: Id::new(v) };
// The settings here are placeholders: the params' own replace them when the filter renders
// and when it is written to PSD (`export_item`).
let neutral = SmartFilter { command: String::new(), params: J::Null, blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
let item = Descriptor::new("filterFX")
.with("Nm ", Value::Text(photocraft_psd::descriptor::UnicodeString::new_nul("Displace...")))
.with("blendOptions", blend_options(&neutral))
+9 -9
View File
@@ -149,7 +149,7 @@ fn range_entry(i: usize, o: &J) -> Result<Option<Value>, String> {
/// The `filterFXList` item for an `image.adjustments.hueSaturation` smart filter.
pub(super) fn export_item(f: &SmartFilter) -> Result<Descriptor, String> {
let p = &f.params;
let p = f.params.as_ref();
let obj = match p {
J::Object(o) => o,
J::Null => &Map::new(),
@@ -230,7 +230,7 @@ mod tests {
let c = |r: f64, g: f64, b: f64| {
Value::Descriptor(Descriptor::new("RGBC").with("Rd ", Value::Double(r)).with("Grn ", Value::Double(g)).with("Bl ", Value::Double(b)))
};
let neutral = SmartFilter { command: String::new(), params: J::Null, blend: BlendMode::Normal, opacity: 1.0, visible: true };
let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: BlendMode::Normal, opacity: 1.0, visible: true };
Descriptor::new("filterFX")
.with("Nm ", Value::Text(UnicodeString::new_nul("Farbton/Sättigung...")))
.with("blendOptions", blend_options(&neutral))
@@ -255,7 +255,7 @@ mod tests {
}
fn filter(params: J) -> SmartFilter {
SmartFilter { command: COMMAND.into(), params, blend: BlendMode::Normal, opacity: 1.0, visible: true }
SmartFilter { command: COMMAND.into(), params: params.into(), blend: BlendMode::Normal, opacity: 1.0, visible: true }
}
#[test]
@@ -268,13 +268,13 @@ mod tests {
let f = filter_from_item(&observed());
assert_eq!(f.command, COMMAND);
assert_eq!(
f.params,
*f.params,
json!({"colorize": false, "hue": 12, "saturation": -20, "lightness": 5,
"yellows": {"hue": -16, "saturation": 10, "lightness": -5, "range": [15, 45, 75, 105]}})
);
let c = filter_from_item(&item(fltr(true, vec![hst2(None, 200, 40, -10)])));
assert_eq!(c.command, COMMAND);
assert_eq!(c.params, json!({"colorize": true, "hue": 200, "saturation": 40, "lightness": -10}));
assert_eq!(*c.params, json!({"colorize": true, "hue": 200, "saturation": 40, "lightness": -10}));
}
#[test]
@@ -300,11 +300,11 @@ mod tests {
fn command_params_are_written_like_the_dialog() {
let export = |p: J| filter_from_item(&item_for_filter(&filter(p)).unwrap()).params;
// Partial params: the command's defaults, numbers rounded and clamped.
assert_eq!(export(json!({"hue": 12.4})), json!({"colorize": false, "hue": 12, "saturation": 0, "lightness": 0}));
assert_eq!(export(json!({"hue": -400, "lightness": 101, "layer": 3})), json!({"colorize": false, "hue": -180, "saturation": 0, "lightness": 100}));
assert_eq!(export(J::Null), json!({"colorize": false, "hue": 0, "saturation": 0, "lightness": 0}));
assert_eq!(*export(json!({"hue": 12.4})), json!({"colorize": false, "hue": 12, "saturation": 0, "lightness": 0}));
assert_eq!(*export(json!({"hue": -400, "lightness": 101, "layer": 3})), json!({"colorize": false, "hue": -180, "saturation": 0, "lightness": 100}));
assert_eq!(*export(J::Null), json!({"colorize": false, "hue": 0, "saturation": 0, "lightness": 0}));
// Colorize starts at 25 % saturation and wraps the hue into 0..360.
assert_eq!(export(json!({"colorize": true, "hue": -30})), json!({"colorize": true, "hue": 330, "saturation": 25, "lightness": 0}));
assert_eq!(*export(json!({"colorize": true, "hue": -30})), json!({"colorize": true, "hue": 330, "saturation": 25, "lightness": 0}));
// The full set (`to_params`): neutral ranges at their default extent are left out, reds
// are written within one turn.
let mut full = json!({"hue": 0, "saturation": 0, "lightness": 0, "colorize": false});
+7 -7
View File
@@ -108,7 +108,7 @@ pub(super) fn import_params(item: &Descriptor) -> Option<J> {
let planes = planes_of(f)?;
// Neutral blending in the template: the model's own fields replace it on export.
let mut template = item.clone();
let neutral = SmartFilter { command: String::new(), params: J::Null, blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
let neutral = SmartFilter { command: String::new(), params: J::Null.into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
set(&mut template, "blendOptions", blend_options(&neutral));
set(&mut template, "enab", Value::Boolean(true));
// Export refuses a larger template; such an item stays opaque, so it is still written back.
@@ -275,7 +275,7 @@ mod tests {
"blendOptions",
blend_options(&SmartFilter {
command: String::new(),
params: J::Null,
params: J::Null.into(),
blend: photocraft_color::BlendMode::Normal,
opacity: 1.0,
visible: true,
@@ -331,7 +331,7 @@ mod tests {
fn edited_planes_round_trip_and_keep_the_rest_of_the_template() {
let src = two_planes(708);
let mut f = filter_from_item(&src);
f.params["planes"][1]["dst"][2] = json!([155.5, 118.0]);
std::sync::Arc::make_mut(&mut f.params)["planes"][1]["dst"][2] = json!([155.5, 118.0]);
f.opacity = 0.5;
f.visible = false;
let item = item_for_filter(&f).unwrap();
@@ -351,7 +351,7 @@ mod tests {
let planes = vec![Plane { src: V, dst: W }];
let f = SmartFilter {
command: COMMAND.into(),
params: json!({"planes": planes}),
params: json!({"planes": planes}).into(),
blend: photocraft_color::BlendMode::Multiply,
opacity: 0.25,
visible: true,
@@ -372,7 +372,7 @@ mod tests {
// Edit › Perspective Warp on a smart object stores its own id; it is written too, with a
// straighten baked in.
let f = SmartFilter { command: EDIT_COMMAND.into(), params: json!({"planes": planes, "straighten": "auto", "interpolation": "bilinear"}), ..f };
let f = SmartFilter { command: EDIT_COMMAND.into(), params: json!({"planes": planes, "straighten": "auto", "interpolation": "bilinear"}).into(), ..f };
assert!(item_for_filter(&f).is_ok());
}
@@ -429,7 +429,7 @@ mod tests {
fn bad_exports_report_errors() {
let ok = filter_from_item(&one_plane()).params;
let with = |k: &str, v: J| {
let mut p = ok.clone();
let mut p = ok.as_ref().clone();
p[k] = v;
p
};
@@ -448,7 +448,7 @@ mod tests {
with(TEMPLATE_KEY, json!(to_hex(&Descriptor::new("filterFX").to_bytes()))),
with(TEMPLATE_KEY, json!("0".repeat(MAX_TEMPLATE_BYTES * 2 + 2))),
] {
let f = SmartFilter { command: COMMAND.into(), params: p.clone(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
let f = SmartFilter { command: COMMAND.into(), params: p.clone().into(), blend: photocraft_color::BlendMode::Normal, opacity: 1.0, visible: true };
assert!(export_item(&f).is_err(), "{p}");
}
}
+18 -18
View File
@@ -1806,7 +1806,7 @@ mod tests {
let item = photoshop_no_options_item("Weichzeichnen", Value::Integer(id));
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!(f.command, command);
assert_eq!(f.params, json!({}));
assert_eq!(*f.params, json!({}));
assert_eq!((f.visible, f.blend), (false, BlendMode::Multiply));
assert!((f.opacity - 0.6).abs() < 1e-6);
}
@@ -1869,7 +1869,7 @@ mod tests {
let item = ps_item("Smart Sharpen...", fltr.clone(), 1131);
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!(f.command, "filter.sharpen.smartSharpen");
assert_eq!(f.params, json!({"amount": 120.0, "radius": 2.5, "reduceNoise": 5.0}));
assert_eq!(*f.params, json!({"amount": 120.0, "radius": 2.5, "reduceNoise": 5.0}));
assert_eq!(written_fltr(&f), fltr);
let d = item_for_filter(&f).unwrap();
assert_eq!(int(&d, "filterID"), Some(1131));
@@ -1890,7 +1890,7 @@ mod tests {
let keys: Vec<&str> = f.params.as_object().unwrap().keys().map(String::as_str).collect();
assert_eq!(keys.len(), 3, "{keys:?}");
assert!(keys.iter().all(|k| ["amount", "radius", "reduceNoise"].contains(k)), "{keys:?}");
assert_eq!(f.params, json!({"amount": 200.0, "radius": 1.0, "reduceNoise": 10.0}));
assert_eq!(*f.params, json!({"amount": 200.0, "radius": 1.0, "reduceNoise": 10.0}));
assert_eq!(written_fltr(&f), smart_sharpen(200.0, 1.0, 10.0, "GsnB"));
}
@@ -1973,7 +1973,7 @@ mod tests {
let item = ps_item("Dust & Scratches...", fltr.clone(), 1_148_417_107);
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!(f.command, "filter.noise.dustAndScratches");
assert_eq!(f.params, json!({"radius": radius, "threshold": threshold}));
assert_eq!(*f.params, json!({"radius": radius, "threshold": threshold}));
assert_eq!(written_fltr(&f), fltr);
assert_eq!(int(&item_for_filter(&f).unwrap(), "filterID"), Some(1_148_417_107));
}
@@ -2040,7 +2040,7 @@ mod tests {
("filter.noise.dustAndScratches", json!([]), json!({"radius": 1, "threshold": 0})),
] {
let back = filter_from_item(&item_for_filter(&sf(command, p.clone())).unwrap());
assert_eq!((back.command.as_str(), &back.params), (command, &want), "{p}");
assert_eq!((back.command.as_str(), back.params.as_ref()), (command, &want), "{p}");
}
}
@@ -2162,7 +2162,7 @@ mod tests {
let item = measured_item(fltr, 51.0, 255.0);
// Through bytes, as read from a file.
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!((f.command.as_str(), &f.params), (command, &params));
assert_eq!((f.command.as_str(), f.params.as_ref()), (command, &params));
// Written back in the measured shape: same class, keys and their order, value types
// (long, bool, enum with its type), filterID; Tiles also its colours.
let back = item_for_filter(&f).unwrap();
@@ -2181,12 +2181,12 @@ mod tests {
assert_eq!(filter_from_item(&bare).command, UNSUPPORTED_FILTER);
// Only Tiles reads the entry's colours; Wind and Extrude write the defaults.
let f = filter_from_item(&measured_item(wind("Wnd ", "Left"), 51.0, 0.0));
assert_eq!(f.params, json!({"method": "wind", "direction": "fromRight"}));
assert_eq!(*f.params, json!({"method": "wind", "direction": "fromRight"}));
let back = item_for_filter(&f).unwrap();
assert_eq!((back.get("FrgC"), back.get("BckC")), (Some(&rgbc(0.0)), Some(&rgbc(255.0))));
// Tiles without colours in its params (or the entry) writes the engine's black and white.
let f = filter_from_item(&Descriptor::new("filterFX").with("Fltr", Value::Descriptor(tiles(long(4), long(5), "FlBc"))));
assert_eq!(f.params, json!({"count": 4, "maxOffset": 5, "fill": "background"}));
assert_eq!(*f.params, json!({"count": 4, "maxOffset": 5, "fill": "background"}));
let back = item_for_filter(&f).unwrap();
assert_eq!((back.get("FrgC"), back.get("BckC")), (Some(&rgbc(0.0)), Some(&rgbc(255.0))));
}
@@ -2339,7 +2339,7 @@ mod tests {
let item = measured_named_item(b"Ofst", "Offset...", offset_fltr(Value::Integer(h), Value::Integer(v), enumv("FlMd", fill)));
let f = filter_from_item(&item);
assert_eq!(f.command, "filter.other.offset");
assert_eq!(f.params, json!({"horizontal": h, "vertical": v, "undefinedAreas": ours}));
assert_eq!(*f.params, json!({"horizontal": h, "vertical": v, "undefinedAreas": ours}));
// Written back exactly as measured.
assert_eq!(item_for_filter(&f).unwrap(), item);
}
@@ -2409,7 +2409,7 @@ mod tests {
let item = measured_named_item(b"Cstm", "Custom...", custom_fltr(Value::Integer(scale), Value::Integer(offset), longs(&kernel)));
let f = filter_from_item(&item);
assert_eq!(f.command, "filter.other.custom");
assert_eq!(f.params, json!({"kernel": kernel, "scale": scale, "offset": offset}));
assert_eq!(*f.params, json!({"kernel": kernel, "scale": scale, "offset": offset}));
assert_eq!(item_for_filter(&f).unwrap(), item);
}
}
@@ -2488,7 +2488,7 @@ mod tests {
for (code, ty) in [("CrsD", "coarseDots"), ("LngS", "longStrokes")] {
let item = pixelate_item("Mezzotint...", Descriptor::new("Mztn").with("MztT", enumv("MztT", code)), 1_299_870_830);
let f = filter_from_item(&item);
assert_eq!((f.command.as_str(), &f.params), ("filter.pixelate.mezzotint", &json!({"type": ty})));
assert_eq!((f.command.as_str(), f.params.as_ref()), ("filter.pixelate.mezzotint", &json!({"type": ty})));
}
// No type or an unknown type is Fine Dots, like the engine; the seed
// (PhotoCraft only) is not saved.
@@ -2527,7 +2527,7 @@ mod tests {
let item = pixelate_item("Color Halftone...", measured.clone(), 1_131_180_616);
let f = filter_from_item(&item);
assert_eq!(f.command, "filter.pixelate.colorHalftone");
assert_eq!(f.params, json!({"maxRadius": 6, "channel1": 10, "channel2": 20, "channel3": 30, "channel4": 40}));
assert_eq!(*f.params, json!({"maxRadius": 6, "channel1": 10, "channel2": 20, "channel3": 30, "channel4": 40}));
assert_eq!(get_desc(&item_for_filter(&f).unwrap(), "Fltr"), Some(&measured));
// Defaults and hostile values write longs within the dialog's ranges.
let d = item_for_filter(&sf("filter.pixelate.colorHalftone", json!({"maxRadius": f64::NAN, "channel1": 1e12, "channel2": -1e12, "channel3": "x"})))
@@ -2664,7 +2664,7 @@ mod tests {
for (fltr, id, command, params) in cases {
let item = ps_item("x", fltr.clone(), id);
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!((f.command.as_str(), &f.params), (command, &params), "{fltr:?}");
assert_eq!((f.command.as_str(), f.params.as_ref()), (command, &params), "{fltr:?}");
let out = item_for_filter(&f).unwrap();
assert_eq!(out.get("filterID"), Some(&Value::Integer(id)));
assert_eq!(out.get("hasoptions"), Some(&Value::Boolean(true)));
@@ -2839,7 +2839,7 @@ mod tests {
let item = photoshop_item(id, fltr);
// Through bytes, as read from a file.
let f = filter_from_item(&Descriptor::from_bytes(&item.to_bytes()).unwrap());
assert_eq!((f.command.as_str(), &f.params), (command, &params));
assert_eq!((f.command.as_str(), f.params.as_ref()), (command, &params));
// Written back in the measured shape: same class, keys, value types and filterID.
let back = item_for_filter(&f).unwrap();
for key in ["Fltr", "filterID", "BckC"] {
@@ -2858,7 +2858,7 @@ mod tests {
assert_eq!(item_for_filter(&f).unwrap().get("BckC"), Some(&rgbc(51.0)));
let mut item = photoshop_item(b"Crst", Descriptor::new("Crst").with("ClSz", Value::Integer(7)));
set(&mut item, "BckC", rgbc(51.0));
assert_eq!(filter_from_item(&item).params, json!({"cellSize": 7}));
assert_eq!(*filter_from_item(&item).params, json!({"cellSize": 7}));
// Our random seed has no PSD key: it is not written.
let fltr = |c: &str, p: J| item_for_filter(&sf(c, p)).unwrap().get("Fltr").cloned();
assert_eq!(
@@ -3551,7 +3551,7 @@ mod tests {
for (hex, command, params) in cases {
let item = photoshop(hex);
let f = filter_from_item(&item);
assert_eq!((f.command.as_str(), &f.params), (command, &params));
assert_eq!((f.command.as_str(), f.params.as_ref()), (command, &params));
// Written back, it is Photoshop's item again: name, colours, settings and key order.
assert_eq!(item_for_filter(&f).unwrap().items, item.items, "{command}");
}
@@ -3638,12 +3638,12 @@ mod tests {
fn render_filters_made_in_photocraft_are_written() {
// Without colours (an older document or a script), Photoshop's default black and white.
let back = filter_from_item(&item_for_filter(&sf("filter.render.clouds", json!({"seed": 7}))).unwrap());
assert_eq!(back.params, json!({"seed": 7, "foreground": [0.0, 0.0, 0.0, 1.0], "background": [1.0, 1.0, 1.0, 1.0]}));
assert_eq!(*back.params, json!({"seed": 7, "foreground": [0.0, 0.0, 0.0, 1.0], "background": [1.0, 1.0, 1.0, 1.0]}));
// A seed above i32::MAX keeps its bit pattern, and colours may be hex.
let p = json!({"variance": 30, "strength": 10, "seed": 4_000_000_000u32, "foreground": "#ff0000", "background": [0.0, 0.0, 1.0, 1.0]});
let back = filter_from_item(&item_for_filter(&sf("filter.render.fibers", p)).unwrap());
assert_eq!(
back.params,
*back.params,
json!({"variance": 30, "strength": 10, "seed": 4_000_000_000u32, "foreground": [1.0, 0.0, 0.0, 1.0], "background": [0.0, 0.0, 1.0, 1.0]})
);
let flare = sf("filter.render.lensFlare", json!({"brightness": 120, "centerX": 0.25, "centerY": 0.75, "lens": "moviePrime"}));
+3 -3
View File
@@ -387,10 +387,10 @@ impl History {
self.held_states().fold(0, |n, s| n.saturating_add(tile_bytes(&s.doc, &mut seen)))
}
/// Keep pixel memory within [`History::max_bytes`]: the current document's tiles plus the
/// tiles only history holds (newest states first). The oldest undo states that don't fit are
/// Keep retained tile/filter-parameter memory within [`History::max_bytes`]: the current
/// document plus allocations only history holds (newest states first). The oldest undo states that don't fit are
/// dropped; the most recent one is always kept so the last step can be undone. The History
/// Brush's source state ([`History::set_brush_source`]) is never dropped: its unique tiles are
/// Brush's source state ([`History::set_brush_source`]) is never dropped: its unique allocations are
/// counted first, and the newest states fill what remains. Returns how many states were
/// dropped.
pub fn trim(&mut self, current: &Document) -> usize {
+24
View File
@@ -122,3 +122,27 @@ fn aliased_filters_and_snapshots_are_counted_once() {
let empty = Document::new("empty", Size::new(8, 8), ColorMode::Rgb, SampleType::U8);
assert_eq!(history.extra_bytes(&empty, &[&pinned, &pinned]), expected);
}
#[test]
fn cleared_history_retains_filter_payloads_until_the_next_edit() {
let mut current = Arc::new(document());
let mut history = History::new(20);
edit(&mut history, &mut current, |d| d.name = "renamed".into());
edit(&mut history, &mut current, |d| {
let LayerContent::Smart(smart) = &mut d.layers[0].content else { panic!("fixture") };
smart.smart_filters[0].params = json!({"__displacePsd": "cd".repeat(4096)}).into();
});
let retained = history.pixel_bytes(&current);
let unique = history.unique_bytes(&current);
assert!(unique >= 16384, "old filter payload is retained by undo");
assert!(history.clear_states());
assert_eq!(history.pixel_bytes(&current), retained);
assert_eq!(history.unique_bytes(&current), unique, "undoable Clear History still owns the old payload");
let (restored, _) = history.undo(current.clone()).unwrap();
current = restored;
assert_eq!(history.pixel_bytes(&current), retained);
assert!(history.clear_states());
edit(&mut history, &mut current, |d| d.name = "after clear".into());
assert_eq!(history.unique_bytes(&current), 0);
assert_eq!(history.pixel_bytes(&current), History::default().pixel_bytes(&current));
}
+1 -1
View File
@@ -1111,7 +1111,7 @@ pub(crate) fn preview_document_for_target(
mask.feather /= run_k as f32;
}
for f in &mut sm.smart_filters {
scale_preview_params(&f.command, &mut f.params, run_k);
scale_preview_params(&f.command, std::sync::Arc::make_mut(&mut f.params), run_k);
}
if command == "layer.smartFilter.setParams" {
let f = sm.smart_filters.get(index)?;
+1 -1
View File
@@ -391,7 +391,7 @@ pub fn open_editor(app: &mut PhotocraftApp, ctx: &egui::Context, layer: u64, sm:
};
if let Some(d) = app.ui.dialog_mut(id) {
d.fields.insert("__smartFilter".into(), target);
if let Value::Object(p) = &f.params {
if let Value::Object(p) = f.params.as_ref() {
for (k, v) in p {
if !k.starts_with("__") {
d.fields.insert(k.clone(), v.clone());
+1 -1
View File
@@ -172,7 +172,7 @@ fn filter_editor_starts_missing_parameters_from_defaults_not_remembered_values()
let ctx = egui::Context::default();
let mut sm = smart(&app, layer).clone();
let index = sm.smart_filters.iter().position(|f| f.command == command).unwrap();
sm.smart_filters[index].params = json!({"amount": 125, "radius": 2});
sm.smart_filters[index].params = json!({"amount": 125, "radius": 2}).into();
super::open_editor(&mut app, &ctx, layer.0, &sm, index);
let fields = &app.ui.dialogs.last().unwrap().fields;