// ============================================================================ // Отправка модели на https://view.furnit.market // // Запускается прямо в Базис-Мебельщике: соберёт открытую модель, отправит // на сайт и покажет короткую ссылку для заказчика. // // Куда класть: в любую папку, которую Базис видит как папку скриптов. // Ничего рядом с этим файлом не требуется — он самодостаточен. Временные // файлы кладутся в системную папку временных файлов и удаляются после. // // Ключ доступа уже вписан внутрь при скачивании из кабинета. Если ключ // сменить на сайте, скачайте этот файл заново. // ============================================================================ var KK_SITE = 'https://view.furnit.market'; var KK_TOKEN = 'f894463bd5cc6f3ed946691aabb54396e3954fe29777c61f'; var KK_VER = '2.18'; /* Текст окна (сайт/lib/client/okno.ps1) — подставляется целиком при скачивании из кабинета. Держать его отдельным файлом, а не строками здесь, надо затем, что окно тогда можно проверять как обычный скрипт: разбором синтаксиса и прогоном `okno.ps1 -Samoprover`. Двух разных окон не бывает — оно ровно одно. */ var KK_OKNO = "# ============================================================================\r\n# Одно окно на весь запуск: телефон, ход работ, ссылка.\r\n#\r\n# Кто это запускает: скрипт Базиса (basis-export.js) первым же делом, скрыто\r\n# и НЕ ДОЖИДАЯСЬ — и сразу уходит читать модель. Дальше Базис молчит и только\r\n# переписывает файлы вестей в папке запуска, а всё, что видит человек, рисует\r\n# это окно.\r\n#\r\n# Почему окно здесь, а не в Базисе. Окна Базиса умеют только показаться и\r\n# дождаться нажатия: ShowModal останавливает скрипт, перерисовать надпись во\r\n# время работы нечем, готового ползунка у него нет вовсе. Ползунок в окне\r\n# Базиса не сделать — он замрёт и побелеет ровно так, как жалуется владелец.\r\n#\r\n# ВАЖНО ПРО КОДИРОВКУ. Файл кладётся на диск скриптом Базиса, то есть в\r\n# cp1251, и PowerShell читает его системной кодировкой — русский внутри\r\n# кавычек доезжает. Поэтому имена переменных здесь ТОЛЬКО латиницей, а\r\n# русский текст живёт исключительно в кавычках: пересохранят файл в другой\r\n# кодировке — испортятся надписи, но скрипт продолжит работать.\r\n#\r\n# ВАЖНО ПРО ЧТЕНИЕ. Файлы вестей читаются только общим доступом\r\n# (FileShare ReadWrite). Обычный ReadAllText запирает файл на чтение, и если\r\n# попасть ровно в миг, когда Базис его переписывает, отказ получит БАЗИС —\r\n# окно сломает сборщик модели.\r\n# ============================================================================\r\nparam(\r\n [string]$Dir, # папка запуска: там сырьё и файлы вестей\r\n [switch]$Samoprover # проверка расчётной части: без окна и без сети\r\n)\r\n\r\n$ErrorActionPreference = 'Stop'\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n\r\n# ------------------------------------------------------------------ мелочи\r\n\r\n\u003c#\r\n Чтение файла, который прямо сейчас может переписывать Базис.\r\n Любая беда — возвращаем пустоту и пропускаем такт: недочитанная весть\r\n не повод показывать человеку ошибку.\r\n#\u003e\r\nfunction Read-Kk([string]$Path) {\r\n if ([string]::IsNullOrEmpty($Path)) { return $null }\r\n if (-not (Test-Path -LiteralPath $Path)) { return $null }\r\n $fs = $null\r\n try {\r\n $fs = [IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')\r\n $sr = New-Object IO.StreamReader($fs, [Text.Encoding]::Default)\r\n $txt = $sr.ReadToEnd()\r\n return $txt\r\n } catch {\r\n return $null\r\n } finally {\r\n if ($null -ne $fs) { try { $fs.Close() } catch {} }\r\n }\r\n}\r\n\r\n\u003c#\r\n Разбор JSON, который пишет Базис. Он пишет чистым ASCII (русское уходит\r\n как \\uXXXX), поэтому кодировка тут ни при чём. Недописанный файл не\r\n разберётся — это и есть признак «ещё пишется», ждём следующего такта.\r\n#\u003e\r\nfunction Read-KkJson([string]$Path) {\r\n $txt = Read-Kk $Path\r\n if ([string]::IsNullOrWhiteSpace($txt)) { return $null }\r\n try { return ($txt | ConvertFrom-Json) } catch { return $null }\r\n}\r\n\r\n\u003c#\r\n Телефон.\r\n\r\n Поле стоит заготовкой под будущий вход: опознание клиента задумано по\r\n номеру, но пока кабинет выбирается ключом, и работу номер НЕ держит —\r\n пусто значит пусто, отправка идёт как шла. Придираться к виду записи\r\n тоже не надо: человек пишет и «+7 777 123-45-67», и «87771234567».\r\n\r\n Возвращаем: одни цифры — годный номер; пусто — поля не трогали;\r\n «?» — что-то ввели, но на номер это не похоже, и об этом стоит сказать,\r\n а не молча выбросить.\r\n#\u003e\r\nfunction Test-Phone([string]$Raw) {\r\n if ($null -eq $Raw) { return '' }\r\n $d = ($Raw -replace '[^0-9]', '')\r\n if ($d.Length -eq 0) { return '' }\r\n if ($d.Length -lt 10 -or $d.Length -gt 15) { return '?' }\r\n return $d\r\n}\r\n\r\n\u003c#\r\n Где лежит запомненный номер. В %APPDATA%, у каждого пользователя свой:\r\n спрашивать номер каждый раз — ровно то, на что жалуются.\r\n#\u003e\r\nfunction Get-PhonePath() {\r\n $base = [Environment]::GetFolderPath('ApplicationData')\r\n if ([string]::IsNullOrEmpty($base)) { return '' }\r\n return (Join-Path (Join-Path $base 'kk3d') 'phone.txt')\r\n}\r\n\r\nfunction Load-Phone() {\r\n $p = Get-PhonePath\r\n if ([string]::IsNullOrEmpty($p)) { return '' }\r\n $t = Read-Kk $p\r\n if ($null -eq $t) { return '' }\r\n return $t.Trim()\r\n}\r\n\r\nfunction Save-Phone([string]$Digits) {\r\n $p = Get-PhonePath\r\n if ([string]::IsNullOrEmpty($p)) { return }\r\n try {\r\n $d = Split-Path -Parent $p\r\n if (-not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }\r\n [IO.File]::WriteAllText($p, $Digits, [Text.Encoding]::Default)\r\n } catch {}\r\n}\r\n\r\n\u003c#\r\n Весть о ходе чтения модели. Базис пишет ЧИСТЫМ ASCII — «READ|812»,\r\n «TEX|4|12», «WRITE|0», — а русские слова подставляем здесь. Так на самом\r\n частом пути (весть переписывается по нескольку раз в секунду) кодировки\r\n нет вообще.\r\n#\u003e\r\nfunction Say-Hod([string]$Line) {\r\n if ([string]::IsNullOrWhiteSpace($Line)) { return 'Читаю модель' }\r\n $p = $Line.Trim().Split('|')\r\n $n = 0\r\n if ($p.Length -gt 1) { [void][int]::TryParse($p[1], [ref]$n) }\r\n $m = 0\r\n if ($p.Length -gt 2) { [void][int]::TryParse($p[2], [ref]$m) }\r\n switch ($p[0]) {\r\n 'READ' { if ($n -gt 0) { return \"Читаю модель: деталей $n\" } else { return 'Читаю модель' } }\r\n 'TEX' { return \"Ищу картинки текстур: $n из $m\" }\r\n 'WRITE' { return 'Готовлю оглавление модели' }\r\n default { return 'Читаю модель' }\r\n }\r\n}\r\n\r\n\u003c# Длительность словами: рядом с бегущей полоской обязано быть что-то, что\r\n МЕНЯЕТСЯ по другой причине. Бегущая полоска сама по себе не доказывает,\r\n что работа идёт, — она бежит и над мёртвым Базисом. #\u003e\r\nfunction Say-Time([int]$Seconds) {\r\n $m = [int][Math]::Floor($Seconds / 60)\r\n $s = $Seconds % 60\r\n return ('{0}:{1:D2}' -f $m, $s)\r\n}\r\n\r\n\u003c# Что отправляем из папки сырья. Список видов картинок обязан совпадать с\r\n тем, что принимает сайт: пропустишь webp или gif — картинка молча не\r\n доедет, а в отчёте будет написано, что она нашлась. #\u003e\r\n$KkMask = '^(tri_\\d{1,6}\\.kkr|man_\\d{1,6}\\.kkm|manifest\\.json|sborka\\.json|zamechaniya\\.json|final\\.json|tex_\\d{1,4}\\.(jpg|png|webp|gif|bmp))$'\r\n\r\nfunction Get-KkFiles([string]$From) {\r\n if (-not (Test-Path -LiteralPath $From)) { return @() }\r\n return @(Get-ChildItem -LiteralPath $From | Where-Object { -not $_.PSIsContainer -and $_.Name -match $KkMask })\r\n}\r\n\r\nfunction Get-Percent([long]$Sent, [long]$Total) {\r\n if ($Total -le 0) { return 0 }\r\n $p = [int](100 * $Sent / $Total)\r\n if ($p -lt 0) { return 0 }\r\n if ($p -gt 100) { return 100 }\r\n return $p\r\n}\r\n\r\n\u003c#\r\n Разбор ответа сайта на «готово». Держим тот же вид строки, что и прежний\r\n помощник: OK|url|деталей|треугольников|версияPS|версияНаСайте — на нём\r\n стоит запасной путь, и разъезжаться им нельзя.\r\n#\u003e\r\nfunction Parse-Otvet([string]$Line) {\r\n $out = @{ ok = $false; url = ''; nodes = ''; tris = ''; cur = ''; err = '' }\r\n if ([string]::IsNullOrWhiteSpace($Line)) { $out.err = 'ответа нет'; return $out }\r\n $p = $Line.Split('|')\r\n if ($p[0] -eq 'OK') {\r\n $out.ok = $true\r\n if ($p.Length -gt 1) { $out.url = $p[1] }\r\n if ($p.Length -gt 2) { $out.nodes = $p[2] }\r\n if ($p.Length -gt 3) { $out.tris = $p[3] }\r\n if ($p.Length -gt 5) { $out.cur = $p[5] }\r\n } else {\r\n $out.err = (($p | Select-Object -Skip 1) -join '|')\r\n if ([string]::IsNullOrWhiteSpace($out.err)) { $out.err = $Line }\r\n }\r\n return $out\r\n}\r\n\r\n# --------------------------------------------------------------- разговор с сайтом\r\n\r\nfunction Post-Kk([string]$Url, [byte[]]$Body, [int]$Timeout, [string]$Token, [string]$Phone) {\r\n $r = [Net.HttpWebRequest]::Create($Url)\r\n $r.Method = 'POST'\r\n if ($Timeout -le 0) { $Timeout = 60000 }\r\n $r.Timeout = $Timeout\r\n $r.ReadWriteTimeout = 900000\r\n $r.ContentType = 'application/octet-stream'\r\n $r.Headers.Add('X-KK-Token', $Token)\r\n # Номер уходит ЗАГОЛОВКОМ, никогда не строкой запроса: личные данные в\r\n # адресе оседают в журналах сервера и в истории браузера.\r\n if (-not [string]::IsNullOrEmpty($Phone)) { $r.Headers.Add('X-KK-Phone', $Phone) }\r\n if ($null -eq $Body) { $Body = New-Object byte[] 0 }\r\n $r.ContentLength = $Body.Length\r\n try {\r\n $s = $r.GetRequestStream(); $s.Write($Body, 0, $Body.Length); $s.Close()\r\n $p = $r.GetResponse()\r\n $sr = New-Object IO.StreamReader($p.GetResponseStream(), [Text.Encoding]::UTF8)\r\n $t = $sr.ReadToEnd(); $sr.Close(); $p.Close()\r\n $j = $null\r\n try { $j = $t | ConvertFrom-Json } catch { $j = $null }\r\n if ($null -eq $j) {\r\n $sh = ('' + $t) -replace '\\s+', ' '\r\n if ($sh.Length -gt 200) { $sh = $sh.Substring(0, 200) }\r\n throw ('сайт ответил не по делу: ' + $sh)\r\n }\r\n return $j\r\n } catch [Net.WebException] {\r\n $w = $_.Exception\r\n if ($null -ne $w.Response) {\r\n $sr = New-Object IO.StreamReader($w.Response.GetResponseStream(), [Text.Encoding]::UTF8)\r\n $t = $sr.ReadToEnd(); $sr.Close()\r\n $j = $null\r\n try { $j = $t | ConvertFrom-Json } catch {}\r\n if ($null -ne $j -and $j.error) { throw ('' + $j.error) }\r\n throw ('сервер ответил: ' + $t)\r\n }\r\n throw ('нет связи с сайтом. ' + $w.Message)\r\n }\r\n}\r\n\r\n# ------------------------------------------------------- проверка расчётной части\r\n\u003c#\r\n Ключ -Samoprover гоняет всё, что можно проверить без экрана и без сети.\r\n Обработчики кнопок вынесены в именованные функции нарочно: нажать кнопку\r\n на непоказанной форме нельзя — PerformClick молчит, и проверка радостно\r\n прошла бы при полностью мёртвой кнопке.\r\n#\u003e\r\nif ($Samoprover) {\r\n $bad = 0\r\n function Ok([string]$Name, [bool]$Cond) {\r\n if ($Cond) { Write-Host (\" OK \" + $Name) }\r\n else { Write-Host (\" СБОЙ \" + $Name) -ForegroundColor Red; $script:bad++ }\r\n }\r\n Write-Host ''\r\n Write-Host ' Проверка расчётной части окна' -ForegroundColor Cyan\r\n\r\n Ok 'телефон: обычный казахстанский' ((Test-Phone '+7 777 123-45-67') -eq '77771234567')\r\n Ok 'телефон: без плюса и пробелов' ((Test-Phone '87771234567') -eq '87771234567')\r\n Ok 'телефон: пусто — так и надо' ((Test-Phone '') -eq '')\r\n Ok 'телефон: одни пробелы — пусто' ((Test-Phone ' ') -eq '')\r\n Ok 'телефон: одни буквы — пусто' ((Test-Phone 'позвоните мне') -eq '')\r\n Ok 'телефон: девять цифр — опечатка' ((Test-Phone '123456789') -eq '?')\r\n Ok 'телефон: шестнадцать — опечатка' ((Test-Phone '1234567890123456') -eq '?')\r\n Ok 'телефон: null не роняет' ((Test-Phone $null) -eq '')\r\n\r\n Ok 'ход: чтение с числом' ((Say-Hod 'READ|812') -eq 'Читаю модель: деталей 812')\r\n Ok 'ход: чтение без числа' ((Say-Hod 'READ|0') -eq 'Читаю модель')\r\n Ok 'ход: картинки' ((Say-Hod 'TEX|4|12') -eq 'Ищу картинки текстур: 4 из 12')\r\n Ok 'ход: оглавление' ((Say-Hod 'WRITE|0') -eq 'Готовлю оглавление модели')\r\n Ok 'ход: пустая весть' ((Say-Hod '') -eq 'Читаю модель')\r\n Ok 'ход: мусор не роняет' ((Say-Hod 'ЧТО-ТО|х|у') -eq 'Читаю модель')\r\n\r\n Ok 'время 0:07' ((Say-Time 7) -eq '0:07')\r\n Ok 'время 1:05' ((Say-Time 65) -eq '1:05')\r\n Ok 'время 12:00' ((Say-Time 720) -eq '12:00')\r\n\r\n Ok 'проценты: половина' ((Get-Percent 50 100) -eq 50)\r\n Ok 'проценты: пустой объём' ((Get-Percent 10 0) -eq 0)\r\n Ok 'проценты: не больше ста' ((Get-Percent 200 100) -eq 100)\r\n\r\n $o1 = Parse-Otvet 'OK|https://3d.furnit.market/abc|753|462141|5.1|2.18'\r\n Ok 'ответ: удача разобрана' ($o1.ok -and $o1.url -eq 'https://3d.furnit.market/abc' -and $o1.nodes -eq '753' -and $o1.cur -eq '2.18')\r\n $o2 = Parse-Otvet 'ERR|Место кончилось: занято 26,2 МБ'\r\n Ok 'ответ: беда разобрана' ((-not $o2.ok) -and $o2.err -eq 'Место кончилось: занято 26,2 МБ')\r\n $o3 = Parse-Otvet ''\r\n Ok 'ответ: пустота не роняет' ((-not $o3.ok) -and $o3.err -eq 'ответа нет')\r\n\r\n $tdir = Join-Path ([IO.Path]::GetTempPath()) ('kk3d_prov_' + [Guid]::NewGuid().ToString('N'))\r\n New-Item -ItemType Directory -Path $tdir -Force | Out-Null\r\n foreach ($n in @('tri_0001.kkr', 'manifest.json', 'sborka.json', 'tex_0001.jpg', 'final.json')) {\r\n [IO.File]::WriteAllBytes((Join-Path $tdir $n), (New-Object byte[] 10))\r\n }\r\n foreach ($n in @('zadanie.json', 'hod.txt', 'gotovo.json', 'svodka.json', 'result.txt', 'send.ps1')) {\r\n [IO.File]::WriteAllBytes((Join-Path $tdir $n), (New-Object byte[] 10))\r\n }\r\n $ff = Get-KkFiles $tdir\r\n Ok 'маска: берём только файлы модели' ($ff.Count -eq 5)\r\n Ok 'маска: служебное не берём' (-not ($ff | Where-Object { $_.Name -eq 'zadanie.json' }))\r\n\r\n # чтение общим доступом: файл, открытый на запись другим, обязан читаться\r\n $busy = Join-Path $tdir 'hod.txt'\r\n $hold = [IO.File]::Open($busy, 'Open', 'Write', 'Read')\r\n $got = Read-Kk $busy\r\n $hold.Close()\r\n Ok 'чтение занятого файла не падает' ($null -ne $got)\r\n Ok 'чтение несуществующего = пусто' ($null -eq (Read-Kk (Join-Path $tdir 'нет-такого.txt')))\r\n Ok 'недописанный JSON = пусто' ($null -eq (Read-KkJson (Join-Path $tdir 'gotovo.json')))\r\n\r\n Remove-Item -LiteralPath $tdir -Recurse -Force -ErrorAction SilentlyContinue\r\n Write-Host ''\r\n if ($bad -eq 0) { Write-Host ' ВСЕ ПРОВЕРКИ ПРОШЛИ' -ForegroundColor Green; exit 0 }\r\n Write-Host (\" СБОЕВ: \" + $bad) -ForegroundColor Red\r\n exit 1\r\n}\r\n\r\n# ==================================================================== окно\r\n\r\nif ([string]::IsNullOrEmpty($Dir)) { exit 1 }\r\nif (-not $Dir.EndsWith('\\')) { $Dir = $Dir + '\\' }\r\n\r\n$Z = Read-KkJson ($Dir + 'zadanie.json')\r\nif ($null -eq $Z) { exit 1 }\r\n\r\nAdd-Type -AssemblyName System.Windows.Forms\r\nAdd-Type -AssemblyName System.Drawing\r\n[Windows.Forms.Application]::EnableVisualStyles()\r\n\r\n\u003c#\r\n Показать себя.\r\n\r\n Запуск идёт стилем окна 0 — так консоль не мелькает даже на миг. Беда в\r\n том, что этот стиль прячет и ПЕРВОЕ окно процесса: форма создаётся,\r\n свойство Visible говорит «да», а на экране пусто. Проверено замером:\r\n IsWindowVisible возвращал False при Visible=True. Поэтому показываемся\r\n руками через user32 и проверяем результат тем же IsWindowVisible —\r\n свойству формы верить нельзя, оно врёт.\r\n#\u003e\r\n$U32 = $null\r\ntry {\r\n $U32 = Add-Type -MemberDefinition @'\r\n[DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr h, int n);\r\n[DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr h);\r\n[DllImport(\"user32.dll\")] public static extern bool IsWindowVisible(IntPtr h);\r\n'@ -Name 'KkWin' -Namespace 'Kk' -PassThru\r\n} catch { $U32 = $null }\r\n\r\n$Site = '' + $Z.site\r\n$Token = '' + $Z.token\r\n$Ver = '' + $Z.ver\r\n$Title = '' + $Z.title\r\n$Api = \"$Site/cabinet/upload\"\r\n$Otchet = '' + $Z.otchet\r\n\r\n# ------------------------------------------------------------------ состояние\r\n$St = @{\r\n faza = 'sbor' # sbor | zamechaniya | otpravka | gotovo | beda\r\n phone = ''\r\n hochet = $false # человек нажал «Отправить»\r\n ready = $false # Базис дочитал модель\r\n reshenie = '' # OKNO — Базис отдал дело нам; SAM — шлёт сам\r\n zam = @() # замечания выгрузки\r\n svodka = ''\r\n nachalo = [DateTime]::Now\r\n poslednyaya = [DateTime]::Now # когда приходила последняя весть\r\n zanyato = $false # идёт отправка: не пускаем второй раз\r\n stop = $false\r\n url = ''\r\n zamKogda = [DateTime]::Now # когда замечания вышли на экран\r\n}\r\n\r\n# ------------------------------------------------------------------- разметка\r\n$F = New-Object Windows.Forms.Form\r\n$F.Text = 'Отправка модели в 3D'\r\n$F.Size = New-Object Drawing.Size(580, 470)\r\n$F.StartPosition = 'CenterScreen'\r\n$F.FormBorderStyle = 'FixedDialog'\r\n$F.MaximizeBox = $false\r\n$F.MinimizeBox = $false\r\n$F.AutoScaleMode = 'Font'\r\n$F.TopMost = $true\r\n\r\n$LabModel = New-Object Windows.Forms.Label\r\n$LabModel.Location = New-Object Drawing.Point(18, 15)\r\n$LabModel.Size = New-Object Drawing.Size(530, 22)\r\n$LabModel.Font = New-Object Drawing.Font($F.Font.FontFamily, 10.5, [Drawing.FontStyle]::Bold)\r\n$LabModel.Text = 'Модель: ' + $Title\r\n$F.Controls.Add($LabModel)\r\n\r\n$LabPhone = New-Object Windows.Forms.Label\r\n$LabPhone.Location = New-Object Drawing.Point(18, 50)\r\n$LabPhone.Size = New-Object Drawing.Size(530, 20)\r\n# Поле — заготовка под будущий вход по номеру. Сегодня оно работу не держит,\r\n# и врать об этом нельзя: подписываем честно.\r\n$LabPhone.Text = 'Ваш номер телефона — пока можно не заполнять'\r\n$F.Controls.Add($LabPhone)\r\n\r\n$BoxPhone = New-Object Windows.Forms.TextBox\r\n$BoxPhone.Location = New-Object Drawing.Point(18, 72)\r\n$BoxPhone.Size = New-Object Drawing.Size(300, 26)\r\n$BoxPhone.Font = New-Object Drawing.Font($F.Font.FontFamily, 11)\r\n$BoxPhone.Text = Load-Phone\r\n$F.Controls.Add($BoxPhone)\r\n\r\n$BtnSend = New-Object Windows.Forms.Button\r\n$BtnSend.Location = New-Object Drawing.Point(330, 71)\r\n$BtnSend.Size = New-Object Drawing.Size(215, 30)\r\n$BtnSend.Text = 'Отправить'\r\n$F.Controls.Add($BtnSend)\r\n\r\n$LabErr = New-Object Windows.Forms.Label\r\n$LabErr.Location = New-Object Drawing.Point(18, 102)\r\n$LabErr.Size = New-Object Drawing.Size(530, 20)\r\n$LabErr.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n$LabErr.Text = ''\r\n$F.Controls.Add($LabErr)\r\n\r\n# полоса замечаний: видна только тогда, когда есть о чём предупредить\r\n$LabZam = New-Object Windows.Forms.Label\r\n$LabZam.Location = New-Object Drawing.Point(18, 128)\r\n$LabZam.Size = New-Object Drawing.Size(530, 20)\r\n$LabZam.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n$LabZam.Font = New-Object Drawing.Font($F.Font.FontFamily, 9.5, [Drawing.FontStyle]::Bold)\r\n$LabZam.Visible = $false\r\n$F.Controls.Add($LabZam)\r\n\r\n$BoxZam = New-Object Windows.Forms.TextBox\r\n$BoxZam.Location = New-Object Drawing.Point(18, 150)\r\n$BoxZam.Size = New-Object Drawing.Size(530, 150)\r\n$BoxZam.Multiline = $true\r\n$BoxZam.ReadOnly = $true\r\n$BoxZam.ScrollBars = 'Vertical'\r\n$BoxZam.BackColor = [Drawing.Color]::FromArgb(252, 246, 244)\r\n$BoxZam.Visible = $false\r\n$F.Controls.Add($BoxZam)\r\n\r\n# готовая ссылка — на том же месте, где были замечания\r\n$LabUrl = New-Object Windows.Forms.Label\r\n$LabUrl.Location = New-Object Drawing.Point(18, 128)\r\n$LabUrl.Size = New-Object Drawing.Size(530, 20)\r\n$LabUrl.Text = 'Ссылка для заказчика (уже скопирована в буфер обмена):'\r\n$LabUrl.Visible = $false\r\n$F.Controls.Add($LabUrl)\r\n\r\n$BoxUrl = New-Object Windows.Forms.TextBox\r\n$BoxUrl.Location = New-Object Drawing.Point(18, 150)\r\n$BoxUrl.Size = New-Object Drawing.Size(530, 26)\r\n$BoxUrl.Font = New-Object Drawing.Font($F.Font.FontFamily, 11)\r\n$BoxUrl.ReadOnly = $true\r\n$BoxUrl.Visible = $false\r\n$F.Controls.Add($BoxUrl)\r\n\r\n$BtnOpen = New-Object Windows.Forms.Button\r\n$BtnOpen.Location = New-Object Drawing.Point(18, 186)\r\n$BtnOpen.Size = New-Object Drawing.Size(170, 30)\r\n$BtnOpen.Text = 'Открыть модель'\r\n$BtnOpen.Visible = $false\r\n$F.Controls.Add($BtnOpen)\r\n\r\n$BtnCopy = New-Object Windows.Forms.Button\r\n$BtnCopy.Location = New-Object Drawing.Point(198, 186)\r\n$BtnCopy.Size = New-Object Drawing.Size(170, 30)\r\n$BtnCopy.Text = 'Скопировать ссылку'\r\n$BtnCopy.Visible = $false\r\n$F.Controls.Add($BtnCopy)\r\n\r\n$BtnClose = New-Object Windows.Forms.Button\r\n$BtnClose.Location = New-Object Drawing.Point(378, 186)\r\n$BtnClose.Size = New-Object Drawing.Size(170, 30)\r\n$BtnClose.Text = 'Закрыть'\r\n$BtnClose.Visible = $false\r\n$F.Controls.Add($BtnClose)\r\n\r\n# ход работ — всегда внизу\r\n$LabHod = New-Object Windows.Forms.Label\r\n$LabHod.Location = New-Object Drawing.Point(18, 355)\r\n$LabHod.Size = New-Object Drawing.Size(430, 20)\r\n$LabHod.Text = 'Читаю модель'\r\n$F.Controls.Add($LabHod)\r\n\r\n$LabTime = New-Object Windows.Forms.Label\r\n$LabTime.Location = New-Object Drawing.Point(455, 355)\r\n$LabTime.Size = New-Object Drawing.Size(93, 20)\r\n$LabTime.TextAlign = 'MiddleRight'\r\n$LabTime.Text = '0:00'\r\n$F.Controls.Add($LabTime)\r\n\r\n$Pb = New-Object Windows.Forms.ProgressBar\r\n$Pb.Location = New-Object Drawing.Point(18, 378)\r\n$Pb.Size = New-Object Drawing.Size(530, 18)\r\n$Pb.Style = 'Marquee'\r\n$Pb.MarqueeAnimationSpeed = 30\r\n$F.Controls.Add($Pb)\r\n\r\n# ------------------------------------------------------------------ действия\r\n\r\nfunction Set-Hod([string]$Text) {\r\n $LabHod.Text = $Text\r\n}\r\n\r\n\u003c# Отчёт о запуске Базис уже записал и уже отправил в кабинет. Нам остаётся\r\n дописать в местный файл, чем всё кончилось: по нему разбирают беды. #\u003e\r\nfunction Add-Otchet([string]$Line) {\r\n if ([string]::IsNullOrEmpty($Otchet)) { return }\r\n try {\r\n [IO.File]::AppendAllText($Otchet, \"`r`n\" + $Line + \"`r`n\", [Text.Encoding]::Default)\r\n } catch {}\r\n}\r\n\r\nfunction Show-Gotovo([string]$Url, [string]$Cur) {\r\n $St.faza = 'gotovo'\r\n $St.url = $Url\r\n $LabZam.Visible = $false\r\n $BoxZam.Visible = $false\r\n $BoxPhone.Enabled = $false\r\n $BtnSend.Visible = $false\r\n $LabUrl.Visible = $true\r\n $BoxUrl.Text = $Url\r\n $BoxUrl.Visible = $true\r\n $BtnOpen.Visible = $true\r\n $BtnCopy.Visible = $true\r\n $BtnClose.Visible = $true\r\n $Pb.Style = 'Continuous'\r\n $Pb.Value = 100\r\n $LabErr.Text = ''\r\n Set-Hod 'Готово'\r\n try { Set-Clipboard -Value $Url } catch { try { $Url | clip.exe } catch {} }\r\n # Скачанные копии скрипта копятся в папке Базиса и на вид одинаковы:\r\n # без этой строки можно час искать ошибку, которой давно нет.\r\n if (-not [string]::IsNullOrEmpty($Cur) -and $Cur -ne $Ver) {\r\n $LabErr.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n $LabErr.Text = \"Запущена СТАРАЯ копия скрипта: у вас $Ver, на сайте $Cur. Скачайте заново в кабинете.\"\r\n }\r\n}\r\n\r\nfunction Show-Beda([string]$Text) {\r\n $St.faza = 'beda'\r\n $St.zanyato = $false\r\n $Pb.Style = 'Continuous'\r\n $Pb.Value = 0\r\n $LabErr.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n $LabErr.Text = $Text\r\n Set-Hod 'Не получилось'\r\n # Сырьё НЕ трогаем: «Повторить» пошлёт ту же папку заново, без нового\r\n # чтения модели. Раньше папка сносилась и всё собранное пропадало.\r\n $BtnSend.Text = 'Повторить'\r\n $BtnSend.Enabled = $true\r\n $BtnSend.Visible = $true\r\n # Кнопки уводим ПОД список замечаний: он может остаться на экране, и\r\n # накладывать их друг на друга нельзя.\r\n $BtnSend.Location = New-Object Drawing.Point(18, 310)\r\n $BtnSend.Size = New-Object Drawing.Size(215, 30)\r\n $BtnClose.Location = New-Object Drawing.Point(243, 310)\r\n $BtnClose.Size = New-Object Drawing.Size(170, 30)\r\n $BtnClose.Visible = $true\r\n}\r\n\r\n\u003c#\r\n Отправка. Логика та же, что у прежнего помощника: init, куски по\r\n мегабайту, done. Отличий три: настоящая полоска вместо надписи в консоли,\r\n номер телефона заголовком и DoEvents между кусками, чтобы окно не\r\n белело — ровно та беда, из-за которой всё и затевалось.\r\n#\u003e\r\nfunction Start-Otpravka() {\r\n if ($St.zanyato) { return }\r\n $St.zanyato = $true\r\n $St.stop = $false\r\n $St.faza = 'otpravka'\r\n $LabZam.Visible = $false\r\n $BoxZam.Visible = $false\r\n $LabErr.Text = ''\r\n $BoxPhone.Enabled = $false\r\n $BtnSend.Enabled = $false\r\n $BtnSend.Text = 'Прервать'\r\n $BtnSend.Enabled = $true\r\n $Pb.Style = 'Continuous'\r\n $Pb.Value = 0\r\n\r\n try {\r\n $files = Get-KkFiles $Dir\r\n if ($files.Count -eq 0) { throw 'в папке нет файлов модели' }\r\n $total = [int64](($files | Measure-Object -Property Length -Sum).Sum)\r\n\r\n Set-Hod 'Начинаю отправку'\r\n [Windows.Forms.Application]::DoEvents()\r\n $qt = [Uri]::EscapeDataString($Title)\r\n # Вес сырья — прикидка сверху: сайт знает, что готовая модель в разы\r\n # легче, и по этому числу откажет только там, где места не хватит\r\n # наверняка. Зато откажет сразу, а не через полчаса передачи.\r\n $ini = Post-Kk \"$Api`?a=init\u0026title=$qt\u0026bytes=$total\" $null 60000 $Token $St.phone\r\n if (-not $ini.ok) { throw ('' + $ini.error) }\r\n $sid = $ini.sid\r\n $cur = ''\r\n try { if ($ini.cur_ver) { $cur = [string]$ini.cur_ver } } catch {}\r\n\r\n $sent = [int64]0\r\n foreach ($f in $files) {\r\n $fs = [IO.File]::OpenRead($f.FullName)\r\n try {\r\n $buf = New-Object byte[] 1048576\r\n while (($n = $fs.Read($buf, 0, $buf.Length)) -gt 0) {\r\n if ($St.stop) { throw 'отправка прервана' }\r\n $part = New-Object byte[] $n\r\n [Array]::Copy($buf, 0, $part, 0, $n)\r\n $r = Post-Kk \"$Api`?a=chunk\u0026sid=$sid\u0026name=$($f.Name)\" $part 180000 $Token ''\r\n if (-not $r.ok) { throw ('' + $r.error) }\r\n $sent = $sent + $n\r\n $Pb.Value = Get-Percent $sent $total\r\n Set-Hod ('Отправляю: ' + $Pb.Value + '%')\r\n [Windows.Forms.Application]::DoEvents()\r\n }\r\n } finally { $fs.Close() }\r\n }\r\n\r\n # Сколько сайт будет собирать модель — честно неизвестно, поэтому\r\n # здесь полоска бежит, а не показывает выдуманный процент.\r\n $Pb.Style = 'Marquee'\r\n Set-Hod 'Сайт собирает модель'\r\n [Windows.Forms.Application]::DoEvents()\r\n $fin = Post-Kk \"$Api`?a=done\u0026sid=$sid\" $null 900000 $Token ''\r\n if (-not $fin.ok) { throw ('' + $fin.error) }\r\n\r\n $url = '' + $fin.url\r\n Add-Otchet ('Ответ отправки: OK|' + $url)\r\n try {\r\n [IO.File]::WriteAllText(($Dir + 'result.txt'),\r\n ('OK|' + $url + '|' + $fin.stat.nodes + '|' + $fin.stat.triangles + '|' +\r\n $PSVersionTable.PSVersion + '|' + $cur), [Text.Encoding]::Default)\r\n } catch {}\r\n Show-Gotovo $url $cur\r\n } catch {\r\n $msg = '' + $_.Exception.Message\r\n Add-Otchet ('Ответ отправки: ERR|' + $msg)\r\n Show-Beda $msg\r\n } finally {\r\n $St.zanyato = $false\r\n }\r\n}\r\n\r\n\u003c# Показать замечания и спросить. Это единственный щелчок, который мы\r\n добавляем нарочно: ошибка в кромке = деталь уедет незакатанной = брак.\r\n Узнать об этом надо ДО того, как модель уйдёт заказчику. #\u003e\r\nfunction Show-Zamechaniya() {\r\n $St.faza = 'zamechaniya'\r\n $n = $St.zam.Count\r\n $LabZam.Text = \"Проверьте перед отправкой: замечаний $n\"\r\n $LabZam.Visible = $true\r\n $BoxZam.Text = (($St.zam + @('', '--- что собралось ---', '') + $St.svodka.Split(\"`n\")) -join \"`r`n\")\r\n $BoxZam.Visible = $true\r\n $BtnSend.Text = 'Всё равно отправить'\r\n \u003c#\r\n Кнопку гасим на секунду. Замечания появляются сами, посреди работы, и\r\n человек в этот миг может уже нажимать «Отправить» — нажатие, начатое\r\n ДО того, как замечания вышли на экран, сошло бы за согласие с ними.\r\n Проверено: без этой заминки модель с потерянной кромкой уезжала с\r\n первого же нажатия. Секунда — чтобы взгляд успел заметить, что окно\r\n изменилось и кнопка называется иначе.\r\n #\u003e\r\n $BtnSend.Enabled = $false\r\n $St.zamKogda = [DateTime]::Now\r\n Set-Hod 'Модель прочитана, жду вашего решения'\r\n $Pb.Style = 'Continuous'\r\n $Pb.Value = 0\r\n}\r\n\r\n\u003c#\r\n Можно ли уже отправлять САМИМ: модель дочитана, Базис отдал дело нам,\r\n номер введён, и замечания — если они есть — человек увидел.\r\n\r\n Проверка «шаг ровно sbor» тут не для красоты. Без неё окно после\r\n неудачной отправки начинало долбить сайт по кругу: такт таймера снова\r\n видел «модель готова, номер есть» и снова лез отправлять. На живом\r\n прогоне так набралось полсотни попыток за минуту. Из беды, из готового\r\n и из показа замечаний отправка начинается ТОЛЬКО нажатием.\r\n#\u003e\r\nfunction Try-Start() {\r\n if ($St.zanyato) { return }\r\n if ($St.faza -ne 'sbor') { return }\r\n if (-not $St.ready) { return }\r\n if ($St.reshenie -ne 'OKNO') { return }\r\n if (-not $St.hochet) { return }\r\n Start-Otpravka\r\n}\r\n\r\nfunction On-Send() {\r\n if ($St.faza -eq 'otpravka') { $St.stop = $true; return }\r\n # Ввод и с клавиши (Enter в поле номера) идёт сюда же, поэтому погашенную\r\n # кнопку проверяем здесь, а не полагаемся на то, что её нельзя нажать.\r\n if (-not $BtnSend.Enabled) { return }\r\n if ($St.faza -eq 'beda') {\r\n $BtnSend.Text = 'Отправить'\r\n Start-Otpravka\r\n return\r\n }\r\n \u003c#\r\n Номер работу не держит. Он понадобится, когда вход пойдёт по телефону;\r\n пока кабинет выбирается ключом, и пустое поле — не повод не отправить\r\n модель. Ругаемся только на явную опечатку: ввели что-то, а на номер\r\n это не похоже.\r\n #\u003e\r\n $d = Test-Phone $BoxPhone.Text\r\n if ($d -eq '?') {\r\n $LabErr.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n $LabErr.Text = 'Похоже, номер введён не полностью. Исправьте или очистите поле.'\r\n return\r\n }\r\n $LabErr.Text = ''\r\n $St.phone = $d\r\n $St.hochet = $true\r\n if (-not [string]::IsNullOrEmpty($d)) { Save-Phone $d }\r\n $BoxPhone.Enabled = $false\r\n if ($St.faza -eq 'zamechaniya') {\r\n $St.faza = 'sbor'\r\n Start-Otpravka\r\n return\r\n }\r\n $BtnSend.Enabled = $false\r\n if (-not $St.ready) { Set-Hod 'Жду, пока дочитается модель' }\r\n Try-Start\r\n}\r\n\r\n$BtnSend.Add_Click({ On-Send })\r\n$BtnOpen.Add_Click({ try { Start-Process $St.url } catch {} })\r\n$BtnCopy.Add_Click({\r\n try { Set-Clipboard -Value $St.url; $BtnCopy.Text = 'Скопировано' } catch {}\r\n})\r\n$BtnClose.Add_Click({ $F.Close() })\r\n$BoxPhone.Add_KeyDown({ if ($_.KeyCode -eq 'Return') { On-Send } })\r\n\r\n# Пока идёт передача, закрыть окно нельзя: оборванная посередине отправка\r\n# оставляет на сайте недобранный сеанс, а человеку — недоумение.\r\n$F.Add_FormClosing({\r\n if ($St.faza -eq 'otpravka' -and -not $St.stop) {\r\n $_.Cancel = $true\r\n $LabErr.ForeColor = [Drawing.Color]::FromArgb(200, 85, 61)\r\n $LabErr.Text = 'Идёт отправка. Нажмите «Прервать», если надо остановить.'\r\n }\r\n})\r\n\r\n# --------------------------------------------------------------------- такты\r\n$Timer = New-Object Windows.Forms.Timer\r\n$Timer.Interval = 250\r\n$Timer.Add_Tick({\r\n if ($St.zanyato) { return } # во время отправки вестей от Базиса уже нет\r\n\r\n $sec = [int]([DateTime]::Now - $St.nachalo).TotalSeconds\r\n $LabTime.Text = Say-Time $sec\r\n\r\n # заминка после появления замечаний вышла — кнопку можно нажимать\r\n if ($St.faza -eq 'zamechaniya' -and -not $BtnSend.Enabled) {\r\n if (([DateTime]::Now - $St.zamKogda).TotalMilliseconds -gt 1000) { $BtnSend.Enabled = $true }\r\n }\r\n\r\n # решение Базиса: он мог не дождаться нас и уйти отправлять сам\r\n if ([string]::IsNullOrEmpty($St.reshenie)) {\r\n $r = Read-Kk ($Dir + 'reshenie.txt')\r\n if ($null -ne $r) {\r\n $St.reshenie = $r.Trim()\r\n if ($St.reshenie -eq 'SAM') { $Timer.Stop(); $F.Close(); return }\r\n }\r\n }\r\n\r\n if (-not $St.ready) {\r\n $h = Read-Kk ($Dir + 'hod.txt')\r\n if ($null -ne $h -and $St.faza -eq 'sbor' -and -not $St.hochet) {\r\n Set-Hod (Say-Hod $h)\r\n $St.poslednyaya = [DateTime]::Now\r\n } elseif ($null -ne $h) {\r\n $St.poslednyaya = [DateTime]::Now\r\n }\r\n\r\n $g = Read-KkJson ($Dir + 'gotovo.json')\r\n if ($null -ne $g) {\r\n $St.ready = $true\r\n if (-not $g.ok) {\r\n $Timer.Stop()\r\n Show-Beda ('Не получилось прочитать модель: ' + $g.why)\r\n $BtnSend.Visible = $false\r\n return\r\n }\r\n $s = Read-KkJson ($Dir + 'svodka.json')\r\n if ($null -ne $s) {\r\n if ($null -ne $s.zam) { $St.zam = @($s.zam) }\r\n if ($null -ne $s.svodka) { $St.svodka = ($s.svodka -join \"`n\") }\r\n }\r\n if ($St.zam.Count -gt 0) {\r\n Show-Zamechaniya\r\n } else {\r\n Set-Hod 'Модель прочитана'\r\n Try-Start\r\n }\r\n return\r\n }\r\n\r\n # Бегущая полоска бежит одинаково и над работающим Базисом, и над\r\n # мёртвым. Если вестей давно нет — говорим словами, а не убаюкиваем.\r\n if (([DateTime]::Now - $St.poslednyaya).TotalSeconds -gt 90) {\r\n Set-Hod 'Вестей от Базиса нет больше минуты. Он ещё читает модель или закрылся.'\r\n }\r\n return\r\n }\r\n\r\n Try-Start\r\n})\r\n\r\n# ------------------------------------------------------------------- показ\r\n$F.Add_Shown({\r\n # Два независимых способа показаться: ключ окна в команде запуска и\r\n # прямой вызов user32. Провал здесь означает пустой экран, поэтому\r\n # страхуемся дважды.\r\n if ($null -ne $U32) {\r\n try {\r\n [void]$U32::ShowWindow($F.Handle, 5)\r\n [void]$U32::SetForegroundWindow($F.Handle)\r\n } catch {}\r\n }\r\n $vidno = $true\r\n if ($null -ne $U32) {\r\n try { $vidno = $U32::IsWindowVisible($F.Handle) } catch { $vidno = $true }\r\n }\r\n # Признак «окно живо и его ВИДНО» — по нему Базис решает, отдавать ли нам\r\n # дело. Свойству формы Visible верить нельзя: оно говорит «да» и тогда,\r\n # когда на экране пусто.\r\n try {\r\n if ($vidno) {\r\n [IO.File]::WriteAllText(($Dir + 'okno-jiv.txt'), 'ok', [Text.Encoding]::Default)\r\n } else {\r\n [IO.File]::WriteAllText(($Dir + 'okno-nevidimo.txt'), 'net', [Text.Encoding]::Default)\r\n }\r\n } catch {}\r\n $BoxPhone.Focus()\r\n $Timer.Start()\r\n})\r\n\r\n# Поверх чужих окон — только на миг показа, иначе окно будет мешать работать.\r\n$Snyat = New-Object Windows.Forms.Timer\r\n$Snyat.Interval = 700\r\n$Snyat.Add_Tick({ $F.TopMost = $false; $Snyat.Stop() })\r\n$Snyat.Start()\r\n\r\n[void]$F.ShowDialog()\r\n$Timer.Stop()\r\n\r\n# ------------------------------------------------------------------- уборка\r\nif ($St.faza -ne 'gotovo') {\r\n Add-Otchet 'Ответ отправки: окно закрыли, модель не отправлена.'\r\n}\r\n\u003c#\r\n Уборка. Папку сносим только тогда, когда Базис ТОЧНО дописал в неё всё\r\n (то есть мы видели gotovo.json). Иначе он ещё пишет оглавление, а мы\r\n выдернем папку у него из-под рук — и он скажет человеку, что не смог\r\n записать модель. В этом случае оставляем отметку: папку уберёт сам Базис,\r\n когда дойдёт до передачи дела.\r\n#\u003e\r\nif ($St.ready) {\r\n try { Remove-Item -LiteralPath $Dir.TrimEnd('\\') -Recurse -Force -ErrorAction SilentlyContinue } catch {}\r\n} else {\r\n try { [IO.File]::WriteAllText(($Dir + 'otmena.txt'), 'x', [Text.Encoding]::Default) } catch {}\r\n}\r\nexit 0\r\n"; /* Папки с картинками текстур. Вписываются при скачивании из кабинета: Базис держит в детали только имя картинки, а полного пути скрипту не отдаёт. */ var KK_TEXDIRS = ''; // ---------------------------------------------------------------- мелочи function kkNum(v) { // округление до 0.001 мм: дальше точности всё равно нет, а объём заметно меньше var n = Math.round(v * 1000) / 1000; return (n === 0) ? '0' : String(n); } function kkStr(v) { if (v === null || v === undefined) return ''; return String(v).replace(/[\r\n\t]+/g, ' '); } function kkJsonStr(s) { s = kkStr(s); var out = '', i, c; for (i = 0; i < s.length; i++) { c = s.charAt(i); if (c === '"' || c === '\\') out += '\\' + c; else if (c < ' ') out += ' '; else out += c; } return '"' + out + '"'; } function kkJson(v) { var t = typeof v, i, out, first, k; if (v === null || v === undefined) return 'null'; if (t === 'number') return isFinite(v) ? String(Math.round(v * 1000) / 1000) : '0'; if (t === 'boolean') return v ? 'true' : 'false'; if (t === 'string') return kkJsonStr(v); if (v instanceof Array) { out = '['; for (i = 0; i < v.length; i++) { if (i) out += ','; out += kkJson(v[i]); } return out + ']'; } out = '{'; first = true; for (k in v) { if (!v.hasOwnProperty(k)) continue; if (v[k] === undefined) continue; if (!first) out += ','; first = false; out += kkJsonStr(k) + ':' + kkJson(v[k]); } return out + '}'; } // ------------------------------------------------------- мировые координаты /* * У Базиса нет прямого доступа к матрице объекта, зато есть ToGlobal — * перевод точки в мировые координаты. Прощупываем им начало координат и три * единичных направления: этого достаточно, чтобы восстановить всё * преобразование и дальше считать самим, без вызова ToGlobal на каждую * вершину. На сотнях тысяч вершин разница по времени — десятки раз. */ function kkFrame(obj) { var o = obj.ToGlobal(NewVector(0, 0, 0)); var x = obj.ToGlobal(NewVector(1, 0, 0)); var y = obj.ToGlobal(NewVector(0, 1, 0)); var z = obj.ToGlobal(NewVector(0, 0, 1)); return { ox: o.x, oy: o.y, oz: o.z, xx: x.x - o.x, xy: x.y - o.y, xz: x.z - o.z, yx: y.x - o.x, yy: y.y - o.y, yz: y.z - o.z, zx: z.x - o.x, zy: z.y - o.y, zz: z.z - o.z }; } // =============================================================== накопитель /* * Геометрию копим кусками и сбрасываем в файлы. Собирать всё в одну строку * нельзя: на большой модели это сотни мегабайт в памяти Базиса, и он падает. */ var kkDir = ''; // папка для временных файлов var kkBuf = []; // строки треугольников, ждущие записи var kkBufLen = 0; var kkTriFile = 0; // сколько файлов геометрии уже записано var kkTriTotal = 0; // сколько треугольников всего var kkFail = ''; function kkPad4(n) { var s = String(n); while (s.length < 4) s = '0' + s; return s; } function kkFlush() { if (kkFail || !kkBuf.length) return; try { kkTriFile++; system.writeTextFile(kkDir + 'tri_' + kkPad4(kkTriFile) + '.kkr', kkBuf.join('')); kkBuf = []; kkBufLen = 0; try { CollectGarbage(); } catch (e) {} } catch (e) { kkFail = 'не удалось записать геометрию: ' + (e.message || e); } } function kkTri(ax, ay, az, bx, by, bz, cx, cy, cz) { if (kkFail) return; var s = kkNum(ax) + ' ' + kkNum(ay) + ' ' + kkNum(az) + ' ' + kkNum(bx) + ' ' + kkNum(by) + ' ' + kkNum(bz) + ' ' + kkNum(cx) + ' ' + kkNum(cy) + ' ' + kkNum(cz) + '\n'; kkBuf[kkBuf.length] = s; kkBufLen += s.length; kkTriTotal++; /* Габарит текущей детали копим по тем самым вершинам, что уезжают в геометрию: тогда центры деталей в данных сборки совпадут с центрами тел в готовой модели один в один, без поправок на округления Базиса. */ kkSbBoxTri(ax, ay, az, bx, by, bz, cx, cy, cz); if (kkBufLen >= 8000000) kkFlush(); // около 8 МБ на файл } /** Сколько треугольников у объекта прямо сейчас (0, если не отдались). */ function kkTriCount(obj) { var c = 0, k, lc = 0; try { lc = obj.TriListsCount; } catch (e) { return 0; } try { for (k = 0; k < lc; k++) c += obj.TriLists[k].Count; } catch (e) {} return c; } /* * Какой стороне детали принадлежит треугольник — в ОСЯХ САМОЙ ДЕТАЛИ, где * Z смотрит из лицевой пласти наружу: 1 — лицевая пласть, -1 — обратная, * 0 — торец или не понять. Нужно, чтобы отделить пласть под облицовку. * * Нормаль берём у самого Базиса, а нет её — считаем по трём вершинам. * Порог 0.98 — это отклонение до ~11°: фрезерованный фасад под плёнкой * тоже весь «лицевой», кроме крутых стенок фрезеровки, а они — торцы. */ function kkTriSide(t, v1, v2, v3) { var nz = NaN; try { var nrm = t.Normal; if (nrm && nrm.z !== undefined) nz = Number(nrm.z); } catch (e) {} if (!isFinite(nz)) { try { var ux = v2.x - v1.x, uy = v2.y - v1.y, uz = v2.z - v1.z; var wx = v3.x - v1.x, wy = v3.y - v1.y, wz = v3.z - v1.z; var cx = uy * wz - uz * wy, cy = uz * wx - ux * wz, cz = ux * wy - uy * wx; var len = Math.sqrt(cx * cx + cy * cy + cz * cz); if (len > 1e-9) nz = cz / len; } catch (e2) {} } if (!isFinite(nz)) return 0; if (nz > 0.98) return 1; if (nz < -0.98) return -1; return 0; } /** * Записывает треугольники объекта в мировых координатах. * * Без sel — всё тело подряд. С sel ({top, bottom}) лицевая и/или обратная * пласть откладываются и дописываются ПОСЛЕ тела, каждая своим куском: * тело, потом лицевая, потом обратная. Так у каждого куска свой сплошной * отрезок геометрии, и его можно отдать под свой материал — тело под * материал детали, пласть под облицовку. * * Возвращает {n, top, bottom} — сколько треугольников в каждом куске. */ function kkWriteBody(obj, sel) { var out = { n: 0, top: 0, bottom: 0 }; var listCount = 0, m, i, j, list, n, t, v1, v2, v3, side, tri; var topBuf = [], botBuf = []; /* Фигурные детали и детали с выборками поначалу отдают пустой список: Базис достраивает такие поверхности по контуру только по просьбе. Без этого вызова на месте детали в модели остаётся дыра. */ if (kkTriCount(obj) === 0) { try { obj.AddTrianglesFromHoles(); } catch (e) {} } try { listCount = obj.TriListsCount; } catch (e) { return out; } if (!listCount) return out; try { m = kkFrame(obj); } catch (e) { return out; } for (i = 0; i < listCount; i++) { try { list = obj.TriLists[i]; } catch (e) { continue; } try { n = list.Count; } catch (e) { continue; } for (j = 0; j < n; j++) { /* Ссылку на список переполучаем на ходу. На длинных деталях она «протухает», и остаток поверхности пропал бы молча: ошибка внутри цикла гасится, деталь уехала бы наполовину. */ if (j > 0 && (j % 8000) === 0) { try { list = obj.TriLists[i]; } catch (e) {} try { CollectGarbage(); } catch (e) {} } try { t = list.Triangles[j]; v1 = t.Vertex1; v2 = t.Vertex2; v3 = t.Vertex3; tri = [ m.ox + v1.x * m.xx + v1.y * m.yx + v1.z * m.zx, m.oy + v1.x * m.xy + v1.y * m.yy + v1.z * m.zy, m.oz + v1.x * m.xz + v1.y * m.yz + v1.z * m.zz, m.ox + v2.x * m.xx + v2.y * m.yx + v2.z * m.zx, m.oy + v2.x * m.xy + v2.y * m.yy + v2.z * m.zy, m.oz + v2.x * m.xz + v2.y * m.yz + v2.z * m.zz, m.ox + v3.x * m.xx + v3.y * m.yx + v3.z * m.zx, m.oy + v3.x * m.xy + v3.y * m.yy + v3.z * m.zy, m.oz + v3.x * m.xz + v3.y * m.yz + v3.z * m.zz ]; side = 0; if (sel) { side = kkTriSide(t, v1, v2, v3); if (side === 1 && !sel.top) side = 0; if (side === -1 && !sel.bottom) side = 0; } if (side === 1) topBuf[topBuf.length] = tri; else if (side === -1) botBuf[botBuf.length] = tri; else { kkTri(tri[0], tri[1], tri[2], tri[3], tri[4], tri[5], tri[6], tri[7], tri[8]); out.n++; } } catch (e) {} } } /* Отложенные пласти — следом за телом, каждая своим сплошным куском. */ for (j = 0; j < topBuf.length; j++) { tri = topBuf[j]; kkTri(tri[0], tri[1], tri[2], tri[3], tri[4], tri[5], tri[6], tri[7], tri[8]); } out.top = topBuf.length; for (j = 0; j < botBuf.length; j++) { tri = botBuf[j]; kkTri(tri[0], tri[1], tri[2], tri[3], tri[4], tri[5], tri[6], tri[7], tri[8]); } out.bottom = botBuf.length; return out; } // ================================================================ текстуры /* * Картинки материалов. В модели их нет: Базис держит их отдельными файлами * на диске, а в детали лежит только путь. Здесь запоминаем, что копировать, * а сами файлы копируем одним заходом после сбора — так один и тот же дуб не * поедет двадцать раз, по разу на каждую деталь. * * Имя на сервере присваиваем своё, номером. В именах материалов Базиса живут * кириллица, кавычки, скобки и пробелы, а имя файла на сервере — это ровно * то место, куда чужой текст пускать нельзя. */ var kkTexByPath = {}; // путь в нижнем регистре → имя, под которым поедет var kkTexList = []; // [{src, safe, recs}] var kkTexNotes = []; // что не поехало и почему — попадёт в отчёт var kkTexSent = 0; var kkTexBytes = 0; var KK_TEX_ONE = 8 * 1024 * 1024; // потолок на одну картинку var KK_TEX_ALL = 60 * 1024 * 1024; // потолок на все вместе /* * Поиск картинки по имени. Базис отдаёт имя, а файл лежит в его папках * текстур — где именно, скрипт не знает. Поэтому один раз обходим указанные * папки и запоминаем, что где лежит. Обход делаем лениво: если все картинки * нашлись по прямому пути, он не понадобится вовсе. */ var kkTexIndex = null; // имя в нижнем регистре → полный путь var kkTexIndexed = 0; // сколько файлов просмотрено var kkTexWant = {}; // что ищем: 1 — ещё ищем, 2 — нашли var kkTexWantLeft = 0; // сколько осталось найти function kkTexBase(path) { var p = String(path); var slash = Math.max(p.lastIndexOf('\\'), p.lastIndexOf('/')); return (slash >= 0) ? p.substring(slash + 1) : p; } function kkTexNoExt(name) { var dot = String(name).lastIndexOf('.'); return (dot > 0) ? String(name).substring(0, dot) : String(name); } function kkTexScan(fso, folder, depth) { /* Глубину не ограничиваем: у мебельщика папки вложены как сложилось — поставщик, коллекция, год. Ограничение стоит только на общее число файлов, чтобы случайно указанный диск целиком не подвесил Базис. */ if (depth > 24 || kkTexIndexed > 200000 || kkTexWantLeft === 0) return; var it, f, nm, low, bare; try { for (it = new Enumerator(folder.Files); !it.atEnd(); it.moveNext()) { f = it.item(); nm = String(f.Name); if (!kkTexExt(nm)) continue; kkTexIndexed++; low = nm.toLowerCase(); bare = '~' + kkTexNoExt(low); /* Запоминаем только то, что ищем. Складывать в память все шесть тысяч имён незачем: нужных обычно полтора десятка. */ if (kkTexWant[low] === 1) { kkTexWant[low] = 2; kkTexIndex[low] = String(f.Path); kkTexWantLeft--; } if (kkTexWant[bare] === 1) { kkTexWant[bare] = 2; kkTexIndex[bare] = String(f.Path); kkTexWantLeft--; } if (kkTexWantLeft === 0) return; // всё нашли — дальше не ходим if (kkTexIndexed > 200000) return; } } catch (e) {} try { for (it = new Enumerator(folder.SubFolders); !it.atEnd(); it.moveNext()) { kkTexScan(fso, it.item(), depth + 1); if (kkTexWantLeft === 0) return; } } catch (e) {} } /* * Где искать картинки, если Базис отдал одно имя. * * Порядок такой, чтобы человеку не пришлось ничего указывать: * 1) настройки самого Базиса — «Пути к файлам», строка «Текстуры». * Где бы папку ни поменяли, скрипт узнает об этом сам; * 2) папка рядом с открытой моделью. Модели и текстуры у мебельщика * обычно лежат в одном хозяйстве: поднимаемся от файла модели вверх * и ищем папку, в имени которой есть «текстур»; * 3) то, что указано в кабинете — на случай, если первые два не помогли. */ var kkTexAuto = []; // что нашли сами — попадёт в отчёт var kkTexDirs = null; // папки, в которых ищем /** Похоже ли имя папки на хранилище текстур. */ function kkLooksTexDir(name) { return /текстур|texture/i.test(String(name)); } /** Путь к открытой модели, если Базис его отдаёт. */ function kkModelFile() { var f = ''; try { if (typeof currentFileData !== 'undefined' && currentFileData) f = kkStr(currentFileData.filename || ''); } catch (e) {} return f; } /** Поиск папки текстур рядом с моделью: вверх по дереву и вширь на уровень. */ function kkTexDirsNearModel(fso) { var out = [], file = kkModelFile(); if (!file) return out; var dir = ''; try { dir = fso.GetParentFolderName(file); } catch (e) { return out; } /* Подниматься приходится высоко: у владельца модель лежит на шести уровнях ниже общей папки (заказы, год, месяц, адрес, комната). На каждом уровне смотрим только сами подпапки, поэтому обход дешёвый. */ for (var up = 0; up < 10 && dir; up++) { try { if (!fso.FolderExists(dir)) break; var it = new Enumerator(fso.GetFolder(dir).SubFolders); for (; !it.atEnd(); it.moveNext()) { var sub = it.item(); if (kkLooksTexDir(sub.Name)) out[out.length] = String(sub.Path); } } catch (e) {} if (out.length) break; // нашли на этом уровне — выше не лезем var next = ''; try { next = fso.GetParentFolderName(dir); } catch (e) {} if (!next || next === dir) break; dir = next; } return out; } /* * Настройки Базиса «Пути к файлам» лежат в реестре Windows. Ключа мы заранее * не знаем — у разных версий он свой, — поэтому обходим ветку программ и * берём значения, похожие на папку текстур. Обход ограничен: две сотни ключей * и только там, где в имени есть «базис» или «bazis». */ function kkTexDirsFromRegistry() { var out = []; try { var reg = GetObject('winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\default:StdRegProv'); var HKCU = 0x80000001; var enumKeys = function (root, path) { try { var inp = reg.Methods_('EnumKey').InParameters.SpawnInstance_(); inp.hDefKey = root; inp.sSubKeyName = path; var res = reg.ExecMethod_('EnumKey', inp); return res.sNames ? res.sNames.toArray() : []; } catch (e) { return []; } }; var enumVals = function (root, path) { try { var inp = reg.Methods_('EnumValues').InParameters.SpawnInstance_(); inp.hDefKey = root; inp.sSubKeyName = path; var res = reg.ExecMethod_('EnumValues', inp); return res.sNames ? res.sNames.toArray() : []; } catch (e) { return []; } }; var getStr = function (root, path, name) { try { var inp = reg.Methods_('GetStringValue').InParameters.SpawnInstance_(); inp.hDefKey = root; inp.sSubKeyName = path; inp.sValueName = name; var res = reg.ExecMethod_('GetStringValue', inp); return kkStr(res.sValue || ''); } catch (e) { return ''; } }; var seen = 0; var walk = function (path, depth) { if (depth > 3 || seen > 200) return; seen++; var names = enumVals(HKCU, path), i, v; for (i = 0; i < names.length; i++) { v = getStr(HKCU, path, names[i]); if (v && kkLooksTexDir(v)) out[out.length] = v.replace(/[\\\/]+$/, ''); } var keys = enumKeys(HKCU, path); for (i = 0; i < keys.length && seen <= 200; i++) walk(path + '\\' + keys[i], depth + 1); }; var top = enumKeys(HKCU, 'Software'); for (var i = 0; i < top.length; i++) { if (/базис|bazis|basis/i.test(top[i])) walk('Software\\' + top[i], 1); } } catch (e) {} return out; } /* * Папка текстур, выведенная из уже найденной картинки. У владельца Базис не * отдал имя файла модели, и искать папку было не от чего — зато путь к одной * из картинок пришёл полным. Поднимаемся от него вверх до папки, похожей на * хранилище текстур: остальные картинки лежат там же. */ function kkTexDirsFromFound() { var out = [], i, dir, cut, nm; for (i = 0; i < kkTexList.length; i++) { if (!kkTexList[i].done) continue; dir = String(kkTexList[i].src); for (var up = 0; up < 12; up++) { cut = Math.max(dir.lastIndexOf('\\'), dir.lastIndexOf('/')); if (cut <= 2) break; dir = dir.substring(0, cut); nm = dir.substring(Math.max(dir.lastIndexOf('\\'), dir.lastIndexOf('/')) + 1); if (kkLooksTexDir(nm)) { out[out.length] = dir; break; } } } return out; } /** Список папок с текстурами. Дешёвый: сами файлы не обходит. */ function kkTexDirsInit(fso) { if (kkTexDirs !== null) return; var dirs = [], seenDir = {}; var addDirs = function (list, src) { for (var j = 0; j < list.length; j++) { var one = kkStr(list[j]); if (!one) continue; var low = one.toLowerCase(); if (seenDir[low]) continue; seenDir[low] = 1; dirs[dirs.length] = one; kkTexAuto[kkTexAuto.length] = src + ': ' + one; } }; addDirs(kkTexDirsFromFound(), 'от уже найденной картинки'); addDirs(kkTexDirsFromRegistry(), 'из настроек Базиса'); addDirs(kkTexDirsNearModel(fso), 'рядом с моделью'); if (KK_TEXDIRS && KK_TEXDIRS.indexOf('TEXDIRS') < 0) addDirs(KK_TEXDIRS.split(';'), 'из кабинета'); kkTexDirs = dirs; } /* * Указатель «имя файла → путь». Нужен только тогда, когда картинку не удалось * найти складыванием папки с путём из материала. Обход шести тысяч файлов * занимает пару секунд, и делать его зря незачем: у мебельщика почти всегда * срабатывает склейка, потому что Базис хранит путь вместе с подпапкой. */ /** * Ищет в папках текстур сразу ВСЕ недостающие картинки и останавливается, * как только нашлась последняя. Обходить библиотеку целиком, чтобы потом * взять из неё полтора десятка имён, — пустая трата времени: обход шести * тысяч файлов занимает секунду, а нужное обычно лежит в первой же папке. */ function kkTexBuildIndex(fso) { if (kkTexIndex !== null) return; kkTexIndex = {}; // собираем список того, что ещё не нашлось kkTexWant = {}; kkTexWantLeft = 0; for (var q = 0; q < kkTexList.length; q++) { var t = kkTexList[q]; if (t.done) continue; var nm = kkTexBase(t.srcRaw).toLowerCase(); if (kkTexWant[nm] === undefined) { kkTexWant[nm] = 1; kkTexWantLeft++; } var bare = '~' + kkTexNoExt(nm); if (kkTexWant[bare] === undefined) { kkTexWant[bare] = 1; kkTexWantLeft++; } } if (kkTexWantLeft === 0) return; kkTexDirsInit(fso); for (var i = 0; i < kkTexDirs.length; i++) { var d = kkStr(kkTexDirs[i]); if (!d) continue; try { if (fso.FolderExists(d)) kkTexScan(fso, fso.GetFolder(d), 0); else kkTexNotes[kkTexNotes.length] = 'папка текстур не найдена: ' + d; } catch (e) { kkTexNotes[kkTexNotes.length] = 'папку текстур не прочитать: ' + d; } } } /* * Ищет картинку. Сначала складываем папку текстур с тем, что отдал материал: * Базис хранит путь ОТНОСИТЕЛЬНО своей папки текстур — в редакторе материалов * он так и показан, «ЛДСП\\Цемент светлый.jpg». Это самый точный способ: он * не спутает две картинки с одинаковым именем из разных папок. * Не сложилось — ищем по одному имени файла. */ function kkTexFindRel(fso, rel) { kkTexDirsInit(fso); var r = String(rel).replace(/^[\\\/]+/, ''); if (!r) return ''; for (var i = 0; i < kkTexDirs.length; i++) { var full = kkTexDirs[i].replace(/[\\\/]+$/, '') + '\\' + r; try { if (fso.FileExists(full)) return full; } catch (e) {} } return ''; } /** Ищет картинку по имени в указанных папках. Возвращает путь или пустую строку. */ function kkTexFind(fso, name) { kkTexBuildIndex(fso); var low = String(name).toLowerCase(); if (kkTexIndex[low]) return kkTexIndex[low]; /* Базис может назвать картинку с одним расширением, а на диске лежать другое: имя декора то же, а файл пересохранён. Пробуем без расширения. */ var byName = kkTexIndex['~' + kkTexNoExt(low)]; return byName || ''; } function kkTexExt(path) { var dot = String(path).lastIndexOf('.'); if (dot < 0) return ''; var e = String(path).substring(dot + 1).toLowerCase(); if (e === 'jpeg' || e === 'jfif') return 'jpg'; if (e === 'jpg' || e === 'png' || e === 'webp' || e === 'gif') return e; /* bmp браузеры показывают, хотя весит он вчетверо больше сжатого. Держим его в списке нарочно: библиотечные декоры Базиса нередко лежат именно в bmp, и без него такой фасад остался бы заливкой цвета. */ if (e === 'bmp') return e; /* Остаются tif, ico, emf, wmf и даже видео. В браузере на трёхмерной модели они не показываются, а перекодировать их здесь нечем. Такой материал останется заливкой цвета, и это будет видно в отчёте — молчать об этом нельзя. */ return ''; } function kkTexRegister(fullPath) { var key = String(fullPath).toLowerCase(); if (kkTexByPath[key] !== undefined) return kkTexByPath[key]; var ext = kkTexExt(fullPath); if (!ext) { kkTexNotes[kkTexNotes.length] = 'не картинка: ' + fullPath; kkTexByPath[key] = ''; return ''; } var safe = 'tex_' + kkPad4(kkTexList.length + 1) + '.' + ext; kkTexList[kkTexList.length] = { src: String(fullPath), srcRaw: String(fullPath), safe: safe, recs: [] }; kkTexByPath[key] = safe; return safe; } /** Запоминаем, какие записи материалов ссылаются на эту картинку. */ function kkTexLink(safe, rec) { for (var i = 0; i < kkTexList.length; i++) { if (kkTexList[i].safe === safe) { kkTexList[i].recs[kkTexList[i].recs.length] = rec; return; } } } /** * Копирует картинки в папку выгрузки. Что не скопировалось — вычёркиваем из * материалов: лучше деталь без картинки, чем ссылка в никуда. */ function kkTexCopy(fso) { var i, j, t, f, size; /* Сначала находим ВСЕ картинки и только потом копируем. Порядок важен для скорости: Базис отдаёт путь относительно своей папки текстур, поэтому почти всё находится складыванием — за миллисекунды и без единого обхода папок. Обход остаётся только для того, что не нашлось, и ищет сразу все недостающие имена разом, останавливаясь на последнем найденном. */ for (i = 0; i < kkTexList.length; i++) { t = kkTexList[i]; try { if (fso.FileExists(t.src)) { t.done = true; t.foundBy = 'путь из материала'; continue; } var rel = kkTexFindRel(fso, t.srcRaw); if (rel) { t.src = rel; t.done = true; t.foundBy = 'папка текстур + путь из материала'; } } catch (e0) {} } for (i = 0; i < kkTexList.length; i++) { t = kkTexList[i]; if (t.done) continue; try { var byName = kkTexFind(fso, kkTexBase(t.srcRaw)); if (byName) { t.src = byName; t.done = true; t.foundBy = 'поиском по имени'; } } catch (e1) {} } for (i = 0; i < kkTexList.length; i++) { t = kkTexList[i]; var why = ''; try { if (!fso.FileExists(t.src)) why = 'файла нет на диске'; } catch (e) { why = 'не читается: ' + (e.message || e); } /* Размер читаем ОТДЕЛЬНО и не даём ему помешать копированию. Базис отдаёт его так, что JScript не всегда может сравнить с числом («Cannot convert object to primitive value»), — и из-за проверки потолка терялась уже найденная картинка. Не смогли узнать размер — копируем как есть: потолок нужен от случайных гигабайтов, а не затем, чтобы выбрасывать нормальные файлы. */ size = -1; if (!why) { try { size = Number(fso.GetFile(t.src).Size); } catch (e1) { size = -1; } if (!(size >= 0)) size = -1; // NaN тоже считаем «неизвестно» if (size === 0) why = 'файл пустой'; else if (size > KK_TEX_ONE) why = 'слишком большой (' + Math.round(size / 1048576) + ' МБ)'; else if (size > 0 && kkTexBytes + size > KK_TEX_ALL) why = 'не поместился в общий объём'; } if (!why) { try { fso.CopyFile(t.src, kkDir + t.safe, true); if (size > 0) kkTexBytes += size; kkTexSent++; t.copied = true; continue; } catch (e2) { why = 'не скопировался: ' + (e2.message || e2); } } kkTexNotes[kkTexNotes.length] = t.safe + ' — ' + why + ' (' + t.src + ')'; for (j = 0; j < t.recs.length; j++) { try { delete t.recs[j].texSafe; } catch (e3) { t.recs[j].texSafe = ''; } } } } // ================================================================ материалы var kkMatCache = {}; /* * Где Базис держит картинку материала. * * Обычно — в Material.Path, и тогда всё просто. Но не у всех декоров: у * фасадов из библиотеки материал приходит вовсе без имени и без Path, а * картинка у него есть — это видно по тому, как их показывают другие. Значит * лежит она под другим именем свойства, и какое оно в этой версии Базиса — * заранее не знает никто. * * Поэтому перебираем все правдоподобные имена по очереди. Каждое обращение в * своей ловушке: имени, которого в этой версии нет, довольно, чтобы оборвать * весь скрипт, — на этом уже обжигались. * * Что нашли и что при этом видели, возвращаем вместе: путь идёт в дело, а * список увиденного — в отчёт. Если путь не нашёлся, по отчёту будет видно, * за что зацепиться в следующий раз. */ var KK_TEX_PROPS = [ 'Path', 'TexturePath', 'TextureFileName', 'TextureFile', 'TextureName', 'FileName', 'BitmapFile', 'ImageFile', 'TexFile', 'TexName', 'Image', 'Bitmap', 'Texture' ]; var kkMatBlind = []; // материалы с картинкой, но без пути — в отчёт function kkTexPath(mt) { var out = { path: '', seen: [] }; if (!mt) return out; var i, prop, v, s; for (i = 0; i < KK_TEX_PROPS.length; i++) { prop = KK_TEX_PROPS[i]; v = null; try { v = mt[prop]; } catch (e) { continue; } if (v === null || v === undefined || v === '') continue; /* Свойство может оказаться и методом — тогда зовём. Ровно так себя ведут PathAbsolute и HasAnimation. */ try { if (typeof v === 'function') v = mt[prop](); } catch (e) { continue; } if (v === null || v === undefined || v === '') continue; if (typeof v === 'object') { /* Пришёл не путь, а сам рисунок. Сохранить его отсюда нечем, но записать, что он есть, обязательно. */ s = ''; try { s = kkStr(v.toString()); } catch (e) { s = '(объект)'; } try { if (v.FileName) s += ' FileName=' + kkStr(v.FileName); } catch (e) {} out.seen[out.seen.length] = prop + '=' + s; continue; } s = kkStr(v); if (!s) continue; out.seen[out.seen.length] = prop + '=' + s; if (!out.path && (s.indexOf('\\') >= 0 || s.indexOf('/') >= 0 || s.indexOf('.') > 0)) { out.path = s; } } /* Полный путь: PathAbsolute бывает и методом, и свойством. Звать только методом нельзя — у свойства вызов падает, ошибка гасится, и мы молча остаёмся с одним именем файла вместо пути. */ if (out.path) { try { var abs = mt.PathAbsolute; if (typeof abs === 'function') abs = mt.PathAbsolute(); abs = kkStr(abs); if (abs) out.path = abs; } catch (e) {} } return out; } /* * Запись материала по объекту материала Базиса (mt) и детали (obj). У детали * берём только то, что относится к ней самой, — поворот текстуры и * «Направление». Одна функция на оба случая: материал самой детали и * материал облицовки её пласти, который приходит не от детали, а из списка * материалов модели. */ function kkMaterialFrom(mt, obj, nameHint) { var name = '', color = -1, tex = '', hasTex = false; var sx = 300, sy = 300, ang = 0, transp = 0, mirror = 0; var hasAngle = false; // имя материала: в Базисе это MaterialName, а если пусто — путь к текстуре try { name = kkStr((mt && (mt.MaterialName || mt.Path)) || ''); } catch (e) {} if (!name && nameHint) name = kkStr(nameHint); /* Цвет обязателен к приведению в число. Если Базис отдаст его не числом, в оглавление уехала бы строка, а сборщик на такое отвечает ровным серым — и вся кухня станет гипсовой, молча, без единой ошибки. Отдельно системные цвета Delphi (clDefault и родня): признак у них в старшем байте, а три младших нулевые — деталь стала бы чёрной. Такие считаем «цвет не задан». */ try { var c = mt ? mt.DiffuseColor : -1; color = (typeof c === 'number' && isFinite(c)) ? (c | 0) : -1; if (color !== -1 && (color & 0xFF000000) !== 0) color = -1; } catch (e) { color = -1; } /* Текстура. Нам нужен ПОЛНЫЙ путь к картинке на диске: по нему файл поедет на сайт. Обычно он лежит в Path, но не всегда — см. kkTexPath. */ var texFull = ''; try { var found = kkTexPath(mt); if (found.path) { hasTex = true; texFull = found.path; tex = texFull; var slash = Math.max(tex.lastIndexOf('\\'), tex.lastIndexOf('/')); if (slash >= 0) tex = tex.substring(slash + 1); } else if (found.seen.length) { /* Путь не нашёлся, но материал что-то про картинку знает. Пишем это в отчёт: только так и можно понять, где Базис её держит. */ kkMatBlind[kkMatBlind.length] = (name || 'без имени') + ' — ' + found.seen.join(', '); } } catch (e) { hasTex = false; } // размер плитки текстуры: сначала TexSX/TexSY, затем TexW/TexH try { if (mt && mt.TexSX > 0) sx = mt.TexSX; } catch (e) {} try { if (mt && mt.TexSY > 0) sy = mt.TexSY; } catch (e) {} if (sx === 300) { try { if (mt && mt.TexW > 0) sx = mt.TexW; } catch (e) {} } if (sy === 300) { try { if (mt && mt.TexH > 0) sy = mt.TexH; } catch (e) {} } /* Угол текстуры складывается из двух частей: поворот на самой детали и собственный угол материала. Плюс поправка, когда деталь развёрнута поперёк — иначе волокна лягут не вдоль длинной стороны. */ try { if (obj && typeof obj.TextureAngle === 'number') { ang = obj.TextureAngle; hasAngle = true; } } catch (e) {} try { if (hasAngle && (obj.TextureOrientation || 0) === 1) ang -= Math.PI / 2; } catch (e) {} try { if (mt && typeof mt.Angle === 'number') ang += mt.Angle; } catch (e) {} /* Ориентация и смещение самой картинки в материале — то, что видно в редакторе материалов Базиса: «Ориентация 0/90/180/270», «Смещение X/Y». Это про картинку, а не про деталь: у детали своё «Направление». */ var orient = 0, offX = 0, offY = 0; try { if (mt && typeof mt.Orientation === 'number') orient = mt.Orientation; } catch (e) {} try { if (mt && typeof mt.TexAngle === 'number' && !orient) orient = mt.TexAngle; } catch (e) {} try { if (mt && typeof mt.OffsetX === 'number') offX = mt.OffsetX; } catch (e) {} try { if (mt && typeof mt.OffsetY === 'number') offY = mt.OffsetY; } catch (e) {} try { if (mt && typeof mt.TexOffsetX === 'number' && !offX) offX = mt.TexOffsetX; } catch (e) {} try { if (mt && typeof mt.TexOffsetY === 'number' && !offY) offY = mt.TexOffsetY; } catch (e) {} try { if (mt && typeof mt.TransparentValue === 'number') transp = mt.TransparentValue; } catch (e) {} try { if (mt && typeof mt.MirrorValue === 'number') mirror = mt.MirrorValue; } catch (e) {} if (name === '') name = 'Без имени'; var key = name + '|' + color + '|' + tex; if (kkMatCache[key]) return kkMatCache[key]; var rec = { name: name, hasTex: hasTex, texFile: tex, colorBGR: color, texSX: sx, texSY: sy, texAngle: ang, transp: transp, mirror: mirror, texOrient: orient, texOffX: offX, texOffY: offY }; /* Картинку отправляем под своим номером, а не под именем из Базиса: там кириллица, кавычки и пробелы, а имя файла на сервере — это то, во что нельзя пускать чужой текст. Настоящее имя остаётся в texFile, оно нужно только чтобы показать его человеку. */ if (hasTex && texFull) { var safe = kkTexRegister(texFull); if (safe) { rec.texSafe = safe; kkTexLink(safe, rec); } } kkMatCache[key] = rec; return rec; } /** Материал самой детали. */ function kkMaterial(obj) { var mt = null; try { mt = obj.Material; } catch (e) { mt = null; } return kkMaterialFrom(mt, obj, ''); } /* * Копия записи материала с прозрачностью — для детали под прозрачной * облицовкой (стекло в рамке фасада). Именно копия: запись общая на все * детали с этим материалом, и правка в ней сделала бы прозрачными все полки. */ function kkMatTransp(rec) { if (rec.transp > 0.5) return rec; var key = rec.name + '|' + rec.colorBGR + '|' + rec.texFile + '|transp'; if (kkMatCache[key]) return kkMatCache[key]; var copy = {}, k; for (k in rec) { if (rec.hasOwnProperty(k)) copy[k] = rec[k]; } copy.transp = 0.8; if (copy.texSafe) kkTexLink(copy.texSafe, copy); kkMatCache[key] = copy; return copy; } // ================================================================ облицовки /* * Облицовка пласти — то, чем в Базисе «оклеивают» деталь: плёнка ПВХ на * фасаде из белого МДФ, шпон, стекло в рамке. Толщина у неё часто 0, а * картинка дерева лежит именно на ней, а не на материале детали. Материал * детали при этом — белый МДФ без картинки, и фасады уезжали заливкой. * * У панели облицовки лежат списком Plastics: у каждой — имя материала * (Material) и сторона (Side: 0 — лицевая пласть, 1 — обратная). Имя — это * только имя; сам материал с картинкой надо взять из списка материалов * модели (MtList): MaterialExists(имя) говорит, есть ли такой, а * AddMaterial(имя) отдаёт его. Дальше он читается как любой другой материал. */ var kkPlastStat = { panels: 0, faces: 0, whole: 0, transp: 0, names: {}, order: [] }; var kkPlastProbed = false; var kkMtListsGlobal = null; // общие списки материалов модели, ищутся один раз var kkMtByNameCache = {}; // имя в нижнем регистре → материал или null var kkMtListHow = ''; // чем нашли список материалов — в отчёт /** Где искать материалы по имени. */ function kkMtListsFor(hostMt) { var lists = [], i; try { if (hostMt && hostMt.MtList) { lists[lists.length] = hostMt.MtList; if (!kkMtListHow) kkMtListHow = 'MtList у материала детали'; } } catch (e) {} if (kkMtListsGlobal === null) { kkMtListsGlobal = []; try { if (typeof MtList !== 'undefined' && MtList) kkMtListsGlobal[kkMtListsGlobal.length] = MtList; } catch (e) {} try { if (typeof Model !== 'undefined' && Model && Model.MtList) kkMtListsGlobal[kkMtListsGlobal.length] = Model.MtList; } catch (e) {} try { if (typeof Action !== 'undefined' && Action && Action.MtList) kkMtListsGlobal[kkMtListsGlobal.length] = Action.MtList; } catch (e) {} } for (i = 0; i < kkMtListsGlobal.length; i++) lists[lists.length] = kkMtListsGlobal[i]; return lists; } /** Варианты имени: как есть, до первого перевода строки, причёсанное. */ function kkNameVariants(raw) { var out = [], seen = {}, cands = [], i, s; raw = (raw === null || raw === undefined) ? '' : String(raw); cands[cands.length] = raw; var cut = raw.search(/[\r\n]/); if (cut > 0) cands[cands.length] = raw.substring(0, cut); cands[cands.length] = kkStr(raw); cands[cands.length] = kkStr(raw).replace(/^\s+|\s+$/g, ''); for (i = 0; i < cands.length; i++) { s = cands[i]; if (!s || seen[s]) continue; seen[s] = 1; out[out.length] = s; } return out; } function kkSameName(a, names) { var x = String(a).toLowerCase().replace(/^\s+|\s+$/g, ''), i; for (i = 0; i < names.length; i++) { if (x === names[i].toLowerCase().replace(/^\s+|\s+$/g, '')) return true; } return false; } /** Материал по имени из списка материалов модели; null — не нашёлся. */ function kkMtByName(raw, hostMt) { var names = kkNameVariants(raw); if (!names.length) return null; var key = names[0].toLowerCase(); if (kkMtByNameCache[key] !== undefined) return kkMtByNameCache[key]; var lists = kkMtListsFor(hostMt), found = null, i, j, L, nm, ok, mt; for (i = 0; i < lists.length && !found; i++) { L = lists[i]; for (j = 0; j < names.length && !found; j++) { nm = names[j]; ok = false; try { ok = !!L.MaterialExists(nm); } catch (e) { ok = false; } if (!ok) continue; mt = null; try { mt = L.AddMaterial(nm); } catch (e2) { mt = null; } if (mt) found = mt; } } /* Запасной путь — перебор списка по номеру и сравнение имён. Не зависит от того, как в этой версии зовутся методы поиска. */ if (!found) { for (i = 0; i < lists.length && !found; i++) { L = lists[i]; var cnt = 0; try { cnt = L.Count; } catch (e3) { cnt = 0; } for (j = 0; j < cnt && j < 5000 && !found; j++) { mt = null; try { mt = L.Materials[j]; } catch (e4) { mt = null; } if (!mt) { try { mt = L[j]; } catch (e5) { mt = null; } } if (!mt) continue; var mn = ''; try { mn = kkStr(mt.MaterialName); } catch (e6) {} if (!mn) { try { mn = kkStr(mt.Name); } catch (e7) {} } if (mn && kkSameName(mn, names)) found = mt; } } } kkMtByNameCache[key] = found; return found; } /** Первая встреченная облицовка: что у неё есть, а чего нет — в отчёт. */ function kkProbePlastic(p) { var s = ' облицовка (первая встреченная):'; var has = function (name) { var v; try { v = p[name]; } catch (e) { return name + '=нет'; } if (v === undefined || v === null) return name + '=нет'; if (typeof v === 'function') return name + '=метод'; if (typeof v === 'object') return name + '=объект'; return name + '=' + kkStr(v); }; s += ' ' + has('Material') + ', ' + has('Side') + ', ' + has('Sign') + ', ' + has('Thickness') + ', ' + has('Width') + ', ' + has('Name') + ', ' + has('MaterialName') + ', ' + has('TextureOrientation'); try { s += ' | класс ' + (kkClassOf(p) || '(не отдался)'); } catch (e) {} kkNote(s); } /** * Облицовки панели: {top, bottom, transp, n}. top/bottom — имя материала * (строка) или сам материал (объект), если Базис отдал его объектом. * Прозрачная облицовка (стекло) отдельной пластью не режется — панель * делаем полупрозрачной целиком, как показывают и другие просмотрщики. */ function kkPlastics(obj) { var out = { top: null, bottom: null, transp: false, n: 0 }; var coll = null, cnt = 0, i, p, m, nm, side; try { coll = obj.Plastics; } catch (e) { return out; } if (!coll) return out; try { cnt = coll.Count; } catch (e) { cnt = 0; } if (!(cnt > 0)) return out; for (i = 0; i < cnt; i++) { p = null; try { p = coll.Plastics[i]; } catch (e) { p = null; } if (!p) { try { p = coll.Items[i]; } catch (e) { p = null; } } if (!p) { try { p = coll[i]; } catch (e) { p = null; } } if (!p) continue; if (!kkPlastProbed) { kkPlastProbed = true; kkProbePlastic(p); } m = null; try { m = p.Material; } catch (e) { m = null; } if (m === null || m === undefined || m === '') continue; nm = ''; if (typeof m === 'object') { try { nm = kkStr(m.MaterialName); } catch (e) {} } else nm = kkStr(m); side = -1; try { side = p.Side; } catch (e) { side = -1; } if (typeof side !== 'number') side = -1; out.n++; if (/розрачн/i.test(nm)) { out.transp = true; continue; } /* Сторону не отдали — считаем лицевой: плёнка почти всегда на лице. */ if (side === 1) out.bottom = m; else out.top = m; } return out; } /** * Материал облицовки как запись для оглавления. spec — имя (строка) или * объект материала. Не нашёлся — null: деталь останется в своём материале, * а в отчёте будет написано, какое имя не нашлось. */ function kkPlasticMaterial(spec, obj) { var hostMt = null, mt = null, name = '', k, rec0 = null; try { hostMt = obj.Material; } catch (e) { hostMt = null; } if (typeof spec === 'object') { mt = spec; try { name = kkStr(mt.MaterialName); } catch (e) {} } else { name = kkStr(spec); mt = kkMtByName(spec, hostMt); } var st = kkPlastStat.names[name]; if (!st) { st = { n: 0, how: '', tex: '' }; kkPlastStat.names[name] = st; kkPlastStat.order[kkPlastStat.order.length] = name; } st.n++; if (!mt) { /* Может, такой материал уже читали у какой-то детали — тогда он в кэше записей, и его хватит. */ for (k in kkMatCache) { if (kkMatCache.hasOwnProperty(k) && kkMatCache[k].name === name) { rec0 = kkMatCache[k]; break; } } if (rec0) { st.how = (rec0.hasTex ? 'картинка' : 'цвет') + ' (материал взят у другой детали)'; st.tex = rec0.texFile || ''; return rec0; } st.how = 'НЕ НАЙДЕНА в материалах модели'; return null; } var rec = kkMaterialFrom(mt, obj, name); st.how = rec.hasTex ? 'картинка' : 'цвет'; st.tex = rec.texFile || ''; return rec; } /** Запись детали для оглавления. face: 0 — тело, 1 — лицевая пласть, 2 — обратная. */ function kkPartRec(kind, name, mat, g, blockIdx, animIdx, o, n, face, hw) { var r = { type: kind, name: name || kind, mat: mat, blockIdx: blockIdx, animIdx: animIdx, /* Номер фурнитурного узла: покупное изделие в Базисе собрано блоком (направляющая, петля, опора — несколько тел разом). Схема сборки по этому номеру не даёт узлу развалиться на части: в жизни направляющая на пластинки не разбирается. −1 — узла нет. */ hw: (hw === undefined || hw === null) ? -1 : hw, grainDir: g.dir, grainAxL: g.axL, grainAxW: g.axW, grainLW: g.lw, grainSize: g.size, geom: { o: o * 36, n: n } }; if (face) r.face = face; return r; } // ================================================================ анимация var kkAnims = []; /** * Раскладываем анимацию так, как её ждёт просмотрщик: для поворота пара точек * задаёт ось петли, для сдвига — вектор хода. Записи без оси всё равно * попадают в список: на них ссылаются как на родителей, и если их пропустить, * номера у остальных съедут. */ function kkAnimIndex(obj, parentIdx) { /* HasAnimation встречается и свойством, и методом. Считать его только методом нельзя: у свойства вышло бы «анимации нет», и фасады перестали бы открываться вообще — молча, без единой ошибки. */ var has = false; try { has = obj.HasAnimation; if (typeof has === 'function') has = obj.HasAnimation(); } catch (e) { has = false; } if (!has) return -1; var a = null; try { a = obj.Animation; } catch (e) {} if (!a) return -1; var rec = { salonType: -1, angle: 0, panels: 1, parent: (parentIdx === undefined ? -1 : parentIdx), animType: 0, doorShift: 0, dur: 1, len: 0, axisP1: null, axisP2: null }; try { if (typeof obj.SalonType === 'number') rec.salonType = obj.SalonType; } catch (e) {} try { if (typeof a.AnimType === 'number') rec.animType = a.AnimType; } catch (e) {} try { if (typeof a.DoorAngle === 'number') rec.angle = a.DoorAngle; } catch (e) {} try { if (typeof a.DoorShift === 'number') rec.doorShift = a.DoorShift; } catch (e) {} try { if (typeof a.Duration === 'number' && a.Duration > 0) rec.dur = a.Duration; } catch (e) {} try { var p1 = a.AxisStart, p2 = a.AxisEnd; if (p1 && p2 && p1.x !== undefined) { var w1 = obj.ToGlobal(NewVector(p1.x, p1.y, p1.z)); var w2 = obj.ToGlobal(NewVector(p2.x, p2.y, p2.z)); rec.axisP1 = [w1.x, w1.y, w1.z]; rec.axisP2 = [w2.x, w2.y, w2.z]; var dx = w2.x - w1.x, dy = w2.y - w1.y, dz = w2.z - w1.z; rec.len = Math.sqrt(dx * dx + dy * dy + dz * dz); } } catch (e) {} kkAnims[kkAnims.length] = rec; return kkAnims.length - 1; } // ================================================================== обход var kkModules = []; var kkParts = []; var kkCounters = { panel: 0, fastener: 0, body: 0, profile: 0 }; /* * Перепись модели. Скрипт впервые работает в настоящем Базисе, и мы не знаем * наверняка, как эта версия называет свои объекты и какие свойства у них есть. * Поэтому по дороге записываем, что встретилось, и кладём отчёт отдельным * файлом рядом с временной папкой — он переживает уборку и его можно прислать. */ var kkClsSeen = {}; var kkSkipped = {}; var kkProbed = {}; var kkNotes = []; var kkSeenObj = {}; // UID уже взятых объектов — от повторной выгрузки function kkNote(s) { kkNotes[kkNotes.length] = s; } /* ------------------------------------------------------------ журнал сбоев * * Всё, что пошло не так, обязано быть ВИДНО. Скрипт устроен так, что почти * каждое чтение обёрнуто в try/catch: у разных версий Базиса свойства зовутся * по-разному, и падать из-за одного имени нельзя. Но у молчаливого catch есть * обратная сторона — беда проходит незамеченной. Дороже всего это на кромках: * не показали кромку там, где она есть, — деталь уедет на производство * незакатанной и станет браком. * * Поэтому места, где сбой означает ПОТЕРЮ ДАННЫХ (а не просто «такого имени * в этой версии нет»), пишут сюда. Список показывается первым делом в окне * перед отправкой, попадает в отчёт и уезжает на сайт вместе с ним. */ var kkOops = { n: 0, by: {}, order: [] }; function kkOopsAdd(where, e) { var msg = ''; try { msg = kkStr((e && (e.message || e.description)) || e || ''); } catch (e2) { msg = ''; } if (msg.length > 160) msg = msg.substring(0, 160) + '…'; var rec = kkOops.by[where]; if (!rec) { rec = { n: 0, msg: msg }; kkOops.by[where] = rec; kkOops.order[kkOops.order.length] = where; } if (!rec.msg && msg) rec.msg = msg; rec.n++; kkOops.n++; } /** Первый объект каждого класса: что у него есть, а чего нет. */ function kkProbeObject(obj, cls) { var s = ' ' + (cls || '(имя класса не отдалось)') + ':'; var has = function (name) { var v; try { v = obj[name]; } catch (e) { return name + '=нет'; } if (v === undefined || v === null) return name + '=нет'; if (typeof v === 'function') return name + '=метод'; if (typeof v === 'object') return name + '=есть'; return name + '=' + v; }; s += ' ' + has('Count') + ',' + ' ' + has('Objects') + ',' + ' ' + has('TriListsCount'); s += ', ' + has('HasAnimation') + ', ' + has('GSize') + ', ' + has('Width') + ', ' + has('Height'); s += ', ' + has('TextureOrientation') + ', ' + has('NToGlobal'); s += ', ' + has('Plastics') + ', ' + has('Butts'); /* Данные сборки: галочка «сборочная единица», позиция и обозначение по спецификации, примечание, кромки, отверстия присадки, габарит. Какие из этих имён отдаёт ЭТА версия Базиса, заранее не знает никто — только отсюда и видно, на что опираться, если сборка уехала пустой. */ s += ', ' + has('IsAssemblyUnit') + ', ' + has('ArtPos') + ', ' + has('Designation') + ', ' + has('Notes') + ', ' + has('MakeAssemblyDrawing') + ', ' + has('List') + ', ' + has('Holes') + ', ' + has('DatumMode') + ', ' + has('GabMin') + ', ' + has('GabMax') + ', ' + has('Contour') + ', ' + has('MaterialName') + ', ' + has('MaterialWidth'); try { var bc = obj.Butts ? obj.Butts.Count : 0; if (bc > 0) { s += ', кромок=' + bc; var b0 = null; try { b0 = obj.Butts.Butts[0]; } catch (e) { b0 = null; } if (!b0) { try { b0 = obj.Butts.Items[0]; } catch (e) { b0 = null; } } if (!b0) { try { b0 = obj.Butts[0]; } catch (e) { b0 = null; } } if (b0) { var hb = function (name) { var v; try { v = b0[name]; } catch (e) { return name + '=нет'; } if (v === undefined || v === null) return name + '=нет'; if (typeof v === 'object') { try { return name + '=' + kkStr(v.MaterialName || v.Name || 'объект'); } catch (e2) { return name + '=объект'; } } return name + '=' + kkStr(v); }; s += ' (первая: ' + hb('Material') + ', ' + hb('Thickness') + ', ' + hb('Width') + ', ' + hb('ElemIndex') + ', ' + hb('Allowance') + ')'; } } } catch (e) {} try { var hc = obj.Holes ? obj.Holes.Count : 0; if (hc > 0) { s += ', отверстий=' + hc; var h0 = null; try { h0 = obj.Holes[0]; } catch (e) { h0 = null; } if (!h0) { try { h0 = obj.Holes.Items[0]; } catch (e) { h0 = null; } } if (h0) { var hh = function (name) { var v; try { v = h0[name]; } catch (e) { return name + '=нет'; } if (v === undefined || v === null) return name + '=нет'; if (typeof v === 'object') { try { return name + '=' + kkNum(v.x) + ',' + kkNum(v.y) + ',' + kkNum(v.z); } catch (e2) { return name + '=объект'; } } return name + '=' + kkStr(v); }; s += ' (первое: ' + hh('Radius') + ', ' + hh('Depth') + ', ' + hh('Position') + ', ' + hh('Direction') + ', ' + hh('Through') + ', ' + hh('DrillMode') + ')'; } } } catch (e) {} /* Нормаль первого треугольника: по ней отделяем пласть под облицовку. Важно знать, отдаёт ли её эта версия Базиса. */ try { var nrm = obj.TriLists[0].Triangles[0].Normal; s += ', Normal=' + (nrm ? (kkNum(nrm.x) + ',' + kkNum(nrm.y) + ',' + kkNum(nrm.z)) : 'нет'); } catch (e) { s += ', Normal=нет'; } try { var pc = obj.Plastics ? obj.Plastics.Count : 0; if (pc > 0) s += ', облицовок=' + pc; } catch (e) {} try { var mt = obj.Material; if (mt) { var p = ''; try { p = kkStr(mt.Path); } catch (e) { p = '(нет Path)'; } var pa = ''; try { pa = kkStr(mt.PathAbsolute()); } catch (e) { pa = '(нет PathAbsolute)'; } s += '\r\n материал: Path=' + p + ' | PathAbsolute=' + pa; } } catch (e) {} kkNote(s); } /* Имя класса объекта: Базис отдаёт его строкой вида «[object TFurnPanel]». Через instanceof это делать нельзя — обращение к имени, которого в данной версии Базиса не объявлено, обрывает работу скрипта целиком. Сравнение строк работает всегда и в любой версии. */ function kkClassOf(obj) { var s = ''; /* Спрашиваем именно toString(). String(obj) у COM-объекта сначала берёт «значение по умолчанию» — а это часто имя детали, и тогда вместо класса вернулась бы «Полка». Запасной путь оставлен на случай, если toString у объекта не объявлен вовсе. */ try { s = obj.toString(); } catch (e) { s = ''; } if (!kkLooksClass(s)) { try { s = String(obj); } catch (e) { s = ''; } } return kkLooksClass(s) ? s.substring(8, s.length - 1) : ''; } function kkLooksClass(s) { return (typeof s === 'string' && s.length > 9 && s.substring(0, 8) === '[object ' && s.charAt(s.length - 1) === ']'); } function kkTypeOf(obj) { var cls = kkClassOf(obj); if (cls === 'TFurnPanel') return 'panel'; if (cls === 'TFastener') return 'fastener'; if (cls === 'TExtrusionBody') return 'profile'; if (cls !== '') return 'body'; /* Имя класса не отдалось. Панель важно узнать даже так: «прочее тело» сборщик сваривает по общим вершинам, и панель стала бы похожа на подушку — со сглаженными рёбрами вместо острых. */ try { if (typeof obj.TextureOrientation === 'number') return 'panel'; } catch (e) {} return 'body'; } /** Габаритная рамка, слои и осевые линии в модель не входят. */ function kkSkip(obj) { var cls = kkClassOf(obj); if (cls === 'TModelLimits' || cls === 'TLayer' || cls === 'TLine3D') return 'служебный объект'; /* Рамку рисуют и обычной деталью — тогда по классу её не отличить, а на сайт уехала бы коробка вокруг всей мебели. Смотрим ещё и на имя. */ var nm = ''; try { nm = kkStr(obj.Name); } catch (e) {} if (nm && /габаритн\S*\s+рамк/i.test(nm)) return 'габаритная рамка по имени'; try { if (obj.Visible === false) return 'скрыт'; } catch (e) {} return ''; } /** Есть ли своя геометрия хотя бы у одного ребёнка. */ function kkKidsHaveGeom(obj, count) { var lim = count > 48 ? 48 : count, i, kid, kids = null; try { kids = obj.Objects; } catch (e) { kids = null; } for (i = 0; i < lim; i++) { try { kid = kids ? kids[i] : obj[i]; if (kid && kid.TriListsCount > 0 && kid.TriLists[0]) return true; } catch (e) {} } return false; } /** Направление волокон и габарит панели — по ним просмотрщик кладёт текстуру. */ function kkGrain(obj, kind) { if (kind !== 'panel') return { dir: null, lw: null, size: null }; var dir = null, w = 0, h = 0, th = 0, size = null; // «Направление» из свойств детали: по вертикали или по горизонтали try { if (typeof obj.TextureOrientation === 'number') dir = obj.TextureOrientation; } catch (e) {} /* Габарит детали. Width и Height Базис отдаёт не всегда — на настоящей гардеробной не отдал ни разу, и раскладывать текстуру было не по чему. Настоящий источник — GSize, вектор «длина, ширина, толщина», он же показан в свойствах детали строкой «Габариты». */ try { var g = obj.GSize; if (g && g.x !== undefined) { size = [kkNum1(g.x), kkNum1(g.y), kkNum1(g.z)]; w = g.x; h = g.y; th = g.z; } } catch (e) {} if (!w) { try { w = obj.Length; } catch (e) {} } if (!h) { try { h = obj.Width; } catch (e) {} } if (!th) { try { th = obj.Thickness; } catch (e) {} } if (!w) { try { w = obj.Width; } catch (e) {} } if (!h) { try { h = obj.Height; } catch (e) {} } return { dir: dir, axL: kkAxis(obj, 1, 0), // ось вдоль длины детали axW: kkAxis(obj, 0, 1), // ось вдоль ширины детали lw: (w && h) ? [kkNum1(w), kkNum1(h)] : null, size: size || ((w && h && th) ? [kkNum1(w), kkNum1(h), kkNum1(th)] : null) }; } /* * Собственные оси детали в координатах комнаты. * * Это и есть точный ответ, и знает его сам Базис: у детали свои оси, а * NToGlobal переводит направление из осей детали в мировые. Берём ОБЕ оси * пласти — вдоль длины и вдоль ширины. * * Выбор между ними здесь НЕ делаем. «Направление» в свойствах детали — это * «по вертикали» или «по горизонтали», то есть про комнату; а какая из осей * детали смотрит вверх, видно только когда известна вертикаль всей модели. * Поэтому выбор делает сборщик, а мы отдаём обе оси как есть. */ function kkAxis(obj, dx, dy) { var x = 0, y = 0, z = 0, got = false; try { var v = obj.NToGlobal(NewVector(dx, dy, 0)); if (v && v.x !== undefined) { x = v.x; y = v.y; z = v.z; got = true; } } catch (e) {} /* Нет NToGlobal — тот же вектор через две точки: начало детали и точку, сдвинутую по нужной оси. Разница и есть направление. */ if (!got) { try { var o = obj.ToGlobal(NewVector(0, 0, 0)); var q = obj.ToGlobal(NewVector(dx, dy, 0)); x = q.x - o.x; y = q.y - o.y; z = q.z - o.z; got = true; } catch (e2) {} } if (!got) return null; var len = Math.sqrt(x * x + y * y + z * z); if (!(len > 1e-9)) return null; return [ Math.round(x / len * 10000) / 10000, Math.round(y / len * 10000) / 10000, Math.round(z / len * 10000) / 10000 ]; } /** Округление до 0.1 мм: точнее для габарита детали не нужно. */ function kkNum1(v) { var n = Number(v); return isFinite(n) ? Math.round(n * 10) / 10 : 0; } /** Склонение по числу: 1 замечание, 2 замечания, 5 замечаний. */ function kkWord(n, one, few, many) { n = Math.abs(n) % 100; var d = n % 10; if (n > 10 && n < 20) return many; if (d > 1 && d < 5) return few; if (d === 1) return one; return many; } /** Округление до 0.01 мм — там, где 0,1 мм огрубляет: радиус сверла 1,25. */ function kkNum2(v) { var n = Number(v); return isFinite(n) ? Math.round(n * 100) / 100 : 0; } /* Сколько фурнитурных узлов уже встретилось: номер узла и есть его порядок. */ var kkHwCount = 0; function kkWalk(obj, blockIdx, animIdx, depth, hwIdx) { if (hwIdx === undefined) hwIdx = -1; if (kkFail || depth > 14) return; var count = 0, i, child, kids = null; try { count = obj.Count; } catch (e) { return; } /* Дети лежат в коллекции Objects. Перебор самого объекта по номеру (obj[i]) работает не везде: где не работает — цикл прошёл бы вхолостую и модель оказалась бы «пустой». Поэтому сначала Objects, потом запасной путь по номеру. */ try { kids = obj.Objects; } catch (e) { kids = null; } for (i = 0; i < count; i++) { try { child = kids ? kids[i] : obj[i]; } catch (e) { continue; } if (!child || child === obj) continue; var cls = kkClassOf(child); var clsKey = cls || '(имя класса не отдалось)'; kkClsSeen[clsKey] = (kkClsSeen[clsKey] || 0) + 1; kkUnknownSeen(cls); if (!kkProbed[clsKey]) { kkProbed[clsKey] = 1; kkProbeObject(child, cls); } /* Перепись ведём ДО отсева. Иначе в самом нужном случае — модель уехала пустой, потому что всё отсеялось — отчёт был бы пустым тоже, и причину по нему увидеть было бы нельзя. */ var why = kkSkip(child); if (why) { var sk = clsKey + ' — ' + why; kkSkipped[sk] = (kkSkipped[sk] || 0) + 1; continue; } // размеры и надписи — отдельные объекты модели, у них своя дорога if (cls === 'TSize3D') { var dr = kkDimRecord(child, false); if (dr) kkParts[kkParts.length] = dr; continue; } if (cls === 'TContour3D') { var cr = kkDimRecord(child, true); if (cr) kkParts[kkParts.length] = cr; continue; } var myBlock = blockIdx; var myAnim = animIdx; var sub = 0; try { sub = child.Count; } catch (e) { sub = 0; } /* Модуль — это шкаф, пенал, тумба. Наборы крепежа (TAsmKit) модулями НЕ считаем: на настоящей гардеробной именно из-за них в список «Модули» попали «Конфирмат + Шкант» и «Опора Valpato в сборе», и разбивки по шкафам не получилось. На верхнем уровне модулем считаем и объект с вложенными: так делает рабочий образец. Вложенный блок или покупное изделие (ящик внутри тумбы, петля в сборе, опоры) — НЕ отдельный модуль: он остаётся в модуле своего шкафа. Иначе схема сборки шкафа теряла бы его ящики, петли и опоры, а в списке модулей появлялись «Опора Valpato в сборе». Вложенная сборочная единица для схемы отмечается отдельно (sborka.json). */ var isBlock = blockIdx < 0 && ((cls === 'TFurnBlock' || cls === 'TFurnAsm') || (depth === 0 && sub > 0 && cls !== 'TFurnPanel' && cls !== 'TAsmKit')); if (isBlock) { var nm = ''; try { nm = kkStr(child.Name); } catch (e) {} if (nm === '') nm = 'Модуль ' + (kkModules.length + 1); kkModules[kkModules.length] = nm; myBlock = kkModules.length - 1; } /* Вложенный блок внутри модуля — покупное изделие: направляющая, петля, опора. Заводим ему номер, и все его тела едут одним узлом. Берём САМЫЙ ГЛУБОКИЙ такой блок: направляющая внутри ящика должна остаться отдельным узлом, а не слиться со всем ящиком. Панелей это не касается — лист режут, он разлетается всегда. */ var myHw = hwIdx; if (!isBlock && blockIdx >= 0 && sub > 0 && (cls === 'TFurnBlock' || cls === 'TFurnAsm' || cls === 'TAsmKit')) { kkHwCount++; myHw = kkHwCount; } // анимация: если у объекта своя — дети наследуют её как родителя var own = kkAnimIndex(child, myAnim); if (own >= 0) myAnim = own; /* Один и тот же объект виден из двух мест обхода: он и в сборке, и в общей коллекции модели. Без памяти о взятых он уехал бы двумя копиями в одном месте — поверхности начали бы мерцать. */ var uid = null, twice = false; try { uid = child.UID; } catch (e) {} if (uid !== null && uid !== undefined) { if (kkSeenObj[uid]) twice = true; else kkSeenObj[uid] = 1; } var kind = kkTypeOf(child); /* Данные сборки: модуль или вложенная сборочная единица открывает свою запись и ложится в стопку, пока идём по её поддереву. Любая ошибка здесь гасится и только помечает сборку как пропущенную — сама модель должна уехать в любом случае. */ var sbUi = -1; try { sbUi = kkSbEnter(child, myBlock, isBlock, sub, kind, twice); } catch (e) { kkSbFail(e); } /* Контейнер, у которого геометрия есть и у него самого, и у детей: берём детей, своё тело пропускаем — иначе одна и та же оболочка уедет дважды. Панели это не касается: её тело — только поверхность, а дети — присаженные петли и полкодержатели, это разные вещи. */ var skipBody = twice; if (!skipBody && kind !== 'panel' && sub > 0 && kkTriCount(child) > 0) { skipBody = kkKidsHaveGeom(child, sub); } /* Облицовки пластей есть только у панелей. Если они есть, пласть под облицовкой едет отдельным куском геометрии — под свой материал. */ var pl = null; if (!skipBody && kind === 'panel') { try { pl = kkPlastics(child); } catch (e) { pl = null; } } var sel = (pl && (pl.top || pl.bottom)) ? { top: !!pl.top, bottom: !!pl.bottom } : null; var before = kkTriTotal; kkSbBoxReset(); var w = skipBody ? { n: 0, top: 0, bottom: 0 } : kkWriteBody(child, sel); var hadBody = (w.n + w.top + w.bottom) > 0; /* Отверстия крепежа читаем один раз: они нужны и на цилиндры ниже, и в данные сборки. Дважды ходить к Базису за одним и тем же незачем. */ var sbHoles = null; if (kind === 'fastener' && !twice) { try { sbHoles = kkSbReadHoles(child); } catch (e) { kkSbFail(e); sbHoles = null; } } /* Крепёж без тела, но с отверстиями — это присадка: безымянный «Крепёж» с конфирматами и шкантами, у которого Базис держит только точки сверловки. Тела у него нет, и сборщик его построить не может, а без него на схеме сборки не будет ни мест установки крепежа, ни запасного разбора присадки. Поэтому рисуем по цилиндру на каждое отверстие — только если тела нет ни у самого объекта, ни у детей, иначе вышло бы дважды. */ if (!skipBody && !hadBody && kind === 'fastener' && sbHoles && sbHoles.length && (sub === 0 || !kkKidsHaveGeom(child, sub))) { try { w.n = kkSbHoleCylinders(child, sbHoles); } catch (e) { kkSbFail(e); w.n = 0; } if (w.n > 0) w.cyl = true; } var n = w.n; var g = kkGrain(child, kind); var nm2 = ''; try { nm2 = kkStr(child.Name); } catch (e) {} if (nm2 === '') { try { nm2 = kkStr(child.Designation); } catch (e) {} } /* Присадка по отверстиям уезжает под именем «Крепёж» — под ним её узнаёт и схема сборки, и человек в списке деталей. */ if (nm2 === '' && w.cyl) nm2 = 'Крепёж'; if (n + w.top + w.bottom > 0) { var baseMat = kkMaterial(child); if (pl && pl.transp) { baseMat = kkMatTransp(baseMat); kkPlastStat.transp++; } if (pl && pl.n) kkPlastStat.panels++; var bodyRec = null; if (n > 0) { bodyRec = kkPartRec(kind, nm2, baseMat, g, myBlock, myAnim, before, n, 0, myHw); kkParts[kkParts.length] = bodyRec; } /* Пласти — следом за телом, каждая своим куском. Если пласть отделить не удалось (нормалей нет), облицовка ложится на всю деталь: дерево на всём фасаде лучше, чем белый фасад. */ var o2 = before + n; if (pl && pl.top) { var fm = kkPlasticMaterial(pl.top, child); if (w.top > 0) { kkParts[kkParts.length] = kkPartRec(kind, nm2, fm || baseMat, g, myBlock, myAnim, o2, w.top, 1, myHw); o2 += w.top; kkPlastStat.faces++; } else if (fm && bodyRec) { bodyRec.mat = fm; kkPlastStat.whole++; } } if (pl && pl.bottom) { var fm2 = kkPlasticMaterial(pl.bottom, child); if (w.bottom > 0) { kkParts[kkParts.length] = kkPartRec(kind, nm2, fm2 || baseMat, g, myBlock, myAnim, o2, w.bottom, 2, myHw); o2 += w.bottom; kkPlastStat.faces++; } else if (fm2 && bodyRec && !pl.top) { bodyRec.mat = fm2; kkPlastStat.whole++; } } kkCounters[kind] = (kkCounters[kind] || 0) + 1; } /* Данные сборки по этой детали: позиция панели с кромками, крепёж с отверстиями, профиль, габарит единицы по записанным вершинам. Повторно встреченный объект (twice) пропускаем: в сборке он уже учтён, а второй раз посчитанный конфирмат удвоил бы спецификацию. */ if (!twice) { /* Своя геометрия у объекта есть, если мы её записали — или если пропустили нарочно, потому что её несут дети (skipBody без twice бывает только так). */ try { kkSbPart(child, kind, nm2 || kind, g, hadBody || skipBody, sbHoles); } catch (e) { kkSbFail(e); } } /* Вглубь заходим всегда, в том числе внутрь панели: петли, чашки, полкодержатели и конфирматы в Базисе — дети той панели, к которой присажены. Не зайти внутрь значило бы отдать фасады без единой петли. От повторов бережёт память о взятых объектах выше. */ if (sub > 0) kkWalk(child, myBlock, myAnim, depth + 1, myHw); if (sbUi >= 0) kkSbLeave(); } } // ================================================================ размеры /* * Размеры — это отдельные объекты модели (TSize3D), а не свойство детали. * Геометрии у них нет, только отрезки выносных и размерной линий, поэтому * они едут не в файлах геометрии, а прямо в оглавлении. Само число * просмотрщик рисует сам и всегда разворачивает к камере. * * Один и тот же размер может встретиться в обходе несколько раз (он виден * и из модуля, и из модели), поэтому запоминаем уже взятые по их UID. */ var kkSeenDim = {}; function kkIsClass(obj, name) { return kkClassOf(obj) === name; } function kkDimRecord(d, isContour) { var uid = null; try { uid = d.UID; } catch (e) {} if (uid !== null && uid !== undefined) { if (kkSeenDim[uid]) return null; kkSeenDim[uid] = 1; } var edges = [], ec = 0, k; try { ec = d.EdgesCount; } catch (e) { ec = 0; } for (k = 0; k < ec; k++) { try { var ed = d.Edges[k]; if (ed && ed.GFirst && ed.GLast) { edges[edges.length] = [ [ed.GFirst.x, ed.GFirst.y, ed.GFirst.z], [ed.GLast.x, ed.GLast.y, ed.GLast.z] ]; } } catch (e) {} } var rec = isContour ? { type: 'contour', name: 'Надпись', segs: edges } : { type: 'dim', name: 'Размер', edges: edges }; if (isContour) { try { if (typeof d.Color === 'number') rec.colorBGR = d.Color; } catch (e) {} var nmc = ''; try { nmc = kkStr(d.Name); } catch (e) {} if (nmc) rec.name = nmc; return edges.length ? rec : null; } /* Если рёбра не отдались, размер всё равно можно построить: у него есть собственная длина и вынос, а точки переводятся в мировые координаты. */ if (!edges.length) { var size = 0, len = 0; try { size = d.Size; } catch (e) {} try { len = d.Length; } catch (e) {} if (size && size > 0.01) { try { var g = function (a, b) { var p = d.ToGlobal(NewVector(a, b, 0)); return [p.x, p.y, p.z]; }; rec.fallback = { hasLen: (len > 0.01), a0: g(0, 0), a1: g(0, len > 0 ? len : 0), b0: g(size, 0), b1: g(size, len > 0 ? len : 0) }; } catch (e) {} } if (!rec.fallback) return null; } try { if (typeof d.Value === 'number') rec.value = d.Value; } catch (e) {} if (rec.value === undefined) { try { if (typeof d.Size === 'number') rec.value = d.Size; } catch (e) {} } try { if (typeof d.TextSize === 'number' && d.TextSize > 5 && d.TextSize < 1000) rec.textSize = d.TextSize; } catch (e) {} if (rec.value !== undefined) rec.name = 'Размер ' + Math.round(rec.value); return rec; } // ================================================================= сборка /* * Данные сборки — файл sborka.json рядом с оглавлением. По нему сайт рисует * схему сборки модуля: какие детали в шкафу, где у них кромки, куда присажен * крепёж и сколько его, что Базис считает сборочной единицей. В геометрии * всего этого нет: там только поверхности. * * Собираем в том же проходе kkWalk, что и геометрию, — нарочно. Номер модуля * и центры деталей тогда ровно те же, что уехали в оглавление и в готовую * модель, и сайт сопоставляет запись сборки с телом на сцене по расстоянию * между центрами, а не по именам, которые в Базисе повторяются. * * Файл пишется чистым ASCII: всё, что не латиница, уходит в \uXXXX. Базис * пишет файлы в системной кодировке, а сайт отдаёт этот файл как есть — с * русскими буквами внутри он бы читался кракозябрами. */ var kkSb = { units: [], // сборочные единицы: модули (nested 0) и вложенные с галочкой (nested 1) pos: [], // позиции: панели (k 'p') и профили (k 'r') posKey: {}, // ключ одинаковости позиции → индекс в pos bands: [], // легенда кромок {t, w, mat} bandKey: {}, // ключ кромки → номер (с единицы) mats: [], // материалы панелей {name, art, thick, n, mm2} matKey: {}, // имя|толщина → индекс в mats fastAll: {}, // сводный крепёж: ключ «имя\x01обозначение» → штук fastOrder: [], // ключи крепежа в порядке появления holes: [], // отверстия присадки [x1,y1,z1,x2,y2,z2,r] в ГСК holeKey: {}, // от повторов nFast: 0, // всего крепежа (штук, сосчитанного) err: '', // первая ошибка сбора — тогда файл не пишем written: false }; var kkSbStack = []; // индексы единиц, внутри которых сейчас идёт обход var KK_SB_HOLES_MAX = 20000; // потолок отверстий: больше схеме не нужно, а файл раздуло бы var KK_SB_SEG = 10; // граней у цилиндра отверстия /* Габарит текущей детали по записанным вершинам. Сбрасывается перед записью каждой детали в kkWalk, пополняется из kkTri. */ var kkSbBox = { has: false, x0: 0, y0: 0, z0: 0, x1: 0, y1: 0, z1: 0 }; function kkSbBoxReset() { kkSbBox.has = false; } /* Один вызов на треугольник, сравнения внутри: это самое горячее место скрипта, на большой модели сюда приходят сотни тысяч раз. */ function kkSbBoxTri(ax, ay, az, bx, by, bz, cx, cy, cz) { var b = kkSbBox; if (!b.has) { b.has = true; b.x0 = b.x1 = ax; b.y0 = b.y1 = ay; b.z0 = b.z1 = az; } if (ax < b.x0) b.x0 = ax; else if (ax > b.x1) b.x1 = ax; if (ay < b.y0) b.y0 = ay; else if (ay > b.y1) b.y1 = ay; if (az < b.z0) b.z0 = az; else if (az > b.z1) b.z1 = az; if (bx < b.x0) b.x0 = bx; else if (bx > b.x1) b.x1 = bx; if (by < b.y0) b.y0 = by; else if (by > b.y1) b.y1 = by; if (bz < b.z0) b.z0 = bz; else if (bz > b.z1) b.z1 = bz; if (cx < b.x0) b.x0 = cx; else if (cx > b.x1) b.x1 = cx; if (cy < b.y0) b.y0 = cy; else if (cy > b.y1) b.y1 = cy; if (cz < b.z0) b.z0 = cz; else if (cz > b.z1) b.z1 = cz; } /** Первая ошибка сбора. Дальше сборка считается пропущенной, но экспорт идёт. */ function kkSbFail(e) { if (kkSb.err) return; var m = ''; try { m = kkStr((e && e.message) || e || 'ошибка'); } catch (e2) { m = 'ошибка'; } kkSb.err = m || 'ошибка'; kkNote(' сборка: пропущена — ' + kkSb.err); } /** Число из чего угодно: не число — 0. */ function kkSbNum(v) { var n = Number(v); return isFinite(n) ? n : 0; } /** Вектор {x,y,z} Базиса в массив чисел или null. */ function kkSbVec(v) { try { if (v && v.x !== undefined && v.x !== null) { var x = Number(v.x), y = Number(v.y), z = Number(v.z); if (isFinite(x) && isFinite(y) && isFinite(z)) return [x, y, z]; } } catch (e) {} return null; } /** Габарит объекта из GabMin/GabMax — запасной путь, когда вершин не было. */ function kkSbGab(obj) { var a = null, b = null; try { a = kkSbVec(obj.GabMin); } catch (e) { a = null; } try { b = kkSbVec(obj.GabMax); } catch (e) { b = null; } if (!a || !b) return null; return { min: a, max: b }; } /** Нормаль лицевой пласти (+Z детали) в координатах комнаты, до 0.001. */ /* Ось детали (ax, ay, az) в ГСК, единичный вектор с округлением 0.001. fz — нормаль лицевой пласти; fx/fy — оси контура: по ним страница схемы переводит стороны кромки из осей контура в стороны повёрнутого листа. */ function kkSbDir(obj, ax, ay, az) { var x = 0, y = 0, z = 0, got = false; try { var v = obj.NToGlobal(NewVector(ax, ay, az)); if (v && v.x !== undefined) { x = v.x; y = v.y; z = v.z; got = true; } } catch (e) {} if (!got) { try { var o = obj.ToGlobal(NewVector(0, 0, 0)); var q = obj.ToGlobal(NewVector(ax, ay, az)); x = q.x - o.x; y = q.y - o.y; z = q.z - o.z; got = true; } catch (e2) {} } if (!got) return null; var len = Math.sqrt(x * x + y * y + z * z); if (!(len > 1e-6)) return null; return [Math.round(x / len * 1000) / 1000, Math.round(y / len * 1000) / 1000, Math.round(z / len * 1000) / 1000]; } function kkSbFz(obj) { return kkSbDir(obj, 0, 0, 1); } /* * Единицы, которым принадлежит текущая деталь: от самой вложенной вверх по * стопке до ближайшего модуля включительно. Дальше не идём: вложенный модуль * (покупное изделие внутри шкафа) в готовой модели — отдельный модуль со * своим номером, и его крепёж в спецификацию шкафа не входит. */ function kkSbActive() { var out = [], i, u; for (i = kkSbStack.length - 1; i >= 0; i--) { u = kkSb.units[kkSbStack[i]]; out[out.length] = kkSbStack[i]; if (!u.nested) break; } return out; } /** * Начало контейнера в обходе. Модуль всегда даёт запись единицы (nested 0) — * по одной на каждый номер модуля, чтобы номер в сборке и в готовой модели * был один и тот же. Вложенный контейнер даёт запись (nested 1), только если * в Базисе у него стоит галочка «сборочная единица». Возвращает индекс * открытой единицы (он же положен в стопку) или -1. */ function kkSbEnter(obj, blockIdx, isBlock, sub, kind, twice) { var au = false; if (isBlock || (sub > 0 && kind === 'body' && !twice)) { try { au = (obj.IsAssemblyUnit === true); } catch (e) { au = false; } } if (!isBlock && !au) return -1; var name = ''; if (isBlock) name = kkModules[blockIdx] || ''; if (!name) { try { name = kkStr(obj.Name); } catch (e) {} } if (!name) name = isBlock ? ('Модуль ' + (blockIdx + 1)) : 'Блок'; var u = { top: blockIdx, nested: isBlock ? 0 : 1, au: au ? 1 : 0, name: name, pos: '', dz: '', note: '', draw: 1, box: null, gab: null, fast: {}, fastOrder: [], kids: [] }; try { u.pos = kkStr(obj.ArtPos); } catch (e) {} try { u.dz = kkStr(obj.Designation); } catch (e) {} try { u.note = kkStr(obj.Notes); } catch (e) {} try { if (obj.MakeAssemblyDrawing === false) u.draw = 0; } catch (e) {} u.gab = kkSbGab(obj); kkSb.units[kkSb.units.length] = u; var idx = kkSb.units.length - 1; /* Вложенная единица — ребёнок той, внутри которой встретилась. */ if (u.nested && kkSbStack.length) { var parent = kkSb.units[kkSbStack[kkSbStack.length - 1]]; parent.kids[parent.kids.length] = idx; } kkSbStack[kkSbStack.length] = idx; return idx; } function kkSbLeave() { if (kkSbStack.length) kkSbStack.length = kkSbStack.length - 1; } /** Габарит записанной детали — в единицы, которым она принадлежит. */ function kkSbGrowUnits(box) { var act = kkSbActive(), i, u, b; for (i = 0; i < act.length; i++) { u = kkSb.units[act[i]]; b = u.box; if (!b) { u.box = { x0: box.x0, y0: box.y0, z0: box.z0, x1: box.x1, y1: box.y1, z1: box.z1 }; continue; } if (box.x0 < b.x0) b.x0 = box.x0; if (box.x1 > b.x1) b.x1 = box.x1; if (box.y0 < b.y0) b.y0 = box.y0; if (box.y1 > b.y1) b.y1 = box.y1; if (box.z0 < b.z0) b.z0 = box.z0; if (box.z1 > b.z1) b.z1 = box.z1; } } /** Центр детали для записи экземпляра: по вершинам, иначе по GabMin/GabMax, иначе нули. */ function kkSbInst(obj) { var ui = kkSbStack.length ? kkSbStack[kkSbStack.length - 1] : -1; var b = kkSbBox; if (b.has) { return [ui, kkNum1((b.x0 + b.x1) / 2), kkNum1((b.y0 + b.y1) / 2), kkNum1((b.z0 + b.z1) / 2)]; } var g = kkSbGab(obj); if (g) return [ui, kkNum1((g.min[0] + g.max[0]) / 2), kkNum1((g.min[1] + g.max[1]) / 2), kkNum1((g.min[2] + g.max[2]) / 2)]; return [ui, 0, 0, 0]; } /** Номер кромки в легенде (с единицы): одинаковые по толщине, ширине и материалу — один номер. * mtObj — материал кромки, если Базис отдал его объектом прямо в Butts; * host — сама панель: через её материал ищется список материалов модели. * У новой записи пробуем достать цвет (c, BGR как у DiffuseColor) и имя * картинки (tex) — ими страница схемы красит торцы. Не нашли — полей нет: * отсутствие поля и есть признак «цвет неизвестен». */ function kkSbBandNo(t, w, mat, mtObj, host, butt) { var key = t + 'x' + w + '|' + mat; if (kkSb.bandKey[key]) return kkSb.bandKey[key]; var rec = { t: t, w: w, mat: mat }; try { var mt = mtObj || null; if (!mt && mat) { var hostMt = null; try { hostMt = host ? host.Material : null; } catch (eh) { hostMt = null; } mt = kkMtByName(mat, hostMt); } /* Обозначение и свес живут у самой КРОМКИ, а не у материала ленты: «Кромка ПВХ, 1х19» и 40 — те самые колонки окна «Используемая облицовка». Берём с первой кромки, которая завела эту запись. */ if (butt) { var sg = kkGet(butt, 'Sign'); if (sg !== undefined && sg !== null && sg !== '') rec.sign = kkStr(sg); var ov = kkGet(butt, 'Overhung'); if (typeof ov === 'number' && isFinite(ov)) rec.sv = kkNum1(ov); } if (mt) { /* цвет — та же санитария, что у kkMaterialFrom: только число, системные цвета Delphi (признак в старшем байте) — не цвет */ var c = -1; try { var cv = mt.DiffuseColor; c = (typeof cv === 'number' && isFinite(cv)) ? (cv | 0) : -1; if (c !== -1 && (c & 0xFF000000) !== 0) c = -1; } catch (ec) { c = -1; } if (c !== -1) rec.c = c; /* Чем показывать ленту — цветом или фотографией — решает сама база материалов: у материала есть признак «брать цвет». Разведка на живой модели: у «Дуб седан» и «Дуб Вотан» он снят, и цвет у них стоит белой заглушкой — оттого торцы и выходили белыми, хотя фотография в базе есть. */ var cu = 1; try { var cuv = mt.ColorUse; if (cuv === false || cuv === 0) cu = 0; } catch (ec2) { cu = 1; } rec.cu = cu; /* Картинка ленты: полный путь и отправка файла — тем же способом, что у материалов деталей. Одна и та же картинка на несколько лент (или общая с ЛДСП) отправляется один раз: kkTexRegister помнит уже виденные пути. */ var found = kkTexPath(mt); if (found && found.path) { var tx = kkStr(found.path); var sl = Math.max(tx.lastIndexOf('\\'), tx.lastIndexOf('/')); rec.tex = sl >= 0 ? tx.substring(sl + 1) : tx; var safe = kkTexRegister(found.path); if (safe) { rec.texSafe = safe; /* Связываем запись с картинкой: если файл не скопируется (не нашёлся, велик, занят), kkTexCopy сам уберёт texSafe отсюда — и страница не пойдёт за ссылкой в никуда. */ kkTexLink(safe, rec); } } } } catch (e) {} kkSb.bands[kkSb.bands.length] = rec; kkSb.bandKey[key] = kkSb.bands.length; return kkSb.bands.length; } /** * Кромки панели — [Д1, Д2, Ш1, Ш2]: номер кромки в легенде или 0. Д1/Д2 — * две стороны вдоль длины L, Ш1/Ш2 — вдоль ширины W. Первая — та, что ближе * к началу осей детали (низ/лево в контуре), вторая — дальняя. * * Кромки лежат в Butts, у каждой — номер элемента контура (ElemIndex). По * самому элементу (Pos1/Pos2) видно, какая это сторона. Где длина — решаем * по самому контуру: та его сторона, что совпала с L. Если контур не * читается, кромка ложится в первую свободную клетку — кромка в легенде * лучше, чем потерянная. */ /* Вдоль какой оси контура шла длина Д у последней панели (1 — X, 0 — Y): вместе с fx/fy страница схемы по этому восстанавливает стороны кромки. */ var kkSbLastAx = 1; function kkSbButts(obj, L) { var e = [0, 0, 0, 0]; var coll = null, cnt = 0, j, b; kkSbLastAx = 1; try { coll = obj.Butts; } catch (e0) { kkOopsAdd('список кромок детали не отдался', e0); return e; } if (!coll) return e; try { cnt = coll.Count; } catch (e0) { cnt = 0; } if (!(cnt > 0)) return e; /* Рамка контура: центр и размеры. По ним — сторона каждой кромки. */ var c = null, cc = 0, k, el, p1, p2; var minx = 1e9, miny = 1e9, maxx = -1e9, maxy = -1e9, any = false; try { c = obj.Contour; } catch (e0) { c = null; } if (c) { try { cc = c.Count; } catch (e0) { cc = 0; } for (k = 0; k < cc && k < 4000; k++) { el = null; try { el = c.Objects[k]; } catch (e0) { el = null; } if (!el) { try { el = c[k]; } catch (e0) { el = null; } } if (!el) continue; try { p1 = el.Pos1; p2 = el.Pos2; if (p1 && p1.x !== undefined) { if (p1.x < minx) minx = p1.x; if (p1.x > maxx) maxx = p1.x; if (p1.y < miny) miny = p1.y; if (p1.y > maxy) maxy = p1.y; any = true; } if (p2 && p2.x !== undefined) { if (p2.x < minx) minx = p2.x; if (p2.x > maxx) maxx = p2.x; if (p2.y < miny) miny = p2.y; if (p2.y > maxy) maxy = p2.y; any = true; } } catch (e0) {} } } var cx = (minx + maxx) / 2, cy = (miny + maxy) / 2; /* Длина вдоль X контура, если ширина контура ближе к L, чем его высота. */ var alongX = any ? (Math.abs((maxx - minx) - L) <= Math.abs((maxy - miny) - L)) : true; kkSbLastAx = alongX ? 1 : 0; for (j = 0; j < cnt && j < 64; j++) { b = null; try { b = coll.Butts[j]; } catch (e0) { b = null; } if (!b) { try { b = coll.Items[j]; } catch (e0) { b = null; } } if (!b) { try { b = coll[j]; } catch (e0) { b = null; } } if (!b) continue; /* m обнуляется каждый круг: var живёт на всю функцию, и при сбое чтения Material кромке достался бы цвет предыдущей */ var mat = '', t = 0, w = 0, m = null; try { m = b.Material; if (m !== null && m !== undefined) { if (typeof m === 'object') { try { mat = kkStr(m.MaterialName || m.Name); } catch (e1) {} } else mat = kkStr(m); } } catch (e0) {} if (!mat) { try { mat = kkStr(b.MaterialName); } catch (e0) {} } try { t = kkNum1(b.Thickness); } catch (e0) { t = 0; } try { w = Math.round(kkSbNum(b.Width)); } catch (e0) { w = 0; } var bn = kkSbBandNo(t, w, mat, (m && typeof m === 'object') ? m : null, obj, b); var side = -1; if (any && c) { try { var ei = Number(b.ElemIndex); el = null; try { el = c.Objects[ei]; } catch (e1) { el = null; } if (!el) { try { el = c[ei]; } catch (e1) { el = null; } } p1 = el.Pos1; p2 = el.Pos2; var dx = Math.abs(p2.x - p1.x), dy = Math.abs(p2.y - p1.y); var mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2; var horiz = dx >= dy; var first = horiz ? (my <= cy) : (mx <= cx); if (horiz === alongX) side = first ? 0 : 1; // сторона длины else side = first ? 2 : 3; // сторона ширины } catch (e0) { side = -1; } } /* Сторону не определили — кромку в клетки НЕ кладём. Прежде она падала в первую свободную, и на детали со скруглением кромка с фигурного куска показывалась на стороне, где её нет: сервис «ставил кромку просто так». Все кромки целиком лежат в bt, а на каком куске контура каждая — в con (со скрипта 2.10). */ if (side >= 0 && e[side] === 0) e[side] = bn; } return e; } /* ==================================================== кромки: разведка и контур * * Кромка в Базисе — это не только материал. У неё есть номер элемента контура * (на какой стороне наклеена) и припуск, а у самой ленты — обозначение, * артикул, свес и, возможно, признак «криволинейная». Признак важен на * производстве: криволинейную ленту клеят на другом станке и дороже, поэтому * криволинейная кромка на ПРЯМОЙ стороне — ошибка в заказе, и её надо * показывать. Но как эти свойства зовутся в этой версии Базиса, заранее не * известно — в разобранном чужом клиенте их не читают вовсе. Поэтому здесь * они ПРОБУЮТСЯ и найденное уходит в отчёт: по нему и решим, что читать * всерьёз в следующей версии. * * Заодно пишем сам контур детали поэлементно: у боковины со скруглением или * вырезом кусков контура больше четырёх, и в четыре клетки «две по длине, две * по ширине» они не помещаются — часть кромок теряется, а дуга от прямой не * отличается никак. По контуру страница раскрасит торец точно. */ var kkSbDiag = { buttsSeen: 0, // сколько кромок Базис отдал всего buttsPlaced: 0, // сколько из них легло в четыре клетки e contPanels: 0, // у скольких панелей записан контур contLong: 0, // у скольких контур длиннее четырёх элементов contMax: 0, // самый длинный контур arcSeen: 0, // сколько дуг встретилось tyUnknown: 0, // у скольких элементов тип не отдался profButts: 0, // кромок у профилей (столешницы и погонаж) multi: [], // подробная проба панелей, где кромок больше одной contAlt: 0, // у скольких контур нашёлся не в Contour, а в Contours noContN: 0, // у скольких деталей контура нет вовсе noCont: [], // и как эти детали зовутся — по ним владелец найдёт их в Базисе /* Кромка — самое дорогое место: не показали её там, где она есть, и деталь уедет незакатанной, то есть в брак. Поэтому считаем каждую потерю отдельно и показываем прямо в окне перед отправкой. */ buttNoElem: 0, // кромок, у которых Базис не сказал, на каком куске контура buttNoCon: 0, // кромок на деталях, у которых контура нет вовсе buttLost: 0, // кромок, не легших ни на один кусок контура buttLostWho: [], // и на каких деталях probed: false, probedEl: false, probedNo: false, byMat: {}, // материал панели → сколько панелей и кромок matOrder: [] }; /* Имена, которые НИКОГДА не зовём, даже если попросят. Выгрузка обязана только читать, а в Базисе половина имён — команды: Clear() вычищает список, OrderContours() переставляет контур. Один такой вызов в разведчике 1.1 стёр контур и кромки у пяти деталей живой кухни — больше не повторяем. */ var KK_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 kkForbidden(name) { var i; for (i = 0; i < KK_NEVER.length; i++) { if (String(name).substring(0, KK_NEVER[i].length) === KK_NEVER[i]) return true; } return false; } /** Свойство или метод: в Базисе одно и то же имя бывает и тем, и другим. */ function kkGet(o, name) { var v; try { v = o[name]; } catch (e) { return undefined; } if (typeof v === 'function') { if (kkForbidden(name)) return undefined; // команда, а не ответ: не зовём try { v = o[name](); } catch (e2) { return undefined; } } return v; } /** Строка «что у объекта есть» по списку имён — для отчёта. */ function kkProbeList(o, names) { var found = [], missed = [], i, v; for (i = 0; i < names.length; i++) { v = kkGet(o, names[i]); if (v === undefined || v === null || v === '') { missed[missed.length] = names[i]; continue; } if (typeof v === 'object') { found[found.length] = names[i] + '=объект(' + (kkClassOf(v) || '?') + ')'; continue; } found[found.length] = names[i] + '=' + kkStr(v); } return { found: found, missed: missed }; } var KK_BUTT_PROPS = ['Material', 'MaterialName', 'Name', 'Designation', 'Code', 'Article', 'Art', 'ElemIndex', 'Index', 'Side', 'Length', 'Thickness', 'Width', 'Allowance', 'Overhang', 'Sves', 'Ledge', 'Offset', 'Trim', 'Cut', 'CutPanel', 'TrimPanel', 'Curved', 'IsCurved', 'Curvilinear', 'Curve', 'IsCurve', 'BendType', 'ButtType', 'Kind', 'Type', 'Tape', 'TapeMode', 'Band', 'IsTape', 'Radius', 'Visible']; var KK_MAT_PROPS = ['MaterialName', 'Name', 'Designation', 'Code', 'Article', 'Art', 'Thickness', 'Width', 'Overhang', 'Sves', 'Curved', 'IsCurved', 'Curvilinear', 'BandType', 'DiffuseColor', 'Path']; var KK_EL_PROPS = ['IsLine', 'IsArc', 'IsCircle', 'ElType', 'ObjType', 'ArcDir', 'Direction', 'CirRadius', 'ArcRadius', 'Radius', 'ObjLength', 'Length', 'Bulge', 'Center', 'Pos1', 'Pos2']; /** Первая встреченная кромка и её материал — что есть, а чего нет. */ function kkProbeButt(b, mt) { var r = kkProbeList(b, KK_BUTT_PROPS); kkNote(' кромка (первая встреченная): ' + (r.found.length ? r.found.join(', ') : 'ни одного свойства не отдалось')); kkNote(' класс: ' + (kkClassOf(b) || '(не отдался)') + '; НЕ отдали: ' + r.missed.join(', ')); if (mt) { var rm = kkProbeList(mt, KK_MAT_PROPS); kkNote(' материал кромки: ' + rm.found.join(', ')); kkNote(' класс: ' + (kkClassOf(mt) || '(не отдался)') + '; НЕ отдали: ' + rm.missed.join(', ')); /* Артикул Базис держит второй строкой имени — показываем строки раздельно */ var raw = ''; try { raw = String(mt.MaterialName || ''); } catch (e) { raw = ''; } if (raw.search(/[\r\n]/) >= 0) kkNote(' имя материала по строкам: ' + kkStr(raw.replace(/[\r\n]+/g, ' / '))); } else { kkNote(' материал кромки: объектом не отдался (кромка вернула только имя строкой)'); } } /** Первый встреченный элемент контура: по нему видно, отдаёт ли Базис дуги. */ function kkProbeContEl(el, c, obj) { var r = kkProbeList(el, KK_EL_PROPS); kkNote(' элемент контура (первый): ' + (r.found.length ? r.found.join(', ') : 'ни одного свойства не отдалось')); kkNote(' класс: ' + (kkClassOf(el) || '(не отдался)') + '; НЕ отдали: ' + r.missed.join(', ')); var s = ''; var oc = null; try { oc = c.OrderContours; } catch (e) { oc = null; } s += 'OrderContours=' + (oc === undefined || oc === null ? 'нет' : (typeof oc === 'function' ? 'метод' : 'свойство')); var vb = null; try { vb = obj.IsButtVisible; } catch (e) { vb = null; } s += ', IsButtVisible=' + (vb === undefined || vb === null ? 'нет' : (typeof vb === 'function' ? 'метод' : 'свойство')); kkNote(' у контура и панели: ' + s); } /** * Кромки как их отдал Базис, без раскладки по четырём клеткам: * {list: [[номер в легенде, номер элемента контура, припуск], …], map, n}. * По длине list видно, сколько кромок потерялось при раскладке. */ function kkSbButtsRaw(obj, nameForDiag) { var out = { list: [], map: {}, n: 0 }; var coll = null, cnt = 0, j, b; try { coll = obj.Butts; } catch (e0) { kkOopsAdd('список кромок детали не отдался', e0); return out; } if (!coll) return out; try { cnt = coll.Count; } catch (e0) { kkOopsAdd('число кромок у детали не отдалось', e0); cnt = 0; } if (!(cnt > 0)) return out; out.n = cnt; var det = []; for (j = 0; j < cnt && j < 64; j++) { b = null; try { b = coll.Butts[j]; } catch (e0) { b = null; } if (!b) { try { b = coll.Items[j]; } catch (e0) { b = null; } } if (!b) { try { b = coll[j]; } catch (e0) { b = null; } } if (!b) { kkOopsAdd('кромка не достаётся из списка ни одним путём', null); continue; } var mat = '', t = 0, w = 0, m = null, ei = -1, al = -1; try { m = b.Material; if (m !== null && m !== undefined) { if (typeof m === 'object') { try { mat = kkStr(m.MaterialName || m.Name); } catch (e1) {} } else mat = kkStr(m); } } catch (e0) {} if (!mat) { try { mat = kkStr(b.MaterialName); } catch (e0) {} } try { t = kkNum1(b.Thickness); } catch (e0) { t = 0; } try { w = Math.round(kkSbNum(b.Width)); } catch (e0) { w = 0; } var mtObj = (m && typeof m === 'object') ? m : null; var bn = kkSbBandNo(t, w, mat, mtObj, obj, b); if (!kkSbDiag.probed) { kkSbDiag.probed = true; var mtProbe = mtObj; if (!mtProbe && mat) { try { mtProbe = kkMtByName(mat, obj.Material); } catch (e1) { mtProbe = null; } } kkProbeButt(b, mtProbe); } var eiv = kkGet(b, 'ElemIndex'); ei = (typeof eiv === 'number' && isFinite(eiv)) ? (eiv | 0) : -1; var alv = kkGet(b, 'Allowance'); al = (typeof alv === 'number' && isFinite(alv)) ? kkNum1(alv) : -1; /* Номер отреза. В Базисе кромку наносят двумя способами: «каждую отдельно» — прямолинейная закатка, и «один отрез на несколько кромок» — когда выделяют подряд прямые и дуги и клеят одной лентой; это и есть криволинейная работа, другой станок и другая цена. Кромки одного отреза несут один и тот же номер. */ var cuv = kkGet(b, 'CutIndex'); var cut = (typeof cuv === 'number' && isFinite(cuv)) ? (cuv | 0) : -1; /* Подрезка панели под кромку (распорочный размер) — галочка «подрезать» в окне облицовки. */ var clv = kkGet(b, 'ClipPanel'); var clip = (clv === true) ? 1 : (clv === false ? 0 : -1); out.list[out.list.length] = [bn, ei, al, cut, clip]; if (ei < 0) kkSbDiag.buttNoElem++; if (ei >= 0 && out.map['e' + ei] === undefined) out.map['e' + ei] = bn; kkSbDiag.buttsSeen++; det[det.length] = 'j' + j + ': эл=' + ei + ', кромка№' + bn + ', припуск=' + al + ', тип Material=' + (m === null || m === undefined ? 'нет' : typeof m) + ', имя=' + (mat || '(пусто)') + ', толщ=' + t + ', шир=' + w; } /* Решающая проба: если у одной детали кромок несколько, а номер элемента контура у всех один — значит перебор отдаёт одну и ту же кромку. */ if (out.list.length > 1 && kkSbDiag.multi.length < 5) { kkSbDiag.multi[kkSbDiag.multi.length] = { name: nameForDiag || '', n: cnt, det: det }; } return out; } /** * Запасной путь к форме детали: Contours. У панели Базиса, кроме Contour * (список кусков), есть и Contours — список контуров. Что там лежит на * самом деле, в этой версии заранее не известно, поэтому разбираем оба * случая: список списков (берём самый длинный — наружный контур длиннее * вырезов) и список самих кусков. Только чтение. */ function kkSbContourAlt(obj) { var L = null, n = 0, i, one, m; try { L = obj.Contours; } catch (e) { return null; } if (!L) return null; try { n = L.Count; } catch (e) { n = 0; } if (!(n > 0)) return null; var best = null, bn = 0; for (i = 0; i < n && i < 20; i++) { one = null; try { one = L.Objects[i]; } catch (e) { one = null; } if (!one) { try { one = L.Items[i]; } catch (e) { one = null; } } if (!one) { try { one = L[i]; } catch (e) { one = null; } } if (!one) continue; m = 0; try { m = one.Count; } catch (e) { m = 0; } if (typeof m === 'number' && isFinite(m) && m > bn) { bn = m; best = one; } } if (best && bn > 0) return { c: best, n: bn }; /* Внутри не списки, а сами куски: тогда Contours и есть список кусков — узнаём по тому, что у первого есть конец отрезка. */ one = null; try { one = L.Objects[0]; } catch (e) { one = null; } if (!one) { try { one = L[0]; } catch (e) { one = null; } } if (one) { var p = null; try { p = one.Pos1; } catch (e) { p = null; } if (p && p.x !== undefined) return { c: L, n: n }; } return null; } /* Детали, у которых Базис не отдал контур: их торец на странице остаётся серым, и владельцу надо знать, какие это детали — по обозначению он найдёт их в Базисе. У первой такой снимаем подробную пробу: что именно Базис про неё говорит. */ function kkSbNoContour(obj) { var nm = '', dz = '', bp = ''; try { nm = kkStr(obj.Name); } catch (e) {} try { dz = kkStr(obj.Designation); } catch (e) {} try { bp = kkStr(obj.ArtPos); } catch (e) {} if (kkSbDiag.noCont.length < 40) { kkSbDiag.noCont[kkSbDiag.noCont.length] = (nm || '(без имени)') + (dz ? ' (' + dz + ')' : ''); } kkSbDiag.noContN++; if (kkSbDiag.probedNo) return; kkSbDiag.probedNo = true; /* Читаем ТОЛЬКО свойства. kkGet зовёт метод, если имя оказалось методом, — здесь так нельзя: имена берутся наугад, а в Базисе есть методы, которые молча правят модель. Метод просто помечаем. */ var say = function (name) { var v; try { v = obj[name]; } catch (e) { return name + '=не отдалось'; } if (typeof v === 'function') return name + '=метод (не звали)'; if (v === undefined) return name + '=нет'; if (v === null) return name + '=null'; if (typeof v === 'object') { var c = 0; try { c = v.Count; } catch (e) { c = -1; } return name + '={' + (kkClassOf(v) || 'объект') + (typeof c === 'number' && c >= 0 ? ' Count=' + c : '') + '}'; } return name + '=' + kkStr(v); }; kkNote(' деталь без контура (первая): «' + (nm || '(без имени)') + '», обозначение ' + (dz || '—') + ', позиция ' + (bp || '—')); kkNote(' ' + say('Contour') + ', ' + say('Contours') + ', ' + say('ContourWidth') + ', ' + say('ContourHeight')); kkNote(' ' + say('Butts') + ', ' + say('Cuts') + ', ' + say('Plastics') + ', ' + say('Holes') + ', ' + say('TriListsCount')); kkNote(' ' + say('Bent') + ', ' + say('BentContour') + ', ' + say('BentDirection') + ', ' + say('Kind') + ', ' + say('Source') + ', ' + say('FrontFace')); } /* Взят ли контур последней панели запасным путём (из Contours). Нужен там, где Базис уже ответил про форму по пустому списку Contour: его ответ тогда относится к пустоте, а не к детали. */ var kkSbLastAlt = 0; /** * Контур детали поэлементно, в её собственных осях (тех же, что fx/fy): * [[тип, x1,y1, x2,y2, cx,cy, направление, кромка, виден, длина], …]. * Тип: 0 отрезок, 1 дуга, 2 окружность, −1 не отдался. */ function kkSbContour(obj, map) { var c = null, cc = 0, k, el; kkSbLastAlt = 0; try { c = obj.Contour; } catch (e0) { kkOopsAdd('контур детали не отдался', e0); return null; } if (!c) { kkOopsAdd('контура у детали нет вовсе', null); return null; } /* OrderContours звать НЕЛЬЗЯ. В руководстве Базиса это команда «выстроить элементы контура в одном направлении» — она МЕНЯЕТ модель, а выгрузка обязана только читать. Вдобавок номера кромок (ElemIndex) прочитаны до неё, а куски здесь нумеруются заново: после перестановки кромка села бы на чужой кусок. Порядок нам и не нужен — у каждого куска записаны свои концы. */ try { cc = c.Count; } catch (e0) { cc = 0; } /* Пустой список — ещё не приговор. У панели есть и второй путь к форме: Contours (список контуров). Пробуем его, прежде чем оставить деталь без контура: без контура торец на странице показывается серым, а это заметно хуже, чем показать его правильно. */ if (!(cc > 0)) { var alt = kkSbContourAlt(obj); if (alt) { c = alt.c; cc = alt.n; kkSbLastAlt = 1; kkSbDiag.contAlt++; } } if (!(cc > 0)) { kkSbNoContour(obj); return null; } if (cc > 200) return null; // тяжёлые детали пропускаем: время дороже /* Номер элемента у кромки (ElemIndex) считан по списку Contour. Если куски взяты из Contours, номера могут указывать не туда — и кромка села бы на чужой кусок. Раскладываем их только когда все номера в этот список помещаются; иначе кромки остаются в bt, а по кускам не разносятся. */ if (kkSbLastAlt && map) { var kk, num, fits = true; for (kk in map) { if (!map.hasOwnProperty(kk)) continue; num = parseInt(kk.substring(1), 10); if (!(num >= 0 && num < cc)) { fits = false; break; } } if (!fits) map = null; } var out = []; for (k = 0; k < cc; k++) { el = null; try { el = c.Objects[k]; } catch (e0) { el = null; } if (!el) { try { el = c[k]; } catch (e0) { el = null; } } if (!el) continue; if (!kkSbDiag.probedEl) { kkSbDiag.probedEl = true; kkProbeContEl(el, c, obj); } var ty = -1, v; v = kkGet(el, 'IsCircle'); if (v === true) ty = 2; if (ty < 0) { v = kkGet(el, 'IsArc'); if (v === true) ty = 1; else if (v === false) ty = 0; } if (ty < 0) { v = kkGet(el, 'IsLine'); if (v === true) ty = 0; } if (ty < 0) { v = kkGet(el, 'ElType'); if (typeof v === 'number' && isFinite(v)) ty = (v | 0) === 0 ? 0 : 1; } if (ty < 0) kkSbDiag.tyUnknown++; if (ty === 1 || ty === 2) kkSbDiag.arcSeen++; var x1 = 0, y1 = 0, x2 = 0, y2 = 0, cx = 0, cy = 0, dir = 0, ln = 0; try { var p1 = el.Pos1; if (p1) { x1 = kkNum1(p1.x); y1 = kkNum1(p1.y); } } catch (e0) {} try { var p2 = el.Pos2; if (p2) { x2 = kkNum1(p2.x); y2 = kkNum1(p2.y); } } catch (e0) {} if (ty === 1 || ty === 2) { try { var ct = el.Center; if (ct) { cx = kkNum1(ct.x); cy = kkNum1(ct.y); } } catch (e0) {} v = kkGet(el, 'ArcDir'); if (v === undefined) v = kkGet(el, 'Direction'); dir = (v === true || v === 1) ? 1 : 0; } v = kkGet(el, 'ObjLength'); if (typeof v === 'number' && isFinite(v)) ln = kkNum1(v); /* Виден ли торец снаружи: у стыкованных деталей кромку не клеят */ var vis = -1; try { if (typeof obj.IsButtVisible === 'function') vis = obj.IsButtVisible(k, 0.2) ? 1 : 0; } catch (e0) { vis = -1; } var bn = map ? map['e' + k] : undefined; out[out.length] = [ty, x1, y1, x2, y2, cx, cy, dir, (bn === undefined ? 0 : bn), vis, ln]; } if (!out.length) return null; kkSbDiag.contPanels++; if (out.length > 4) kkSbDiag.contLong++; if (out.length > kkSbDiag.contMax) kkSbDiag.contMax = out.length; return out; } /** Материал панели в список материалов; возвращает индекс. art — вторая строка имени материала, там Базис держит артикул. */ function kkSbMatIndex(obj, T, L, W) { var name = '', raw = '', art = ''; try { name = kkStr(obj.MaterialName); } catch (e) {} try { raw = String(obj.Material.MaterialName || ''); } catch (e) { raw = ''; } if (!name) name = kkStr(raw); if (!name) name = 'Без имени'; var cut = raw.search(/[\r\n]/); if (cut >= 0) art = kkStr(raw.substring(cut + 1)).replace(/^\s+|\s+$/g, ''); var key = name + '|' + T; var mi = kkSb.matKey[key]; if (mi === undefined) { kkSb.mats[kkSb.mats.length] = { name: name, art: art, thick: T, n: 0, mm2: 0 }; mi = kkSb.mats.length - 1; kkSb.matKey[key] = mi; } kkSb.mats[mi].n++; kkSb.mats[mi].mm2 += L * W; return mi; } /** Панель → позиция (k 'p'). g — то, что вернул kkGrain: size = [L, W, T] из GSize. */ function kkSbPanel(obj, name, g) { var L = 0, W = 0, T = 0; if (g && g.size) { L = g.size[0]; W = g.size[1]; T = g.size[2]; } else if (g && g.lw) { L = g.lw[0]; W = g.lw[1]; } if (!T) { try { T = kkNum1(obj.Thickness); } catch (e) { T = 0; } } /* Погонный материал (плинтус, профиль ручки) Базис тоже держит панелью, но с шириной материала; в спецификации это профиль, а не деталь. */ var mw = 0; try { mw = kkSbNum(obj.MaterialWidth); } catch (e) { mw = 0; } if (mw > 0) { var pm = ''; try { pm = kkStr(obj.MaterialName); } catch (e) {} /* Кромки читаем ДО ухода в профиль: столешница едет сюда (у неё задана ширина материала), а кромка у неё есть — прежде она терялась совсем, потому что до чтения дело не доходило. */ var praw = kkSbButtsRaw(obj, name); if (praw.list.length) kkSbDiag.profButts += praw.list.length; kkSbProfile(obj, name, pm, Math.max(L, W), praw.list.length ? praw.list : null); return; } var bp = '', dz = ''; try { bp = kkStr(obj.ArtPos); } catch (e) {} try { dz = kkStr(obj.Designation); } catch (e) {} var mi = kkSbMatIndex(obj, T, L, W); var cur = 0; try { cur = obj.Contour.IsContourRectangle() ? 0 : 1; } catch (e) { cur = 0; } var e4 = kkSbButts(obj, L); /* Всё новое собираем ПОСЛЕ e4 и в стороне от него: если новая ветка упадёт, старые поля останутся целы. */ var raw = null, con = null; try { raw = kkSbButtsRaw(obj, name); } catch (e) { kkOopsAdd('чтение кромок детали прервалось', e); raw = null; } try { con = kkSbContour(obj, raw ? raw.map : null); } catch (e) { kkOopsAdd('чтение контура детали прервалось', e); con = null; } /* Контур пришёл запасным путём — значит на IsContourRectangle Базис отвечал по пустому списку и сказал «не прямоугольник» про пустоту. Куски у нас на руках: четыре отрезка — прямоугольная деталь. */ if (kkSbLastAlt && con && con.length === 4) { var straight = 1, s; for (s = 0; s < 4; s++) { if (con[s][0] !== 0) { straight = 0; break; } } if (straight) cur = 0; } /* Сверка по кромкам: сколько Базис отдал и сколько легло на куски контура. Разошлось — значит на странице торец покажется голым, а он не голый. Это прямая дорога к браку, поэтому считаем поимённо. */ try { if (raw && raw.list.length) { if (!con || !con.length) { kkSbDiag.buttNoCon += raw.list.length; if (kkSbDiag.buttLostWho.length < 30) { kkSbDiag.buttLostWho[kkSbDiag.buttLostWho.length] = (name || '(без имени)') + (dz ? ' (' + dz + ')' : '') + ' — кромок ' + raw.list.length + ', контура нет'; } } else { var onCon = 0, r2; for (r2 = 0; r2 < con.length; r2++) { if (con[r2][8]) onCon++; } if (onCon < raw.list.length) { kkSbDiag.buttLost += (raw.list.length - onCon); if (kkSbDiag.buttLostWho.length < 30) { kkSbDiag.buttLostWho[kkSbDiag.buttLostWho.length] = (name || '(без имени)') + (dz ? ' (' + dz + ')' : '') + ' — кромок ' + raw.list.length + ', легло ' + onCon; } } } } } catch (e) { kkOopsAdd('сверка кромок с контуром сорвалась', e); } try { var placed = 0, q; for (q = 0; q < 4; q++) { if (e4[q]) placed++; } kkSbDiag.buttsPlaced += placed; var mn = kkSb.mats[mi] ? kkSb.mats[mi].name : '?'; if (!kkSbDiag.byMat[mn]) { kkSbDiag.byMat[mn] = { n: 0, wb: 0, b: 0 }; kkSbDiag.matOrder[kkSbDiag.matOrder.length] = mn; } kkSbDiag.byMat[mn].n++; if (raw && raw.list.length) { kkSbDiag.byMat[mn].wb++; kkSbDiag.byMat[mn].b += raw.list.length; } } catch (e) {} var key = 'p\x01' + name + '\x01' + L + '\x01' + W + '\x01' + T + '\x01' + mi + '\x01' + e4.join(',') + '\x01' + cur + '\x01' + bp; var pi = kkSb.posKey[key]; if (pi === undefined) { kkSb.pos[kkSb.pos.length] = { k: 'p', bp: bp, dz: dz, fz: kkSbFz(obj), name: name, fx: kkSbDir(obj, 1, 0, 0), fy: kkSbDir(obj, 0, 1, 0), ax: kkSbLastAx, L: L, W: W, T: T, mi: mi, e: e4, cur: cur, con: con, bt: (raw && raw.list.length) ? raw.list : null, inst: [] }; pi = kkSb.pos.length - 1; kkSb.posKey[key] = pi; } var rec = kkSb.pos[pi]; rec.inst[rec.inst.length] = kkSbInst(obj); } /** Профиль → позиция (k 'r'): имя, материал, длина; bt — кромки, если есть (столешница). */ function kkSbProfile(obj, name, pm, L, bt) { L = Math.round(kkSbNum(L)); if (!(L > 0)) return; if (!name) name = 'Профиль'; var bp = '', dz = ''; try { bp = kkStr(obj.ArtPos); } catch (e) {} try { dz = kkStr(obj.Designation); } catch (e) {} var key = 'r\x01' + name + '\x01' + pm + '\x01' + L + '\x01' + bp; var pi = kkSb.posKey[key]; if (pi === undefined) { kkSb.pos[kkSb.pos.length] = { k: 'r', bp: bp, dz: dz, name: name, pm: pm, L: L, bt: bt || null, inst: [] }; pi = kkSb.pos.length - 1; kkSb.posKey[key] = pi; } var rec = kkSb.pos[pi]; rec.inst[rec.inst.length] = kkSbInst(obj); } /** * Отверстия крепежа в его собственных осях: [{r, d, p:[x,y,z], v:[x,y,z]}]. * Базис держит присадку на крепеже: радиус, глубина, устье (Position) и * направление (Direction) сверления. Запасные пути к элементу — Items[i] и * Holes[i]: как зовётся коллекция в этой версии, заранее не известно. */ function kkSbReadHoles(obj) { var out = [], hc = null, n = 0, i, h, r, d, p, v; try { hc = obj.Holes; } catch (e) { return out; } if (!hc) return out; try { n = hc.Count; } catch (e) { n = 0; } if (!(n > 0)) return out; for (i = 0; i < n && i < 4000; i++) { h = null; try { h = hc[i]; } catch (e) { h = null; } if (!h) { try { h = hc.Items[i]; } catch (e) { h = null; } } if (!h) { try { h = hc.Holes[i]; } catch (e) { h = null; } } if (!h) continue; r = 0; d = 0; p = null; v = null; /* Диаметр Базис держит круглым (2,5 / 4,5 / 8 / 15 / 35), а радиус — половинчатым (1,25 / 2,25). От округления радиуса до 0,1 мм на странице выходило ⌀2,6 вместо 2,5, поэтому берём диаметр, а радиус оставляем запасным путём. */ try { r = Number(h.Diameter) / 2; } catch (e) { r = 0; } if (!(r > 0)) { try { r = Number(h.Radius); } catch (e) { r = 0; } } try { d = Number(h.Depth); } catch (e) { d = 0; } try { p = kkSbVec(h.Position); } catch (e) { p = null; } try { v = kkSbVec(h.Direction); } catch (e) { v = null; } if (!(isFinite(r) && isFinite(d) && r > 0 && d > 0 && p && v)) continue; out[out.length] = { r: r, d: d, p: p, v: v }; } return out; } /** Отверстия → в общий список, в координатах комнаты, до 0.1 мм, без повторов. */ function kkSbHolesAdd(obj, list) { if (!list || !list.length) return; var i, h, g1, g2, rec, key; for (i = 0; i < list.length; i++) { if (kkSb.holes.length >= KK_SB_HOLES_MAX) return; h = list[i]; try { g1 = obj.ToGlobal(NewVector(h.p[0], h.p[1], h.p[2])); g2 = obj.ToGlobal(NewVector(h.p[0] + h.v[0] * h.d, h.p[1] + h.v[1] * h.d, h.p[2] + h.v[2] * h.d)); if (!g1 || !g2 || g1.x === undefined || g2.x === undefined) continue; rec = [kkNum1(g1.x), kkNum1(g1.y), kkNum1(g1.z), kkNum1(g2.x), kkNum1(g2.y), kkNum1(g2.z), kkNum2(h.r)]; } catch (e) { continue; } key = 'h' + rec.join(','); if (kkSb.holeKey[key]) continue; kkSb.holeKey[key] = 1; kkSb.holes[kkSb.holes.length] = rec; } } /** * Цилиндр на каждое отверстие крепежа — геометрия в общий поток (kkTri), в * координатах комнаты через ToGlobal (кадр объекта, как у kkWriteBody). * Ось — от устья вдоль направления сверления на глубину, радиус — радиус * отверстия, с торцами. Возвращает число записанных треугольников. */ function kkSbHoleCylinders(obj, list) { var m = null, n = 0, i, h; try { m = kkFrame(obj); } catch (e) { return 0; } var W = function (x, y, z) { return [m.ox + x * m.xx + y * m.yx + z * m.zx, m.oy + x * m.xy + y * m.yy + z * m.zy, m.oz + x * m.xz + y * m.yz + z * m.zz]; }; var T = function (a, b, c) { kkTri(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]); n++; }; for (i = 0; i < list.length; i++) { h = list[i]; var ax = h.v[0], ay = h.v[1], az = h.v[2]; var len = Math.sqrt(ax * ax + ay * ay + az * az); if (!(len > 1e-9)) continue; ax /= len; ay /= len; az /= len; /* Две оси поперёк сверла: u и w, правая тройка (u, w, a). */ var hx = 1, hy = 0, hz = 0; if (Math.abs(ax) > 0.9) { hx = 0; hy = 1; } var ux = hy * az - hz * ay, uy = hz * ax - hx * az, uz = hx * ay - hy * ax; var ul = Math.sqrt(ux * ux + uy * uy + uz * uz); if (!(ul > 1e-9)) continue; ux /= ul; uy /= ul; uz /= ul; var wx = ay * uz - az * uy, wy = az * ux - ax * uz, wz = ax * uy - ay * ux; var px = h.p[0], py = h.p[1], pz = h.p[2]; var qx = px + ax * h.d, qy = py + ay * h.d, qz = pz + az * h.d; var r = h.r, k, a1 = [], a2 = [], c1 = W(px, py, pz), c2 = W(qx, qy, qz); for (k = 0; k < KK_SB_SEG; k++) { var an = 2 * Math.PI * k / KK_SB_SEG, cs = Math.cos(an), sn = Math.sin(an); var ox = r * (ux * cs + wx * sn), oy = r * (uy * cs + wy * sn), oz = r * (uz * cs + wz * sn); a1[k] = W(px + ox, py + oy, pz + oz); a2[k] = W(qx + ox, qy + oy, qz + oz); } for (k = 0; k < KK_SB_SEG; k++) { var k2 = (k + 1) % KK_SB_SEG; T(a1[k], a1[k2], a2[k2]); // боковина, наружу T(a1[k], a2[k2], a2[k]); T(c2, a2[k], a2[k2]); // дно — наружу вдоль сверла T(c1, a1[k2], a1[k]); // устье — наружу против сверла } } return n; } /** Крепёж: в сводный список и в единицы, которым принадлежит; отверстия — в общий список. */ function kkSbFastener(obj, name, hasGeom, holes) { kkSbHolesAdd(obj, holes); var dm = 0; try { dm = Number(obj.DatumMode); } catch (e) { dm = 0; } /* Код 6 у чужого экспорта тоже не считается: смысл его Базис не раскрыл, но в смете такой крепёж не учитывают. */ if (dm === 6) return; /* Без тела и с одним отверстием или без них — это «пустая» метка, а не крепёж. С двумя и больше отверстиями — присадка: считается. */ if (!hasGeom && !(holes && holes.length > 1)) return; if (!name) name = 'Крепёж'; var art = ''; try { art = kkStr(obj.Designation); } catch (e) {} var key = name + '\x01' + art; if (!kkSb.fastAll[key]) { kkSb.fastAll[key] = 0; kkSb.fastOrder[kkSb.fastOrder.length] = key; } kkSb.fastAll[key]++; kkSb.nFast++; var act = kkSbActive(), i, u; for (i = 0; i < act.length; i++) { u = kkSb.units[act[i]]; if (!u.fast[key]) { u.fast[key] = 0; u.fastOrder[u.fastOrder.length] = key; } u.fast[key]++; } } /** * Деталь после записи её геометрии: что из неё идёт в сборку. name — как в * оглавлении; g — kkGrain; hasGeom — есть ли у объекта своё тело (без учёта * цилиндров по отверстиям); holes — прочитанные отверстия крепежа (или null). */ function kkSbPart(obj, kind, name, g, hasGeom, holes) { if (kkSbBox.has) kkSbGrowUnits(kkSbBox); if (kind === 'panel') { kkSbPanel(obj, name, g); return; } if (kind === 'fastener') { if (name === 'fastener') name = ''; kkSbFastener(obj, name, hasGeom, holes); return; } if (kind === 'profile') { var pm = '', L = 0; try { pm = kkStr(obj.MaterialName); } catch (e) {} if (!pm) { try { pm = kkStr(obj.Material.MaterialName); } catch (e) {} } /* Длина выдавливания у профиля лежит в Thickness; нет её — длинная сторона габарита. */ try { L = Math.abs(kkSbNum(obj.Thickness)); } catch (e) { L = 0; } if (!(L > 0)) { try { var gs = obj.GSize; if (gs && gs.x !== undefined) L = Math.max(kkSbNum(gs.x), kkSbNum(gs.y), kkSbNum(gs.z)); } catch (e) { L = 0; } } kkSbProfile(obj, (name === 'profile') ? '' : name, pm, L); } } /** Сводка крепежа {ключ→штук} в список [{name, art, n}], по имени. */ function kkSbFastList(dict, order) { var out = [], i, key, cut; for (i = 0; i < order.length; i++) { key = order[i]; cut = key.indexOf('\x01'); out[out.length] = { name: key.substring(0, cut), art: key.substring(cut + 1), n: dict[key] }; } out.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); }); return out; } /** * Номера позиций. Сначала плитные материалы (толщина от 10), потом тонкие; * внутри — ходовые раньше редких, потом по имени. Внутри материала детали * крупные раньше мелких. Профили — в конце, по материалу и длине. Так * нумеруют спецификацию и другие, и человеку она привычна. */ function kkSbNumber() { var mats = kkSb.mats, pos = kkSb.pos, mo = [], i, j, p = 0; for (i = 0; i < mats.length; i++) mo[mo.length] = i; mo.sort(function (a, b) { var A = mats[a], B = mats[b]; var pa = (A.thick >= 10) ? 0 : 1, pb = (B.thick >= 10) ? 0 : 1; if (pa !== pb) return pa - pb; if (A.n !== B.n) return B.n - A.n; return A.name < B.name ? -1 : (A.name > B.name ? 1 : 0); }); for (i = 0; i < mo.length; i++) { var list = []; for (j = 0; j < pos.length; j++) { if (pos[j].k === 'p' && pos[j].mi === mo[i]) list[list.length] = pos[j]; } list.sort(function (a, b) { var ma = Math.max(a.L, a.W), mb = Math.max(b.L, b.W); if (ma !== mb) return mb - ma; if (a.W !== b.W) return b.W - a.W; return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); }); for (j = 0; j < list.length; j++) list[j].p = ++p; } var prof = []; for (j = 0; j < pos.length; j++) { if (pos[j].k === 'r') prof[prof.length] = pos[j]; } prof.sort(function (a, b) { if (a.pm !== b.pm) return a.pm < b.pm ? -1 : 1; return b.L - a.L; }); for (j = 0; j < prof.length; j++) prof[j].p = ++p; /* Панель без материала в списке (не должно быть) — всё равно с номером. */ for (j = 0; j < pos.length; j++) { if (pos[j].p === undefined) pos[j].p = ++p; } } /** Собирает всё в объект для sborka.json. */ function kkSbBuild(title, order) { var i, j, u, out = { v: 1, cv: 'kk3d ' + KK_VER, product: { name: title, order: order || '' } }; var units = []; for (i = 0; i < kkSb.units.length; i++) { u = kkSb.units[i]; var b = u.box, gmin = null, gmax = null, g = [0, 0, 0]; if (b) { gmin = [kkNum1(b.x0), kkNum1(b.y0), kkNum1(b.z0)]; gmax = [kkNum1(b.x1), kkNum1(b.y1), kkNum1(b.z1)]; } else if (u.gab) { gmin = [kkNum1(u.gab.min[0]), kkNum1(u.gab.min[1]), kkNum1(u.gab.min[2])]; gmax = [kkNum1(u.gab.max[0]), kkNum1(u.gab.max[1]), kkNum1(u.gab.max[2])]; } if (gmin) g = [kkNum1(gmax[0] - gmin[0]), kkNum1(gmax[1] - gmin[1]), kkNum1(gmax[2] - gmin[2])]; u.g = g; u.gmin = gmin; u.gmax = gmax; } for (i = 0; i < kkSb.units.length; i++) { u = kkSb.units[i]; var kids = []; for (j = 0; j < u.kids.length; j++) { var kid = kkSb.units[u.kids[j]]; kids[kids.length] = { pos: kid.pos, dz: kid.dz, name: kid.name, n: 1, g: kid.g }; } units[units.length] = { top: u.top, nested: u.nested, au: u.au, name: u.name, pos: u.pos, dz: u.dz, n: 1, g: u.g, gmin: u.gmin, gmax: u.gmax, note: u.note, draw: u.draw, fast: kkSbFastList(u.fast, u.fastOrder), kids: kids }; } out.units = units; out.bands = kkSb.bands; var mats = []; for (i = 0; i < kkSb.mats.length; i++) { var m = kkSb.mats[i]; mats[mats.length] = { name: m.name, art: m.art, thick: m.thick, n: m.n, mm2: Math.round(m.mm2) }; } out.mats = mats; kkSbNumber(); var pos = []; for (i = 0; i < kkSb.pos.length; i++) { var r = kkSb.pos[i]; if (r.k === 'p') { var rec = { p: r.p, k: 'p', bp: r.bp, dz: r.dz, fz: r.fz, fx: r.fx, fy: r.fy, ax: r.ax, name: r.name, L: r.L, W: r.W, T: r.T, mi: r.mi, e: r.e, cur: r.cur, inst: r.inst }; /* Контур и сырые кромки пишем, только если они есть: страницы старых выгрузок читают их через проверку, а лишние пустые поля зря утяжеляют файл. */ if (r.con && r.con.length) rec.con = r.con; if (r.bt && r.bt.length) rec.bt = r.bt; pos[pos.length] = rec; } else { var rr = { p: r.p, k: 'r', bp: r.bp, dz: r.dz, name: r.name, pm: r.pm, L: r.L, inst: r.inst }; if (r.bt && r.bt.length) rr.bt = r.bt; pos[pos.length] = rr; } } out.pos = pos; out.fast = kkSbFastList(kkSb.fastAll, kkSb.fastOrder); out.holes = kkSb.holes; return out; } /** Строка для окна и отчёта: что собрано по сборке. */ function kkSbSummaryLine() { if (kkSb.err) return 'Сборка: пропущено (' + kkSb.err + ')'; var i, nTop = 0, nAu = 0, nNested = 0, nBandC = 0; for (i = 0; i < kkSb.units.length; i++) { if (kkSb.units[i].nested) { nNested++; continue; } nTop++; if (kkSb.units[i].au) nAu++; } for (i = 0; i < kkSb.bands.length; i++) { if (kkSb.bands[i].c !== undefined || kkSb.bands[i].tex !== undefined) nBandC++; } return 'Сборка: модулей ' + nTop + ' (с галочкой «сборочная единица» ' + nAu + ')' + (nNested ? ', вложенных единиц ' + nNested : '') + ', позиций ' + kkSb.pos.length + ', кромок ' + kkSb.bands.length + (kkSb.bands.length ? ' (с цветом ' + nBandC + ')' : '') + ', наклеено кромок ' + kkSbDiag.buttsSeen + ((kkSbDiag.buttsSeen - kkSbDiag.profButts) > kkSbDiag.buttsPlaced ? ' (в четыре стороны легло ' + kkSbDiag.buttsPlaced + ')' : '') + ', контуров ' + kkSbDiag.contPanels + (kkSbDiag.arcSeen ? ' (дуг ' + kkSbDiag.arcSeen + ')' : '') + (kkSbDiag.contAlt ? ' (из Contours ' + kkSbDiag.contAlt + ')' : '') + (kkSbDiag.noContN ? ', БЕЗ КОНТУРА ' + kkSbDiag.noContN : '') + ', отверстий ' + kkSb.holes.length + ', крепежа ' + kkSb.nFast + ' шт.'; } /* Какие именно детали Базис отдал без контура — строками для окна и отчёта. У них на странице торец остаётся серым, и владельцу надо знать, где искать их в Базисе: по имени и обозначению. */ function kkSbNoContourLines() { if (!kkSbDiag.noContN) return []; var n = kkSbDiag.noCont.length; return [ ' БЕЗ КОНТУРА И БЕЗ КРОМОК — ' + kkSbDiag.noContN + ' дет.: ' + kkSbDiag.noCont.join('; ') + (kkSbDiag.noContN > n ? ' …и ещё ' + (kkSbDiag.noContN - n) : ''), ' Базис про них молчит: ни формы, ни кромок. Торец у них будет серым.' ]; } /** Записывает sborka.json рядом с оглавлением. При любой ошибке — не пишет и помечает сборку пропущенной. */ function kkSbWriteFile(title, order) { if (kkSb.err) return false; try { var data = kkSbBuild(title, order); system.writeTextFile(kkDir + 'sborka.json', kkJsonAscii(data)); kkSb.written = true; return true; } catch (e) { kkSbFail(e); return false; } } /* ------------------------------------------------------- ЧТО ПОШЛО НЕ ТАК * * Один список всего, что не сложилось: и настоящие сбои чтения (журнал * kkOops), и «данные не пришли» — когда исключения не было, а сведений нет. * Показывается ПЕРВЫМ делом в окне перед отправкой и в отчёте, чтобы про * беду узнавали сразу, а не через сутки. * * Кромки идут первой строкой нарочно: это единственное место, где наша * ошибка стоит денег. Показали торец голым, а кромка на нём есть — деталь * уедет незакатанной и станет браком. */ function kkTroubleLines() { var L = [], d = kkSbDiag, i, w; /* ---- кромки ---- */ var lost = d.buttNoCon + d.buttLost; if (lost > 0) { L[L.length] = 'КРОМКИ: ' + lost + ' из ' + d.buttsSeen + ' не легли на свои куски контура.'; L[L.length] = ' На странице эти торцы покажутся голыми, хотя кромка на них есть.'; for (i = 0; i < d.buttLostWho.length && i < 12; i++) L[L.length] = ' · ' + d.buttLostWho[i]; if (d.buttLostWho.length > 12) L[L.length] = ' · …и ещё ' + (d.buttLostWho.length - 12); } if (d.buttNoElem > 0) { L[L.length] = 'КРОМКИ: у ' + d.buttNoElem + ' шт. Базис не сказал, на каком куске контура они наклеены.'; } /* ---- контур ---- */ if (d.noContN > 0) { w = kkSbNoContourLines(); for (i = 0; i < w.length; i++) L[L.length] = w[i].replace(/^\s+/, ''); } if (d.tyUnknown > 0) { L[L.length] = 'КОНТУР: у ' + d.tyUnknown + ' кусков не отдался тип (прямая это или дуга).'; } /* ---- сборка целиком ---- */ if (kkSb.err) L[L.length] = 'СБОРКА пропущена: ' + kkSb.err + ' — схемы сборки у этой модели не будет.'; /* ---- картинки ---- */ if (kkTexList.length && kkTexSent < kkTexList.length) { L[L.length] = 'КАРТИНКИ: нашлось ' + kkTexSent + ' из ' + kkTexList.length + ' — материалы без картинки покажутся цветом.'; } for (i = 0; i < kkTexNotes.length && i < 6; i++) L[L.length] = 'КАРТИНКИ: ' + kkTexNotes[i]; /* ---- сверка чисел и незнакомые объекты ---- */ w = kkSvodLines(); for (i = 0; i < w.length; i++) L[L.length] = w[i]; /* ---- журнал сбоев чтения ---- */ for (i = 0; i < kkOops.order.length && i < 20; i++) { w = kkOops.by[kkOops.order[i]]; L[L.length] = 'СБОЙ ЧТЕНИЯ: ' + kkOops.order[i] + ' — ' + w.n + ' ' + (w.n === 1 ? 'раз' : 'раз(а)') + (w.msg ? ' (' + w.msg + ')' : ''); } if (kkOops.order.length > 20) L[L.length] = 'СБОЙ ЧТЕНИЯ: …и ещё ' + (kkOops.order.length - 20) + ' видов'; return L; } /* Виды объектов, которые скрипт разбирает осознанно. Всё прочее уезжает «прочим телом»: геометрия доедет, но чем эта штука была, мы не знаем. Такое надо видеть — по этому списку скрипт и дописывается дальше. */ var KK_KNOWN_CLS = { TFurnPanel: 1, TFastener: 1, TExtrusionBody: 1, TFurnBlock: 1, TFurnAsm: 1, TAsmKit: 1, TModel3D: 1, TModelLimits: 1, TSize3D: 1, TContour3D: 1, TLine3D: 1, TLayer: 1, TFurnPart: 1, /* Импортированная модель — мойка, плита, ручка: то, что нарисовано не в Базисе и вставлено файлом. Едет «прочим телом», и это верно: своей спецификации у неё нет. Узнали на живой кухне 25.08.2026 — их там 182. Держим в известных нарочно: если такое каждый раз попадало бы в «не разобрали», список замечаний превратился бы в шум, а шум читать перестают — и настоящую беду в нём проглядят. */ TImportedMesh: 1 }; var kkUnknownCls = {}, kkUnknownOrder = []; function kkUnknownSeen(cls) { if (!cls || KK_KNOWN_CLS[cls]) return; if (kkUnknownCls[cls] === undefined) { kkUnknownCls[cls] = 0; if (kkUnknownOrder.length < 40) kkUnknownOrder[kkUnknownOrder.length] = cls; } kkUnknownCls[cls]++; } /* * Перепись: сколько чего Базис отдал и сколько из этого записано. По ней и * сверяется, всё ли доехало — сначала здесь, потом на сайте, где сборщик * пересчитывает то же самое уже по готовой модели. Числа с двух сторон * должны совпасть; разошлись — значит по дороге что-то потерялось, и это * попадёт в замечания. */ function kkSvod() { var s = { parts: kkParts.length, // деталей записано в выгрузку tris: kkTriTotal, // треугольников panels: kkCounters.panel || 0, fast: kkCounters.fastener || 0, prof: kkCounters.profile || 0, body: kkCounters.body || 0, modules: kkModules.length, skipped: 0, seenPanels: kkClsSeen['TFurnPanel'] || 0, seenFast: kkClsSeen['TFastener'] || 0, tex: kkTexList.length, texSent: kkTexSent }; var k; for (k in kkSkipped) { if (kkSkipped.hasOwnProperty(k)) s.skipped += kkSkipped[k]; } if (!kkSb.err) { s.pos = kkSb.pos.length; s.bands = kkSb.bands.length; s.butts = kkSbDiag.buttsSeen; s.holes = kkSb.holes.length; s.units = kkSb.units.length; s.mats = kkSb.mats.length; } return s; } /* Строки сверки: где числа не сошлись — там и потеря. */ function kkSvodLines() { var L = [], s = kkSvod(), k, n; /* Базис показал столько-то панелей, а записали меньше — значит часть отсеялась. Само по себе это законно (скрытые, габаритная рамка), но видеть это надо: молча пропавшая деталь и есть худший случай. */ if (s.seenPanels > s.panels) { L[L.length] = 'СВЕРКА: Базис показал ' + s.seenPanels + ' панелей, в выгрузку попало ' + s.panels + '.'; for (k in kkSkipped) { if (kkSkipped.hasOwnProperty(k)) L[L.length] = ' отсеяно: ' + k + ' — ' + kkSkipped[k]; } } if (s.seenFast > s.fast && (s.seenFast - s.fast) > 0 && !s.skipped) { L[L.length] = 'СВЕРКА: крепежа Базис показал ' + s.seenFast + ', записано ' + s.fast + '.'; } /* Незнакомые виды объектов: геометрия доехала, но что это было — неясно. По этому списку скрипт и дописывается. */ n = 0; for (k = 0; k < kkUnknownOrder.length; k++) n += kkUnknownCls[kkUnknownOrder[k]]; if (n > 0) { var parts = []; for (k = 0; k < kkUnknownOrder.length && k < 12; k++) { /* без знака «×»: отчёт пишется системной кодировкой, и там он превращается в вопросительный знак */ parts[parts.length] = kkUnknownOrder[k] + ' — ' + kkUnknownCls[kkUnknownOrder[k]] + ' шт.'; } L[L.length] = 'НЕ РАЗОБРАЛИ: встретились незнакомые виды объектов (' + n + ' шт.): ' + parts.join(', ') + '.'; L[L.length] = ' Их объём в модель попал, но чем они были — скрипт не знает. Пришлите отчёт, допишем.'; } return L; } /* * Замечания — файлом рядом с моделью. Едут вместе с ней и ложатся в её папку * на сайте; на странице модели у хозяина есть кнопка, и по ней сразу видно, * что не сошлось, — или что всё чисто. Пишем чистым ASCII, как sborka.json: * кодировка тут ни при чём. Возвращает число замечаний. */ function kkZamWrite() { var zl = kkTroubleLines(); system.writeTextFile(kkDir + 'zamechaniya.json', kkJsonAscii({ v: 1, cv: 'kk3d ' + KK_VER, date: (new Date()).toLocaleString(), n: zl.length, lines: zl, svod: kkSvod() })); return zl.length; } /* Тот же список, готовый к показу: с заголовком или со словом «чисто». */ function kkTroubleBlock(wide) { var L = kkTroubleLines(), out = [], i; if (!L.length) { return [wide ? 'ЧТО ПОШЛО НЕ ТАК: ничего, всё прочиталось.' : ' замечаний нет']; } out[out.length] = wide ? ('!!! ЧТО ПОШЛО НЕ ТАК — ' + L.length + ' ' + kkWord(L.length, 'строка', 'строки', 'строк') + ' !!!') : 'ЧТО ПОШЛО НЕ ТАК:'; for (i = 0; i < L.length; i++) out[out.length] = ' ' + L[i]; return out; } // ------------------------------------------------------- JSON чистым ASCII /* * Отдельный сериализатор для sborka.json: всё, что не печатная латиница, * уходит в \uXXXX. Обычный kkJson этого не делает нарочно — его читает * сборщик, которому привычна системная кодировка, и менять его нельзя. */ function kkHex4(n) { var h = n.toString(16); while (h.length < 4) h = '0' + h; return h; } function kkJsonAsciiStr(s) { s = (s === null || s === undefined) ? '' : String(s); var out = [], i, c, code; for (i = 0; i < s.length; i++) { c = s.charAt(i); code = s.charCodeAt(i); if (c === '"' || c === '\\') out[out.length] = '\\' + c; else if (code < 32 || code > 126) out[out.length] = '\\u' + kkHex4(code); else out[out.length] = c; } return '"' + out.join('') + '"'; } function kkJsonAscii(v) { var t = typeof v, i, parts, k; if (v === null || v === undefined) return 'null'; if (t === 'number') return isFinite(v) ? String(Math.round(v * 1000) / 1000) : '0'; if (t === 'boolean') return v ? 'true' : 'false'; if (t === 'string') return kkJsonAsciiStr(v); parts = []; if (v instanceof Array) { for (i = 0; i < v.length; i++) parts[parts.length] = kkJsonAscii(v[i]); return '[' + parts.join(',') + ']'; } for (k in v) { if (!v.hasOwnProperty(k)) continue; if (v[k] === undefined) continue; parts[parts.length] = kkJsonAsciiStr(k) + ':' + kkJsonAscii(v[k]); } return '{' + parts.join(',') + '}'; } // ============================================================ окно отчёта /* * Что скрипт нашёл — показываем ДО отправки, пока ещё можно передумать. * Без этого человек узнаёт о пропавшей текстуре только по готовой модели в * браузере и не может понять, на каком шаге всё пошло не так. */ function kkSummaryText(title) { var L = [], i, t; L[L.length] = 'Модель: ' + title; L[L.length] = ''; /* Первым делом — беды. Раньше их приходилось выискивать в середине длинного отчёта, и о потерянной кромке узнавали уже с производства. */ t = kkTroubleBlock(true); for (i = 0; i < t.length; i++) L[L.length] = t[i]; L[L.length] = ''; L[L.length] = 'СОБРАНО'; L[L.length] = ' деталей ' + kkParts.length + ', треугольников ' + kkTriTotal; L[L.length] = ' модулей ' + kkModules.length + (kkModules.length ? ': ' + kkModules.slice(0, 3).join(', ') + (kkModules.length > 3 ? '…' : '') : ''); L[L.length] = ' панелей ' + (kkCounters.panel || 0) + ', крепежа ' + (kkCounters.fastener || 0) + ', профилей ' + (kkCounters.profile || 0) + ', прочих ' + (kkCounters.body || 0); L[L.length] = ' открывающихся деталей ' + kkAnims.length; /* Данные сборки — то, из чего сайт рисует схему сборки модуля. Если Базис не отдал кромки или отверстия, видно сразу, до отправки. */ L[L.length] = ' ' + kkSbSummaryLine(); var noc = kkSbNoContourLines(); for (i = 0; i < noc.length; i++) L[L.length] = noc[i]; /* Направление волокон и габарит детали: по ним раскладывается текстура. Если их нет, дерево ляжет не туда, и это надо видеть сразу. */ var nDir = 0, nSize = 0; for (i = 0; i < kkParts.length; i++) { if (kkParts[i].grainDir !== null && kkParts[i].grainDir !== undefined) nDir++; if (kkParts[i].grainSize) nSize++; } var nVec = 0; for (i = 0; i < kkParts.length; i++) if (kkParts[i].grainAxL) nVec++; L[L.length] = ' с направлением волокон ' + nDir + ', с габаритом ' + nSize + ', с осью волокон ' + nVec; for (i = 0; i < kkParts.length; i++) { if (kkParts[i].grainSize) { L[L.length] = ' пример: ' + kkParts[i].name + ' — ' + kkParts[i].grainSize.join(' x ') + ' мм, направление ' + kkParts[i].grainDir; break; } } L[L.length] = ''; L[L.length] = 'КАРТИНКИ ТЕКСТУР'; if (!kkTexList.length) { L[L.length] = ' материалов с картинкой нет — все цветные'; } else { L[L.length] = ' нашлось ' + kkTexSent + ' из ' + kkTexList.length + ', объём ' + Math.round(kkTexBytes / 1024) + ' КБ'; for (i = 0; i < kkParts.length; i++) { var mm = kkParts[i].mat; if (mm && mm.hasTex) { L[L.length] = ' плитка ' + mm.texSX + ' x ' + mm.texSY + ' мм, ориентация ' + mm.texOrient + ', угол ' + (Math.round(mm.texAngle * 100) / 100) + ', смещение ' + mm.texOffX + ',' + mm.texOffY; break; } } if (kkTexAuto.length) { L[L.length] = ' папка текстур:'; for (i = 0; i < kkTexAuto.length && i < 3; i++) L[L.length] = ' ' + kkTexAuto[i]; } else if (kkTexDirs === null) { L[L.length] = ' искать папку не понадобилось: Базис дал полные пути'; } else { L[L.length] = ' папка текстур НЕ НАЙДЕНА'; } for (i = 0; i < kkTexList.length && i < 12; i++) { t = kkTexList[i]; L[L.length] = ' ' + (t.done ? '+' : '—') + ' ' + kkTexBase(t.srcRaw); L[L.length] = ' ' + (t.done ? t.foundBy : 'не нашлась'); } if (kkTexList.length > 12) L[L.length] = ' …и ещё ' + (kkTexList.length - 12); } /* Облицовки пластей — плёнка на фасадах. Если их нашли, но материал по имени не отыскался, фасад останется в цвете детали — это надо видеть. */ if (kkPlastStat.panels) { L[L.length] = ''; L[L.length] = 'ОБЛИЦОВКИ ПЛАСТЕЙ'; L[L.length] = ' панелей с облицовкой ' + kkPlastStat.panels + ', отделено пластей ' + kkPlastStat.faces + (kkPlastStat.whole ? ', положена на всю деталь у ' + kkPlastStat.whole : '') + (kkPlastStat.transp ? ', прозрачных ' + kkPlastStat.transp : ''); for (i = 0; i < kkPlastStat.order.length && i < 6; i++) { var pn = kkPlastStat.order[i], ps = kkPlastStat.names[pn]; L[L.length] = ' ' + pn + ' — ' + ps.how + (ps.tex ? ' ' + ps.tex : '') + ' (' + ps.n + ' шт)'; } if (kkPlastStat.order.length > 6) L[L.length] = ' …и ещё ' + (kkPlastStat.order.length - 6); } if (kkTexNotes.length) { L[L.length] = ''; L[L.length] = 'ЗАМЕЧАНИЯ'; for (i = 0; i < kkTexNotes.length && i < 6; i++) L[L.length] = ' ' + kkTexNotes[i]; } if (kkMatBlind.length) { L[L.length] = ''; L[L.length] = 'МАТЕРИАЛЫ С КАРТИНКОЙ, НО БЕЗ ПУТИ К НЕЙ'; for (i = 0; i < kkMatBlind.length && i < 6; i++) L[L.length] = ' ' + kkMatBlind[i]; L[L.length] = ' (полный список — в файле отчёта)'; } return L.join('\n'); } /** * Показывает отчёт и спрашивает, отправлять ли. Если Базис не умеет * спрашивать — просто показываем и отправляем: терять собранное обидно. */ function kkAskSend(title) { var text = kkSummaryText(title) + '\n\nОтправить модель на сайт?'; try { if (typeof confirm === 'function') return confirm(text); } catch (e) {} try { alert(text); } catch (e2) {} return true; } // ============================================================== отправка /* * Отправку делает маленький помощник на PowerShell: у Базиса нет средств * послать запрос в сеть. Помощник пишется во временную папку рядом с * сырьём и удаляется вместе с ней. */ function kkUploaderText(dir, resFile, title) { var L = []; L[L.length] = '$ErrorActionPreference = "Stop"'; /* Путь к файлу ответа и способ его написать идут ПЕРВЫМИ строками, а сразу за ними — общая ловушка. Что бы дальше ни случилось (оборвался интернет посреди куска, антивирус занял файл, кончилось место), ответ будет написан. Иначе окно помощника просто закрывается, Базис говорит «отправка не вернула ответа», и понять причину нельзя ничем. */ L[L.length] = '$Res = "' + resFile + '"'; L[L.length] = 'function Say([string]$t) { [IO.File]::WriteAllText($Res, $t, [Text.Encoding]::Default); exit }'; L[L.length] = 'trap { Say ("ERR|сбой отправки: " + $_.Exception.Message); }'; L[L.length] = '[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12'; L[L.length] = '$Site = "' + KK_SITE + '"'; L[L.length] = '$Token = "' + KK_TOKEN + '"'; L[L.length] = '$Raw = "' + dir + '"'; /* Название модели — это текст, который ввёл человек: «Кухня $ 100», кавычки, знак ударения. Внутри двойных кавычек PowerShell знак доллара и обратный апостроф остаются живыми и подставляют что попало, вплоть до выполнения постороннего кода. Поэтому обезвреживаем их до подстановки. */ var psTitle = title.replace(/`/g, '``') .replace(/"/g, "'") .replace(/\$/g, function () { return '`$'; }); L[L.length] = '$Title = "' + psTitle + '"'; L[L.length] = '$Api = "$Site/cabinet/upload"'; L[L.length] = 'function Post([string]$u, [byte[]]$b, [int]$to) {'; L[L.length] = ' if ($to -le 0) { $to = 60000 }'; L[L.length] = ' $r = [Net.HttpWebRequest]::Create($u); $r.Method = "POST"'; L[L.length] = ' $r.Timeout = $to; $r.ReadWriteTimeout = 900000'; L[L.length] = ' $r.ContentType = "application/octet-stream"'; L[L.length] = ' $r.Headers.Add("X-KK-Token", $Token)'; L[L.length] = ' if ($null -eq $b) { $b = New-Object byte[] 0 }'; L[L.length] = ' $r.ContentLength = $b.Length'; L[L.length] = ' try {'; L[L.length] = ' $s = $r.GetRequestStream(); $s.Write($b, 0, $b.Length); $s.Close()'; L[L.length] = ' $p = $r.GetResponse()'; L[L.length] = ' $sr = New-Object IO.StreamReader($p.GetResponseStream(), [Text.Encoding]::UTF8)'; L[L.length] = ' $t = $sr.ReadToEnd(); $sr.Close(); $p.Close()'; /* Успешный ответ тоже надо уметь не разобрать: вместо JSON приходит страница-заглушка хостера, ответ корпоративного фильтра или пустота. Без этой проверки помощник падал бы молча. */ L[L.length] = ' $j = $null'; L[L.length] = ' try { $j = $t | ConvertFrom-Json } catch { $j = $null }'; L[L.length] = ' if ($null -eq $j) {'; L[L.length] = ' $sh = ("" + $t) -replace "\s+", " "'; L[L.length] = ' if ($sh.Length -gt 200) { $sh = $sh.Substring(0, 200) }'; L[L.length] = ' Say ("ERR|сайт ответил не по делу: " + $sh)'; L[L.length] = ' }'; L[L.length] = ' return $j'; L[L.length] = ' } catch [Net.WebException] {'; L[L.length] = ' $w = $_.Exception'; L[L.length] = ' if ($null -ne $w.Response) {'; L[L.length] = ' $sr = New-Object IO.StreamReader($w.Response.GetResponseStream(), [Text.Encoding]::UTF8)'; L[L.length] = ' $t = $sr.ReadToEnd(); $sr.Close()'; L[L.length] = ' $j = $null; try { $j = $t | ConvertFrom-Json } catch {}'; L[L.length] = ' if ($null -ne $j -and $j.error) { Say ("ERR|" + $j.error) }'; L[L.length] = ' Say ("ERR|сервер ответил: " + $t)'; L[L.length] = ' }'; L[L.length] = ' Say ("ERR|нет связи с сайтом. " + $w.Message)'; L[L.length] = ' }'; L[L.length] = '}'; /* Что отправлять из папки сырья. Список видов картинок должен совпадать с тем, что принимает сайт: пропустишь здесь webp или gif — картинка молча не доедет, а в отчёте будет написано, что она нашлась. */ L[L.length] = '$mask = "^(tri_\\d{1,6}\\.kkr|manifest\\.json|sborka\\.json|zamechaniya\\.json|final\\.json|tex_\\d{1,4}\\.(jpg|png|webp|gif|bmp))$"'; L[L.length] = '$files = @(Get-ChildItem -LiteralPath $Raw | Where-Object { -not $_.PSIsContainer -and $_.Name -match $mask })'; L[L.length] = 'if ($files.Count -eq 0) { Say "ERR|в папке нет файлов модели" }'; L[L.length] = '$total = [int64](($files | Measure-Object -Property Length -Sum).Sum)'; /* Вес сырья говорим сайту ДО первого куска. Это прикидка сверху: готовая модель в разы легче сырья, и сайт это знает — по такому числу он откажет только там, где места не хватит наверняка. Зато отказ придёт сразу, а не после получаса передачи. */ L[L.length] = '$ini = Post "$Api`?a=init&title=$([Uri]::EscapeDataString($Title))&bytes=$total" $null 60000'; L[L.length] = 'if (-not $ini.ok) { Say ("ERR|" + $ini.error) }'; L[L.length] = '$sid = $ini.sid; $sent = 0'; // версия скрипта, лежащего сейчас на сайте: пригодится, чтобы сверить со своей L[L.length] = '$Cur = ""; try { if ($ini.cur_ver) { $Cur = [string]$ini.cur_ver } } catch {}'; L[L.length] = 'foreach ($f in $files) {'; L[L.length] = ' $fs = [IO.File]::OpenRead($f.FullName)'; L[L.length] = ' try {'; L[L.length] = ' $buf = New-Object byte[] 1048576'; L[L.length] = ' while (($n = $fs.Read($buf, 0, $buf.Length)) -gt 0) {'; L[L.length] = ' $part = New-Object byte[] $n; [Array]::Copy($buf, 0, $part, 0, $n)'; L[L.length] = ' $r = Post "$Api`?a=chunk&sid=$sid&name=$($f.Name)" $part 180000'; L[L.length] = ' if (-not $r.ok) { Say ("ERR|" + $r.error) }'; L[L.length] = ' $sent += $n'; L[L.length] = ' $pc = [int](100 * $sent / $total)'; L[L.length] = ' Write-Progress -Activity "Отправка модели на сайт" -Status "$pc%" -PercentComplete $pc'; L[L.length] = ' }'; L[L.length] = ' } finally { $fs.Close() }'; L[L.length] = '}'; L[L.length] = 'Write-Progress -Activity "Отправка модели на сайт" -Completed'; L[L.length] = '$fin = Post "$Api`?a=done&sid=$sid" $null 900000'; L[L.length] = 'if (-not $fin.ok) { Say ("ERR|" + $fin.error) }'; L[L.length] = 'try { Set-Clipboard -Value $fin.url } catch { try { $fin.url | clip.exe } catch {} }'; L[L.length] = 'Say ("OK|" + $fin.url + "|" + $fin.stat.nodes + "|" + $fin.stat.triangles + "|" + $PSVersionTable.PSVersion + "|" + $Cur)'; return L.join('\r\n'); } // ================================================================== отчёт /* * Отчёт о запуске. Скрипт работает на чужом компьютере, в программе, которой * у нас нет, и без него любой сбой пришлось бы разбирать по одной строке * из окна. Файл остаётся на диске после уборки — его можно просто прислать. */ function kkReportPath(tmp) { return tmp + '\\kk3d-otchet.txt'; } /* * Отчёт уходит на сайт сам. * * Раньше он лежал во временной папке на чужом компьютере: чтобы узнать, что * пошло не так, надо было попросить владельца найти файл и прислать. Из-за * этого о беде узнавали через сутки, а то и не узнавали вовсе — так и вышло * со стёртыми кромками. Теперь отчёт уезжает в кабинет после КАЖДОГО запуска, * удачного и нет, и лежит там списком: «Из Базиса» → «Отчёты». * * Своих средств послать запрос в сеть у Базиса нет, поэтому пишем того же * маленького помощника на PowerShell, что и для отправки модели. Отправка * никогда не должна мешать работе: любая беда здесь остаётся строкой в окне. */ function kkReportSend(sh, tmp) { if (!sh) return ''; if (KK_SITE.indexOf('HOST') >= 0 || KK_TOKEN.indexOf('TOKEN') >= 0) return ''; var file = kkReportPath(tmp); var psFile = tmp + '\\kk3d-otchet-send.ps1'; var resFile = tmp + '\\kk3d-otchet-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/otchet")'; 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'); sh.Run('powershell -NoProfile -ExecutionPolicy Bypass -File "' + psFile + '"', 0, true); var ans = ''; try { ans = kkStr(system.readTextFile(resFile)); } catch (e) { ans = ''; } if (ans.substring(0, 3) === 'OK|') return 'Отчёт о запуске отправлен в кабинет.'; if (ans.substring(0, 4) === 'ERR|') return 'Отчёт в кабинет не ушёл: ' + ans.substring(4, 160); return 'Отчёт в кабинет не ушёл: ответа нет.'; } catch (e) { return 'Отчёт в кабинет не ушёл: ' + (e.message || e); } } function kkReport(tmp, title, res) { try { var L = [], k; L[L.length] = 'Отчёт скрипта выгрузки в 3D, версия ' + KK_VER; L[L.length] = 'Дата: ' + (new Date()).toLocaleString(); L[L.length] = 'Сайт: ' + KK_SITE; L[L.length] = 'Модель: ' + title; L[L.length] = ''; var tb = kkTroubleBlock(true); for (k = 0; k < tb.length; k++) L[L.length] = tb[k]; L[L.length] = ''; L[L.length] = 'Собрано: модулей ' + kkModules.length + ', деталей ' + kkParts.length + ', треугольников ' + kkTriTotal + ', файлов геометрии ' + kkTriFile; L[L.length] = 'По видам: панелей ' + (kkCounters.panel || 0) + ', крепежа ' + (kkCounters.fastener || 0) + ', профилей ' + (kkCounters.profile || 0) + ', прочих тел ' + (kkCounters.body || 0); L[L.length] = 'Открывающихся деталей: ' + kkAnims.length; L[L.length] = kkSbSummaryLine() + (kkSb.err ? '' : (kkSb.written ? ' Файл sborka.json записан.' : ' Файл sborka.json не писался.')); var noc = kkSbNoContourLines(); for (k = 0; k < noc.length; k++) L[L.length] = noc[k]; L[L.length] = ''; L[L.length] = 'Что встретилось в модели:'; for (k in kkClsSeen) { if (kkClsSeen.hasOwnProperty(k)) L[L.length] = ' ' + k + ' — ' + kkClsSeen[k]; } L[L.length] = ''; L[L.length] = 'Что отброшено и почему:'; var any = false; for (k in kkSkipped) { if (kkSkipped.hasOwnProperty(k)) { L[L.length] = ' ' + k + ' — ' + kkSkipped[k]; any = true; } } if (!any) L[L.length] = ' ничего'; L[L.length] = ''; L[L.length] = 'Свойства первого объекта каждого вида:'; for (k = 0; k < kkNotes.length; k++) L[L.length] = kkNotes[k]; L[L.length] = ''; L[L.length] = 'Картинки текстур: отправлено ' + kkTexSent + ' из ' + kkTexList.length + ', объём ' + Math.round(kkTexBytes / 1024) + ' КБ'; L[L.length] = 'Папки текстур из кабинета: ' + (KK_TEXDIRS && KK_TEXDIRS.indexOf('TEXDIRS') < 0 ? KK_TEXDIRS : '(не указаны)'); L[L.length] = 'Файл модели: ' + (kkModelFile() || '(не отдался)'); if (kkTexAuto.length) { L[L.length] = 'Папки текстур, найденные самим скриптом:'; for (k = 0; k < kkTexAuto.length; k++) L[L.length] = ' ' + kkTexAuto[k]; } else if (kkTexDirs !== null) { L[L.length] = 'Папок с текстурами не нашлось ни одной.'; } if (kkTexIndexed) L[L.length] = 'Картинок в указателе: ' + kkTexIndexed; if (kkTexList.length) { L[L.length] = 'Что Базис отдал как путь к картинке:'; for (k = 0; k < kkTexList.length && k < 8; k++) { L[L.length] = ' ' + kkTexList[k].safe + ' <- ' + kkTexList[k].srcRaw + (kkTexList[k].foundBy ? ' [нашли ' + kkTexList[k].foundBy + ': ' + kkTexList[k].src + ']' : ''); } } if (kkTexNotes.length) { L[L.length] = 'Что с картинками не так:'; for (k = 0; k < kkTexNotes.length; k++) L[L.length] = ' ' + kkTexNotes[k]; } /* ---- КРОМКИ: разведка. Ради этого блока версия 2.10 и делалась: по нему станет видно, что Базис отдаёт на самом деле — все ли кромки детали доходят, есть ли признак «криволинейная», откуда брать обозначение и артикул ленты. */ L[L.length] = ''; L[L.length] = 'КРОМКИ — разведка'; /* Потерю считаем только по панелям: у столешниц и погонажа клеток e нет вовсе, и их кромки в раскладку не идут по устройству. */ var lostB = kkSbDiag.buttsSeen - kkSbDiag.profButts - kkSbDiag.buttsPlaced; L[L.length] = ' наклеено кромок всего: ' + kkSbDiag.buttsSeen + ' (из них у столешниц и погонажа ' + kkSbDiag.profButts + ')' + ', у панелей разложено по четырём сторонам: ' + kkSbDiag.buttsPlaced + (lostB > 0 ? ' <-- НЕ ПОМЕСТИЛОСЬ ' + lostB + ' (сторону не определили или клетка занята)' : ''); L[L.length] = ' кромок у столешниц и погонажа: ' + kkSbDiag.profButts + ' (прежние версии их не читали вовсе)'; L[L.length] = ' контур записан у панелей: ' + kkSbDiag.contPanels + ', из них длиннее четырёх кусков: ' + kkSbDiag.contLong + ', самый длинный: ' + kkSbDiag.contMax + ', дуг: ' + kkSbDiag.arcSeen + ', тип куска не отдался: ' + kkSbDiag.tyUnknown; L[L.length] = ' по материалам панелей (панелей / из них с кромкой / кромок):'; for (k = 0; k < kkSbDiag.matOrder.length && k < 20; k++) { var mnm = kkSbDiag.matOrder[k], mst = kkSbDiag.byMat[mnm]; L[L.length] = ' ' + mnm + ' — ' + mst.n + ' / ' + mst.wb + ' / ' + mst.b; } if (kkSbDiag.multi.length) { L[L.length] = ' детали, где кромок несколько (если номер элемента у всех один — Базис отдаёт одну и ту же кромку):'; for (k = 0; k < kkSbDiag.multi.length; k++) { var mu = kkSbDiag.multi[k]; L[L.length] = ' «' + mu.name + '», кромок ' + mu.n + ':'; for (var q2 = 0; q2 < mu.det.length; q2++) L[L.length] = ' ' + mu.det[q2]; } } else { L[L.length] = ' деталей, где кромок больше одной, не встретилось'; } /* Все кромки из списка материалов модели: видит ли скрипт то же, что владелец видит в окне «Используемая облицовка». */ try { var lists = kkMtListsFor(null), li, lj, LL, cntM, mtx, nmx, shown = 0; L[L.length] = ' что в списке материалов модели похоже на кромку:'; for (li = 0; li < lists.length && shown < 40; li++) { LL = lists[li]; cntM = 0; try { cntM = LL.Count; } catch (e9) { cntM = 0; } for (lj = 0; lj < cntM && lj < 5000 && shown < 40; lj++) { mtx = null; try { mtx = LL.Materials[lj]; } catch (e9) { mtx = null; } if (!mtx) { try { mtx = LL[lj]; } catch (e9) { mtx = null; } } if (!mtx) continue; nmx = ''; try { nmx = kkStr(mtx.MaterialName); } catch (e9) {} if (!nmx) { try { nmx = kkStr(mtx.Name); } catch (e9) {} } if (!nmx || nmx.toLowerCase().indexOf('кромк') < 0) continue; shown++; L[L.length] = ' ' + nmx; } } if (!shown) L[L.length] = ' ни одного (или список материалов не отдался)'; } catch (e8) { L[L.length] = ' список материалов прочитать не вышло'; } /* Облицовки пластей — где лежит дерево фасадов. */ var mtHow = kkMtListHow; if (!mtHow) { mtHow = (kkMtListsGlobal === null) ? 'не понадобился' : (kkMtListsGlobal.length ? 'общий список (MtList / Model.MtList / Action.MtList)' : 'НЕ НАЙДЕН'); } L[L.length] = ''; L[L.length] = 'Облицовки пластей: панелей с облицовкой ' + kkPlastStat.panels + ', отделено пластей ' + kkPlastStat.faces + ', положена на всю деталь у ' + kkPlastStat.whole + ', прозрачных ' + kkPlastStat.transp; L[L.length] = 'Список материалов модели: ' + mtHow; for (k = 0; k < kkPlastStat.order.length; k++) { var pn2 = kkPlastStat.order[k], ps2 = kkPlastStat.names[pn2]; L[L.length] = ' ' + pn2 + ' — ' + ps2.how + (ps2.tex ? ' ' + ps2.tex : '') + ' (' + ps2.n + ' шт)'; } /* Материалы, у которых картинка вроде есть, а пути к ней нет. Это главный след, по которому ищут пропавшие декоры: здесь видно, под каким именем свойства Базис держит её в ЭТОЙ версии. */ if (kkMatBlind.length) { L[L.length] = ''; L[L.length] = 'Материалы с картинкой, но без пути к ней:'; for (k = 0; k < kkMatBlind.length; k++) L[L.length] = ' ' + kkMatBlind[k]; } L[L.length] = ''; L[L.length] = 'Ответ отправки: ' + (res || '(пусто)'); if (kkFail) L[L.length] = 'Сбой сбора: ' + kkFail; system.writeTextFile(kkReportPath(tmp), L.join('\r\n')); } catch (e) {} } // ======================================================= окно и вести о ходе /* * Всё, что видит человек за запуск, рисует ОДНО окно. Это отдельная маленькая * программа на PowerShell: её текст лежит в KK_OKNO, Базис кладёт его во * временную папку и запускает СКРЫТО и НЕ ДОЖИДАЯСЬ, а сам сразу уходит * читать модель и лишь переписывает вести о ходе в файлах. * * Почему окно не своё, базисное. Окна Базиса умеют только показаться и * дождаться нажатия: ShowModal останавливает скрипт, перерисовать надпись во * время работы нечем, готовой полоски у него нет вовсе. Ползунок в окне * Базиса замер бы и побелел — ровно то, на что жаловались. А послать запрос * в сеть Базис не умеет в принципе, поэтому PowerShell тут неизбежен; * уходит не он, а его чёрная консоль: её не видно ни мига. * * Договор между Базисом и окном — только файлы в папке запуска: * zadanie.json куда слать, чем подписываться, как зовут модель * hod.txt весть о ходе чтения, чистым ASCII * svodka.json что собралось и что не понравилось * gotovo.json пишется ПОСЛЕДНИМ: пока его нет, окно файлов не трогает * reshenie.txt OKNO — дело за окном, SAM — Базис шлёт сам * okno-jiv.txt окно поднялось и его видно * otmena.txt окно закрыли раньше времени, папку убирает Базис */ var kkHodLast = 0; var kkTexSeen = 0; /* * Весть о ходе. Пишем ЧИСТЫМ ASCII — русские слова подставляет окно: на самом * частом пути (несколько раз в секунду) кодировки тогда нет вовсе. Не чаще * раза в 300 мс, иначе Базис будет больше писать файл, чем читать модель. */ function kkHodSet(line, force) { var now = (new Date()).getTime(); if (!force && now - kkHodLast < 300) return; kkHodLast = now; try { system.writeTextFile(kkDir + 'hod.txt', line); } catch (e) {} } /* * Вести снимаем ОБЁРТКАМИ поверх готовых имён. Ядро сбора зовёт эти функции * по глобальному имени, поэтому подмена срабатывает сама, и в самом ядре не * меняется ни одной строки — а трогать его нельзя. Не встало — теряем только * счётчик, сбор от этого не страдает. */ function kkHodHooks() { try { var wb = kkWriteBody; kkWriteBody = function () { try { kkHodSet('READ|' + kkParts.length); } catch (e) {} return wb.apply(null, arguments); }; } catch (e) {} try { var fl = kkFlush; kkFlush = function () { try { kkHodSet('READ|' + kkParts.length); } catch (e) {} return fl.apply(null, arguments); }; } catch (e) {} try { var tf = kkTexFind; kkTexFind = function () { try { kkTexSeen++; kkHodSet('TEX|' + kkTexSeen + '|' + kkTexList.length); } catch (e) {} return tf.apply(null, arguments); }; } catch (e) {} } /* * Задание окну. Чистым ASCII через kkJsonAscii: русское уезжает как \uXXXX и * вопрос кодировки между Базисом и PowerShell снимается целиком, а не * проверяется на удачу. */ function kkZadanieWrite(title, order, otchet) { system.writeTextFile(kkDir + 'zadanie.json', kkJsonAscii({ site: KK_SITE, token: KK_TOKEN, ver: KK_VER, title: kkStr(title), order: kkStr(order), otchet: kkStr(otchet) })); } /* * Что собралось и что не понравилось. Окно показывает это ДО отправки — * замечания первыми: ошибка в кромке означает, что деталь уедет незакатанной, * и узнать об этом надо здесь, а не в цехе. */ function kkSvodkaWrite(title) { var zam = [], svod = []; try { zam = kkTroubleLines(); } catch (e) { kkOopsAdd('замечания для окна не собрались', e); } try { svod = kkSummaryText(title).split('\n'); } catch (e) {} try { system.writeTextFile(kkDir + 'svodka.json', kkJsonAscii({ zam: zam, svodka: svod })); } catch (e) { kkOopsAdd('сводка для окна не записалась', e); } } /* Последним пишем «готово». Пока этого файла нет, окно не смотрит на файлы модели: порядок записи и есть весь договор. */ function kkGotovoWrite(ok, why) { try { system.writeTextFile(kkDir + 'gotovo.json', kkJsonAscii( ok ? { ok: true, parts: kkParts.length, tris: kkTriTotal } : { ok: false, why: kkStr(why) })); } catch (e) {} } function kkSay(name, text) { try { system.writeTextFile(kkDir + name, text); } catch (e) {} } /* Ровная пауза без нагрузки: своего сна у Базиса нет, а крутить пустой цикл на чужом компьютере нельзя — вентилятор взвоет на ровном месте. */ function kkPause(sh) { try { sh.Run('cmd /c ping -n 3 127.0.0.1 > nul', 0, true); } catch (e) {} } function kkHasFile(fso, path) { try { return !!fso.FileExists(path); } catch (e) { return false; } } /* * Поднялось ли окно и видно ли его. * * Спрашивать надо ПЕРЕД каждой передачей дела, в том числе когда передаём * дурную весть. Проверка поймала ровно эту ошибку: при сбое сбора скрипт * отдавал беду окну не глядя, и если окно не поднялось, человек не видел * ничего вообще — ни ссылки, ни причины. * * Окно поднимается за секунду-полторы, а модель читается минутами, — обычно * оно давно на экране. Но на модели из трёх деталей сбор может обогнать * запуск, поэтому даём ему договорённое время. */ function kkOknoZhiv(fso, sh) { var k; for (k = 0; k < 4; k++) { if (kkHasFile(fso, kkDir + 'okno-jiv.txt')) return true; kkPause(sh); } return false; } /* * Уборка за прошлыми запусками. Если окно убьют диспетчером, убирать станет * некому, а в папке лежит ключ доступа. Время берём из САМОГО ИМЕНИ папки * (kk3d_<время>) — так не нужно спрашивать у системы даты и нельзя ошибиться * с чужой папкой. Всё лишнее пропускаем молча: уборка не повод падать. */ function kkCleanOld(fso, tmp, mine) { try { var edge = (new Date()).getTime() - 24 * 3600 * 1000; var it = new Enumerator(fso.GetFolder(tmp).SubFolders); for (; !it.atEnd(); it.moveNext()) { var d = it.item(), n = String(d.Name); if (n.indexOf('kk3d_') !== 0) continue; var when = parseInt(n.substring(5), 10); if (!when || when > edge) continue; if (String(d.Path) + '\\' === mine) continue; try { fso.DeleteFolder(d.Path, true); } catch (e) {} } } catch (e) {} } // ============================================================ ход всей работы /* * Порядок такой: поднять окно, молча прочитать модель, всё записать, отдать * дело окну. Ни одного alert за удачный запуск. * * Запасной путь оставлен целиком и нарочно: не поднялось окно — работаем * ровно так, как работали до него, через kkAskSend и kkUploaderText. Обе * дороги не должны зависеть друг от друга, иначе одна опечатка убивает сразу * обе, и человек остаётся ни с чем. */ function kkMain() { var fso, sh, tmp; if (KK_TOKEN.indexOf('TOKEN') >= 0) { alert('В этом файле не подставлен ключ.\n\nСкачайте скрипт заново в личном кабинете на сайте.'); return; } try { fso = NewCOMObject('Scripting.FileSystemObject'); sh = NewCOMObject('WScript.Shell'); } catch (e) { alert('Не удалось обратиться к системе Windows.\n\n' + (e.message || e)); return; } // временная папка: своя у каждого пользователя, ничего не привязано к дискам try { tmp = sh.ExpandEnvironmentStrings('%TEMP%'); if (!tmp || tmp === '%TEMP%') tmp = 'C:\\Windows\\Temp'; } catch (e) { tmp = 'C:\\Windows\\Temp'; } var stamp = (new Date()).getTime(); kkDir = tmp + '\\kk3d_' + stamp + '\\'; try { fso.CreateFolder(kkDir.substring(0, kkDir.length - 1)); } catch (e) { alert('Не удалось создать временную папку:\n' + kkDir); return; } kkCleanOld(fso, tmp, kkDir); /* Имя модели и номер заказа Базис держит не в самой модели, а в данных открытого файла. Пробуем по очереди: название изделия, его код, имя файла — что найдётся первым. */ var title = '', order = ''; try { if (typeof currentFileData !== 'undefined' && currentFileData) { if (currentFileData.article) { try { order = kkStr(currentFileData.article.OrderName || ''); } catch (e) {} try { title = kkStr(currentFileData.article.NameWithCode() || ''); } catch (e) {} if (!title) { try { title = kkStr(currentFileData.article.Code || ''); } catch (e) {} } if (!title) { try { title = kkStr(currentFileData.article.Name || ''); } catch (e) {} } } if (!title) { try { title = kkStr(currentFileData.filename || ''); } catch (e) {} } } } catch (e) {} if (!title) { try { title = kkStr(Model.Name); } catch (e) {} } if (!title) title = 'Модель'; // из полного имени файла оставляем только само имя, без пути и расширения var slash = Math.max(title.lastIndexOf('\\'), title.lastIndexOf('/')); if (slash >= 0) title = title.substring(slash + 1); var dot = title.lastIndexOf('.'); if (dot > 0) title = title.substring(0, dot); title = kkStr(title); if (!title) title = 'Модель'; /* ---- окно ---- Поднимаем ПЕРВЫМ делом, до чтения модели: чтение идёт минутами, и всё это время человек должен видеть движение, а не пустой экран. Стиль окна 0 прячет консоль с самого рождения — она не мелькает даже на миг; ключ -WindowStyle Hidden и вызов ShowWindow внутри самого окна — два независимых способа всё-таки показаться, потому что промах здесь означает пустой экран и брошенную работу. */ var oknoFile = tmp + '\\kk3d-okno-' + stamp + '.ps1'; var okno = false; try { if (KK_OKNO && KK_OKNO.length > 200 && KK_OKNO.indexOf('OKNO') !== 0) { system.writeTextFile(oknoFile, KK_OKNO); kkZadanieWrite(title, order, kkReportPath(tmp)); kkHodSet('READ|0', true); sh.Run('powershell -NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File "' + oknoFile + '" -Dir "' + kkDir.substring(0, kkDir.length - 1) + '"', 0, false); okno = true; } } catch (e) { okno = false; } if (okno) kkHodHooks(); // ---- сбор ---- try { kkWalk(Model, -1, -1, 0); kkFlush(); } catch (e) { kkFail = 'сбор модели прервался: ' + (e.message || e); } if (kkFail || kkTriTotal === 0) { var why = kkFail ? kkFail : 'в модели не нашлось ни одной детали с объёмом'; /* Отчёт пишем и отправляем ЗДЕСЬ, как и раньше: беда со сбором — самое важное, что вообще уезжает в кабинет, и зависеть от того, доживёт ли окно, она не должна. */ kkReport(tmp, title, ''); var sentF = kkReportSend(sh, tmp); if (okno && kkOknoZhiv(fso, sh)) { kkGotovoWrite(false, why); kkSay('reshenie.txt', 'OKNO'); return; // беду покажет окно, оно же уберёт папку } if (okno) kkSay('reshenie.txt', 'SAM'); alert('Не получилось собрать модель.\n\n' + why + '\n\nОтчёт о запуске: ' + kkReportPath(tmp) + (sentF ? '\n' + sentF : '')); try { fso.DeleteFolder(kkDir.substring(0, kkDir.length - 1), true); } catch (e2) {} return; } /* ---- картинки текстур ---- Копируем до оглавления: если картинка не доехала, из материала надо успеть убрать ссылку на неё, иначе сборщик будет искать то, чего нет. */ try { kkTexCopy(fso); } catch (e) { kkTexNotes[kkTexNotes.length] = 'копирование картинок прервалось: ' + (e.message || e); } /* ---- оглавление ---- Пишем ДО того, как спросить человека: спрашивает теперь окно, и оно же показывает замечания. Запись стоит копейки, а порядок «сначала все файлы, gotovo.json последним» — весь договор с окном. */ kkHodSet('WRITE|0', true); try { var man = { format: 'kk-raw', version: 1, units: 'mm', client: 'basis-export ' + KK_VER, model: { order: order, name: title, file: '' }, modules: kkModules, anims: kkAnims, parts: kkParts }; system.writeTextFile(kkDir + 'manifest.json', kkJson(man)); /* Замечания едут ВМЕСТЕ С МОДЕЛЬЮ и ложатся рядом с ней на сайте: на странице модели есть кнопка, и по ней сразу видно, что не прочиталось — или что всё чисто. Иначе беду замечают в цехе. Пишем чистым ASCII, как sborka.json: кодировка тут ни при чём. */ try { kkZamWrite(); } catch (e) { kkOopsAdd('замечания не записались', e); } /* Данные сборки — рядом с оглавлением и строго до final.json: помощник отправляет всё, что лежит в папке к его запуску. Ошибка здесь гасится внутри: модель без схемы сборки лучше, чем никакой. */ kkSbWriteFile(title, order); system.writeTextFile(kkDir + 'final.json', kkJson({ tri_count: kkTriTotal, tri_files: kkTriFile, manifest_bytes: 0, part_count: kkParts.length })); } catch (e) { var whyMan = 'не удалось записать оглавление модели: ' + (e.message || e); if (okno && kkOknoZhiv(fso, sh)) { kkGotovoWrite(false, whyMan); kkSay('reshenie.txt', 'OKNO'); return; } if (okno) kkSay('reshenie.txt', 'SAM'); alert('Не удалось записать оглавление модели.\n\n' + (e.message || e)); return; } /* ---- отдаём дело окну ---- Сначала сводка и отчёт, потом gotovo.json — окно ждёт именно его. Отчёт о запуске Базис пишет и отправляет САМ, не перекладывая на окно: по отчётам ловят потерянные кромки, и пропадать они не должны, даже если окно закроют или убьют. Итог отправки окно допишет в тот же файл. */ kkSvodkaWrite(title); kkReport(tmp, title, ''); if (okno && kkOknoZhiv(fso, sh)) { /* Человек мог закрыть окно, пока шёл сбор. Тогда убирать за собой нам: окно нарочно не трогает папку, пока Базис в неё пишет. */ if (kkHasFile(fso, kkDir + 'otmena.txt')) { try { fso.DeleteFolder(kkDir.substring(0, kkDir.length - 1), true); } catch (e) {} return; } kkGotovoWrite(true); kkSay('reshenie.txt', 'OKNO'); kkReportSend(sh, tmp); return; // ни одного окна Базиса за весь удачный запуск } /* Окно не поднялось. Говорим ему об этом на случай, если оно всё же родится позже, — и уходим прежней дорогой, проверенной годами. */ if (okno) kkSay('reshenie.txt', 'SAM'); // ---- запасной путь: отправка так, как было до окна ---- if (!kkAskSend(title)) { try { fso.DeleteFolder(kkDir.substring(0, kkDir.length - 1), true); } catch (e) {} return; } var resFile = kkDir + 'result.txt'; var psFile = kkDir + 'send.ps1'; try { system.writeTextFile(psFile, kkUploaderText(kkDir, resFile, title)); } catch (e) { alert('Не удалось подготовить отправку.\n\n' + (e.message || e)); return; } try { sh.Run('powershell -NoProfile -ExecutionPolicy Bypass -File "' + psFile + '"', 1, true); } catch (e) { alert('Не удалось запустить отправку.\n\n' + (e.message || e)); return; } // ---- итог ---- var res = ''; try { res = kkStr(system.readTextFile(resFile)); } catch (e) {} /* Второй заход другим способом: чтение через Базис изредка возвращает пустоту там, где обычное чтение файла срабатывает. Потерять готовую ссылку из-за этого было бы обидно. */ if (!res) { try { var ts = fso.OpenTextFile(resFile, 1, false); res = kkStr(ts.ReadAll()); ts.Close(); } catch (e) {} } /* Если в начале файла всё-таки окажется метка кодировки, она превратится в один-три посторонних символа перед меткой ответа. Срезаем их. */ var cut = 0; while (cut < res.length && cut < 4 && res.charAt(cut) !== 'O' && res.charAt(cut) !== 'E') cut++; if (cut > 0 && cut < 4) res = res.substring(cut); kkReport(tmp, title, res); var sentRep = kkReportSend(sh, tmp); try { fso.DeleteFolder(kkDir.substring(0, kkDir.length - 1), true); } catch (e) {} if (!res) { alert('Отправка не вернула ответа.\n\n' + 'Проверьте подключение к интернету и попробуйте ещё раз.\n\n' + 'Отчёт о запуске: ' + kkReportPath(tmp) + (sentRep ? '\n' + sentRep : '')); return; } var p = res.split('|'); /* Версию показываем в каждом окне, а сайт при загрузке сообщает, какая версия лежит на нём сейчас. Скачанные копии скрипта копятся в папке и выглядят одинаково: без этой проверки можно час искать ошибку, которой давно нет, просто запустив прошлогодний файл. */ var vLine = 'Скрипт версии ' + KK_VER; /* Замечания повторяем и в последнем окне: окно со сводкой человек уже закрыл, а знать про потерянную кромку надо ДО того, как деталь уедет в цех. Подробности — в отчёте, он же лежит в кабинете. */ var tn = kkTroubleLines().length; var warn = tn ? ('\n\nБЫЛО ' + tn + ' ' + kkWord(tn, 'замечание', 'замечания', 'замечаний') + ' — смотрите отчёт: кабинет → «Из Базиса» → «Отчёты».') : ''; var stale = ''; if (p.length > 5) { var cur = kkStr(p[5]); if (cur !== '' && cur !== KK_VER) { stale = '\n\nВНИМАНИЕ: запущена СТАРАЯ копия скрипта.\n' + 'У вас версия ' + KK_VER + ', а на сайте уже ' + cur + '.\n' + 'Скачайте скрипт заново в личном кабинете.'; } } if (p[0] === 'OK') { alert('Модель «' + title + '» загружена.\n\n' + 'Деталей: ' + p[2] + ', треугольников: ' + p[3] + '\n\n' + 'Ссылка для заказчика (уже скопирована в буфер обмена):\n' + p[1] + '\n\n' + vLine + stale + warn + (sentRep ? '\n' + sentRep : '')); } else { alert('Отправить не получилось.\n\n' + p.slice(1).join('|') + '\n\n' + vLine + warn + '\n\nОтчёт о запуске: ' + kkReportPath(tmp) + (sentRep ? '\n' + sentRep : '')); } } // ================================================================== запуск /* * Ниже этой метки — одна строка, и так нарочно. Проверка на подделке Базиса * (проверки/тест-скрипта-базиса.js) отрезает файл ровно здесь и зовёт kkMain * сама: пока весь пуск лежал внутри этой скобки, он не был покрыт ни одной * проверкой, а именно в нём теперь живут окно и передача дела. */ (function () { kkMain(); })();