Здравствуйте! Недавно начал работать с продуктом Winnum и столкнулся с трудностью: не могу найти документацию к Winnum SDK. На официальном сайте разработчика мне найти ее не удалось.
Может быть, кто-то из сообщества сталкивался с подобной задачей и знает, где можно скачать руководство по его использованию? Буду очень благодарен за любую помощь или наводку.
В приложении Цифровой двойник довольно часто используются кнопки. Я решил создать вторую версию панели с кнопками для 3D в стиле ЧПУ-станка. Здесь будет представлен расширенный функционал. В данном посте я хотел бы рассказать о том как добавить их себе в 3D-сцену и настроить. Они смогут помочь стилизовать сцену и улучшить восприятие.
1. Скачать файл ViewButtons (во вложении) и импортировать себе в сцену (Файл - Импорт). 2. Если мы начнём воспроизведение сцены они появятся в правом нижнем углу. Здесь можно увидеть название цеха, индикатор и 9 кнопок: 4 кнопки с видами, Автооблёт, Ночной режим, Скриншот, Сброс кнопка ИНФО. У каждой кнопки есть hotkey и подписан в скобках.
Каждая кнопка с видом при нажатии будет перемещать камеру в заданное место. При Автооблёте камера будет крутиться вокруг сцены. Ночной режим выключает освещение на сцене и изменяет её в тёмно-синие оттенки. Скриншот делает снимок экрана, при этом панель с кнопками не будет на нём отображена. Сброс отменяет действия и/или возвращает камеру на начальную позицию. Также снизу есть показатель FPS, текущее время и дата. 3. Кнопка ИНФО при нажатии откроет всплывающее окно с системной информацией и технической поддержкой. При нажатии на кнопку support@winnum.ru пользователя будет переносить в почту и начинать письмо в support. 4. Внутри ViewButtons в Объект - Действия будет лежать код самой панели. 5. Чтобы настроить кнопки с видами нужно изменить логику для них в коде. (9-47 строки)
var SView, View1, View2, View3, View4;
var currentAnimation = null;
var isNightMode = false;
var frameCount = 0;
var lastTime = performance.now();
var fps = 0;
// Обновите функции видов для отображения правильных углов
function ScriptSView() {
console.log('Общий вид активирован');
camera.position.set(12.51, 77.63, 66.77);
camera.rotation.set(-52 * Math.PI / 180, 0, 0);
camera.updateProjectionMatrix();
camera.updateMatrix();
// Убираем вызов updateCoordinates()
updateStatus('ОБЩИЙ ВИД', '#00ff00');
}
// Новый функционал: Сброс вида
function resetView() {
if (currentAnimation) {
stopAnimation();
}
ScriptSView();
updateStatus('ВИД СБРОШЕН', '#00ff00');
}
// Новый функционал: АВТООБЛЕТ
function toggleAnimation() {
if (currentAnimation) {
stopAnimation();
updateStatus('АВТООБЛЕТ ОСТАНОВЛЕН', '#ff4444');
} else {
startAnimation();
updateStatus('АВТООБЛЕТ АКТИВЕН', '#00ff00');
}
}
function startAnimation() {
var startTime = Date.now();
var radius = 80;
var height = 60;
var speed = 0.3;
// Сохраняем начальную позицию для возврата
if (!window.originalCameraPosition) {
window.originalCameraPosition = camera.position.clone();
window.originalCameraRotation = camera.rotation.clone();
}
if (currentAnimation) {
requestAnimationFrame(currentAnimation);
}
};
currentAnimation();
}
function stopAnimation() {
currentAnimation = null;
// Возвращаем камеру в исходное положение
if (window.originalCameraPosition) {
camera.position.copy(window.originalCameraPosition);
camera.rotation.copy(window.originalCameraRotation);
camera.updateMatrix();
}
}
// Новый функционал: Скриншот
function takeScreenshot() {
try {
renderer.render(scene, camera);
var imageData = renderer.domElement.toDataURL('image/png');
var link = document.createElement('a');
link.href = imageData;
link.download = 'cnc_screenshot_' + new Date().toISOString().replace(/:/g, '-') + '.png';
link.click();
// Обновление статус бара
function updateStatus(message, color) {
var statusBar = document.getElementById('status-bar');
if (statusBar) {
statusBar.innerHTML = '<span style="color:' + color + '">■</span> ' + message;
statusBar.style.color = color;
}
}
// Обновление времени
function updateTime() {
var now = new Date();
var timeDisplay = document.getElementById('time-display');
var dateDisplay = document.getElementById('date-display');
if (timeDisplay) {
timeDisplay.textContent = now.toLocaleTimeString();
}
if (dateDisplay) {
dateDisplay.textContent = now.toLocaleDateString();
}
}
// Обновление координат камеры
// Альтернатива если THREE.Math.radToDeg не работает
function updateCoordinates() {
var coordDisplay = document.getElementById('coord-display');
if (coordDisplay && camera) {
// Ручное преобразование радиан в градусы
var radToDeg = function(rad) {
return (rad * (180 / Math.PI) + 360) % 360;
};
var euler = new THREE.Euler();
euler.setFromQuaternion(camera.quaternion, 'YXZ');
var pitch = radToDeg(euler.x);
var yaw = radToDeg(euler.y);
var roll = radToDeg(euler.z);
// Агрессивное округление
var x = Math.round(camera.position.x);
var y = Math.round(camera.position.y);
var z = Math.round(camera.position.z);
var fpsDisplay = document.getElementById('fps-display');
if (fpsDisplay) {
fpsDisplay.textContent = 'FPS: ' + fps;
// Меняем цвет в зависимости от FPS
if (fps < 30) {
fpsDisplay.style.color = '#ff4444';
} else if (fps < 50) {
fpsDisplay.style.color = '#ff9900';
} else {
fpsDisplay.style.color = '#00ff00';
}
}
}
requestAnimationFrame(updateFPS);
}
// Горячие клавиши
function initHotkeys() {
document.addEventListener('keydown', function(event) {
switch(event.key.toLowerCase()) {
case 'a': toggleAnimation(); break;
case 'n': toggleNightMode(); break;
case 'p': takeScreenshot(); break;
case 'r': resetView(); break;
case 'i': showInfoModal(); break;
case '1': ScriptSView(); break;
case '2': ScriptView1(); break;
case '3': ScriptView2(); break;
case '4': ScriptView3(); break;
}
});
}
В приложении Цифровой двойник довольно часто используются кнопки. Я решил создать панель с кнопками для 3D в стиле ЧПУ-станка. В данном посте я хотел бы рассказать о том как добавить их себе в 3D-сцену и настроить. Они смогут помочь стилизовать сцену и улучшить восприятие.
1. Скачать файл ViewButtons (во вложении) и импортировать себе в сцену (Файл - Импорт). 2. Если мы начнём воспроизведение сцены они появятся в правом нижнем углу. Здесь можно увидеть название цеха и 5 кнопок: 4 кнопки с видами, кнопка INFO. Каждая кнопка с видом при нажатии будет перемещать камеру в заданное место. 3. Кнопка INFO при нажатии откроет всплывающее окно с системной информацией и технической поддержкой. При нажатии на кнопку support@winnum.ru пользователя будет переносить в почту и начинать письмо в support. 4. Внутри ViewButtons в Объект - Действия будет лежать код самой панели. 5. Чтобы настроить кнопки с видами нужно изменить логику для них в коде в параметрах по camera.position и camera.rotation. (386-436 строки)
Сегодня хочу рассказать вам одну интересную тему;
Редактор динамических приложений уже содержит в себе множество библиотек для JavaScript, библиотеки это подготовленный заранее набор функций, который позволяет сократить код, сосредоточившись на выполняемой задаче. Вам не нужно самостоятельно отрисовывать график в формате svg, за вас это может сделать встроенный chart.js, d3.js, c3.js.
Тем не менее нельзя предусмотреть заранее все задачи, которые могут решаться с помощью динамических приложений, поэтому я расскажу как действовать, если нужная библиотека или дополнение к ней не нашлись в списке библиотек редактора.
Для начала попробуем загрузить библиотеки online, т.е. предполагается, что пользователь может пользоваться интернетом, библиотеки в этом случае загружаются через ссылку.
1. Добавление через HTML
Любой контейнер для этого подойдет, он загружается асинхронно и перейдет по ссылке, инициализировав библиотеку
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
Это самый простой и работающий способ, главное дождаться загрузки элемента перед использованием библиотеки. Эта ссылка дана для примера JQuery уже установлен в редакторе, скачивать его повторно не нужно.
bootstrap тоже уже установлен, делать это повторно не нужно
Вдруг, выясняется, что у заказчика изолированная сеть и все эти ссылки не работают. Как быть?
Почти все библиотеки можно скачать себе в медиа библиотеку и использовать как обычные JS или css
Перейдите по ссылке самостоятельно и библиотека откроется в браузере в виде сокращенного JavaScript кода.
Библиотеки специально хранятся в сжатом виде без правил оформления и комментариев, часто используются однобуквенные переменные.
Весь текст скопируйте в файл с расширением .js для JavaScript и .css для стилей.
Загрузите файлы в медиа библиотеку и используйте ссылки на эти файлы вместо ссылок cdn.
В медиа библиотеке предусмотрена отдельная кнопка "скопировать ссылку". Сама ссылка выглядит так:
Для того чтобы избежать проблем при импорте вашего приложения на другие платформы не стоит использовать прямые ссылки на папки с файлами, они могут измениться.
Ранее мы уже делали это, когда инициализировали наш manifest.js. Повторим еще раз.
Код:
$(window).ready(function(){
loadResources(true);
});
function loadResources(initial){
var oid = '';
var appid = '';
baseSdkUtils.the = {};
baseSdkUtils.service.WNFactory.getPersistable(baseApplicationDesignerUtils.getAppid(), function(data){
if ( !baseSdkUtils.isSuccess(data) ){
alert('Error: ' + baseSdkUtils.decode(data.innerHTML));
console.warn(baseSdkUtils.decode(data.innerHTML));
return;
}
/******* Случай, когда код выполняется в приложении ******/
if ( data.getElementsByTagName('item').length > 0 ){
oid = data.getElementsByTagName('item')[0].getAttribute('ApplicationInfo__classNameA15');
oid += ':';
appid = data.getElementsByTagName('item')[0].getAttribute('ApplicationInfo__idA15');
oid += appid;
}
/****** Случай, когда код выполняется в редакторе ********/
else{
oid = baseApplicationDesignerUtils.oid;
appid = oid.split(':')[1];
}
/****** Создание структуры для хранения атрибутов ********/
baseSdkUtils.the.appoid = baseApplicationDesignerUtils.getAppid();
baseSdkUtils.the.appid = appid;
baseSdkUtils.the.oid = oid;
baseSdkUtils.the.media_url = '/' + baseSdkUtils.appId + '/resources/themes/current/images/app/ui/designer/media/' + appid + '/';
Вы можете не делать все это повторно, у вас уже есть переменная baseSdkUtils.the.media_url, которая содержит весь путь до библиотеки, остается указать файл, если он в отдельной папке, это тоже нужно добавить к названию файла.
Помимо описанных способов вы можете так же использовать ajax, как и в примере, если запускать что либо из библиотеки сразу не нужно, уберите поле success, асинхронный вызов в данном случае тоже не обязателен, если библиотека инициализируется один раз.
Будьте осторожны, некоторые библиотеки влияют на скорость открытия страницы, поищите, возможно есть сокращенная версия библиотеки, которой вам будет достаточно.
Создавайте свои библиотеки с набором нужных функций и стилей, чтобы использовать их в дальнейших разработках.
Вы можете создать заготовку html элемента неограниченного размера. Добавьте туда все стили и скрипты, которые участвуют непосредственно в элементе и работают локально и переносите ее в новые приложения, вам больше не понадобится создавать весь интерфейс заново:
Для начала, хотел бы предупредить, что хотя ниже и упоминается конкретно модуль Испытания, но этот "Лайфхак" пригодится в любом другом месте. Поэтому смело окунайтесь в пост.
WINNUM SDK предлагает довольно обширное количество различных методов для разработки приложений. Однако, я недавно натолкнулся на некоторую проблему. В модуле Winnum Испытания мне хотелось получить информацию по испытаниям и вообще пользоваться стандартными отчетами у себя. Испытания предлагают два варианта нахождения прошедших испытаний:
1. Отчет об испытаниях;
2. Отчет по оборудованию;
Второй вариант легко повторить у себя в приложении, для этого не обязательно получать весь отчет. В поле ввода вводится стенд, а так же время, по которому и производится поиск. Не очень трудно догадаться, что выполнив функцию getSignal и указав конкретный сигнал, а можно и сразу несколько - мы получим все данные по прошедшим испытаниям, и даже больше чем в отчете, если нам это надо.
Сложность возникает, когда мы смотрим на Отчет об испытаниях.
Отчет отрабатывает довольно быстро а данные из этой таблицы были бы очень полезны в моем приложении.
Я хотел найти путь, по которому смог бы что-то получить из этой таблицы, но решение оказалось простым и изящным.
Далее просто описываю путь, повторяйте за мной
Нажимаем F12
Переходим на вкладку Сеть
Далее (выражаясь общими словами) выполняем запрос:
Далее нажимаю два раза на запрос в WinnumAjaxAuthHandler и вижу:
т.е. если в браузере отправить такой запрос, то я сразу получу все данные для этой таблицы. И даже ссылки и картинки присутствуют в ответе.
Осталось дело за малым - научиться работать с Ajax:
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // или response.text() если данные не в JSON
})
.then(data => {
console.log('Полученные данные:', data);
// Здесь вы можете работать с полученными данными
})
.catch(error => {
console.error('Произошла ошибка:', error);
});
});
Запускаем, нажимаем:
Теперь у вас есть данные и вы можете соорудить таблицу, графики, мини-отчеты, использовать дальше для любых своих целей.
Всем привет!
Если мы хотим создать еще одну страницу динамического приложения, мы заранее продумываем где и когда будем ее открывать. Самый простой способ - кнопка, но возможны и более сложные многоходовочки.
Итак: создали кнопку, зашли в настройки и создаем событие: "Переход на страницу" и получаем готовое решение, которое работает в большинстве случаев:
Интерфейс редактора динамических приложений даже подсказывает нам название новой страницы и автоматом заменяет uuid - в коде ничего не надо править.
Проблема возникает когда мы создаем несколько дистрибутивов одного и того же приложения, с теми же самыми страницами и импортируем его снова и снова, например таким образом сохраняем старые версии, чтобы опробовать разные варианты. Или просто хотим сохранить все версии для отслеживания изменений и подписываем все варианты приложений.
В этом случае окажется, что когда импортируется новая страница приложения, то создаются для страниц новые uuid. Для главной страницы тоже.
Можем обойти эту проблему, создав более сложный переход, но универсальный, бонусом мы сможем еще и что-то передать на другую страницу прямо в адресной строке -
/*************************************************/
function thisGoToPage( param, page_title ){
baseSdkUtils.service.WNDynamicApplicationHelper.getPageByTitle(
baseSdkUtils.the.appoid,
page_title,
function( data ){
var item = data.getElementsByTagName('item')[0];
var oid = item.getAttribute('elementId');
window.location.href = '/Winnum/views/pages/app/dynamic/dgw.jsp?puuid=' + oid + '&appid=' + baseSdkUtils.the.appoid + param + '&mode=yes' ;
});
}
Функция thisGoToPage принимает 2 аргумента - параметр в виде строки, при этом можно писать несколько параметров через &, например &interval=day, он появится в адресной строке, откуда его легко прочитать и использовать.
Ну и название страницы, тоже строковая переменная.
baseSdkUtils.the.appoid - это переменная уже есть у вас с нужным значением, если вы подключали медиа библиотеку стандартным скриптом. Если нет - то посмотрите на адресную строку в вашем редакторе - oid - это он и есть.
Теперь uuid и другие временные переменные не употребляются при переходе на страницу, у значит переход сработает всегда.
Если вы хотите, чтобы все подсказки исчезли на всей странице, то удалите их сразу после загрузки страницы. Так вы сразу избавитесь от всплывающих надписей, таких как "Контейнер(Пустой)" и т.д.
В редакторе лучше переименовывать эти атрибуты, они помогают пользоваться поиском на странице в редакторе:
По этой же причине они появятся снова после перезапуска, если их стереть из поля ввода атрибута.
А уже при запуске приложения их можно удалить все одной командой, например так:
Код:
function loadResoursesProcessor(){
document.querySelectorAll('[title]').forEach(element => element.removeAttribute('title'));
// Остальной код
}
Сегодня мы будем делать график загрузки оборудования полностью своими руками:
К этому графику можно добавить временную шкалу и сделать его полноценным отчетом.
У данного метода есть как преимущества, так и недостатки.
Главное преимущество - это точность расположения элементов. Кроме того самостоятельное рисование позволяет создать абсолютно любой график с любыми временными промежутками и отрезками, варьировать размеры и цвета, а так же дает понимание того как этот график строится.
Недостаток в том, что все погрешности измерений, если они предусмотрены - видны на таком графике, т.к. он привязывается ко временным отрезкам, а не относительным величинам, можно увидеть небольшие изменения тегов, в 1 пиксель, из-за чего будут сливаться цвета.
В этой функции мы используем " getPriorityTagDuration " для получения всех временных отрезков тегов исходя из их приоритетности.
В результате мы получаем временные коды в миллисекундах, что дает возможность пересчитать местоположение и ширину закрашенной области. Для подсказки при наведении используем <title> в svg. Кроме того график можно обрезать и растягивать, если нужно разбить его на несколько смен, или сделать подробный график - как в стандартном приложении, для этого не нужно вызывать функцию для каждого часа. она не даст более точный результат, промежутка может быть не видно на графике, только потому, что он меньше пикселя шириной.
Ниже код функции и пример ее вызова:
appid - id приложения, можно увидеть в адресной строке
id - id html элемента, куда вставлять график
from - время от - указывается в закодированном виде, можно использовать baseSdkUtils.encode("20.02.2025 00:00:00");
to - baseSdkUtils.encode("20.02.2025 23:00:00");
product - id продукта, т.е. станка - oid - в карточке станка в адресной строке - "WNProduct"
function createBarCart(appid, id, from, to, product){
var fromYMDformat = from.split("%")[0].split(".")[2] + "-" + from.split("%")[0].split(".")[1] + "-" + from.split("%")[0].split(".")[0];
var toYDMformat = to.split("%")[0].split(".")[2] + "-" + to.split("%")[0].split(".")[1] + "-" + to.split("%")[0].split(".")[0];
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
var dd = yesterday.getDate();
var mm = yesterday.getMonth() + 1;
var yyyy = yesterday.getFullYear();
if(dd<10){ dd = "0" + dd;}
if(mm<10){ mm = "0" + mm;}
var thisday = dd+"."+mm+"."+yyyy;
var today = new Date();
var td=today.getDate();
var tm = today.getMonth() + 1;
var ty = today.getFullYear();
var todaydmy = ty+"-"+tm+"-"+td;
var realtoday = td+"."+tm+"."+ty;
var yesterdaydmy = thisday.split('.')[2] + "-" + thisday.split('.')[1] + "-" + thisday.split('.')[0];
var startOfDayY = new Date(baseSdkUtils.decode(from).split(".")[2].split(" ")[0], baseSdkUtils.decode(from).split(".")[1] - 1, baseSdkUtils.decode(from).split(".")[0], baseSdkUtils.decode(from).split(" ")[1].split(":")[0], baseSdkUtils.decode(from).split(" ")[1].split(":")[1], baseSdkUtils.decode(from).split(" ")[1].split(":")[2]);
var startOfDayMs = Number(startOfDayY.getTime());
var endtOfDayY = new Date(baseSdkUtils.decode(to));
var endOfDayMs = endtOfDayY.getTime();
var arrayForDraw = [];
var tagColorSet = {};
baseSdkUtils.service.WNApplicationHelper.getTag(appid,function(data){
for (var i = 0; i < data.getElementsByTagName('item').length; i++){
var item = data.getElementsByTagName('item')[i];
var tagDescription = baseSdkUtils.decode(item.getAttribute('tagName'));
var tagColor = baseSdkUtils.decode(item.getAttribute('tagColor'));
tagColorSet[tagDescription] = tagColor;
}
baseSdkUtils.service.WNApplicationTagHelper.getPriorityTagDuration(appid, product, fromYMDformat, toYDMformat, function(data){
if ( baseSdkUtils.isSuccess(data) ){
var str = '<svg viewBox="0 0 100 2">';
for (var i = 0; i < data.getElementsByTagName('item').length; i++){
var item = data.getElementsByTagName('item')[i];
var timems = item.getAttribute('timeData');
var tagName = baseSdkUtils.decode(item.getAttribute('TAG'));
var startTime = baseSdkUtils.decode(item.getAttribute('START'));
var endTime = baseSdkUtils.decode(item.getAttribute('END'));
var now = new Date();
var nowMs = now.getTime();
var x1 = (Number(timems.split(',')[0]) - startOfDayMs)/864000;
if(x1 < 0) x1 = 0;
}
var start = baseSdkUtils.decode(item.getAttribute('START'));
var end = baseSdkUtils.decode(item.getAttribute('END'));
var serial = baseSdkUtils.decode(item.getAttribute('SERIAL_NUMBER'));
var duration = baseSdkUtils.decode(item.getAttribute('DURATION'));
Структура <SVG> дает возможность построить векторное изображение, а это значит, что вы можете увеличивать его, приближать или растянуть, качество при этом не упадет.
Вы можете задать свои цвета вместо конструкции ColorTag, назначив их явно, тогда не придется искать их среди возможных тегов этого приложения, но они будут статичны.
График можно обновлять и сделать его онлайн. В самом начале функции определяется множество переменных, это связано с необходимостью получения сегодняшнего и вчерашнего дня, чтобы можно было создавать график за текущую и предшествующую смены. Если вам не нужны привязки к текущим моментам, вы можете пользоваться getPriorityTagDuration сразу, при том время в ней - начала и конца указывать с точностью до суток. Да такой график можно построить за 3 суток сразу. Но помните, что это не длительные отчеты - чем больше данных - тем дольше ждать ответа, а значит между вызовами нужны паузы.
Готовое решение:
1. Скачать файл objectSelector.html в медиабиблиотеку приложения
2. Поместить файл html в контейнер как обычно: $('#id').load(baseSdkUtils.the.media_url + '/ objectSelector.html');
3. Выбранные объекты доступны в элементе: document.getElementById('objectSelectorResult').value
----------------
Теперь подробнее:
objectSelector.html