@vargov/mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DATA-LICENSE.md +30 -0
- package/LICENSE +24 -0
- package/README.md +130 -2
- package/data/brand-facts.json +204 -0
- package/data/llms-configurator.txt +26 -0
- package/data/llms-en.txt +93 -0
- package/data/llms-ru.txt +94 -0
- package/data/manifest.json +61 -0
- package/data/open-skus.json +1 -0
- package/data/organization.jsonld +265 -0
- package/data/products.json +68278 -0
- package/data/sketchfab-models.json +1843 -0
- package/dist/bim.js +165 -0
- package/dist/brand.js +133 -0
- package/dist/catalog.js +329 -0
- package/dist/data.js +148 -0
- package/dist/index.js +16 -0
- package/dist/server.js +101 -0
- package/package.json +52 -4
package/dist/bim.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// how_to_get_bim: the steps and the contents of the IFC4 / FBX files.
|
|
2
|
+
// Wording follows the public sources of the brand, not this server's own ideas:
|
|
3
|
+
// - en/llms.txt and llms.txt of vargov-ai-kb (sections "For architects and specifiers", "New in October 2026");
|
|
4
|
+
// - knowledge/ifc4-in-the-model-essay.md (IfcDiscreteAccessory, IfcLightFixture/IfcFurnishingElement, GUIDs);
|
|
5
|
+
// - knowledge/whats-new-2026-10.md (FBX, production brief, 1:1 drawing);
|
|
6
|
+
// - docs/ssylka-i-tekst-dlya-tz.md of the configurator (link keys; the "Share" button).
|
|
7
|
+
import { findEntry, hasMountScheme, kindNotes, mountSchemeNote } from './catalog.js';
|
|
8
|
+
import { getData } from './data.js';
|
|
9
|
+
function statusEn(code, kind, configurable, page) {
|
|
10
|
+
if (!configurable) {
|
|
11
|
+
return (`## ${code}: calculated individually\n\n` +
|
|
12
|
+
`The configurator does not calculate ${code}, so there is no IFC4 export, no FBX export, no PDF specification from the configurator and no 1:1 ceiling drawing for it, and the production brief cannot be started for it (360 of the 605 compositions open with a full calculation; this one is not among them). ` +
|
|
13
|
+
`Product page: ${page}. For a calculation contact the brand: info@vargov.ru, WhatsApp +7 916 537 33 52, Telegram @AntonVargov.\n`);
|
|
14
|
+
}
|
|
15
|
+
const lines = [`## ${code}: opens in the configurator with a full calculation (${kind})`, ''];
|
|
16
|
+
lines.push(`IFC4 (BIM) and FBX (3ds Max) are exported by the visitor; the PDF specification and the tender sheet come from the same "Documents" drawer.`);
|
|
17
|
+
lines.push(hasMountScheme(kind) ? `1:1 ceiling drawing: yes, from the production brief after the brief is started in production (see below).` : `1:1 ceiling drawing: no. ${mountSchemeNote(kind) ?? ''}`);
|
|
18
|
+
for (const n of kindNotes(code, kind))
|
|
19
|
+
lines.push(n);
|
|
20
|
+
lines.push('');
|
|
21
|
+
return lines.join('\n') + '\n';
|
|
22
|
+
}
|
|
23
|
+
function statusRu(code, kind, configurable, page) {
|
|
24
|
+
if (!configurable) {
|
|
25
|
+
return (`## ${code}: рассчитывается индивидуально\n\n` +
|
|
26
|
+
`Конфигуратор не рассчитывает ${code}: у этой композиции нет выгрузки IFC4 и FBX, нет PDF-спецификации из конфигуратора и нет монтажной схемы 1:1, ТЗ на производство для неё не заводится (с полным расчётом открываются 360 композиций из 605; этой среди них нет). ` +
|
|
27
|
+
`Карточка: ${page}. За расчётом — к бренду: info@vargov.ru, WhatsApp +7 916 537 33 52, Telegram @AntonVargov.\n`);
|
|
28
|
+
}
|
|
29
|
+
const kindRu = { cloud: 'облако элементов', ribbon: 'лента', chain: 'цепочка', frame: 'композиция на каркасе', 'ready-scene': 'готовая сцена' };
|
|
30
|
+
const lines = [`## ${code}: открывается в конфигураторе с полным расчётом (${kindRu[kind ?? 'cloud'] ?? kind})`, ''];
|
|
31
|
+
lines.push('IFC4 (BIM) и FBX (3ds Max) посетитель выгружает сам; PDF-спецификация и тендерный лист — в том же ящике «Документы».');
|
|
32
|
+
if (hasMountScheme(kind))
|
|
33
|
+
lines.push('Монтажная схема 1:1: есть — со страницы ТЗ, после запуска ТЗ в производство (ниже).');
|
|
34
|
+
else if (kind === 'ribbon')
|
|
35
|
+
lines.push('Монтажной схемы 1:1 нет: у ленты нет облака элементов, устройство своё.');
|
|
36
|
+
else if (kind === 'frame')
|
|
37
|
+
lines.push('Монтажной схемы 1:1 нет: у элементов своих нитей к потолку нет — они висят на каркасе, а сам каркас подвешен к потолку; в IFC4 записано число подвесов каркаса (свойство SuspensionCount), но не их координаты, а сами точки видны на чертеже каркаса и на виде сбоку в конфигураторе.');
|
|
38
|
+
else if (kind === 'ready-scene')
|
|
39
|
+
lines.push('Монтажной схемы 1:1 нет: это настенное бра — композиция готовая, а не рассчитанная, раскладки нет, значит, нет и разметки потолка; точек крепления к потолку в IFC4 нет.');
|
|
40
|
+
if (kind === 'ribbon')
|
|
41
|
+
lines.push('Лента: длина и ширина плана и высота композиции читаются из ссылки; пластика ленты (волна/спираль) выбирается в конфигураторе; кнопка выгрузки FBX у ленты — в панели ленты.');
|
|
42
|
+
if (kind === 'chain')
|
|
43
|
+
lines.push('Цепочка: форма на сборку не влияет — её раскладывает свой движок.');
|
|
44
|
+
if (kind === 'frame')
|
|
45
|
+
lines.push('Каркас: число элементов конфигуратор считает по шагу и длине каркаса, руками его не ввести.');
|
|
46
|
+
if (kind === 'ready-scene')
|
|
47
|
+
lines.push('Готовая сцена: открывается как собственная 3D-модель; размеры плана в ссылке заменяются размером изделия; поля «Высота потолка» нет: в IFC4 модель ставится под потолок 2700 мм (значение по умолчанию), и в самом файле эта высота свойством не объявлена.');
|
|
48
|
+
if (code === 'LC0106')
|
|
49
|
+
lines.push('LC0106: вместо размеров плана конфигуратор берёт число медуз и просвет между ними (ключи jelly и jgap); w/l/h в ссылке для этой композиции не действуют.');
|
|
50
|
+
lines.push('');
|
|
51
|
+
return lines.join('\n') + '\n';
|
|
52
|
+
}
|
|
53
|
+
const GUIDE_EN = `# How to get the IFC4 (BIM) and FBX files of a Vargov®Design composition
|
|
54
|
+
|
|
55
|
+
**Scope.** The configurator (https://vargov.design, Russian copy https://configurator.vargov.ru) opens 360 of the 605 catalogue compositions with a full calculation: a 3D layout for the room, the ceiling fixing points, a PDF specification, a tender sheet, an IFC4 model for BIM and an FBX model for 3ds Max (verified 25 October 2026 against the configurator's open-skus.json and the catalogue dataset). The other compositions are calculated individually by the brand: they have no export and no 1:1 drawing. The site page of a composition that qualifies carries the line "IFC4 · PDF spec sheet · tender sheet — in the configurator".
|
|
56
|
+
|
|
57
|
+
## Steps
|
|
58
|
+
|
|
59
|
+
1. Open the configurator with the composition — a link from \`build_configurator_link\` already carries the plan dimensions, the ceiling height and the language (for example \`https://vargov.design/?sku=LC0599&w=1000&l=1000&ceil=3200&lang=en\`).
|
|
60
|
+
2. Set the plan (Length × Width — the zone the composition is laid out in), the height of the cloud of elements, pick element sizes and colours, and choose the form of the composition at the step "Composition form". The configurator lays the elements out in 3D and counts the ceiling fixing points.
|
|
61
|
+
3. Go to the step **"3D composition scheme"** and open the **"Documents"** drawer.
|
|
62
|
+
4. Set the **ceiling height** in the field above the IFC button (default 2,700 mm — the room height, not a product size). It goes into the IFC4 file (the model is built under that ceiling, the fixing points sit on its plane, property CeilingHeight) and into AR; it does not change the layout, the mass or the specification. The ready scene LC0406-1 has no such field: its IFC4 model is placed under a 2,700 mm ceiling (the default), and the file does not declare that height as a property.
|
|
63
|
+
5. Click **"Download IFC (BIM)"**. Right under it, click **"Download FBX (3ds Max)"**. A ribbon has its own export button, in the ribbon panel.
|
|
64
|
+
6. The **"Share"** button copies a link that reopens the very same calculation (the address bar of the configurator does not reflect the state — use the copied link). The configurator's **"Fill in the spec"** button carries the composition into the online production brief (see below).
|
|
65
|
+
|
|
66
|
+
## What is in the IFC4 file
|
|
67
|
+
|
|
68
|
+
- Schema IFC4. Lit elements are exported as \`IfcLightFixture\`, unlit ones as \`IfcFurnishingElement\` — the test is applied to the element, not to the article. Known exception: in LC0523 the light comes from backlight contours, so its elements go out as \`IfcFurnishingElement\`.
|
|
69
|
+
- Every point where the composition meets the ceiling is its own \`IfcDiscreteAccessory\` object in its own place, with the number of elements hanging from it and, where the mass is known, the estimated load on that point (a backlight-contour point carries a load of zero). Compositions on a frame hang from the frame, and the frame itself is suspended from the ceiling — the file records the number of frame suspensions (property SuspensionCount) but not their coordinates; the ready scene LC0406-1 is a wall sconce. Their files carry no ceiling fixing points (5 of the 360).
|
|
70
|
+
- The BIM properties — loads, mass, power, article codes — are in the IFC4 file (the FBX has none).
|
|
71
|
+
- What the IFC4 file does not contain: the cables and wires themselves (only the points where they reach the ceiling), the load-bearing frame, and the backlight contours.
|
|
72
|
+
- GUIDs are derived from the configuration: the same configuration always gives the same GUIDs, different ones give different GUIDs, so two compositions of one article combine in one federated model without clashes.
|
|
73
|
+
- The origin stays on the floor; the model is built for the stated ceiling height. On a non-Russian interface the names in the file are in English.
|
|
74
|
+
- There is no Revit family (.rfa) — the brand does not issue one. DXF drawings exist for the brand's production and dealers only.
|
|
75
|
+
|
|
76
|
+
## What is in the FBX file
|
|
77
|
+
|
|
78
|
+
- The composition assembled, as separate objects, in millimetres; the suspension strands as solid bodies; the ceiling fixings as separate objects (one per ceiling hole: two strands meeting in one hole share one fixing; a strand that runs from the frame rather than from the ceiling carries none); the ceiling plane. Unlike IFC4, the FBX also carries the frame and the metal base.
|
|
79
|
+
- The origin is the ceiling and the composition hangs down (IFC4 keeps the origin on the floor).
|
|
80
|
+
- Colour and opacity carry over; there are no textures and no UVs; identical elements are copies of one geometry.
|
|
81
|
+
- No BIM properties: loads, mass, power and article codes stay in the IFC4 file.
|
|
82
|
+
- Plain import: the scale is declared inside the file, so a 1,500 mm composition opens as 1,500 mm whatever the system units are, and the importer itself turns it to the Z-up that 3ds Max expects.
|
|
83
|
+
|
|
84
|
+
## 1:1 ceiling setting-out drawing (PDF)
|
|
85
|
+
|
|
86
|
+
- Comes from the online production brief, https://vargov.ru/en/specifications (Russian and English only): no sign-in and no registration, only the "Object" field is required; a saved brief opens by its own link with a key. Available right after saving: the "3D configuration" page, the brief as a PDF in Russian and English, and the FBX export. The brief carries no price — it is a production brief, not a quote and not an order.
|
|
87
|
+
- The 1:1 drawing **unlocks only after a manager starts the brief in production**; until then the button is inactive. It is preliminary: before the ceiling is set out it is confirmed by the production engineer, and that note is printed on the sheets.
|
|
88
|
+
- Four sheets — plotter and A4, each for the ceiling and for the floor; print at exactly 100 % (a 100 mm segment on the sheet is there to check with a ruler); the plotter sheet runs in one piece up to 5 m and is split beyond that.
|
|
89
|
+
- Exists for 345 of the 360 compositions: not for the 10 ribbons, not for the 4 compositions on a frame, not for the ready scene LC0406-1. Not issued to the client when the brief specifies mounting on the metal base: the base arrives with its holes and fixings already in place (with that mounting the brief offers one power option, 12 V through a driver).
|
|
90
|
+
|
|
91
|
+
## Pages
|
|
92
|
+
|
|
93
|
+
- IFC4 export: https://vargov.ru/en/configurator/ifc4 · FBX export: https://vargov.ru/en/configurator/fbx
|
|
94
|
+
- How many ceiling mounting points a fitting needs: https://vargov.ru/en/guides/mounting-points
|
|
95
|
+
- Essay "The light fitting that never arrives in the model": https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/knowledge/ifc4-in-the-model-essay.md
|
|
96
|
+
- Installation and wiring guides (PDF, eight languages): https://vargov.ru/en/installation
|
|
97
|
+
`;
|
|
98
|
+
const GUIDE_RU = `# Как получить IFC4 (BIM) и FBX композиции Vargov®Design
|
|
99
|
+
|
|
100
|
+
**Круг.** Конфигуратор (https://configurator.vargov.ru, международная площадка https://vargov.design) открывает с полным расчётом 360 композиций каталога из 605: 3D-раскладка под помещение, точки крепления к потолку, PDF-спецификация, тендерный лист, модель IFC4 для BIM и модель FBX для 3ds Max (проверено 25.10.2026 прогоном open-skus.json конфигуратора и датасета каталога). Остальные композиции бренд рассчитывает индивидуально: выгрузки и схемы 1:1 у них нет. На карточке композиции, которая открывается с расчётом, стоит строка «IFC4 · PDF-спецификация · тендерный лист — в конфигураторе».
|
|
101
|
+
|
|
102
|
+
## Шаги
|
|
103
|
+
|
|
104
|
+
1. Откройте конфигуратор с композицией — ссылка из \`build_configurator_link\` уже несёт размеры плана, высоту потолка и язык (например \`https://configurator.vargov.ru/?sku=LC0599&w=1000&l=1000&ceil=3200&lang=ru\`).
|
|
105
|
+
2. Задайте план (Длина × Ширина — зона, в которой раскладывается композиция), высоту облака элементов, размеры и цвета элементов и выберите форму на шаге «Форма композиции». Конфигуратор раскладывает элементы в 3D и считает точки крепления к потолку.
|
|
106
|
+
3. Перейдите на шаг **«3D схема композиции»** и откройте ящик **«Документы»**.
|
|
107
|
+
4. Укажите **высоту потолка** в поле над кнопкой IFC (по умолчанию 2700 мм — высота помещения, не размер изделия). Она уходит в IFC4 (сборка под потолок, точки крепления на его плоскости, свойство CeilingHeight) и в AR; на раскладку, массу и спецификацию не влияет. У готовой сцены LC0406-1 этого поля нет: в IFC4 она ставится под потолок 2700 мм (значение по умолчанию), и в самом файле эта высота свойством не объявлена.
|
|
108
|
+
5. Нажмите **«Скачать IFC (BIM)»**. Сразу под ней — **«Скачать FBX (3ds Max)»**. У ленты своя кнопка выгрузки — в панели ленты.
|
|
109
|
+
6. Кнопка **«Поделиться»** копирует ссылку, которая открывает тот же расчёт (адресная строка конфигуратора состояние не отражает — берите скопированную ссылку). Кнопка **«Заполнить ТЗ»** переносит композицию в онлайн-ТЗ на производство (ниже).
|
|
110
|
+
|
|
111
|
+
## Что лежит в файле IFC4
|
|
112
|
+
|
|
113
|
+
- Схема IFC4. Светящиеся элементы выгружаются классом \`IfcLightFixture\`, несветящиеся — \`IfcFurnishingElement\`; признак берётся по элементу, а не по артикулу. Известное исключение: у LC0523 свет дают контуры подсветки, поэтому её элементы уходят классом \`IfcFurnishingElement\`.
|
|
114
|
+
- Каждая точка, где композиция встречается с потолком, — свой объект \`IfcDiscreteAccessory\` на своём месте; на нём записано число элементов, которые на нём висят, и, когда масса известна, расчётная нагрузка на эту точку (у точки контура подсветки нагрузка нулевая). Композиции на каркасе висят на каркасе, а сам каркас подвешен к потолку — в файл уходит число подвесов каркаса (свойство SuspensionCount), но не их координаты; готовая сцена LC0406-1 — настенное бра. Точек крепления к потолку в их файлах нет (5 артикулов из 360).
|
|
115
|
+
- Свойства BIM — нагрузки, масса, мощность, коды артикулов — лежат в IFC4 (в FBX их нет).
|
|
116
|
+
- Чего в IFC4 нет: самих тросов и проводов (только точки, где они приходят в потолок), несущего каркаса и контуров подсветки.
|
|
117
|
+
- GUID строятся от конфигурации: одна и та же конфигурация даёт те же GUID, разные — разные, поэтому две композиции одного артикула сводятся в одну BIM-модель без совпадений.
|
|
118
|
+
- Начало координат — на полу; файл строится под заданную высоту потолка. На нерусском интерфейсе имена в файле английские.
|
|
119
|
+
- Библиотечного семейства Revit (.rfa) у бренда нет. Чертежи DXF — только для производства и дилеров бренда.
|
|
120
|
+
|
|
121
|
+
## Что лежит в файле FBX
|
|
122
|
+
|
|
123
|
+
- Композиция в сборе отдельными объектами, в миллиметрах; нити подвеса — телами; потолочные крепления — отдельными объектами (одно крепление на каждое отверстие в потолке: две нити, сошедшиеся в одно отверстие, несут один узел; нить от каркаса крепления не несёт); плоскость потолка. В отличие от IFC4, в FBX есть каркас и металлическая база.
|
|
124
|
+
- Начало координат — потолок, композиция висит вниз (у IFC4 начало на полу).
|
|
125
|
+
- Цвет и прозрачность переносятся; текстур и UV нет; одинаковые элементы — копии одной геометрии.
|
|
126
|
+
- Свойств BIM нет: нагрузки, масса, мощность и коды артикулов остаются в IFC4.
|
|
127
|
+
- Вставляется обычным импортом: масштаб объявлен внутри файла, композиция 1500 мм открывается как 1500 мм при любых системных единицах, разворот в Z-вверх делает сам импортёр.
|
|
128
|
+
|
|
129
|
+
## Монтажная схема разметки потолка 1:1 (PDF)
|
|
130
|
+
|
|
131
|
+
- Выдаётся страницей онлайн-ТЗ https://vargov.ru/specifications (только русский и английский): вход и регистрация не нужны, обязательно только поле «Объект»; сохранённое ТЗ открывается по своей ссылке с ключом. Сразу после сохранения доступны страница «3D-конфигурация», PDF ТЗ на русском и английском и выгрузка FBX. Цены в ТЗ нет — это техническое задание на производство, не заявка и не заказ.
|
|
132
|
+
- Схема 1:1 **открывается только после того, как менеджер запустит ТЗ в производство**; до этого кнопка неактивна. Схема предварительная: перед разметкой потолка её подтверждает технолог производства, и эта пометка напечатана на самих листах.
|
|
133
|
+
- Четыре листа — для плоттера и A4, каждый на потолок и на пол; печать строго 100 % (на листе отрезок 100 мм для проверки линейкой); лист плоттера идёт одним куском до 5 м, дальше режется.
|
|
134
|
+
- Есть у 345 из 360 композиций: нет у 10 лент, у 4 композиций на каркасе и у готовой сцены LC0406-1. Заказчику не выдаётся, если в ТЗ выбран монтаж на металлическую базу: база приходит с готовыми отверстиями и монтажными элементами (при таком монтаже подключение в ТЗ одно — 12 В через драйвер).
|
|
135
|
+
|
|
136
|
+
## Страницы
|
|
137
|
+
|
|
138
|
+
- Выгрузка IFC4: https://vargov.ru/configurator/ifc4 · выгрузка FBX: https://vargov.ru/configurator/fbx
|
|
139
|
+
- Сколько точек крепления нужно светильнику: https://vargov.ru/guides/tochki-krepleniya
|
|
140
|
+
- Эссе «Светильник, который не доезжает до модели»: https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/knowledge/ifc4-in-the-model-essay.md
|
|
141
|
+
- Монтаж и подключение (PDF на восьми языках): https://vargov.ru/installation
|
|
142
|
+
`;
|
|
143
|
+
export function bimGuide(codeInput, lang = 'en') {
|
|
144
|
+
const ru = lang === 'ru';
|
|
145
|
+
let head = '';
|
|
146
|
+
if (codeInput && codeInput.trim()) {
|
|
147
|
+
const e = findEntry(codeInput);
|
|
148
|
+
if (!e) {
|
|
149
|
+
head = ru
|
|
150
|
+
? `## Код «${codeInput}» в каталоге не найден (LC0001…LC0602, у части артикулов суффикс -1/-2). Найдите код через search_compositions.\n\n`
|
|
151
|
+
: `## Code "${codeInput}" is not in the catalogue (LC0001…LC0602, some with a -1/-2 suffix). Find the code with search_compositions.\n\n`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const page = e.p.urls?.[ru ? 'ru' : 'en'] ?? '';
|
|
155
|
+
head = (ru ? statusRu : statusEn)(e.code, e.kind, e.configurable, page) + '\n';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return head + (ru ? GUIDE_RU : GUIDE_EN);
|
|
159
|
+
}
|
|
160
|
+
export function bimCounts() {
|
|
161
|
+
const d = getData();
|
|
162
|
+
const open = d.openSet.size;
|
|
163
|
+
const withScheme = [...d.openSet].filter((c) => hasMountScheme(d.byCode.get(c)?.kind ?? (d.ribbons.has(c) ? 'ribbon' : d.chains.has(c) ? 'chain' : d.frames.has(c) ? 'frame' : d.readyScenes.has(c) ? 'ready-scene' : 'cloud'))).length;
|
|
164
|
+
return { open, withScheme, ribbons: d.ribbons.size, frames: d.frames.size, readyScenes: d.readyScenes.size, chains: d.chains.size };
|
|
165
|
+
}
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// brand_facts: a compact card of the brand. Numbers are computed from the bundled data
|
|
2
|
+
// (products.json, open-skus.json, organization.jsonld) so that they can be checked, not typed in.
|
|
3
|
+
import { bimCounts } from './bim.js';
|
|
4
|
+
import { CONFIGURATOR_INTL, CONFIGURATOR_RU, SITE } from './catalog.js';
|
|
5
|
+
import { FAMILIES, getData } from './data.js';
|
|
6
|
+
const FAMILY_PAGES = {
|
|
7
|
+
lighting: 'lighting',
|
|
8
|
+
decorative: 'decorative',
|
|
9
|
+
'sculptural-decor': 'sculptural-decor',
|
|
10
|
+
'floor-table-lamps': 'floor-table-lamps',
|
|
11
|
+
};
|
|
12
|
+
export function brandFacts(lang = 'en') {
|
|
13
|
+
const d = getData();
|
|
14
|
+
const org = d.organization;
|
|
15
|
+
const person = d.person;
|
|
16
|
+
const store = d.store;
|
|
17
|
+
const counts = bimCounts();
|
|
18
|
+
const ru = lang === 'ru';
|
|
19
|
+
const families = Object.fromEntries(FAMILIES.map((f) => {
|
|
20
|
+
const items = d.products.filter((p) => p.category === f);
|
|
21
|
+
const label = items[0]?.category_label ?? { ru: f, en: f };
|
|
22
|
+
return [f, { count: items.length, label_ru: label.ru, label_en: label.en, page_ru: `${SITE}/${FAMILY_PAGES[f]}`, page_en: `${SITE}/en/${FAMILY_PAGES[f]}` }];
|
|
23
|
+
}));
|
|
24
|
+
const codes = d.products.map((p) => p.code).sort();
|
|
25
|
+
const awards = org?.award ?? [];
|
|
26
|
+
const awardsCount = awards.length;
|
|
27
|
+
// If the knowledge base ships references/brand-facts.json, pass it through under its own key — minus the
|
|
28
|
+
// `forbidden` wordings: they exist for the knowledge base's own checker, and an assistant reading them
|
|
29
|
+
// here could repeat the very phrases they forbid.
|
|
30
|
+
const extra = d.brandFacts ? { brand_facts_file: withoutKey(d.brandFacts, 'forbidden') } : {};
|
|
31
|
+
return {
|
|
32
|
+
name: org?.name ?? 'Vargov®Design',
|
|
33
|
+
alternate_names: org?.alternateName ?? ['Vargov Design', 'VARGOV'],
|
|
34
|
+
description: org?.description,
|
|
35
|
+
founded: org?.foundingDate,
|
|
36
|
+
founder: person ? { name: person.name, name_ru: person.alternateName, title: person.jobTitle, page: person.url, links: person.sameAs } : undefined,
|
|
37
|
+
made_to_order: ru
|
|
38
|
+
? 'Каждая композиция собирается под конкретный интерьер: масштаб и геометрия подстраиваются под пространство. Цены, материалы и размеры композиций не публикуются; стоимость — по заявке.'
|
|
39
|
+
: 'Every composition is assembled for a specific interior; its scale and geometry are adjusted to the space. Prices, materials and composition dimensions are not published; a quote comes per request.',
|
|
40
|
+
catalogue: {
|
|
41
|
+
compositions: d.products.length,
|
|
42
|
+
code_range: `${codes[0]}…${codes[codes.length - 1]}`,
|
|
43
|
+
families,
|
|
44
|
+
page_ru: `${SITE}/catalog`,
|
|
45
|
+
page_en: `${SITE}/en/catalog`,
|
|
46
|
+
product_page_pattern: `${SITE}/en/catalog/<code in lower case> (Russian: ${SITE}/catalog/<code>)`,
|
|
47
|
+
},
|
|
48
|
+
configurator: {
|
|
49
|
+
url_international: CONFIGURATOR_INTL,
|
|
50
|
+
url_russian: CONFIGURATOR_RU,
|
|
51
|
+
compositions_with_full_calculation: counts.open,
|
|
52
|
+
compositions_with_1to1_ceiling_drawing: counts.withScheme,
|
|
53
|
+
without_1to1_drawing: { ribbons: counts.ribbons, on_frame: counts.frames, ready_scenes: counts.readyScenes },
|
|
54
|
+
exports_by_the_visitor: ['IFC4 model (BIM)', 'FBX model (3ds Max)', 'PDF specification', 'tender sheet'],
|
|
55
|
+
production_brief: ru ? `${SITE}/specifications` : `${SITE}/en/specifications`,
|
|
56
|
+
llms: 'https://vargov.design/llms.txt',
|
|
57
|
+
ifc4_page: ru ? `${SITE}/configurator/ifc4` : `${SITE}/en/configurator/ifc4`,
|
|
58
|
+
fbx_page: ru ? `${SITE}/configurator/fbx` : `${SITE}/en/configurator/fbx`,
|
|
59
|
+
},
|
|
60
|
+
awards: {
|
|
61
|
+
count: awardsCount,
|
|
62
|
+
wording_en: `${awardsCount} awards from international juries`,
|
|
63
|
+
wording_ru: `${awardsCount} достижений на международных премиях`,
|
|
64
|
+
note: ru
|
|
65
|
+
? 'Среди них — номинация Awwwards 2026 за 3D-конфигуратор (номинация, не победа) и членство Антона Варгова в High Jury премии ADD Awards 2024.'
|
|
66
|
+
: 'Among them: an Awwwards 2026 nomination for the 3D configurator (a nomination, not a win) and Anton Vargov\'s High Jury membership at ADD Awards 2024.',
|
|
67
|
+
page: ru ? `${SITE}/awards` : `${SITE}/en/awards`,
|
|
68
|
+
verified_list: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/knowledge/awards-verified.md',
|
|
69
|
+
list: awards,
|
|
70
|
+
},
|
|
71
|
+
certification: {
|
|
72
|
+
summary: ru
|
|
73
|
+
? 'Два серийных сертификата ТР ЕАЭС (ТР ТС 004/2011, 020/2011) и две декларации ТР ЕАЭС 037/2016 на 310 артикулов LC, знак EAC, действуют до 2031 года; сертификат серийный — покрывает каждое изделие артикула, а не партию.'
|
|
74
|
+
: 'Two EAEU serial certificates (TR CU 004/2011, 020/2011) and two TR EAEU 037/2016 declarations covering 310 LC articles, EAC mark, valid until 2031; serial scope means every unit of a listed article is covered, not a single batch.',
|
|
75
|
+
page: ru ? `${SITE}/certification` : `${SITE}/en/certification`,
|
|
76
|
+
certified_in_dataset: d.products.filter((p) => p.cert).length,
|
|
77
|
+
},
|
|
78
|
+
trademarks: org?.identifier?.map((i) => ({ registry: i.propertyID, number: i.value, url: i.url })),
|
|
79
|
+
showroom: store
|
|
80
|
+
? {
|
|
81
|
+
name: store.name,
|
|
82
|
+
address: store.address ? `${store.address.streetAddress}, ${store.address.addressLocality}, ${store.address.postalCode ?? ''} ${store.address.addressCountry}`.trim() : undefined,
|
|
83
|
+
address_en: '24 Nakhimovsky Prospekt, bldg. 1, Pavilion 2, Stand 212, Moscow',
|
|
84
|
+
hours: store.openingHoursSpecification?.map((h) => `${h.dayOfWeek.length === 7 ? 'daily' : h.dayOfWeek.join(', ')} ${h.opens}–${h.closes}`),
|
|
85
|
+
telephone: store.telephone,
|
|
86
|
+
page: ru ? `${SITE}/showroom` : `${SITE}/en/showroom`,
|
|
87
|
+
maps: store.hasMap,
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
contacts: {
|
|
91
|
+
email: org?.email,
|
|
92
|
+
whatsapp: org?.telephone,
|
|
93
|
+
telegram_requests: '@AntonVargov (https://t.me/AntonVargov)',
|
|
94
|
+
telegram_channel: '@vargov_design (https://t.me/vargov_design)',
|
|
95
|
+
},
|
|
96
|
+
dealers: {
|
|
97
|
+
official_dealers_page: ru ? `${SITE}/official-dealers` : `${SITE}/en/official-dealers`,
|
|
98
|
+
installations_page: ru ? `${SITE}/official-dealers` : `${SITE}/en/official-dealers`,
|
|
99
|
+
installations_list: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/knowledge/installations.md',
|
|
100
|
+
},
|
|
101
|
+
languages: {
|
|
102
|
+
site: ['ru', 'en', 'de', 'it', 'fr', 'es', 'vi', 'ar'],
|
|
103
|
+
site_pattern: `${SITE}/<lang>/… (Russian at the root, English at /en)`,
|
|
104
|
+
knowledge_base_only: ['zh'],
|
|
105
|
+
production_brief: ['ru', 'en'],
|
|
106
|
+
},
|
|
107
|
+
links: {
|
|
108
|
+
site: SITE,
|
|
109
|
+
site_llms: `${SITE}/llms.txt`,
|
|
110
|
+
knowledge_base: 'https://github.com/vargov3-spec/vargov-ai-kb',
|
|
111
|
+
knowledge_base_llms_en: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/en/llms.txt',
|
|
112
|
+
knowledge_base_llms_ru: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/llms.txt',
|
|
113
|
+
configurator_llms: 'https://vargov.design/llms.txt',
|
|
114
|
+
dataset_huggingface: 'https://huggingface.co/datasets/vargov-design/vargov-design-catalog',
|
|
115
|
+
dataset_json: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/datasets/products.json',
|
|
116
|
+
catalog_jsonld: 'https://raw.githubusercontent.com/vargov3-spec/vargov-ai-kb/main/references/catalog.jsonld',
|
|
117
|
+
press_kit: ru ? `${SITE}/press` : `${SITE}/en/press`,
|
|
118
|
+
social: org?.sameAs,
|
|
119
|
+
},
|
|
120
|
+
data_snapshot: d.manifest ? { updated_at: d.manifest.updated_at, files: d.manifest.files?.map((f) => ({ file: f.file, sha256: f.sha256.slice(0, 16) })) } : undefined,
|
|
121
|
+
licence: 'Data: CC BY 4.0 (Vargov®Design). Server code: MIT.',
|
|
122
|
+
...extra,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Deep copy of a JSON value with every property named `key` removed at any depth. */
|
|
126
|
+
function withoutKey(value, key) {
|
|
127
|
+
if (Array.isArray(value))
|
|
128
|
+
return value.map((v) => withoutKey(v, key));
|
|
129
|
+
if (value && typeof value === 'object') {
|
|
130
|
+
return Object.fromEntries(Object.entries(value).filter(([k]) => k !== key).map(([k, v]) => [k, withoutKey(v, key)]));
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Tool logic: search, composition card, configurator link. Pure functions over getData().
|
|
2
|
+
import { FAMILIES, LANGS, getData, normalizeCode } from './data.js';
|
|
3
|
+
export const SEARCH_LIMIT_MAX = 50;
|
|
4
|
+
export const SEARCH_LIMIT_DEFAULT = 10;
|
|
5
|
+
const TEXT_CAP = 2000;
|
|
6
|
+
export const SITE = 'https://vargov.ru';
|
|
7
|
+
/** Hosts of the configurator: the Russian interface lives on the Russian copy, every other language on vargov.design
|
|
8
|
+
* (docs/ssylka-i-tekst-dlya-tz.md of the configurator, section 4; the knowledge base links them the same way). */
|
|
9
|
+
export const CONFIGURATOR_RU = 'https://configurator.vargov.ru/';
|
|
10
|
+
export const CONFIGURATOR_INTL = 'https://vargov.design/';
|
|
11
|
+
export function cap(s, n = TEXT_CAP) {
|
|
12
|
+
if (!s)
|
|
13
|
+
return undefined;
|
|
14
|
+
return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s;
|
|
15
|
+
}
|
|
16
|
+
export function sitePage(code, lang = 'en') {
|
|
17
|
+
const slug = code.toLowerCase();
|
|
18
|
+
return lang === 'ru' ? `${SITE}/catalog/${slug}` : `${SITE}/${lang}/catalog/${slug}`;
|
|
19
|
+
}
|
|
20
|
+
const CODE_RE = /^lc\d{4}(-\d)?$/i;
|
|
21
|
+
function tokens(q) {
|
|
22
|
+
return q
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.normalize('NFC')
|
|
25
|
+
.split(/[^\p{L}\p{N}-]+/u)
|
|
26
|
+
.map((t) => t.replace(/^-+|-+$/g, ''))
|
|
27
|
+
.filter((t) => t.length >= 2);
|
|
28
|
+
}
|
|
29
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
const WORD_START = '(?:^|[^\\p{L}\\p{N}])';
|
|
31
|
+
const WORD_END = '(?=$|[^\\p{L}\\p{N}])';
|
|
32
|
+
/**
|
|
33
|
+
* Word-level matching with a light stem tolerance:
|
|
34
|
+
* - short tokens (< 5 letters) match whole words only ("spa" does not find "space");
|
|
35
|
+
* - longer tokens match the start of a word ("lobby" finds "lobbies"), and so do their stems
|
|
36
|
+
* minus one letter (≥ 5) and minus two letters (≥ 7): "гостиная" finds "гостиной".
|
|
37
|
+
*/
|
|
38
|
+
function compileToken(t) {
|
|
39
|
+
const isCode = CODE_RE.test(t) || /^\d{4}$/.test(t);
|
|
40
|
+
if (isCode)
|
|
41
|
+
return { raw: t, code: normalizeCode(t), res: [] };
|
|
42
|
+
const res = [];
|
|
43
|
+
if (t.length < 5)
|
|
44
|
+
res.push(new RegExp(WORD_START + esc(t) + WORD_END, 'u'));
|
|
45
|
+
else {
|
|
46
|
+
res.push(new RegExp(WORD_START + esc(t), 'u'));
|
|
47
|
+
res.push(new RegExp(WORD_START + esc(t.slice(0, -1)), 'u'));
|
|
48
|
+
if (t.length >= 7)
|
|
49
|
+
res.push(new RegExp(WORD_START + esc(t.slice(0, -2)), 'u'));
|
|
50
|
+
}
|
|
51
|
+
return { raw: t, code: null, res };
|
|
52
|
+
}
|
|
53
|
+
function hit(field, tok) {
|
|
54
|
+
if (!field)
|
|
55
|
+
return false;
|
|
56
|
+
for (const re of tok.res)
|
|
57
|
+
if (re.test(field))
|
|
58
|
+
return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
function scoreEntry(e, toks) {
|
|
62
|
+
let score = 0;
|
|
63
|
+
let matched = 0;
|
|
64
|
+
for (const t of toks) {
|
|
65
|
+
let s = 0;
|
|
66
|
+
if (t.code) {
|
|
67
|
+
// a code token matches codes only — no tolerance, or "LC0564" would bring every LC05xx
|
|
68
|
+
if (e.code === t.code)
|
|
69
|
+
s += 1000;
|
|
70
|
+
else if (e.code.startsWith(t.code))
|
|
71
|
+
s += 500; // LC0543 also brings LC0543-1, LC0543-2
|
|
72
|
+
if (s > 0)
|
|
73
|
+
matched++;
|
|
74
|
+
score += s;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (hit(e.f.code, t))
|
|
78
|
+
s += 100;
|
|
79
|
+
if (hit(e.f.type, t))
|
|
80
|
+
s += 10;
|
|
81
|
+
if (hit(e.f.where, t))
|
|
82
|
+
s += 5;
|
|
83
|
+
if (hit(e.f.style, t))
|
|
84
|
+
s += 3;
|
|
85
|
+
if (hit(e.f.desc, t))
|
|
86
|
+
s += 1;
|
|
87
|
+
if (s === 0 && hit(e.f.other, t))
|
|
88
|
+
s += 1;
|
|
89
|
+
if (s > 0)
|
|
90
|
+
matched++;
|
|
91
|
+
score += s;
|
|
92
|
+
}
|
|
93
|
+
return { score, matched };
|
|
94
|
+
}
|
|
95
|
+
export function searchCompositions(params) {
|
|
96
|
+
const d = getData();
|
|
97
|
+
const limit = Math.max(1, Math.min(SEARCH_LIMIT_MAX, Math.floor(params.limit ?? SEARCH_LIMIT_DEFAULT)));
|
|
98
|
+
const toks = tokens(params.query ?? '').map(compileToken);
|
|
99
|
+
let pool = d.entries.filter((e) => {
|
|
100
|
+
if (params.family && e.p.category !== params.family)
|
|
101
|
+
return false;
|
|
102
|
+
if (params.lit !== undefined && e.lit !== params.lit)
|
|
103
|
+
return false;
|
|
104
|
+
if (params.awarded !== undefined && e.p.award_winning !== params.awarded)
|
|
105
|
+
return false;
|
|
106
|
+
if (params.configurable !== undefined && e.configurable !== params.configurable)
|
|
107
|
+
return false;
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
let note;
|
|
111
|
+
let scored;
|
|
112
|
+
if (toks.length === 0) {
|
|
113
|
+
scored = pool.map((e) => ({ e, score: 0 }));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const all = pool.map((e) => ({ e, ...scoreEntry(e, toks) }));
|
|
117
|
+
let kept = all.filter((x) => x.matched === toks.length && x.score > 0);
|
|
118
|
+
if (kept.length === 0) {
|
|
119
|
+
kept = all.filter((x) => x.score > 0);
|
|
120
|
+
if (kept.length > 0)
|
|
121
|
+
note = 'No composition matched every word of the query; showing compositions that match some of them.';
|
|
122
|
+
}
|
|
123
|
+
scored = kept;
|
|
124
|
+
}
|
|
125
|
+
scored.sort((a, b) => b.score - a.score || Number(b.e.p.award_winning) - Number(a.e.p.award_winning) || a.e.code.localeCompare(b.e.code));
|
|
126
|
+
const results = scored.slice(0, limit).map(({ e }) => toRow(e));
|
|
127
|
+
return { total_matches: scored.length, shown: results.length, results, ...(note ? { note } : {}) };
|
|
128
|
+
}
|
|
129
|
+
function toRow(e) {
|
|
130
|
+
return {
|
|
131
|
+
code: e.code,
|
|
132
|
+
name_en: e.nameEn,
|
|
133
|
+
name_ru: e.nameRu,
|
|
134
|
+
family: e.p.category,
|
|
135
|
+
type_en: e.p.type?.en ?? '',
|
|
136
|
+
configurable: e.configurable,
|
|
137
|
+
kind: e.kind,
|
|
138
|
+
awarded: e.p.award_winning,
|
|
139
|
+
lit_hint: e.lit,
|
|
140
|
+
page_en: e.p.urls?.en ?? sitePage(e.code, 'en'),
|
|
141
|
+
page_ru: e.p.urls?.ru ?? sitePage(e.code, 'ru'),
|
|
142
|
+
image: e.p.image,
|
|
143
|
+
snippet_en: cap(e.p.snippet?.en, 200),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------------------------
|
|
147
|
+
// get_composition
|
|
148
|
+
export function findEntry(codeInput) {
|
|
149
|
+
return getData().byCode.get(normalizeCode(codeInput)) ?? null;
|
|
150
|
+
}
|
|
151
|
+
export function mountSchemeNote(kind) {
|
|
152
|
+
switch (kind) {
|
|
153
|
+
case 'ribbon':
|
|
154
|
+
return 'No 1:1 ceiling drawing: a ribbon has no cloud of elements; it is built differently.';
|
|
155
|
+
case 'frame':
|
|
156
|
+
return 'No 1:1 ceiling drawing: the elements have no strands of their own to the ceiling — they hang from the frame, and the frame itself is suspended from the ceiling; the IFC4 file records the number of frame suspensions (property SuspensionCount) but not their coordinates, and the suspension points themselves are shown on the frame drawing and on the side view in the configurator.';
|
|
157
|
+
case 'ready-scene':
|
|
158
|
+
return 'No 1:1 ceiling drawing: it is a wall sconce — the composition is ready-made rather than calculated, so there is no layout and no ceiling setting-out; the IFC4 file has no ceiling fixing points.';
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function hasMountScheme(kind) {
|
|
164
|
+
return kind === 'cloud' || kind === 'chain';
|
|
165
|
+
}
|
|
166
|
+
export function kindNotes(code, kind) {
|
|
167
|
+
const notes = [];
|
|
168
|
+
if (kind === 'ribbon')
|
|
169
|
+
notes.push('Ribbon: the plan Length/Width and the composition height are read from the link; the ribbon shape (wave/spiral) is chosen in the configurator; the FBX export button of a ribbon is in the ribbon panel.');
|
|
170
|
+
if (kind === 'chain')
|
|
171
|
+
notes.push('Chain composition: the form card has no effect on the assembly — the configurator lays the chain out with its own engine.');
|
|
172
|
+
if (kind === 'frame')
|
|
173
|
+
notes.push('Composition on a load-bearing frame: the number of elements is computed by the configurator from the frame pitch and length and cannot be typed by hand.');
|
|
174
|
+
if (kind === 'ready-scene')
|
|
175
|
+
notes.push('Ready scene: the composition opens as its own 3D model; plan dimensions in the link are replaced by the size of the piece; there is no ceiling-height field: the IFC4 model is placed under a 2,700 mm ceiling (the default), and the file does not declare that height as a property.');
|
|
176
|
+
if (code === 'LC0106')
|
|
177
|
+
notes.push('LC0106: instead of plan dimensions the configurator takes the number of jellyfish and the gap between them (link keys jelly and jgap); w/l/h in the link are not used for this composition.');
|
|
178
|
+
return notes;
|
|
179
|
+
}
|
|
180
|
+
export function compositionCard(codeInput, lang = 'en') {
|
|
181
|
+
const d = getData();
|
|
182
|
+
const e = findEntry(codeInput);
|
|
183
|
+
if (!e)
|
|
184
|
+
return null;
|
|
185
|
+
const p = e.p;
|
|
186
|
+
const L = LANGS.includes(lang) ? lang : 'en';
|
|
187
|
+
const sk = d.sketchfab.get(e.code);
|
|
188
|
+
const configurator = e.configurable
|
|
189
|
+
? {
|
|
190
|
+
opens_with_full_calculation: true,
|
|
191
|
+
kind: e.kind,
|
|
192
|
+
exports: ['IFC4 model (BIM)', 'FBX model (3ds Max)', 'PDF specification', 'tender sheet'],
|
|
193
|
+
mount_scheme_1to1: hasMountScheme(e.kind),
|
|
194
|
+
...(mountSchemeNote(e.kind) ? { mount_scheme_1to1_note: mountSchemeNote(e.kind) } : {}),
|
|
195
|
+
// element_size_series_mm (ряды размеров элементов из open-skus.json) снят 27.10.2026 до слова владельца:
|
|
196
|
+
// правило бренда «размеры не называть» — решение за ним, вернуть можно, восстановив этот блок из истории.
|
|
197
|
+
...(d.open.mixPairs?.[e.code] ? { paired_with: d.open.mixPairs[e.code] } : {}),
|
|
198
|
+
link_en: linkUrl(e.code, 'en'),
|
|
199
|
+
link_ru: linkUrl(e.code, 'ru'),
|
|
200
|
+
notes: kindNotes(e.code, e.kind),
|
|
201
|
+
how_to_get_bim: 'call how_to_get_bim with this code',
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
opens_with_full_calculation: false,
|
|
205
|
+
note: 'Calculated individually: the configurator does not calculate this composition, so it has no IFC4/FBX export, no PDF specification from the configurator and no 1:1 ceiling drawing. The link below opens the configurator showcase without a calculation; the production brief cannot be started for it. Contact the brand for a calculation.',
|
|
206
|
+
link_en: `${CONFIGURATOR_INTL}?sku=${encodeURIComponent(e.code)}&lang=en`,
|
|
207
|
+
link_ru: `${CONFIGURATOR_RU}?sku=${encodeURIComponent(e.code)}&lang=ru`,
|
|
208
|
+
};
|
|
209
|
+
const otherType = L !== 'ru' && L !== 'en' && p.type?.[L] ? { [L]: p.type[L] } : {};
|
|
210
|
+
return {
|
|
211
|
+
code: e.code,
|
|
212
|
+
name: { ru: e.nameRu, en: e.nameEn },
|
|
213
|
+
family: p.category,
|
|
214
|
+
family_label: p.category_label,
|
|
215
|
+
type: { ru: p.type?.ru, en: p.type?.en, ...otherType },
|
|
216
|
+
lit_hint: e.lit,
|
|
217
|
+
pages: p.urls,
|
|
218
|
+
image: p.image,
|
|
219
|
+
gallery_images: p.gallery_total,
|
|
220
|
+
texts: {
|
|
221
|
+
lang: L,
|
|
222
|
+
description: cap(p.description?.[L] ?? p.description?.en),
|
|
223
|
+
where_it_works: cap(p.where_it_works?.[L] ?? p.where_it_works?.en),
|
|
224
|
+
style: cap(p.style?.[L] ?? p.style?.en),
|
|
225
|
+
made_to_order: cap(p.made_to_order?.[L] ?? p.made_to_order?.en),
|
|
226
|
+
},
|
|
227
|
+
models_3d: {
|
|
228
|
+
...(p.model3d ? { '3ddd_ru': p.model3d } : {}),
|
|
229
|
+
...(p.model3d_en ? { '3dsky_en': p.model3d_en } : {}),
|
|
230
|
+
...(p.models_all ? { all_models_3ddd: p.models_all } : {}),
|
|
231
|
+
...(p.models_all_en ? { all_models_3dsky: p.models_all_en } : {}),
|
|
232
|
+
...(sk ? { sketchfab: { url: sk.url, published_models_of_this_code: sk.count } } : {}),
|
|
233
|
+
},
|
|
234
|
+
awards: {
|
|
235
|
+
award_winning: p.award_winning,
|
|
236
|
+
list: (p.awards ?? []).map((a) => ({ program: a.program_en ?? a.program, year: a.year, level_en: a.level_en, level_ru: a.level_ru, source: a.href })),
|
|
237
|
+
},
|
|
238
|
+
certification: {
|
|
239
|
+
certified: p.cert !== null && p.cert !== undefined,
|
|
240
|
+
note: p.cert
|
|
241
|
+
? 'Covered by the brand\'s EAEU serial certification (every unit of the article, not a batch). Details: https://vargov.ru/en/certification'
|
|
242
|
+
: 'Not in the list of certified articles of the dataset snapshot. Details: https://vargov.ru/en/certification',
|
|
243
|
+
},
|
|
244
|
+
configurator,
|
|
245
|
+
elements_in_stock: 'Stock changes four times a day; see https://vargov.ru/en/in-stock',
|
|
246
|
+
not_published: 'No price, no materials and no composition dimensions are published: every composition is made to order and scaled to the room. Quotes come per request (info@vargov.ru, WhatsApp +7 916 537 33 52, Telegram @AntonVargov).',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** The configurator does not build a plan smaller than 300 × 300 mm (link-keys contract, memory of the configurator's author, 25.09.2026). */
|
|
250
|
+
export const PLAN_MIN_MM = 300;
|
|
251
|
+
/** Sanity ceiling of this server, not a rule of the configurator. */
|
|
252
|
+
const MM_SANITY_MAX = 50_000;
|
|
253
|
+
function intMm(name, v, min) {
|
|
254
|
+
if (v === undefined || v === null)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (!Number.isFinite(v) || !Number.isInteger(v))
|
|
257
|
+
return { error: `${name} must be an integer number of millimetres (the configurator reads integer mm only)` };
|
|
258
|
+
if (v < min)
|
|
259
|
+
return { error: `${name} must be at least ${min} mm` + (min === PLAN_MIN_MM ? ' — the configurator does not build a plan smaller than 300 × 300 mm' : '') };
|
|
260
|
+
if (v > MM_SANITY_MAX)
|
|
261
|
+
return { error: `${name} exceeds ${MM_SANITY_MAX} mm — check the units (millimetres expected)` };
|
|
262
|
+
return v;
|
|
263
|
+
}
|
|
264
|
+
export function buildConfiguratorLink(params) {
|
|
265
|
+
const d = getData();
|
|
266
|
+
const code = normalizeCode(params.code);
|
|
267
|
+
const e = d.byCode.get(code);
|
|
268
|
+
const lang = params.lang && LANGS.includes(params.lang) ? params.lang : 'en';
|
|
269
|
+
if (!d.openSet.has(code)) {
|
|
270
|
+
return {
|
|
271
|
+
error: e
|
|
272
|
+
? `${code} is calculated individually: the configurator does not open it with a calculation (360 of 605 compositions do). A link with ?sku=${code} would only open the showcase. Use the product page and contact the brand for a calculation.`
|
|
273
|
+
: `${code} is not a code of the Vargov®Design catalogue (LC0001…LC0602, some with a -1/-2 suffix). Use search_compositions to find the code.`,
|
|
274
|
+
code,
|
|
275
|
+
...(e ? { product_page: e.p.urls?.[lang] ?? sitePage(code, lang) } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const w = intMm('lengthMm', params.lengthMm, PLAN_MIN_MM);
|
|
279
|
+
const l = intMm('widthMm', params.widthMm, PLAN_MIN_MM);
|
|
280
|
+
const h = intMm('heightMm', params.heightMm, 1);
|
|
281
|
+
const ceil = intMm('ceilingMm', params.ceilingMm, 1);
|
|
282
|
+
for (const v of [w, l, h, ceil])
|
|
283
|
+
if (v && typeof v === 'object')
|
|
284
|
+
return v;
|
|
285
|
+
const q = new URLSearchParams();
|
|
286
|
+
const keys = {};
|
|
287
|
+
q.set('sku', code);
|
|
288
|
+
keys.sku = 'article code';
|
|
289
|
+
if (typeof w === 'number') {
|
|
290
|
+
q.set('w', String(w));
|
|
291
|
+
keys.w = 'the "Length" field of the plan, mm (the letter is misleading: w = Length)';
|
|
292
|
+
}
|
|
293
|
+
if (typeof l === 'number') {
|
|
294
|
+
q.set('l', String(l));
|
|
295
|
+
keys.l = 'the "Width" field of the plan, mm (l = Width)';
|
|
296
|
+
}
|
|
297
|
+
if (typeof h === 'number') {
|
|
298
|
+
q.set('h', String(h));
|
|
299
|
+
keys.h = 'height of the cloud of elements, mm (for a ribbon: the composition height)';
|
|
300
|
+
}
|
|
301
|
+
if (typeof ceil === 'number') {
|
|
302
|
+
q.set('ceil', String(ceil));
|
|
303
|
+
keys.ceil = 'room ceiling height, mm (default 2700): goes into the IFC4 file and AR, does not change the layout';
|
|
304
|
+
}
|
|
305
|
+
q.set('lang', lang);
|
|
306
|
+
keys.lang = 'interface language';
|
|
307
|
+
if (params.openOnFormStep) {
|
|
308
|
+
q.set('step', 'form');
|
|
309
|
+
keys.step = 'form — opens the page scrolled to the "Composition form" step';
|
|
310
|
+
}
|
|
311
|
+
const host = lang === 'ru' ? CONFIGURATOR_RU : CONFIGURATOR_INTL;
|
|
312
|
+
const notes = [
|
|
313
|
+
lang === 'ru'
|
|
314
|
+
? 'Russian interface: the Russian copy configurator.vargov.ru; the same query works on vargov.design.'
|
|
315
|
+
: 'Visitors from Russia are redirected from vargov.design to configurator.vargov.ru with the same query.',
|
|
316
|
+
'All dimensions are integer millimetres. The plan Length × Width is the zone the composition is laid out in, not a product size; the composition is scaled to it.',
|
|
317
|
+
'Element sizes, colours and the composition form are chosen in the configurator; its "Share" button then copies a link that reopens the exact same calculation (the address bar does not reflect the state).',
|
|
318
|
+
...kindNotes(code, d.byCode.get(code)?.kind ?? null),
|
|
319
|
+
];
|
|
320
|
+
return { url: host + '?' + q.toString(), code, lang, keys, notes };
|
|
321
|
+
}
|
|
322
|
+
export function isLinkError(x) {
|
|
323
|
+
return !!x && typeof x === 'object' && 'error' in x;
|
|
324
|
+
}
|
|
325
|
+
function linkUrl(code, lang) {
|
|
326
|
+
const r = buildConfiguratorLink({ code, lang });
|
|
327
|
+
return isLinkError(r) ? null : r.url;
|
|
328
|
+
}
|
|
329
|
+
export { FAMILIES };
|