К содержимому

Three.js + DSK ​

Подключим тот же инспектор DSK к настоящей 3D-сцене. Three.js отвечает за объекты, освещение, определение объекта под указателем и камеру. DSK добавляет инпуты X/Y и поворота, линейки, направляющие и маркер комментария. Ортографическая камера позволяет точно сопоставить двумерные линейки с координатами сцены.

Сцена загружается вместе со страницей. Перетащите любой объект, чтобы выбрать и переместить его, или измените значения в инспекторе. Перетаскивание пустого места двигает камеру; с зажатым Shift можно начать такое движение поверх объекта. После клика по сцене колесо меняет масштаб относительно указателя. Уместить показывает оба объекта, Сбросить возвращает исходное состояние. Вытяните направляющую из линейки; выберите её и нажмите Delete для удаления.

Когда холст в фокусе, стрелки перемещают выбранный объект на одну единицу, Shift + стрелка - на десять, +/- меняют масштаб, F умещает сцену, а Escape снимает выделение. Пока холст не получил фокус, страница прокручивается как обычно. Изменения хранятся только в памяти. Для примера нужна поддержка WebGL.

1. Установите зависимости ​

В приложении Vite с TypeScript:

sh
npm i @flowscape-ui/dsk three@0.186.1
npm i -D @types/three@0.186.0

Three.js не требует UI-фреймворка. Адаптер использует обычные DOM-элементы и TypeScript. Общая оболочка также применяется в других примерах интеграции.

text
index.html
src/
  main.ts
  shared/
    example-shell.ts
    example-shell.css
  three/
    mount-example.ts
    style.css

2. Подготовьте контейнер и запустите сцену ​

html
<!-- index.html -->
<div id="example"></div>
<script type="module" src="/src/main.ts"></script>
ts
// src/main.ts
import { mountExample } from './three/mount-example.ts';

const root = document.querySelector<HTMLElement>('#example');
if (!root) throw new Error('Missing example root');
const dispose = mountExample(root);

// Call dispose() when your route or component unmounts.
if (import.meta.hot) import.meta.hot.dispose(dispose);

Полные файлы находятся в просмотрщике проекта ниже. Оболочка создаёт три инпута DSK над областью высотой 420px, размещает линейки вне рендерера и добавляет отдельный DOM-слой для маркера. Монтируйте Three.js только в shell.surface, чтобы не заменять инспектор или линейки.

3. Добавьте объекты в настоящую сцену ​

Основной файл создаёт WebGLRenderer, сцену, ортографическую камеру и два объекта. У каждого объекта есть внешняя группа для редактируемого преобразования и внутренняя геометрия с постоянным наклоном в 3D:

ts
// Inside src/three/mount-example.ts
const box = new Group();
const boxMesh = new Mesh(
  new BoxGeometry(160, 110, 50),
  new MeshStandardMaterial({ color: '#ff0073', roughness: 0.38 }),
);
boxMesh.rotation.set(0.3, -0.36, 0);
box.add(boxMesh);
box.position.set(150, -130, 0);
scene.add(box);

Наклон геометрии делает глубину блока заметной. Изменение поворота в инспекторе вращает внешнюю группу вокруг оси Z экрана, сохраняя этот наклон. Белый BoxHelper обозначает выбранную группу. Проекцию определяют камеры Three.js, а DSK не участвует в отрисовке объектов.

4. Передайте камеру линейкам ​

В Three.js положительное направление Y обычно идёт вверх. Здесь инспектор использует направление вниз, как и в остальных редакторских примерах. Камера смотрит строго вдоль оси Z, поэтому преобразование точное:

ts
// centerX / centerY use the inspector's downward-positive Y coordinates.
camera.position.set(centerX, -centerY, 1000);
camera.lookAt(centerX, -centerY, 0);
camera.zoom = zoom;
camera.updateProjectionMatrix();

shell.setCamera({
  panX: width / 2 - centerX * zoom,
  panY: height / 2 - centerY * zoom,
  zoom,
});

При масштабе 1 единица сцены занимает один CSS-пиксель. Точка (x, y) из инспектора отображается внутри области просмотра в (x * zoom + panX, y * zoom + panY). Ширина и высота относятся к этой области без линеек и панели инструментов. Плотность пикселей устройства влияет на буфер рендеринга, но не на координаты линеек.

Почему используется ортографическая проекция

Камера остаётся перпендикулярной плоскости XY. У объектов есть глубина и освещение, но перспектива и орбитальное вращение камеры отключены. Перспективную или орбитальную камеру нельзя описать единым двумерным смещением и масштабом для всех глубин. Для такого сценария проецируйте точки маркеров через Vector3.project(camera) и определите рабочую плоскость, которую измеряют линейки.

5. Свяжите выделение с инпутами DSK ​

Оболочка слушает обычные события input компонентов DSK. Для изменения выбранной группы достаточно небольшого преобразования координат:

ts
shell.onTransform((field, value) => {
  if (!selected || !Number.isFinite(value)) return;
  if (field === 'x') selected.position.x = value;
  if (field === 'y') selected.position.y = -value;
  if (field === 'rotation') selected.rotation.z = (-value * Math.PI) / 180;
  render();
});

Обратное обновление выполняется после каждого изменения сцены:

ts
shell.setSelection(
  selected
    ? {
        name: selected.name,
        x: selected.position.x,
        y: -selected.position.y,
        rotation: (-selected.rotation.z * 180) / Math.PI,
      }
    : null,
);

X/Y обозначают центр группы, а не левый верхний угол её видимых границ. В инспекторе поворот задан градусами по часовой стрелке, в Three.js - радианами. При пустом выделении поля отключаются. Программное присваивание значений DSK не создаёт события, а оболочка не перезаписывает инпут во время его редактирования.

При нажатии указателя Raycaster.setFromCamera проверяет пересечение луча с реальными объектами. Соответствующая группа становится выбранной. Захват указателя сохраняет перетаскивание при выходе за границы холста; отпускание или отмена его останавливают. Клавиатура изменяет те же свойства объекта. Подробнее в руководстве по выбору объектов.

6. Прикрепите комментарий и ограничьте перерисовки ​

Комментарий всегда следует за розовым блоком, даже когда выбран фиолетовый объект:

ts
scene.updateMatrixWorld(true);
const bounds = new Box3().setFromObject(box);
shell.setMarker(bounds.max.x, -bounds.min.y);

При этой фронтальной ортографической камере крайняя правая и нижняя координаты мировой ограничивающей рамки дают стабильную точку привязки. Маркер - DOM-аннотация DSK, а не геометрия Three.js. Направляющие служат визуальной подсказкой и не включают привязку объектов.

Рендеринг происходит только после преобразования, изменения камеры или размера контейнера. Постоянного цикла анимации нет. ResizeObserver согласует буфер рендеринга и границы камеры с контейнером. Плотность пикселей ограничена значением 2, чтобы ограничить размер GPU-буфера.

7. Освободите ресурсы при выходе ​

Возвращаемая функция очистки отключает наблюдатель размеров, удаляет обработчики указателя и клавиатуры, освобождает геометрии и материалы, завершает работу рендерера и WebGL-контекста, затем удаляет оболочку DSK. Ресурсы GPU в Three.js требуют явной очистки даже после удаления холста из DOM. Подробнее в руководстве по очистке ресурсов.

При SSR импортируйте и монтируйте модуль только после появления контейнера на клиенте:

ts
// Run inside your framework's client mount hook.
const { mountExample } = await import('./three/mount-example.ts');
const dispose = mountExample(host);
// Register dispose with that component's unmount hook.

Пример реализует базовое выделение и преобразования, а не полноценный 3D-редактор с историей отмены. Можно сохранить оболочку DSK и добавить свою модель документа, инструменты преобразования и сохранение.

8. Изучите и скачайте проект ​

Ниже доступны исходники, включая общую оболочку и полный адаптер Three.js. Скачайте ZIP, распакуйте его, откройте терминал в dsk-three-example, затем выполните npm install и npm run dev. Команда npm run build проверит типы и соберёт приложение.

Нужны Node.js 22.12+ и интернет для установки зависимостей. Архив содержит собранную копию DSK в vendor, поэтому запускается независимо от репозитория DSK. Three.js распространяется по лицензии MIT.

ПОЛНЫЙ ПРОЕКТdsk-three-example
Скачать ZIP
main.ts
srcmain.ts

Автоматическая загрузка и очистка при навигации или перезагрузке.

import './page.css';

const root = document.querySelector<HTMLElement>('#example');
const loading = document.querySelector<HTMLElement>('#loading');
if (!root || !loading) throw new Error('Missing example elements');
const host = root;
const status = loading;
let active = true;
let dispose: (() => void) | undefined;
const observer = new MutationObserver(() => {
  if (host.dataset.ready === 'true') {
    status.hidden = true;
    observer.disconnect();
  }
});
observer.observe(host, { attributes: true, attributeFilter: ['data-ready'] });

// Load this engine only after the browser has created its host.
async function start() {
  try {
    const { mountExample } = await import('./three/mount-example.ts');
    if (!active) return;
    const cleanup = await mountExample(host, document.documentElement.lang === 'ru');
    if (!active) { cleanup(); return; }
    dispose = cleanup;
    if (host.dataset.ready === 'true') status.hidden = true;
  } catch (error) {
    if (!active) return;
    status.textContent = 'The example could not load. Check the console and reload the page.';
    console.error(error);
  }
}
void start();

// In a routed application, call cleanup when leaving this page.
function cleanup() {
  active = false;
  observer.disconnect();
  dispose?.();
}
if (import.meta.hot) import.meta.hot.dispose(cleanup);
Только чтениеTS · UTF-8
Распакуйте ZIP и выполните в папке проекта:npm installnpm run dev