вступление

Fable - одна из самых захватывающих вещей, которые произошли в экосистеме F # за последние несколько лет. Это мост, который соединил разработчиков F # с огромным миром JavaScript, сделал все, что он предлагает, доступным, в то же время позволяя им писать код на их любимом языке.

Неудивительно, что к настоящему времени практически все, кто использует F # для развлечения и получения прибыли и занимается веб-разработкой, уже пробовали Fable и играли с SAFE Stack, рассказывали / писали в блогах о своем опыте, успешно использовали эти технологии в производстве, создали соответствующие инструменты. и библиотеки или способствовали им. Я тоже пробовал и играл, но пока не сделал ничего полезного для других (ни Fable, ни F # в целом). Адвент-календарь F # казался хорошей возможностью исправить это, оказав на меня реальное давление, поэтому я решил поучаствовать и сделать что-нибудь Fable.

Этот пост - история моего пути: выбор того, что делать, демонстрация результатов, процесса и извлеченных уроков. В конце я делюсь пошаговым руководством о том, как сделать что-то подобное, а также ссылками на полезные ресурсы. Итак, если вам не хочется читать длинную часть рассказа, не стесняйтесь пропустить ее и перейти к «Результаты», а затем к «Хотите сделать что-то подобное? Вот краткое руководство ’.

Выбор темы и начало работы

Чтобы немного подкрепить свой выбор. Я какое-то время интересовался компьютерной графикой, хотя и не ботанистым, зная математику, а скорее в стиле компьютерной игры с существующими инструментами: я повеселился с 3Ds MAX в В средней школе создал очень простую программу просмотра объектов сетки на C # с использованием Direct3D в качестве промежуточного проекта в колледже, а мой дипломный проект был попыткой восстановить трехмерные объекты на основе их изображений, сделанных под разными углами. После этого прошло 8 лет корпоративной веб-разработки, и трехмерное развлечение стало лишь слабым, но приятным воспоминанием. Так почему бы мне не заняться здесь чем-то связанным с компьютерной графикой? Было бы приятно сделать даже небольшой шаг в этом направлении.

Я начал поискать в Google и наткнулся на репозиторий fable-graphics, где участники создали привязки Fable на основе определений TypeScript для множества существующих библиотек CG JavaScript и портировали некоторые примеры с JS на F #. Я заметил, что репозиторий содержит привязки к Three.js, 3D-библиотеке JavaScript, но не имеет реализованных примеров для нее. Я подумал: Отлично! Затем я перенесу несколько примеров с JS на F # с помощью Fable!

Я больше смотрел на Three.js, и это поразило меня. Поскольку я какое-то время внимательно не следил за развитием компьютерной графики, у меня было лишь смутное представление о том, что возможно сейчас, в 2018 году. Ух ты! Взгляните, например, на этот образец. Или этот. Все это работает в браузере без задержек, и для создания таких вещей не нужно много кода! Мне это кажется даже более удивительным, чем Wi-Fi в самолетах.

Клонировав репозиторий, я попытался собрать и запустить код, но потерпел неудачу, а затем обнаружил, что репозиторий практически мертв. Комментарии в выпуске GitHub привели меня к новым репозиториям, одно с новыми привязками, а второе с Образцами Pixi (это аккуратная библиотека 2D-графики). Однако новых привязок Three.js не было, поэтому мне нужно было сгенерировать их самостоятельно, и это было нормально, поскольку я знал, что существует инструмент, который мог бы выполнять эту работу.

TS 2 Fable спешит на помощь! Если вы установите этот инструмент, найдите определения типов TypeScript для своей JS-библиотеки здесь, в репозитории Определенного Типа, загрузите их и загрузите в инструмент, он сгенерирует объявления интерфейса F #. Они позволят вашему коду F # понять, что эти типы существуют, и дадут достаточно знаний для Fable, чтобы он мог преобразовать ваш код F #, который использует библиотеку, в код JavaScript, который правильно ссылается на вещи из библиотеки.

Открыв Определения TS для Three.js, я немного испугался. Тонны файлов. Что делать, если ts2fable не может генерировать несколько файлов с взаимозависимостями? К счастью, может, так как не так давно. Ура!

Установив ts2fable и загрузив определения типов в папку «three», я выполнил следующую команду:

ts2fable three/index.d.ts TestCompile/ThreeJs.fs -e three

Вы, наверное, догадались, что файл index.d.ts является файлом «точки входа». В результате я получил огромный бандл-файл с 6К + строками кода F #. Я включил этот файл в тестовый проект, и он успешно скомпилировался после того, как я внес незначительное исправление, добавив «конец интерфейса» к пустым определениям интерфейсов. Довольно круто! Однако я столкнулся с проблемой при попытке запустить приложение. Ошибки были следующего вида:

FABLE: Cannot have two module members with same name: Camera

И еще много всего в этом роде. Оказалось, что это ошибка BABEL (или ограничение ¯ \ _ (ツ) _ / ¯). Я просмотрел определения типов Three.js и обнаружил, что существует модуль Core, а типы из него используются в других модулях с псевдонимами, что, по-видимому, приводит к беспорядку, когда происходит Fable - ›BABEL -› JS. Я подумал: «Хорошо, а что, если я найду простейший возможный образец и буду использовать только часть Core?» Прокомментировал все, кроме Core - нет, не будет работать, так как другие модули зависят от Core, а Core зависит от других модулей - прекрасно ! И, о, вот почему пространство имен в моем огромном сгенерированном файле определений F # объявлено как rec, что, кстати, приводит к тому, что VS Code сходит с ума и время от времени зависает (я не виню это! 6K строк, rec…). Я закончил тем, что бросил этот файл в суть, чтобы выделить синтаксис, и использовал его для поиска вещей, так как я все равно не собирался оставлять его в проекте.

Учитывая вышесказанное, моей целью в этом приключении стало следующее:

  • Возьмем не самый крутой, а самый простой пример, который использует только Three.js и ничего больше, потому что время - ›0
  • Глядя на код JavaScript в примере, используйте выходной файл ts2fable в качестве ссылки и найдите только те типы, которые мне нужны для образца, только с нужными мне полями и функциями, и создайте свою собственную минималистичную версию файла привязок F #
  • Переносите JavaScript на F #, сохраняя все как можно ближе к исходному образцу для прозрачности, и пусть все работает.

Полученные результаты

В итоге я выбрал следующий образец: camera_array. Идея состоит в том, чтобы отобразить сцену, в которой N экземпляров 3D-объекта визуализируются в анимированной манере, поворачиваясь. Также есть базовая настройка света и фоновая текстура, которая помогает продемонстрировать тень, отбрасываемую объектом при его повороте при освещении светом.

Вывод, визуальная часть

Я немного изменил образец для развлечения. Просто изменили размеры цилиндров, чтобы они превратились в конусы (это поцелуи Херши!), И изменили используемые цвета. Это настолько "адвентский", насколько я могу его получить без использования текстур или нестандартных моделей. Конфеты и рождественские краски!

Если вы не видели эти золотые поцелуи Херши в реальной жизни, вот как они выглядят:

Вывод, часть кода

Код F # перенесенного образца был максимально приближен к исходному образцу JavaScript. Я признаю, что это не идиоматично, это в значительной степени противоположно «F # code we love» (не показывайте этот код своим детям! Пожалуйста) и содержит некоторые уловки, которые я рассмотрю в следующем разделе. Единственное, что хорошо в этом коде, так это то, что он удобен для обучения, поскольку позволяет просматривать исходный код JS, F # и сгенерированный JS и легко видеть, что происходит.

Я поместил сюда код образца: w0lya / fable-three

Проект основан на минимальном шаблоне для Fable 2, поэтому, если вы будете следовать инструкциям в шаблоне, у вас все должно работать.

А вот вывод ts2fable в виде Fable.Import.Three.fs

Процесс

После того, как я выполнил всю настройку и подготовку, сама реализация оказалась довольно простой и не такой полной приключений, как начало работы. Просто нужно было выяснить несколько вещей.

Поскольку исходная логика JavaScript реализована в императивном порядке, мне пришлось следовать этому стилю в F #, чтобы «имитировать» его.

  1. Первое, с чем я столкнулся, - это возможность создавать объекты с помощью конструкторов с параметрами. Если бы у меня был тип, а не определения интерфейса, это было бы довольно просто. Для интерфейсов я сделал следующее:
// Out initial type as-is.
type [<AllowNullLiteral>] Vector4 =
abstract x : float with get,set
abstract y : float with get, set
abstract z  : float with get, set
abstract w  : float with get, set
// Add this as a constructor with optional parameters:
and [<AllowNullLiteral>] Vector4Type =
[<Emit("new THREE.$0($1...)")>] abstract Create:  ?p1:obj * ?p2:obj * ?p3:obj * ?p4:obj -> Vector4
// Further in the type definition file, add a type with global exports for 'constructing' your objects.
type [<Erase>] Globals =
    [<Global>] static member Vector4 with get(): Vector4Type = jsNative and set(v: Vector4Type): unit = jsNative

Учитывая это, я могу написать следующий код:

subcamera.bounds <- Globals.Vector4.Create( (x / amount, y / amount, size, size )

И в результате получу следующий JavaScript, который мне и нужен:

subcamera.bounds = new THREE.Vector4(x / amount, y / amount, size, size);

Вы, наверное, заметили атрибуты [‹Emit›], [‹Erase›] и [‹Global›] в коде. Они довольно интуитивно понятны: первый переопределяет, какой JS-код создается на основе вашего F # кода, второй отмечает вещи, которые следует игнорировать при генерации JS, а третий обозначает вещи, которые должны быть глобально доступны в вашем результирующем JS. Чтобы узнать об этом больше, просмотрите ссылки, которыми я делюсь в конце сообщения.

2. Вторая вещь, которая вызвала небольшую неприятность, заключалась в том, что в исходном JS есть несколько глобальных переменных. К сожалению, мне пришлось преодолеть эту проблему, используя не самый приятный вид взлома Emit. Моя проблема заключалась в том, что если я объявлю глобальные переменные таким образом,

let globalCamera = Globals.ArrayCamera.Create()

В итоге я получаю это в JavaScript:

const globalCamera = new THREE.ArrayCamera()

Если я добавлю «изменяемый», глобальное объявление будет рассматриваться как функция, и если я попытаюсь установить для него какие-то свойства, я получу

globalCamera().propertyname = ...

чего я не хочу.

Чтобы преодолеть это, мне пришлось два раза определить глобальные вещи: один, чтобы сделать JS счастливым и на самом деле иметь там vars, а второй - сделать счастливым F #:

// 1) To actually have vars in JS
[<Emit("var globalScene, globalMesh, globalCamera, globalRenderer;")>]
let globalVarsDeclaration: unit = jsNative
// 2) For F# not to be sad. [<Erase>] helps to exclude this stuff when generating JS.
let [<Erase>] globalCamera = Globals.ArrayCamera.Create()
let [<Erase>] globalScene = Globals.Scene.Create()
let [<Erase>] globalRenderer =  Globals.WebGLRenderer.Create()
let [<Erase>] globalMesh = Globals.Mesh.Create()

С указанным выше я получаю правильное объявление, но если я ссылаюсь на них и пытаюсь установить какое-либо свойство для этих объектов, в выводе JavaScript я получаю следующее:

Object(_fable_fable_core_2_0_10_Util__WEBPACK_IMPORTED_MODULE_0__["equals"])(globalScene, scene)

И, конечно, не работает :) Это привело меня к еще более сомнительным взломам. Я объявил локальную камеру, сцену, сетку и средство визуализации внутри функции init (), в отличие от глобального, локального 'let mutable ' объявление приводит к разумному JS и позволяет устанавливать свойства по мере необходимости. Правильно настроив локальные объекты, я назначил их глобальным переменным по 2 раза каждый, один через Emit (для JS), одно простое присвоение (для F #). Я не добавляю здесь фрагменты кода для этой части, чтобы не перегружать сообщение, но если вам интересно, все это находится в файле App.fs. Хотя я почти уверен, что существует более приятный способ преодолеть эти проблемы.

Помимо этого, в определениях типов были биты, которые нужно было немного подправить, например отсутствует метод, требуются необязательные параметры или необходим базовый тип. Но в целом я смог заставить все работать относительно безболезненно.

Инструменты

Здесь я просто перечислю инструменты, которые использовал. Я все делал в Linux (Ubuntu 18.04), поэтому это повлияло на некоторые варианты.

  • Код Visual Studio с Ionide
  • GitKraken
  • Инструменты разработчика Chromium
  • Gitzip для получения небольших фрагментов больших репозиториев (например, трех привязок Typescript)
  • Ts2fable, как терминальный инструмент, так и онлайн
  • новый Fable 2.0 Online REPL
  • Legacy Fable REPL, потому что новый REPL выводил то, что меня пугало. например это:
let returnVal$$1 = Mesh$$$$002Ector();
Mesh$$set_geometry$$Z47ACBE6C(returnVal$$1, geometry);

Однако, когда полученный в результате JavaScript стал отлаживаемым, я перестал использовать онлайн-REPL и просто переключился на отладку сгенерированного JS в VS Code с горячей перезагрузкой, что было просто отличным опытом для разработчиков. Кстати, использование Emit для вставки оператора отладчика может быть здесь полезно.

Хотите сделать что-то подобное? Вот краткое руководство

Если вы любите приключения и хотели бы портировать один из более захватывающих примеров Three.js (или сделать то же самое с любой другой библиотекой JS, в которой доступны определения TypeScript), то вот краткое изложение.

Шаги

  1. Найдите определения TypeScript в своей библиотеке здесь и загрузите его с помощью такого инструмента, как gitzip.
  2. Установите ts2fable, в директории с TS запустите команду (см. Readme для нужного для вашего случая)
  3. Создайте проект F #, например скопировав и используя минимальный шаблон для Fable 2. Добавьте туда файл F # TS и ссылку в .fsproj. Вы можете либо проверить правильность файла в проекте, создав и запустив его, либо бросить файл в оперативный REPL и попробовать запустить его.
  4. Если вам не повезло, вам придется создавать свои собственные определения типов вручную, настраивать их для поддержки конструкторов, исправлять выбросы JS и т. Д. :)

5. Не забудьте включить библиотеку JavaScript в виде пакета NuGet или ссылки на CDN.

6. Напишите свой код F # в App.fs и убедитесь, что он преобразован в рабочий JS :)

Полезные ссылки

Возможные дальнейшие действия

Возможности безграничны. Для меня или для тех, кто тоже был в восторге от этого, вот несколько идей, которые стоит рассмотреть:

  • Создайте полноценное БЕЗОПАСНОЕ приложение на основе стека с потрясающими возможностями 3D.
  • Добавьте взаимодействие с пользователем, сделайте приложение настраиваемым: измените количество, цвет и многое другое. Это могло бы даже стать Додзё?
  • Измените логику и сделайте код F # больше похожим на код F #, который мы любим, чем на код, который нам не нравится.
  • Подумайте, как преодолеть ошибку, связанную с именами Babel. как я могу автоматически повторно сгенерировать определения типов, чтобы у них не было этой проблемы, и в то же время вывод JS можно было бы использовать мгновенно, без необходимости взламывать пространство имен. Это было бы замечательно, поскольку это «разблокировало бы» всю библиотеку Three.js для использования из F #!

Конец.

Будем очень благодарны за любые отзывы / вопросы / комментарии / критику / троллинг :) Не стесняйтесь сообщать мне об ошибке здесь или в Твиттере.