├── home
├── public
│ ├── editors
│ │ └── main
│ │ │ ├── manifest.json
│ │ │ └── icon.svg
│ └── locales
│ │ ├── en
│ │ ├── plugin.json
│ │ └── home.json
│ │ ├── it
│ │ ├── plugin.json
│ │ └── home.json
│ │ ├── zh-TW
│ │ ├── plugin.json
│ │ └── home.json
│ │ ├── pt-BR
│ │ ├── plugin.json
│ │ └── home.json
│ │ └── ru
│ │ ├── plugin.json
│ │ └── home.json
├── index.d.ts
├── editors
│ └── main
│ │ ├── links.ts
│ │ ├── index.pug
│ │ ├── index.styl
│ │ └── index.ts
├── tsconfig.json
└── package.json
├── settings
├── public
│ ├── editors
│ │ └── settings
│ │ │ ├── manifest.json
│ │ │ └── icon.svg
│ └── locales
│ │ ├── en
│ │ ├── plugin.json
│ │ └── settingsEditors.json
│ │ ├── it
│ │ ├── plugin.json
│ │ └── settingsEditors.json
│ │ ├── pt-BR
│ │ ├── plugin.json
│ │ └── settingsEditors.json
│ │ └── ru
│ │ ├── plugin.json
│ │ └── settingsEditors.json
├── index.d.ts
├── tsconfig.json
├── settingsEditors
│ └── SettingsEditorPlugin.d.ts
├── editors
│ └── settings
│ │ ├── index.pug
│ │ ├── index.styl
│ │ └── index.ts
└── package.json
├── documentation
├── public
│ ├── editors
│ │ └── documentation
│ │ │ ├── manifest.json
│ │ │ └── icon.svg
│ └── locales
│ │ ├── zh-TW
│ │ └── plugin.json
│ │ ├── ru
│ │ └── plugin.json
│ │ ├── th
│ │ └── plugin.json
│ │ ├── en
│ │ └── plugin.json
│ │ ├── pt-BR
│ │ └── plugin.json
│ │ └── sv
│ │ └── plugin.json
├── index.d.ts
├── DocumentationPlugin.d.ts
├── tsconfig.json
├── editors
│ └── documentation
│ │ ├── index.pug
│ │ ├── index.styl
│ │ └── index.ts
├── package.json
└── package-lock.json
├── .gitignore
├── README.md
├── three
├── index.d.ts
├── tsconfig.json
├── helpers
│ ├── index.ts
│ ├── GridHelper.ts
│ ├── TransformMarker.ts
│ ├── SelectionBoxRenderer.ts
│ ├── TransformGizmos.ts
│ └── TransformControls.ts
├── package-lock.json
├── package.json
├── mainGulpfile.js
├── helpers.d.ts
├── main
│ ├── main.ts
│ ├── Camera2DControls.ts
│ ├── Camera.ts
│ └── Camera3DControls.ts
└── main.d.ts
├── textEditorWidget
├── index.d.ts
├── data
│ ├── index.ts
│ ├── TextEditorSettingsResource.ts
│ └── textEditorUserSettings.ts
├── public
│ └── locales
│ │ ├── pt-BR
│ │ └── settingsEditors.json
│ │ └── en
│ │ └── settingsEditors.json
├── tsconfig.json
├── settingsEditors
│ ├── index.ts
│ └── TextEditorSettingsEditor.ts
├── package.json
├── widget
│ ├── widget.styl
│ └── widget.ts
├── widget.d.ts
├── package-lock.json
├── operational-transform.d.ts
└── widgetGulpfile.js
└── LICENSE.txt
/home/public/editors/main/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "pinned": true
3 | }
4 |
--------------------------------------------------------------------------------
/settings/public/editors/settings/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "pinned": false
3 | }
4 |
--------------------------------------------------------------------------------
/documentation/public/editors/documentation/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "pinned": false
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/*.html
3 | **/*.css
4 | **/*.js
5 | !**/*Gulpfile.js
6 |
--------------------------------------------------------------------------------
/home/public/locales/en/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "main": {
4 | "title": "Home"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/home/public/locales/it/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "main": {
4 | "title": "Home"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/home/public/locales/zh-TW/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "main": {
4 | "title": "首頁"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/home/public/locales/pt-BR/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "main": {
4 | "title": "Home"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/home/public/locales/ru/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "main": {
4 | "title": "Главная"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Common Superpowers plugins
2 |
3 | These plugins are used by most [Superpowers](http://superpowers-html5.com/) systems.
4 |
--------------------------------------------------------------------------------
/settings/public/locales/en/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "settings": {
4 | "title": "Settings"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/settings/public/locales/it/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "settings": {
4 | "title": "Impostazioni"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/settings/public/locales/en/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "namespaces": {
3 | "general": "General",
4 | "editors": "Editors"
5 | }
6 | }
--------------------------------------------------------------------------------
/settings/public/locales/it/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "namespaces": {
3 | "general": "Generale",
4 | "editors": "Editor"
5 | }
6 | }
--------------------------------------------------------------------------------
/settings/public/locales/pt-BR/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "settings": {
4 | "title": "Configurações"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/settings/public/locales/ru/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "settings": {
4 | "title": "Настройки"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/documentation/public/locales/zh-TW/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "文件"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/settings/public/locales/pt-BR/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "namespaces": {
3 | "general": "Geral",
4 | "editors": "Editores"
5 | }
6 | }
--------------------------------------------------------------------------------
/documentation/public/locales/ru/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "Документация"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/documentation/public/locales/th/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "เอกสารคู่มือ"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/home/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/settings/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/settings/public/locales/ru/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "namespaces": {
3 | "general": "Основное",
4 | "editors": "Редакторы"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/three/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/documentation/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/documentation/public/locales/en/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "Documentation"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/documentation/public/locales/pt-BR/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "Documentação"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/documentation/public/locales/sv/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "editors": {
3 | "documentation": {
4 | "title": "Dokumentation"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/textEditorWidget/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/textEditorWidget/data/index.ts:
--------------------------------------------------------------------------------
1 | import TextEditorSettingsResource from "./TextEditorSettingsResource";
2 |
3 | SupCore.system.data.registerResource("textEditorSettings", TextEditorSettingsResource);
4 |
--------------------------------------------------------------------------------
/textEditorWidget/public/locales/pt-BR/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "TextEditor": {
3 | "label": "Editor de Texto",
4 | "tabSize": "Tamanho do Tab",
5 | "useSoftTab": "Usar Soft Tab"
6 | }
7 | }
--------------------------------------------------------------------------------
/textEditorWidget/public/locales/en/settingsEditors.json:
--------------------------------------------------------------------------------
1 | {
2 | "TextEditor": {
3 | "label": "Text Editor",
4 | "tabSize": "Tab size",
5 | "useSoftTab": "Use soft tab",
6 | "keyMap": "Key map",
7 | "theme": "Theme"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/documentation/DocumentationPlugin.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace SupClient {
2 | // Dummy boolean because we need to register something,
3 | // but really, the plugin's name is all we need.
4 | export type DocumentationPlugin = { isFirstSection: boolean };
5 | }
6 |
--------------------------------------------------------------------------------
/home/editors/main/links.ts:
--------------------------------------------------------------------------------
1 | if (SupApp != null) {
2 | document.querySelector(".sidebar .links").addEventListener("click", (event: any) => {
3 | if (event.target.tagName !== "A") return;
4 |
5 | event.preventDefault();
6 | SupApp.openLink(event.target.href);
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/home/public/locales/zh-TW/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "title": "連結",
4 | "officialWebsite": "官方網站",
5 | "devRoadmap": "開發規劃",
6 | "skypeGroupChat": "Skype 群組聊天: ",
7 | "forums": "論壇: ",
8 | "chatPlaceholder": "輸入聊天訊息"
9 | },
10 | "onlineMembers": "線上成員"
11 | }
--------------------------------------------------------------------------------
/home/public/locales/ru/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "title": "Ссылки",
4 | "officialWebsite": "Официальный сайт",
5 | "devRoadmap": "План разработки",
6 | "communityForums": "Форумы сообщества",
7 | "chatPlaceholder": "Чат"
8 | },
9 | "onlineMembers": "Учасники"
10 | }
11 |
--------------------------------------------------------------------------------
/home/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "rootDir": "./",
7 | "typeRoots": [ "../../../../../node_modules/@types" ]
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "typings"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/three/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "rootDir": "./",
7 | "typeRoots": [ "../../../../../node_modules/@types" ]
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "typings"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/home/public/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "title": "Links",
4 | "officialWebsite": "Official website",
5 | "devRoadmap": "Development roadmap",
6 | "communityForums": "Community forums",
7 | "chatPlaceholder": "Type here to chat"
8 | },
9 | "onlineMembers": "Online members"
10 | }
--------------------------------------------------------------------------------
/settings/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "rootDir": "./",
7 | "typeRoots": [ "../../../../../node_modules/@types" ]
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "typings"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/documentation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "rootDir": "./",
7 | "typeRoots": [ "../../../../../node_modules/@types" ]
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "typings"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/textEditorWidget/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": true,
6 | "rootDir": "./",
7 | "typeRoots": [ "../../../../../node_modules/@types" ]
8 | },
9 | "exclude": [
10 | "node_modules",
11 | "typings"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/home/public/locales/it/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "title": "Link",
4 | "officialWebsite": "Website ufficiale",
5 | "devRoadmap": "Roadmap di sviluppo",
6 | "communityForums": "Forum della community",
7 | "chatPlaceholder": "Scrivi qui per chattare"
8 | },
9 | "onlineMembers": "Membri online"
10 | }
11 |
--------------------------------------------------------------------------------
/settings/settingsEditors/SettingsEditorPlugin.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace SupClient {
2 | export interface SettingsEditor {
3 | new(container: HTMLDivElement, projectClient: SupClient.ProjectClient): any;
4 | }
5 |
6 | export interface SettingsEditorPlugin {
7 | namespace: string;
8 | editor: SettingsEditor;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/textEditorWidget/settingsEditors/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import TextEditorSettingsEditor from "./TextEditorSettingsEditor";
4 |
5 | SupClient.registerPlugin("settingsEditors", "TextEditor", {
6 | namespace: "editors",
7 | editor: TextEditorSettingsEditor
8 | });
9 |
--------------------------------------------------------------------------------
/home/public/locales/pt-BR/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "title": "Links",
4 | "officialWebsite": "Site Oficial",
5 | "devRoadmap": "Roteiro de Desenvolvimento",
6 | "skypeGroupChat": "Conversa em Grupo no Skype: ",
7 | "forums": "Fóruns: ",
8 | "chatPlaceholder": "Digite aqui para conversar"
9 | },
10 | "onlineMembers": "Membros Online"
11 | }
--------------------------------------------------------------------------------
/documentation/editors/documentation/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= t("plugin:editors.documentation.title")
5 | link(rel="stylesheet",href="/styles/reset.css")
6 | link(rel="stylesheet",href="/styles/dialogs.css")
7 | link(rel="stylesheet",href="index.css")
8 |
9 | body
10 | nav
11 | ul
12 |
13 | main
14 |
15 | script(src="/SupCore.js")
16 | script(src="/SupClient.js")
17 | script(src="index.js")
18 |
--------------------------------------------------------------------------------
/home/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-home-plugin",
3 | "description": "Home plugin for Superpowers, the HTML5 app for real-time collaborative projects",
4 | "version": "1.0.0",
5 | "license": "ISC",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/superpowers/superpowers-common-plugins.git"
9 | },
10 | "scripts": {
11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=."
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/settings/editors/settings/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= t("plugin:editors.settings.title")
5 | link(rel="stylesheet",href="/styles/reset.css")
6 | link(rel="stylesheet",href="/styles/treeView.css")
7 | link(rel="stylesheet",href="/styles/dialogs.css")
8 | link(rel="stylesheet",href="index.css")
9 |
10 | body
11 | main
12 |
13 | script(src="/SupCore.js")
14 | script(src="/SupClient.js")
15 | script(src="index.js")
16 |
--------------------------------------------------------------------------------
/settings/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-settings-plugin",
3 | "description": "Settings plugin for Superpowers, the HTML5 app for real-time collaborative projects",
4 | "version": "1.0.0",
5 | "license": "ISC",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/superpowers/superpowers-common-plugins.git"
9 | },
10 | "scripts": {
11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=."
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/three/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import GridHelper from "./GridHelper";
2 | import SelectionBoxRenderer from "./SelectionBoxRenderer";
3 | import TransformControls from "./TransformControls";
4 | import TransformMarker from "./TransformMarker";
5 | import { GizmoMaterial } from "./TransformGizmos";
6 |
7 | (global as any).SupTHREE.GridHelper = GridHelper;
8 | (global as any).SupTHREE.SelectionBoxRenderer = SelectionBoxRenderer;
9 | (global as any).SupTHREE.TransformControls = TransformControls;
10 | (global as any).SupTHREE.TransformMarker = TransformMarker;
11 | (global as any).SupTHREE.GizmoMaterial = GizmoMaterial;
12 |
--------------------------------------------------------------------------------
/three/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-three-plugin",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/three": {
8 | "version": "0.0.24",
9 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.0.24.tgz",
10 | "integrity": "sha1-PtF9zL+dODtu8ngD42npEn4zlVk=",
11 | "dev": true
12 | },
13 | "three": {
14 | "version": "0.88.0",
15 | "resolved": "https://registry.npmjs.org/three/-/three-0.88.0.tgz",
16 | "integrity": "sha1-QlbC/Djk+yOg0j66K2zOTfjkZtU="
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/documentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-documentation-plugin",
3 | "description": "Documentation plugin for Superpowers, the HTML5 app for real-time collaborative projects",
4 | "version": "1.0.0",
5 | "license": "ISC",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/superpowers/superpowers-common-plugins.git"
9 | },
10 | "scripts": {
11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=."
12 | },
13 | "dependencies": {
14 | "marked": "^0.6.1"
15 | },
16 | "devDependencies": {
17 | "@types/marked": "^0.6.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/three/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-three-plugin",
3 | "description": "Three.js for Superpowers, the HTML5 app for real-time collaborative projects",
4 | "version": "1.0.0",
5 | "license": "ISC",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/superpowers/superpowers-common-plugins.git"
9 | },
10 | "scripts": {
11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=. --silent && gulp --gulpfile=mainGulpfile.js"
12 | },
13 | "dependencies": {
14 | "three": "^0.88.0"
15 | },
16 | "devDependencies": {
17 | "@types/three": "0.0.24"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/three/mainGulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require("gulp");
2 | const tasks = [];
3 |
4 | // Browserify
5 | const browserify = require("browserify");
6 | const source = require("vinyl-source-stream");
7 |
8 | function makeBrowserify(src, dest, output) {
9 | gulp.task(`${output}-browserify`, () => {
10 | return browserify(src, { standalone: "SupTHREE" })
11 | .transform("brfs").bundle()
12 | .pipe(source(`${output}.js`))
13 | .pipe(gulp.dest(dest));
14 | });
15 | tasks.push(`${output}-browserify`);
16 | }
17 |
18 | makeBrowserify("./main/main.js", "./public/", "main");
19 |
20 |
21 | // All
22 | gulp.task("default", gulp.parallel(tasks));
23 |
--------------------------------------------------------------------------------
/textEditorWidget/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-text-editor-widget-plugin",
3 | "description": "Collaborative text editor widget for Superpowers, the HTML5 app for real-time collaborative projects",
4 | "version": "1.0.0",
5 | "license": "ISC",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/superpowers/superpowers-common-plugins.git"
9 | },
10 | "scripts": {
11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=. --silent && gulp --gulpfile=widgetGulpfile.js"
12 | },
13 | "dependencies": {
14 | "operational-transform": "^0.2.3"
15 | },
16 | "devDependencies": {
17 | "@types/codemirror": "0.0.34",
18 | "codemirror": "^5.16.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/documentation/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-documentation-plugin",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/marked": {
8 | "version": "0.6.2",
9 | "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.2.tgz",
10 | "integrity": "sha512-yl4Y+AXghz2VWsrIK0rXJpYYcKI1sIbaLoa+ByHq7WFHZfuN0+iJMtgn0zIXh1/DbKn6BIbp9oHuHZjEIb7QlQ==",
11 | "dev": true
12 | },
13 | "marked": {
14 | "version": "0.6.1",
15 | "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.1.tgz",
16 | "integrity": "sha512-+H0L3ibcWhAZE02SKMqmvYsErLo4EAVJxu5h3bHBBDvvjeWXtl92rGUSBYHL2++5Y+RSNgl8dYOAXcYe7lp1fA=="
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/textEditorWidget/widget/widget.styl:
--------------------------------------------------------------------------------
1 | @require "../node_modules/codemirror/lib/codemirror.css"
2 | @require "../node_modules/codemirror/addon/dialog/dialog.css"
3 | @require "../node_modules/codemirror/addon/hint/show-hint.css"
4 | @require "../node_modules/codemirror/addon/fold/foldgutter.css"
5 | // @require "../node_modules/codemirror/theme/monokai.css"
6 |
7 | .text-editor-container
8 | position relative
9 | flex 1
10 |
11 | textarea
12 | opacity 0
13 |
14 | .text-editor + .CodeMirror
15 | position absolute
16 | top 0
17 | left 0
18 | right 0
19 | bottom 0
20 | height auto
21 | font-size 14px
22 |
23 | .text-editor + .CodeMirror, .CodeMirror-hints
24 | font-family "Consolas", monospace
25 |
26 | .CodeMirror-hints { max-width: none; }
27 |
--------------------------------------------------------------------------------
/textEditorWidget/data/TextEditorSettingsResource.ts:
--------------------------------------------------------------------------------
1 | interface TextEditorSettingsResourcePub {
2 | tabSize: number;
3 | softTab: boolean;
4 | }
5 |
6 | export default class TextEditorSettingsResource extends SupCore.Data.Base.Resource {
7 |
8 | static schema: SupCore.Data.Schema = {
9 | tabSize: { type: "number", min: 1, mutable: true },
10 | softTab: { type: "boolean", mutable: true },
11 | };
12 |
13 | pub: TextEditorSettingsResourcePub;
14 |
15 | constructor(id: string, pub: any, server: ProjectServer) {
16 | super(id, pub, TextEditorSettingsResource.schema, server);
17 | }
18 |
19 | init(callback: Function) {
20 | this.pub = {
21 | tabSize: 2,
22 | softTab: true
23 | };
24 |
25 | super.init(callback);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/textEditorWidget/widget.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | interface EditCallback {
5 | (text: string, origin: string): void;
6 | }
7 | interface SendOperationCallback {
8 | (operation: OperationData): void;
9 | }
10 |
11 | interface TextEditorWidgetOptions {
12 | extraKeys?: { [name: string]: string|Function };
13 | editCallback?: EditCallback;
14 | mode: string;
15 | sendOperationCallback: SendOperationCallback;
16 | }
17 |
18 | declare class TextEditorWidget {
19 | codeMirrorInstance: CodeMirror.EditorFromTextArea;
20 | clientId: number;
21 |
22 | constructor(projectClient: SupClient.ProjectClient, clientId: string, textArea: HTMLTextAreaElement, options: TextEditorWidgetOptions);
23 | setText(text: string): void;
24 | receiveEditText(operationData: OperationData): void;
25 | clear(): void;
26 | }
27 |
--------------------------------------------------------------------------------
/documentation/editors/documentation/index.styl:
--------------------------------------------------------------------------------
1 | body
2 | display flex
3 |
4 | nav
5 | width 250px
6 | overflow-y auto
7 | position fixed
8 | top 0
9 | background #ddd
10 | bottom 0
11 |
12 | ul
13 | list-style none
14 | margin 0
15 | padding 0.5em
16 |
17 | li
18 | padding 0
19 | margin 0
20 |
21 | a
22 | display block
23 | color #444
24 | padding 0.25em 0.5em
25 | text-decoration none
26 |
27 | &:hover
28 | background #ccc
29 |
30 | &:active
31 | color #fff
32 |
33 | &.active
34 | background #444
35 | color #eee
36 |
37 | main
38 | margin-left 250px
39 | padding 1em
40 | flex 1
41 |
42 | article
43 | flex 1
44 | display none
45 | &.active { display: block; }
46 | overflow-y auto
47 |
48 | > *:first-child
49 | margin-top 0
50 |
51 | h1
52 | text-transform uppercase
53 | font-size 2em
54 | h2
55 | font-size 1.5em
--------------------------------------------------------------------------------
/textEditorWidget/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "superpowers-common-text-editor-widget-plugin",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/codemirror": {
8 | "version": "0.0.34",
9 | "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.34.tgz",
10 | "integrity": "sha1-lGRIYH5Ama+F7KiDXRpP8zyY0hk=",
11 | "dev": true
12 | },
13 | "codemirror": {
14 | "version": "5.30.0",
15 | "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.30.0.tgz",
16 | "integrity": "sha512-pfJV/7fLAUUenuGK3iANkQu1AxNLuWpeF7HV6YFDjSBMp53F8FTa2F6oPs9NKAHFweT2m08usmXUIA+7sohdew==",
17 | "dev": true
18 | },
19 | "operational-transform": {
20 | "version": "0.2.3",
21 | "resolved": "https://registry.npmjs.org/operational-transform/-/operational-transform-0.2.3.tgz",
22 | "integrity": "sha1-zzJ3QxK0u5pGR465Sfpqyq3V1Ag="
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/home/editors/main/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | link(rel="stylesheet", href="/styles/reset.css")
5 | link(rel="stylesheet", href="index.css")
6 | title Home
7 | body
8 | .main
9 | .project-info
10 | // TODO: If in browser, add button to add to/open in Superpowers
11 | .chat
12 | ol
13 | .chat-input
14 | textarea(placeholder= t("home:links.chatPlaceholder"),maxlength=300)
15 | .sidebar
16 | .links
17 | ul
18 | li
19 | a(href="http://superpowers-html5.com/",target="_blank")= t("home:links.officialWebsite")
20 | li
21 | a(href="http://docs.superpowers-html5.com/en/development/roadmap",target="_blank")= t("home:links.devRoadmap")
22 | li
23 | a(href="http://itch.io/engine/superpowers/community",target="_blank")= t("home:links.communityForums")
24 | .members
25 | ul
26 |
27 | script(src="/SupCore.js")
28 | script(src="/SupClient.js")
29 | script(src="index.js")
30 |
--------------------------------------------------------------------------------
/three/helpers/GridHelper.ts:
--------------------------------------------------------------------------------
1 | export default class GridHelper {
2 | private gridHelper: THREE.GridHelper;
3 |
4 | constructor(private root: THREE.Object3D, size: number, step: number, opacity: number = 0.25) {
5 | this.setup(size, step, opacity);
6 | }
7 |
8 | setup(size: number, step: number, opacity: number) {
9 | if (this.gridHelper != null) {
10 | this.root.remove(this.gridHelper);
11 | this.gridHelper.geometry.dispose();
12 | this.gridHelper.material.dispose();
13 | }
14 |
15 | const divisions = Math.ceil(size / step);
16 | const actualSize = divisions * step;
17 |
18 | this.gridHelper = new THREE.GridHelper(actualSize, divisions, 0xffffff, 0xffffff);
19 | this.gridHelper.material.transparent = true;
20 | this.gridHelper.material.opacity = opacity;
21 |
22 | this.root.add(this.gridHelper);
23 | this.gridHelper.updateMatrixWorld(false);
24 |
25 | return this;
26 | }
27 |
28 | setVisible(visible: boolean) {
29 | this.gridHelper.visible = visible;
30 | return this;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/textEditorWidget/data/textEditorUserSettings.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "events";
2 |
3 | const storageKey = "superpowers.common.textEditorWidget";
4 |
5 | const item = window.localStorage.getItem(storageKey);
6 | export let pub: {
7 | formatVersion: number;
8 | keyMap: string;
9 | theme: string;
10 | [key: string]: any;
11 | } = item != null ? JSON.parse(item) : {
12 | formatVersion: 2,
13 | keyMap: "sublime",
14 | theme: "default"
15 | };
16 |
17 | if (pub.formatVersion === 1) {
18 | pub.formatVersion = 2;
19 | edit("theme", "default");
20 | }
21 |
22 | export const emitter = new EventEmitter();
23 |
24 | window.addEventListener("storage", (event) => {
25 | if (event.key !== storageKey) return;
26 |
27 | const oldPub = pub;
28 | pub = JSON.parse(event.newValue);
29 |
30 | if (oldPub.keyMap !== pub.keyMap) emitter.emit("keyMap");
31 | if (oldPub.theme !== pub.theme) emitter.emit("theme");
32 | });
33 |
34 | export function edit(key: string, value: any) {
35 | pub[key] = value;
36 | window.localStorage.setItem(storageKey, JSON.stringify(pub));
37 | }
38 |
--------------------------------------------------------------------------------
/three/helpers/TransformMarker.ts:
--------------------------------------------------------------------------------
1 | export default class TransformMarker {
2 | private line: THREE.LineSegments;
3 |
4 | constructor(root: THREE.Object3D) {
5 | const geometry = new THREE.Geometry();
6 | geometry.vertices.push(
7 | new THREE.Vector3( -0.25, 0, 0 ), new THREE.Vector3( 0.25, 0, 0 ),
8 | new THREE.Vector3( 0, -0.25, 0 ), new THREE.Vector3( 0, 0.25, 0 ),
9 | new THREE.Vector3( 0, 0, -0.25 ), new THREE.Vector3( 0, 0, 0.25 )
10 | );
11 |
12 | this.line = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.25, transparent: true }));
13 | this.line.layers.set(1);
14 | root.add(this.line);
15 | this.line.updateMatrixWorld(false);
16 | }
17 |
18 | move(target: THREE.Object3D) {
19 | this.line.visible = true;
20 | this.line.position.copy(target.getWorldPosition());
21 | this.line.quaternion.copy(target.getWorldQuaternion());
22 | this.line.updateMatrixWorld(false);
23 | return this;
24 | }
25 |
26 | hide() {
27 | this.line.visible = false;
28 | return this;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Superpowers is distributed under the ISC license,
2 | in the hope of making it as useful as possible for everyone.
3 | https://en.wikipedia.org/wiki/ISC_license
4 |
5 | We are a welcoming community and we'd love to have you contributing!
6 | https://github.com/superpowers
7 |
8 | ------------------------------------------------------------------------------
9 |
10 | Copyright © 2014-2016, Sparklin Labs
11 |
12 | Permission to use, copy, modify, and/or distribute this software for any
13 | purpose with or without fee is hereby granted, provided that the above
14 | copyright notice and this permission notice appear in all copies.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
17 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
18 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
19 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
20 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
21 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
22 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23 |
--------------------------------------------------------------------------------
/textEditorWidget/operational-transform.d.ts:
--------------------------------------------------------------------------------
1 | declare module "operational-transform" {
2 | class Document {
3 | text: string;
4 | operations: TextOperation[];
5 |
6 | constructor(text: string, revisionId: number);
7 | apply(operation: TextOperation, revision: number): TextOperation;
8 | getRevisionId(): number;
9 | }
10 |
11 | class TextOperation {
12 | userId: string;
13 | ops: TextOp[];
14 |
15 | baseLength: number;
16 | targetLength: number;
17 |
18 | constructor(userId?: string);
19 | serialize(): OperationData;
20 | deserialize(data: OperationData): boolean;
21 | retain(amount: number): void;
22 | insert(text: string): void;
23 | delete(text: string): void;
24 | apply(text: string): string;
25 | invert(): TextOperation;
26 | clone(): TextOperation;
27 | equal(otherOperation: TextOperation): boolean;
28 | compose(otherOperation: TextOperation): TextOperation;
29 | transform(otherOperation: TextOperation): TextOperation[];
30 | gotPriority(otherId: string): boolean;
31 | }
32 |
33 | class TextOp {
34 | type: string;
35 | attributes: any;
36 |
37 | constructor(type: string, attributes: any);
38 | }
39 | }
40 |
41 | interface OperationData {
42 | userId: string;
43 | ops: Array<{type: string; attributes: any}>;
44 | }
45 |
--------------------------------------------------------------------------------
/textEditorWidget/widgetGulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require("gulp");
2 | const tasks = [ "stylus", "copy-cm-modes", "copy-cm-themes" ];
3 |
4 | // Stylus
5 | const stylus = require("gulp-stylus");
6 | const concatCss = require("gulp-concat-css");
7 |
8 | gulp.task("stylus", () => gulp.src("./widget/widget.styl").pipe(stylus({ errors: true, compress: true })).pipe(concatCss("widget.css")).pipe(gulp.dest("./public/")));
9 |
10 | // Browserify
11 | const browserify = require("browserify");
12 | const source = require("vinyl-source-stream");
13 |
14 | function makeBrowserify(src, dest, output) {
15 | gulp.task(`${output}-browserify`, () => {
16 | return browserify(src, { standalone: "TextEditorWidget" })
17 | .transform("brfs").bundle()
18 | .pipe(source(`${output}.js`))
19 | .pipe(gulp.dest(dest));
20 | });
21 | tasks.push(`${output}-browserify`);
22 | }
23 |
24 | makeBrowserify("./widget/widget.js", "./public/", "widget");
25 |
26 | // Copy CodeMirror modes
27 | gulp.task("copy-cm-modes", () => gulp.src([ "node_modules/codemirror/mode/**/*" ]).pipe(gulp.dest("public/codemirror/mode")));
28 |
29 | // Copy CodeMirror themes
30 | gulp.task("copy-cm-themes", () => gulp.src([ "node_modules/codemirror/theme/**/*" ]).pipe(gulp.dest("public/codemirror/theme")));
31 |
32 | // All
33 | gulp.task("default", gulp.parallel(tasks));
34 |
--------------------------------------------------------------------------------
/settings/editors/settings/index.styl:
--------------------------------------------------------------------------------
1 | body
2 | display flex
3 |
4 | main
5 | flex 1
6 |
7 | > header
8 | font-size 2em
9 | background-color #eee
10 | color #666
11 | padding 0.25em 0.5em
12 | text-transform uppercase
13 | border-bottom 1px solid #aaa
14 | &:not(:first-child) { border-top: 1px solid #aaa; }
15 |
16 | > div
17 | display flex
18 | flex-wrap wrap
19 |
20 | section
21 | padding 1em
22 | width calc(500px + 2em)
23 |
24 | header
25 | font-size 1.5em
26 | margin-bottom 0.5em
27 |
28 | section table
29 | width 500px
30 | border-collapse collapse
31 | font-size 12px
32 |
33 | th, td
34 | border 1px solid #ccc
35 |
36 | th
37 | text-align left
38 | white-space nowrap
39 | overflow-x hidden
40 | text-overflow ellipsis
41 | font-weight normal
42 | background #eee
43 | padding 0 0.5em
44 |
45 | td input, td select, td textarea
46 | width 100%
47 | margin 0
48 | padding 0.5em 0.25em
49 | border none
50 |
51 | td select
52 | padding 0.25em 0
53 |
54 | td input[type=checkbox]
55 | width auto
56 | margin 0.5em
57 | cursor pointer
58 |
59 | td input[type=color]
60 | padding 0
61 |
62 | td
63 | .inputs
64 | display flex
65 |
66 | input:not(:last-of-type)
67 | border-right 1px solid #ccc
68 |
69 | .list input:not(:last-of-type)
70 | border-bottom 1px solid #ccc
71 |
72 | td input.color
73 | font-family "Consolas", monospace
74 |
--------------------------------------------------------------------------------
/three/helpers.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace SupTHREE {
4 | export class GridHelper {
5 | constructor(root: THREE.Object3D, size: number, step: number, opacity?: number);
6 | setup(size: number, step: number, opacity: number): this;
7 | setVisible(visible: boolean): this;
8 | }
9 |
10 | export class SelectionBoxRenderer {
11 | constructor(root: THREE.Object3D);
12 | setTarget(target: THREE.Object3D): this;
13 | move(): this;
14 | resize(): this;
15 | hide(): this;
16 | }
17 |
18 | export class TransformControls extends THREE.Object3D {
19 | translationSnap: number;
20 | rotationSnap: number;
21 | root: THREE.Object3D;
22 |
23 | constructor(scene: THREE.Scene, threeCamera: SupTHREE.Camera, canvas: HTMLCanvasElement);
24 | dispose(): void;
25 |
26 | setVisible(visible: boolean): this;
27 | attach(object: THREE.Object3D): this;
28 | detach(): this;
29 |
30 | update(): void;
31 | getMode(): string;
32 | setMode(mode: string): this;
33 | setSpace(space: string): this;
34 | enable(): this;
35 | disable(): this;
36 | }
37 |
38 | export class TransformMarker {
39 | constructor(root: THREE.Object3D);
40 | move(target: THREE.Object3D): this;
41 | hide(): this;
42 | }
43 |
44 | type ColorName = "white"|"red"|"green"|"blue"|"yellow"|"cyan"|"magenta";
45 | export class GizmoMaterial extends THREE.MeshBasicMaterial {
46 | constructor(parameters?: THREE.MeshBasicMaterialParameters);
47 |
48 | setColor(colorName: ColorName): void;
49 | highlight(highlighted: boolean): void;
50 | setDisabled(disabled: boolean): void;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/home/editors/main/index.styl:
--------------------------------------------------------------------------------
1 | body
2 | display flex
3 |
4 | .main
5 | flex 1
6 | display flex
7 | flex-flow column
8 |
9 | .project-info
10 | .project-name
11 | font-size 2em
12 |
13 | .chat
14 | flex 1
15 | flex-basis 0
16 | overflow-y scroll
17 | padding 0.5em
18 | word-wrap break-word
19 |
20 | ol
21 | list-style none
22 | margin 0
23 | padding 0
24 | white-space pre-wrap
25 | -webkit-user-select text
26 | -moz-user-select text
27 | user-select text
28 | cursor initial
29 |
30 | li
31 | margin-bottom 0.25em
32 | padding 0.125em
33 |
34 | &.day-separator
35 | padding 0
36 | text-align center
37 | text-transform uppercase
38 | position relative
39 | color #666
40 |
41 | > hr
42 | height 1px
43 | margin 0.5em 0
44 | border none
45 | background #ccc
46 |
47 | > div
48 | position absolute
49 | top -0.5em
50 | left 0
51 | right 0
52 |
53 | > div > div
54 | display inline-block
55 | padding 0 1em
56 | background #fff
57 |
58 | .timestamp
59 | color #888
60 | font-size smaller
61 | padding-right 0.5em
62 |
63 | .author
64 | font-weight bold
65 |
66 | .chat-input
67 | border-top 1px solid #ccc
68 | padding 0.5em
69 | background #eee
70 | line-height 0
71 |
72 | .chat-input textarea
73 | width 100%
74 | resize none
75 | line-height 1.25
76 | border 1px solid #ccc
77 | padding 0.5em
78 | font-size 14px
79 |
80 | .sidebar {
81 | width: 200px;
82 | border-left: 1px solid #ddd;
83 |
84 | display: flex;
85 | flex-flow: column;
86 |
87 | ol, ul {
88 | list-style: none;
89 | padding: 0;
90 | margin: 0;
91 | }
92 | }
93 |
94 | .sidebar .links {
95 | background: #eee;
96 | padding: 1em;
97 | border-bottom: 1px solid #ddd;
98 | }
99 |
100 | .sidebar .members {
101 | flex: 1 1 0;
102 | padding: 1em;
103 | }
104 |
--------------------------------------------------------------------------------
/three/main/main.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | (window as any).THREE = THREE;
3 | THREE.Euler.DefaultOrder = "YXZ";
4 |
5 | import Camera from "./Camera";
6 | import Camera2DControls from "./Camera2DControls";
7 | import Camera3DControls from "./Camera3DControls";
8 |
9 | export { Camera, Camera2DControls, Camera3DControls };
10 |
11 | export function createWebGLRenderer(params?: THREE.WebGLRendererParameters) {
12 | if (params == null) params = {};
13 | if (params.precision == null) params.precision = "mediump";
14 | if (params.alpha == null) params.alpha = false;
15 | if (params.antialias == null) params.antialias = false;
16 | // NOTE: We ask for a stencil buffer by default because of a Firefox bug:
17 | // Without it, Firefox will often return a 16-bit depth buffer
18 | // (rather than a more useful 24-bit depth buffer).
19 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=1202387
20 | if (params.stencil == null) params.stencil = true;
21 |
22 | const renderer = new THREE.WebGLRenderer(params);
23 |
24 | return renderer;
25 | }
26 |
27 | export class Ticker {
28 | private previousTimestamp = 0;
29 | private accumulatedTime = 0;
30 |
31 | private maxAccumulatedTime: number;
32 | private timeStep: number;
33 |
34 | constructor(private tickCallback: () => boolean, options?: SupTHREE.TickerOptions) {
35 | if (options == null) options = { timeStep: 1000 / 60, maxLateTicks: 5 };
36 | this.timeStep = options.timeStep;
37 | this.maxAccumulatedTime = options.maxLateTicks * options.timeStep;
38 | }
39 |
40 | tick(timestamp: number) {
41 | this.accumulatedTime += timestamp - this.previousTimestamp;
42 | this.previousTimestamp = timestamp;
43 |
44 | let ticks = 0;
45 |
46 | if (this.accumulatedTime > this.maxAccumulatedTime) this.accumulatedTime = this.maxAccumulatedTime;
47 |
48 | while (this.accumulatedTime >= this.timeStep) {
49 | if (this.tickCallback != null) {
50 | const keepGoing = this.tickCallback();
51 | if (!keepGoing) break;
52 | }
53 |
54 | this.accumulatedTime -= this.timeStep;
55 | ticks++;
56 | }
57 |
58 | return ticks;
59 | }
60 |
61 | reset() {
62 | this.previousTimestamp = 0;
63 | this.accumulatedTime = 0;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/three/helpers/SelectionBoxRenderer.ts:
--------------------------------------------------------------------------------
1 | export default class SelectionBoxRenderer {
2 | private mesh: THREE.Mesh;
3 | private target: THREE.Object3D;
4 |
5 | constructor(root: THREE.Object3D) {
6 | this.mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.BackSide }));
7 | root.add(this.mesh);
8 | }
9 |
10 | setTarget(target: THREE.Object3D) {
11 | this.target = target;
12 | this.mesh.visible = true;
13 | this.move();
14 | this.resize();
15 | return this;
16 | }
17 |
18 | move() {
19 | this.mesh.position.copy(this.target.getWorldPosition());
20 | this.mesh.quaternion.copy(this.target.getWorldQuaternion());
21 | this.mesh.updateMatrixWorld(false);
22 | return this;
23 | }
24 |
25 | resize() {
26 | const vec = new THREE.Vector3();
27 | const box = new THREE.Box3();
28 | const inverseTargetMatrixWorld = new THREE.Matrix4().compose(this.target.getWorldPosition(), this.target.getWorldQuaternion(), { x: 1, y: 1, z: 1 } as THREE.Vector3);
29 |
30 | inverseTargetMatrixWorld.getInverse(inverseTargetMatrixWorld);
31 |
32 | this.target.traverse((node: THREE.Mesh) => {
33 | const geometry = node.geometry;
34 |
35 | if (geometry != null) {
36 | node.updateMatrixWorld(false);
37 |
38 | if (geometry instanceof THREE.Geometry) {
39 | const vertices = geometry.vertices;
40 |
41 | for (let i = 0, il = vertices.length; i < il; i++) {
42 | vec.copy(vertices[i]).applyMatrix4(node.matrixWorld).applyMatrix4(inverseTargetMatrixWorld);
43 | box.expandByPoint(vec);
44 | }
45 |
46 | } else if (geometry instanceof THREE.BufferGeometry && (geometry.attributes as any)["position"] != null) {
47 | const positions: Float32Array = (geometry.attributes as any)["position"].array;
48 |
49 | for (let i = 0, il = positions.length; i < il; i += 3) {
50 | vec.set(positions[i], positions[i + 1], positions[i + 2]);
51 | vec.applyMatrix4(node.matrixWorld).applyMatrix4(inverseTargetMatrixWorld);
52 | box.expandByPoint(vec);
53 | }
54 | }
55 | }
56 | });
57 |
58 | const size = box.getSize();
59 | const thickness = 0.1;
60 | this.mesh.scale.copy(size).add(new THREE.Vector3(thickness, thickness, thickness));
61 | this.mesh.updateMatrixWorld(false);
62 | return this;
63 | }
64 |
65 | hide() {
66 | this.mesh.visible = false;
67 | return this;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/three/main.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace SupTHREE {
4 | export function createWebGLRenderer(params?: THREE.WebGLRendererParameters): THREE.WebGLRenderer;
5 |
6 | // `maxLateTicks` limits how many late ticks to try and catch up.
7 | // This helps avoid falling into the "black pit of despair" or "doom spiral"
8 | // where every tick takes longer than the previous one.
9 | // See http://blogs.msdn.com/b/shawnhar/archive/2011/03/25/technical-term-that-should-exist-quot-black-pit-of-despair-quot.aspx
10 | interface TickerOptions { timeStep: number; maxLateTicks: number; }
11 |
12 | export class Ticker {
13 | constructor(tickCallback: () => boolean, options?: SupTHREE.TickerOptions);
14 |
15 | /**
16 | * @returns Number of ticks processed
17 | */
18 | tick(accumulatedTime: number): number;
19 |
20 | reset(): void;
21 | }
22 |
23 | export class Camera {
24 | threeCamera: THREE.OrthographicCamera|THREE.PerspectiveCamera;
25 |
26 | constructor(root: THREE.Object3D, canvas: HTMLCanvasElement)
27 | computeAspectRatio(): this;
28 | setOrthographicMode(isOrthographic: boolean): this;
29 | setFOV(fov: number): this;
30 | setOrthographicScale(orthographicScale: number): this;
31 | getOrthographicScale(): number;
32 | setViewport(x: number, y: number, width: number, height: number): this;
33 | getViewport(): { x: number; y: number; width: number; height: number; };
34 | setDepth(depth: number): this;
35 | setNearClippingPlane(nearClippingPlane: number): this;
36 | setFarClippingPlane(farClippingPlane: number): this;
37 | render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, channels: number[]): void;
38 | }
39 |
40 | interface Camera2DControlsOptions {
41 | zoomMin?: number;
42 | zoomMax?: number;
43 | zoomSpeed?: number;
44 | zoomCallback?: Function;
45 | moveCallback?: Function;
46 | }
47 |
48 | export class Camera2DControls {
49 | constructor(camera: Camera, canvas: HTMLCanvasElement, options?: Camera2DControlsOptions);
50 | setMultiplier(newMultiplier: number): this;
51 | }
52 |
53 | export class Camera3DControls {
54 | constructor(root: THREE.Object3D, camera: Camera, canvas: HTMLCanvasElement);
55 | setEnabled(enabled: boolean): this;
56 | resetOrbitPivot(position: THREE.Vector3, radius?: number): this;
57 | getOrbitPivot(): { position: THREE.Vector3, radius: number };
58 | setPosition(position: THREE.Vector3): this;
59 | getPosition(): THREE.Vector3;
60 | setOrientation(orientation: { theta: number; phi: number; gamma: number; }): this;
61 | getOrientation(): { theta: number; phi: number; gamma: number; };
62 | hasJustPanned(): boolean;
63 | setMoveSpeed(moveSpeed: number): this;
64 | update(): void;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/home/public/editors/main/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
78 |
--------------------------------------------------------------------------------
/settings/editors/settings/index.ts:
--------------------------------------------------------------------------------
1 | import * as async from "async";
2 |
3 | let data: {
4 | projectClient: SupClient.ProjectClient;
5 | };
6 |
7 | const socket = SupClient.connect(SupClient.query.project);
8 |
9 | // NOTE: Listening for "welcome" rather than "connect"
10 | // because SupCore.system.id is only set after "welcome"
11 | socket.on("welcome", onWelcome);
12 | socket.on("disconnect", SupClient.onDisconnected);
13 |
14 | function onWelcome() {
15 | data = { projectClient: new SupClient.ProjectClient(socket) };
16 | loadPlugins();
17 | }
18 |
19 | function loadPlugins() {
20 | const i18nFiles: SupClient.i18n.File[] = [];
21 |
22 | SupClient.fetch(`/systems/${SupCore.system.id}/plugins.json`, "json", (err: Error, pluginsInfo: SupCore.PluginsInfo) => {
23 | for (const pluginName of pluginsInfo.list) {
24 | const root = `/systems/${SupCore.system.id}/plugins/${pluginName}`;
25 | i18nFiles.push({ root, name: "settingsEditors" });
26 | }
27 |
28 | async.parallel([
29 | (cb) => {
30 | SupClient.i18n.load(i18nFiles, cb);
31 | }, (cb) => {
32 | async.each(pluginsInfo.list, (pluginName, cb) => {
33 | const pluginPath = `/systems/${SupCore.system.id}/plugins/${pluginName}`;
34 | async.each(["data", "settingsEditors"], (name, cb) => {
35 | SupClient.loadScript(`${pluginPath}/bundles/${name}.js`, cb);
36 | }, cb);
37 | }, cb);
38 | }
39 | ], setupSettings);
40 | });
41 | }
42 |
43 | function setupSettings() {
44 | const mainElt = document.querySelector("main") as HTMLDivElement;
45 |
46 | const plugins = SupClient.getPlugins("settingsEditors");
47 | const sortedNames = Object.keys(plugins);
48 | sortedNames.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
49 |
50 | const createSection = (namespace: string) => {
51 | const header = SupClient.html("header", { parent: mainElt, textContent: SupClient.i18n.t(`settingsEditors:namespaces.${namespace}`) });
52 | const root = SupClient.html("div", `namespace-${namespace}`, { parent: mainElt });
53 |
54 | return { header, root };
55 | };
56 |
57 | // Create general section first so we are sure it is displayed above
58 | const generalSection = createSection("general");
59 |
60 | for (const name of sortedNames) {
61 | const namespace = plugins[name].content.namespace;
62 | let sectionRootElt = mainElt.querySelector(`div.namespace-${namespace}`) as HTMLDivElement;
63 | if (sectionRootElt == null) sectionRootElt = createSection(namespace).root;
64 |
65 | const sectionElt = SupClient.html("section", { parent: sectionRootElt });
66 |
67 | const headerElt = SupClient.html("header", { parent: sectionElt });
68 | SupClient.html("a", { parent: headerElt, textContent: SupClient.i18n.t(`settingsEditors:${name}.label`), id: name });
69 |
70 | const editorContentElt = SupClient.html("div", { parent: sectionElt });
71 |
72 | const settingEditorClass = plugins[name].content.editor;
73 | new settingEditorClass(editorContentElt, data.projectClient);
74 | }
75 |
76 | // Remove general section if it's empty
77 | if (generalSection.root.children.length === 0) {
78 | mainElt.removeChild(generalSection.header);
79 | mainElt.removeChild(generalSection.root);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/textEditorWidget/settingsEditors/TextEditorSettingsEditor.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | import TextEditorSettingsResource from "../data/TextEditorSettingsResource";
5 | import * as textEditorUserSettings from "../data/textEditorUserSettings";
6 |
7 | const modes = fs.readdirSync(path.join(__dirname, "../node_modules/codemirror/theme"));
8 |
9 | export default class TextEditorSettingsEditor {
10 | resource: TextEditorSettingsResource;
11 |
12 | tabSizeField: HTMLInputElement;
13 | softTabField: HTMLInputElement;
14 | keyMapField: HTMLSelectElement;
15 | themeField: HTMLSelectElement;
16 |
17 | constructor(container: HTMLDivElement, projectClient: SupClient.ProjectClient) {
18 | const { tbody } = SupClient.table.createTable(container);
19 |
20 | // Project settings
21 | const tabSizeRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("settingsEditors:TextEditor.tabSize"));
22 | this.tabSizeField = SupClient.table.appendNumberField(tabSizeRow.valueCell, "", { min: 1 });
23 | this.tabSizeField.addEventListener("change", (event: any) => {
24 | projectClient.editResource("textEditorSettings", "setProperty", "tabSize", parseInt(event.target.value, 10));
25 | });
26 |
27 | const softTabRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("settingsEditors:TextEditor.useSoftTab"));
28 | this.softTabField = SupClient.table.appendBooleanField(softTabRow.valueCell, true);
29 | this.softTabField.addEventListener("change", (event: any) => {
30 | projectClient.editResource("textEditorSettings", "setProperty", "softTab", event.target.checked);
31 | });
32 |
33 | projectClient.subResource("textEditorSettings", this);
34 |
35 | // User settings
36 | const keyMapRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("settingsEditors:TextEditor.keyMap"));
37 | this.keyMapField = SupClient.table.appendSelectBox(keyMapRow.valueCell, { "sublime": "Sublime", "emacs": "Emacs", "vim": "Vim" }, textEditorUserSettings.pub.keyMap);
38 | this.keyMapField.addEventListener("change", (event: any) => {
39 | textEditorUserSettings.edit("keyMap", event.target.value);
40 | });
41 |
42 | textEditorUserSettings.emitter.addListener("keyMap", () => {
43 | this.keyMapField.value = textEditorUserSettings.pub.keyMap;
44 | });
45 |
46 | const themeRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("settingsEditors:TextEditor.theme"));
47 | const themeValues: { [value: string]: string } = { "default": "default" };
48 | for (const mode of modes) {
49 | const modeNoExtension = mode.slice(0, mode.length - 4);
50 | themeValues[modeNoExtension] = modeNoExtension;
51 | }
52 | this.themeField = SupClient.table.appendSelectBox(themeRow.valueCell, themeValues, textEditorUserSettings.pub.theme);
53 | this.themeField.addEventListener("change", (event: any) => {
54 | textEditorUserSettings.edit("theme", event.target.value);
55 | });
56 |
57 | textEditorUserSettings.emitter.addListener("theme", () => {
58 | this.themeField.value = textEditorUserSettings.pub.theme;
59 | });
60 | }
61 |
62 | onResourceReceived = (resourceId: string, resource: TextEditorSettingsResource) => {
63 | this.resource = resource;
64 |
65 | this.tabSizeField.value = resource.pub.tabSize.toString();
66 | this.softTabField.checked = resource.pub.softTab;
67 | }
68 |
69 | onResourceEdited = (resourceId: string, command: string, propertyName: string) => {
70 | switch (propertyName) {
71 | case "tabSize": this.tabSizeField.value = this.resource.pub.tabSize.toString(); break;
72 | case "softTab": this.softTabField.checked = this.resource.pub.softTab; break;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/three/main/Camera2DControls.ts:
--------------------------------------------------------------------------------
1 | import Camera from "./Camera";
2 |
3 | export default class Camera2DControls {
4 | private mousePosition = new THREE.Vector3(0, 0, 0);
5 | private options: SupTHREE.Camera2DControlsOptions;
6 | private multiplier = 1;
7 | private isMoving = false;
8 |
9 | constructor(private camera: Camera, private canvas: HTMLCanvasElement, options?: SupTHREE.Camera2DControlsOptions) {
10 | this.options = options != null ? options : {};
11 | if (this.options.zoomSpeed == null) this.options.zoomSpeed = 1.5;
12 | if (this.options.zoomMin == null) this.options.zoomMin = 0.1;
13 | if (this.options.zoomMax == null) this.options.zoomMax = 10000;
14 |
15 | canvas.addEventListener("mousedown", this.onMouseDown);
16 | canvas.addEventListener("mousemove", this.onMouseMove);
17 | canvas.addEventListener("wheel", this.onWheel);
18 | canvas.addEventListener("keypress", this.onKeyPress);
19 | document.addEventListener("mouseup", this.onMouseUp);
20 | canvas.addEventListener("mouseout", this.onMouseUp);
21 | canvas.addEventListener("contextmenu", (event) => { event.preventDefault(); });
22 | }
23 |
24 | setMultiplier(newMultiplier: number) {
25 | this.multiplier = newMultiplier;
26 | const newOrthographicScale = this.camera.orthographicScale * this.multiplier;
27 | this.changeOrthographicScale(newOrthographicScale);
28 | }
29 |
30 | private onMouseDown = (event: MouseEvent) => {
31 | if (event.button === 1 || (event.button === 0 && event.altKey)) this.isMoving = true;
32 | }
33 |
34 | private onMouseUp = (event: MouseEvent) => {
35 | if (event.button === 0 || event.button === 1) this.isMoving = false;
36 | }
37 |
38 | private onMouseMove = (event: MouseEvent) => {
39 | const rect = this.canvas.getBoundingClientRect();
40 | this.mousePosition.x = (event.clientX - rect.left) / this.canvas.clientWidth * 2 - 1;
41 | this.mousePosition.y = -((event.clientY - rect.top) / this.canvas.clientHeight * 2 - 1);
42 |
43 | if (this.isMoving) {
44 | const cameraZ = this.camera.threeCamera.position.z;
45 | this.camera.threeCamera.position
46 | .set(-event.movementX / this.canvas.clientWidth * 2, event.movementY / this.canvas.clientHeight * 2, 0)
47 | .unproject(this.camera.threeCamera)
48 | .z = cameraZ;
49 | this.camera.threeCamera.updateMatrixWorld(false);
50 | if (this.options.moveCallback != null) this.options.moveCallback();
51 | }
52 | }
53 |
54 | private onWheel = (event: WheelEvent) => {
55 | if (event.ctrlKey) return;
56 |
57 | let newOrthographicScale: number;
58 | if (event.deltaY > 0) newOrthographicScale = Math.min(this.options.zoomMax, this.camera.orthographicScale * this.multiplier * this.options.zoomSpeed);
59 | else if (event.deltaY < 0) newOrthographicScale = Math.max(this.options.zoomMin, this.camera.orthographicScale * this.multiplier / this.options.zoomSpeed);
60 | else return;
61 |
62 | this.changeOrthographicScale(newOrthographicScale, this.mousePosition);
63 | }
64 |
65 | private onKeyPress = (event: KeyboardEvent) => {
66 | if (SupClient.Dialogs.BaseDialog.activeDialog != null) return;
67 |
68 | if (event.keyCode === 43 /* Ctrl+Numpad+ */) {
69 | const newOrthographicScale = Math.max(this.options.zoomMin, this.camera.orthographicScale * this.multiplier / this.options.zoomSpeed);
70 | this.changeOrthographicScale(newOrthographicScale);
71 | }
72 | if (event.keyCode === 45 /* Ctrl+Numpad- */) {
73 | const newOrthographicScale = Math.min(this.options.zoomMax, this.camera.orthographicScale * this.multiplier * this.options.zoomSpeed);
74 | this.changeOrthographicScale(newOrthographicScale);
75 | }
76 | }
77 |
78 | private changeOrthographicScale(newOrthographicScale: number, mousePosition = { x: 0, y: 0 }) {
79 | const startPosition = new THREE.Vector3(mousePosition.x, mousePosition.y, 0).unproject(this.camera.threeCamera);
80 | this.camera.setOrthographicScale(newOrthographicScale / this.multiplier);
81 | const endPosition = new THREE.Vector3(mousePosition.x, mousePosition.y, 0).unproject(this.camera.threeCamera);
82 |
83 | this.camera.threeCamera.position.x += startPosition.x - endPosition.x;
84 | this.camera.threeCamera.position.y += startPosition.y - endPosition.y;
85 | this.camera.threeCamera.updateMatrixWorld(false);
86 | if (this.options.zoomCallback != null) this.options.zoomCallback();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/documentation/editors/documentation/index.ts:
--------------------------------------------------------------------------------
1 | import * as async from "async";
2 | import * as marked from "marked";
3 |
4 | let data: {
5 | projectClient: SupClient.ProjectClient;
6 | };
7 |
8 | const socket = SupClient.connect(SupClient.query.project);
9 | socket.on("welcome", onWelcome);
10 | socket.on("disconnect", SupClient.onDisconnected);
11 |
12 | let loaded = false;
13 | let initialSection: string;
14 | window.addEventListener("message", (event: any) => {
15 | if (event.data.type === "setState") {
16 | if (!loaded) initialSection = event.data.state.section;
17 | else openDocumentation(event.data.state.section);
18 | }
19 | });
20 |
21 | function onWelcome() {
22 | data = { projectClient: new SupClient.ProjectClient(socket), };
23 |
24 | loadPlugins();
25 | }
26 |
27 | function loadPlugins() {
28 | SupClient.fetch(`/systems/${SupCore.system.id}/plugins.json`, "json", (err: Error, pluginsInfo: SupCore.PluginsInfo) => {
29 | async.each(pluginsInfo.list, (pluginName, cb) => {
30 | const pluginPath = `/systems/${SupCore.system.id}/plugins/${pluginName}`;
31 | SupClient.loadScript(`${pluginPath}/bundles/documentation.js`, cb);
32 | }, (err) => { setupDocs(); });
33 | });
34 | }
35 |
36 | const navListElt = document.querySelector("nav ul");
37 | const mainElt = document.querySelector("main");
38 |
39 | if (SupApp != null) {
40 | mainElt.addEventListener("click", (event) => {
41 | const target = event.target as HTMLAnchorElement;
42 | if (target.tagName !== "A") return;
43 |
44 | event.preventDefault();
45 | SupApp.openLink(target.href);
46 | });
47 | }
48 |
49 | function openDocumentation(name: string) {
50 | (navListElt.querySelector("li a.active") as HTMLAnchorElement).classList.remove("active");
51 | (mainElt.querySelector("article.active") as HTMLElement).classList.remove("active");
52 | navListElt.querySelector(`[data-name=${name}]`).classList.add("active");
53 | document.getElementById(`documentation-${name}`).classList.add("active");
54 | }
55 |
56 | function setupDocs() {
57 | const docs = SupClient.getPlugins("documentation");
58 | if (docs == null) {
59 | mainElt.textContent = "This system doesn't have any documentation included.";
60 | return;
61 | }
62 |
63 | const languageCode = SupClient.cookies.get("supLanguage");
64 | const liEltsByTranslatedName: { [translatedName: string]: HTMLLIElement } = {};
65 |
66 | async.each(Object.keys(docs), (name, cb) => {
67 | const liElt = document.createElement("li");
68 | const anchorElt = document.createElement("a");
69 | anchorElt.dataset["name"] = name;
70 | anchorElt.href = `#${name}`;
71 | liElt.appendChild(anchorElt);
72 |
73 | const articleElt = document.createElement("article");
74 | articleElt.id = `documentation-${name}`;
75 | mainElt.appendChild(articleElt);
76 |
77 | function onDocumentationLoaded(content: string) {
78 | articleElt.innerHTML = marked(content);
79 |
80 | const translatedName = articleElt.firstElementChild.textContent;
81 | anchorElt.textContent = translatedName;
82 |
83 | if (docs[name].content.isFirstSection) navListElt.appendChild(liElt);
84 | else liEltsByTranslatedName[translatedName] = liElt;
85 |
86 | if (SupApp == null) {
87 | const linkElts = articleElt.querySelectorAll("a") as any as HTMLAnchorElement[];
88 | for (const linkElt of linkElts) linkElt.target = "_blank";
89 | }
90 | cb(null);
91 | }
92 |
93 | const pluginPath = SupClient.getPlugins("documentation")[name].path;
94 | SupClient.fetch(`${pluginPath}/documentation/${name}.${languageCode}.md`, "text", (err, data) => {
95 | if (err != null) {
96 | SupClient.fetch(`${pluginPath}/documentation/${name}.en.md`, "text", (err, data) => {
97 | onDocumentationLoaded(data);
98 | });
99 | return;
100 | }
101 | onDocumentationLoaded(data);
102 | });
103 | }, () => {
104 | const sortedNames = Object.keys(liEltsByTranslatedName).sort((a, b) => { return (a.toLowerCase() < b.toLowerCase()) ? -1 : 1; });
105 | for (const name of sortedNames) navListElt.appendChild(liEltsByTranslatedName[name]);
106 |
107 | navListElt.addEventListener("click", (event: any) => {
108 | if (event.target.tagName !== "A") return;
109 | openDocumentation(event.target.dataset["name"]);
110 | });
111 |
112 | (navListElt.querySelector("li a")).classList.add("active");
113 | (mainElt.querySelector("article")).classList.add("active");
114 | loaded = true;
115 | if (initialSection != null) openDocumentation(initialSection);
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/three/main/Camera.ts:
--------------------------------------------------------------------------------
1 | export default class Camera{
2 | fov = 45;
3 | orthographicScale = 10;
4 |
5 | threeCamera: THREE.OrthographicCamera|THREE.PerspectiveCamera;
6 | viewport = { x: 0, y: 0, width: 1, height: 1 };
7 |
8 | layers: number[] = [];
9 | depth = 0;
10 | nearClippingPlane = 0.1;
11 | farClippingPlane = 1000;
12 |
13 | cachedRatio: number;
14 | isOrthographic: boolean;
15 | projectionNeedsUpdate: boolean;
16 |
17 | constructor(root: THREE.Object3D, private canvas: HTMLCanvasElement) {
18 | this.setOrthographicMode(false);
19 | this.computeAspectRatio();
20 | }
21 |
22 | computeAspectRatio() {
23 | this.cachedRatio = (this.canvas.clientWidth * this.viewport.width) / (this.canvas.clientHeight * this.viewport.height);
24 | this.projectionNeedsUpdate = true;
25 | return this;
26 | }
27 |
28 | setOrthographicMode(isOrthographic: boolean) {
29 | this.isOrthographic = isOrthographic;
30 |
31 | if (this.isOrthographic) {
32 | this.threeCamera = new THREE.OrthographicCamera(-this.orthographicScale * this.cachedRatio / 2,
33 | this.orthographicScale * this.cachedRatio / 2,
34 | this.orthographicScale / 2, -this.orthographicScale / 2,
35 | this.nearClippingPlane, this.farClippingPlane);
36 | }
37 | else this.threeCamera = new THREE.PerspectiveCamera(this.fov, this.cachedRatio, this.nearClippingPlane, this.farClippingPlane);
38 |
39 | this.projectionNeedsUpdate = true;
40 | return this;
41 | }
42 |
43 | setFOV(fov: number) {
44 | this.fov = fov;
45 | if (!this.isOrthographic) this.projectionNeedsUpdate = true;
46 | return this;
47 | }
48 |
49 | setOrthographicScale(orthographicScale: number) {
50 | this.orthographicScale = orthographicScale;
51 | if (this.isOrthographic) {
52 | // NOTE: Apply immediately because it's used for ray calculation
53 | const orthographicCamera = this.threeCamera as THREE.OrthographicCamera;
54 | orthographicCamera.left = -this.orthographicScale * this.cachedRatio / 2;
55 | orthographicCamera.right = this.orthographicScale * this.cachedRatio / 2;
56 | orthographicCamera.top = this.orthographicScale / 2;
57 | orthographicCamera.bottom = -this.orthographicScale / 2;
58 | this.threeCamera.updateProjectionMatrix();
59 | }
60 | return this;
61 | }
62 |
63 | getOrthographicScale() { return this.orthographicScale; }
64 |
65 | setViewport(x: number, y: number, width: number, height: number) {
66 | this.viewport.x = x;
67 | this.viewport.y = y;
68 | this.viewport.width = width;
69 | this.viewport.height = height;
70 | this.projectionNeedsUpdate = true;
71 | this.computeAspectRatio();
72 | return this;
73 | }
74 |
75 | getViewport() { return { x: this.viewport.x, y: this.viewport.y, width: this.viewport.width, height: this.viewport.height }; }
76 |
77 | setDepth(depth: number) {
78 | this.depth = depth;
79 | return this;
80 | }
81 |
82 | setNearClippingPlane(nearClippingPlane: number) {
83 | this.nearClippingPlane = nearClippingPlane;
84 | this.threeCamera.near = this.nearClippingPlane;
85 | this.projectionNeedsUpdate = true;
86 | return this;
87 | }
88 |
89 | setFarClippingPlane(farClippingPlane: number) {
90 | this.farClippingPlane = farClippingPlane;
91 | this.threeCamera.far = this.farClippingPlane;
92 | this.projectionNeedsUpdate = true;
93 | return this;
94 | }
95 |
96 | render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, channels: number[]) {
97 | if (this.projectionNeedsUpdate) {
98 | this.projectionNeedsUpdate = false;
99 |
100 | if (this.isOrthographic) {
101 | const orthographicCamera = this.threeCamera;
102 | orthographicCamera.left = -this.orthographicScale * this.cachedRatio / 2;
103 | orthographicCamera.right = this.orthographicScale * this.cachedRatio / 2;
104 | orthographicCamera.top = this.orthographicScale / 2;
105 | orthographicCamera.bottom = -this.orthographicScale / 2;
106 | }
107 | else {
108 | const perspectiveCamera = this.threeCamera;
109 | perspectiveCamera.fov = this.fov;
110 | perspectiveCamera.aspect = this.cachedRatio;
111 | }
112 | this.threeCamera.updateProjectionMatrix();
113 | }
114 |
115 | renderer.setViewport(
116 | this.viewport.x * this.canvas.width , (1 - this.viewport.y - this.viewport.height) * this.canvas.height,
117 | this.viewport.width * this.canvas.width, this.viewport.height * this.canvas.height
118 | );
119 |
120 | for (const channel of channels) {
121 | renderer.clearDepth();
122 | this.threeCamera.layers.set(channel);
123 | renderer.render(scene, this.threeCamera);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/home/editors/main/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import "./links";
3 |
4 | let data: { room: SupCore.Data.Room; };
5 | let socket: SocketIOClient.Socket;
6 |
7 | const ui = {
8 | chatHistoryContainer: document.querySelector(".chat"),
9 | chatHistory: document.querySelector(".chat ol"),
10 | roomUsers: document.querySelector(".members ul")
11 | };
12 |
13 | function start() {
14 | socket = SupClient.connect(SupClient.query.project);
15 | socket.on("connect", onConnected);
16 | socket.on("disconnect", SupClient.onDisconnected);
17 |
18 | // Chat
19 | document.querySelector(".chat-input textarea").addEventListener("keydown", onChatInputKeyDown);
20 | document.querySelector(".chat").addEventListener("click", onLinkClicked);
21 | }
22 |
23 | function onConnected() {
24 | data = {};
25 | // FIXME Add support in ProjectClient?
26 | socket.emit("sub", "rooms", "home", onRoomReceived);
27 | socket.on("edit:rooms", onRoomEdited);
28 | }
29 |
30 | function onRoomReceived(err: string, room: any) {
31 | data.room = new SupCore.Data.Room(room);
32 |
33 | for (const roomUser of data.room.pub.users) appendRoomUser(roomUser);
34 |
35 | for (const entry of data.room.pub.history) appendHistoryEntry(entry);
36 | scrollToBottom();
37 | }
38 |
39 | let onRoomCommands: any = {};
40 | function onRoomEdited(id: string, command: string, ...args: any[]) {
41 | Object.getPrototypeOf(data.room)[`client_${command}`].apply(data.room, args);
42 | if (onRoomCommands[command] != null) onRoomCommands[command].apply(data.room, args);
43 | }
44 |
45 | function scrollToBottom() {
46 | setTimeout(() => { ui.chatHistoryContainer.scrollTop = ui.chatHistoryContainer.scrollHeight; }, 0);
47 | }
48 |
49 | // Firefox 41 loses the scroll position when going back to the tab
50 | // so we'll manually restore it when the tab is activated
51 | let savedScrollTop = 0;
52 |
53 | ui.chatHistoryContainer.addEventListener("scroll", (event) => {
54 | savedScrollTop = ui.chatHistoryContainer.scrollTop;
55 | });
56 |
57 | window.addEventListener("message", (event) => {
58 | if (event.data.type === "activate") {
59 | setTimeout(() => { ui.chatHistoryContainer.scrollTop = savedScrollTop; }, 0);
60 | }
61 | });
62 |
63 | const appendDaySeparator = (date: Date) => {
64 | const separatorElt = document.createElement("li");
65 | separatorElt.className = "day-separator";
66 |
67 | separatorElt.appendChild(document.createElement("hr"));
68 |
69 | const dateDiv = document.createElement("div");
70 | separatorElt.appendChild(dateDiv);
71 |
72 | const dateInnerDiv = document.createElement("div");
73 | dateInnerDiv.textContent = date.toDateString();
74 | dateDiv.appendChild(dateInnerDiv);
75 |
76 | ui.chatHistory.appendChild(separatorElt);
77 | };
78 |
79 | let previousDay: string;
80 | interface Entry {
81 | author: string;
82 | text: string;
83 | timestamp: number;
84 | }
85 |
86 | const addressRegex = new RegExp("^(http[s]?:\\/\\/(www\\.)?|ftp:\\/\\/(www\\.)?|www\\.){1}([0-9A-Za-z-\\.@:%_\+~#=]+)+((\\.[a-zA-Z]{2,3})+)(/(.)*)?(\\?(.)*)?");
87 | function appendHistoryEntry(entry: Entry) {
88 | const date = new Date(entry.timestamp);
89 | const day = date.toDateString();
90 | if (previousDay !== day) {
91 | appendDaySeparator(date);
92 | previousDay = day;
93 | }
94 |
95 | const entryElt = document.createElement("li");
96 |
97 | const timestampSpan = document.createElement("span");
98 | timestampSpan.className = "timestamp";
99 | const time = `00${date.getHours()}`.slice(-2) + ":" + `00${date.getMinutes()}`.slice(-2);
100 | timestampSpan.textContent = time;
101 | entryElt.appendChild(timestampSpan);
102 |
103 | const authorSpan = document.createElement("span");
104 | authorSpan.className = "author";
105 | authorSpan.textContent = entry.author;
106 | entryElt.appendChild(authorSpan);
107 |
108 | const addressTest = addressRegex.exec(entry.text);
109 | if (addressTest != null) {
110 | const beforeAddress = entry.text.slice(0, addressTest.index);
111 | const beforeTextSpan = document.createElement("span");
112 | beforeTextSpan.className = "text";
113 | beforeTextSpan.textContent = `: ${beforeAddress}`;
114 | entryElt.appendChild(beforeTextSpan);
115 |
116 | const addressTextLink = document.createElement("a");
117 | addressTextLink.className = "text";
118 | addressTextLink.textContent = addressTest[0];
119 | addressTextLink.href = addressTest[0];
120 | entryElt.appendChild(addressTextLink);
121 |
122 | const afterAddress = entry.text.slice(addressTest.index + addressTest[0].length);
123 | const afterTextSpan = document.createElement("span");
124 | afterTextSpan.className = "text";
125 | afterTextSpan.textContent = afterAddress;
126 | entryElt.appendChild(afterTextSpan);
127 |
128 | } else {
129 | const textSpan = document.createElement("span");
130 | textSpan.className = "text";
131 | textSpan.textContent = `: ${entry.text}`;
132 | entryElt.appendChild(textSpan);
133 | }
134 |
135 | ui.chatHistory.appendChild(entryElt);
136 | }
137 |
138 | onRoomCommands.appendMessage = (entry: Entry) => {
139 | if (window.parent != null) window.parent.postMessage({ type: "chat", content: `${entry.author}: ${entry.text}` }, window.location.origin);
140 | appendHistoryEntry(entry);
141 | scrollToBottom();
142 | };
143 |
144 | function appendRoomUser(roomUser: { id: string; connectionCount: number; }) {
145 | const roomUserElt = document.createElement("li");
146 | roomUserElt.dataset["userId"] = roomUser.id;
147 | roomUserElt.textContent = roomUser.id;
148 | ui.roomUsers.appendChild(roomUserElt);
149 | }
150 |
151 | onRoomCommands.join = (roomUser: { id: string; connectionCount: number; }) => {
152 | if (roomUser.connectionCount === 1) appendRoomUser(roomUser);
153 | };
154 |
155 | onRoomCommands.leave = (roomUserId: string) => {
156 | if (data.room.users.byId[roomUserId] == null) {
157 | const roomUserElt = ui.roomUsers.querySelector(`li[data-user-id=${roomUserId}]`);
158 | roomUserElt.parentElement.removeChild(roomUserElt);
159 | }
160 | };
161 |
162 | function onChatInputKeyDown(event: any) {
163 | if (event.keyCode !== 13 || event.shiftKey) return;
164 | event.preventDefault();
165 | if (!socket.connected) return;
166 |
167 | socket.emit("edit:rooms", "home", "appendMessage", this.value, (err: string) => {
168 | if (err != null) { new SupClient.Dialogs.InfoDialog(err); return; }
169 | });
170 |
171 | this.value = "";
172 | }
173 |
174 | function onLinkClicked(event: MouseEvent) {
175 | const anchorElt = event.target as HTMLAnchorElement;
176 | if (anchorElt.tagName === "A") {
177 | event.preventDefault();
178 |
179 | if (SupApp != null) SupApp.openLink(anchorElt.href);
180 | else window.open(anchorElt.href, "_blank");
181 | }
182 | }
183 |
184 | SupClient.i18n.load([{ root: path.join(window.location.pathname, "../.."), name: "home" }], start);
185 |
--------------------------------------------------------------------------------
/settings/public/editors/settings/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
92 |
--------------------------------------------------------------------------------
/documentation/public/editors/documentation/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
156 |
--------------------------------------------------------------------------------
/three/main/Camera3DControls.ts:
--------------------------------------------------------------------------------
1 | import Camera from "./Camera";
2 |
3 | const tmpVector3 = new THREE.Vector3();
4 | const tmpQuaternion = new THREE.Quaternion();
5 | const tmpEuler = new THREE.Euler();
6 | const upVector = new THREE.Vector3(0, 1, 0);
7 |
8 | const epsilon = 0.001;
9 | const lerpFactor = 0.3;
10 | const minOrbitRadius = 0.001;
11 | const maxOrbitRadius = 500;
12 | const initialOrbitRadius = 10;
13 |
14 | const panningSpeed = 0.005;
15 | const orbitingSpeed = 0.008;
16 | const orbitingThetaSpeed = Math.PI / 56;
17 | const rotateGammaSpeed = 0.02;
18 | const zoomingSpeed = 1.2;
19 |
20 | export default class Camera3DControls {
21 | private enabled = true;
22 | private moveSpeed = 0.3;
23 |
24 | private wantToPan = false;
25 | private hasMovedWhilePanning = false;
26 |
27 | private isOrbiting = false;
28 | private isOrbitingThetaLeft = false;
29 | private isOrbitingThetaRight = false;
30 | private orbitPivot: THREE.Vector3;
31 | private targetOrbitPivot: THREE.Vector3;
32 |
33 | private orbitRadius = initialOrbitRadius;
34 | private targetOrbitRadius = initialOrbitRadius;
35 |
36 | // Horizontal angle
37 | private theta: number;
38 | private targetTheta: number;
39 | // Vertical angle
40 | private phi: number;
41 | private targetPhi: number;
42 | // Forward angle
43 | private gamma: number;
44 | private targetGamma: number;
45 |
46 | private moveVector = new THREE.Vector3();
47 | private pivotMarker: THREE.LineSegments;
48 | private pivotMarkerOpacity = 0;
49 |
50 | constructor(private root: THREE.Object3D, private camera: Camera, private canvas: HTMLCanvasElement) {
51 | this.orbitPivot = new THREE.Vector3(0, 0, -this.orbitRadius).applyQuaternion(this.camera.threeCamera.quaternion).add(this.camera.threeCamera.position);
52 | this.targetOrbitPivot = this.orbitPivot.clone();
53 |
54 | tmpQuaternion.setFromUnitVectors(this.camera.threeCamera.up, upVector);
55 | tmpVector3.copy(this.camera.threeCamera.position).sub(this.orbitPivot).applyQuaternion(tmpQuaternion);
56 |
57 | this.theta = Math.atan2(tmpVector3.x, tmpVector3.z);
58 | this.targetTheta = this.theta;
59 | this.phi = Math.atan2(Math.sqrt(tmpVector3.x * tmpVector3.x + tmpVector3.z * tmpVector3.z), tmpVector3.y);
60 | this.targetPhi = this.phi;
61 | this.gamma = this.targetGamma = 0;
62 |
63 | const pivotGeometry = new THREE.Geometry();
64 | pivotGeometry.vertices.push(
65 | new THREE.Vector3( -0.5, 0, 0 ), new THREE.Vector3( 0.5, 0, 0 ),
66 | new THREE.Vector3( 0, -0.5, 0 ), new THREE.Vector3( 0, 0.5, 0 ),
67 | new THREE.Vector3( 0, 0, -0.5 ), new THREE.Vector3( 0, 0, 0.5 )
68 | );
69 |
70 | this.pivotMarker = new THREE.LineSegments(pivotGeometry, new THREE.LineBasicMaterial({ color: 0xffffff, opacity: this.pivotMarkerOpacity, transparent: true }));
71 | this.pivotMarker.layers.set(1);
72 | root.add(this.pivotMarker);
73 |
74 | canvas.addEventListener("mousedown", this.onMouseDown);
75 | canvas.addEventListener("mousemove", this.onMouseMove);
76 | canvas.addEventListener("wheel", this.onWheel);
77 | canvas.addEventListener("keydown", this.onKeyDown);
78 | document.addEventListener("keyup", this.onKeyUp);
79 | document.addEventListener("mouseup", this.onMouseUp);
80 | canvas.addEventListener("mouseout", this.onMouseUp);
81 | canvas.addEventListener("contextmenu", (event) => { event.preventDefault(); });
82 | window.addEventListener("blur", this.onBlur);
83 | }
84 |
85 | private onMouseDown = (event: MouseEvent) => {
86 | if (!this.enabled) return;
87 | if (this.wantToPan || this.isOrbiting) return;
88 |
89 | if (event.button === 2) {
90 | this.wantToPan = true;
91 | this.hasMovedWhilePanning = false;
92 |
93 | } else if (event.button === 1 || (event.button === 0 && event.altKey)) {
94 | this.isOrbiting = true;
95 | if ((this.canvas as any).requestPointerLock) (this.canvas as any).requestPointerLock();
96 | else if ((this.canvas as any).webkitRequestPointerLock) (this.canvas as any).webkitRequestPointerLock();
97 | else if ((this.canvas as any).mozRequestPointerLock) (this.canvas as any).mozRequestPointerLock();
98 |
99 | this.targetOrbitPivot = new THREE.Vector3(0, 0, -this.targetOrbitRadius).applyQuaternion(this.camera.threeCamera.quaternion).add(this.camera.threeCamera.position);
100 |
101 | tmpQuaternion.setFromUnitVectors(this.camera.threeCamera.up, upVector);
102 | tmpVector3.copy(this.camera.threeCamera.position).sub(this.targetOrbitPivot).applyQuaternion(tmpQuaternion);
103 |
104 | this.theta = Math.atan2(tmpVector3.x, tmpVector3.z);
105 | this.targetTheta = this.theta;
106 | this.phi = Math.atan2(Math.sqrt(tmpVector3.x * tmpVector3.x + tmpVector3.z * tmpVector3.z), tmpVector3.y);
107 | this.targetPhi = this.phi;
108 | }
109 | }
110 |
111 | private onMouseMove = (event: MouseEvent) => {
112 | if (!this.enabled) return;
113 |
114 | if (this.wantToPan) {
115 | this.hasMovedWhilePanning = true;
116 |
117 | const panningMultiplier = panningSpeed * (1 + Math.sqrt(this.targetOrbitRadius));
118 | tmpVector3.set(-event.movementX * panningMultiplier, event.movementY * panningMultiplier, 0).applyQuaternion(this.camera.threeCamera.quaternion);
119 | this.targetOrbitPivot.add(tmpVector3);
120 | this.camera.threeCamera.position.add(tmpVector3);
121 | this.camera.threeCamera.updateMatrixWorld(false);
122 |
123 | } else if (this.isOrbiting) {
124 | this.targetTheta -= event.movementX * orbitingSpeed;
125 | this.targetPhi -= event.movementY * orbitingSpeed;
126 |
127 | this.targetPhi = Math.max(0.001, Math.min(Math.PI - 0.001, this.targetPhi));
128 | }
129 | }
130 |
131 | private onWheel = (event: WheelEvent) => {
132 | if (!this.enabled) return;
133 |
134 | if (event.deltaY > 0) this.targetOrbitRadius *= zoomingSpeed;
135 | else if (event.deltaY < 0) this.targetOrbitRadius /= zoomingSpeed;
136 | else return;
137 |
138 | this.targetOrbitRadius = Math.min(Math.max(this.targetOrbitRadius, minOrbitRadius), maxOrbitRadius);
139 | }
140 |
141 | private onKeyDown = (event: KeyboardEvent) => {
142 | if (!this.enabled) return;
143 | if (event.ctrlKey || event.altKey || event.metaKey) return;
144 |
145 | if (event.keyCode === 87 /* W */ || event.keyCode === 90 /* Z */) {
146 | this.moveVector.z = -1;
147 | } else if (event.keyCode === 83 /* S */) {
148 | this.moveVector.z = 1;
149 | } else if (event.keyCode === 81 /* W */ || event.keyCode === 65 /* A */) {
150 | this.moveVector.x = -1;
151 | } else if (event.keyCode === 68 /* D */) {
152 | this.moveVector.x = 1;
153 | } else if (event.keyCode === 32 /* SPACE */) {
154 | this.moveVector.y = 1;
155 | } else if (event.keyCode === 16 /* SHIFT */) {
156 | this.moveVector.y = -1;
157 | } else if (event.keyCode === 74 /* J */) {
158 | this.targetGamma = Math.min(this.targetGamma + rotateGammaSpeed, Math.PI / 2);
159 | } else if (event.keyCode === 75 /* K */) {
160 | this.targetGamma = Math.max(this.targetGamma - rotateGammaSpeed, -Math.PI / 2);
161 | } else if (event.keyCode === 33 /* Page Up */) {
162 | this.isOrbitingThetaLeft = true;
163 | } else if (event.keyCode === 34 /* Page Down */) {
164 | this.isOrbitingThetaRight = true;
165 | }
166 | }
167 |
168 | private onKeyUp = (event: KeyboardEvent) => {
169 | if (event.keyCode === 87 /* W */ || event.keyCode === 90 /* Z */) {
170 | this.moveVector.z = 0;
171 | } else if (event.keyCode === 83 /* S */) {
172 | this.moveVector.z = 0;
173 | } else if (event.keyCode === 81 /* W */ || event.keyCode === 65 /* A */) {
174 | this.moveVector.x = 0;
175 | } else if (event.keyCode === 68 /* D */) {
176 | this.moveVector.x = 0;
177 | } else if (event.keyCode === 32 /* SPACE */) {
178 | this.moveVector.y = 0;
179 | } else if (event.keyCode === 16 /* SHIFT */) {
180 | this.moveVector.y = 0;
181 | } else if (event.keyCode === 33 /* Page Up */) {
182 | this.isOrbitingThetaLeft = false;
183 | } else if (event.keyCode === 34 /* Page Down */) {
184 | this.isOrbitingThetaRight = false;
185 | }
186 | }
187 |
188 | private onMouseUp = (event: MouseEvent) => {
189 | if (event.button === 2) {
190 | this.wantToPan = false;
191 | this.hasMovedWhilePanning = false;
192 |
193 | } else if (event.button === 1 || event.button === 0) {
194 | this.isOrbiting = false;
195 | if ((document as any).exitPointerLock) (document as any).exitPointerLock();
196 | else if ((document as any).webkitExitPointerLock) (document as any).webkitExitPointerLock();
197 | else if ((document as any).mozExitPointerLock) (document as any).mozExitPointerLock();
198 | }
199 | }
200 |
201 | private onBlur = () => {
202 | this.moveVector.set(0, 0, 0);
203 | this.wantToPan = false;
204 |
205 | if (this.isOrbiting) {
206 | this.isOrbiting = false;
207 | if ((document as any).exitPointerLock) (document as any).exitPointerLock();
208 | else if ((document as any).webkitExitPointerLock) (document as any).webkitExitPointerLock();
209 | else if ((document as any).mozExitPointerLock) (document as any).mozExitPointerLock();
210 | }
211 |
212 | this.isOrbitingThetaLeft = false;
213 | this.isOrbitingThetaRight = false;
214 | }
215 |
216 | setEnabled(enabled: boolean) {
217 | this.enabled = enabled;
218 |
219 | if (!this.enabled) this.onBlur();
220 |
221 | return this;
222 | }
223 |
224 | resetOrbitPivot(position: THREE.Vector3, radius?: number) {
225 | if (!this.enabled) return this;
226 |
227 | this.targetOrbitPivot.copy(position);
228 | if (radius != null) this.targetOrbitRadius = Math.min(Math.max(radius, minOrbitRadius), maxOrbitRadius);
229 | else this.targetOrbitRadius = Math.max(this.targetOrbitRadius, initialOrbitRadius);
230 | return this;
231 | }
232 |
233 | getOrbitPivot() {
234 | return { position: this.targetOrbitPivot.clone(), radius: this.targetOrbitRadius };
235 | }
236 |
237 | setMoveSpeed(moveSpeed: number) {
238 | this.moveSpeed = moveSpeed;
239 | return this;
240 | }
241 |
242 | setPosition(position: THREE.Vector3) {
243 | if (!this.enabled) return this;
244 |
245 | tmpVector3.x = this.orbitRadius * Math.sin(this.targetPhi) * Math.sin(this.targetTheta);
246 | tmpVector3.y = this.orbitRadius * Math.cos(this.targetPhi);
247 | tmpVector3.z = this.orbitRadius * Math.sin(this.targetPhi) * Math.cos(this.targetTheta);
248 | tmpVector3.applyQuaternion(tmpQuaternion.clone().inverse());
249 | tmpVector3.sub(position).negate();
250 |
251 | this.targetOrbitPivot.copy(tmpVector3);
252 | return this;
253 | }
254 | getPosition() { return this.camera.threeCamera.position; }
255 |
256 | setOrientation(orientation: { theta: number; phi: number; gamma: number; }) {
257 | if (!this.enabled) return this;
258 |
259 | this.targetTheta = orientation.theta;
260 | this.targetPhi = orientation.phi;
261 | this.targetGamma = orientation.gamma;
262 | return this;
263 | }
264 | getOrientation() { return { theta: this.theta, phi: this.phi, gamma: this.gamma }; }
265 |
266 | hasJustPanned() { return this.wantToPan && this.hasMovedWhilePanning; }
267 |
268 | update() {
269 | if (this.moveVector.length() !== 0) {
270 | const rotatedMoveVector = this.moveVector.clone();
271 | rotatedMoveVector.applyQuaternion(this.camera.threeCamera.quaternion).normalize().multiplyScalar(this.moveSpeed);
272 | this.camera.threeCamera.position.add(rotatedMoveVector);
273 | this.targetOrbitPivot.add(rotatedMoveVector);
274 | }
275 |
276 | this.orbitPivot.lerp(this.targetOrbitPivot, lerpFactor);
277 | this.orbitRadius += (this.targetOrbitRadius - this.orbitRadius) * lerpFactor;
278 |
279 | if (this.isOrbitingThetaLeft) this.targetTheta += orbitingThetaSpeed;
280 | if (this.isOrbitingThetaRight) this.targetTheta -= orbitingThetaSpeed;
281 |
282 | if (Math.abs(this.targetTheta - this.theta) > epsilon) this.theta += (this.targetTheta - this.theta) * lerpFactor;
283 | else this.theta = this.targetTheta;
284 |
285 | if (Math.abs(this.targetPhi - this.phi) > epsilon) this.phi += (this.targetPhi - this.phi) * lerpFactor;
286 | else this.phi = this.targetPhi;
287 |
288 | if (Math.abs(this.targetGamma - this.gamma) > epsilon) this.gamma += (this.targetGamma - this.gamma) * lerpFactor;
289 | else this.gamma = this.targetGamma;
290 |
291 | tmpVector3.x = this.orbitRadius * Math.sin(this.phi) * Math.sin(this.theta);
292 | tmpVector3.y = this.orbitRadius * Math.cos(this.phi);
293 | tmpVector3.z = this.orbitRadius * Math.sin(this.phi) * Math.cos(this.theta);
294 | tmpVector3.applyQuaternion(tmpQuaternion.clone().inverse());
295 |
296 | this.camera.threeCamera.position.copy(this.orbitPivot).add(tmpVector3);
297 | this.camera.threeCamera.lookAt(this.orbitPivot);
298 | tmpEuler.setFromQuaternion(this.camera.threeCamera.quaternion);
299 | tmpEuler.z = this.gamma;
300 | this.camera.threeCamera.setRotationFromEuler(tmpEuler);
301 | this.camera.threeCamera.updateMatrixWorld(false);
302 |
303 | // Update marker
304 | if (this.orbitPivot.distanceTo(this.targetOrbitPivot) > 0.1 ||
305 | Math.abs(this.orbitRadius - this.targetOrbitRadius) > 0.1 || this.isOrbiting || this.wantToPan) {
306 | this.pivotMarker.material.opacity = 0.5;
307 | } else {
308 | this.pivotMarker.material.opacity *= 1 - lerpFactor;
309 | }
310 |
311 | this.pivotMarker.position.copy(this.orbitPivot);
312 | this.pivotMarker.updateMatrixWorld(false);
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/textEditorWidget/widget/widget.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import TextEditorSettingsResource from "../data/TextEditorSettingsResource";
4 | import * as textEditorUserSettings from "../data/textEditorUserSettings";
5 |
6 | import * as OT from "operational-transform";
7 |
8 | import * as CodeMirror from "codemirror";
9 | (window as any).CodeMirror = CodeMirror;
10 |
11 | /* tslint:disable */
12 | require("codemirror/addon/search/search");
13 | require("codemirror/addon/search/searchcursor");
14 | require("codemirror/addon/edit/closebrackets");
15 | require("codemirror/addon/comment/comment");
16 | require("codemirror/addon/hint/show-hint");
17 | require("codemirror/addon/selection/active-line");
18 | require("codemirror/addon/fold/foldcode");
19 | require("codemirror/addon/fold/foldgutter");
20 | require("codemirror/addon/fold/brace-fold");
21 | require("codemirror/addon/fold/comment-fold");
22 | require("codemirror/addon/fold/indent-fold");
23 |
24 | require("codemirror/keymap/sublime");
25 | require("codemirror/keymap/emacs");
26 | require("codemirror/keymap/vim");
27 | /* tslint:enable */
28 |
29 | class TextEditorWidget {
30 | private textEditorResource: TextEditorSettingsResource;
31 | codeMirrorInstance: CodeMirror.EditorFromTextArea;
32 |
33 | private editCallback: EditCallback;
34 | private sendOperationCallback: SendOperationCallback;
35 |
36 | clientId: string;
37 | private tmpCodeMirrorDoc = new CodeMirror.Doc("");
38 | private texts: string[] = [];
39 |
40 | private undoTimeout: number;
41 | private undoStack: OT.TextOperation[] = [];
42 | private undoQuantityByAction: number[] = [];
43 | private redoStack: OT.TextOperation[] = [];
44 | private redoQuantityByAction: number[] = [];
45 |
46 | private sentOperation: OT.TextOperation;
47 | private pendingOperation: OT.TextOperation;
48 |
49 | private useSoftTab = true;
50 |
51 | private linkElt = SupClient.html("link", { parent: document.head, rel: "stylesheet" });
52 |
53 | constructor(projectClient: SupClient.ProjectClient, clientId: string, textArea: HTMLTextAreaElement, options: TextEditorWidgetOptions) {
54 | const extraKeys: { [name: string]: string|Function|boolean } = {
55 | "F9": () => { /* Disable line re-ordering */ },
56 | "Ctrl-T": false,
57 | "Tab": (cm: any) => {
58 | if (cm.getSelection() !== "") cm.execCommand("indentMore");
59 | else {
60 | if (this.useSoftTab) cm.execCommand("insertSoftTab");
61 | else cm.execCommand("insertTab");
62 | }
63 | },
64 | "Ctrl-Z": () => { this.undo(); },
65 | "Cmd-Z": () => { this.undo(); },
66 | "Shift-Ctrl-Z": () => { this.redo(); },
67 | "Shift-Cmd-Z": () => { this.redo(); },
68 | "Ctrl-Y": () => { this.redo(); },
69 | "Cmd-Y": () => { this.redo(); },
70 | "Alt-F": "findPersistent"
71 | };
72 | if (options.extraKeys != null) {
73 | for (const keyName in options.extraKeys) {
74 | extraKeys[keyName] = options.extraKeys[keyName];
75 | }
76 | }
77 |
78 | this.editCallback = options.editCallback;
79 | this.sendOperationCallback = options.sendOperationCallback;
80 |
81 | this.codeMirrorInstance = CodeMirror.fromTextArea(textArea, {
82 | lineNumbers: true,
83 | gutters: ["line-error-gutter", "CodeMirror-linenumbers", "CodeMirror-foldgutter"],
84 | indentWithTabs: false, indentUnit: 2, tabSize: 2,
85 | extraKeys: extraKeys,
86 | keyMap: textEditorUserSettings.pub.keyMap,
87 | viewportMargin: Infinity,
88 | mode: options.mode,
89 | readOnly: true
90 | });
91 |
92 | this.updateTheme();
93 |
94 | this.codeMirrorInstance.setOption("matchBrackets", true);
95 | this.codeMirrorInstance.setOption("styleActiveLine", true);
96 | this.codeMirrorInstance.setOption("autoCloseBrackets", true);
97 | this.codeMirrorInstance.setOption("foldGutter", true);
98 |
99 | this.codeMirrorInstance.on("changes", this.edit);
100 | this.codeMirrorInstance.on("beforeChange", this.beforeChange);
101 |
102 | this.setupAppMenu();
103 |
104 | this.clientId = clientId;
105 | projectClient.subResource("textEditorSettings", this);
106 |
107 | textEditorUserSettings.emitter.addListener("keyMap", () => {
108 | this.codeMirrorInstance.setOption("keyMap", textEditorUserSettings.pub.keyMap);
109 | });
110 |
111 | textEditorUserSettings.emitter.addListener("theme", () => {
112 | this.updateTheme();
113 | });
114 | }
115 |
116 | private setupAppMenu() {
117 | if (SupApp == null) return;
118 |
119 | const menu = SupApp.createMenu();
120 | menu.append(SupApp.createMenuItem({
121 | label: SupClient.i18n.t("common:actions.cut"),
122 | accelerator: "CmdOrCtrl+X",
123 | click: () => { document.execCommand("cut"); }
124 | }));
125 | menu.append(SupApp.createMenuItem({
126 | label: SupClient.i18n.t("common:actions.copy"),
127 | accelerator: "CmdOrCtrl+C",
128 | click: () => { document.execCommand("copy"); }
129 | }));
130 | menu.append(SupApp.createMenuItem({
131 | label: SupClient.i18n.t("common:actions.paste"),
132 | accelerator: "CmdOrCtrl+V",
133 | click: () => { document.execCommand("paste"); }
134 | }));
135 |
136 | const win = SupApp.getCurrentWindow();
137 |
138 | this.codeMirrorInstance.getWrapperElement().addEventListener("contextmenu", (event) => {
139 | event.preventDefault();
140 | menu.popup({ window: win });
141 | return false;
142 | });
143 | }
144 |
145 | private updateTheme() {
146 | if (textEditorUserSettings.pub.theme === "default") {
147 | this.linkElt.href = "";
148 | this.codeMirrorInstance.setOption("theme", textEditorUserSettings.pub.theme);
149 | } else {
150 | this.linkElt.href = `../../../../common/textEditorWidget/codemirror/theme/${textEditorUserSettings.pub.theme}.css`;
151 | this.codeMirrorInstance.setOption("theme", textEditorUserSettings.pub.theme);
152 | }
153 | }
154 |
155 | setText(text: string) {
156 | this.undoStack.length = 0;
157 | this.undoQuantityByAction.length = 0; this.undoQuantityByAction.push(0);
158 | this.redoStack.length = 0;
159 | this.redoQuantityByAction.length = 0; this.redoQuantityByAction.push(0);
160 |
161 | this.codeMirrorInstance.getDoc().setValue(text);
162 | this.codeMirrorInstance.setOption("readOnly", false);
163 |
164 | this.codeMirrorInstance.focus();
165 | }
166 |
167 | beforeChange = (instance: CodeMirror.Editor, change: any) => {
168 | if (change.origin === "setValue" || change.origin === "network") return;
169 | const lastText = instance.getDoc().getValue();
170 | if (lastText !== this.texts[this.texts.length - 1]) this.texts.push(lastText);
171 | }
172 |
173 | edit = (instance: CodeMirror.Editor, changes: CodeMirror.EditorChange[]) => {
174 | if (this.editCallback != null)
175 | this.editCallback(this.codeMirrorInstance.getDoc().getValue(), (changes[0]).origin);
176 |
177 | let undoRedo = false;
178 | let operationToSend: OT.TextOperation;
179 | for (let changeIndex = 0; changeIndex < changes.length; changeIndex++) {
180 | const change = changes[changeIndex];
181 |
182 | // Modification from an other person
183 | if (change.origin === "setValue" || change.origin === "network") continue;
184 |
185 | this.tmpCodeMirrorDoc.setValue(this.texts[changeIndex]);
186 |
187 | const operation = new OT.TextOperation(this.clientId);
188 | for (let line = 0; line < change.from.line; line++) operation.retain(this.tmpCodeMirrorDoc.getLine(line).length + 1);
189 | operation.retain(change.from.ch);
190 |
191 | let offset = 0;
192 | if (change.removed.length !== 1 || change.removed[0] !== "") {
193 | for (let index = 0; index < change.removed.length; index++) {
194 | const text = change.removed[index];
195 | if (index !== 0) {
196 | operation.delete("\n");
197 | offset += 1;
198 | }
199 |
200 | operation.delete(text);
201 | offset += text.length;
202 | }
203 | }
204 |
205 | if (change.text.length !== 1 || change.text[0] !== "") {
206 | for (let index = 0; index < change.text.length; index++) {
207 | if (index !== 0) operation.insert("\n");
208 | operation.insert(change.text[index]);
209 | }
210 | }
211 |
212 | const beforeLength = (operation.ops[0].attributes.amount != null) ? operation.ops[0].attributes.amount : 0;
213 | operation.retain(this.tmpCodeMirrorDoc.getValue().length - beforeLength - offset);
214 |
215 | if (operationToSend == null) operationToSend = operation.clone();
216 | else operationToSend = operationToSend.compose(operation);
217 |
218 | if (change.origin === "undo" || change.origin === "redo") undoRedo = true;
219 | }
220 |
221 | this.texts.length = 0;
222 | if (operationToSend == null) return;
223 |
224 | if (!undoRedo) {
225 | if (this.undoTimeout != null) {
226 | clearTimeout(this.undoTimeout);
227 | this.undoTimeout = null;
228 | }
229 |
230 | this.undoStack.push(operationToSend.clone().invert());
231 | this.undoQuantityByAction[this.undoQuantityByAction.length - 1] += 1;
232 | if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] > 20) this.undoQuantityByAction.push(0);
233 | else {
234 | this.undoTimeout = window.setTimeout(() => {
235 | this.undoTimeout = null;
236 | this.undoQuantityByAction.push(0);
237 | }, 500);
238 | }
239 |
240 | this.redoStack.length = 0;
241 | this.redoQuantityByAction.length = 0;
242 | }
243 |
244 | if (this.sentOperation == null) {
245 | this.sendOperationCallback(operationToSend.serialize());
246 |
247 | this.sentOperation = operationToSend;
248 | } else {
249 | if (this.pendingOperation != null) this.pendingOperation = this.pendingOperation.compose(operationToSend);
250 | else this.pendingOperation = operationToSend;
251 | }
252 | }
253 |
254 | receiveEditText(operationData: OperationData) {
255 | if (this.clientId === operationData.userId) {
256 | if (this.pendingOperation != null) {
257 | this.sendOperationCallback(this.pendingOperation.serialize());
258 |
259 | this.sentOperation = this.pendingOperation;
260 | this.pendingOperation = null;
261 | } else this.sentOperation = null;
262 | return;
263 | }
264 |
265 | // Transform operation and local changes
266 | let operation = new OT.TextOperation();
267 | operation.deserialize(operationData);
268 |
269 | if (this.sentOperation != null) {
270 | [this.sentOperation, operation] = this.sentOperation.transform(operation);
271 |
272 | if (this.pendingOperation != null) [this.pendingOperation, operation] = this.pendingOperation.transform(operation);
273 | }
274 | this.undoStack = transformStack(this.undoStack, operation);
275 | this.redoStack = transformStack(this.redoStack, operation);
276 |
277 | this.applyOperation(operation.clone(), "network", false);
278 | }
279 |
280 | applyOperation(operation: OT.TextOperation, origin: string, moveCursor: boolean) {
281 | let cursorPosition = 0;
282 | let line = 0;
283 | for (const op of operation.ops) {
284 | switch (op.type) {
285 | case "retain": {
286 | while (true) {
287 | if (op.attributes.amount <= this.codeMirrorInstance.getDoc().getLine(line).length - cursorPosition) break;
288 |
289 | op.attributes.amount -= this.codeMirrorInstance.getDoc().getLine(line).length + 1 - cursorPosition;
290 | cursorPosition = 0;
291 | line++;
292 | }
293 |
294 | cursorPosition += op.attributes.amount;
295 | }
296 | break;
297 |
298 | case "insert": {
299 | const cursor = this.codeMirrorInstance.getDoc().getCursor();
300 |
301 | const texts = op.attributes.text.split("\n");
302 | for (let textIndex = 0; textIndex < texts.length; textIndex++) {
303 | let text = texts[textIndex];
304 | if (textIndex !== texts.length - 1) text += "\n";
305 | (this.codeMirrorInstance).replaceRange(text, { line, ch: cursorPosition }, null, origin);
306 | cursorPosition += text.length;
307 |
308 | if (textIndex !== texts.length - 1) {
309 | cursorPosition = 0;
310 | line++;
311 | }
312 | }
313 |
314 | if (line === cursor.line && cursorPosition === cursor.ch) {
315 | if (!operation.gotPriority(this.clientId)) {
316 | for (let i = 0; i < op.attributes.text.length; i++) (this.codeMirrorInstance).execCommand("goCharLeft");
317 | }
318 | }
319 |
320 | if (moveCursor) (this.codeMirrorInstance).setCursor(line, cursorPosition);
321 | // use this way insted ? this.codeMirrorInstance.getDoc().setCursor({ line, ch: cursorPosition });
322 | }
323 | break;
324 |
325 | case "delete": {
326 | const texts = op.attributes.text.split("\n");
327 |
328 | for (let textIndex = 0; textIndex < texts.length; textIndex++) {
329 | const text = texts[textIndex];
330 | if (texts[textIndex + 1] != null) (this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line: line + 1, ch: 0 }, origin);
331 | else (this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line, ch: cursorPosition + text.length }, origin);
332 |
333 | if (moveCursor) (this.codeMirrorInstance).setCursor(line, cursorPosition);
334 | }
335 | break;
336 | }
337 | }
338 | }
339 | }
340 |
341 | undo() {
342 | if (this.undoStack.length === 0) return;
343 |
344 | if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] === 0) this.undoQuantityByAction.pop();
345 | const undoQuantityByAction = this.undoQuantityByAction[this.undoQuantityByAction.length - 1];
346 |
347 | for (let i = 0; i < undoQuantityByAction; i++) {
348 | const operationToUndo = this.undoStack[this.undoStack.length - 1];
349 | this.applyOperation(operationToUndo.clone(), "undo", true);
350 |
351 | this.undoStack.pop();
352 | this.redoStack.push(operationToUndo.invert());
353 | }
354 |
355 | if (this.undoTimeout != null) {
356 | clearTimeout(this.undoTimeout);
357 | this.undoTimeout = null;
358 | }
359 |
360 | this.redoQuantityByAction.push(this.undoQuantityByAction[this.undoQuantityByAction.length - 1]);
361 | this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = 0;
362 | }
363 |
364 | redo() {
365 | if (this.redoStack.length === 0) return;
366 |
367 | const redoQuantityByAction = this.redoQuantityByAction[this.redoQuantityByAction.length - 1];
368 | for (let i = 0; i < redoQuantityByAction; i++) {
369 | const operationToRedo = this.redoStack[this.redoStack.length - 1];
370 | this.applyOperation(operationToRedo.clone(), "undo", true);
371 |
372 | this.redoStack.pop();
373 | this.undoStack.push(operationToRedo.invert());
374 | }
375 |
376 | if (this.undoTimeout != null) {
377 | clearTimeout(this.undoTimeout);
378 | this.undoTimeout = null;
379 |
380 | this.undoQuantityByAction.push(this.redoQuantityByAction[this.redoQuantityByAction.length - 1]);
381 | }
382 | else this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = this.redoQuantityByAction[this.redoQuantityByAction.length - 1];
383 |
384 | this.undoQuantityByAction.push(0);
385 | this.redoQuantityByAction.pop();
386 | }
387 |
388 | clear() {
389 | if (this.undoTimeout != null) clearTimeout(this.undoTimeout);
390 | }
391 |
392 | onResourceReceived = (resourceId: string, resource: TextEditorSettingsResource) => {
393 | this.textEditorResource = resource;
394 |
395 | this.codeMirrorInstance.setOption("tabSize", resource.pub.tabSize);
396 | this.codeMirrorInstance.setOption("indentUnit", resource.pub.tabSize);
397 | this.codeMirrorInstance.setOption("indentWithTabs", !resource.pub.softTab);
398 | this.useSoftTab = resource.pub.softTab;
399 | }
400 |
401 | onResourceEdited = (resourceId: string, command: string, propertyName: string) => {
402 | switch (propertyName) {
403 | case "tabSize":
404 | this.codeMirrorInstance.setOption("tabSize", this.textEditorResource.pub.tabSize);
405 | this.codeMirrorInstance.setOption("indentUnit", this.textEditorResource.pub.tabSize);
406 | break;
407 | case "softTab":
408 | this.useSoftTab = this.textEditorResource.pub.softTab;
409 | this.codeMirrorInstance.setOption("indentWithTabs", !this.textEditorResource.pub.softTab);
410 | break;
411 | }
412 | }
413 | }
414 | export = TextEditorWidget;
415 |
416 | function transformStack(stack: OT.TextOperation[], operation: OT.TextOperation) {
417 | if (stack.length === 0) return stack;
418 |
419 | const newStack: OT.TextOperation[] = [];
420 | for (let i = stack.length - 1; i > 0; i--) {
421 | const pair = stack[i].transform(operation);
422 | newStack.push(pair[0]);
423 | operation = pair[1];
424 | }
425 | return newStack.reverse();
426 | }
427 |
--------------------------------------------------------------------------------
/three/helpers/TransformGizmos.ts:
--------------------------------------------------------------------------------
1 | const lineRadius = 0.015;
2 |
3 | type ColorName = "white"|"red"|"green"|"blue"|"yellow"|"cyan"|"magenta";
4 | const colors: { [colorName: string]: { enabled: number; disabled: number } } = {
5 | white: { enabled: 0xffffff, disabled: 0xffffff },
6 | red: { enabled: 0xe5432e, disabled: 0x646464 },
7 | green: { enabled: 0x5bd72f, disabled: 0xb0b0b0 },
8 | blue: { enabled: 0x3961d4, disabled: 0x606060 },
9 | yellow: { enabled: 0xffff00, disabled: 0xececec },
10 | cyan: { enabled: 0x00ffff, disabled: 0xc8c8c8 },
11 | magenta: { enabled: 0xff00ff, disabled: 0x484848 }
12 | };
13 |
14 | export class GizmoMaterial extends THREE.MeshBasicMaterial {
15 | private enabledColor: THREE.Color;
16 | private disabledColor: THREE.Color;
17 | private oldOpacity: number;
18 |
19 | constructor(parameters?: THREE.MeshBasicMaterialParameters) {
20 | super(parameters);
21 |
22 | this.transparent = true;
23 |
24 | this.setValues(parameters);
25 | this.enabledColor = this.color.clone();
26 | this.disabledColor = this.color.clone();
27 | this.oldOpacity = this.opacity;
28 | }
29 |
30 | setColor(colorName: ColorName) {
31 | this.color.setHex(colors[colorName].enabled);
32 | this.enabledColor.setHex(colors[colorName].enabled);
33 | this.disabledColor.setHex(colors[colorName].disabled);
34 | }
35 |
36 | highlight(highlighted: boolean) {
37 | if (highlighted) {
38 | this.color.setRGB(1, 1, 0);
39 | this.opacity = 1;
40 | } else {
41 | this.color.copy(this.enabledColor);
42 | this.opacity = this.oldOpacity;
43 | }
44 | }
45 |
46 | setDisabled(disabled: boolean) {
47 | this.color.copy(disabled ? this.disabledColor : this.enabledColor);
48 | }
49 | }
50 |
51 | const pickerMaterial = new GizmoMaterial({ visible: false, transparent: false, side: THREE.DoubleSide });
52 |
53 | export abstract class TransformGizmo extends THREE.Object3D {
54 | protected handlesRoot: THREE.Object3D;
55 | pickersRoot: THREE.Object3D;
56 | private planesRoot: THREE.Object3D;
57 |
58 | protected planes: { [name: string]: THREE.Mesh } = {};
59 | activePlane: THREE.Mesh;
60 |
61 | constructor() {
62 | super();
63 |
64 | this.handlesRoot = new THREE.Object3D();
65 | this.pickersRoot = new THREE.Object3D();
66 | this.planesRoot = new THREE.Object3D();
67 |
68 | this.add(this.handlesRoot);
69 | this.add(this.pickersRoot);
70 | this.add(this.planesRoot);
71 |
72 | // Planes
73 | const planeGeometry = new THREE.PlaneBufferGeometry(50, 50, 2, 2);
74 | const planeMaterial = new THREE.MeshBasicMaterial({ visible: false, side: THREE.DoubleSide });
75 |
76 | const planes: { [planeName: string]: THREE.Mesh; } = {
77 | "XY": new THREE.Mesh(planeGeometry, planeMaterial),
78 | "YZ": new THREE.Mesh(planeGeometry, planeMaterial),
79 | "XZ": new THREE.Mesh(planeGeometry, planeMaterial),
80 | "XYZE": new THREE.Mesh(planeGeometry, planeMaterial)
81 | };
82 |
83 | this.activePlane = planes["XYZE"];
84 |
85 | planes["YZ"].rotation.set(0, Math.PI / 2, 0);
86 | planes["XZ"].rotation.set(- Math.PI / 2, 0, 0);
87 |
88 | for (const planeName in planes) {
89 | planes[planeName].name = planeName;
90 | this.planesRoot.add(planes[planeName]);
91 | this.planes[planeName] = planes[planeName];
92 | }
93 |
94 | // Handles and Pickers
95 | this.initGizmos();
96 |
97 | // Reset Transformations
98 | this.traverse((child) => {
99 | child.layers.set(1);
100 |
101 | if (child instanceof THREE.Mesh) {
102 | child.updateMatrix();
103 |
104 | const tempGeometry = child.geometry.clone();
105 | tempGeometry.applyMatrix(child.matrix);
106 | child.geometry = tempGeometry;
107 |
108 | child.position.set(0, 0, 0);
109 | child.rotation.set(0, 0, 0);
110 | child.scale.set(1, 1, 1);
111 | }
112 | });
113 | }
114 |
115 | highlight(axis: string) {
116 | this.traverse((child: any) => {
117 | if (child.material != null && child.material.highlight != null) {
118 | child.material.highlight(child.name === axis);
119 | }
120 | });
121 | }
122 |
123 | setDisabled(disabled: boolean) {
124 | this.traverse((child: any) => {
125 | if (child.material != null && child.material.setDisabled != null) {
126 | child.material.setDisabled(disabled);
127 | }
128 | });
129 | }
130 |
131 | setupGizmo(name: string, object: THREE.Mesh, parent: THREE.Object3D, position?: [number, number, number], rotation?: [number, number, number], colorName?: ColorName) {
132 | object.name = name;
133 |
134 | if (position != null) object.position.set(position[0], position[1], position[2]);
135 | if (rotation != null) object.rotation.set(rotation[0], rotation[1], rotation[2]);
136 | if (colorName != null) (object.material as GizmoMaterial).setColor(colorName);
137 |
138 | parent.add(object);
139 | }
140 |
141 | update(rotation: THREE.Euler, eye: THREE.Vector3) {
142 | const vec1 = new THREE.Vector3(0, 0, 0);
143 | const vec2 = new THREE.Vector3(0, 1, 0);
144 | const lookAtMatrix = new THREE.Matrix4();
145 |
146 | this.traverse(function(child) {
147 | if (child.name.search("E") !== - 1) {
148 | child.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(eye, vec1, vec2));
149 | } else if (child.name.search("X") !== - 1 || child.name.search("Y") !== - 1 || child.name.search("Z") !== - 1) {
150 | child.quaternion.setFromEuler(rotation);
151 | }
152 | });
153 | }
154 |
155 | abstract initGizmos(): void;
156 | abstract setActivePlane(axis: string, eye: THREE.Vector3): void;
157 | }
158 |
159 | export class TransformGizmoTranslate extends TransformGizmo {
160 | initGizmos() {
161 | // Handles
162 | const geometry = new THREE.CylinderGeometry(0, 0.06, 0.2, 12, 1, false);
163 | const mesh = new THREE.Mesh(geometry);
164 | mesh.position.y = 0.5;
165 | mesh.updateMatrix();
166 |
167 | const arrowGeometry = new THREE.Geometry();
168 | arrowGeometry.merge(geometry, mesh.matrix);
169 |
170 | const lineGeometry = new THREE.CylinderGeometry(lineRadius, lineRadius, 1);
171 |
172 | this.setupGizmo("X", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
173 | this.setupGizmo("X", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
174 |
175 | this.setupGizmo("Y", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
176 | this.setupGizmo("Y", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
177 |
178 | this.setupGizmo("Z", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
179 | this.setupGizmo("Z", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
180 |
181 | const handlePlaneGeometry = new THREE.PlaneBufferGeometry(0.29, 0.29);
182 | this.setupGizmo("XY", new THREE.Mesh(handlePlaneGeometry, new GizmoMaterial({ opacity: 0.5, side: THREE.DoubleSide })), this.handlesRoot, [ 0.15, 0.15, 0 ], null, "yellow");
183 | this.setupGizmo("YZ", new THREE.Mesh(handlePlaneGeometry, new GizmoMaterial({ opacity: 0.5, side: THREE.DoubleSide })), this.handlesRoot, [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ], "cyan");
184 | this.setupGizmo("XZ", new THREE.Mesh(handlePlaneGeometry, new GizmoMaterial({ opacity: 0.5, side: THREE.DoubleSide })), this.handlesRoot, [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ], "magenta");
185 |
186 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.OctahedronGeometry(0.1, 0), new GizmoMaterial({ opacity: 0.8 })), this.handlesRoot, [ 0, 0, 0 ], [ 0, 0, 0 ], "white");
187 |
188 | // Pickers
189 | this.setupGizmo("X", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ]);
190 | this.setupGizmo("Y", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0.6, 0 ]);
191 | this.setupGizmo("Z", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ]);
192 |
193 | this.setupGizmo("XY", new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), this.pickersRoot, [ 0.2, 0.2, 0 ]);
194 | this.setupGizmo("YZ", new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), this.pickersRoot, [ 0, 0.2, 0.2 ], [ 0, Math.PI / 2, 0 ]);
195 | this.setupGizmo("XZ", new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), this.pickersRoot, [ 0.2, 0, 0.2 ], [ - Math.PI / 2, 0, 0 ]);
196 |
197 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.OctahedronGeometry(0.2, 0), pickerMaterial), this.pickersRoot);
198 | }
199 |
200 | setActivePlane(axis: string, eye: THREE.Vector3) {
201 | const tempMatrix = new THREE.Matrix4();
202 | eye.applyMatrix4(tempMatrix.getInverse(tempMatrix.extractRotation(this.planes["XY"].matrixWorld)) );
203 |
204 | switch (axis) {
205 | case "X":
206 | if (Math.abs(eye.y) > Math.abs(eye.z)) this.activePlane = this.planes["XZ"];
207 | else this.activePlane = this.planes["XY"];
208 | break;
209 | case "Y":
210 | if (Math.abs(eye.x) > Math.abs(eye.z)) this.activePlane = this.planes["YZ"];
211 | else this.activePlane = this.planes["XY"];
212 | break;
213 | case "Z":
214 | if (Math.abs(eye.x) > Math.abs(eye.y)) this.activePlane = this.planes["YZ"];
215 | else this.activePlane = this.planes["XZ"];
216 | break;
217 | case "XYZ":
218 | this.activePlane = this.planes["XYZE"];
219 | break;
220 | case "XY":
221 | case "YZ":
222 | case "XZ":
223 | this.activePlane = this.planes[axis];
224 | break;
225 | }
226 | }
227 | }
228 |
229 | export class TransformGizmoRotate extends TransformGizmo {
230 | initGizmos() {
231 | const radius = 0.7;
232 | const globalRadius = radius * 1.2;
233 |
234 | // Handles
235 | const ringGeometry = new THREE.TorusGeometry(radius, lineRadius, 4, 32);
236 | this.setupGizmo("X", new THREE.Mesh(ringGeometry, new GizmoMaterial({ side: THREE.DoubleSide })), this.handlesRoot, null, [ 0, -Math.PI / 2, -Math.PI / 2 ], "red");
237 | this.setupGizmo("Y", new THREE.Mesh(ringGeometry, new GizmoMaterial({ side: THREE.DoubleSide })), this.handlesRoot, null, [ Math.PI / 2, 0, 0 ], "green");
238 | this.setupGizmo("Z", new THREE.Mesh(ringGeometry, new GizmoMaterial({ side: THREE.DoubleSide })), this.handlesRoot, null, [ 0, 0, -Math.PI / 2 ], "blue");
239 |
240 | const globalRingGeometry = new THREE.RingGeometry(globalRadius - lineRadius, globalRadius + lineRadius, 32, 8);
241 | this.setupGizmo("E", new THREE.Mesh(globalRingGeometry, new GizmoMaterial({ opacity: 0.8, side: THREE.DoubleSide })), this.handlesRoot, null, null, "white");
242 |
243 | // Pickers
244 | const pickerThickness = 0.08;
245 |
246 | const torusGeometry = new THREE.TorusGeometry(radius, lineRadius * 2, 4, 16);
247 | this.setupGizmo("X", new THREE.Mesh(torusGeometry, pickerMaterial), this.pickersRoot, null, [ 0, - Math.PI / 2, - Math.PI / 2 ]);
248 | this.setupGizmo("Y", new THREE.Mesh(torusGeometry, pickerMaterial), this.pickersRoot, null, [ Math.PI / 2, 0, 0 ]);
249 | this.setupGizmo("Z", new THREE.Mesh(torusGeometry, pickerMaterial), this.pickersRoot, null, [ 0, 0, - Math.PI / 2 ]);
250 |
251 | const globalTorusGeometry = new THREE.RingGeometry(globalRadius - pickerThickness, globalRadius + pickerThickness, 16, 8);
252 | this.setupGizmo("E", new THREE.Mesh(globalTorusGeometry, pickerMaterial), this.pickersRoot);
253 | }
254 |
255 | setActivePlane(axis: string) {
256 | if (axis === "X") this.activePlane = this.planes["YZ"];
257 | else if (axis === "Y") this.activePlane = this.planes["XZ"];
258 | else if (axis === "Z") this.activePlane = this.planes["XY"];
259 | else if (axis === "E") this.activePlane = this.planes["XYZE"];
260 | }
261 | }
262 |
263 | export class TransformGizmoScale extends TransformGizmo {
264 | initGizmos() {
265 | // Handles
266 | const geometry = new THREE.BoxGeometry(0.125, 0.125, 0.125);
267 | const mesh = new THREE.Mesh(geometry);
268 | mesh.position.y = 0.5;
269 | mesh.updateMatrix();
270 |
271 | const arrowGeometry = new THREE.Geometry();
272 | arrowGeometry.merge(geometry, mesh.matrix);
273 |
274 | const lineGeometry = new THREE.CylinderGeometry(lineRadius, lineRadius, 1);
275 |
276 | this.setupGizmo("X", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
277 | this.setupGizmo("X", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
278 |
279 | this.setupGizmo("Y", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
280 | this.setupGizmo("Y", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
281 |
282 | this.setupGizmo("Z", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
283 | this.setupGizmo("Z", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
284 |
285 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.OctahedronGeometry(0.1, 0), new GizmoMaterial({ opacity: 0.8 })), this.handlesRoot, [ 0, 0, 0 ], [ 0, 0, 0 ], "white");
286 |
287 | // Pickers
288 | this.setupGizmo("X", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ]);
289 | this.setupGizmo("Y", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0.6, 0 ]);
290 | this.setupGizmo("Z", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ]);
291 |
292 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), pickerMaterial), this.pickersRoot);
293 | }
294 |
295 | setActivePlane(axis: string, eye: THREE.Vector3) {
296 | const tempMatrix = new THREE.Matrix4();
297 | eye.applyMatrix4(tempMatrix.getInverse(tempMatrix.extractRotation(this.planes["XY"].matrixWorld)));
298 |
299 | if (axis === "X") {
300 | if (Math.abs(eye.y) > Math.abs(eye.z)) this.activePlane = this.planes["XZ"];
301 | else this.activePlane = this.planes["XY"];
302 |
303 | } else if (axis === "Y") {
304 | if (Math.abs(eye.x) > Math.abs(eye.z)) this.activePlane = this.planes["YZ"];
305 | else this.activePlane = this.planes["XY"];
306 |
307 | } else if (axis === "Z") {
308 | if (Math.abs(eye.x) > Math.abs(eye.y)) this.activePlane = this.planes["YZ"];
309 | else this.activePlane = this.planes["XZ"];
310 |
311 | } else if (axis === "XYZ") this.activePlane = this.planes["XYZE"];
312 | }
313 | }
314 |
315 | export class TransformGizmoResize extends TransformGizmo {
316 | initGizmos() {
317 | // Handles
318 | const geometry = new THREE.BoxGeometry(0.2, 0.03, 0.2);
319 | const mesh = new THREE.Mesh(geometry);
320 | mesh.position.y = 0.5;
321 | mesh.updateMatrix();
322 |
323 | const arrowGeometry = new THREE.Geometry();
324 | arrowGeometry.merge(geometry, mesh.matrix);
325 |
326 | const lineGeometry = new THREE.CylinderGeometry(lineRadius, lineRadius, 1);
327 |
328 | this.setupGizmo("X", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
329 | this.setupGizmo("X", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ], "red");
330 |
331 | this.setupGizmo("Y", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
332 | this.setupGizmo("Y", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0.5, 0 ], null, "green");
333 |
334 | this.setupGizmo("Z", new THREE.Mesh(arrowGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
335 | this.setupGizmo("Z", new THREE.Mesh(lineGeometry, new GizmoMaterial()), this.handlesRoot, [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ], "blue");
336 |
337 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.OctahedronGeometry(0.1, 0), new GizmoMaterial({ opacity: 0.8 })), this.handlesRoot, [ 0, 0, 0 ], [ 0, 0, 0 ], "white");
338 |
339 | // Pickers
340 | this.setupGizmo("X", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ]);
341 | this.setupGizmo("Y", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0.6, 0 ]);
342 | this.setupGizmo("Z", new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), this.pickersRoot, [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ]);
343 |
344 | this.setupGizmo("XYZ", new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), pickerMaterial), this.pickersRoot);
345 | }
346 |
347 | setActivePlane(axis: string, eye: THREE.Vector3) {
348 | const tempMatrix = new THREE.Matrix4();
349 | eye.applyMatrix4(tempMatrix.getInverse(tempMatrix.extractRotation(this.planes["XY"].matrixWorld)));
350 |
351 | if (axis === "X") {
352 | if (Math.abs(eye.y) > Math.abs(eye.z)) this.activePlane = this.planes["XZ"];
353 | else this.activePlane = this.planes["XY"];
354 |
355 | } else if (axis === "Y") {
356 | if (Math.abs(eye.x) > Math.abs(eye.z)) this.activePlane = this.planes["YZ"];
357 | else this.activePlane = this.planes["XY"];
358 |
359 | } else if (axis === "Z") {
360 | if (Math.abs(eye.x) > Math.abs(eye.y)) this.activePlane = this.planes["YZ"];
361 | else this.activePlane = this.planes["XZ"];
362 |
363 | } else if (axis === "XYZ") this.activePlane = this.planes["XYZE"];
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/three/helpers/TransformControls.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * https://github.com/mrdoob/three.js/blob/master/examples/js/controls/TransformControls.js
3 | * Rewritten in TypeScript and modified by bilou84
4 | */
5 |
6 | import { TransformGizmo, TransformGizmoTranslate, TransformGizmoRotate, TransformGizmoScale, TransformGizmoResize } from "./TransformGizmos";
7 |
8 | const ray = new THREE.Raycaster();
9 | const pointerVector = new THREE.Vector2();
10 |
11 | const point = new THREE.Vector3();
12 | const offset = new THREE.Vector3();
13 |
14 | const rotation = new THREE.Vector3();
15 | const offsetRotation = new THREE.Vector3();
16 |
17 | const lookAtMatrix = new THREE.Matrix4();
18 | const eye = new THREE.Vector3();
19 |
20 | const tempMatrix = new THREE.Matrix4();
21 | const tempVector = new THREE.Vector3();
22 | const tempQuaternion = new THREE.Quaternion();
23 | const unitX = new THREE.Vector3(1, 0, 0);
24 | const unitY = new THREE.Vector3(0, 1, 0);
25 | const unitZ = new THREE.Vector3(0, 0, 1);
26 |
27 | const quaternionXYZ = new THREE.Quaternion();
28 | const quaternionX = new THREE.Quaternion();
29 | const quaternionY = new THREE.Quaternion();
30 | const quaternionZ = new THREE.Quaternion();
31 | const quaternionE = new THREE.Quaternion();
32 |
33 | const oldPosition = new THREE.Vector3();
34 | const oldScale = new THREE.Vector3();
35 | const oldRotationMatrix = new THREE.Matrix4();
36 |
37 | const parentRotationMatrix = new THREE.Matrix4();
38 | const parentScale = new THREE.Vector3();
39 |
40 | const worldPosition = new THREE.Vector3();
41 | const worldRotation = new THREE.Euler();
42 | const worldRotationMatrix = new THREE.Matrix4();
43 | const camPosition = new THREE.Vector3();
44 | const camRotation = new THREE.Euler();
45 |
46 | export default class TransformControls extends THREE.Object3D {
47 | visible = false;
48 | translationSnap: number;
49 | rotationSnap: number;
50 | root = new THREE.Object3D();
51 |
52 | private target: THREE.Object3D;
53 | private externVisible = true;
54 | private size = 1;
55 | private axis: string;
56 | private mode: "translate"|"rotate"|"scale"|"resize" = "translate";
57 | private space = "local";
58 | private dragging = false;
59 | private disabled = true;
60 | private gizmo: { [name: string]: TransformGizmo; } = {
61 | "translate": new TransformGizmoTranslate(),
62 | "rotate": new TransformGizmoRotate(),
63 | "scale": new TransformGizmoScale(),
64 | "resize": new TransformGizmoResize()
65 | };
66 |
67 | private changeEvent = { type: "change", target: null as any };
68 | private mouseDownEvent = { type: "mouseDown", target: null as any };
69 | private mouseUpEvent = { type: "mouseUp", mode: this.mode, target: null as any };
70 | private objectChangeEvent = { type: "objectChange", target: null as any };
71 |
72 | constructor(scene: THREE.Scene, private camera: SupTHREE.Camera, private domElement: HTMLElement) {
73 | super();
74 |
75 | scene.add(this);
76 | scene.add(this.root);
77 |
78 | for (const type in this.gizmo) {
79 | const gizmoObj = this.gizmo[type];
80 | gizmoObj.visible = (type === this.mode);
81 | this.add(gizmoObj);
82 | }
83 |
84 | this.enable();
85 | }
86 |
87 | dispose() {
88 | this.disable();
89 | }
90 |
91 | setVisible(visible: boolean) {
92 | this.externVisible = visible;
93 | this.visible = this.externVisible && this.target != null;
94 | return this;
95 | }
96 |
97 | attach(object: THREE.Object3D) {
98 | this.target = object;
99 | this.visible = this.externVisible;
100 | this.update();
101 | return this;
102 | }
103 |
104 | detach() {
105 | this.target = null;
106 | this.visible = false;
107 | this.axis = null;
108 | return this;
109 | }
110 |
111 | getMode() { return this.mode; }
112 |
113 | setMode(mode: "translate"|"rotate"|"scale"|"resize") {
114 | this.mode = mode;
115 |
116 | for (const type in this.gizmo) this.gizmo[type].visible = (type === mode);
117 |
118 | if (this.target == null) return this;
119 | this.update();
120 | this.dispatchEvent(this.changeEvent);
121 | return this;
122 | }
123 |
124 | setSize(size: number) {
125 | this.size = size;
126 | if (this.target == null) return this;
127 |
128 | this.update();
129 | this.dispatchEvent(this.changeEvent);
130 | return this;
131 | }
132 |
133 | setSpace(space: string) {
134 | this.space = space;
135 | if (this.target == null) return this;
136 |
137 | this.update();
138 | this.dispatchEvent(this.changeEvent);
139 | return this;
140 | }
141 |
142 | enable() {
143 | if (!this.disabled) return this;
144 |
145 | this.domElement.addEventListener("mousedown", this.onPointerDown, false);
146 | this.domElement.addEventListener("touchstart", this.onPointerDown, false);
147 |
148 | this.domElement.addEventListener("mousemove", this.onPointerHover, false);
149 | this.domElement.addEventListener("touchmove", this.onPointerHover, false);
150 |
151 | this.domElement.addEventListener("mousemove", this.onPointerMove, false);
152 | this.domElement.addEventListener("touchmove", this.onPointerMove, false);
153 |
154 | this.domElement.addEventListener("mouseup", this.onPointerUp, false);
155 | this.domElement.addEventListener("mouseout", this.onPointerUp, false);
156 | this.domElement.addEventListener("touchend", this.onPointerUp, false);
157 | this.domElement.addEventListener("touchcancel", this.onPointerUp, false);
158 | this.domElement.addEventListener("touchleave", this.onPointerUp, false);
159 |
160 | this.dragging = false;
161 | this.disabled = false;
162 | for (const gizmoName in this.gizmo) this.gizmo[gizmoName].setDisabled(false);
163 |
164 | return this;
165 | }
166 |
167 | disable() {
168 | if (this.disabled) return this;
169 |
170 | this.domElement.removeEventListener("mousedown", this.onPointerDown);
171 | this.domElement.removeEventListener("touchstart", this.onPointerDown);
172 |
173 | this.domElement.removeEventListener("mousemove", this.onPointerHover);
174 | this.domElement.removeEventListener("touchmove", this.onPointerHover);
175 |
176 | this.domElement.removeEventListener("mousemove", this.onPointerMove);
177 | this.domElement.removeEventListener("touchmove", this.onPointerMove);
178 |
179 | this.domElement.removeEventListener("mouseup", this.onPointerUp);
180 | this.domElement.removeEventListener("mouseout", this.onPointerUp);
181 | this.domElement.removeEventListener("touchend", this.onPointerUp);
182 | this.domElement.removeEventListener("touchcancel", this.onPointerUp);
183 | this.domElement.removeEventListener("touchleave", this.onPointerUp);
184 |
185 | this.dragging = false;
186 | this.disabled = true;
187 | for (const gizmoName in this.gizmo) this.gizmo[gizmoName].setDisabled(true);
188 |
189 | return this;
190 | }
191 |
192 | update(copyTarget = true) {
193 | if (this.target == null) return;
194 |
195 | if (copyTarget) {
196 | this.root.position.copy(this.target.getWorldPosition());
197 | this.root.quaternion.copy(this.target.getWorldQuaternion());
198 |
199 | const width = this.target.userData.width;
200 | const height = this.target.userData.height;
201 | const depth = this.target.userData.depth;
202 |
203 | if (this.mode === "resize") {
204 | this.root.scale.x = Math.abs(width);
205 | this.root.scale.y = Math.abs(height);
206 | this.root.scale.z = Math.abs(depth);
207 | } else {
208 | this.root.scale.x = (width < 0 ? -1 : 1) * this.target.scale.x;
209 | this.root.scale.y = (height < 0 ? -1 : 1) * this.target.scale.y;
210 | this.root.scale.z = (depth < 0 ? -1 : 1) * this.target.scale.z;
211 | }
212 | }
213 | this.root.updateMatrixWorld(false);
214 | worldPosition.setFromMatrixPosition(this.root.matrixWorld);
215 |
216 | // NOTE: Workaround for negative scales messing with extracted rotation — elisee
217 | const scaleX = this.root.scale.x / Math.abs(this.root.scale.x);
218 | const scaleY = this.root.scale.y / Math.abs(this.root.scale.y);
219 | const scaleZ = this.root.scale.z / Math.abs(this.root.scale.z);
220 | const negativeScaleFixMatrix = new THREE.Matrix4().makeScale(scaleX, scaleY, scaleZ);
221 | worldRotation.setFromRotationMatrix(tempMatrix.extractRotation(this.root.matrixWorld).multiply(negativeScaleFixMatrix));
222 |
223 | this.camera.threeCamera.updateMatrixWorld(false);
224 | camPosition.setFromMatrixPosition(this.camera.threeCamera.matrixWorld);
225 | camRotation.setFromRotationMatrix(tempMatrix.extractRotation(this.camera.threeCamera.matrixWorld));
226 |
227 | const scale = worldPosition.distanceTo(camPosition) / 8 * this.size;
228 | this.position.copy(worldPosition);
229 | this.scale.set(scale, scale, scale);
230 |
231 | eye.copy(camPosition).sub(worldPosition).normalize();
232 |
233 | if (this.space === "local" || this.mode === "scale" || this.mode === "resize") this.gizmo[this.mode].update(worldRotation, eye);
234 | else if (this.space === "world") this.gizmo[this.mode].update(new THREE.Euler(), eye);
235 |
236 | if (!this.disabled) this.gizmo[this.mode].highlight(this.axis);
237 |
238 | this.updateMatrixWorld(true);
239 | }
240 |
241 | private onPointerDown = (event: MouseEvent|TouchEvent) => {
242 | if (this.target == null || this.dragging || ((event as MouseEvent).button != null && (event as MouseEvent).button !== 0) || event.altKey) return;
243 |
244 | const pointer: MouseEvent = (event as any).changedTouches ? (event as any).changedTouches[ 0 ] : event;
245 |
246 | if (pointer.button === 0 || pointer.button == null) {
247 | const intersect = this.intersectObjects(pointer, this.gizmo[this.mode].pickersRoot.children);
248 |
249 | if (intersect != null) {
250 | event.preventDefault();
251 | event.stopPropagation();
252 |
253 | this.dispatchEvent(this.mouseDownEvent);
254 | this.axis = intersect.object.name;
255 | this.update();
256 |
257 | eye.copy(camPosition).sub(worldPosition).normalize();
258 |
259 | this.gizmo[this.mode].setActivePlane(this.axis, eye);
260 |
261 | const planeIntersect = this.intersectObjects(pointer, [ this.gizmo[this.mode].activePlane ]);
262 |
263 | if (planeIntersect != null) {
264 | oldPosition.copy(this.root.position);
265 | oldScale.copy(this.root.scale);
266 |
267 | oldRotationMatrix.extractRotation(this.root.matrix);
268 | worldRotationMatrix.extractRotation(this.root.matrixWorld);
269 |
270 | parentRotationMatrix.extractRotation(this.root.parent.matrixWorld);
271 | parentScale.setFromMatrixScale(tempMatrix.getInverse(this.root.parent.matrixWorld));
272 |
273 | offset.copy(planeIntersect.point);
274 | }
275 | }
276 | }
277 |
278 | this.dragging = true;
279 | }
280 |
281 | private onPointerHover = (event: MouseEvent|TouchEvent) => {
282 | if (this.target == null || this.dragging || ((event as MouseEvent).button != null && (event as MouseEvent).button !== 0)) return;
283 |
284 | const pointer: MouseEvent = (event as any).changedTouches ? (event as any).changedTouches[ 0 ] : event;
285 |
286 | let newAxis: string;
287 | const intersect = this.intersectObjects(pointer, this.gizmo[this.mode].pickersRoot.children);
288 |
289 | if (intersect != null) {
290 | newAxis = intersect.object.name;
291 | event.preventDefault();
292 | }
293 |
294 | if (this.axis !== newAxis) {
295 | this.axis = newAxis;
296 | this.update();
297 | this.dispatchEvent(this.changeEvent);
298 | }
299 | }
300 |
301 | private onPointerMove = (event: MouseEvent|TouchEvent) => {
302 | if (this.target == null || this.axis == null || !this.dragging || ((event as MouseEvent).button != null && (event as MouseEvent).button !== 0)) return;
303 |
304 | const pointer: MouseEvent = (event as any).changedTouches ? (event as any).changedTouches[ 0 ] : event;
305 |
306 | const planeIntersect = this.intersectObjects(pointer, [ this.gizmo[this.mode].activePlane ]);
307 | if (planeIntersect == null) return;
308 |
309 | event.preventDefault();
310 | event.stopPropagation();
311 |
312 | point.copy(planeIntersect.point);
313 |
314 | switch (this.mode) {
315 | case "translate": {
316 | point.sub(offset);
317 | point.multiply(parentScale);
318 |
319 | if (this.space === "local") {
320 | point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
321 |
322 | if (this.axis.search("X") === -1) point.x = 0;
323 | if (this.axis.search("Y") === -1) point.y = 0;
324 | if (this.axis.search("Z") === -1) point.z = 0;
325 |
326 | point.applyMatrix4(oldRotationMatrix);
327 |
328 | this.root.position.copy(oldPosition);
329 | this.root.position.add(point);
330 | }
331 |
332 | if (this.space === "world" || this.axis.search("XYZ") !== -1) {
333 | if (this.axis.search("X") === -1) point.x = 0;
334 | if (this.axis.search("Y") === -1) point.y = 0;
335 | if (this.axis.search("Z") === -1) point.z = 0;
336 |
337 | point.applyMatrix4(tempMatrix.getInverse(parentRotationMatrix));
338 |
339 | this.root.position.copy(oldPosition);
340 | this.root.position.add(point);
341 | }
342 |
343 | if (this.translationSnap !== null) {
344 | if (this.space === "local") this.root.position.sub(worldPosition).applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
345 |
346 | if (this.axis.search("X") !== -1) this.root.position.x = Math.round(this.root.position.x / this.translationSnap) * this.translationSnap;
347 | if (this.axis.search("Y") !== -1) this.root.position.y = Math.round(this.root.position.y / this.translationSnap) * this.translationSnap;
348 | if (this.axis.search("Z") !== -1) this.root.position.z = Math.round(this.root.position.z / this.translationSnap) * this.translationSnap;
349 |
350 | if (this.space === "local") this.root.position.applyMatrix4(worldRotationMatrix).add(worldPosition);
351 | }
352 | } break;
353 |
354 | case "scale": {
355 | point.sub(offset);
356 | point.multiply(parentScale);
357 |
358 | if (this.axis === "XYZ") {
359 | const scale = 1 + ( (point.y) / Math.max(oldScale.x, oldScale.y, oldScale.z));
360 |
361 | this.root.scale.x = oldScale.x * scale;
362 | this.root.scale.y = oldScale.y * scale;
363 | this.root.scale.z = oldScale.z * scale;
364 | } else {
365 | point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
366 |
367 | if (this.axis === "X") this.root.scale.x = oldScale.x * (1 + point.x / oldScale.x);
368 | if (this.axis === "Y") this.root.scale.y = oldScale.y * (1 + point.y / oldScale.y);
369 | if (this.axis === "Z") this.root.scale.z = oldScale.z * (1 + point.z / oldScale.z);
370 | }
371 | } break;
372 |
373 | case "resize": {
374 | point.sub(offset);
375 | point.multiply(parentScale);
376 |
377 | const multiplier = 16;
378 |
379 | if (this.axis === "XYZ") {
380 | const scale = 1 + ( (point.y) / Math.max(oldScale.x, oldScale.y, oldScale.z) * multiplier);
381 |
382 | this.root.scale.x = oldScale.x * scale;
383 | this.root.scale.y = oldScale.y * scale;
384 | this.root.scale.z = oldScale.z * scale;
385 | } else {
386 | point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
387 |
388 | if (this.axis === "X") this.root.scale.x = oldScale.x * (1 + point.x / oldScale.x * multiplier);
389 | if (this.axis === "Y") this.root.scale.y = oldScale.y * (1 + point.y / oldScale.y * multiplier);
390 | if (this.axis === "Z") this.root.scale.z = oldScale.z * (1 + point.z / oldScale.z * multiplier);
391 | }
392 |
393 | this.root.scale.x = Math.round(Math.max(1, this.root.scale.x));
394 | this.root.scale.y = Math.round(Math.max(1, this.root.scale.y));
395 | this.root.scale.z = Math.round(Math.max(1, this.root.scale.z));
396 | } break;
397 |
398 | case "rotate": {
399 | point.sub(worldPosition);
400 | point.multiply(parentScale);
401 | tempVector.copy(offset).sub(worldPosition);
402 | tempVector.multiply(parentScale);
403 |
404 | if (this.axis === "E") {
405 | point.applyMatrix4(tempMatrix.getInverse(lookAtMatrix));
406 | tempVector.applyMatrix4(tempMatrix.getInverse(lookAtMatrix));
407 |
408 | rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
409 | offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));
410 |
411 | tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));
412 |
413 | quaternionE.setFromAxisAngle(eye, rotation.z - offsetRotation.z);
414 | quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);
415 |
416 | tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionE);
417 | tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);
418 |
419 | this.root.quaternion.copy(tempQuaternion);
420 |
421 | } else if (this.axis === "XYZE") {
422 | quaternionE.setFromEuler(point.clone().cross(tempVector).normalize() as any); // rotation axis
423 |
424 | tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));
425 | quaternionX.setFromAxisAngle(quaternionE as any, - point.clone().angleTo(tempVector));
426 | quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);
427 |
428 | tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionX);
429 | tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);
430 |
431 | this.root.quaternion.copy(tempQuaternion);
432 |
433 | } else if (this.space === "local") {
434 | point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
435 |
436 | tempVector.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));
437 |
438 | rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
439 | offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));
440 |
441 | quaternionXYZ.setFromRotationMatrix(oldRotationMatrix);
442 |
443 | if (this.rotationSnap !== null) {
444 | quaternionX.setFromAxisAngle(unitX, Math.round( (rotation.x - offsetRotation.x) / this.rotationSnap) * this.rotationSnap);
445 | quaternionY.setFromAxisAngle(unitY, Math.round( (rotation.y - offsetRotation.y) / this.rotationSnap) * this.rotationSnap);
446 | quaternionZ.setFromAxisAngle(unitZ, Math.round( (rotation.z - offsetRotation.z) / this.rotationSnap) * this.rotationSnap);
447 | } else {
448 | quaternionX.setFromAxisAngle(unitX, rotation.x - offsetRotation.x);
449 | quaternionY.setFromAxisAngle(unitY, rotation.y - offsetRotation.y);
450 | quaternionZ.setFromAxisAngle(unitZ, rotation.z - offsetRotation.z);
451 | }
452 |
453 | if (this.axis === "X") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionX);
454 | if (this.axis === "Y") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionY);
455 | if (this.axis === "Z") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionZ);
456 |
457 | this.root.quaternion.copy(quaternionXYZ);
458 |
459 | } else if (this.space === "world") {
460 | rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
461 | offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));
462 |
463 | tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));
464 |
465 | if (this.rotationSnap !== null) {
466 | quaternionX.setFromAxisAngle(unitX, Math.round( (rotation.x - offsetRotation.x) / this.rotationSnap) * this.rotationSnap);
467 | quaternionY.setFromAxisAngle(unitY, Math.round( (rotation.y - offsetRotation.y) / this.rotationSnap) * this.rotationSnap);
468 | quaternionZ.setFromAxisAngle(unitZ, Math.round( (rotation.z - offsetRotation.z) / this.rotationSnap) * this.rotationSnap);
469 | } else {
470 | quaternionX.setFromAxisAngle(unitX, rotation.x - offsetRotation.x);
471 | quaternionY.setFromAxisAngle(unitY, rotation.y - offsetRotation.y);
472 | quaternionZ.setFromAxisAngle(unitZ, rotation.z - offsetRotation.z);
473 | }
474 |
475 | quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);
476 |
477 | if (this.axis === "X") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionX);
478 | if (this.axis === "Y") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionY);
479 | if (this.axis === "Z") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionZ);
480 |
481 | tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);
482 |
483 | this.root.quaternion.copy(tempQuaternion);
484 | }
485 | } break;
486 | }
487 |
488 | this.update(false);
489 | this.dispatchEvent(this.changeEvent);
490 | this.dispatchEvent(this.objectChangeEvent);
491 | }
492 |
493 | private onPointerUp = (event: MouseEvent) => {
494 | if (event.button != null && event.button !== 0 || event.altKey) return;
495 |
496 | if (this.dragging && (this.axis !== null)) {
497 | this.mouseUpEvent.mode = this.mode;
498 | this.dispatchEvent(this.mouseUpEvent);
499 | }
500 |
501 | this.dragging = false;
502 | this.onPointerHover(event);
503 | }
504 |
505 | private intersectObjects(pointer: MouseEvent, objects: THREE.Object3D[]) {
506 | const rect = this.domElement.getBoundingClientRect();
507 | const viewport = this.camera.getViewport();
508 |
509 | pointerVector.x = ((pointer.clientX - rect.left) / rect.width * 2 - 1) / viewport.width;
510 | pointerVector.y = -((pointer.clientY - rect.top) / rect.height * 2 - 1) / viewport.height;
511 | ray.setFromCamera(pointerVector, this.camera.threeCamera);
512 |
513 | const intersections = ray.intersectObjects(objects, true);
514 | return intersections[0];
515 | }
516 | }
517 |
--------------------------------------------------------------------------------