diff --git a/apps/docs/src/components/try-map/langs/de.ts b/apps/docs/src/components/try-map/langs/de.ts index da598453..0cf9dedf 100644 --- a/apps/docs/src/components/try-map/langs/de.ts +++ b/apps/docs/src/components/try-map/langs/de.ts @@ -233,6 +233,7 @@ const pack: Pack = { bookshelf: 'Bücherregal', cube_grid: 'Würfelregal', bookcase: 'Hohes Bücherregal', + shelving_unit: 'Regal', lamp: 'Stehlampe', counter: 'Küchenzeile', sink: 'Spülenschrank', diff --git a/apps/docs/src/components/try-map/langs/es.ts b/apps/docs/src/components/try-map/langs/es.ts index 311408f7..d02cf5f1 100644 --- a/apps/docs/src/components/try-map/langs/es.ts +++ b/apps/docs/src/components/try-map/langs/es.ts @@ -232,6 +232,7 @@ const pack: Pack = { bookshelf: 'Estantería', cube_grid: 'Estantería de cubos', bookcase: 'Estantería alta', + shelving_unit: 'Estantería de almacenaje', lamp: 'Lámpara de pie', counter: 'Encimera de cocina', sink: 'Encimera de fregadero', diff --git a/apps/docs/src/components/try-map/langs/fr.ts b/apps/docs/src/components/try-map/langs/fr.ts index f690d1a9..87b3db0e 100644 --- a/apps/docs/src/components/try-map/langs/fr.ts +++ b/apps/docs/src/components/try-map/langs/fr.ts @@ -233,6 +233,7 @@ const pack: Pack = { bookshelf: 'Bibliothèque', cube_grid: 'Étagère à cases', bookcase: 'Grande bibliothèque', + shelving_unit: 'Rayonnage', lamp: 'Lampadaire', counter: 'Plan de travail', sink: 'Meuble évier', diff --git a/apps/docs/src/components/try-map/langs/it.ts b/apps/docs/src/components/try-map/langs/it.ts index 8aac9b49..f6264fb6 100644 --- a/apps/docs/src/components/try-map/langs/it.ts +++ b/apps/docs/src/components/try-map/langs/it.ts @@ -226,6 +226,7 @@ const pack: Pack = { bookshelf: 'Libreria', cube_grid: 'Scaffale a cubi', bookcase: 'Libreria alta', + shelving_unit: 'Scaffale', lamp: 'Lampada da terra', counter: 'Piano di lavoro', sink: 'Mobile lavello', diff --git a/apps/docs/src/components/try-map/langs/uk.ts b/apps/docs/src/components/try-map/langs/uk.ts index 0558aeb5..eea27569 100644 --- a/apps/docs/src/components/try-map/langs/uk.ts +++ b/apps/docs/src/components/try-map/langs/uk.ts @@ -252,6 +252,7 @@ const pack: Pack = { bookshelf: 'Книжкова полиця', cube_grid: 'Стелаж-куб', bookcase: 'Висока книжкова шафа', + shelving_unit: 'Стелаж', lamp: 'Підлогова лампа', counter: 'Кухонна стільниця', sink: 'Стільниця з мийкою', diff --git a/apps/docs/src/components/try-map/langs/vi.ts b/apps/docs/src/components/try-map/langs/vi.ts index 11799b77..0b935bf9 100644 --- a/apps/docs/src/components/try-map/langs/vi.ts +++ b/apps/docs/src/components/try-map/langs/vi.ts @@ -231,6 +231,7 @@ const pack: Pack = { bookshelf: 'Kệ sách', cube_grid: 'Hộc lưu trữ', bookcase: 'Tủ sách cao', + shelving_unit: 'Tủ kệ', lamp: 'Đèn sàn', counter: 'Mặt bếp', sink: 'Mặt chậu rửa', diff --git a/apps/docs/src/components/try-map/strings.ts b/apps/docs/src/components/try-map/strings.ts index 534adb6c..63cfd9c3 100644 --- a/apps/docs/src/components/try-map/strings.ts +++ b/apps/docs/src/components/try-map/strings.ts @@ -48,6 +48,7 @@ export const MODEL_NAMES: Record = { bookshelf: ['Bookshelf', 'مكتبة كتب'], cube_grid: ['Cube storage', 'وحدة تخزين مكعبات'], bookcase: ['Tall bookcase', 'مكتبة طويلة'], + shelving_unit: ['Shelving unit', 'وحدة رفوف'], lamp: ['Floor lamp', 'مصباح أرضي'], counter: ['Kitchen counter', 'سطح مطبخ'], sink: ['Sink counter', 'سطح بحوض'], diff --git a/apps/docs/src/content/docs/ar/users/location-map.mdx b/apps/docs/src/content/docs/ar/users/location-map.mdx index 88dce5a3..b191d703 100644 --- a/apps/docs/src/content/docs/ar/users/location-map.mdx +++ b/apps/docs/src/content/docs/ar/users/location-map.mdx @@ -114,7 +114,7 @@ import clipWebm from '../../../../assets/clips/map-add-tv.webm?url'; 4. اكتب جزءًا من الاسم أو المعرّف القصير في **ابحث عن شيء في** الغرفة، ثم اضغط الشيء. يظهر في منتصف الغرفة، محددًا. 5. بطاقته تقول أي شكل حصل عليه ولم: **شكل: تلفزيون، من نوعه** أو **من اسمه**. عندما لا يكون Kept متأكدًا، تقول البطاقة **تخمين** وتظهر **اختر شكلًا**. 6. **تغيير** (أو **اختر شكلًا**) يفتح الأشكال. **مقترح** يأتي أولًا، ثم كل شكل في مجموعته (أثاث، تخزين، على الحائط، إلكترونيات، مطبخ وغسيل، أدوات، نباتات وخارجي، مركبات، سلالم ومناطق)، كلٌّ بصورة. **ابحث عن شكل** يبحث في الأسماء وكلماتها الأخرى بأي من لغات Kept: "fridge"، "Kühlschrank" و"ثلاجة" كلها تجد الثلاجة. بينما تكتب، تظهر التطابقات فقط، والأشكال التي يبدأ اسمها أو كلمتها بما كتبت تأتي أولًا: "ca" يجد Car، Camera وWebcam قبل Tool cart. كلمات بلغتك والإنجليزية تطابق من بدايتها؛ كلمة بلغة أخرى تطابق بمجرد كتابتها كلها. امسح البحث لترى المقترح والمجموعات مجددًا. على الهاتف الأشكال تبقى فوق لوحة المفاتيح وتمرر. اضغط شكلًا لاستخدامه. كل شكل، مع مقاسه وكلماته، في [مكتبة الأشكال](/ar/users/map-shapes/)؛ **ينقصك شكل؟ اطلبه** تحت الأشكال يفتح نموذج GitHub لطلب واحد. -شكلان يأخذان مقاسهما منك: رف **وحدة تخزين مكعبات** له **مكعبات بالعرض** و**مكعبات بالارتفاع** على بطاقته (٢ × ٢ في البداية؛ **اقلبه على جانبه** يضجع ١ × ٤ كـ ٤ × ١)، و**مكتبة طويلة** لها **العرض**، **العمق** و**الارتفاع** بالسنتيمترات (بالبوصات في النظام الإمبراطوري) و**عدد الرفوف** (٨٠ × ٢٨ × ٢٠٢ سم مع ٦ في البداية). ترسم الخريطة كل مكعب وكل رف. +ثلاثة أشكال تأخذ مقاسها منك: رف **وحدة تخزين مكعبات** له **مكعبات بالعرض** و**مكعبات بالارتفاع** على بطاقته (٢ × ٢ في البداية؛ **اقلبه على جانبه** يضجع ١ × ٤ كـ ٤ × ١)، و**مكتبة طويلة** لها **العرض**، **العمق** و**الارتفاع** بالسنتيمترات (بالبوصات في النظام الإمبراطوري) و**عدد الرفوف** (٨٠ × ٢٨ × ٢٠٢ سم مع ٦ في البداية). ترسم الخريطة كل مكعب وكل رف. و**وحدة رفوف** لها المقاسات الثلاثة نفسها و**عدد الرفوف**، رفّان على الأقل (٩٠ × ٥٠ × ١٨٠ سم مع ٤ في البداية): أربع قوائم، والرف السفلي مرفوع عن الأرض والعلوي في مستوى القوائم. 7. اسحب الشيء حيث يقف. الأشياء الصغيرة يمكن أن تقف على مكتب، طاولة، رف أو صندوق. **تدوير** يدوره ربع دورة. 8. اضغط **مُنجز**. الرسالة تعرض **تراجع**. **إلغاء** قبل مُنجز يرمي الجلسة كلها، بعد سؤال. diff --git a/apps/docs/src/content/docs/ar/users/map-shapes.mdx b/apps/docs/src/content/docs/ar/users/map-shapes.mdx index 753d8990..80ebf854 100644 --- a/apps/docs/src/content/docs/ar/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/ar/users/map-shapes.mdx @@ -7,7 +7,7 @@ import ShapeLibrary from '../../../../components/shapes/ShapeLibrary.astro'; كل شكل يمكن لـ [خريطة الموقع](/ar/users/location-map/) رسمه، في المجموعات التي يعرضها منتقي الشكل. لكل منها صورته، وحجمه (العرض × العمق × الارتفاع، قبل تدويره) والكلمات التي تجده في **ابحث عن شكل**، أو التي تمنح الشيء شكله من اسمه. هذه الصفحة مبنية من كتالوج الأشكال الخاص بـ Kept، لذا فهي تسرد بالضبط ما يحتويه إصدار Kept لديك. -الأحجام نموذجية، بالسنتيمترات: الشيء يحتفظ بحجم شكله على الخريطة، مهما كان حجمه الحقيقي. عندما تكون **الوحدات** على **إمبراطوري** (الإعدادات → أنا → العرض)، تُظهر الخريطة الأحجام بالأقدام والبوصات وتقبل بطاقة المكتبة البوصات؛ يحفظ Kept السنتيمترات في الحالتين. شكلان يتم تحديد حجمهما من بطاقتهما بدلاً من ذلك: **وحدة تخزين مكعبات** حسب مكعباتها أفقياً وعمودياً (كل مكعب ٣٣ سم، ٣٩ سم عمقاً: ٢ × ٢ هو ٧٧ × ٣٩ × ٧٧ سم، ٤ × ٤ هو ١٤٧ × ٣٩ × ١٤٧ سم)، و **مكتبة طويلة** حسب عرضها وعمقها وارتفاعها ومستوياتها. أحجامها هنا هي التي تبدأ بها. الأثاث ذو المقصورات يأخذ حجمه الخاص: تُرسم مقصوراته بداخله. والخزانة المصنوعة من قالب خزانة تُرسم كأقرب خزانة سيرفر فوق عدد وحداتها، وأجهزتها عند وحداتها. +الأحجام نموذجية، بالسنتيمترات: الشيء يحتفظ بحجم شكله على الخريطة، مهما كان حجمه الحقيقي. عندما تكون **الوحدات** على **إمبراطوري** (الإعدادات → أنا → العرض)، تُظهر الخريطة الأحجام بالأقدام والبوصات وتقبل بطاقة المكتبة ووحدة الرفوف البوصات؛ يحفظ Kept السنتيمترات في الحالتين. ثلاثة أشكال يتم تحديد حجمها من بطاقتها بدلاً من ذلك: **وحدة تخزين مكعبات** حسب مكعباتها أفقياً وعمودياً (كل مكعب ٣٣ سم، ٣٩ سم عمقاً: ٢ × ٢ هو ٧٧ × ٣٩ × ٧٧ سم، ٤ × ٤ هو ١٤٧ × ٣٩ × ١٤٧ سم)، و **مكتبة طويلة** حسب عرضها وعمقها وارتفاعها ومستوياتها، و **وحدة رفوف** حسب عرضها وعمقها وارتفاعها وعدد رفوفها. أحجامها هنا هي التي تبدأ بها. الأثاث ذو المقصورات يأخذ حجمه الخاص: تُرسم مقصوراته بداخله. والخزانة المصنوعة من قالب خزانة تُرسم كأقرب خزانة سيرفر فوق عدد وحداتها، وأجهزتها عند وحداتها. **شكل مفقود؟** [اطلبه](https://github.com/keptinventory/kept/issues/new?template=shape-request.yml) بحساب GitHub: قل ما هو، وأعطِ حجمه وأضف صورة أو رابطاً. أو [أضفه بنفسك](/ar/developers/add-a-shape/). diff --git a/apps/docs/src/content/docs/de/users/location-map.mdx b/apps/docs/src/content/docs/de/users/location-map.mdx index 654b503f..695a8d60 100644 --- a/apps/docs/src/content/docs/de/users/location-map.mdx +++ b/apps/docs/src/content/docs/de/users/location-map.mdx @@ -223,11 +223,11 @@ dort hinsetzt. Besitzer, Admins und Mitglieder können das. Wörtern, ist in der [Formbibliothek](/de/users/map-shapes/); **Fehlt eine Form? Anfragen** unter den Formen öffnet ein GitHub-Formular, um eine zu bitten. - Zwei Formen nehmen ihre Größe von dir: ein **Würfelregal** hat **Fächer nebeneinander** und + Drei Formen nehmen ihre Größe von dir: ein **Würfelregal** hat **Fächer nebeneinander** und **Fächer übereinander** auf seiner Karte (2 × 2 am Anfang; **Auf die Seite legen** legt ein 1 × 4 als 4 × 1 hin), und ein **Hohes Bücherregal** hat seine **Breite**, **Tiefe** und **Höhe** in Zentimetern (Zoll im imperialen System) und seine **Ebenen** (80 × 28 × 202 cm mit 6 am Anfang). Die Karte - zeichnet jedes Fach und jede Ebene. + zeichnet jedes Fach und jede Ebene. Ein **Regal** hat dieselben drei Maße und seine **Regalböden**, mindestens zwei (90 × 50 × 180 cm mit 4 am Anfang): vier Pfosten, der unterste Regalboden etwas über dem Fußboden, der oberste bündig mit den Pfosten. 7. Zieh den Gegenstand dahin, wo er steht. Kleine Dinge können auf einem Schreibtisch, einer Arbeitsplatte, einem Regal oder einer Kiste stehen. **Drehen** dreht ihn um ein Viertel. 8. Tippe **Erledigt**. Der Toast bietet **Rückgängig**. **Abbrechen** vor Erledigt wirft die diff --git a/apps/docs/src/content/docs/de/users/map-shapes.mdx b/apps/docs/src/content/docs/de/users/map-shapes.mdx index 0f4f4c21..26a1889c 100644 --- a/apps/docs/src/content/docs/de/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/de/users/map-shapes.mdx @@ -8,7 +8,7 @@ import ShapeLibrary from '../../../../components/shapes/ShapeLibrary.astro'; Jede Form, die die [Standortkarte](/de/users/location-map/) zeichnen kann, in den Gruppen, wie der Formwähler sie anzeigt. Jede hat ihr Bild, ihre Größe (Breite × Tiefe × Höhe, bevor sie gedreht wird) und die Wörter, die sie in **Form suchen** finden oder einem Ding aus seinem Namen seine Form geben. Diese Seite wird aus Kepts eigenem Formenkatalog erstellt, daher listet sie genau, was deine Kept-Version hat. -Größen sind typische Angaben, in Zentimetern: Ein Ding behält auf der Karte die Größe seiner Form, egal wie groß es wirklich ist. Mit **Einheiten** auf **Imperial** (Einstellungen → Profil → Anzeige) zeigt die Karte Größen in Fuß und Zoll, und die Karte eines Bücherregals nimmt Zoll an; Kept speichert so oder so Zentimeter. Zwei Formen werden auf ihrer Karte anders dimensioniert: **Würfelregal** nach Würfeln in Breite und Höhe (jeder Würfel 33 cm, 39 cm tief: 2 × 2 sind 77 × 39 × 77 cm, 4 × 4 sind 147 × 39 × 147 cm) und **Hohes Bücherregal** nach Breite, Tiefe, Höhe und Ebenen. Die hier angegebenen Größen sind ihre Startgrößen. Möbel mit Fächern haben ihre eigene Größe: Die Fächer werden darin eingezeichnet. Ein Schrank aus einer Schrankvorlage wird als nächstgrößerer Serverschrank gezeichnet, mit seinen +Größen sind typische Angaben, in Zentimetern: Ein Ding behält auf der Karte die Größe seiner Form, egal wie groß es wirklich ist. Mit **Einheiten** auf **Imperial** (Einstellungen → Profil → Anzeige) zeigt die Karte Größen in Fuß und Zoll, und die Karte eines Bücherregals oder Regals nimmt Zoll an; Kept speichert so oder so Zentimeter. Drei Formen werden auf ihrer Karte anders dimensioniert: **Würfelregal** nach Würfeln in Breite und Höhe (jeder Würfel 33 cm, 39 cm tief: 2 × 2 sind 77 × 39 × 77 cm, 4 × 4 sind 147 × 39 × 147 cm), **Hohes Bücherregal** nach Breite, Tiefe, Höhe und Ebenen und **Regal** nach Breite, Tiefe, Höhe und Regalböden. Die hier angegebenen Größen sind ihre Startgrößen. Möbel mit Fächern haben ihre eigene Größe: Die Fächer werden darin eingezeichnet. Ein Schrank aus einer Schrankvorlage wird als nächstgrößerer Serverschrank gezeichnet, mit seinen Geräten bei ihren Einheiten. **Fehlt eine Form?** [Fordere sie an](https://github.com/keptinventory/kept/issues/new?template=shape-request.yml) diff --git a/apps/docs/src/content/docs/developers/add-a-shape.md b/apps/docs/src/content/docs/developers/add-a-shape.md index 283456fe..2c92aae8 100644 --- a/apps/docs/src/content/docs/developers/add-a-shape.md +++ b/apps/docs/src/content/docs/developers/add-a-shape.md @@ -49,8 +49,9 @@ The key is stored in a location's layout, so never rename or remove one once it ## A shape sized by its numbers -The cube shelf sized by its grid (`cube_grid`) and the bookcase with levels (`bookcase`) are drawn -from numbers someone sets on the card, not from one size. A node's stored model carries them after +The cube shelf sized by its grid (`cube_grid`), the bookcase with levels (`bookcase`) and the +shelving unit with shelves (`shelving_unit`) are drawn from numbers someone sets on the card, not +from one size. A node's stored model carries them after the key, so the layout's format, the API and the database stay as they were: | Model | Means | @@ -59,11 +60,15 @@ the key, so the layout's format, the API and the database stay as they were: | `cube_grid_4x1` | 4 across, 1 up (a 1 × 4 lying down) | | `bookcase` | 80 × 28 × 202 cm, 6 levels | | `bookcase_60x28x150_4` | 60 × 28 × 150 cm (width, depth, height), 4 levels | +| `shelving_unit` | 90 × 50 × 180 cm, 4 shelves | +| `shelving_unit_120x60x200_4` | 120 × 60 × 200 cm (width, depth, height), 4 shelves | `sizedOf` reads the numbers (null for anything else, out of range, or spelled another way: the first size is only ever the bare key), `sizedKey` writes them, `mapModel` gives the size and `baseModel` the catalogue key, which the names, the groups and the pictures use. -`apps/web/src/map/engine/sized.ts` turns the numbers into boards and the spaces between them, and +`apps/web/src/map/engine/sized.ts` turns the numbers into boards and the spaces between them, +`apps/web/src/map/engine/draw.ts` gives the boards their materials (its `sized` function), +`apps/web/src/map/compartments.ts` puts furniture's compartments in those spaces, and `apps/web/src/map/size.tsx` is the card's fields. A new sized shape goes in all of these, and in `isMapModel`'s tests and the server's layout test. diff --git a/apps/docs/src/content/docs/es/users/location-map.mdx b/apps/docs/src/content/docs/es/users/location-map.mdx index 2dbb90a2..b9d8c93e 100644 --- a/apps/docs/src/content/docs/es/users/location-map.mdx +++ b/apps/docs/src/content/docs/es/users/location-map.mdx @@ -114,7 +114,7 @@ Los estantes, armarios, áreas y cajas que están directamente en una habitació 4. Escribe parte del nombre o el ID corto en **Busca un objeto en** la habitación, luego toca el objeto. Aparece en medio de la habitación, seleccionado. 5. Su tarjeta dice qué forma tuvo y por qué: **Forma: TV, de su tipo** o **de su nombre**. Cuando Kept no está seguro, la tarjeta dice **una suposición** y muestra **Elige una forma**. 6. **Cambiar** (o **Elige una forma**) abre las formas. **Sugerido** va primero, luego todas las formas de su grupo (muebles, almacenamiento, en la pared, electrónica, cocina y lavandería, herramientas, plantas y exterior, vehículos, escaleras y áreas), cada una con una imagen. **Busca una forma** busca los nombres y sus otras palabras en cualquiera de los idiomas de Kept: «nevera», «Kühlschrank» y «ثلاجة» encuentran la nevera. Mientras escribes, solo se ven las que coinciden, y las formas cuyo nombre o palabra empieza por lo que escribiste salen primero: «ne» encuentra Nevera, Webcam y Cámara antes que Carrito de herramientas. Las palabras en tu idioma e inglés coinciden desde su inicio; una palabra en otro idioma coincide al escribirla toda. Borra la búsqueda para ver **Sugerido** y los grupos de nuevo. En el móvil las formas se quedan sobre el teclado y se desplazan. Toca una forma para usarla. Cada forma, con su tamaño y palabras, está en la [biblioteca de formas](/es/users/map-shapes/); **¿Falta una forma? Pídela** debajo de las formas abre un formulario de GitHub para pedirla. - Dos formas toman su tamaño de ti: una estantería **Estantería de cubos** tiene **Cubos a lo ancho** y **Cubos hacia arriba** en su tarjeta (2 × 2 al principio; **Poner de lado** tumba una de 1 × 4 como 4 × 1), y una **Estantería alta** tiene su **Ancho**, **Profundidad** y **Altura** en centímetros (pulgadas en imperial) y sus **Niveles** (80 × 28 × 202 cm con 6 al principio). El mapa dibuja cada cubo y cada nivel. + Tres formas toman su tamaño de ti: una estantería **Estantería de cubos** tiene **Cubos a lo ancho** y **Cubos hacia arriba** en su tarjeta (2 × 2 al principio; **Poner de lado** tumba una de 1 × 4 como 4 × 1), y una **Estantería alta** tiene su **Ancho**, **Profundidad** y **Altura** en centímetros (pulgadas en imperial) y sus **Niveles** (80 × 28 × 202 cm con 6 al principio). El mapa dibuja cada cubo y cada nivel. Una **Estantería de almacenaje** tiene esos mismos tres tamaños y sus **Estantes**, dos como mínimo (90 × 50 × 180 cm con 4 al principio): cuatro postes, el estante de abajo separado del suelo y el de arriba a ras de los postes. 7. Arrastra el objeto donde está. Los objetos pequeños pueden estar en un escritorio, encimera, estante o caja. **Girar** lo gira un cuarto. 8. Toca **Listo**. El aviso ofrece **Deshacer**. **Cancelar** antes de Listo tira la sesión entera, tras preguntar. diff --git a/apps/docs/src/content/docs/es/users/map-shapes.mdx b/apps/docs/src/content/docs/es/users/map-shapes.mdx index a006c559..fa5651e6 100644 --- a/apps/docs/src/content/docs/es/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/es/users/map-shapes.mdx @@ -8,7 +8,7 @@ import ShapeLibrary from '../../../../components/shapes/ShapeLibrary.astro'; Cada forma que puede dibujar el [mapa de ubicación](/es/users/location-map/), en los grupos que muestra el selector de formas. Cada una tiene su imagen, su tamaño (ancho × fondo × alto, antes de girarla) y las palabras que la encuentran en **Busca una forma**, o que dan una forma a un objeto por su nombre. Esta página se genera a partir del catálogo de formas de Kept, así que enumera exactamente lo que tiene tu versión de Kept. -Los tamaños son típicos, en centímetros: un objeto mantiene en el mapa el tamaño de su forma, sea cual sea su tamaño real. Con **Unidades** en **Imperial** (Configuración → Yo → Visualización), el mapa muestra los tamaños en pies y pulgadas y la tarjeta de una estantería acepta pulgadas; Kept guarda centímetros de todos modos. Dos formas se dimensionan en su tarjeta en su lugar: **Estantería de cubos** por sus cubos de ancho y alto (cada cubo de 33 cm, 39 cm de fondo: 2 × 2 son 77 × 39 × 77 cm, 4 × 4 son 147 × 39 × 147 cm), y **Estantería alta** por su ancho, fondo, alto y niveles. Los tamaños que aparecen aquí son los que tienen al empezar. El mobiliario con compartimentos tiene su propio tamaño: sus compartimentos se dibujan dentro de él. Un bastidor hecho con una plantilla de bastidor se dibuja como el bastidor de servidor más cercano por encima de sus unidades, con sus equipos en sus unidades. +Los tamaños son típicos, en centímetros: un objeto mantiene en el mapa el tamaño de su forma, sea cual sea su tamaño real. Con **Unidades** en **Imperial** (Configuración → Yo → Visualización), el mapa muestra los tamaños en pies y pulgadas y la tarjeta de una estantería acepta pulgadas; Kept guarda centímetros de todos modos. Tres formas se dimensionan en su tarjeta en su lugar: **Estantería de cubos** por sus cubos de ancho y alto (cada cubo de 33 cm, 39 cm de fondo: 2 × 2 son 77 × 39 × 77 cm, 4 × 4 son 147 × 39 × 147 cm), **Estantería alta** por su ancho, fondo, alto y niveles, y **Estantería de almacenaje** por su ancho, fondo, alto y estantes. Los tamaños que aparecen aquí son los que tienen al empezar. El mobiliario con compartimentos tiene su propio tamaño: sus compartimentos se dibujan dentro de él. Un bastidor hecho con una plantilla de bastidor se dibuja como el bastidor de servidor más cercano por encima de sus unidades, con sus equipos en sus unidades. **¿Falta una forma?** [Solicítala](https://github.com/keptinventory/kept/issues/new?template=shape-request.yml) con una cuenta de GitHub: di qué es, indica su tamaño y añade una foto o un enlace. O diff --git a/apps/docs/src/content/docs/fr/users/location-map.mdx b/apps/docs/src/content/docs/fr/users/location-map.mdx index 7f2aacf8..69cf1d95 100644 --- a/apps/docs/src/content/docs/fr/users/location-map.mdx +++ b/apps/docs/src/content/docs/fr/users/location-map.mdx @@ -223,11 +223,11 @@ mis. Les propriétaires, admins et membres peuvent le faire. défilent. Touchez une forme pour l'utiliser. Toutes les formes, avec leur taille et leurs mots, sont dans la [bibliothèque de formes](/fr/users/map-shapes/) ; **Il manque une forme ? Demandez-la** sous les formes ouvre un formulaire GitHub pour en demander une. - Deux formes prennent leur taille chez vous : une **Étagère à cases** a **Cases en largeur** et + Trois formes prennent leur taille chez vous : une **Étagère à cases** a **Cases en largeur** et **Cases en hauteur** sur sa fiche (2 × 2 au début ; **Coucher sur le côté** couche un 1 × 4 en 4 × 1), et une **Grande bibliothèque** a sa **Largeur**, sa **Profondeur** et sa **Hauteur** en centimètres (en pouces en impérial) et ses **Niveaux** (80 × 28 × 202 cm avec 6 au début). La carte - dessine chaque case et chaque niveau. + dessine chaque case et chaque niveau. Un **Rayonnage** a les trois mêmes dimensions et ses **Tablettes**, deux au moins (90 × 50 × 180 cm avec 4 au début) : quatre montants, la tablette du bas au-dessus du sol et celle du haut au ras des montants. 7. Glissez l'objet où il se tient. Les petits objets peuvent se poser sur un bureau, un comptoir, une étagère ou une boîte. **Tourner** le fait pivoter d'un quart. 8. Touchez **Terminé**. Le message propose **Annuler**. **Annuler** avant Terminé jette toute la diff --git a/apps/docs/src/content/docs/fr/users/map-shapes.mdx b/apps/docs/src/content/docs/fr/users/map-shapes.mdx index d3939294..ceb66935 100644 --- a/apps/docs/src/content/docs/fr/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/fr/users/map-shapes.mdx @@ -12,10 +12,8 @@ qui la trouvent dans **Trouver une forme**, ou qui donnent sa forme à un objet Les tailles sont indicatives, en centimètres : un objet garde la taille de sa forme sur la carte, quelle que soit sa taille réelle. Avec **Unités** sur **Impérial** (Paramètres → Moi → Affichage), la carte affiche les tailles en pieds -et pouces et la fiche d'une bibliothèque accepte des pouces ; Kept garde des centimètres dans tous les cas. Deux -formes sont dimensionnées sur leur fiche plutôt : **Étagère à cases** par ses cubes en largeur et en hauteur (chaque cube de 33 -cm, profondeur 39 cm : 2 × 2 fait 77 × 39 × 77 cm, 4 × 4 fait 147 × 39 × 147 cm), et **Grande bibliothèque** par -sa largeur, sa profondeur, sa hauteur et ses niveaux. Les tailles données ici sont celles de départ. Le mobilier +et pouces et la fiche d'une bibliothèque ou d'un rayonnage accepte des pouces ; Kept garde des centimètres dans tous les cas. Trois formes sont dimensionnées sur leur fiche plutôt : **Étagère à cases** par ses cubes en largeur et en hauteur (chaque cube de 33 +cm, profondeur 39 cm : 2 × 2 fait 77 × 39 × 77 cm, 4 × 4 fait 147 × 39 × 147 cm), **Grande bibliothèque** par sa largeur, sa profondeur, sa hauteur et ses niveaux, et **Rayonnage** par sa largeur, sa profondeur, sa hauteur et ses tablettes. Les tailles données ici sont celles de départ. Le mobilier à compartiments prend sa propre taille : ses compartiments y sont dessinés. Une baie faite d'un modèle de baie est dessinée comme la baie serveur la plus proche au-dessus de ses unités, avec ses équipements à leurs unités. diff --git a/apps/docs/src/content/docs/it/users/location-map.mdx b/apps/docs/src/content/docs/it/users/location-map.mdx index 0560f2e6..bcfc6d89 100644 --- a/apps/docs/src/content/docs/it/users/location-map.mdx +++ b/apps/docs/src/content/docs/it/users/location-map.mdx @@ -190,10 +190,10 @@ una scrivania o l'auto, è sulla mappa solo quando la metti lì. Proprietari, ad ricerca per rivedere Suggerito e i gruppi. Sul telefono le forme stanno sopra la tastiera e scorrono. Tocca una forma per usarla. Ogni forma, con la sua misura e parole, è nella [libreria delle forme](/it/users/map-shapes/); **Manca una forma? Chiedila** sotto le forme apre un modulo GitHub per chiederne una. - Due forme prendono la loro misura da te: uno scaffale **Scaffale a cubi** ha **Cubi in larghezza** e + Tre forme prendono la loro misura da te: uno scaffale **Scaffale a cubi** ha **Cubi in larghezza** e **Cubi in altezza** sulla sua scheda (2 × 2 all'inizio; **Metti di lato** sdraia un 1 × 4 come un 4 × 1), e una **Libreria alta** ha la sua **Larghezza**, **Profondità** e **Altezza** in centimetri (pollici in imperiale) e i suoi - **Ripiani** (80 × 28 × 202 cm con 6 all'inizio). La mappa disegna ogni cubo e ogni ripiano. + **Ripiani** (80 × 28 × 202 cm con 6 all'inizio). La mappa disegna ogni cubo e ogni ripiano. Uno **Scaffale** ha le stesse tre misure e i suoi **Ripiani**, almeno due (90 × 50 × 180 cm con 4 all'inizio): quattro montanti, il ripiano più basso sollevato dal pavimento e quello più alto a filo dei montanti. 7. Trascina l'oggetto dove sta. Gli oggetti piccoli possono stare su una scrivania, bancone, scaffale o scatola. **Ruota** lo gira di un quarto. 8. Tocca **Fatto**. Il toast offre **Annulla**. **Annulla** prima di Fatto butta via tutta la sessione, dopo aver chiesto. diff --git a/apps/docs/src/content/docs/it/users/map-shapes.mdx b/apps/docs/src/content/docs/it/users/map-shapes.mdx index d65ca14d..27421825 100644 --- a/apps/docs/src/content/docs/it/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/it/users/map-shapes.mdx @@ -12,10 +12,8 @@ dal catalogo delle forme di Kept, quindi elenca esattamente ciò che ha la tua v Le dimensioni sono quelle tipiche, in centimetri: un oggetto mantiene le dimensioni della sua forma sulla mappa, qualunque sia la sua dimensione reale. Con **Unità** su **Imperiale** (Impostazioni → Profilo → Aspetto), la mappa mostra le dimensioni in piedi e pollici e la scheda di una -libreria accetta pollici; Kept salva i centimetri in ogni caso. Due -forme hanno invece le dimensioni indicate sulla loro scheda: **Scaffale a cubi** per i suoi cubi in larghezza e in altezza (ogni cubo 33 -cm, 39 cm di profondità: 2 × 2 è 77 × 39 × 77 cm, 4 × 4 è 147 × 39 × 147 cm), e **Libreria alta** per -la sua larghezza, profondità, altezza e livelli. Le dimensioni qui sono quelle iniziali. I mobili +libreria o di uno scaffale accetta pollici; Kept salva i centimetri in ogni caso. Tre forme hanno invece le dimensioni indicate sulla loro scheda: **Scaffale a cubi** per i suoi cubi in larghezza e in altezza (ogni cubo 33 +cm, 39 cm di profondità: 2 × 2 è 77 × 39 × 77 cm, 4 × 4 è 147 × 39 × 147 cm), **Libreria alta** per la sua larghezza, profondità, altezza e livelli, e **Scaffale** per la sua larghezza, profondità, altezza e ripiani. Le dimensioni qui sono quelle iniziali. I mobili con scomparti hanno le proprie dimensioni: i loro scomparti vengono disegnati all'interno. Un armadio fatto da un modello di armadio rack è disegnato come l'armadio server più vicino sopra le sue unità, con i suoi apparati alle loro unità. diff --git a/apps/docs/src/content/docs/uk/users/location-map.mdx b/apps/docs/src/content/docs/uk/users/location-map.mdx index 9f970615..72d74154 100644 --- a/apps/docs/src/content/docs/uk/users/location-map.mdx +++ b/apps/docs/src/content/docs/uk/users/location-map.mdx @@ -120,7 +120,7 @@ import clipWebm from '../../../../assets/clips/map-add-tv.webm?url'; 4. Введіть частину назви або короткого ID у **Знайти річ у** кімнаті, потім натисніть на річ. Вона з'являється посередині кімнати, виділена. 5. Її картка каже, яку форму вона отримала і чому: **Форма: Телевізор, від її типу** або **від її назви**. Коли Kept не впевнена, картка каже **припущення** і показує **Виберіть форму**. 6. **Змінити** (або **Вибрати форму**) відкриває форми. **Запропоновано** йде першими, потім уся форма в її групі (меблі, сховища, на стіні, електроніка, кухня й пральня, інструменти, рослини й двір, транспорт, сходи й зони), кожна з картинкою. **Знайти форму** шукає назви й їх інші слова в будь-якій з мов Kept: «fridge», «Kühlschrank» і «ثلاجة» все знаходять холодильник. Поки ви пишете, показуються лише збіги, а форми, чия назва чи слово починається на те, що ви написали, йдуть першими: «ca» знаходить Car, Camera і Webcam раніше Tool cart. Слова вашою мовою й англійською збігаються від початку; слово іншою мовою — якщо написали його цілим. Очистіть пошук, щоб знову бачити Запропоновано й групи. На телефоні форми лишаються над клавіатурою і прокручуються. Натисніть на форму, щоб обрати її. Кожна форма, з її розміром і словами, в [бібліотеці форм](/uk/users/map-shapes/); **Немає форми? Запитати** під формами відкриває форму GitHub, щоб попросити одну. - Дві форми беруть свій розмір від вас: у **Стелаж-куб** на картці є **Куби в ширину** і **Куби вгору** (2 × 2 спочатку; **Повернути на бік** кладе 1 × 4 як 4 × 1), а у **Висока книжкова шафа** — її **Ширина**, **Глибина** і **Висота** у сантиметрах (у дюймах в імперській системі) і її **Рівні** (80 × 28 × 202 см з 6 спочатку). Карта малює кожен куб і кожен рівень. + Три форми беруть свій розмір від вас: у **Стелаж-куб** на картці є **Куби в ширину** і **Куби вгору** (2 × 2 спочатку; **Повернути на бік** кладе 1 × 4 як 4 × 1), а у **Висока книжкова шафа** — її **Ширина**, **Глибина** і **Висота** у сантиметрах (у дюймах в імперській системі) і її **Рівні** (80 × 28 × 202 см з 6 спочатку). Карта малює кожен куб і кожен рівень. У **Стелаж** ті самі три розміри і його **Полиці**, щонайменше дві (90 × 50 × 180 см з 4 спочатку): чотири стійки, нижня полиця над підлогою, а верхня врівень зі стійками. 7. Перетягніть річ туди, де вона стоїть. **Повернути** вертає її на чверть. 8. Натисніть **Готово**. Тост пропонує **Скасувати**. **Скасувати** до Готово викидає всю сесію, запитуючи. diff --git a/apps/docs/src/content/docs/uk/users/map-shapes.mdx b/apps/docs/src/content/docs/uk/users/map-shapes.mdx index e490bcf7..0b3429e2 100644 --- a/apps/docs/src/content/docs/uk/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/uk/users/map-shapes.mdx @@ -7,7 +7,7 @@ import ShapeLibrary from '../../../../components/shapes/ShapeLibrary.astro'; Кожна форма, яку може малювати [карта локацій](/uk/users/location-map/), у групах, які показує вибір форми. У кожної є її зображення, її розмір (ширина × глибина × висота, до повороту) та слова, що знаходять її в **Знайти форму**, або які дають річ її форму з її назви. Ця сторінка побудована з власного каталогу форм Kept, тому вона перелічує саме те, що має ваша версія Kept. -Розміри типові, у сантиметрах: річ зберігає розмір своєї форми на карті, будь-який її реальний розмір. Коли **Одиниці** — **Імперська** (Налаштування → Я → Відображення), карта показує розміри у футах і дюймах, а картка книжкової шафи приймає дюйми; Kept у будь-якому разі зберігає сантиметри. Дві форми мають розмір на їх картці: **Стелаж-куб** за її кубами по горизонталі та вертикалі (кожен куб 33 см, 39 см глибини: 2 × 2 це 77 × 39 × 77 см, 4 × 4 це 147 × 39 × 147 см), та **Висока книжкова шафа** за її шириною, глибиною, висотою та рівнями. Їхні розміри тут — це початкові. Меблі з відсіками має власний розмір: його відсіки малюються всередині нього. Стійку з шаблону стійки намальовано як найближчу серверну стійку понад її юніти, з обладнанням на його юнітах. +Розміри типові, у сантиметрах: річ зберігає розмір своєї форми на карті, будь-який її реальний розмір. Коли **Одиниці** — **Імперська** (Налаштування → Я → Відображення), карта показує розміри у футах і дюймах, а картка книжкової шафи чи стелажа приймає дюйми; Kept у будь-якому разі зберігає сантиметри. Три форми мають розмір на їх картці: **Стелаж-куб** за її кубами по горизонталі та вертикалі (кожен куб 33 см, 39 см глибини: 2 × 2 це 77 × 39 × 77 см, 4 × 4 це 147 × 39 × 147 см), **Висока книжкова шафа** за її шириною, глибиною, висотою та рівнями, та **Стелаж** за його шириною, глибиною, висотою та полицями. Їхні розміри тут — це початкові. Меблі з відсіками має власний розмір: його відсіки малюються всередині нього. Стійку з шаблону стійки намальовано як найближчу серверну стійку понад її юніти, з обладнанням на його юнітах. **Немає потрібної форми?** [Запитайте її](https://github.com/keptinventory/kept/issues/new?template=shape-request.yml) з обліковим записом GitHub: опишіть, що це, вкажіть її розмір і додайте фото або посилання. Або [додайте її самостійно](/uk/developers/add-a-shape/). diff --git a/apps/docs/src/content/docs/users/location-map.mdx b/apps/docs/src/content/docs/users/location-map.mdx index a648a2ff..b77e6862 100644 --- a/apps/docs/src/content/docs/users/location-map.mdx +++ b/apps/docs/src/content/docs/users/location-map.mdx @@ -205,11 +205,11 @@ members can do it. Tap a shape to use it. Every shape, with its size and words, is in the [shape library](/users/map-shapes/); **Missing a shape? Request it** under the shapes opens a GitHub form to ask for one. - Two shapes take their size from you: a **Cube storage** shelf has **Cubes across** and + Three shapes take their size from you: a **Cube storage** shelf has **Cubes across** and **Cubes up** on its card (2 × 2 at first; **Turn on its side** lays a 1 × 4 down as a 4 × 1), and a **Tall bookcase** has its **Width**, **Depth** and **Height** in centimetres (inches in imperial) and its - **Levels** (80 × 28 × 202 cm with 6 at first). The map draws each cube and each level. + **Levels** (80 × 28 × 202 cm with 6 at first). The map draws each cube and each level. A **Shelving unit** has the same three sizes and its **Shelves**, two at least (90 × 50 × 180 cm with 4 at first): four posts, the bottom shelf off the floor and the top one level with the posts. 7. Drag the thing where it stands. Small things can stand on a desk, counter, shelf or box. **Turn** turns it a quarter. 8. Tap **Done**. The toast offers **Undo**. **Cancel** before Done throws the whole session away, diff --git a/apps/docs/src/content/docs/users/map-shapes.mdx b/apps/docs/src/content/docs/users/map-shapes.mdx index 25f42ef4..f469c4d1 100644 --- a/apps/docs/src/content/docs/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/users/map-shapes.mdx @@ -12,10 +12,8 @@ from Kept's own shape catalogue, so it lists exactly what your version of Kept h Sizes are typical ones, in centimetres: a thing keeps its shape's size on the map, whatever its real size. With **Units** set to **Imperial** (Settings → Me → Display), the map shows sizes in -feet and inches and a bookcase's card takes inches; Kept keeps centimetres either way. Two -shapes are sized on their card instead: **Cube storage** by its cubes across and up (each cube 33 -cm, 39 cm deep: 2 × 2 is 77 × 39 × 77 cm, 4 × 4 is 147 × 39 × 147 cm), and **Tall bookcase** by -its width, depth, height and levels. Their sizes here are the ones they start at. Furniture +feet and inches and a bookcase's or a shelving unit's card takes inches; Kept keeps centimetres either way. Three shapes are sized on their card instead: **Cube storage** by its cubes across and up (each cube 33 +cm, 39 cm deep: 2 × 2 is 77 × 39 × 77 cm, 4 × 4 is 147 × 39 × 147 cm), **Tall bookcase** by its width, depth, height and levels, and **Shelving unit** by its width, depth, height and shelves. Their sizes here are the ones they start at. Furniture with compartments takes its own size: its compartments are drawn inside it. A rack made from a rack template is drawn as the server rack nearest above its units, with its gear at its units. diff --git a/apps/docs/src/content/docs/vi/users/location-map.mdx b/apps/docs/src/content/docs/vi/users/location-map.mdx index 81219933..a8f52883 100644 --- a/apps/docs/src/content/docs/vi/users/location-map.mdx +++ b/apps/docs/src/content/docs/vi/users/location-map.mdx @@ -119,7 +119,7 @@ Kệ, tủ, khu vực và hộp đứng ngay trong một phòng đã trên bản 4. Gõ một phần tên hoặc ID ngắn trong **Tìm đồ vật trong** phòng, rồi chạm vào đồ vật. Nó xuất hiện giữa phòng, được chọn. 5. Thẻ nói hình dạng nó có và tại sao: **Hình dạng: TV, từ loại của nó** hoặc **từ tên của nó**. Khi Kept không chắc, thẻ nói **một đoán** và hiển thị **Chọn hình dạng**. 6. **Thay đổi** (hoặc **Chọn hình dạng**) mở các hình dạng. **Được gợi ý** lên trước, rồi mọi hình dạng trong nhóm của nó (đồ gia dụng, chứa đồ, trên tường, điện tử, nhà bếp và giặt ủi, công cụ, cây và ngoài trời, phương tiện, cầu thang và khu vực), mỗi cái một ảnh. **Tìm hình dạng** tìm tên và từ khác của chúng trong mọi ngôn ngữ của Kept: "tủ lạnh", "Kühlschrank" và "ثلاجة" đều tìm tủ lạnh. Khi bạn gõ, chỉ kết quả khớp hiện, và hình dạng có tên hoặc từ bắt đầu bằng cái bạn gõ lên đầu: "xe" tìm Xe, Máy ảnh và Webcam trước Xe đẩy công cụ. Từ trong ngôn ngữ của bạn và tiếng Anh khớp từ đầu; từ trong ngôn ngữ khác khớp khi bạn gõ hết nó. Xóa tìm kiếm để thấy lại Được gợi ý và các nhóm. Trên điện thoại hình dạng ở trên bàn phím và cuộn. Chạm một hình dạng để dùng nó. Mọi hình dạng, với kích thước và từ của nó, trong [thư viện hình dạng](/vi/users/map-shapes/); **Thiếu hình dạng? Yêu cầu nó** dưới các hình dạng mở biểu mẫu GitHub để xin một cái. - Hai hình dạng lấy kích thước từ bạn: một kệ **Hộc lưu trữ** có **Số ô theo chiều ngang** và **Số ô theo chiều dọc** trên thẻ (2 × 2 ban đầu; **Xoay nằm nghiêng** đặt 1 × 4 nằm thành 4 × 1), và một **Tủ sách cao** có **Chiều rộng**, **Chiều sâu** và **Chiều cao** tính bằng centimet (inch ở hệ Anh) và **Số tầng** (80 × 28 × 202 cm với 6 ban đầu). Bản đồ vẽ từng ô và từng tầng. + Ba hình dạng lấy kích thước từ bạn: một kệ **Hộc lưu trữ** có **Số ô theo chiều ngang** và **Số ô theo chiều dọc** trên thẻ (2 × 2 ban đầu; **Xoay nằm nghiêng** đặt 1 × 4 nằm thành 4 × 1), và một **Tủ sách cao** có **Chiều rộng**, **Chiều sâu** và **Chiều cao** tính bằng centimet (inch ở hệ Anh) và **Số tầng** (80 × 28 × 202 cm với 6 ban đầu). Bản đồ vẽ từng ô và từng tầng. Một **Tủ kệ** có ba kích thước đó và **Số tầng kệ**, ít nhất hai (90 × 50 × 180 cm với 4 ban đầu): bốn cột, kệ dưới cùng cách sàn và kệ trên cùng ngang đầu cột. 7. Kéo đồ vật đến chỗ nó đứng. Đồ vật nhỏ có thể đứng trên bàn, quầy, kệ hoặc hộp. **Xoay** xoay nó một góc vuông. 8. Chạm **Xong**. Thông báo đề xuất **Hoàn tác**. **Hủy** trước Xong vứt cả phiên, sau khi hỏi. diff --git a/apps/docs/src/content/docs/vi/users/map-shapes.mdx b/apps/docs/src/content/docs/vi/users/map-shapes.mdx index 734f330a..97d47702 100644 --- a/apps/docs/src/content/docs/vi/users/map-shapes.mdx +++ b/apps/docs/src/content/docs/vi/users/map-shapes.mdx @@ -7,7 +7,7 @@ import ShapeLibrary from '../../../../components/shapes/ShapeLibrary.astro'; Mọi hình dạng mà [bản đồ địa điểm](/vi/users/location-map/) có thể vẽ, trong các nhóm mà trình chọn hình dạng hiển thị. Mỗi hình đều có hình ảnh, kích thước (rộng × sâu × cao, trước khi xoay) và các từ khóa tìm kiếm trong **Tìm hình dạng**, hoặc để gán hình dạng cho một đồ vật từ tên của nó. Trang này được xây dựng từ danh mục hình dạng của Kept, nên liệt kê chính xác những gì phiên bản Kept của bạn có. -Kích thước là những kích thước điển hình, tính bằng centimet: một đồ vật giữ kích thước hình dạng của nó trên bản đồ, bất kể kích thước thực tế của nó. Khi **Đơn vị đo** là **Hệ Anh** (Cài đặt → Tôi → Hiển thị), bản đồ hiển thị kích thước bằng feet và inch và thẻ của tủ sách nhận inch; Kept vẫn lưu centimet trong mọi trường hợp. Hai hình dạng được xác định kích thước trên thẻ của chúng: **Hộc lưu trữ** theo số ô ngang và dọc (mỗi ô 33 cm, sâu 39 cm: 2 × 2 là 77 × 39 × 77 cm, 4 × 4 là 147 × 39 × 147 cm), và **Tủ sách cao** theo rộng, sâu, cao và số tầng. Kích thước ở đây là kích thước khởi tạo. Đồ nội thất có ngăn chứa lấy kích thước của chính nó: các ngăn được vẽ bên trong nó. Tủ làm từ mẫu tủ rack được vẽ như tủ máy chủ gần nhất trên số đơn vị của nó, với thiết bị ở các đơn vị của chúng. +Kích thước là những kích thước điển hình, tính bằng centimet: một đồ vật giữ kích thước hình dạng của nó trên bản đồ, bất kể kích thước thực tế của nó. Khi **Đơn vị đo** là **Hệ Anh** (Cài đặt → Tôi → Hiển thị), bản đồ hiển thị kích thước bằng feet và inch và thẻ của tủ sách hoặc tủ kệ nhận inch; Kept vẫn lưu centimet trong mọi trường hợp. Ba hình dạng được xác định kích thước trên thẻ của chúng: **Hộc lưu trữ** theo số ô ngang và dọc (mỗi ô 33 cm, sâu 39 cm: 2 × 2 là 77 × 39 × 77 cm, 4 × 4 là 147 × 39 × 147 cm), **Tủ sách cao** theo rộng, sâu, cao và số tầng, và **Tủ kệ** theo rộng, sâu, cao và số tầng kệ. Kích thước ở đây là kích thước khởi tạo. Đồ nội thất có ngăn chứa lấy kích thước của chính nó: các ngăn được vẽ bên trong nó. Tủ làm từ mẫu tủ rack được vẽ như tủ máy chủ gần nhất trên số đơn vị của nó, với thiết bị ở các đơn vị của chúng. **Thiếu hình dạng?** [Yêu cầu thêm](https://github.com/keptinventory/kept/issues/new?template=shape-request.yml) bằng tài khoản GitHub: nói rõ đó là gì, cho biết kích thước và thêm ảnh hoặc liên kết. Hoặc [tự thêm](/vi/developers/add-a-shape/). diff --git a/apps/server/src/history/summary.test.ts b/apps/server/src/history/summary.test.ts index 9184a8ec..c9ec173e 100644 --- a/apps/server/src/history/summary.test.ts +++ b/apps/server/src/history/summary.test.ts @@ -192,6 +192,27 @@ describe('arrangements in words (#140)', () => { ), ), ).toEqual(['map.resize {"name":"Corner desk"}', 'map.resize {"name":"Shelf 2"}']); + // A shelving unit's new size or shelves (#485), and a change from it, by its code. + expect( + text( + arrangeLines( + [n('shelf', { model: 'shelving_unit' })], + [n('shelf', { model: 'shelving_unit_120x60x200_4' })], + names, + ), + ), + ).toEqual(['map.resize {"name":"Shelf 2"}']); + expect( + text( + arrangeLines( + [n('shelf', { model: 'shelving_unit_120x60x200_4' })], + [n('shelf', { model: 'bookcase' })], + names, + ), + ), + ).toEqual([ + 'map.shape {"name":"Shelf 2","from":"shelving_unit","to":"bookcase","vocab":"model"}', + ]); // The web says a shape by its code, so the numbers stay out of it. expect( text( diff --git a/apps/server/src/map/map.test.ts b/apps/server/src/map/map.test.ts index 2c65cdd9..8f02a52f 100644 --- a/apps/server/src/map/map.test.ts +++ b/apps/server/src/map/map.test.ts @@ -428,6 +428,31 @@ describe('PUT /api/v1/locations/:id/layout', () => { expect((await putLayout(ola, overlapping)).statusCode).toBe(200); }); + it('stores a shelving unit’s size and shelves in the model, and refuses others (#485)', async () => { + const base = fullLayout(null); + const sized = { + ...base, + nodes: [{ ...base.nodes[0], model: 'shelving_unit_120x60x200_4' }, base.nodes[1]], + }; + const res = await putLayout(max, sized); + expect(res.statusCode, res.body).toBe(200); + const map = await json(getMap(vic)); + expect(map.nodes.find((n) => n.id === ids.shelf)?.layout?.model).toBe( + 'shelving_unit_120x60x200_4', + ); + const version = (res.json() as { version: string }).version; + for (const model of [ + 'shelving_unit_90x50x180_4', // its first size is the bare key + 'shelving_unit_90x50x40_3', // three shelves in 40 cm + 'shelving_unit_90x50x180_1', + 'shelving_unit_90x81x180_4', + ]) { + const bad = { ...base, version, nodes: [{ ...base.nodes[0], model }] }; + const r = await putLayout(ola, bad); + expect(r.statusCode, model).toBe(400); + } + }); + it('stores a cube shelf’s grid and a bookcase’s size in the model, and refuses others (#240, #241)', async () => { const base = fullLayout(null); const sized = { diff --git a/apps/web/src/locales/ar/messages.po b/apps/web/src/locales/ar/messages.po index 473a9bc1..f504028e 100644 --- a/apps/web/src/locales/ar/messages.po +++ b/apps/web/src/locales/ar/messages.po @@ -27946,6 +27946,7 @@ msgid "Shelf {n}" msgstr "رف {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "وحدة رفوف" @@ -29300,3 +29301,8 @@ msgstr "تغيّرت وحدة العدّاد" #: src/map/labels.ts msgid "Water purifier" msgstr "جهاز تنقية مياه" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "عدد الرفوف" diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 379c32d7..78d3cfe7 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -27946,6 +27946,7 @@ msgid "Shelf {n}" msgstr "Regalboden {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "Regal" @@ -29300,3 +29301,8 @@ msgstr "Zählereinheit geändert" #: src/map/labels.ts msgid "Water purifier" msgstr "Wasserfilteranlage" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Regalböden" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index c7a98e6b..72cdea57 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -27946,6 +27946,7 @@ msgid "Shelf {n}" msgstr "Shelf {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "Shelving unit" @@ -29300,3 +29301,8 @@ msgstr "Meter unit changed" #: src/map/labels.ts msgid "Water purifier" msgstr "Water purifier" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Shelves" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 8acc3921..855e7ce4 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -27946,8 +27946,9 @@ msgid "Shelf {n}" msgstr "Estante {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" -msgstr "Estantería" +msgstr "Estantería de almacenaje" #: src/components/places/furniture.tsx msgid "Template" @@ -29300,3 +29301,8 @@ msgstr "Unidad del medidor cambiada" #: src/map/labels.ts msgid "Water purifier" msgstr "Purificador de agua" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Estantes" diff --git a/apps/web/src/locales/fr/messages.po b/apps/web/src/locales/fr/messages.po index 9474fac2..2c8e2211 100644 --- a/apps/web/src/locales/fr/messages.po +++ b/apps/web/src/locales/fr/messages.po @@ -27946,6 +27946,7 @@ msgid "Shelf {n}" msgstr "Étagère {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "Rayonnage" @@ -29300,3 +29301,8 @@ msgstr "Unité du compteur modifiée" #: src/map/labels.ts msgid "Water purifier" msgstr "Purificateur d’eau" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Tablettes" diff --git a/apps/web/src/locales/it/messages.po b/apps/web/src/locales/it/messages.po index 94c71809..dbf94a86 100644 --- a/apps/web/src/locales/it/messages.po +++ b/apps/web/src/locales/it/messages.po @@ -27946,6 +27946,7 @@ msgid "Shelf {n}" msgstr "Ripiano {n}" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "Scaffale" @@ -29300,3 +29301,8 @@ msgstr "Unità del contatore cambiata" #: src/map/labels.ts msgid "Water purifier" msgstr "Depuratore d’acqua" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Ripiani" diff --git a/apps/web/src/locales/uk/messages.po b/apps/web/src/locales/uk/messages.po index 9fdd3915..f932916d 100644 --- a/apps/web/src/locales/uk/messages.po +++ b/apps/web/src/locales/uk/messages.po @@ -24284,6 +24284,7 @@ msgid "Shelves" msgstr "Полиці" #: src/components/places/furniture.tsx +#: src/map/labels.ts msgid "Shelving unit" msgstr "Стелаж" @@ -31283,3 +31284,8 @@ msgstr "Одиницю лічильника змінено" #: src/map/labels.ts msgid "Water purifier" msgstr "Очищувач води" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Полиці" diff --git a/apps/web/src/locales/vi/messages.po b/apps/web/src/locales/vi/messages.po index ea8a159a..b482ffa7 100644 --- a/apps/web/src/locales/vi/messages.po +++ b/apps/web/src/locales/vi/messages.po @@ -21349,3 +21349,8 @@ msgstr "Đã đổi đơn vị đồng hồ đo" #: src/map/labels.ts msgid "Water purifier" msgstr "Máy lọc nước" + +#: src/map/size.tsx +msgctxt "how many shelves" +msgid "Shelves" +msgstr "Số tầng kệ" diff --git a/apps/web/src/map/compartments.test.ts b/apps/web/src/map/compartments.test.ts index 51512361..e8dc6157 100644 --- a/apps/web/src/map/compartments.test.ts +++ b/apps/web/src/map/compartments.test.ts @@ -45,6 +45,44 @@ describe('compartment boxes', () => { expect(one.y).toBeGreaterThan(six.y); }); + it('uses a shelving unit’s shelves, the first the top one (#485)', () => { + const key = 'shelving_unit_120x60x200_4'; + const boxes = compartmentBoxes(key, { columns: 1, rows: 4 }, cells(1, 4)); + const bottomOf = (id: string) => { + const b = boxes.get(id) as { y: number; h: number }; + return cm(b.y - b.h / 2); + }; + // The bottom shelf stands 8 cm up, 2 cm thick; the top one is flush with the posts. + expect(bottomOf('c4')).toBe(10); + expect(bottomOf('c1')).toBe(200); + expect(bottomOf('c2')).toBeGreaterThan(bottomOf('c3')); + // Shelves that aren't the furniture's are divided evenly instead: thirds of its 2 m. + const other = compartmentBoxes(key, { columns: 1, rows: 3 }, cells(1, 3)); + expect(other.size).toBe(3); + expect((other.get('c1') as { y: number }).y).toBeCloseTo(2 - 2 / 6, 3); + expect((other.get('c3') as { y: number }).y).toBeCloseTo(2 / 6, 3); + }); + + it('gives the built-in shelving unit’s 4 shelves the shape’s own, as it starts', () => { + const boxes = compartmentBoxes('shelving_unit', { columns: 1, rows: 4 }, cells(1, 4)); + const bottom = boxes.get('c4') as { y: number; h: number }; + const top = boxes.get('c1') as { y: number; h: number }; + expect(cm(bottom.y - bottom.h / 2)).toBe(10); + expect(cm(top.y - top.h / 2)).toBe(180); + }); + + it('keeps the top shelf’s compartment to 40 cm, however far apart the shelves', () => { + const boxes = compartmentBoxes( + 'shelving_unit_90x50x300_2', + { columns: 1, rows: 2 }, + cells(1, 2), + ); + const top = boxes.get('c1') as { h: number }; + const bottom = boxes.get('c2') as { h: number }; + expect(top.h).toBeLessThanOrEqual(0.4); + expect(bottom.h).toBeGreaterThan(2); + }); + it('divides any other shape evenly as it stands', () => { // The chest of drawers' model, 1.14 m wide and 1.88 m high, as 1 column of 9. const boxes = compartmentBoxes('drawers', { columns: 1, rows: 9 }, cells(1, 9)); diff --git a/apps/web/src/map/compartments.ts b/apps/web/src/map/compartments.ts index 7f59eb62..3088bc5c 100644 --- a/apps/web/src/map/compartments.ts +++ b/apps/web/src/map/compartments.ts @@ -4,9 +4,10 @@ * in the shape's own frame (engine/sized.ts): its corner at the origin, width along x, up along y, * depth along z, front towards +z; each box by its middle, metres. * - * A cube shelf or a bookcase drawn from its numbers (sized.ts) whose grid is the furniture's gives - * each compartment its own opening; any other shape is divided evenly, as the furniture stands, on - * its front: drawn shapes have no openings to follow. + * A cube shelf, a bookcase or a shelving unit drawn from its numbers (sized.ts) whose grid is the + * furniture's gives each compartment its own opening (a shelving unit's shelf, what stands on it); + * any other shape is divided evenly, as the furniture stands, on its front: drawn shapes have no + * openings to follow. */ import { type MapShapeKey, mapModel, sizedOf } from '@kept/shared'; import type { MapCompartment } from '@/api/map/types'; @@ -44,6 +45,10 @@ function cellBoxes(model: MapShapeKey, grid: Grid): (row: number, col: number) = const { spaces } = sizedParts(sized); return (row) => spaces[grid.rows - row] as Box; } + if (sized?.base === 'shelving_unit' && grid.columns === 1 && sized.shelves === grid.rows) { + const { spaces } = sizedParts(sized); + return (row) => spaces[grid.rows - row] as Box; + } const m = mapModel(model); const h = m.h ?? 1; const cw = m.w / grid.columns; diff --git a/apps/web/src/map/engine/draw.ts b/apps/web/src/map/engine/draw.ts index 4f4e53ef..bd30ab07 100644 --- a/apps/web/src/map/engine/draw.ts +++ b/apps/web/src/map/engine/draw.ts @@ -1446,17 +1446,30 @@ export function drawnModels(F: Materials): Record { } /** - * A sized shape (#240, #241) from its boards (sized.ts): a white cube shelf, open at the back, or - * an oak bookcase with books on each level but the top one. + * A sized shape (#240, #241, #485) from its boards (sized.ts): a white cube shelf, open at the + * back; an oak bookcase with books on each level but the top one; or a shelving unit, steel posts + * and pine shelves, with a fabric storage box on every other shelf. */ function sized(g: THREE.Group, s: SizedShape, F: Materials): void { const { boards, spaces } = sizedParts(s); const cubes = s.base === 'cube_grid'; for (const b of boards) { - const m = cubes ? F.white : b.part === 'back' ? F.oakDark : F.oak; + const m = cubes ? F.white : b.part === 'post' ? F.steel : b.part === 'back' ? F.oakDark : F.oak; rb(g, b.w, b.h, b.d, b.x, b.y, b.z, m, b.part === 'back' ? 0 : 0.003); } if (cubes) return; + if (s.base === 'shelving_unit') { + spaces.slice(0, -1).forEach((sp, i) => { + if (i % 2) return; + const bw = Math.min(0.4, sp.w * 0.45); + const bh = Math.min(0.28, sp.h - 0.03); + const bd = Math.min(0.4, sp.d - 0.06); + if (bh <= 0.04) return; + const x = sp.x - sp.w / 2 + 0.02 + bw / 2 + (i % 4 ? sp.w - bw - 0.04 : 0); + rb(g, bw, bh, bd, x, sp.y - sp.h / 2 + bh / 2, sp.z, F.fabricDark, 0.01); + }); + return; + } const books = [F.booksA, F.booksB, F.booksC, F.booksD, F.booksE, F.booksF]; spaces.slice(0, -1).forEach((sp, level) => { const x0 = sp.x - sp.w / 2; diff --git a/apps/web/src/map/engine/sized.test.ts b/apps/web/src/map/engine/sized.test.ts index 6323dbdd..5febfe6d 100644 --- a/apps/web/src/map/engine/sized.test.ts +++ b/apps/web/src/map/engine/sized.test.ts @@ -109,3 +109,45 @@ describe('a bookcase drawn with its levels (#241)', () => { expect(top.y + top.h / 2).toBeLessThan(1.8); }); }); + +describe('a shelving unit drawn with its shelves (#485)', () => { + it('stands 90 × 50 × 180 cm with 4 shelves at first, as its model', () => { + const { boards, spaces } = partsOf('shelving_unit'); + expect(bounds(boards)).toEqual({ w: 90, h: 180, d: 50, floor: 0 }); + expect(boards.filter((b) => b.part === 'shelf')).toHaveLength(4); + expect(spaces).toHaveLength(4); + }); + + it('gives the top shelf 40 cm at most, however far apart the shelves', () => { + const { spaces } = partsOf('shelving_unit_90x50x300_2'); + expect((spaces.at(-1) as Box).h).toBeLessThanOrEqual(0.4); + expect((spaces.at(0) as Box).h).toBeGreaterThan(2); + }); + + it.each([2, 4, 6, 12])( + 'stands %i shelves on four posts, the bottom off the floor and the top flush', + (shelves) => { + const key = sizedKey({ base: 'shelving_unit', w: 120, d: 60, h: 200, shelves }); + const { boards, spaces } = partsOf(key); + expect(bounds(boards)).toEqual({ w: 120, h: 200, d: 60, floor: 0 }); + // Open on every side: four posts the full height, no sides, back or plinth. + const posts = boards.filter((b) => b.part === 'post'); + expect(posts).toHaveLength(4); + for (const p of posts) expect(cm(p.h)).toBe(200); + expect(boards.filter((b) => b.part !== 'post' && b.part !== 'shelf')).toEqual([]); + const shelf = boards.filter((b) => b.part === 'shelf').sort((a, b) => a.y - b.y); + expect(shelf).toHaveLength(shelves); + expect(cm((shelf[0] as Box).y - (shelf[0] as Box).h / 2)).toBeGreaterThan(0); + const top = shelf[shelf.length - 1] as Box; + expect(cm(top.y + top.h / 2)).toBe(200); + // Evenly apart, a space on each shelf: the top one's above it, 40 cm at most. + const gaps = new Set(shelf.slice(1).map((b, i) => cm(b.y - (shelf[i] as Box).y))); + expect(gaps.size).toBe(1); + expect(spaces).toHaveLength(shelves); + for (const [i, s] of spaces.entries()) { + const board = shelf[i] as Box; + expect(cm(s.y - s.h / 2), `space ${i}`).toBe(cm(board.y + board.h / 2)); + } + }, + ); +}); diff --git a/apps/web/src/map/engine/sized.ts b/apps/web/src/map/engine/sized.ts index 442cf16a..62759b13 100644 --- a/apps/web/src/map/engine/sized.ts +++ b/apps/web/src/map/engine/sized.ts @@ -1,18 +1,23 @@ /** - * Sized shapes (#240, #241), as boards: a cube shelf from its grid and a bookcase from its size and - * levels, so the map draws whatever grid or size a node's model carries without a model of its - * own. Pure numbers (draw.ts turns them into boxes). Metres, in the shape's own frame: its corner - * at the origin, width along x, up along y, depth along z, front towards +z; each box by its - * middle. `spaces` are the openings between the boards: a cube each, or a level each. + * Sized shapes (#240, #241, #485), as boards: a cube shelf from its grid, a bookcase from its size + * and levels and a shelving unit from its size and shelves, so the map draws whatever grid or size + * a node's model carries without a model of its own. Pure numbers (draw.ts turns them into boxes). + * Metres, in the shape's own frame: its corner at the origin, width along x, up along y, depth + * along z, front towards +z; each box by its middle. `spaces` are the openings between the boards: + * a cube each, a level each, or what stands on each shelf. */ import { CUBE_GRID, cubeRunCm, type SizedShape } from '@kept/shared'; export type Box = { x: number; y: number; z: number; w: number; h: number; d: number }; -export type Board = Box & { part: 'side' | 'shelf' | 'divider' | 'back' | 'plinth' }; +export type Board = Box & { part: 'side' | 'shelf' | 'divider' | 'back' | 'plinth' | 'post' }; /** A bookcase's boards (metres): its sides, shelves and top, its back and its plinth. */ const CASE = { t: 0.018, back: 0.004, plinth: 0.06 } as const; +/** A shelving unit's parts (metres): its posts, its shelves, how high the bottom one stands and + * the most room the top one is given. */ +const RACKING = { post: 0.035, t: 0.02, bottom: 0.08, topRoom: 0.4 } as const; + const rect = (x0: number, y0: number, z0: number, w: number, h: number, d: number): Box => ({ x: x0 + w / 2, y: y0 + h / 2, @@ -73,7 +78,33 @@ function bookcase(wc: number, dc: number, hc: number, levels: number) { return { boards, spaces }; } +/** A shelving unit: four posts the full height and its shelves evenly between the bottom one, + * off the floor, and the top, flush with the posts; open on every side. What stands on a shelf + * is its space, up to the next one; the top shelf's is above the unit, 40 cm at most. */ +function shelvingUnit(wc: number, dc: number, hc: number, shelves: number) { + const { post, t, bottom, topRoom } = RACKING; + const w = wc / 100; + const d = dc / 100; + const h = hc / 100; + const boards: Board[] = []; + for (const x of [0, w - post]) + for (const z of [0, d - post]) boards.push(box('post', x, 0, z, post, h, post)); + const step = (h - t - bottom) / (shelves - 1); + const room = step - t; + const spaces: Box[] = []; + for (let i = 0; i < shelves; i++) { + const y = i === shelves - 1 ? h - t : bottom + i * step; + boards.push(box('shelf', 0, y, 0, w, t, d)); + spaces.push( + rect(post, y + t, 0, w - 2 * post, i < shelves - 1 ? room : Math.min(room, topRoom), d), + ); + } + return { boards, spaces }; +} + /** A sized shape's boards and the spaces between them. */ export function sizedParts(s: SizedShape): { boards: Board[]; spaces: Box[] } { - return s.base === 'cube_grid' ? cubeGrid(s.across, s.up) : bookcase(s.w, s.d, s.h, s.levels); + if (s.base === 'cube_grid') return cubeGrid(s.across, s.up); + if (s.base === 'shelving_unit') return shelvingUnit(s.w, s.d, s.h, s.shelves); + return bookcase(s.w, s.d, s.h, s.levels); } diff --git a/apps/web/src/map/labels.ts b/apps/web/src/map/labels.ts index 08f2539c..e96ab630 100644 --- a/apps/web/src/map/labels.ts +++ b/apps/web/src/map/labels.ts @@ -37,6 +37,7 @@ export function useModelNames(): Record { bookshelf: t`Bookshelf`, cube_grid: t`Cube storage`, bookcase: t`Tall bookcase`, + shelving_unit: t`Shelving unit`, lamp: t`Floor lamp`, counter: t`Kitchen counter`, sink: t`Sink counter`, diff --git a/apps/web/src/map/size.test.tsx b/apps/web/src/map/size.test.tsx index 95afc09c..f241db13 100644 --- a/apps/web/src/map/size.test.tsx +++ b/apps/web/src/map/size.test.tsx @@ -136,6 +136,36 @@ describe('SizeFields: a bookcase (#241)', () => { }); }); +describe('SizeFields: a shelving unit (#485)', () => { + it('shows its size in centimetres and its shelves, 90 × 50 × 180 with 4 at first', async () => { + await renderUI(); + expect(value('Width')).toBe('90 cm'); + expect(value('Depth')).toBe('50 cm'); + expect(value('Height')).toBe('180 cm'); + expect(value('Shelves')).toBe('4'); + expect(screen.queryByRole('textbox', { name: 'Levels' })).toBeNull(); + expectLogicalOnly(); + }); + + it('changes its size and its shelves, two at least and what its height fits', async () => { + const onModel = vi.fn(); + const { user } = await renderUI(); + const depth = screen.getByRole('textbox', { name: 'Depth' }); + await user.clear(depth); + await user.type(depth, '80{Enter}'); + expect(onModel).toHaveBeenLastCalledWith('shelving_unit_90x80x180_4'); + const shelves = screen.getByRole('textbox', { name: 'Shelves' }); + await user.clear(shelves); + await user.type(shelves, '1{Enter}'); + expect(onModel).toHaveBeenLastCalledWith('shelving_unit_90x50x180_2'); + // A 60 cm high one holds four shelves at most. + const height = screen.getByRole('textbox', { name: 'Height' }); + await user.clear(height); + await user.type(height, '60{Enter}'); + expect(onModel).toHaveBeenLastCalledWith('shelving_unit_90x50x60_4'); + }); +}); + // #385: in imperial a bookcase is sized in whole inches and a cube shelf's outside size in inches, // kept in centimetres as before. function inImperial(ui: ReactElement) { diff --git a/apps/web/src/map/size.tsx b/apps/web/src/map/size.tsx index a60f6127..32760752 100644 --- a/apps/web/src/map/size.tsx +++ b/apps/web/src/map/size.tsx @@ -1,13 +1,14 @@ /** - * Size (#240, #241): a sized shape's numbers on its card while arranging. A cube shelf by its cubes - * across and up, with its outside size, and Turn on its side (lying down is the same unit with the - * two swapped); a bookcase by its width, depth and height in centimetres and its levels, kept to - * what its height fits. Fields like Height: arrow keys, typing and the − and + buttons, what is - * typed read by @kept/shared's `readLength`, digits only (Western and Eastern Arabic both; what - * isn't a whole number said so, never another size). Each change is a new model for the shape - * (sizedKey), so the map draws it at its new size and saves it with the arrangement. Digits follow - * the person's setting (D143). In imperial (#385) the sizes are whole inches, an inch a step, kept - * as whole centimetres as before; feet and inches or centimetres typed with their unit read too. + * Size (#240, #241, #485): a sized shape's numbers on its card while arranging. A cube shelf by its + * cubes across and up, with its outside size, and Turn on its side (lying down is the same unit + * with the two swapped); a bookcase by its width, depth and height in centimetres and its levels, + * and a shelving unit the same with its shelves, each count kept to what its height fits. Fields + * like Height: arrow keys, typing and the − and + buttons, what is typed read by @kept/shared's + * `readLength`, digits only (Western and Eastern Arabic both; what isn't a whole number said so, + * never another size). Each change is a new model for the shape (sizedKey), so the map draws it at + * its new size and saves it with the arrangement. Digits follow the person's setting (D143). In + * imperial (#385) the sizes are whole inches, an inch a step, kept as whole centimetres as before; + * feet and inches or centimetres typed with their unit read too. */ import { BOOKCASE, @@ -20,7 +21,9 @@ import { type MapShapeKey, readLength, readSpan, + SHELVING_UNIT, type SizedShape, + shelvingMaxShelves, sizedKey, sizedOf, } from '@kept/shared'; @@ -179,6 +182,26 @@ export function SizeFields({ ); } else { + const lim = s.base === 'bookcase' ? BOOKCASE : SHELVING_UNIT; + // Its count (a bookcase's levels, a shelving unit's shelves), kept to what a height fits. + const count = + s.base === 'bookcase' + ? { + label: t`Levels`, + value: s.levels, + min: BOOKCASE.levels[0], + max: bookcaseMaxLevels(s.h), + onChange: (levels: number) => set({ ...s, levels }), + fit: (h: number) => ({ ...s, h, levels: Math.min(s.levels, bookcaseMaxLevels(h)) }), + } + : { + label: t({ message: 'Shelves', context: 'how many shelves' }), + value: s.shelves, + min: SHELVING_UNIT.shelves[0], + max: shelvingMaxShelves(s.h), + onChange: (shelves: number) => set({ ...s, shelves }), + fit: (h: number) => ({ ...s, h, shelves: Math.min(s.shelves, shelvingMaxShelves(h)) }), + }; body = ( <> set({ ...s, w })} /> set({ ...s, d })} /> set({ ...s, h, levels: Math.min(s.levels, bookcaseMaxLevels(h)) })} + min={lim.h[0]} + max={lim.h[1]} + onChange={(h) => set(count.fit(h))} /> set({ ...s, levels })} + label={count.label} + value={count.value} + min={count.min} + max={count.max} + onChange={count.onChange} /> ); diff --git a/apps/web/src/map/thumbs/shapes.webp b/apps/web/src/map/thumbs/shapes.webp index f1e2b1ce..601fc976 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 a63ca20f..9e2ac8a0 100644 --- a/apps/web/src/map/thumbs/sprite.ts +++ b/apps/web/src/map/thumbs/sprite.ts @@ -6,7 +6,7 @@ import type { MapModelKey } from '@kept/shared'; export const THUMB_CELL = 88; export const THUMB_COLS = 8; -export const THUMB_ROWS = 12; +export const THUMB_ROWS = 13; /** Each shape's cell, row by row. */ export const THUMB_CELLS: Readonly> = { @@ -37,75 +37,76 @@ export const THUMB_CELLS: Readonly> = { bookshelf: 24, cube_grid: 25, bookcase: 26, - lamp: 27, - counter: 28, - sink: 29, - fridge: 30, - coffee_machine: 31, - microwave: 32, - dining: 33, - bed: 34, - wardrobe: 35, - table_lamp: 36, - desk: 37, - monitor: 38, - laptop: 39, - monitor_arm: 40, - dual_monitor_arm: 41, - pc_tower: 42, - keyboard: 43, - mouse: 44, - webcam: 45, - camera: 46, - speaker: 47, - portable_speaker: 48, - floor_speaker: 49, - boombox: 50, - smart_speaker: 51, - soundbar: 52, - headphones: 53, - headset: 54, - earbuds: 55, - router: 56, - printer: 57, - console: 58, - controller: 59, - phone: 60, - tablet: 61, - power_strip: 62, - external_drive: 63, - mini_pc: 64, - nas: 65, - sbc: 66, - network_switch: 67, - patch_panel: 68, - ups: 69, - device: 70, - chair: 71, - cabinet: 72, - rack_24u: 73, - rack_42u: 74, - box: 75, - washer: 76, - 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, + shelving_unit: 27, + lamp: 28, + counter: 29, + sink: 30, + fridge: 31, + coffee_machine: 32, + microwave: 33, + dining: 34, + bed: 35, + wardrobe: 36, + table_lamp: 37, + desk: 38, + monitor: 39, + laptop: 40, + monitor_arm: 41, + dual_monitor_arm: 42, + pc_tower: 43, + keyboard: 44, + mouse: 45, + webcam: 46, + camera: 47, + speaker: 48, + portable_speaker: 49, + floor_speaker: 50, + boombox: 51, + smart_speaker: 52, + soundbar: 53, + headphones: 54, + headset: 55, + earbuds: 56, + router: 57, + printer: 58, + console: 59, + controller: 60, + phone: 61, + tablet: 62, + power_strip: 63, + external_drive: 64, + mini_pc: 65, + nas: 66, + sbc: 67, + network_switch: 68, + patch_panel: 69, + ups: 70, + device: 71, + chair: 72, + cabinet: 73, + rack_24u: 74, + rack_42u: 75, + box: 76, + washer: 77, + water_purifier: 78, + workbench: 79, + tool_wall: 80, + wall_tv: 81, + wall_shelf: 82, + wall_cabinet: 83, + rack_wall_6u: 84, + rack_wall_12u: 85, + picture: 86, + clock: 87, + hooks: 88, + car: 89, + bike: 90, + water_tank: 91, + dish: 92, + stairs: 93, + stairwell: 94, + zone: 95, + item: 96, }; /** Each shape's height as drawn, in centimetres, from its rendered bounds. */ @@ -137,6 +138,7 @@ export const THUMB_HEIGHTS_CM: Readonly> = { bookshelf: 200, cube_grid: 77, bookcase: 202, + shelving_unit: 180, lamp: 165, counter: 92, sink: 129.2, diff --git a/packages/shared/src/map-shapes.test.ts b/packages/shared/src/map-shapes.test.ts index d92d9c4a..ae9a3be2 100644 --- a/packages/shared/src/map-shapes.test.ts +++ b/packages/shared/src/map-shapes.test.ts @@ -1099,3 +1099,42 @@ describe('a water purifier standing on the floor (#443)', () => { expect(m.wall).toBeUndefined(); }); }); + +describe('a shelving unit sized by its numbers (#485)', () => { + it.each([ + ['Shelving unit', 'shelving_unit'], + ['Storage shelf', 'shelving_unit'], + ['Garage shelving', 'shelving_unit'], + ['Pantry shelf', 'shelving_unit'], + ['Étagère de rangement', 'shelving_unit'], + ['Lagerregal', 'shelving_unit'], + ['Scaffale da garage', 'shelving_unit'], + ['Estantería de almacenaje', 'shelving_unit'], + ['Стелаж для зберігання', 'shelving_unit'], + ['Kệ để đồ', 'shelving_unit'], + ['وحدة رفوف', 'shelving_unit'], + ] 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('suggests it beside the metal shelf for plain shelving, giving neither by itself', () => { + const s = suggestShapes({ name: 'Shelving' }); + expect(s.confident).toBe(false); + expect(s.shapes.map((x) => x.model)).toEqual( + expect.arrayContaining(['metal_shelf', 'shelving_unit']), + ); + // A plain shelf or rack stays the metal shelf. + expect(first({ name: 'Rack' })).toBe('metal_shelf'); + expect(first({ name: 'Shelf' })).toBe('metal_shelf'); + }); + + it('finds it by search in each language, with storage', () => { + expect(searchShapes('shelving u')[0]).toBe('shelving_unit'); + expect(searchShapes('lagerreg', {}, 'de')[0]).toBe('shelving_unit'); + expect(SHAPE_GROUP.shelving_unit).toBe('storage'); + for (const lang of SHAPE_LANGS) + expect(shapeWordsByLang('shelving_unit')[lang].length, lang).toBeGreaterThan(0); + }); +}); diff --git a/packages/shared/src/map-shapes.ts b/packages/shared/src/map-shapes.ts index 8276c0ec..9e0f2882 100644 --- a/packages/shared/src/map-shapes.ts +++ b/packages/shared/src/map-shapes.ts @@ -63,6 +63,7 @@ export const SHAPE_GROUP: Readonly> = { bookshelf: 'storage', cube_grid: 'storage', bookcase: 'storage', + shelving_unit: 'storage', lamp: 'furniture', counter: 'kitchen', sink: 'kitchen', @@ -741,6 +742,49 @@ export const SHAPE_WORDS: Readonly> = { 'estantería alta', 'estantería grande', ], + /** #485: a shelving unit's words; plain "shelving" is the metal shelf's too. */ + shelving_unit: [ + 'shelving', + 'shelving unit', + 'storage shelf', + 'storage shelves', + 'storage shelving', + 'storage rack', + 'garage shelf', + 'garage shelving', + 'pantry shelf', + 'pantry shelving', + 'boltless shelving', + 'وحدة رفوف', + 'رفوف تخزين', + 'رف تخزين', + 'رفوف مخزن', + 'étagère de rangement', + 'rayonnage', + 'étagère de garage', + 'étagère de cave', + 'lagerregal', + 'kellerregal', + 'garagenregal', + 'schwerlastregal', + 'steckregal', + 'vorratsregal', + 'scaffale da garage', + 'scaffale portaoggetti', + 'scaffalatura', + 'scaffale da cantina', + 'scaffale dispensa', + 'kệ để đồ', + 'kệ chứa đồ', + 'kệ kho hàng', + 'стелаж для зберігання', + 'господарський стелаж', + 'стелаж для гаража', + 'estantería de almacenaje', + 'estantería de almacenamiento', + 'estantería de garaje', + 'estantería de despensa', + ], lamp: [ 'lamp', 'floor lamp', @@ -2389,6 +2433,12 @@ export const SHAPE_WORD_LANGS: Readonly { expect(sizedOf('bookshelf')).toBeNull(); }); }); + +describe('a shelving unit sized by its width, depth, height and shelves (#485)', () => { + const size = (key: Parameters[0]) => { + const m = mapModel(key); + return [m.w, m.d, m.h ?? 0].map((v) => Math.round(v * 100)); + }; + + it('starts at 90 × 50 × 180 cm with 4 shelves, and takes the request’s sizes', () => { + expect(SHELVING_UNIT.start).toEqual({ w: 90, d: 50, h: 180, shelves: 4 }); + expect(size('shelving_unit')).toEqual([90, 50, 180]); + expect(sizedOf('shelving_unit')).toEqual({ + base: 'shelving_unit', + w: 90, + d: 50, + h: 180, + shelves: 4, + }); + const garage = sizedKey({ base: 'shelving_unit', w: 120, d: 60, h: 200, shelves: 4 }); + expect(garage).toBe('shelving_unit_120x60x200_4'); + expect(size(garage)).toEqual([120, 60, 200]); + expect(baseModel(garage)).toBe('shelving_unit'); + expect(mapModel(garage)).toMatchObject({ kind: 'storage', holds: true }); + expect(carryOf(garage)).toBeNull(); + }); + + it('keeps one spelling per size, the first bare, within the database’s check', () => { + expect(sizedKey({ base: 'shelving_unit', ...SHELVING_UNIT.start })).toBe('shelving_unit'); + for (const key of [ + 'shelving_unit', + 'shelving_unit_60x30x120_3', + 'shelving_unit_240x80x300_12', + ]) { + const s = sizedOf(key); + expect(s, key).not.toBeNull(); + if (s) expect(sizedKey(s)).toBe(key); + expect(isMapModel(key), key).toBe(true); + expect(key).toMatch(/^[a-z][a-z0-9_]{0,39}$/); + } + for (const key of [ + 'shelving_unit_90x50x180_4', // the first size is spelled bare + 'shelving_unit_90x50x180_1', // two shelves at least: the bottom one and the top + 'shelving_unit_90x50x40_3', // three shelves in 40 cm + 'shelving_unit_29x50x180_4', + 'shelving_unit_90x81x180_4', + 'shelving_unit_90x50x301_4', + 'shelving_unit_090x50x180_4', + 'shelving_unit_90x50', + 'shelving_unit_90x50x180', + ]) + expect(isMapModel(key), key).toBe(false); + expect(shelvingMaxShelves(180)).toBe(12); + expect(shelvingMaxShelves(40)).toBe(2); + expect(shelvingMaxShelves(60)).toBe(4); + }); + + it('leaves the bookcase and the metal shelf as they were', () => { + expect(sizedOf('bookcase_90x50x180_5')).toEqual({ + base: 'bookcase', + w: 90, + d: 50, + h: 180, + levels: 5, + }); + expect(sizedOf('metal_shelf')).toBeNull(); + expect(mapModel('metal_shelf')).toMatchObject({ glb: 'steel_frame_shelves_02' }); + }); +}); diff --git a/packages/shared/src/map.ts b/packages/shared/src/map.ts index 1fcf7cfd..885c9324 100644 --- a/packages/shared/src/map.ts +++ b/packages/shared/src/map.ts @@ -22,9 +22,9 @@ * to the wall it faces away from, at a height: `wall` is where its middle hangs until someone * says otherwise. Which wall comes from its turn (wallOfTurn); the layout stores the height. * - * Sized shapes (#240, #241): a cube shelf and a bookcase are drawn from their numbers, which a - * node's stored model carries after the shape's key (SizedModelKey). Every other model has one - * size, its catalogue entry's. + * Sized shapes (#240, #241, #485): a cube shelf, a bookcase and a shelving unit are drawn from + * their numbers, which a node's stored model carries after the shape's key (SizedModelKey). Every + * other model has one size, its catalogue entry's. */ import { ROOM_SIDE_CM } from './length.js'; @@ -220,6 +220,9 @@ export const MAP_MODELS = { cube_grid: { w: 0.77, d: 0.39, h: 0.77, kind: 'storage', top: 0.77, holds: true }, /** A bookcase drawn from its size, 80 × 28 × 202 cm, with 6 levels. */ bookcase: { w: 0.8, d: 0.28, h: 2.02, kind: 'storage', holds: true }, + /** A shelving unit drawn from its size (#485): four posts and its shelves, open on every side; + * 90 × 50 × 180 cm with 4 shelves, as the built-in shelving unit template has. */ + shelving_unit: { w: 0.9, d: 0.5, h: 1.8, kind: 'storage', holds: true }, lamp: { w: 0.4, d: 0.4, kind: 'thing' }, counter: { w: 1.8, d: 0.62, kind: 'furniture', top: 0.92, holds: true }, sink: { w: 1.2, d: 0.62, kind: 'furniture', top: 0.92, holds: true }, @@ -364,25 +367,30 @@ export const MAP_MODELS = { export type MapModelKey = keyof typeof MAP_MODELS; export const MAP_MODEL_KEYS = Object.keys(MAP_MODELS) as MapModelKey[]; -// ---- Sized shapes (#240, #241) ----------------------------------------------------------------- +// ---- Sized shapes (#240, #241, #485) ----------------------------------------------------------- /** * A cube shelf's grid, `across` × `up` cubes (lying down is the same unit with the numbers * swapped: 1 × 4 standing, 4 × 1 lying); a bookcase's width, depth and height in centimetres and - * its levels, the spaces things stand in, the bottom one included. + * its levels, the spaces things stand in, the bottom one included; a shelving unit's width, depth + * and height in centimetres and its shelves, the boards things stand on, from the bottom one to + * the top. */ export type SizedShape = | { base: 'cube_grid'; across: number; up: number } - | { base: 'bookcase'; w: number; d: number; h: number; levels: number }; + | { base: 'bookcase'; w: number; d: number; h: number; levels: number } + | { base: 'shelving_unit'; w: number; d: number; h: number; shelves: number }; /** * A sized shape as a node stores it, its numbers after its key: `cube_grid_3x2` (3 across, 2 up), - * `bookcase_60x28x106_3` (centimetres, then levels). Its first size is the bare key. One spelling - * per size, so the layout's format, the API and the database's check stay as they were. + * `bookcase_60x28x106_3` (centimetres, then levels), `shelving_unit_120x60x200_4` (centimetres, + * then shelves). Its first size is the bare key. One spelling per size, so the layout's format, + * the API and the database's check stay as they were. */ export type SizedModelKey = | `cube_grid_${number}x${number}` - | `bookcase_${number}x${number}x${number}_${number}`; + | `bookcase_${number}x${number}x${number}_${number}` + | `shelving_unit_${number}x${number}x${number}_${number}`; /** What a node's model may be: a catalogue shape, or a sized shape with its numbers. */ export type MapShapeKey = MapModelKey | SizedModelKey; @@ -407,6 +415,16 @@ export const BOOKCASE = { start: { w: 80, d: 28, h: 202, levels: 6 }, } as const; +/** A shelving unit's limits in centimetres (#485), and its first size: two shelves at least, + * the bottom one and the top. */ +export const SHELVING_UNIT = { + w: [30, 240], + d: [15, 80], + h: [40, 300], + shelves: [2, 12], + start: { w: 90, d: 50, h: 180, shelves: 4 }, +} as const; + /** A cube shelf's side in centimetres for `n` cubes. */ export const cubeRunCm = (n: number): number => 2 * CUBE_GRID.frame + n * CUBE_GRID.cube + (n - 1) * CUBE_GRID.divider; @@ -415,6 +433,10 @@ export const cubeRunCm = (n: number): number => export const bookcaseMaxLevels = (h: number): number => Math.max(BOOKCASE.levels[0], Math.min(BOOKCASE.levels[1], Math.floor(h / 15))); +/** How many shelves a shelving unit this high (cm) can have: one every 15 cm at most. */ +export const shelvingMaxShelves = (h: number): number => + Math.max(SHELVING_UNIT.shelves[0], Math.min(SHELVING_UNIT.shelves[1], Math.floor(h / 15))); + const within = (v: number, [lo, hi]: readonly [number, number]) => Number.isInteger(v) && v >= lo && v <= hi; @@ -422,6 +444,13 @@ const within = (v: number, [lo, hi]: readonly [number, number]) => function sizedOk(s: SizedShape): boolean { if (s.base === 'cube_grid') return within(s.across, [1, CUBE_GRID.max]) && within(s.up, [1, CUBE_GRID.max]); + if (s.base === 'shelving_unit') + return ( + within(s.w, SHELVING_UNIT.w) && + within(s.d, SHELVING_UNIT.d) && + within(s.h, SHELVING_UNIT.h) && + within(s.shelves, [SHELVING_UNIT.shelves[0], shelvingMaxShelves(s.h)]) + ); return ( within(s.w, BOOKCASE.w) && within(s.d, BOOKCASE.d) && @@ -433,6 +462,10 @@ function sizedOk(s: SizedShape): boolean { const isStart = (s: SizedShape): boolean => { if (s.base === 'cube_grid') return s.across === CUBE_GRID.start.across && s.up === CUBE_GRID.start.up; + if (s.base === 'shelving_unit') { + const u = SHELVING_UNIT.start; + return s.w === u.w && s.d === u.d && s.h === u.h && s.shelves === u.shelves; + } const a = BOOKCASE.start; return s.w === a.w && s.d === a.d && s.h === a.h && s.levels === a.levels; }; @@ -440,28 +473,31 @@ const isStart = (s: SizedShape): boolean => { /** A sized shape's stored model: its bare key at its first size. */ export function sizedKey(s: SizedShape): MapShapeKey { if (isStart(s)) return s.base; - return s.base === 'cube_grid' - ? `cube_grid_${s.across}x${s.up}` - : `bookcase_${s.w}x${s.d}x${s.h}_${s.levels}`; + if (s.base === 'cube_grid') return `cube_grid_${s.across}x${s.up}`; + if (s.base === 'shelving_unit') return `shelving_unit_${s.w}x${s.d}x${s.h}_${s.shelves}`; + return `bookcase_${s.w}x${s.d}x${s.h}_${s.levels}`; } const SIZED_KEY = - /^(cube_grid|bookcase)_([1-9][0-9]{0,2})x([1-9][0-9]{0,2})(?:x([1-9][0-9]{0,2})_([1-9][0-9]?))?$/; + /^(cube_grid|bookcase|shelving_unit)_([1-9][0-9]{0,2})x([1-9][0-9]{0,2})(?:x([1-9][0-9]{0,2})_([1-9][0-9]?))?$/; /** A sized shape's numbers from a stored model (a bare key gives its first size), or null for * any other model and for numbers out of range or spelled another way. */ export function sizedOf(key: string): SizedShape | null { if (key === 'cube_grid') return { base: 'cube_grid', ...CUBE_GRID.start }; if (key === 'bookcase') return { base: 'bookcase', ...BOOKCASE.start }; + if (key === 'shelving_unit') return { base: 'shelving_unit', ...SHELVING_UNIT.start }; const m = SIZED_KEY.exec(key); if (!m) return null; - const [, base, a, b, c, levels] = m; + const [, base, a, b, c, count] = m; const s: SizedShape | null = base === 'cube_grid' && c === undefined ? { base, across: Number(a), up: Number(b) } : base === 'bookcase' && c !== undefined - ? { base, w: Number(a), d: Number(b), h: Number(c), levels: Number(levels) } - : null; + ? { base, w: Number(a), d: Number(b), h: Number(c), levels: Number(count) } + : base === 'shelving_unit' && c !== undefined + ? { base, w: Number(a), d: Number(b), h: Number(c), shelves: Number(count) } + : null; return s && sizedOk(s) && sizedKey(s) === key ? s : null; } @@ -471,8 +507,8 @@ export const baseModel = (key: MapShapeKey): MapModelKey => /** A sized shape's model: its catalogue entry at its own size. */ function sizedModel(s: SizedShape): MapModel { - if (s.base === 'bookcase') - return { ...MAP_MODELS.bookcase, w: s.w / 100, d: s.d / 100, h: s.h / 100 }; + if (s.base === 'bookcase' || s.base === 'shelving_unit') + return { ...MAP_MODELS[s.base], w: s.w / 100, d: s.d / 100, h: s.h / 100 }; const h = cubeRunCm(s.up) / 100; return { ...MAP_MODELS.cube_grid, w: cubeRunCm(s.across) / 100, h, top: h }; }