mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 03:27:14 -06:00
Merge PR #2109
This commit is contained in:
@@ -29,6 +29,10 @@ pub struct Filter {
|
||||
/// Only these photos (Find Similar results…); empty = no constraint.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub only: Vec<PhotoId>,
|
||||
/// Show nothing: an explicit empty result (Find Similar with no matches), since an empty
|
||||
/// `only` means "no constraint".
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub only_none: bool,
|
||||
/// Any of these labels (the filter bar's multi-select); empty = no constraint.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub labels: Vec<ColorLabel>,
|
||||
@@ -374,6 +378,9 @@ impl Filter {
|
||||
if !self.labels.is_empty() && !p.label.is_some_and(|l| self.labels.contains(&l)) {
|
||||
return false;
|
||||
}
|
||||
if self.only_none {
|
||||
return false;
|
||||
}
|
||||
if !self.only.is_empty() && !prepared.only.as_ref().map_or_else(|| self.only.contains(&p.id), |only| only.contains(&p.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -109,13 +109,15 @@ fn find_similar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = sig(s, q) {
|
||||
let sim = lightcraft_pipeline::cull::similarity(&me, &v);
|
||||
if sim >= min {
|
||||
hits.push((q, sim));
|
||||
hits.push((q, sim.clamp(0.0, 1.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.sort_by(|a, b| b.1.total_cmp(&a.1));
|
||||
if p.get("filter").and_then(Value::as_bool).unwrap_or(true) {
|
||||
s.filter.only = hits.iter().map(|h| h.0).collect();
|
||||
// an empty `only` means "no constraint": no hits is its own state that shows no photos
|
||||
s.filter.only_none = s.filter.only.is_empty();
|
||||
}
|
||||
Ok(json!({"photos": hits.iter().map(|(i, v)| json!({"id": i.0, "similarity": (v * 1000.0).round() / 1000.0})).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
@@ -1250,6 +1250,43 @@ fn find_similar_filters_to_look_alikes() {
|
||||
assert!(s.visible_cloned().len() > vis.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_similar_with_no_hits_shows_no_photos() {
|
||||
let mut s = demo();
|
||||
let r = s.execute("library.findSimilar", &json!({"similarity": 1.5})).unwrap();
|
||||
assert_eq!(r["photos"].as_array().map(Vec::len), Some(0));
|
||||
assert_eq!(s.visible_cloned().len(), 0, "an empty Find Similar result must show no photos, not the whole library");
|
||||
s.execute("library.clearFilter", &json!({})).unwrap();
|
||||
assert_eq!(s.visible_cloned().len(), s.catalog.len(), "Clear Filters brings everything back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_similar_of_a_flat_photo_shows_an_empty_view_and_counts_zero() {
|
||||
let mut s = demo();
|
||||
let photo = s.catalog.photos().next().unwrap().clone();
|
||||
// a flat photo's signature is all zeros: it is not similar even to itself
|
||||
s.signatures.insert(crate::media::content_key(&photo), [0.0; 64]);
|
||||
let r = s.execute("library.findSimilar", &json!({"id": photo.id.0})).unwrap();
|
||||
assert_eq!(r["photos"].as_array().map(Vec::len), Some(0));
|
||||
assert_eq!(s.visible_cloned().len(), 0, "no matches show nothing, not the whole library");
|
||||
assert_eq!(s.catalog.count(&s.filter), 0);
|
||||
let chips = crate::filter_chips(&s.filter, &s.catalog);
|
||||
assert_eq!(chips.iter().map(|c| c.label.as_str()).collect::<Vec<_>>(), ["Only 0 photos"]);
|
||||
s.execute("library.filter", &chips[0].clear).unwrap();
|
||||
assert_eq!(s.filter, Default::default(), "the chip clears the empty state");
|
||||
assert_eq!(s.visible_cloned().len(), s.catalog.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_similar_values_stay_within_zero_and_one() {
|
||||
let mut s = demo();
|
||||
let id = s.catalog.photos().next().unwrap().id;
|
||||
let r = s.execute("library.findSimilar", &json!({"id": id.0, "similarity": -1.0})).unwrap();
|
||||
let sims: Vec<f64> = r["photos"].as_array().unwrap().iter().map(|h| h["similarity"].as_f64().unwrap()).collect();
|
||||
assert_eq!(sims.len(), s.catalog.len());
|
||||
assert!(sims.iter().all(|v| (0.0..=1.0).contains(v)), "{sims:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_develop_adds_to_each_photo() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
|
||||
@@ -214,8 +214,9 @@ pub fn filter_chips(f: &Filter, cat: &Catalog) -> Vec<FilterChip> {
|
||||
if let Some(rs) = f.rule_set.as_ref().filter(|r| !r.rules.is_empty()) {
|
||||
add(format!("Rules: {}", rs.describe_with(cat)), json!({"ruleSet": Null}));
|
||||
}
|
||||
if !f.only.is_empty() {
|
||||
add(format!("Only {} photos", f.only.len()), json!({"only": []}));
|
||||
if f.only_none || !f.only.is_empty() {
|
||||
let n = if f.only_none { 0 } else { f.only.len() };
|
||||
add(format!("Only {n} photos"), json!({"only": [], "onlyNone": false}));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn display_label(chip: &FilterChip, filter: &lightcraft_catalog::Filt
|
||||
};
|
||||
}
|
||||
if chip.clear.get("only").is_some() {
|
||||
return format!("{} {}", tr("Only"), crate::i18n::tr_format!("{n} photos", n = filter.only.len()));
|
||||
return format!("{} {}", tr("Only"), crate::i18n::tr_format!("{n} photos", n = if filter.only_none { 0 } else { filter.only.len() }));
|
||||
}
|
||||
if let Some((prefix, value)) = chip.label.split_once(": ") {
|
||||
let value = if matches!(prefix, "Flag" | "Type") {
|
||||
|
||||
Reference in New Issue
Block a user