// ============================================================================ // РАЗВЕДКА БАЗИСА — что программа вообще умеет отдать про модель // // Этот скрипт НИЧЕГО НЕ МЕНЯЕТ в модели. Он открывает её, перебирает всё, до // чего может дотянуться, пишет отчёт // // %TEMP%\kk3d-razvedka.txt // // и сам отправляет его на сайт — искать файл руками не нужно. Отчёт нужен, // чтобы понять, какие сведения Базис отдаёт на самом деле: имена свойств у // разных версий отличаются, и угадывать их вслепую нельзя. По этому отчёту // дальше и строится выгрузка. // // Куда класть: в любую папку, которую Базис видит как папку скриптов. // Запускать на открытой модели — лучше на такой, где есть всё сразу: // скруглённые детали, разные кромки, присадка, фасады, столешница, // погонаж и побольше разной фурнитуры. // ============================================================================ var KK_RZ_VER = '1.5'; var KK_SITE = 'https://view.furnit.market'; var KK_TOKEN = 'f894463bd5cc6f3ed946691aabb54396e3954fe29777c61f'; var RZ = []; // строки отчёта var RZ_MAX_LINES = 20000; // ограничитель: отчёт должен остаться читаемым function rz(s) { if (RZ.length < RZ_MAX_LINES) RZ[RZ.length] = (s === undefined || s === null) ? '' : String(s); } function rzHead(s) { rz(''); rz('=================================================================='); rz(s); rz('=================================================================='); } function rzStr(v) { if (v === null || v === undefined) return ''; return String(v).replace(/[\r\n\t]+/g, ' / '); } function rzNum(v) { var n = Number(v); if (!isFinite(n)) return String(v); return String(Math.round(n * 1000) / 1000); } /* Класс объекта — только строкой: instanceof в Базисе роняет скрипт, если такого имени в этой версии нет. */ function rzClass(o) { var s = ''; try { s = String(o); } catch (e) { return '(не отдался)'; } var m = s.match(/\[object\s+([^\]]+)\]/); return m ? m[1] : s.substring(0, 40); } /* Какие методы можно звать. Разведка обязана только СМОТРЕТЬ: в Базисе есть методы, которые молча меняют модель (OrderContours «выстраивает элементы контура в одном направлении», Add/Delete/Clear правят списки). Поэтому зовём лишь те, что заведомо просто отвечают; остальные помечаем как метод и не трогаем. */ var RZ_CALL_OK = { IsLine: 1, IsArc: 1, IsCircle: 1, IsClosed: 1, IsContourRectangle: 1, IsRectangle: 1, IsThrough: 1, IsTape: 1, IsCurved: 1, IsCurve: 1, IsCurvilinear: 1, IsBand: 1, IsButt: 1, IsPanelRotated: 1, IsAssemblyUnit: 1, HasAnimation: 1, ObjLength: 1, Length: 1, Area: 1, Square: 1, Perimeter: 1, Radius: 1, CirRadius: 1, ArcRadius: 1, ArcAngle: 1, Pos1Angle: 1, Pos2Angle: 1, ArcDir: 1, PathAbsolute: 1, NameWithCode: 1, MaterialName: 1, Name: 1, Designation: 1, Code: 1, Article: 1, Thickness: 1, Width: 1, Height: 1, Depth: 1, Weight: 1, Price: 1, Cost: 1, Count: 1, Center: 1, Pos1: 1, Pos2: 1, GSize: 1, Size: 1, GabMin: 1, GabMax: 1, Normal: 1 }; /* ЗАПРЕТ поверх белого списка — на случай, если имя попадёт в список по недосмотру. Стоило это дорого: разведчик 1.1 (25.08.2026, прожил считанные минуты) звал КАЖДЫЙ найденный метод, в списках имён у контура и у кромок стояли Clear/Delete — и `Clear()` вычистил контур и кромки у пяти деталей живой кухни владельца. Имя, начинающееся с любого из этих слов, не зовём НИКОГДА. */ var RZ_NEVER = ['Clear', 'Delete', 'Remove', 'Add', 'Set', 'Insert', 'Exchange', 'Extract', 'Move', 'Transform', 'Rotate', 'Scale', 'Merge', 'Build', 'Assign', 'Fit', 'Elastic', 'Invert', 'Subtract', 'Addition', 'Rounding', 'Facet', 'Order', 'Load', 'Save', 'Make', 'Roll', 'Copy', 'Sort', 'Fix', 'Sync', 'Convert', 'Approximate', 'Bend', 'Unbend', 'Triangulate', 'Draw', 'Symmetry']; function rzForbidden(name) { var i; for (i = 0; i < RZ_NEVER.length; i++) { if (name.substring(0, RZ_NEVER[i].length) === RZ_NEVER[i]) return true; } return false; } /* Свойство или метод: одно и то же имя в Базисе бывает и тем, и другим (так ведут себя PathAbsolute и HasAnimation). */ function rzGet(o, name) { var v; try { v = o[name]; } catch (e) { return { how: 'ошибка', val: null }; } if (v === undefined) return { how: 'нет', val: null }; if (v === null) return { how: 'null', val: null }; if (typeof v === 'function') { if (rzForbidden(name)) return { how: 'метод (не вызывали: правит модель)', val: null }; if (!RZ_CALL_OK[name]) return { how: 'метод (не вызывали: может менять модель)', val: null }; try { return { how: 'метод()', val: o[name]() }; } catch (e2) { return { how: 'метод(с аргументами)', val: null }; } } return { how: 'свойство', val: v }; } /* Значение в строку: объекты разворачиваем на один уровень — у Базиса точки и векторы приходят объектами, и без этого в отчёте была бы пустота. */ function rzVal(v) { if (v === null || v === undefined) return ''; if (typeof v !== 'object') return rzStr(v); var s = rzClass(v), extra = [], k, names = ['x', 'y', 'z', 'Count', 'Name', 'MaterialName', 'FileName']; for (k = 0; k < names.length; k++) { var g = rzGet(v, names[k]); if (g.how === 'нет' || g.how === 'ошибка' || g.val === null) continue; if (typeof g.val === 'object') continue; extra[extra.length] = names[k] + '=' + rzStr(g.val); } return '{' + s + (extra.length ? ' ' + extra.join(' ') : '') + '}'; } /* ПОЛНЫЙ перебор свойств. Первая разведка показала, что for..in у объектов Базиса работает и отдаёт весь список — значит имена больше не надо угадывать: берём всё, что есть, и печатаем со значениями. Методы не зовём (кроме заведомо безответных), а помечаем. */ function rzDump(o, title) { if (!o) { rz(' ' + title + ': объекта нет'); return; } rz(' ' + title + ' [класс ' + rzClass(o) + '] — ВСЁ, что отдал Базис:'); var names = [], k, i, g, meth = []; try { for (k in o) { names[names.length] = k; if (names.length > 200) break; } } catch (e) {} if (!names.length) { rz(' перебор свойств у этого объекта не работает'); return; } for (i = 0; i < names.length; i++) { g = rzGet(o, names[i]); if (g.how === 'нет' || g.how === 'ошибка') { meth[meth.length] = names[i] + '(' + g.how + ')'; continue; } if (g.val === null && g.how.substring(0, 5) === 'метод') { meth[meth.length] = names[i]; continue; } if (g.how === 'метод(с аргументами)') { meth[meth.length] = names[i] + '(нужны аргументы)'; continue; } rz(' ' + names[i] + ' = ' + rzVal(g.val)); } if (meth.length) rz(' действия и недоступное: ' + meth.join(', ')); } /* Сводка по ВСЕМ кромкам модели: какие значения встречаются у каждого свойства и на каких деталях. Именно она отвечает на главный вопрос — отличается ли чем-нибудь криволинейная лента от прямолинейной. */ var rzButtRows = []; var RZ_BUTT_KEYS = ['ButtType', 'Overhung', 'ClipPanel', 'Sign', 'CutAssigned', 'CutIndex', 'Allowance', 'Thickness', 'Width', 'FrontFace', 'Checked', 'Profile', 'Tag']; function rzCollectButts(obj, pname) { var coll = null, cnt = 0, j, b; try { coll = obj.Butts; } catch (e) { return; } if (!coll) return; try { cnt = coll.Count; } catch (e) { cnt = 0; } if (!(cnt > 0)) return; for (j = 0; j < cnt && j < 32; j++) { b = null; try { b = coll.Butts[j]; } catch (e) { b = null; } if (!b) { try { b = coll.Items[j]; } catch (e) { b = null; } } if (!b) { try { b = coll[j]; } catch (e) { b = null; } } if (!b) continue; var row = { part: pname, mat: '', v: {} }, q, g; /* Имя ленты: обычно Базис отдаёт строкой, но если вдруг объектом — берём имя из него, иначе в сводку попадёт «[object TMaterial]» */ try { var bm0 = b.Material; if (bm0 && typeof bm0 === 'object') row.mat = rzStr(bm0.MaterialName || bm0.Name); else row.mat = rzStr(bm0); } catch (e) {} for (q = 0; q < RZ_BUTT_KEYS.length; q++) { g = rzGet(b, RZ_BUTT_KEYS[q]); row.v[RZ_BUTT_KEYS[q]] = (g.how === 'нет' || g.how === 'ошибка' || g.val === null) ? '' : rzVal(g.val); } if (rzButtRows.length < 4000) rzButtRows[rzButtRows.length] = row; } } /* Перебор имён-кандидатов: что нашлось — с значением, чего нет — списком. */ function rzProbe(o, names, title) { if (!o) { rz(' ' + title + ': объекта нет'); return; } rz(' ' + title + ' [класс ' + rzClass(o) + ']'); var found = [], missed = [], i, g; for (i = 0; i < names.length; i++) { g = rzGet(o, names[i]); if (g.how === 'нет') { missed[missed.length] = names[i]; continue; } if (g.how === 'ошибка') { missed[missed.length] = names[i] + '(ошибка)'; continue; } if (g.how === 'метод(с аргументами)') { found[found.length] = names[i] + ' = метод с аргументами'; continue; } if (g.val === null && g.how.substring(0, 5) === 'метод') { found[found.length] = names[i] + ' = ' + g.how; continue; } found[found.length] = names[i] + ' = ' + rzVal(g.val) + (g.how === 'метод()' ? ' [метод]' : ''); } for (i = 0; i < found.length; i++) rz(' ' + found[i]); if (missed.length) rz(' НЕТ: ' + missed.join(', ')); /* IsButtVisible зовём с аргументами отдельно — здесь только помечаем */ var own = [], k2; try { for (k2 in o) { own[own.length] = k2; if (own.length > 60) break; } } catch (e) {} if (own.length) rz(' перебор for..in дал: ' + own.join(', ')); } // ------------------------------------------------------------ списки имён var RZ_COMMON = ['Name', 'Names', 'Caption', 'Text', 'UID', 'ID', 'Id', 'Index', 'Count', 'Objects', 'Items', 'Parent', 'Owner', 'Visible', 'Enabled', 'Selected', 'Marked', 'Locked', 'Level', 'Designation', 'Code', 'Article', 'Art', 'ArtPos', 'Pos', 'Position', 'Notes', 'Note', 'Comment', 'Info', 'UserData', 'Tag', 'Kind', 'Type', 'ObjType', 'ElType', 'Class', 'ClassName']; var RZ_PANEL = ['GSize', 'Size', 'Width', 'Height', 'Length', 'Thickness', 'Depth', 'Material', 'MaterialName', 'MaterialWidth', 'MaterialLength', 'Texture', 'TextureAngle', 'TextureOrientation', 'Contour', 'Butts', 'Plastics', 'Holes', 'Grooves', 'Cuts', 'Millings', 'Direction', 'GrainDirection', 'Grain', 'IsAssemblyUnit', 'MakeAssemblyDrawing', 'DatumMode', 'GabMin', 'GabMax', 'ToGlobal', 'NToGlobal', 'ToLocal', 'Normal', 'TriLists', 'TriListsCount', 'HasAnimation', 'IsButtVisible', 'IsPanelRotated', 'Rotated', 'Area', 'Square', 'Perimeter', 'Weight', 'Mass', 'Price', 'Cost', 'Quantity', 'Qty', 'Amount', 'Number', 'Num', /* найдено перебором for..in у живой панели 25.08.2026 — второй путь к форме детали и всё, что рядом с ним; по ним ищем причину пустого контура */ 'Contours', 'ContourWidth', 'ContourHeight', 'Bent', 'BentContour', 'BentDirection', 'InterlayerThickness', 'Source', 'AsPanel', 'FrontFace', 'ZThickness', 'MiddleZ', 'HighZ', 'LowZ']; var RZ_BUTT = ['Material', 'MaterialName', 'Name', 'Designation', 'Code', 'Article', 'Art', 'ElemIndex', 'Index', 'Elem', 'Element', 'Side', 'Face', 'Length', 'Thickness', 'Width', 'Allowance', 'Padding', 'Overhang', 'Sves', 'Ledge', 'Offset', 'Gap', 'Trim', 'Cut', 'CutPanel', 'TrimPanel', 'Cutting', 'Fugue', 'Fugovka', 'Curved', 'IsCurved', 'Curvilinear', 'Curve', 'IsCurve', 'Radius', 'MinRadius', 'BendType', 'ButtType', 'Kind', 'Type', 'Mode', 'Method', 'Tape', 'TapeMode', 'Band', 'IsTape', 'Visible', 'Glue', 'Machine', 'Station', 'Price', 'Cost']; var RZ_CONT_EL = ['Pos1', 'Pos2', 'Center', 'Radius', 'CirRadius', 'ArcRadius', 'ArcDir', 'Direction', 'ArcAngle', 'Pos1Angle', 'Pos2Angle', 'Bulge', 'IsLine', 'IsArc', 'IsCircle', 'AsArc', 'AsLine', 'ElType', 'ObjType', 'Kind', 'Type', 'ObjLength', 'Length', 'Index', 'Number', 'Butt', 'ButtIndex', 'Material', 'MaterialName', 'Visible', 'Marked', 'PointAt', 'MidPoint', 'Middle', 'StartPoint', 'EndPoint', 'Tangent', 'Normal']; var RZ_CONTOUR = ['Count', 'Objects', 'Items', 'IsContourRectangle', 'IsRectangle', 'OrderContours', 'Order', 'Area', 'Square', 'Perimeter', 'Length', 'GabMin', 'GabMax', 'Closed', 'IsClosed', 'Add', 'Clear', 'Delete']; var RZ_MAT = ['MaterialName', 'Name', 'Designation', 'Code', 'Article', 'Art', 'NameWithCode', 'Path', 'PathAbsolute', 'FileName', 'Texture', 'TextureName', 'Bitmap', 'Image', 'Picture', 'DiffuseColor', 'Color', 'AmbientColor', 'SpecularColor', 'TransparentValue', 'MirrorValue', 'Thickness', 'Width', 'Length', 'Height', 'Density', 'Price', 'Cost', 'Unit', 'Units', /* свойства именно ленты: свес и признак криволинейной кромки — их ищем и у самой кромки, и у её материала, потому что где они лежат, заранее не известно */ 'Overhang', 'Sves', 'Ledge', 'Curved', 'IsCurved', 'Curvilinear', 'IsCurvilinear', 'BandType', 'ButtType', 'MinRadius', 'Radius', 'Machine', 'TexSX', 'TexSY', 'TexW', 'TexH', 'Angle', 'TexAngle', 'Orientation', 'OffsetX', 'OffsetY', 'MtList', 'Kind', 'Type', 'MatType', 'IsBand', 'IsButt', 'Group', 'GroupName', 'Folder']; var RZ_HOLE = ['Radius', 'Diameter', 'Depth', 'Position', 'Point', 'Pos', 'Direction', 'Vector', 'Through', 'IsThrough', 'DrillMode', 'Mode', 'Type', 'Kind', 'Tool', 'Side', 'Face', 'Index']; var RZ_BLOCK = ['Name', 'Designation', 'ArtPos', 'Notes', 'IsAssemblyUnit', 'MakeAssemblyDrawing', 'Count', 'Objects', 'GabMin', 'GabMax', 'GSize', 'Price', 'Cost', 'Weight', 'Quantity', 'HasAnimation', 'ToGlobal', 'Level', 'Parent']; var RZ_MODEL = ['Name', 'FileName', 'Path', 'Count', 'Objects', 'MtList', 'Materials', 'Order', 'OrderNumber', 'Customer', 'Client', 'Author', 'Date', 'Comment', 'Notes', 'Description', 'GabMin', 'GabMax', 'Price', 'Cost', 'Weight', 'Version', 'Units']; var RZ_GLOBAL = ['Model', 'MtList', 'Action', 'Application', 'App', 'Basis', 'System', 'Document', 'ActiveModel', 'CurrentModel', 'currentFileData', 'Params', 'Settings', 'Options', 'Config']; // ------------------------------------------------------ обход модели var rzSeen = {}, rzOrder = []; // класс → сколько встретилось var rzSample = {}; // класс → первый образец var rzPanels = [], rzFast = [], rzBlocks = [], rzProfiles = []; var rzHealth = []; // по строчке на каждую панель: есть ли контур и кромки /* Коротко о панели: сколько кусков контура и кромок Базис по ней отдал. Только чтение свойств, ничего не вызываем сверх белого списка. */ function rzPanelHealth(obj) { var h = { obj: obj, name: '', dz: '', bp: '', cc: -1, nb: -1, gs: '', mt: '' }; try { h.name = rzStr(obj.Name); } catch (e) {} try { h.dz = rzStr(obj.Designation); } catch (e) {} try { h.bp = rzStr(obj.ArtPos); } catch (e) {} try { h.mt = rzStr(obj.MaterialName); } catch (e) {} try { h.cc = Number(obj.Contour.Count); } catch (e) { h.cc = -1; } try { h.nb = Number(obj.Butts.Count); } catch (e) { h.nb = -1; } if (!isFinite(h.cc)) h.cc = -1; if (!isFinite(h.nb)) h.nb = -1; try { var g = obj.GSize; if (g && g.x !== undefined) h.gs = Math.round(g.x) + '×' + Math.round(g.y) + '×' + Math.round(g.z); } catch (e) {} return h; } var rzDeep = 0; /* Фурнитура бывает очень разной: петли, направляющие, опоры, ручки, полкодержатели. Берём по образцу КАЖДОГО названия, а не первый попавшийся — иначе про все виды, кроме конфирмата, мы так ничего и не узнаем. */ var rzFastByName = {}, rzFastNames = []; /* То же с материалами: у ЛДСП, ХДФ, МДФ, плёнки, столешницы и кромки свойства заполнены по-разному. */ var rzMatByName = {}, rzMatNames = []; function rzWalk(obj, depth) { if (!obj || depth > 12) return; var cls = rzClass(obj); if (!rzSeen[cls]) { rzSeen[cls] = 0; rzOrder[rzOrder.length] = cls; rzSample[cls] = obj; } rzSeen[cls]++; if (depth > rzDeep) rzDeep = depth; if (cls === 'TFurnPanel' && rzPanels.length < 400) rzPanels[rzPanels.length] = obj; /* Здоровье КАЖДОЙ панели, а не только образцов: у живой кухни владельца пять деталей Базис отдал без контура и без кромок, и надо видеть все такие, а не первую попавшуюся. */ if (cls === 'TFurnPanel' && rzHealth.length < 4000) rzHealth[rzHealth.length] = rzPanelHealth(obj); if (cls === 'TFastener' && rzFast.length < 40) rzFast[rzFast.length] = obj; if ((cls === 'TFurnBlock' || cls === 'TFurnAsm' || cls === 'TAsmKit') && rzBlocks.length < 40) rzBlocks[rzBlocks.length] = obj; if (cls === 'TExtrusionBody' && rzProfiles.length < 40) rzProfiles[rzProfiles.length] = obj; /* кромки — со ВСЕХ деталей, для сводки по видам кромки */ if (cls === 'TFurnPanel') { var pn0 = ''; try { pn0 = rzStr(obj.Name); } catch (e) { pn0 = ''; } try { rzCollectButts(obj, pn0); } catch (e) {} } /* образец на каждое название фурнитуры */ if (cls === 'TFastener' && rzFastNames.length < 60) { var fn = ''; try { fn = rzStr(obj.Name); } catch (e) { fn = ''; } if (!fn) fn = '(без имени)'; if (!rzFastByName[fn]) { rzFastByName[fn] = { obj: obj, n: 0 }; rzFastNames[rzFastNames.length] = fn; } rzFastByName[fn].n++; } else if (cls === 'TFastener') { var fn2 = ''; try { fn2 = rzStr(obj.Name) || '(без имени)'; } catch (e) { fn2 = '(без имени)'; } if (rzFastByName[fn2]) rzFastByName[fn2].n++; } /* образец на каждый материал детали */ if (cls === 'TFurnPanel' && rzMatNames.length < 40) { var mn = ''; try { mn = rzStr(obj.MaterialName); } catch (e) { mn = ''; } if (!mn) { try { mn = rzStr(obj.Material.MaterialName); } catch (e) { mn = ''; } } if (mn && !rzMatByName[mn]) { var mo = null; try { mo = obj.Material; } catch (e) { mo = null; } rzMatByName[mn] = { obj: mo, n: 0 }; rzMatNames[rzMatNames.length] = mn; } if (mn && rzMatByName[mn]) rzMatByName[mn].n++; } var cnt = 0; try { cnt = obj.Count; } catch (e) { cnt = 0; } if (!(cnt > 0)) return; var i, kid; for (i = 0; i < cnt && i < 4000; i++) { kid = null; try { kid = obj.Objects[i]; } catch (e) { kid = null; } if (!kid) { try { kid = obj[i]; } catch (e) { kid = null; } } if (kid) rzWalk(kid, depth + 1); } } /* Панели-образцы: берём разные — с кромками, скруглённая, с облицовкой, с присадкой, столешница. По одной каждого вида хватает, чтобы увидеть все свойства, а отчёт остаётся коротким. */ function rzPickPanels() { var out = [], seenKind = {}, i, p, kind, nb, cur, np, mw; for (i = 0; i < rzPanels.length; i++) { p = rzPanels[i]; nb = 0; cur = 0; np = 0; mw = 0; /* Деталь с пустым контуром сюда не берём: на IsContourRectangle Базис ответит «не прямоугольник» про пустоту, и такая деталь заняла бы место образца скруглённой. Для них есть свой раздел. */ var cc0 = -1; try { cc0 = Number(p.Contour.Count); } catch (e) { cc0 = -1; } if (!(cc0 > 0)) continue; try { nb = p.Butts.Count; } catch (e) { nb = 0; } try { cur = p.Contour.IsContourRectangle() ? 0 : 1; } catch (e) { cur = 0; } try { np = p.Plastics.Count; } catch (e) { np = 0; } try { mw = Number(p.MaterialWidth) || 0; } catch (e) { mw = 0; } kind = mw > 0 ? 'погонаж (столешница, профиль)' : cur ? 'деталь со скруглением или вырезом' : nb > 2 ? 'деталь с несколькими кромками' : nb ? 'деталь с кромкой' : np ? 'деталь с облицовкой пласти' : 'простая деталь'; if (seenKind[kind]) continue; seenKind[kind] = 1; out[out.length] = { obj: p, kind: kind, nb: nb, cur: cur, np: np, mw: mw }; if (out.length >= 6) break; } return out; } function rzPanelReport(rec, no) { var p = rec.obj, nm = ''; try { nm = rzStr(p.Name); } catch (e) {} rzHead('ДЕТАЛЬ ' + no + ' — ' + rec.kind + ': «' + nm + '»'); rz(' кромок ' + rec.nb + ', облицовок ' + rec.np + ', контур ' + (rec.cur ? 'НЕ прямоугольный' : 'прямоугольный') + (rec.mw ? ', ширина материала ' + rec.mw + ' (Базис считает это погонажем)' : '')); rzProbe(p, RZ_COMMON, 'общие свойства детали'); rzProbe(p, RZ_PANEL, 'свойства детали'); // ----- материал детали var mt = null; try { mt = p.Material; } catch (e) { mt = null; } rz(''); rzProbe(mt, RZ_MAT, 'материал детали'); // ----- контур var c = null; try { c = p.Contour; } catch (e) { c = null; } rz(''); rzProbe(c, RZ_CONTOUR, 'контур детали'); if (c) { var cc = 0, k, el; try { cc = c.Count; } catch (e) { cc = 0; } rz(' кусков контура: ' + cc); for (k = 0; k < cc && k < 8; k++) { el = null; try { el = c.Objects[k]; } catch (e) { el = null; } if (!el) { try { el = c[k]; } catch (e) { el = null; } } if (!el) { rz(' кусок ' + k + ': не отдался'); continue; } rz(''); rzDump(el, 'кусок контура ' + k); /* виден ли торец этого куска снаружи: у стыкованных деталей кромку не клеят, и это надо уметь различать */ var vb = rzGet(p, 'IsButtVisible'); if (vb.how !== 'нет' && vb.how !== 'ошибка') { var vv = ''; try { vv = String(p.IsButtVisible(k, 0.2)); } catch (e) { vv = '(с такими аргументами не работает)'; } rz(' IsButtVisible(' + k + ', 0.2) = ' + vv); } } if (cc > 8) rz(' …и ещё ' + (cc - 8) + ' кусков (в отчёт не пишем)'); } // ----- кромки var bt = null; try { bt = p.Butts; } catch (e) { bt = null; } rz(''); rzProbe(bt, ['Count', 'Objects', 'Items', 'Butts', 'Add', 'Clear', 'Delete'], 'список кромок детали'); if (bt) { var bc = 0, j, b; try { bc = bt.Count; } catch (e) { bc = 0; } for (j = 0; j < bc && j < 6; j++) { b = null; try { b = bt.Butts[j]; } catch (e) { b = null; } if (!b) { try { b = bt.Items[j]; } catch (e) { b = null; } } if (!b) { try { b = bt[j]; } catch (e) { b = null; } } if (!b) { rz(' кромка ' + j + ': не отдалась ни одним путём (Butts[j], Items[j], [j])'); continue; } rz(''); rzDump(b, 'кромка ' + j); /* объект подрезки: в Базисе это галочка «подрезать» у облицовки */ var bcut = null; try { bcut = b.Cut; } catch (e) { bcut = null; } if (bcut && typeof bcut === 'object') rzDump(bcut, 'подрезка (Cut) кромки ' + j); /* материал самой ленты: где искать обозначение, артикул и, возможно, признак криволинейной кромки */ var bm = null; try { bm = b.Material; } catch (e) { bm = null; } if (bm && typeof bm === 'object') { rzDump(bm, 'материал кромки ' + j + ' (объектом)'); } else { rz(' материал кромки ' + j + ' отдан строкой: «' + rzStr(bm) + '»'); var byName = rzFindMaterial(rzStr(bm)); if (byName) rzDump(byName, 'он же, найденный в списке материалов'); else rz(' в списке материалов модели по этому имени ничего не нашлось'); } } if (bc > 6) rz(' …и ещё ' + (bc - 6) + ' кромок (в отчёт не пишем)'); } // ----- облицовки пластей var pl = null; try { pl = p.Plastics; } catch (e) { pl = null; } if (pl) { rz(''); rzProbe(pl, ['Count', 'Objects', 'Items', 'Plastics'], 'список облицовок пласти'); var pc = 0, q, pp; try { pc = pl.Count; } catch (e) { pc = 0; } for (q = 0; q < pc && q < 3; q++) { pp = null; try { pp = pl.Plastics[q]; } catch (e) { pp = null; } if (!pp) { try { pp = pl.Items[q]; } catch (e) { pp = null; } } if (!pp) { try { pp = pl[q]; } catch (e) { pp = null; } } if (pp) { rz(''); rzProbe(pp, ['Material', 'MaterialName', 'Name', 'Side', 'Sign', 'Thickness', 'Width', 'Type', 'Kind'], 'облицовка ' + q); } } } // ----- присадка на самой детали var hl = null; try { hl = p.Holes; } catch (e) { hl = null; } if (hl) { rz(''); rzProbe(hl, ['Count', 'Objects', 'Items', 'Holes'], 'присадка детали'); var hc = 0, h, hh; try { hc = hl.Count; } catch (e) { hc = 0; } for (h = 0; h < hc && h < 3; h++) { hh = null; try { hh = hl.Holes[h]; } catch (e) { hh = null; } if (!hh) { try { hh = hl.Items[h]; } catch (e) { hh = null; } } if (!hh) { try { hh = hl[h]; } catch (e) { hh = null; } } if (hh) { rz(''); rzProbe(hh, RZ_HOLE, 'отверстие ' + h); } } } } /* Материал по имени из списков материалов модели. */ var rzLists = null; function rzMtLists() { if (rzLists) return rzLists; rzLists = []; var add = function (L, how) { if (L) rzLists[rzLists.length] = { L: L, how: how }; }; try { if (typeof MtList !== 'undefined' && MtList) add(MtList, 'глобальный MtList'); } catch (e) {} try { if (typeof Model !== 'undefined' && Model && Model.MtList) add(Model.MtList, 'Model.MtList'); } catch (e) {} try { if (typeof Action !== 'undefined' && Action && Action.MtList) add(Action.MtList, 'Action.MtList'); } catch (e) {} var i; for (i = 0; i < rzPanels.length && i < 5; i++) { try { if (rzPanels[i].Material && rzPanels[i].Material.MtList) { add(rzPanels[i].Material.MtList, 'MtList у материала детали'); break; } } catch (e) {} } return rzLists; } function rzFindMaterial(nm) { if (!nm) return null; var ls = rzMtLists(), i, mt; for (i = 0; i < ls.length; i++) { mt = null; try { if (ls[i].L.MaterialExists(nm)) mt = ls[i].L.AddMaterial(nm); } catch (e) { mt = null; } if (mt) return mt; } return null; } // ============================================================ запуск (function () { var t0 = new Date(); rz('РАЗВЕДКА БАЗИСА, версия ' + KK_RZ_VER); rz('Дата: ' + t0.toLocaleString()); rz(''); rz('Этот отчёт ничего не отправляет и ничего не меняет в модели.'); rz('Он показывает, какие сведения Базис отдаёт скрипту: имена свойств у'); rz('разных версий отличаются, и по этому отчёту делается выгрузка.'); // ---- что вообще доступно снаружи rzHead('ЧТО ДОСТУПНО СКРИПТУ (глобальные имена)'); var i, g, nm; for (i = 0; i < RZ_GLOBAL.length; i++) { nm = RZ_GLOBAL[i]; var v; try { v = eval('typeof ' + nm + ' !== "undefined" ? ' + nm + ' : undefined'); } catch (e) { v = undefined; } if (v === undefined || v === null) { rz(' ' + nm + ' — нет'); continue; } rz(' ' + nm + ' — есть ' + rzVal(v)); } // ---- модель var mdl = null; try { mdl = Model; } catch (e) { mdl = null; } rzHead('МОДЕЛЬ ЦЕЛИКОМ'); rzProbe(mdl, RZ_MODEL, 'свойства модели'); var cfd = null; try { cfd = currentFileData; } catch (e) { cfd = null; } rzProbe(cfd, ['filename', 'FileName', 'path', 'Path', 'name', 'Name', 'dir', 'Dir', 'changed', 'Changed'], 'сведения о файле модели'); // ---- обход if (mdl) rzWalk(mdl, 0); rzHead('ЧТО ВСТРЕТИЛОСЬ В МОДЕЛИ'); rz(' глубина вложенности: ' + rzDeep); for (i = 0; i < rzOrder.length; i++) rz(' ' + rzOrder[i] + ' — ' + rzSeen[rzOrder[i]]); // ---- по одному образцу каждого класса: общие свойства rzHead('ПЕРВЫЙ ОБЪЕКТ КАЖДОГО ВИДА — ОБЩИЕ СВОЙСТВА'); for (i = 0; i < rzOrder.length && i < 20; i++) { rz(''); rzProbe(rzSample[rzOrder[i]], RZ_COMMON, rzOrder[i]); } // ---- детали: подробно, по одной каждого вида var picks = rzPickPanels(); rzHead('ДЕТАЛИ ПОДРОБНО (выбрано ' + picks.length + ' разных)'); for (i = 0; i < picks.length; i++) rzPanelReport(picks[i], i + 1); /* ---- ГЛАВНОЕ по нынешней задаче: детали, про которые Базис молчит. У пяти деталей живой кухни владельца Contour пуст (Count=0) и Butts пуст — торец у них на странице выходит серым. Здесь их перечисление и подробная проба рядом со ЗДОРОВОЙ деталью того же имени: разница между двумя дампами и есть ответ. */ var bad = [], good = [], q; for (q = 0; q < rzHealth.length; q++) { if (rzHealth[q].cc > 0) good[good.length] = rzHealth[q]; else bad[bad.length] = rzHealth[q]; } rzHead('ДЕТАЛИ, У КОТОРЫХ БАЗИС НЕ ОТДАЛ КОНТУР — ' + bad.length + ' из ' + rzHealth.length); if (!bad.length) rz(' таких нет: контур отдался у всех деталей'); else { rz(' У этих деталей Contour.Count = 0 (или список не отдался вовсе).'); rz(' Форму торца по ним построить не из чего, поэтому торец показан серым.'); rz(''); rz(' имя — обозначение (позиция), габарит, кусков контура, кромок, материал:'); for (q = 0; q < bad.length && q < 60; q++) { rz(' ' + (bad[q].name || '(без имени)') + ' — ' + (bad[q].dz || 'обозначения нет') + ' (поз. ' + (bad[q].bp || '—') + '), ' + (bad[q].gs || 'габарит не отдался') + ', кусков ' + (bad[q].cc < 0 ? 'список не отдался' : bad[q].cc) + ', кромок ' + (bad[q].nb < 0 ? 'список не отдался' : bad[q].nb) + ', ' + (bad[q].mt || 'материал не отдался')); } if (bad.length > 60) rz(' …и ещё ' + (bad.length - 60)); /* Сколько таких деталей делят одно обозначение: у живой кухни четыре разные детали шли под одним «01.00.02», и это само по себе примета. */ var byDz = {}, dzOrder = []; for (q = 0; q < bad.length; q++) { var d0 = bad[q].dz || '(нет)'; if (!byDz[d0]) { byDz[d0] = 0; dzOrder[dzOrder.length] = d0; } byDz[d0]++; } rz(''); rz(' по обозначениям: ' + (function () { var s = [], w; for (w = 0; w < dzOrder.length && w < 20; w++) s[s.length] = dzOrder[w] + ' × ' + byDz[dzOrder[w]]; return s.join(', '); })()); /* Подробно: две больные детали и здоровая того же имени — рядом. */ var shown = 0; for (q = 0; q < bad.length && shown < 2; q++) { shown++; rzPanelReport({ obj: bad[q].obj, kind: 'БЕЗ КОНТУРА', nb: bad[q].nb, cur: 1, np: 0, mw: 0 }, 'без контура ' + shown); var twin = null, w2; for (w2 = 0; w2 < good.length; w2++) { if (good[w2].name === bad[q].name) { twin = good[w2]; break; } } if (!twin) { for (w2 = 0; w2 < good.length; w2++) { if (good[w2].gs === bad[q].gs) { twin = good[w2]; break; } } } if (!twin && good.length) twin = good[0]; if (twin) { rz(''); rz(' ↓ для сравнения — ЗДОРОВАЯ деталь: ' + (twin.name || '(без имени)') + ' ' + (twin.gs || '') + ', кусков ' + twin.cc + ', кромок ' + twin.nb); rzPanelReport({ obj: twin.obj, kind: 'здоровая, для сравнения', nb: twin.nb, cur: 0, np: 0, mw: 0 }, 'сравнение ' + shown); } else { rz(' здоровой детали для сравнения в модели не нашлось'); } } } // ---- фурнитура: по образцу на каждое название if (rzFastNames.length) { rzHead('ФУРНИТУРА И КРЕПЁЖ — по образцу на каждое название (' + rzFastNames.length + ')'); var RZ_FAST = ['Name', 'Designation', 'Article', 'Code', 'Art', 'ArtPos', 'Holes', 'DatumMode', 'GSize', 'Size', 'Material', 'MaterialName', 'ToGlobal', 'NToGlobal', 'TriLists', 'TriListsCount', 'Price', 'Cost', 'Quantity', 'Qty', 'Count', 'Weight', 'HasAnimation', 'SalonType', 'AnimType', 'Angle', 'Kind', 'Type', 'Group', 'GroupName', 'Vendor', 'Maker']; for (i = 0; i < rzFastNames.length; i++) { var fnm = rzFastNames[i], frec = rzFastByName[fnm]; rz(''); rz(' --- «' + fnm + '» — ' + frec.n + ' шт.'); rzProbe(frec.obj, RZ_FAST, 'свойства'); /* Отверстия под эту фурнитуру: у конфирмата, минификса, петли и полкодержателя они разные — важно увидеть все виды. */ var fh = null; try { fh = frec.obj.Holes; } catch (e) { fh = null; } if (!fh) { rz(' отверстий нет'); continue; } var fc = 0; try { fc = fh.Count; } catch (e) { fc = 0; } rz(' отверстий: ' + fc); var hmax = (i === 0) ? 4 : 2; // у первого показываем подробнее for (var hj = 0; hj < fc && hj < hmax; hj++) { var hh2 = null; try { hh2 = fh.Holes[hj]; } catch (e) { hh2 = null; } if (!hh2) { try { hh2 = fh.Items[hj]; } catch (e) { hh2 = null; } } if (!hh2) { try { hh2 = fh[hj]; } catch (e) { hh2 = null; } } if (hh2) rzProbe(hh2, RZ_HOLE, 'отверстие ' + hj); } } } /* ---- ГЛАВНОЕ: все кромки модели по видам. Если криволинейная лента чем-то отличается от прямолинейной, разница обязана быть видна здесь: у какого-то свойства окажутся разные значения на разных деталях. */ rzHead('ВСЕ КРОМКИ МОДЕЛИ — ' + rzButtRows.length + ' шт.'); if (!rzButtRows.length) rz(' кромок в модели не нашлось'); else { var kq, vq, key, val, tally, order2, rec2; rz(' какие значения встречаются у каждого свойства кромки:'); for (kq = 0; kq < RZ_BUTT_KEYS.length; kq++) { key = RZ_BUTT_KEYS[kq]; tally = {}; order2 = []; for (vq = 0; vq < rzButtRows.length; vq++) { val = rzButtRows[vq].v[key]; if (val === '') val = '(нет)'; if (!tally[val]) { tally[val] = { n: 0, ex: [] }; order2[order2.length] = val; } tally[val].n++; if (tally[val].ex.length < 3) tally[val].ex[tally[val].ex.length] = rzButtRows[vq].part; } var line = ' ' + key + ': '; for (vq = 0; vq < order2.length && vq < 12; vq++) { rec2 = tally[order2[vq]]; line += order2[vq] + ' — ' + rec2.n + ' шт'; if (order2.length > 1) line += ' (' + rec2.ex.join(', ') + ')'; line += (vq < order2.length - 1 ? '; ' : ''); } rz(line + (order2.length > 12 ? ' …и ещё ' + (order2.length - 12) + ' значений' : '')); /* Свойство с несколькими значениями — самое ценное: именно оно может оказаться признаком криволинейной ленты. */ if (order2.length > 1) rz(' ^^^ у этого свойства ЗНАЧЕНИЯ РАЗНЫЕ — смотреть в первую очередь'); } rz(''); rz(' кромки по лентам:'); var byMat = {}, matOrd = []; for (vq = 0; vq < rzButtRows.length; vq++) { var mk = rzButtRows[vq].mat || '(без имени)'; if (!byMat[mk]) { byMat[mk] = 0; matOrd[matOrd.length] = mk; } byMat[mk]++; } for (vq = 0; vq < matOrd.length && vq < 30; vq++) rz(' ' + matOrd[vq] + ' — ' + byMat[matOrd[vq]] + ' шт'); /* Каждую ленту разбираем целиком: у одной цвет, у другой картинка — и видно это только у самого материала. Прежде в разбор попадала лишь та лента, что оказалась на образцовой детали. */ rz(''); rz(' КАЖДАЯ ЛЕНТА ПОДРОБНО (цвет, картинка, признак «брать цвет»):'); for (vq = 0; vq < matOrd.length && vq < 20; vq++) { if (matOrd[vq] === '(без имени)') continue; var lmt = rzFindMaterial(matOrd[vq]); rz(''); if (!lmt) { rz(' «' + matOrd[vq] + '» — в справочнике по имени НЕ НАШЛАСЬ'); continue; } rzDump(lmt, ' лента «' + matOrd[vq] + '»'); } rz(''); rz(' первые 25 кромок поимённо (деталь → значения):'); for (vq = 0; vq < rzButtRows.length && vq < 25; vq++) { var r2 = rzButtRows[vq], parts2 = []; for (kq = 0; kq < RZ_BUTT_KEYS.length; kq++) { if (r2.v[RZ_BUTT_KEYS[kq]] !== '') parts2[parts2.length] = RZ_BUTT_KEYS[kq] + '=' + r2.v[RZ_BUTT_KEYS[kq]]; } rz(' «' + r2.part + '»: ' + parts2.join(', ')); } } // ---- материалы деталей: по образцу на каждый if (rzMatNames.length) { rzHead('МАТЕРИАЛЫ ДЕТАЛЕЙ — по образцу на каждый (' + rzMatNames.length + ')'); for (i = 0; i < rzMatNames.length; i++) { var mnm = rzMatNames[i], mrec = rzMatByName[mnm]; rz(''); rz(' --- «' + mnm + '» — деталей ' + mrec.n); rzDump(mrec.obj, 'свойства материала'); } } // ---- модули и сборочные единицы if (rzBlocks.length) { rzHead('МОДУЛИ И СБОРОЧНЫЕ ЕДИНИЦЫ'); rz(' всего найдено: ' + rzBlocks.length); for (i = 0; i < rzBlocks.length && i < 3; i++) { rz(''); rzProbe(rzBlocks[i], RZ_BLOCK, 'модуль ' + i); } } // ---- погонаж if (rzProfiles.length) { rzHead('ПОГОНАЖ (профили, цоколи)'); rzProbe(rzProfiles[0], RZ_COMMON, 'общие свойства'); rzProbe(rzProfiles[0], RZ_PANEL, 'свойства погонажа'); } // ---- материалы модели rzHead('СПИСКИ МАТЕРИАЛОВ МОДЕЛИ'); var ls = rzMtLists(); if (!ls.length) rz(' ни одного списка не нашлось'); for (i = 0; i < ls.length; i++) { rz(''); rz(' ' + ls[i].how + ':'); rzProbe(ls[i].L, ['Count', 'Materials', 'Items', 'MaterialExists', 'AddMaterial', 'Material', 'Names'], ' устройство списка'); var cnt2 = 0, j2, mt2, nm2, shown = 0, kromki = 0; try { cnt2 = ls[i].L.Count; } catch (e) { cnt2 = 0; } rz(' материалов в списке: ' + cnt2); /* Список устроен как словарь (у него есть Keys/Values/ToArray), а не как массив: перебор по номеру ничего не даёт. Берём имена ключами. */ var keys = null, kn = 0, karr = null; try { keys = ls[i].L.Keys; } catch (e) { keys = null; } try { kn = keys ? keys.Count : 0; } catch (e) { kn = 0; } if (!kn && keys) { try { kn = keys.length; } catch (e) { kn = 0; } } /* Ключи словаря достаются по-разному: то массивом, то через ToArray. Пробуем все пути — иначе справочник так и останется закрытым. */ try { if (keys && typeof keys.ToArray === 'function') karr = keys.ToArray(); } catch (e) { karr = null; } if (!karr) { try { if (typeof ls[i].L.ToArray === 'function') karr = ls[i].L.ToArray(); } catch (e) { karr = null; } } if (karr && !kn) { try { kn = karr.length || karr.Count || 0; } catch (e) { kn = 0; } } rz(' ключей в словаре: ' + (kn || '(не отдались)') + (karr ? ', ToArray сработал' : '')); for (j2 = 0; j2 < kn && j2 < 3000; j2++) { nm2 = ''; if (karr) { try { nm2 = rzStr(karr[j2]); } catch (e) { nm2 = ''; } if (!nm2) { try { nm2 = rzStr(karr[j2].Key); } catch (e) { nm2 = ''; } } } if (!nm2) { try { nm2 = rzStr(keys[j2]); } catch (e) { nm2 = ''; } } if (!nm2) { try { nm2 = rzStr(keys.Items[j2]); } catch (e) { nm2 = ''; } } if (!nm2) { try { nm2 = rzStr(keys.Item(j2)); } catch (e) { nm2 = ''; } } if (!nm2) continue; if (nm2.toLowerCase().indexOf('кромк') >= 0) { kromki++; if (kromki <= 25) { rz(' кромка: ' + nm2); if (kromki === 1) { mt2 = rzFindMaterial(nm2); if (mt2) rzDump(mt2, ' первая кромка из справочника — все свойства'); } } continue; } if (shown < 15) { rz(' материал: ' + nm2); shown++; } } rz(' всего похожих на кромку в справочнике: ' + kromki); } /* ---- запись. Папку ищем перебором: в Базисе COM-объекты заводятся через NewCOMObject, а ActiveXObject бывает недоступен — на этом первая версия и споткнулась, свалившись на корень диска C:, куда Windows писать не даёт. Пробуем все пути подряд и проверяем записью, а не догадками. */ var txt = RZ.join('\r\n') + '\r\n'; var cand = [], dir = '', file = '', wrote = false; var addDir = function (d) { if (!d) return; d = String(d); if (d.length < 2) return; while (d.length && d.charAt(d.length - 1) === '\\') d = d.substring(0, d.length - 1); var q; for (q = 0; q < cand.length; q++) { if (cand[q] === d) return; } cand[cand.length] = d; }; var mkCom = function (name) { var o = null; try { o = NewCOMObject(name); } catch (e) { o = null; } if (!o) { try { o = new ActiveXObject(name); } catch (e) { o = null; } } return o; }; var shell0 = mkCom('WScript.Shell'); if (shell0) { try { addDir(shell0.ExpandEnvironmentStrings('%TEMP%')); } catch (e) {} try { addDir(shell0.ExpandEnvironmentStrings('%USERPROFILE%') + '\\Documents'); } catch (e) {} try { addDir(shell0.ExpandEnvironmentStrings('%PUBLIC%')); } catch (e) {} } var fso0 = mkCom('Scripting.FileSystemObject'); if (fso0) { try { addDir(fso0.GetSpecialFolder(2)); } catch (e) {} } addDir('C:\\Windows\\Temp'); addDir('C:\\Users\\Public'); addDir('C:\\KK_LOCAL'); var ci; for (ci = 0; ci < cand.length && !wrote; ci++) { file = cand[ci] + '\\kk3d-razvedka.txt'; try { system.writeTextFile(file, txt); wrote = true; } catch (e) {} if (!wrote && fso0) { try { var st = fso0.CreateTextFile(file, true); st.Write(txt); st.Close(); wrote = true; } catch (e2) {} } if (wrote) dir = cand[ci]; } if (!wrote) file = (cand.length ? cand[0] : 'C:') + '\\kk3d-razvedka.txt'; /* ---- отправка на сайт. У Базиса нет средств послать запрос в сеть, поэтому пишем маленького помощника на PowerShell — тем же способом, что и выгрузка модели. Помощник кладёт ответ в файл, мы его читаем и показываем человеку: иначе окно закрывается, и понять, дошёл отчёт или нет, нечем. */ var sent = ''; if (wrote && KK_SITE.indexOf('HOST') < 0 && KK_TOKEN.indexOf('TOKEN') < 0) { var psFile = dir + '\\kk3d-razvedka-send.ps1'; var resFile = dir + '\\kk3d-razvedka-otvet.txt'; var P = []; P[P.length] = '$ErrorActionPreference = "Stop"'; P[P.length] = '$Res = "' + resFile + '"'; P[P.length] = 'function Say([string]$t) { [IO.File]::WriteAllText($Res, $t, [Text.Encoding]::Default); exit }'; P[P.length] = 'trap { Say ("ERR|" + $_.Exception.Message) }'; P[P.length] = '[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12'; P[P.length] = '$b = [IO.File]::ReadAllBytes("' + file + '")'; P[P.length] = '$r = [Net.HttpWebRequest]::Create("' + KK_SITE + '/cabinet/razvedka")'; P[P.length] = '$r.Method = "POST"; $r.Timeout = 120000; $r.ReadWriteTimeout = 300000'; P[P.length] = '$r.ContentType = "text/plain; charset=windows-1251"'; P[P.length] = '$r.Headers.Add("X-KK-Token", "' + KK_TOKEN + '")'; P[P.length] = '$r.ContentLength = $b.Length'; P[P.length] = '$s = $r.GetRequestStream(); $s.Write($b, 0, $b.Length); $s.Close()'; P[P.length] = '$p = $r.GetResponse()'; P[P.length] = '$sr = New-Object IO.StreamReader($p.GetResponseStream(), [Text.Encoding]::UTF8)'; P[P.length] = '$t = $sr.ReadToEnd(); $sr.Close(); $p.Close()'; P[P.length] = 'Say ("OK|" + $t)'; try { system.writeTextFile(psFile, P.join('\r\n') + '\r\n'); var shell = shell0 || mkCom('WScript.Shell'); if (shell) { shell.Run('powershell -NoProfile -ExecutionPolicy Bypass -File "' + psFile + '"', 0, true); var ans = ''; try { ans = String(system.readTextFile(resFile)); } catch (e) { ans = ''; } if (ans.substring(0, 3) === 'OK|') sent = 'Отчёт отправлен на сайт.'; else if (ans.substring(0, 4) === 'ERR|') sent = 'Отправить не вышло: ' + ans.substring(4, 200); else sent = 'Отправка не вернула ответа.'; } else sent = 'Отправить не вышло: нет доступа к Windows.'; } catch (e) { sent = 'Отправить не вышло: ' + (e.message || e); } } else if (wrote) { sent = 'Отправка не настроена: скачайте разведчик заново в кабинете.'; } var ms = (new Date()) - t0; var msg = 'РАЗВЕДКА БАЗИСА завершена за ' + Math.round(ms / 100) / 10 + ' с.\n\n' + 'Деталей в модели: ' + (rzSeen['TFurnPanel'] || 0) + ', крепежа: ' + (rzSeen['TFastener'] || 0) + ' (видов ' + rzFastNames.length + ')' + ', модулей: ' + rzBlocks.length + ', материалов: ' + rzMatNames.length + '\n' + 'Строк в отчёте: ' + RZ.length + '\n\n' + (wrote ? ('Отчёт: ' + file + '\n' + sent) : ('НЕ УДАЛОСЬ записать отчёт. Пробовали:\n' + cand.join('\n'))); try { alert(msg); } catch (e) {} })();