PageSourceSearch

https://sarafanovo.com/wa-data/public/site/themes/sarafanovo_2023/js/sf-calculator-engine-v2.js?v=20260822-2

js sarafanovo.com collected 2026-10-08 07:11:01 UTC 29,197 bytes, 493 lines download raw bytes

1(function (root, factory) {
2    var api = factory();
3    if (typeof module === "object" && module.exports) {
4        module.exports = api;
5    } else {
6        root.SarafanovoFabricCalculator = api;
7    }
8}(typeof self !== "undefined" ? self : this, function () {
9    "use strict";
10
11    var EPSILON = 1e-9;
12
13    function number(value, name, min, max) {
14        var parsed = typeof value === "number" ? value : Number(String(value).replace(",", "."));
15        if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
16            throw new RangeError(name + ": ожидается число от " + min + " до " + max);
17        }
18        return parsed;
19    }
20
21    function integer(value, name, min, max) {
22        var parsed = number(value, name, min, max);
23        if (!Number.isInteger(parsed)) {
24            throw new RangeError(name + ": ожидается целое число");
25        }
26        return parsed;
27    }
28
29    function roundUp(value, step) {
30        if (!step) {
31            return value;
32        }
33        return Math.ceil((value - EPSILON) / step) * step;
34    }
35
36    function effectiveFabricWidth(input, min, max) {
37        var nominal = number(input.fabricWidthCm, "Номинальная ширина ткани", min, max);
38        var excluded = number(input.excludedWidthCm == null ? 0 : input.excludedWidthCm, "Исключаемая ширина кромок", 0, 40);
39        var widthShrinkage = number(input.widthShrinkagePct == null ? 0 : input.widthShrinkagePct, "Усадка по ширине", 0, 30);
40        var usable = (nominal - excluded) * (1 - widthShrinkage / 100);
41        if (usable < min) {
42            throw new RangeError("Полезная ширина после кромок и усадки меньше " + min + " см");
43        }
44        return {
45            nominalCm: nominal,
46            excludedCm: excluded,
47            widthShrinkagePct: widthShrinkage,
48            usableCm: usable
49        };
50    }
51
52    function normalizePiece(piece, index) {
53        var quantity = integer(piece.quantity == null ? 1 : piece.quantity, "Количество детали " + (index + 1), 1, 1000);
54        return {
55            name: String(piece.name || ("Деталь " + (index + 1))),
56            widthCm: number(piece.widthCm, "Ширина детали " + (index + 1), 0.1, 1000),
57            lengthCm: number(piece.lengthCm, "Длина детали " + (index + 1), 0.1, 5000),
58            quantity: quantity,
59            rotatable: piece.rotatable !== false
60        };
61    }
62
63    function expandPieces(pieces) {
64        var expanded = [];
65        pieces.map(normalizePiece).forEach(function (piece) {
66            for (var i = 0; i < piece.quantity; i += 1) {
67                expanded.push({
68                    name: piece.name,
69                    widthCm: piece.widthCm,
70                    lengthCm: piece.lengthCm,
71                    rotatable: piece.rotatable
72                });
73            }
74        });
75        return expanded.sort(function (a, b) {
76            return (b.widthCm * b.lengthCm) - (a.widthCm * a.lengthCm);
77        });
78    }
79
80    function orientations(piece, directional) {
81        var result = [{ widthCm: piece.widthCm, lengthCm: piece.lengthCm, rotated: false }];
82        if (!directional && piece.rotatable && Math.abs(piece.widthCm - piece.lengthCm) > EPSILON) {
83            result.push({ widthCm: piece.lengthCm, lengthCm: piece.widthCm, rotated: true });
84        }
85        return result;
86    }
87
88    /**
89     * Deterministic shelf layout for rectangular bedding/table-linen pieces.
90     * It is an estimate, not a replacement for a production cutting marker.
91     */
92    function packRectangles(pieces, fabricWidthCm, directional) {
93        var width = number(fabricWidthCm, "Ширина ткани", 30, 500);
94        var shelves = [];
95        var placements = [];
96
97        expandPieces(pieces).forEach(function (piece) {
98            var options = orientations(piece, Boolean(directional));
99            var best = null;
100
101            options.forEach(function (orientation) {
102                if (orientation.widthCm > width + EPSILON) {
103                    return;
104                }
105
106                shelves.forEach(function (shelf, shelfIndex) {
107                    if (shelf.usedWidthCm + orientation.widthCm <= width + EPSILON) {
108                        var addedLength = Math.max(0, orientation.lengthCm - shelf.lengthCm);
109                        var candidate = {
110                            addedLengthCm: addedLength,
111                            unusedWidthCm: width - shelf.usedWidthCm - orientation.widthCm,
112                            shelfIndex: shelfIndex,
113                            orientation: orientation
114                        };
115                        if (!best || candidate.addedLengthCm < best.addedLengthCm - EPSILON ||
116                            (Math.abs(candidate.addedLengthCm - best.addedLengthCm) <= EPSILON && candidate.unusedWidthCm < best.unusedWidthCm)) {
117                            best = candidate;
118                        }
119                    }
120                });
121
122                var newShelf = {
123                    addedLengthCm: orientation.lengthCm,
124                    unusedWidthCm: width - orientation.widthCm,
125                    shelfIndex: shelves.length,
126                    orientation: orientation
127                };
128                if (!best || newShelf.addedLengthCm < best.addedLengthCm - EPSILON ||
129                    (Math.abs(newShelf.addedLengthCm - best.addedLengthCm) <= EPSILON && newShelf.unusedWidthCm < best.unusedWidthCm)) {
130                    best = newShelf;
131                }
132            });
133
134            if (!best) {
135                throw new RangeError("Деталь «" + piece.name + "» не помещается в ширину ткани " + width + " см");
136            }
137
138            if (best.shelfIndex === shelves.length) {
139                shelves.push({ usedWidthCm: 0, lengthCm: 0, placements: [] });
140            }
141            var shelf = shelves[best.shelfIndex];
142            shelf.usedWidthCm += best.orientation.widthCm;
143            shelf.lengthCm = Math.max(shelf.lengthCm, best.orientation.lengthCm);
144            shelf.placements.push(piece.name);
145            placements.push({
146                name: piece.name,
147                shelf: best.shelfIndex + 1,
148                widthCm: best.orientation.widthCm,
149                lengthCm: best.orientation.lengthCm,
150                rotated: best.orientation.rotated
151            });
152        });
153
154        var layout = {
155            fabricWidthCm: width,
156            layoutLengthCm: shelves.reduce(function (sum, shelf) { return sum + shelf.lengthCm; }, 0),
157            shelves: shelves,
158            placements: placements
159        };
160
161        // A greedy heuristic can make a locally attractive rotation that worsens
162        // later shelves. Free rotation must never report more fabric than the same
163        // valid all-unrotated layout, so compare that deterministic fallback.
164        if (!directional) {
165            try {
166                var unrotatedLayout = packRectangles(pieces, width, true);
167                if (unrotatedLayout.layoutLengthCm < layout.layoutLengthCm - EPSILON) {
168                    return unrotatedLayout;
169                }
170            } catch (error) {
171                // The unrotated fallback may be impossible even though a rotated
172                // placement fits. The already-built free layout remains valid.
173                if (!(error instanceof RangeError)) {
174                    throw error;
175                }
176            }
177        }
178
179        return layout;
180    }
181
182    function finishLength(layoutLengthCm, options) {
183        var shrinkagePct = number(options.shrinkagePct == null ? 0 : options.shrinkagePct, "Усадка", 0, 30);
184        var reservePct = number(options.reservePct == null ? 0 : options.reservePct, "Запас", 0, 50);
185        var repeatCm = number(options.repeatCm == null ? 0 : options.repeatCm, "Раппорт", 0, 500);
186        var valueCm = layoutLengthCm;
187
188        // To retain the required cut length after shrinkage, divide by the retained fraction.
189        valueCm = valueCm / (1 - shrinkagePct / 100);
190        valueCm *= 1 + reservePct / 100;
191        var beforeRoundingCm = valueCm;
192        // A repeat is measured on the fabric as purchased, so it must be the final
193        // rounding increment after shrinkage and reserve. Otherwise those allowances
194        // destroy the promised repeat multiple. Without a repeat, use a practical
195        // 5 cm purchase increment.
196        var roundingIncrementCm = repeatCm > 0 ? repeatCm : 5;
197        valueCm = roundUp(valueCm, roundingIncrementCm);
198
199        return {
200            beforeAllowancesM: layoutLengthCm / 100,
201            afterAllowancesBeforeRoundingM: beforeRoundingCm / 100,
202            purchaseLengthM: valueCm / 100,
203            shrinkagePct: shrinkagePct,
204            reservePct: reservePct,
205            repeatCm: repeatCm,
206            roundingIncrementCm: roundingIncrementCm,
207            roundingBasis: repeatCm > 0 ? "pattern-repeat" : "purchase-increment"
208        };
209    }
210
211    function beddingPieces(input) {
212        var seam = number(input.seamCm == null ? 1.5 : input.seamCm, "Припуск на шов", 0, 10);
213        var hem = number(input.hemCm == null ? 2.5 : input.hemCm, "Подгибка", 0, 20);
214        var flap = number(input.pillowFlapCm == null ? 25 : input.pillowFlapCm, "Клапан наволочки", 0, 50);
215        var directional = Boolean(input.directional);
216        var pieces = [];
217
218        // Aggregate limits cover up to 50 UI sets while each editable set keeps
219        // its stricter 10/10/20 component limits in the form controller.
220        var duvetQty = integer(input.duvetQuantity == null ? 0 : input.duvetQuantity, "Количество пододеяльников", 0, 500);
221        if (duvetQty) {
222            var duvetWidth = number(input.duvetWidthCm, "Ширина пододеяльника", 40, 400);
223            var duvetLength = number(input.duvetLengthCm, "Длина пододеяльника", 60, 400);
224            pieces.push({
225                name: "Пододеяльник",
226                widthCm: duvetWidth + 2 * seam,
227                lengthCm: 2 * duvetLength + 2 * seam,
228                quantity: duvetQty,
229                rotatable: !directional
230            });
231        }
232
233        var sheetQty = integer(input.sheetQuantity == null ? 0 : input.sheetQuantity, "Количество простыней", 0, 500);
234        if (sheetQty) {
235            var sheetWidth = number(input.sheetWidthCm, "Ширина простыни", 40, 500);
236            var sheetLength = number(input.sheetLengthCm, "Длина простыни", 60, 500);
237            var fitted = Boolean(input.fittedSheet);
238            if (fitted) {
239                var mattressHeight = number(input.mattressHeightCm, "Высота матраса", 2, 80);
240                var tuck = number(input.fittedTuckCm == null ? 15 : input.fittedTuckCm, "Запас под матрас", 5, 40);
241                // `tuck` is the finished reserve under the mattress. Keep the edge
242                // finish/casing separate so it cannot silently consume that reserve.
243                sheetWidth += 2 * (mattressHeight + tuck + hem);
244                sheetLength += 2 * (mattressHeight + tuck + hem);
245            } else {
246                sheetWidth += 2 * hem;
247                sheetLength += 2 * hem;
248            }
249            pieces.push({ name: fitted ? "Простыня на резинке" : "Простыня", widthCm: sheetWidth, lengthCm: sheetLength, quantity: sheetQty, rotatable: !directional });
250        }
251
252        var pillowQty = integer(input.pillowQuantity == null ? 0 : input.pillowQuantity, "Количество наволочек", 0, 1000);
253        if (pillowQty) {
254            var pillowWidth = number(input.pillowWidthCm, "Ширина наволочки", 20, 150);
255            var pillowLength = number(input.pillowLengthCm, "Длина наволочки", 20, 150);
256            pieces.push({
257                name: "Наволочка",
258                widthCm: pillowWidth + 2 * seam,
259                lengthCm: 2 * pillowLength + flap + 2 * seam,
260                quantity: pillowQty,
261                rotatable: !directional
262            });
263        }
264
265        if (!pieces.length) {
266            throw new RangeError("Добавьте хотя бы одно изделие");
267        }
268        return pieces;
269    }
270
271    function calculateBedding(input) {
272        var pieces = beddingPieces(input);
273        var width = effectiveFabricWidth(input, 30, 500);
274        var packed = packRectangles(pieces, width.usableCm, input.directional);
275        var finished = finishLength(packed.layoutLengthCm, input);
276        return {
277            kind: "bedding",
278            pieces: pieces,
279            layout: packed,
280            width: width,
281            result: finished,
282            disclaimer: "Предварительный расчёт прямоугольной раскладки. Округление общего метража по раппорту не гарантирует совмещение рисунка на каждой детали. Перед покупкой проверьте технологию швов, направление рисунка и карту раскроя."
283        };
284    }
285
286    var GARMENT_DEFAULTS = {
287        straight_skirt: { label: "Прямая юбка", bodyLengthCm: 65, sleeveLengthCm: 0, easeCm: 4, extraCm: 15, pieceFactor: 1 },
288        trousers: { label: "Брюки", bodyLengthCm: 110, sleeveLengthCm: 0, easeCm: 8, extraCm: 25, pieceFactor: 1.45 },
289        blouse: { label: "Блузка / рубашка", bodyLengthCm: 70, sleeveLengthCm: 62, easeCm: 12, extraCm: 25, pieceFactor: 1 },
290        straight_dress: { label: "Прямое платье", bodyLengthCm: 110, sleeveLengthCm: 62, easeCm: 10, extraCm: 30, pieceFactor: 1 },
291        jacket: { label: "Жакет", bodyLengthCm: 75, sleeveLengthCm: 64, easeCm: 14, extraCm: 35, pieceFactor: 1.35 },
292        tshirt: { label: "Футболка / лонгслив", bodyLengthCm: 70, sleeveLengthCm: 25, easeCm: 8, extraCm: 15, pieceFactor: 1 },
293        sundress: { label: "Простой сарафан", bodyLengthCm: 110, sleeveLengthCm: 0, easeCm: 10, extraCm: 30, pieceFactor: 1 },
294        robe: { label: "Халат простого кроя", bodyLengthCm: 115, sleeveLengthCm: 58, easeCm: 18, extraCm: 55, pieceFactor: 1.15 },
295        nightgown: { label: "Ночная сорочка", bodyLengthCm: 105, sleeveLengthCm: 25, easeCm: 18, extraCm: 35, pieceFactor: 1 },
296        child_dress: { label: "Детское платье", bodyLengthCm: 70, sleeveLengthCm: 22, easeCm: 10, extraCm: 25, pieceFactor: 1 }
297    };
298
299    function calculateGarment(input) {
300        var preset = GARMENT_DEFAULTS[input.garment];
301        if (!preset) {
302            throw new RangeError("Выберите тип изделия");
303        }
304        var width = effectiveFabricWidth(input, 70, 320);
305        var fabricWidth = width.usableCm;
306        var circumference = number(input.circumferenceCm, "Контрольный обхват", 40, 250);
307        var bodyLength = number(input.bodyLengthCm == null ? preset.bodyLengthCm : input.bodyLengthCm, "Длина изделия", 20, 250);
308        var sleeveLength = number(input.sleeveLengthCm == null ? preset.sleeveLengthCm : input.sleeveLengthCm, "Длина рукава", 0, 120);
309        var ease = number(input.easeCm == null ? preset.easeCm : input.easeCm, "Прибавка на свободу", 0, 50);
310        var seam = number(input.seamCm == null ? 1.5 : input.seamCm, "Припуск на шов", 0, 10);
311        var halfPatternWidth = (circumference + ease) / 2 + 2 * seam;
312        var bodyRows = fabricWidth + EPSILON >= 2 * halfPatternWidth ? 1 : 2;
313        var sleeveRows = sleeveLength > 0 ? (fabricWidth + EPSILON >= circumference / 2 + ease + 4 * seam ? 1 : 2) : 0;
314        var baseCm = (bodyLength * bodyRows + sleeveLength * sleeveRows + preset.extraCm) * preset.pieceFactor;
315        if (input.directional) {
316            baseCm *= 1.08;
317        }
318        var finished = finishLength(baseCm, input);
319        var center = finished.purchaseLengthM;
320        return {
321            kind: "garment",
322            preset: preset.label,
323            width: width,
324            result: finished,
325            estimateMinM: Math.ceil(center * 0.9 * 20) / 20,
326            estimateMaxM: Math.ceil(center * 1.15 * 20) / 20,
327            bodyRows: bodyRows,
328            sleeveRows: sleeveRows,
329            disclaimer: "Ориентировочный диапазон без конкретной выкройки. Это не готовая карта раскроя: перед покупкой сверьте метраж с инструкцией к выбранной выкройке. Карманы, воротник, подкладка, клетка, крупный раппорт и сложный фасон могут увеличить расход."
330        };
331    }
332
333    function calculateArea(widthCm, lengthM) {
334        return number(widthCm, "Ширина ткани", 0.1, 1000) / 100 * number(lengthM, "Длина ткани", 0.01, 100000);
335    }
336
337    function calculateRollWeight(widthCm, lengthM, gsm) {
338        return calculateArea(widthCm, lengthM) * number(gsm, "Плотность", 1, 5000) / 1000;
339    }
340
341    function calculateRollOuterVolume(diameterCm, widthCm) {
342        var diameterM = number(diameterCm, "Диаметр рулона", 1, 500) / 100;
343        var widthM = number(widthCm, "Ширина рулона", 1, 1000) / 100;
344        return Math.PI * diameterM * diameterM / 4 * widthM;
345    }
346
347    function calculateRollFabricVolume(diameterCm, widthCm, coreDiameterCm) {
348        var diameter = number(diameterCm, "Диаметр рулона", 1, 500);
349        var diameterM = diameter / 100;
350        var coreM = number(coreDiameterCm == null ? 0 : coreDiameterCm, "Диаметр втулки", 0, diameter) / 100;
351        var widthM = number(widthCm, "Ширина рулона", 1, 1000) / 100;
352        return Math.PI * (diameterM * diameterM - coreM * coreM) / 4 * widthM;
353    }
354
355    // Backward-compatible alias: historically this engine exposed the annular
356    // fabric-winding volume under the shorter name.
357    function calculateRollVolume(diameterCm, widthCm, coreDiameterCm) {
358        return calculateRollFabricVolume(diameterCm, widthCm, coreDiameterCm);
359    }
360
361    function calculateNapkins(input) {
362        var width = effectiveFabricWidth(input, 20, 500);
363        var fabricWidth = width.usableCm;
364        var napkinWidth = number(input.napkinWidthCm, "Ширина салфетки", 5, 300);
365        var napkinLength = number(input.napkinLengthCm, "Длина салфетки", 5, 300);
366        var hem = number(input.hemCm == null ? 2 : input.hemCm, "Подгибка", 0, 20);
367        var count = integer(input.quantity, "Количество салфеток", 1, 100000);
368        var cutW = napkinWidth + 2 * hem;
369        var cutL = napkinLength + 2 * hem;
370        var acrossA = Math.floor(fabricWidth / cutW);
371        var acrossB = input.directional ? 0 : Math.floor(fabricWidth / cutL);
372        var useRotated = acrossB > acrossA;
373        var across = useRotated ? acrossB : acrossA;
374        var rowLength = useRotated ? cutW : cutL;
375        if (across < 1) {
376            throw new RangeError("Салфетка с подгибкой не помещается в ширину ткани");
377        }
378        var rows = Math.ceil(count / across);
379        var finished = finishLength(rows * rowLength, input);
380        return { across: across, rows: rows, rotated: useRotated, width: width, result: finished };
381    }
382
383    var PROJECT_PRESETS = {
384        square_scarf: { label: "Косынка или квадратный платок", widthCm: 70, lengthCm: 70, count: 1, hemCm: 1, seamCm: 1, kind: "rectangle" },
385        handkerchief: { label: "Носовой или шейный платок", widthCm: 40, lengthCm: 40, count: 3, hemCm: 0.7, seamCm: 1, kind: "rectangle" },
386        curtain: { label: "Лёгкая занавеска", widthCm: 140, lengthCm: 170, count: 2, hemCm: 4, seamCm: 1, topAllowanceCm: 10, fullness: 1.5, kind: "curtain" },
387        pillow_cover: { label: "Декоративная наволочка-конверт", widthCm: 40, lengthCm: 40, count: 2, hemCm: 2, seamCm: 1.5, flapCm: 20, kind: "pillow" },
388        storage_bag: { label: "Мешочек для хранения", widthCm: 30, lengthCm: 40, count: 2, hemCm: 2, seamCm: 1.5, topAllowanceCm: 6, lining: false, kind: "bag" },
389        organizer: { label: "Настенный органайзер", widthCm: 45, lengthCm: 60, count: 1, hemCm: 2, seamCm: 1.5, pocketWidthCm: 18, pocketLengthCm: 18, pocketCount: 6, kind: "organizer" },
390        patchwork: { label: "Лоскутное полотно из квадратов", widthCm: 60, lengthCm: 80, count: 1, hemCm: 0, seamCm: 0.7, module
390Cm: 10, kind: "patchwork" },
391        tablecloth: { label: "Скатерть", widthCm: 140, lengthCm: 180, count: 1, hemCm: 3, seamCm: 1, kind: "rectangle" },
392        table_runner: { label: "Дорожка на стол", widthCm: 40, lengthCm: 150, count: 1, hemCm: 2, seamCm: 1, kind: "rectangle" },
393        custom_rectangle: { label: "Прямоугольная деталь по своим размерам", widthCm: 50, lengthCm: 50, count: 1, hemCm: 2, seamCm: 1, kind: "rectangle" }
394    };
395
396    function projectPieces(input) {
397        var preset = PROJECT_PRESETS[input.project];
398        if (!preset) {
399            throw new RangeError("Выберите проект");
400        }
401        var width = number(input.widthCm == null ? preset.widthCm : input.widthCm, "Готовая ширина", 3, 500);
402        var length = number(input.lengthCm == null ? preset.lengthCm : input.lengthCm, "Готовая длина", 3, 1000);
403        var count = integer(input.quantity == null ? preset.count : input.quantity, "Количество", 1, 1000);
404        var hem = number(input.hemCm == null ? preset.hemCm : input.hemCm, "Подгибка", 0, 30);
405        var seam = number(input.seamCm == null ? preset.seamCm : input.seamCm, "Припуск на шов", 0, 10);
406        var directional = Boolean(input.directional);
407        var pieces = [];
408        var note = "";
409        var metrics = {};
410
411        if (preset.kind === "rectangle") {
412            pieces.push({ name: preset.label, widthCm: width + 2 * hem, lengthCm: length + 2 * hem, quantity: count, rotatable: !directional });
413            note = "Рассчитаны одинаковые прямоугольные детали с подгибкой по четырём сторонам.";
414        } else if (preset.kind === "curtain") {
415            var fullness = number(input.fullness == null ? preset.fullness : input.fullness, "Коэффициент сборки", 1, 4);
416            var top = number(input.topAllowanceCm == null ? preset.topAllowanceCm : input.topAllowanceCm, "Верхняя обработка", 0, 50);
417            var panelWidth = width * fullness / count;
418            var panelLength = length + top + hem;
419            var repeat = number(input.repeatCm == null ? 0 : input.repeatCm, "Вертикальный раппорт", 0, 500);
420            if (repeat > 0) {
421                panelLength = roundUp(panelLength, repeat);
422            }
423            pieces.push({ name: "Полотно занавески", widthCm: panelWidth + 2 * hem, lengthCm: panelLength, quantity: count, rotatable: false });
424            metrics.panelWidthCm = panelWidth;
425            metrics.panelLengthCm = panelLength;
426            note = "Готовая общая ширина умножена на коэффициент сборки и разделена на число полотен. Верхняя и нижняя обработки заданы отдельно; вертикальный раппорт при наличии округлён на каждом полотне.";
427        } else if (preset.kind === "pillow") {
428            var flap = number(input.flapCm == null ? preset.flapCm : input.flapCm, "Клапан", 0, 80);
429            pieces.push({ name: "Наволочка-конверт", widthCm: width + 2 * seam, lengthCm: 2 * length + flap + 2 * seam, quantity: count, rotatable: !directional });
430            note = "Каждая наволочка рассчитана одной деталью с клапаном. Застёжка, кант и совмещение рисунка требуют отдельной проверки.";
431        } else if (preset.kind === "bag") {
432            var bagTop = number(input.topAllowanceCm == null ? preset.topAllowanceCm : input.topAllowanceCm, "Верхняя обработка", 0, 50);
433            var lining = input.lining == null ? Boolean(preset.lining) : Boolean(input.lining);
434            pieces.push({ name: "Основная деталь мешочка", widthCm: width + 2 * seam, lengthCm: length + bagTop + seam, quantity: count * 2, rotatable: !directional });
435            if (lining) {
436                pieces.push({ name: "Подкладка мешочка", widthCm: width + 2 * seam, lengthCm: length + bagTop + seam, quantity: count * 2, rotatable: !directional });
437            }
438            note = "На один мешочек учтены две основные детали" + (lining ? " и две детали подкладки" : " без подкладки") + ". Шнур и кулиска сверх заданной верхней обработки не включены.";
439        } else if (preset.kind === "organizer") {
440            var pocketWidth = number(input.pocketWidthCm == null ? preset.pocketWidthCm : input.pocketWidthCm, "Ширина кармана", 3, 300);
441            var pocketLength = number(input.pocketLengthCm == null ? preset.pocketLengthCm : input.pocketLengthCm, "Высота кармана", 3, 300);
442            var pocketCount = integer(input.pocketCount == null ? preset.pocketCount : input.pocketCount, "Количество карманов", 1, 100);
443            pieces.push({ name: "Основа органайзера", widthCm: width + 2 * seam, lengthCm: length + 2 * seam, quantity: count, rotatable: !directional });
444            pieces.push({ name: "Карман органайзера", widthCm: pocketWidth + 2 * seam, lengthCm: pocketLength + 2 * seam, quantity: count * pocketCount, rotatable: !directional });
445            note = "Учтены основа и одинаковые накладные карманы. Планки, подвес, подкладка и уплотнитель в расчёт не входят.";
446        } else if (preset.kind === "patchwork") {
447            var module = number(input.moduleCm == null ? preset.moduleCm : input.moduleCm, "Сторона готового квадрата", 2, 100);
448            var columns = Math.ceil(width / module);
449            var rows = Math.ceil(length / module);
450            pieces.push({ name: "Лоскутный квадрат", widthCm: module + 2 * seam, lengthCm: module + 2 * seam, quantity: count * columns * rows, rotatable: !directional });
451            metrics.columns = columns;
452            metrics.rows = rows;
453            metrics.squareCount = count * columns * rows;
454            note = "Рассчитаны только квадраты верха с заданным припуском. Бордюры, подкладка, наполнитель, подбор и совмещение рисунка не включены.";
455        }
456
457        return { preset: preset, pieces: pieces, note: note, metrics: metrics };
458    }
459
460    function calculateProject(input) {
461        var spec = projectPieces(input);
462        var width = effectiveFabricWidth(input, 20, 500);
463        var packed = packRectangles(spec.pieces, width.usableCm, input.directional);
464        var finished = finishLength(packed.layoutLengthCm, input);
465        return {
466            kind: "project",
467            preset: spec.preset.label,
468            pieces: spec.pieces,
469            layout: packed,
470            width: width,
471            metrics: spec.metrics,
472            result: finished,
473            disclaimer: spec.note + " Перед покупкой проверьте технологию обработки и сделайте карту раскроя."
474        };
475    }
476
477    return {
478        beddingPieces: beddingPieces,
479        packRectangles: packRectangles,
480        calculateBedding: calculateBedding,
481        calculateGarment: calculateGarment,
482        calculateArea: calculateArea,
483        calculateRollWeight: calculateRollWeight,
484        calculateRollVolume: calculateRollVolume,
485        calculateRollOuterVolume: calculateRollOuterVolume,
486        calculateRollFabricVolume: calculateRollFabricVolume,
487        calculateNapkins: calculateNapkins,
488        calculateProject: calculateProject,
489        garmentPresets: GARMENT_DEFAULTS,
490        projectPresets: PROJECT_PRESETS,
491        effectiveFabricWidth: effectiveFabricWidth
492    };
493}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.