From 578528f4688e0fed725cadf3b3ad68624dbdc3f7 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Thu, 1 Feb 2024 03:14:00 -0800 Subject: [PATCH] Better handling for items in different zones with the same name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specifically, I was looking at the new "Stormy Cloud Kacheek" items, and was surprised to find that, in the outfit editor, they all get grouped under "Markings" (and therefore the UI treats them as mutually-exclusive via hidden radio button and only bolds one at a time), but they aren't actually conflicting because they occupy different zones named "Markings". In this change, we make the zone groups actually just be *by zone* rather than jumbling all of the zones with the same label together; but in most cases, we still keep the same simplified display. In the case of the "Stormy Cloud Kacheek" items though, we now get a few groups: `Glasses`, `Markings (#6)`, and `Markings (#16)`. Glasses is chosen by coincidence because it's the first zone label for that item alphabetically (even though that item also occupies a third "Markings" zone), and then the other two know to disambiguate from each other. There's an opportunity here to cheat things further, like to *intentionally* select items like "Glasses" that are less ambiguous when possible. I'm not aware of enough other cases like this for that to really matter, though, so I'm just leaving it as-is! I tested this a *bit* on other outfits, and everything looked fine at a glance, so I'm just moving forward—but I'll make an announcement to ask people to help take a look! --- .../wardrobe-2020/WardrobePage/ItemsPanel.js | 4 +- .../WardrobePage/useOutfitState.js | 60 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/app/javascript/wardrobe-2020/WardrobePage/ItemsPanel.js b/app/javascript/wardrobe-2020/WardrobePage/ItemsPanel.js index 6da13cf3..c5e0b4b2 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/ItemsPanel.js +++ b/app/javascript/wardrobe-2020/WardrobePage/ItemsPanel.js @@ -78,9 +78,9 @@ function ItemsPanel({ outfitState, outfitSaving, loading, dispatchToOutfit }) { /> ) : ( - {zonesAndItems.map(({ zoneLabel, items }) => ( + {zonesAndItems.map(({ zoneId, zoneLabel, items }) => ( itemsById[id]) .filter((i) => i); - // We use zone label here, rather than ID, because some zones have the same - // label and we *want* to over-simplify that in this UI. (e.g. there are - // multiple Hat zones, and some items occupy different ones, but mostly let's - // just group them and if they don't conflict then all the better!) + // Loop over all the items, grouping them by zone, and also gathering all the + // zone metadata. const allItems = [...wornItems, ...closetedItems]; - const itemsByZoneLabel = new Map(); + const itemsByZone = new Map(); + const zonesById = new Map(); for (const item of allItems) { if (!item.appearanceOn) { continue; } for (const layer of item.appearanceOn.layers) { - const zoneLabel = layer.zone.label; + const zoneId = layer.zone.id; + zonesById.set(zoneId, layer.zone); - if (!itemsByZoneLabel.has(zoneLabel)) { - itemsByZoneLabel.set(zoneLabel, []); + if (!itemsByZone.has(zoneId)) { + itemsByZone.set(zoneId, []); } - itemsByZoneLabel.get(zoneLabel).push(item); + itemsByZone.get(zoneId).push(item); } } - let zonesAndItems = Array.from(itemsByZoneLabel.entries()).map( - ([zoneLabel, items]) => ({ - zoneLabel: zoneLabel, + // Convert `itemsByZone` into an array of item groups. + let zonesAndItems = Array.from(itemsByZone.entries()).map( + ([zoneId, items]) => ({ + zoneId, + zoneLabel: zonesById.get(zoneId).label, items: [...items].sort((a, b) => a.name.localeCompare(b.name)), }), ); - zonesAndItems.sort((a, b) => a.zoneLabel.localeCompare(b.zoneLabel)); - // As one last step, try to remove zone groups that aren't helpful. + // Sort groups by the zone label's alphabetically, and tiebreak by the zone + // ID. (That way, "Markings (#6)" sorts before "Markings (#16)".) We do this + // before the data simplification step, because it's useful to have + // consistent ordering for the algorithm that might choose to skip zones! + zonesAndItems.sort((a, b) => { + if (a.zoneLabel !== b.zoneLabel) { + return a.zoneLabel.localeCompare(b.zoneLabel); + } else { + return a.zoneId - b.zoneId; + } + }); + + // Data simplification step! Try to remove zone groups that aren't helpful. const groupsWithConflicts = zonesAndItems.filter( ({ items }) => items.length > 1, ); @@ -620,6 +633,25 @@ function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) { } }); + // Finally, for groups with the same label, append the ID number. + // + // First, loop over the groups, to count how many times each zone label is + // used. Then, loop over them again, appending the ID number if count > 1. + const labelCounts = new Map(); + for (const itemZoneGroup of zonesAndItems) { + const { zoneId, zoneLabel } = itemZoneGroup; + + const count = labelCounts.get(zoneLabel) ?? 0; + labelCounts.set(zoneLabel, count + 1); + } + for (const itemZoneGroup of zonesAndItems) { + const { zoneId, zoneLabel } = itemZoneGroup; + + if (labelCounts.get(zoneLabel) > 1) { + itemZoneGroup.zoneLabel += ` (#${zoneId})`; + } + } + return zonesAndItems; }