diff --git a/apps/docs/src/components/try-map/langs/de.ts b/apps/docs/src/components/try-map/langs/de.ts index ef5e01a9..da598453 100644 --- a/apps/docs/src/components/try-map/langs/de.ts +++ b/apps/docs/src/components/try-map/langs/de.ts @@ -283,6 +283,7 @@ const pack: Pack = { rack_42u: 'Serverschrank 42 HE', box: 'Aufbewahrungsbox', washer: 'Waschmaschine', + water_purifier: 'Wasserfilteranlage', workbench: 'Werkbank', tool_wall: 'Werkzeugwand', wall_tv: 'Wandfernseher', diff --git a/apps/docs/src/components/try-map/langs/es.ts b/apps/docs/src/components/try-map/langs/es.ts index aa0cff1d..311408f7 100644 --- a/apps/docs/src/components/try-map/langs/es.ts +++ b/apps/docs/src/components/try-map/langs/es.ts @@ -282,6 +282,7 @@ const pack: Pack = { rack_42u: 'Bastidor de servidor 42U', box: 'Caja de almacenamiento', washer: 'Lavadora', + water_purifier: 'Purificador de agua', workbench: 'Banco de trabajo', tool_wall: 'Pared de herramientas', wall_tv: 'TV de pared', diff --git a/apps/docs/src/components/try-map/langs/fr.ts b/apps/docs/src/components/try-map/langs/fr.ts index 52bee1ad..f690d1a9 100644 --- a/apps/docs/src/components/try-map/langs/fr.ts +++ b/apps/docs/src/components/try-map/langs/fr.ts @@ -283,6 +283,7 @@ const pack: Pack = { rack_42u: 'Baie serveur 42U', box: 'Boîte de rangement', washer: 'Lave-linge', + water_purifier: 'Purificateur d’eau', workbench: 'Établi', tool_wall: 'Panneau à outils', wall_tv: 'Télé murale', diff --git a/apps/docs/src/components/try-map/langs/it.ts b/apps/docs/src/components/try-map/langs/it.ts index 20bf6939..8aac9b49 100644 --- a/apps/docs/src/components/try-map/langs/it.ts +++ b/apps/docs/src/components/try-map/langs/it.ts @@ -276,6 +276,7 @@ const pack: Pack = { rack_42u: 'Armadio rack 42U', box: 'Scatola', washer: 'Lavatrice', + water_purifier: 'Depuratore d’acqua', workbench: 'Banco da lavoro', tool_wall: 'Parete attrezzi', wall_tv: 'TV a parete', diff --git a/apps/docs/src/components/try-map/langs/uk.ts b/apps/docs/src/components/try-map/langs/uk.ts index 70ef91a8..0558aeb5 100644 --- a/apps/docs/src/components/try-map/langs/uk.ts +++ b/apps/docs/src/components/try-map/langs/uk.ts @@ -302,6 +302,7 @@ const pack: Pack = { rack_42u: 'Серверна стійка 42U', box: 'Коробка для зберігання', washer: 'Пральна машина', + water_purifier: 'Очищувач води', workbench: 'Верстак', tool_wall: 'Стіна з інструментами', wall_tv: 'Настінний телевізор', diff --git a/apps/docs/src/components/try-map/langs/vi.ts b/apps/docs/src/components/try-map/langs/vi.ts index 4188eb60..11799b77 100644 --- a/apps/docs/src/components/try-map/langs/vi.ts +++ b/apps/docs/src/components/try-map/langs/vi.ts @@ -281,6 +281,7 @@ const pack: Pack = { rack_42u: 'Tủ rack máy chủ 42U', box: 'Hộp lưu trữ', washer: 'Máy giặt', + water_purifier: 'Máy lọc nước', workbench: 'Bàn thợ', tool_wall: 'Tường dụng cụ', wall_tv: 'TV treo tường', diff --git a/apps/docs/src/components/try-map/strings.ts b/apps/docs/src/components/try-map/strings.ts index b09fce79..534adb6c 100644 --- a/apps/docs/src/components/try-map/strings.ts +++ b/apps/docs/src/components/try-map/strings.ts @@ -98,6 +98,7 @@ export const MODEL_NAMES: Record = { rack_42u: ['Server rack 42U', 'خزانة سيرفر ٤٢ وحدة'], box: ['Storage box', 'صندوق تخزين'], washer: ['Washing machine', 'غسالة'], + water_purifier: ['Water purifier', 'جهاز تنقية مياه'], workbench: ['Workbench', 'طاولة عمل'], tool_wall: ['Tool wall', 'جدار الأدوات'], wall_tv: ['Wall TV', 'تلفزيون حائط'], diff --git a/apps/web/src/locales/ar/messages.po b/apps/web/src/locales/ar/messages.po index a42183ee..473a9bc1 100644 --- a/apps/web/src/locales/ar/messages.po +++ b/apps/web/src/locales/ar/messages.po @@ -29296,3 +29296,7 @@ msgstr "القراءات المحوّلة" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "تغيّرت وحدة العدّاد" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "جهاز تنقية مياه" diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 7137ca0c..379c32d7 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -29296,3 +29296,7 @@ msgstr "Umgerechnete Zählerstände" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Zählereinheit geändert" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Wasserfilteranlage" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index 16ac9778..c7a98e6b 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -29296,3 +29296,7 @@ msgstr "Readings converted" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Meter unit changed" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Water purifier" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 44cd1915..8acc3921 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -29296,3 +29296,7 @@ msgstr "Lecturas convertidas" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Unidad del medidor cambiada" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Purificador de agua" diff --git a/apps/web/src/locales/fr/messages.po b/apps/web/src/locales/fr/messages.po index ec496a03..9474fac2 100644 --- a/apps/web/src/locales/fr/messages.po +++ b/apps/web/src/locales/fr/messages.po @@ -29296,3 +29296,7 @@ msgstr "Relevés convertis" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Unité du compteur modifiée" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Purificateur d’eau" diff --git a/apps/web/src/locales/it/messages.po b/apps/web/src/locales/it/messages.po index 6dfaaf6c..94c71809 100644 --- a/apps/web/src/locales/it/messages.po +++ b/apps/web/src/locales/it/messages.po @@ -29296,3 +29296,7 @@ msgstr "Letture convertite" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Unità del contatore cambiata" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Depuratore d’acqua" diff --git a/apps/web/src/locales/uk/messages.po b/apps/web/src/locales/uk/messages.po index 1ae53191..9fdd3915 100644 --- a/apps/web/src/locales/uk/messages.po +++ b/apps/web/src/locales/uk/messages.po @@ -31279,3 +31279,7 @@ msgstr "Перераховані показники" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Одиницю лічильника змінено" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Очищувач води" diff --git a/apps/web/src/locales/vi/messages.po b/apps/web/src/locales/vi/messages.po index 8b028b8d..ea8a159a 100644 --- a/apps/web/src/locales/vi/messages.po +++ b/apps/web/src/locales/vi/messages.po @@ -21345,3 +21345,7 @@ msgstr "Chỉ số đã quy đổi" #: src/components/history/labels.ts msgid "Meter unit changed" msgstr "Đã đổi đơn vị đồng hồ đo" + +#: src/map/labels.ts +msgid "Water purifier" +msgstr "Máy lọc nước" diff --git a/apps/web/src/map/engine/draw.ts b/apps/web/src/map/engine/draw.ts index a0338b7e..4f4e53ef 100644 --- a/apps/web/src/map/engine/draw.ts +++ b/apps/web/src/map/engine/draw.ts @@ -273,6 +273,8 @@ export function materials() { glass: mat('glass', { color: 0xbfd6e2, roughness: 0.05, transparent: true, opacity: 0.35 }), white: mat('white', { color: 0xf6f5f1, roughness: 0.4 }), appliance: mat('appliance', { color: 0xe9e9e6, roughness: 0.3, metalness: 0.1 }), + /** A dark grey appliance's body (the water purifier, #443). */ + graphite: mat('graphite', { color: 0x3a3d43, roughness: 0.35, metalness: 0.2 }), ceramic: mat('ceramic', { color: 0xefeae1, roughness: 0.3 }), terracotta: mat('terracotta', { color: 0xb0643f, roughness: 0.75 }), soil: mat('soil', { color: 0x4a3726, roughness: 1 }), @@ -1200,6 +1202,27 @@ export function drawnModels(F: Materials): Record { rb(g, 0.2, 0.08, 0.01, 0.15, 0.76, d + 0.004, F.white, 0.008); cy(g, 0.035, 0.035, 0.03, w - 0.12, 0.76, d + 0.01, F.chrome, Math.PI / 2); }, + /** + * A water purifier on the floor (#443): a dark cabinet on a recessed plinth with a brass band, + * a glass top with its buttons along the front edge, and at the back a panel with a lit + * display, the tap under it and a brass handle on top. + */ + water_purifier(g, w, d) { + const top = 1.088; + rb(g, w - 0.02, 0.03, d - 0.03, w / 2, 0.015, (d - 0.03) / 2, F.black, 0); + rb(g, w, top - 0.03, d, w / 2, (top + 0.03) / 2, d / 2, F.graphite, 0.012); + rb(g, w - 0.01, 0.027, 0.004, w / 2, 0.62, d + 0.002, F.brass, 0.002); + rb(g, w, 0.012, d, w / 2, top + 0.006, d / 2, F.blackGloss, 0.004); + rb(g, w - 0.08, 0.02, 0.003, w / 2, top - 0.022, d + 0.0015, F.screen, 0.002); + for (const k of [-1, 0, 1]) sp(g, 0.003, w / 2 + k * 0.03, top - 0.022, d + 0.003, F.glow); + const panel = 0.18; + const py = top + 0.012 + panel / 2; + rb(g, w - 0.02, panel, 0.05, w / 2, py, 0.04, F.graphite, 0.01); + rb(g, 0.08, 0.035, 0.003, w / 2, py + 0.05, 0.0665, F.screenLit, 0.002); + cy(g, 0.008, 0.008, 0.04, w / 2, py - 0.06, 0.085, F.chrome, Math.PI / 2); + cy(g, 0.006, 0.006, 0.03, w / 2, py - 0.075, 0.1, F.chrome); + rb(g, w * 0.55, 0.014, 0.045, w * 0.6, top + 0.012 + panel + 0.003, 0.04, F.brass, 0.006); + }, workbench(g, w, d) { rb(g, w, 0.06, d, w / 2, 0.89, d / 2, F.butcher, 0.01); for (const x of [0.06, w - 0.06]) diff --git a/apps/web/src/map/labels.ts b/apps/web/src/map/labels.ts index 70a86b56..08f2539c 100644 --- a/apps/web/src/map/labels.ts +++ b/apps/web/src/map/labels.ts @@ -87,6 +87,7 @@ export function useModelNames(): Record { rack_42u: t`Server rack 42U`, box: t`Storage box`, washer: t`Washing machine`, + water_purifier: t`Water purifier`, workbench: t`Workbench`, tool_wall: t`Tool wall`, wall_tv: t`Wall TV`, diff --git a/apps/web/src/map/models.test.ts b/apps/web/src/map/models.test.ts index 04656c1c..86ee4620 100644 --- a/apps/web/src/map/models.test.ts +++ b/apps/web/src/map/models.test.ts @@ -51,3 +51,22 @@ describe('the homelab shapes (#171)', () => { } }); }); + +describe('the water purifier (#443)', () => { + it('is drawn in code, filling its own size and no more', async () => { + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null); + const THREE = await import('three'); + const { drawnModels, materials } = await import('./engine/draw'); + const build = drawnModels(materials()).water_purifier; + expect(build).toBeTypeOf('function'); + const m = mapModel('water_purifier'); + const g = new THREE.Group(); + build?.(g, m.w, m.d); + const size = new THREE.Box3().setFromObject(g).getSize(new THREE.Vector3()); + // Its tap and buttons stand a few millimetres proud of its front. + expect(size.x).toBeCloseTo(m.w, 2); + expect(size.y).toBeCloseTo(m.h ?? 0, 2); + expect(size.z).toBeGreaterThanOrEqual(m.d - 0.005); + expect(size.z).toBeLessThanOrEqual(m.d + 0.01); + }); +}); diff --git a/apps/web/src/map/thumbs/shapes.webp b/apps/web/src/map/thumbs/shapes.webp index bc9838bf..f1e2b1ce 100644 Binary files a/apps/web/src/map/thumbs/shapes.webp and b/apps/web/src/map/thumbs/shapes.webp differ diff --git a/apps/web/src/map/thumbs/sprite.ts b/apps/web/src/map/thumbs/sprite.ts index 3978b5a4..a63ca20f 100644 --- a/apps/web/src/map/thumbs/sprite.ts +++ b/apps/web/src/map/thumbs/sprite.ts @@ -87,24 +87,25 @@ export const THUMB_CELLS: Readonly> = { rack_42u: 74, box: 75, washer: 76, - workbench: 77, - tool_wall: 78, - wall_tv: 79, - wall_shelf: 80, - wall_cabinet: 81, - rack_wall_6u: 82, - rack_wall_12u: 83, - picture: 84, - clock: 85, - hooks: 86, - car: 87, - bike: 88, - water_tank: 89, - dish: 90, - stairs: 91, - stairwell: 92, - zone: 93, - item: 94, + water_purifier: 77, + workbench: 78, + tool_wall: 79, + wall_tv: 80, + wall_shelf: 81, + wall_cabinet: 82, + rack_wall_6u: 83, + rack_wall_12u: 84, + picture: 85, + clock: 86, + hooks: 87, + car: 88, + bike: 89, + water_tank: 90, + dish: 91, + stairs: 92, + stairwell: 93, + zone: 94, + item: 95, }; /** Each shape's height as drawn, in centimetres, from its rendered bounds. */ @@ -186,6 +187,7 @@ export const THUMB_HEIGHTS_CM: Readonly> = { rack_42u: 200, box: 34.2, washer: 85, + water_purifier: 129, workbench: 92, tool_wall: 60, wall_tv: 71, diff --git a/packages/shared/src/map-shapes.test.ts b/packages/shared/src/map-shapes.test.ts index 5b6552c4..d92d9c4a 100644 --- a/packages/shared/src/map-shapes.test.ts +++ b/packages/shared/src/map-shapes.test.ts @@ -1003,3 +1003,99 @@ describe('suggestShapes: what a thing is for, in Spanish and Vietnamese', () => expect(named.map((s) => s.model)).not.toContain('printer'); }); }); + +describe('a water purifier standing on the floor (#443)', () => { + it.each([ + ['Water purifier', 'water_purifier'], + ['Kitchen water purifier', 'water_purifier'], + ['Water filtration system', 'water_purifier'], + ['Water dispenser', 'water_purifier'], + ['Purificateur d’eau', 'water_purifier'], + ['Fontaine à eau', 'water_purifier'], + ['Wasserspender', 'water_purifier'], + ['Depuratore d’acqua', 'water_purifier'], + ['Purificador de agua', 'water_purifier'], + ['Очищувач води', 'water_purifier'], + ['Máy lọc nước', 'water_purifier'], + ['جهاز تنقية المياه', 'water_purifier'], + ['جهاز تنقية مياه', 'water_purifier'], + ['Wasserfilteranlage', 'water_purifier'], + ] as const)('%s gets the %s, given by itself', (name, model) => { + const s = suggestShapes({ name }); + expect(s.shapes[0]).toMatchObject({ model, from: 'name' }); + expect(s.confident).toBe(true); + }); + + it('is not sure about a filter cartridge, and keeps the water tank its own', () => { + expect(sure({ name: 'Water filter cartridge' })).toBe(false); + expect(first({ name: 'Water tank' })).toBe('water_tank'); + expect(first({ name: 'Bồn nước' })).toBe('water_tank'); + }); + + // A water filter is as often a jug, a cartridge or a tap's as the cabinet on the floor: the + // plain filter words don't name it, and what these got before the shape came, they still get. + it.each([ + 'Water filter', + 'Water filter jug', + 'Water filter pitcher', + 'Brita water filter', + 'Shower water filter', + 'Water filter replacement', + 'Wasserfilter', + 'Wasserfilter Kanne', + 'Wasserfilter Kartusche', + 'Carafe filtre à eau', + 'Jarra filtro de agua', + "Filtro per l'acqua", + 'Фільтр для води', + 'Bình lọc nước', + 'فلتر مياه', + 'Pet water dispenser', + 'Fontaine à eau pour chat', + ])('gives %s no water purifier', (name) => { + expect(shapeOnAdd({ name })).toEqual({ model: 'item', from: 'guess' }); + expect(suggestShapes({ name }).shapes.map((x) => x.model)).not.toContain('water_purifier'); + }); + + it('never gives a thing of the Filters type the water purifier', () => { + // The sample household's filter (apps/server/src/seed/inventory.ts) keeps its box. + for (const name of ['Water filter cartridge', 'Water filter', 'Water purifier filter']) + expect(shapeOnAdd({ name, typeKey: 'filters' }), name).toEqual({ + model: 'box', + from: 'guess', + }); + expect( + suggestShapes({ name: 'Water purifier', typeKey: 'filters' }).shapes.map((x) => x.model), + ).not.toContain('water_purifier'); + // As the map's server reads it: the type's key and its name. + expect( + shapeOnAdd({ name: 'Water filter cartridge', typeKey: 'filters', typeName: 'Filters' }), + ).toEqual({ model: 'box', from: 'guess' }); + }); + + it('is only a guess for a jug, a cartridge or a replacement named after it', () => { + for (const name of [ + 'Water purifier cartridge', + 'Water purifier replacement', + 'Wasserspender Kanne', + ]) + expect(sure({ name }), name).toBe(false); + }); + + it('finds it by search in each language, with the kitchen and laundry', () => { + expect(searchShapes('water pur')[0]).toBe('water_purifier'); + expect(searchShapes('wasserspen', {}, 'de')[0]).toBe('water_purifier'); + expect(searchShapes('máy lọc', {}, 'vi')[0]).toBe('water_purifier'); + expect(SHAPE_GROUP.water_purifier).toBe('kitchen'); + for (const lang of SHAPE_LANGS) + expect(shapeWordsByLang('water_purifier')[lang].length, lang).toBeGreaterThan(0); + }); + + it('is drawn at 31 × 36 × 129 cm, standing on the floor', () => { + const m = mapModel('water_purifier'); + expect([m.w, m.d, m.h]).toEqual([0.31, 0.36, 1.29]); + expect(m.kind).toBe('thing'); + expect(m.small).toBeUndefined(); + expect(m.wall).toBeUndefined(); + }); +}); diff --git a/packages/shared/src/map-shapes.ts b/packages/shared/src/map-shapes.ts index 647a28c5..8276c0ec 100644 --- a/packages/shared/src/map-shapes.ts +++ b/packages/shared/src/map-shapes.ts @@ -113,6 +113,7 @@ export const SHAPE_GROUP: Readonly> = { rack_42u: 'storage', box: 'storage', washer: 'kitchen', + water_purifier: 'kitchen', workbench: 'tools', tool_wall: 'wall', wall_tv: 'wall', @@ -1794,6 +1795,39 @@ export const SHAPE_WORDS: Readonly> = { 'lavadora', 'lavarropas', ], + /** #443: the request's words for the cabinet, and a floor water dispenser's. The plain filter + * words ("water filter", "Wasserfilter", "فلتر مياه", "bình lọc nước") are left out: as often a + * jug, a cartridge or a tap's filter, they would give those a 129 cm cabinet. */ + water_purifier: [ + 'water purifier', + 'water filtration system', + 'water dispenser', + 'water cooler', + "purificateur d'eau", + "système de filtration d'eau", + 'fontaine à eau', + 'wasserreiniger', + 'wasserfilteranlage', + 'wasserfiltrationssystem', + 'wasserspender', + "depuratore d'acqua", + 'depuratore acqua', + "sistema di filtraggio dell'acqua", + 'erogatore acqua', + 'جهاز تنقية المياه', + 'جهاز تنقية مياه', + 'مبرد مياه', + 'máy lọc nước', + 'máy lọc nước gia đình', + 'hệ thống lọc nước', + 'cây nước nóng lạnh', + 'очищувач води', + 'система фільтрації води', + 'кулер для води', + 'purificador de agua', + 'sistema de filtración de agua', + 'dispensador de agua', + ], workbench: [ 'workbench', 'work bench', @@ -2351,6 +2385,10 @@ export const SHAPE_WORD_LANGS: Readonly> = { filters: { shapes: ['box', 'item'], score: 0.3 }, }; +/** Shapes a built-in type never is, whatever its name says: a filter is a part (#443), never the + * water purifier it goes in. */ +const TYPE_NEVER: Readonly> = { + filters: ['water_purifier'], +}; + /** Folds text for matching: lower case, no accents (Cyrillic й and ї with them), ß as ss, đ as d, * Arabic letter forms and vowel marks folded, punctuation as spaces. */ export function foldText(text: string): string { @@ -3194,8 +3252,54 @@ const SINGULAR_ONLY: ReadonlySet = new Set(['notebook'].flatMap(words)); * Words that make a word name something else entirely: a "Spiral notebook", an "A5 notebook" or * a "Notebook a righe" is stationery, not a laptop, so it falls back to a small thing (there is no * book shape). Whole words only; "Notebook computer" and "Gaming - * notebook" stay laptops. + * notebook" stay laptops. A term may be a phrase: a "Pet water dispenser" is a bowl, not the + * water purifier (#443). */ +/** A pet's, in Kept's languages: what a water dispenser for one is (#443). */ +const PET_WORDS: ReadonlySet = new Set( + [ + 'pet', + 'pets', + 'cat', + 'cats', + 'dog', + 'dogs', + 'chat', + 'chats', + 'chien', + 'chiens', + 'haustier', + 'haustiere', + 'katze', + 'katzen', + 'hund', + 'hunde', + 'animali', + 'gatto', + 'gatti', + 'cane', + 'cani', + 'mascota', + 'mascotas', + 'gato', + 'gatos', + 'perro', + 'perros', + 'тварин', + 'кота', + 'котів', + 'собаки', + 'mèo', + 'chó', + 'thú cưng', + 'قطط', + 'قطة', + 'كلاب', + 'كلب', + 'حيوانات', + ].flatMap(words), +); + const NOT_THE_SHAPE: readonly { model: MapModelKey; term: string; words: ReadonlySet }[] = [ { model: 'laptop', @@ -3291,6 +3395,21 @@ const NOT_THE_SHAPE: readonly { model: MapModelKey; term: string; words: Readonl ['televisor', 'televisión', 'tele', 'tv', 'distancia', 'garaje', 'aire'].flatMap(words), ), }, + ...[ + 'water dispenser', + 'water cooler', + 'fontaine à eau', + 'wasserspender', + 'erogatore acqua', + 'dispensador de agua', + 'кулер для води', + 'cây nước nóng lạnh', + 'مبرد مياه', + ].map((phrase) => ({ + model: 'water_purifier' as const, + term: words(phrase).join(' '), + words: PET_WORDS, + })), ]; /** The shapes a name's words point at: phrases inside a longer matched phrase are dropped. */ @@ -3356,6 +3475,10 @@ const ACCESSORIES: ReadonlySet = new Set( 'ink', 'toner', 'cartridge', + 'replacement', + 'jug', + 'pitcher', + 'shower', 'battery', 'batteries', 'housse', @@ -3369,6 +3492,7 @@ const ACCESSORIES: ReadonlySet = new Set( 'objectif', 'encre', 'cartouche', + 'carafe', 'batterie', 'support', 'hülle', @@ -3380,6 +3504,8 @@ const ACCESSORIES: ReadonlySet = new Set( 'objektiv', 'tinte', 'patrone', + 'kartusche', + 'kanne', 'akku', 'halterung', 'ständer', @@ -3393,6 +3519,7 @@ const ACCESSORIES: ReadonlySet = new Set( 'obiettivo', 'inchiostro', 'cartuccia', + 'caraffa', 'batteria', 'supporto', 'جراب', @@ -3423,6 +3550,7 @@ const ACCESSORIES: ReadonlySet = new Set( 'lente', 'tinta', 'cartucho', + 'jarra', 'batería', 'pila', 'pilas', @@ -3474,11 +3602,7 @@ function nameScores(text: string | null | undefined): Map { const phrase = MARKS_REQUIRED.get(h.terms.join(' ')); if (phrase && !marked.includes(phrase)) return false; return !NOT_THE_SHAPE.some( - (r) => - r.model === h.model && - h.terms.length === 1 && - h.terms[0] === r.term && - ws.some((w) => r.words.has(w)), + (r) => r.model === h.model && h.terms.join(' ') === r.term && ws.some((w) => r.words.has(w)), ); }); const inPhrase = (i: number) => @@ -3544,6 +3668,7 @@ export function suggestShapes(input: ShapeInput, limit = 6): ShapeSuggestions { return s; }; const hint = input.typeKey ? TYPE_SHAPES[input.typeKey] : undefined; + const ruledOut = input.typeKey ? TYPE_NEVER[input.typeKey] : undefined; /** Shapes the type names outright: a confident type's first shape, or a custom type's name. */ const typeNames = new Set(); hint?.shapes.forEach((m, i) => { @@ -3553,11 +3678,13 @@ export function suggestShapes(input: ShapeInput, limit = 6): ShapeSuggestions { }); // A custom type's name is the type speaking: "Television" is as clear as the TV type. for (const [m, v] of nameScores(input.typeName)) { + if (ruledOut?.includes(m)) continue; const s = at(m); s.type = Math.max(s.type, v >= 0.9 ? 0.92 : v); if (v >= 0.9) typeNames.add(m); } const fromName = nameScores(input.name); + for (const m of ruledOut ?? []) fromName.delete(m); for (const [m, v] of fromName) at(m).name = v; // Both pointing at a shape lifts it above either alone (a TV-type thing named "Monitor" ranks diff --git a/packages/shared/src/map.ts b/packages/shared/src/map.ts index 1a421fb1..1fcf7cfd 100644 --- a/packages/shared/src/map.ts +++ b/packages/shared/src/map.ts @@ -305,6 +305,8 @@ export const MAP_MODELS = { rack_42u: { w: 0.6, d: 1.0, h: 2.0, kind: 'storage', holds: true }, box: { w: 0.6, d: 0.4, kind: 'storage', holds: true, small: true, top: 0.34 }, washer: { w: 0.6, d: 0.6, kind: 'thing' }, + /** A water purifier standing on the floor (#443): a narrow cabinet, its tap under a panel. */ + water_purifier: { w: 0.31, d: 0.36, h: 1.29, kind: 'thing' }, workbench: { w: 1.8, d: 0.7, kind: 'furniture', top: 0.92, holds: true }, // On a wall (#138), drawn at real sizes. The tool wall is a 1.2 × 0.6 m pegboard over a bench: // tools hang from its hooks, their tops 5 cm under its top edge.