Repository navigation
Expand file tree
/
Copy pathstate.js
More file actions
507 lines (471 loc) · 36 KB
/
Copy pathstate.js
File metadata and controls
507 lines (471 loc) · 36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
// STATE.JS — глобальное состояние, константы каталога
// Зависимости: нет
// ══════════════════════════════════════════════
// ДАННЫЕ
// ══════════════════════════════════════════════
const SECS = [
{id:'terrace', lbl:'Терраса/Крыльцо', req:'terrace'},
{id:'steps', lbl:'Ступени', req:'steps'},
{id:'paths', lbl:'Дорожки', req:'paths'},
{id:'fence', lbl:'Забор', req:'fence'},
{id:'facade', lbl:'Фасад 1', req:'facade'},
{id:'facade2', lbl:'Фасад 2', req:'facade2'},
{id:'beds', lbl:'Грядки', req:'beds'},
{id:'furniture', lbl:'Мебель', req:'furniture'},
{id:'pool_terrace', lbl:'Терраса у бассейна', req:'pool_terrace'},
];
// Секции с ПРЯМОУГОЛЬНЫМ редактором (терраса и две её отдельно стоящие копии).
// Один редактор на все три: набор прямоугольников, drag за углы, снап к сетке.
// Отличие только в примыкании к дому:
// • terrace — пристраивается к дому: кромки липнут к стенам, в расчёт вершины
// уходят с vertexType 'house';
// • pool_terrace — ОТДЕЛЬНО СТОЯЩАЯ: снап только к сетке и к собственным
// блокам, все вершины 'free', в 3D направление досок берётся по длинной стороне.
// rects/active — имена полей S; colors — палитра плана (свой цвет у каждой секции).
const RECT_SECTIONS = {
terrace: {
rects: 'terraceRects', active: 'activeTerraceRect', house: true,
label: 'Терраса/Крыльцо', short: 'Терраса',
fill: 'rgba(0,150,80,.12)', stroke: '#5a8c5a', bgStroke: 'rgba(0,150,80,.5)',
},
pool_terrace: {
rects: 'poolRects', active: 'activePoolRect', house: false,
label: 'Терраса у бассейна', short: 'Терр. бассейна',
fill: 'rgba(0,80,200,.10)', stroke: '#3a63b0', bgStroke: 'rgba(0,80,200,.5)',
},
};
// Прямоугольники секции (всегда массив; создаётся лениво).
function secRects(secId) {
const c = RECT_SECTIONS[secId];
if (!c) return [];
if (!Array.isArray(S[c.rects])) S[c.rects] = [];
return S[c.rects];
}
function secActiveIdx(secId) {
const c = RECT_SECTIONS[secId];
return c ? S[c.active] : null;
}
function setSecActiveIdx(secId, v) {
const c = RECT_SECTIONS[secId];
if (c) S[c.active] = v;
}
// Маппинг легаси-имён типа дома («человеческие» названия из старого мобильного
// флоу) → typeId дескриптора ('type_NN') или 'no_house'. Единый источник —
// им пользуется getHouseTypeId (viewer3d-core.js): в S.houseType легаси-строка
// может остаться от старых сохранений/вызовов.
const HOUSE_TYPE_MAP = {
'Одноэтажный дом': 'type_01',
'Двухэтажный дом': 'type_09',
'Дом с мансардой': 'type_10',
'Участок без дома': 'no_house',
};
// Дефолтный rect ступеней (нормализованные 0..1 координаты сетки GRID=32 м):
// 2×1.5 м у нижнего края дома. Единый источник для стартового состояния S.steps
// и сбросов (nav-desktop.js) — раньше числа дублировались в трёх местах.
const DEFAULT_STEPS_RECT = { x: 0.45, y: 0.65, w: 0.0625, h: 0.046875 };
// (SEC_SCREEN и CATALOG_COLORS удалены вместе с мобильным флоу: порядок экранов
// wizard'а и 16-цветный фильтр использовались только nav.js/catalog.js.)
// Единственная высота забора, м (правка 2026-08-30: выбор 1.5/1.9 убран,
// в производстве осталась одна высота 1920 мм).
const FENCE_H = 1.92;
// Палитра именованных цветов каталога (имя → hex). Имена — из COLORS.md.
const CATALOG_COLOR_HEX = {
'Венге': '#3B2A1F', 'Серый': '#8C8C8C', 'Антрацит': '#2C2C30', 'Орех': '#7A4E2A',
'Тик': '#B5793A', 'Красный': '#9C4332', 'Песочный': '#D8C49E', 'Светло-коричневый': '#B08D5B',
'Темно-коричневый': '#3E2A1A', 'Бежевый': '#D6BE97', 'Чёрный': '#1A1A1A', 'Белый': '#F2F2F0',
'Слоновая кость': '#EAE0C8', 'Коричневый': '#6B4423', 'Шоколадный': '#4A2F1E',
'Тёмно-серый': '#4A4A4D', 'Суар': '#6E4B2A', 'Каштан': '#6A3A28', 'Светло-серый': '#C9C9C9',
'Дуб': '#9C7A4D', 'Кремовый': '#EFE2C8', 'Шоколад': '#4A2F1E',
'Chocolate mix': '#5A3A22', 'Suar mix': '#6E4B2A', 'Snow mix': '#ECECE8', 'Sand mix': '#D8C49E',
'Grey dark': '#4A4A4D', 'White': '#F2F2F0',
// Цвета МПК (COLORS.md, секция «Террасная доска из МПК»; тег mpk с 2026-07-31)
'Графит': '#43464B', 'Терракотовый': '#A65A40', 'Жемчужный': '#E8E4DC',
'Кофе': '#5C4633', 'Бронза': '#8C6A3F',
};
// Набор цветов на тип элемента (имена из COLORS.md). railing — ограждение террасы
// (отдельный элемент проекта). paths/pool_terrace берут набор террасной доски.
const ELEMENT_COLOR_NAMES = {
// Терраса = ДПК-набор + цвета МПК-доски (раздел 2329 доступен из селектора).
terrace: ['Венге', 'Серый', 'Антрацит', 'Орех', 'Тик', 'Красный', 'Песочный',
'Шоколадный', 'Бежевый', 'Графит'],
railing: ['Антрацит', 'Орех', 'Светло-коричневый', 'Темно-коричневый', 'Бежевый', 'Серый',
'Песочный', 'Чёрный', 'Белый', 'Венге'],
steps: ['Венге', 'Серый', 'Антрацит', 'Орех', 'Тик', 'Слоновая кость', 'Красный', 'Чёрный',
'Темно-коричневый', 'Песочный', 'Светло-коричневый', 'Бежевый', 'Коричневый',
'Шоколадный', 'Тёмно-серый', 'Белый', 'Суар', 'Каштан'],
fence: ['Венге', 'Серый', 'Антрацит', 'Орех', 'Коричневый', 'Чёрный', 'Белый',
'Темно-коричневый', 'Песочный', 'Светло-коричневый', 'Светло-серый', 'Бежевый',
'Тёмно-серый', 'Слоновая кость', 'Красный'],
facade: ['Chocolate mix', 'Suar mix', 'Snow mix', 'Sand mix', 'Grey dark', 'White', 'Орех',
'Серый', 'Венге', 'Антрацит', 'Тик', 'Чёрный', 'Темно-коричневый', 'Песочный',
'Светло-коричневый', 'Бежевый', 'Дуб', 'Коричневый', 'Кремовый', 'Шоколад'],
facade2: ['Chocolate mix', 'Suar mix', 'Snow mix', 'Sand mix', 'Grey dark', 'White', 'Орех',
'Серый', 'Венге', 'Антрацит', 'Тик', 'Чёрный', 'Темно-коричневый', 'Песочный',
'Светло-коричневый', 'Бежевый', 'Дуб', 'Коричневый', 'Кремовый', 'Шоколад'],
beds: ['Венге', 'Серый', 'Коричневый'],
};
// Ценовые тиры по РЕАЛЬНОМУ распределению цен каталога (ревизия 2026-07-31,
// теги dpk/mpk/walls/fences: 250–1305 ₽/м.пог с разрывами на ~500 и ~900).
// Границы фильтра — в _filterRealByPrice (nav-desktop.js), держать в синхроне
// с подписями. Старые подписи «₽/м²» и «от 10 000» были из эпохи заглушек.
// Подписи короткие: фильтр стоит в две колонки по ~140px, длинные названия
// («Сбалансировано», «Премиальное качество») там обрезались. Формулировки — из макета.
// Конкретных цен в подписях нет: диапазоны быстро расходились с прайсом, а выбор
// идёт по уровню товара. Отбор на бэкенде — по ТЕГУ категории (PRICE_TIER_TAGS
// в nav-desktop.js), а не по вилке цен.
const PRICE_TIERS = [
{id:'budget', lbl:'Эконом'},
{id:'balanced', lbl:'Средний'},
{id:'premium', lbl:'Премиум'},
{id:'mpk', lbl:'МПК'},
];
// Реальные разделы каталога API (bitrix_id). Курируем подмножество «товарных»
// категорий. Актуализировано по ревизии API 2026-07-29 (новые теги dpk/walls/fences):
// • 2331 — «Ограждения для террасы из ДПК» (раньше стоял 2332, который в API
// оказался брендом TalverWood);
// • 2680 — «Фасадные системы из ДПК» (тег walls теперь живёт здесь; прежний
// 2683 «Фасадные панели» товаров с тегом больше не отдаёт).
// Раздел грядок (2357) запрашивается по тегу garden_beds (SECTION_TAGS): без тега
// бэкенд отвечал 400 Bad Request и раздел не грузился. Ступени (2330) с 2026-08-23
// приходят по тегу steps.
const CATALOG_SECTIONS = [
{ id: 2314, label: 'Террасная доска ДПК' },
{ id: 2315, label: 'Универсальная доска ДПК' },
{ id: 2329, label: 'Террасная доска МПК' },
{ id: 2330, label: 'Ступени из ДПК' },
{ id: 2357, label: 'Грядки из ДПК' },
{ id: 2331, label: 'Ограждения террасы ДПК' },
{ id: 2348, label: 'Штакетник из ДПК' },
{ id: 2680, label: 'Фасадные системы ДПК' },
{ id: 2430, label: 'Садовая мебель' },
];
// Дефолтный раздел каталога для каждого элемента проекта (sidebar) → bitrix_id.
const CONSTRUCTION_TO_SECTION = {
terrace: 2314, paths: 2314, pool_terrace: 2314,
steps: 2330, beds: 2357, fence: 2348, facade: 2680, facade2: 2680, furniture: 2430,
railing: 2331, // «Ограждения для террасы из ДПК» — отдельный элемент проекта
};
// Тег раздела для выборки ТЕКСТУРИРОВАННЫХ товаров. Превью и 3D-текстуры (texture_urls)
// бэкенд отдаёт только у товаров, помеченных тегом. Без тега раздел возвращает все
// товары, но без texture_urls — тогда превью/текстуры «не приходят». Ключ — bitrix_id.
// Ревизия 2026-07-29: бэкенд перешёл на теги dpk / walls / fences.
// 2026-07-31: добавлен тег mpk — террасная доска из МПК (раздел 2329).
// Теги каталога (актуальный список 2026-08-31): walls, fences, dpk, mpk, steps,
// furniture, garden_beds, fencing. Теги terrasnaya_doska и dpk_steps СНЯТЫ бэкендом
// (отбирали по названию товара в обход структуры каталога и давали неверную выдачу);
// запрос с ними отвечает 400. Ни здесь, ни в пресетах ResourceManager их нет.
// section_id с этой же ревизии выборку НЕ сужает (остался для совместимости) —
// фактический отбор идёт по тегу, поэтому тег обязателен каждому разделу.
const SECTION_TAGS = {
2314: 'dpk', // террасная доска ДПК
2315: 'dpk', // универсальная доска ДПК
2329: 'mpk', // террасная доска МПК
2680: 'walls', // фасадные системы ДПК
2681: 'walls', // стеновые панели ДПК (родитель 2680)
2348: 'fences', // штакетник ДПК (забор)
2430: 'furniture',// садовая мебель (тег furniture, 2026-08-02) — см. SECTION_TAG_ONLY
2331: 'fencing', // ограждения террасы (тег fencing, 2026-08-15)
2330: 'steps', // ступени ДПК (тег steps, 2026-08-23) — см. SECTION_TAG_ONLY
2357: 'garden_beds', // грядки ДПК (тег garden_beds, 2026-08-28): без тега бэкенд
// отвечал 400 Bad Request, и раздел не грузился вовсе
};
// Разделы, которые запрашиваются ТОЛЬКО по тегу, без section_id: товары тега
// разбросаны по многим разделам (мебель — 8 разных main_section_id), и фильтр по
// одному разделу отрезал бы больше половины (2430 даёт 9 из 20 позиций).
// Ключ раздела здесь — просто идентификатор набора для UI и кэша.
// Ограждения (2331) здесь НЕ нужны: подтверждено, что все товары тега fencing лежат
// в одном разделе, поэтому запрашиваем как остальные — section_id + тег.
// Ступени (2330) — тег-онли по указанию бэкенда (2026-08-23): запрос должен быть
// ?tags=steps&limit=50, без section_id.
const SECTION_TAG_ONLY = new Set([2430, 2330]);
// Разделы, которые показываются В ОДНОМ списке с основным. Террасная доска бывает из
// ДПК (2314) и из МПК (2329) — это один выбор для пользователя, и фильтр «Доска из МПК»
// обязан находить товары. Раньше запрашивался только раздел элемента (2314), и МПК в
// выдаче не было вовсе — даже при выключенных фильтрах (жалоба 2026-09-01).
// Запрос идёт МУЛЬТИТЕГОМ (`?tags=dpk,mpk`, поддержан бэкендом 2026-08-31), поэтому
// section_id для таких наборов не ставится — он отрезал бы вторую половину.
const SECTION_MERGE = {
2314: [2329], // террасная доска: ДПК + МПК
};
// Материалы дома (шаг «Параметры дома»). Образцы — квадраты: с текстурой (img из
// assets/) или однотонные (color). id используются в 3D (_houseTexSet в viewer3d-core).
// Палитра дома: один набор на стены, фундамент и рамы (TODO.md, этап 1 п.10).
const HOUSE_COLORS = [
{ id: 'white', color: '#ffffff' }, // белый
{ id: 'beige', color: '#c7ba95' }, // бежевый
{ id: 'gray', color: '#7e7e7e' }, // нейтрально серый
{ id: 'brown', color: '#61564d' }, // коричневый
{ id: 'darkgray', color: '#1d2630' }, // холодный тёмно-серый
];
const HOUSE_MATERIALS = {
roof: {
label: 'Материал крыши',
items: [
{ id: 'tile', img: 'assets/roof_diff_01.jpg' }, // черепица
{ id: 'metal_green', img: 'assets/roof_diff_02.jpg' }, // металл зелёный
{ id: 'metal_red', img: 'assets/roof_diff_03.jpg' }, // металл красный
],
},
// Стены, фундамент и рамы — ОДНА палитра из пяти цветов (TODO.md, этап 1 п.10),
// без текстур. Значения продублированы в _houseTexSet (viewer3d-core.js) — менять
// синхронно, иначе образец в UI разойдётся с цветом в 3D.
base: { label: 'Цвет фундамента', items: HOUSE_COLORS },
wall: { label: 'Цвет стен', items: HOUSE_COLORS },
frame: { label: 'Цвет рам', items: HOUSE_COLORS },
};
// Ключ группы HOUSE_MATERIALS ↔ поле состояния: '<kind>Mat' (roofMat, baseMat,
// wallMat, frameMat). На этом соглашении держатся _dRenderHouseMaterials и
// dSetHouseMat — новая группа материалов не требует правок в них.
// ══════════════════════════════════════════════
// СОСТОЯНИЕ
// ══════════════════════════════════════════════
const S = {
houseType: null,
sections: [],
// paths/fence — polyline-режим (массив точек).
// railing — ограждение террасы: ломаная ВНУТРИ террасы (TODO.md → ОГРАЖДЕНИЯ),
// рисуется как забор и строится теми же секциями.
pts: { paths:[], fence:[], railing:[] },
// Терраса/Крыльцо: массив прямоугольников (boolean union в 3D).
// Все координаты нормированные 0..1 (как в canvas).
terraceRects: [],
activeTerraceRect: null, // индекс выбранного rect или null
// Терраса у бассейна и причал — тот же редактор, отдельно стоящие (RECT_SECTIONS).
poolRects: [],
activePoolRect: null,
// Ступени: один rect (положение + ширина = от пользователя; глубина в 3D
// пересчитывается автоматически из количества подступенков).
// Лестниц может быть несколько (правка 2026-08-30): список + индекс выбранной.
// Свойство S.steps (объявлено ниже) отдаёт ВЫБРАННУЮ лестницу — редактор, снап,
// 3D и смета работают с ней, а обход всех лестниц идёт по stepsList / stepsAll().
stepsList: [{ ...DEFAULT_STEPS_RECT }],
activeSteps: 0,
// Грядки: массив прямоугольников фиксированного размера 3×1 м. Ориентация
// ортогональная — длинная сторона (3 м) вдоль X (w>h) или вдоль Y (w<h).
// Размер не меняется (только перемещение + поворот на 90°). Координаты 0..1.
beds: [],
activeBed: null, // индекс выбранной грядки или null
// Высота борта грядки в метрах. В редакторе НЕ задаётся: высоту, цвет и крепёж
// выбирают на карточке товара (TODO.md → ГРЯДКИ). Пишется при «Применить» из
// товара (_bedHeightFromProduct в nav-desktop.js); 0.20 — значение до выбора.
bedH: 0.20,
// Крепёж выбранной грядки (id из BED_MOUNTS) или null. От него зависит МОДЕЛЬ:
// у грядки с углом свой GLB, с уголком в геометрии (см. planterModelUrl).
bedMount: null,
// Высота настила террасы в метрах: 0.10 … высота фундамента (TODO.md → ТЕРРАСА).
// null — «вровень с фундаментом» (значение по умолчанию до первой настройки).
terraceH: null,
// Высота полотна забора одна — 1920 мм (правка 2026-08-30, выбор высоты убран).
fenceH: FENCE_H,
mats: {},
// Материал настила по элементу (терраса/ступени/дорожки/грядки/бассейн/причал):
// elementId -> { textures } | { color }. Применяется независимо в buildScene3d.
elementMat: {},
samples: [], // [{id, name, color}] — накопленные образцы
activeSample: null, // {id, name, color} — текущий выбранный для примерки
curSec: 0,
// matSubMode удалён вместе с переключателем ТЕРРАСА/ОГРАЖДЕНИЕ в каталоге:
// ограждение — отдельный элемент проекта со своим разделом каталога.
// Фильтры ограждения (TODO.md, этап 2 п.5): вид крышки столба и его сечение.
// МУЛЬТИВЫБОР — массивы значений, пустой = фильтр не задан. Это фильтры КАТАЛОГА
// (TODO п.1): на 3D не влияют, атрибутов под них у товара нет — отбираем по
// названию, см. _railingFilterProducts в nav-desktop.js.
railFilters: { cap: [], postW: [] },
// Сечение столба ограждения в мм — приходит из ВЫБРАННОГО товара
// (_railPostWFromProduct в nav-desktop.js). null — товара нет, сечение родное.
railPostW: null,
// Фильтры грядок: высота борта (мм) и тип крепежа. МУЛЬТИВЫБОР (TODO п.15) —
// массивы значений, пустой = фильтр не задан. Это фильтры КАТАЛОГА: на 3D они
// не влияют, до выбора товара грядка обозначается местом, после — рисуется по
// товару (высота борта из него же). Отбор — _bedFilterProducts.
bedFilters: { h: [], mount: [] },
// Бассейн на террасе у бассейна (TODO.md, этап 2 п.14): прямоугольный или круглый.
// Координаты плана; у круглого w === h (диаметр). null — бассейна нет.
pool: null,
// Калитка в заборе (TODO.md, этап 2 п.8): точка на ломаной, вокруг неё проём
// фиксированной ширины FENCE_GATE_W. null — калитки нет.
fenceGate: null,
// «Вход» в ограждении террасы — разрыв, заданный двумя точками на периметре
// (доли длины главной петли, TODO.md этап 2 п.4). null — входа нет.
// Разрывы («входы») ограждения — по одному на каждый независимый контур террасы
// (правка 2026-08-30). Индекс совпадает с индексом петли в railingLoopsWorld.
railingEntries: [],
// Фильтры каталога — СВОИ у каждого раздела (TODO.md, этап 2 п.1): выбранный
// цвет террасной доски не должен обнулять выдачу в заборе. Ключ — id пункта меню,
// значение — { colors:Set, prices:Set }. Доступ через catFilter() ниже.
catFilters: {},
catSection: null, // выбранный раздел каталога (bitrix_id) или null = дефолт по элементу
catShowResults: false,
estimate: {}, // elementId -> { id, name, price } — выбранный в смету товар по элементу
// Тумблеры разделов (id из data-id → bool): 'steps-railing', 'porch-canopy'…
// Зеркалируются из DOM в ttg/_dCacheToggleDefaults — 3D-слой читает ТОЛЬКО
// отсюда (tgOn), не DOM.
// Значения по умолчанию заданы ЗДЕСЬ, а не разметкой: панель раздела рисуется
// динамически (версия 3D-UI), и на старте тумблера в DOM ещё нет —
// _dCacheToggleDefaults его не увидит.
toggles: { 'steps-railing': true },
pathWidth: 120, // ширина дорожки, см (инпут v-paths-width зеркалится сюда)
// Садовая мебель: точки размещения на плане. Индекс+1 = номер точки на плане.
// { x, y } — нормализованные 0..1 (как остальная разметка);
// rot — поворот вокруг вертикали, радианы, кратно π/2 (0 = «перёд» модели,
// её локальная ось +X, смотрит вдоль мирового +X = вправо на плане);
// product — назначенный из каталога товар { id, name, modelUrl } или null (точка пустая).
furniture: [],
activeFurniture: null, // индекс выбранной точки или null
// Отделка фасада: выбранные вертикальные сегменты стен (segId из house-builder,
// формат 'f{этаж}:e{ребро}:s{сегмент}' → true). Пустой выбор при заданном
// материале (S.elementMat.facade) = «весь фасад». Материал панелей — S.elementMat.facade.
wallZones: {},
// Отделок фасада ДВЕ (ТЗ 2026-09-18): «Отделка фасада 1» и «Отделка фасада 2»
// работают одинаково, но выбор и материал у каждой свой. Второй набор зон —
// здесь, материалы лежат в S.elementMat.facade / .facade2. Кусок принадлежит
// одной отделке: выбор во второй снимает его из первой и наоборот.
wallZones2: {},
// Материалы дома (шаг «Параметры дома»).
roofMat: 'tile', // tile | metal_green | metal_red (единственная группа с текстурами)
// Стены, фундамент и рамы — общая палитра HOUSE_COLORS:
// white | beige | gray | brown | darkgray (только цвет, без текстур).
baseMat: 'beige',
wallMat: 'white',
frameMat: 'brown'
};
// S.steps — ВЫБРАННАЯ лестница из S.stepsList. Свойство, а не поле: весь прежний код
// (редактор, снап, 3D, смета) продолжает работать с одной лестницей, а поддержка
// нескольких сводится к обходу stepsList.
Object.defineProperty(S, 'steps', {
configurable: true,
enumerable: true,
get() {
const list = S.stepsList;
if (!Array.isArray(list) || !list.length) return null;
return list[Math.max(0, Math.min(list.length - 1, S.activeSteps | 0))];
},
set(v) {
if (!Array.isArray(S.stepsList)) S.stepsList = [];
if (v == null) { S.stepsList = []; S.activeSteps = 0; return; }
if (!S.stepsList.length) { S.stepsList = [v]; S.activeSteps = 0; return; }
S.stepsList[Math.max(0, Math.min(S.stepsList.length - 1, S.activeSteps | 0))] = v;
},
});
// Все лестницы проекта (пустой массив, если раздел не размечен).
function stepsAll() { return Array.isArray(S.stepsList) ? S.stepsList : []; }
// (TOTAL и глобальный step удалены — прогресс-бар мобильного wizard'а.)
// ── Характеристики товара (ProductResource.properties) ──────────────────────
// Имена согласованы с бэкендом 2026-08-26; ими же ходит FilterType.PROPERTIES,
// когда обновлённый ResourceManager доедет на стенд:
// заборы dimensions.height
// ограждения components.post_cap.type ['dpk','metal','plastic']
// components.post.dimensions.width
// грядки dimensions.height
// components.corner_bracket.type ['metal','plastic']
const PROP_FENCE_H = 'dimensions.height';
// Готовая модель калитки у товара забора (API 2026-09-18): ссылка на GLB.
const PROP_WICKET_GLB = 'wicket_glb_url';
const PROP_RAIL_CAP = 'components.post_cap.type';
const PROP_RAIL_POST_W = 'components.post.dimensions.width';
const PROP_BED_H = 'dimensions.height';
const PROP_BED_BRACKET = 'components.corner_bracket.type';
// Значение характеристики по пути «a.b.c». Поддерживает и вложенные объекты,
// и плоские ключи ('components.post_cap.type' одной строкой) — какой из вариантов
// придёт от бэкенда, на момент написания не проверено на живых данных.
function productProp(product, path) {
const p = product && product.properties;
if (!p || !path) return undefined;
if (p[path] !== undefined) return p[path];
let cur = p;
for (const key of path.split('.')) {
if (cur === null || typeof cur !== 'object') return undefined;
cur = cur[key];
}
return cur;
}
// Варианты фильтров ограждения (TODO.md, этап 2 п.5).
const RAIL_CAP_TYPES = [
{ id: 'plastic', lbl: 'Пластмасса', re: /пластик|пластмасс|plastic|пвх/i },
{ id: 'dpk', lbl: 'ДПК', re: /дпк|wpc|композит/i },
{ id: 'metal', lbl: 'Металл', re: /металл|алюмин|сталь|metal/i },
];
const RAIL_POST_WIDTHS = [100, 125]; // мм
// Фильтры грядок (TODO.md, этап 2 п.11): высота борта и тип крепежа.
const BED_HEIGHTS = [150, 200, 225, 270, 300]; // мм
// prop — значение характеристики components.corner_bracket.type (бэкенд, 2026-08-27:
// три значения вместо двух, «шарнир» и «угол» теперь различимы). re — запасной
// разбор названия, пока характеристики у товара нет.
// Сечение доски (характеристика каталога core_type): у ступеней приходит solid,
// у террасной доски ДПК и МПК — hollow. Подписи наши, значения — ключи API.
const CORE_TYPES = [
{ id: 'solid', lbl: 'Полнотелая' },
{ id: 'hollow', lbl: 'Пустотелая' },
];
// Направление доски фасадных панелей — фильтр разделов отделки фасада.
// Отбирает бэкенд по характеристике board_direction (ревизия 2026-09-30).
const BOARD_DIRECTIONS = [
{ id: 'vertical', lbl: 'Вертикальная' },
{ id: 'horizontal', lbl: 'Горизонтальная' },
];
// Заполнение забора — фильтр раздела «Забор». Отбирает бэкенд по характеристике
// fence_type (ревизия 2026-09-30).
const FENCE_TYPES = [
{ id: 'woven', lbl: 'Плетёные' },
{ id: 'board', lbl: 'Доски ДПК' },
];
const BED_MOUNTS = [
{ id: 'plastic_hinge', lbl: 'Пластиковый шарнир', prop: 'plastic_joint',
re: /шарнир|hinge/i },
// `\w` в JS-регулярке кириллицу НЕ покрывает (ASCII-семантика), поэтому
// «пластиковый угол» и «металлический угол» не совпадали никогда — окончание
// слова описываем явным классом букв. Порядок слов в названиях каталога
// непостоянный («металлический угол» / «угол металлический»), учитываем оба.
{ id: 'plastic_angle', lbl: 'Пластиковый угол', prop: 'plastic_angle',
re: /пластик[а-яё]*\s+угол(?:ок)?|угол(?:ок)?\s+пластик[а-яё]*/i },
{ id: 'metal_angle', lbl: 'Металлический угол', prop: 'metal',
re: /металл[а-яё]*\s+угол(?:ок)?|угол(?:ок)?\s+металл[а-яё]*/i },
];
// Фильтры каталога выбранного раздела (создаются лениво). secId — id пункта меню.
// Зоны отделки нужного раздела фасада. Разделов два, состояние у каждого своё.
const FACADE_SECS = ['facade', 'facade2'];
function facadeZones(secId) {
if (secId === 'facade2') {
if (!S.wallZones2) S.wallZones2 = {};
return S.wallZones2;
}
if (!S.wallZones) S.wallZones = {};
return S.wallZones;
}
// Другой раздел фасада — тот, из которого кусок надо убрать при выборе.
function facadeOtherSec(secId) {
return (secId === 'facade2') ? 'facade' : 'facade2';
}
// Выбор фильтров, с которым раздел открывается впервые. У террасы по умолчанию
// включена «Пустотелая» (правка 2026-09-26): ходовая доска для настила, а
// полнотелую пользователь включит сам. Снять выбор можно — это только старт.
const CAT_FILTER_DEFAULTS = {
terrace: { core: ['hollow'] },
};
function catFilter(secId) {
const key = secId || '_';
let f = S.catFilters[key];
// core — сечение доски (полнотелая / пустотелая), характеристика core_type:
// отбирает бэкенд, как и ценовую категорию.
if (!f) {
const d = CAT_FILTER_DEFAULTS[key] || {};
f = S.catFilters[key] = { colors: new Set(), prices: new Set(), core: new Set(d.core || []),
dir: new Set(), fence: new Set() };
}
if (!f.core) f.core = new Set();
if (!f.dir) f.dir = new Set(); // направление доски (фасад), см. BOARD_DIRECTIONS
if (!f.fence) f.fence = new Set(); // заполнение забора, см. FENCE_TYPES
return f;
}
// Участок без дома. Единый источник истины для canvas.js / viewer3d-core.js:
// десктоп хранит в S.houseType typeId ('type_NN') или 'no_house' («Пустой участок»);
// null — тип ещё не выбран (дом тоже не рисуем). Легаси-строка — от старого мобильного флоу.
function isEmptyLot() {
return !S.houseType || S.houseType === 'no_house' || S.houseType === 'Участок без дома';
}
// Состояние тумблера редактора по data-id ('terrace-roof', 'steps-railing'…).
// Единственный способ чтения тумблеров из 3D-слоя (viewer3d-*): DOM там не трогаем.
function tgOn(id) {
return !!(S.toggles && S.toggles[id]);
}
// ══════════════════════════════════════════════