├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── Color.ts ├── Config.ts ├── ContextMenu.ts ├── Editor.ts ├── Event.ts ├── Exporter.ts ├── History.ts ├── Loader.ts ├── Settings.ts ├── StencilPlane.ts ├── Types.ts ├── commands │ ├── AddObjectCommand.ts │ ├── Command.ts │ ├── RemoveObjectCommand.ts │ ├── SetPositionCommand.ts │ ├── SetRotationCommand.ts │ ├── SetScaleCommand.ts │ ├── SetSceneCommand.ts │ ├── SetUuidCommand.ts │ ├── SetValueCommand.ts │ ├── index.ts │ └── types.ts ├── controls │ ├── EditorControls.ts │ └── ViewCubeControls.ts ├── defaultConfig.ts ├── defaultSettings.ts ├── index.ts └── utils │ ├── throttle.ts │ └── viewportUtils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | dist 3 | node_modules 4 | .pnp 5 | .pnp.js 6 | 7 | # testing 8 | coverage 9 | 10 | # production 11 | build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2020 three.js authors 4 | Copyright (c) 2021 BAUES Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # building-editor 2 | 3 | [![npm version](https://badge.fury.io/js/building-editor.svg)](https://badge.fury.io/js/building-editor) 4 | 5 | The goal of this project is to provide base implementation of web 3D editor for building/architecture which can be used easily. The codes are based on [three.js](https://github.com/mrdoob/three.js) editor fork, as we respect the great work of three.js. 6 | 7 | > Note: This project is under development. Please remember that there would be breaking changes. Or you can join us to make this project better for users. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install building-editor 13 | ``` 14 | 15 | ## Usage 16 | 17 | [Sample code](https://codesandbox.io/s/sad-fast-t1eh0) 18 | 19 | ```js 20 | import { Editor } from 'building-editor'; 21 | 22 | const editor = new Editor(); 23 | document.body.appendChild(editor.renderer.domElement); 24 | 25 | const init = () => { 26 | const width = window.innerWidth; 27 | const height = window.innerHeight; 28 | editor.renderer.setPixelRatio(window.devicePixelRatio); 29 | editor.renderer.setSize(width, height); 30 | editor.render(); 31 | } 32 | 33 | init(); 34 | ``` 35 | 36 | ## API 37 | 38 | ### Editor 39 | 40 | The main API of this library to create web 3D editor. This includes properties and actions. Note that you need to implement user interactions such as selected, hovered etc., using addEventListener since Editor itself does not provide it. 41 | 42 | #### Constructor 43 | 44 | ##### Editor(config:[Config](#Config),settings:[Settings](#Settings)). 45 | 46 | This creates a new Editor. 47 | 48 | config - configuration data to specify cotrolability of editor (e.g. undo/redo, delete etc). 49 | settings - setting data which summarize view setting such as renderer, camera, scene etc. 50 | 51 | #### Properties 52 | 53 | ##### .config:[Config](#Config). 54 | configuration data to specify cotrolability of editor (e.g. undo/redo, delete etc). 55 | 56 | ##### .settings:[Settings](#Settings). 57 | setting data which summarize view setting such as renderer, camera, scene etc. 58 | 59 | ##### .editorControls:[EditorControls](#EditorControls). 60 | extension of [THREE.EventDispatcher](https://threejs.org/docs/#api/en/core/EventDispatcher) 61 | 62 | ##### .renderer:[THREE.WebGLRenderer](https://threejs.org/docs/index.html?q=webGL#api/en/renderers/WebGLRenderer). 63 | 64 | ##### .DEFAULT_CAMERA:[THREE.Camera](https://threejs.org/docs/#api/en/cameras/Camera). 65 | 66 | ##### .history:[History](#History). 67 | Manage undo/redo history 68 | 69 | ##### .exporter:[Exporter](#Exporter). 70 | Utility class to export geometry in different format (e.g. obj, stl, dae etc) 71 | 72 | ##### .loader:[Loader](#Loader). 73 | Utility class to load geometry file into editor 74 | 75 | ##### .camera:[THREE.Camera](https://threejs.org/docs/#api/en/cameras/Camera). 76 | 77 | ##### .scene:[THREE.Scene](https://threejs.org/docs/?q=scene#api/en/scenes/Scene). 78 | 79 | ##### .sceneHelpers:[THREE.Scene](https://threejs.org/docs/?q=scene#api/en/scenes/Scene). 80 | 81 | ##### .objects:[THREE.Object3D[]](https://threejs.org/docs/?q=object3#api/en/core/Object3D). 82 | 83 | ##### .INITIAL_OBJECTS:[THREE.Object3D[]](https://threejs.org/docs/?q=object3#api/en/core/Object3D). 84 | 85 | ##### .INITIAL_HELPERS:[THREE.Object3D[]](https://threejs.org/docs/?q=object3#api/en/core/Object3D). 86 | 87 | ##### .geometries:{[index:string]:[THREE.BufferGeometry](https://threejs.org/docs/?q=geometr#api/en/core/BufferGeometry)} 88 | 89 | ##### .materials:{[index:string]:[THREE.Material](https://threejs.org/docs/?q=material#api/en/constants/Materials)} 90 | 91 | ##### .textures:{[index:string]:[THREE.Texture](https://threejs.org/docs/?q=material#api/en/constants/Textures)} 92 | 93 | ##### .materialsRefCounter: Map<[THREE.Material](https://threejs.org/docs/?q=material#api/en/constants/Materials),number> 94 | 95 | ##### aminations: {[index:string]:[THREE.AnimationClip](https://threejs.org/docs/#api/en/animation/AnimationClip)[]} 96 | 97 | ##### mixer: [THREE.AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) 98 | 99 | ##### selected: [THREE.Object3D](https://threejs.org/docs/?q=object3#api/en/core/Object3D) | null 100 | selected object in editor 101 | 102 | ##### hovered: [THREE.Object3D](https://threejs.org/docs/?q=object3#api/en/core/Object3D) | null 103 | hovered object in editor 104 | 105 | ##### helpers: {[index:string]: Helper} 106 | summarize following three helpers 107 | - THREE.CameraHelper 108 | - THREE.PointLightHelper 109 | - THREE.DirectionalLightHelper 110 | - THREE.SpotLightHelper 111 | - THREE.HemisphereLightHelper 112 | - THREE.SkeltonHelper 113 | 114 | ##### cameras: {[index:string]: [THREE.Camera](https://threejs.org/docs/#api/en/cameras/Camera)} 115 | 116 | ##### viewportCameras:[THREE.Camera](https://threejs.org/docs/#api/en/cameras/Camera) 117 | 118 | ##### orbitControls: [THREE.OrbitControls](https://threejs.org/docs/#examples/en/controls/OrbitControls) 119 | 120 | ##### viewCubeControls: 121 | 122 | ##### gridHelper: [THREE.GridHelper](https://threejs.org/docs/#api/en/helpers/GridHelper) 123 | 124 | ##### axesHelper: [THREE.AxesHelper](https://threejs.org/docs/#api/en/helpers/AxesHelper) 125 | 126 | ##### planeHelper: [THREE.PlaneHelper](https://threejs.org/docs/#api/en/helpers/PlaneHelper) 127 | 128 | ##### stencilPlane: 129 | 130 | ##### box:[THREE.box3](https://threejs.org/docs/?q=box3#api/en/math/Box3) 131 | 132 | ##### selectionBox:[THREE.BoxHelper](https://threejs.org/docs/?q=box3#api/en/helpers/Box3Helper) 133 | 134 | ##### transformControls:[TransformControls](https://threejs.org/docs/#examples/en/controls/TransformControls) 135 | 136 | ##### raycaster:[THREE.Raycaster](https://threejs.org/docs/#api/en/core/Raycaster) 137 | 138 | ##### mouse:[THREE.Vector2](https://threejs.org/docs/#api/en/math/Vector2) 139 | 140 | ##### contextMenu: 141 | 142 | ##### event:[Event](https://github.com/baues/building-editor/blob/main/src/Event.ts) 143 | 144 | #### Methods. 145 | ##### setConfig(config):void 146 | 147 | ##### objectChanged(object):void 148 | 149 | ##### showGridChanged(showGrid:boolean):void 150 | 151 | ##### render():void 152 | 153 | ##### setScene(scene):void 154 | 155 | ##### changeTransformModel(mode):void 156 | 157 | ##### addObject(object,parent,index):void 158 | 159 | ##### addObjectAsHelper(object):void 160 | 161 | ##### moveObject(object,parent,before):void 162 | 163 | ##### nameObject(object,name):void 164 | 165 | ##### removeObject(object):void 166 | 167 | ##### addGeometry(geometry):void 168 | 169 | ##### setGeometryName(geometry,name):void 170 | 171 | ##### addMaterial(material):void 172 | 173 | ##### addMaterialToRefCounter(material):void 174 | 175 | ##### removeMaterial(material):void 176 | 177 | ##### removeMaterialFromRefCounter(material):void 178 | 179 | ##### getMaterialById(id):THREE.Material | undefined 180 | 181 | ##### setMaterialName(material,name):void 182 | 183 | ##### addTexture(texture):void 184 | 185 | ##### addAnimation(object,animations):void 186 | 187 | ##### addCamera(camera):void 188 | 189 | ##### removeCamera(camera):void 190 | 191 | ##### addHelper(object):void 192 | 193 | ##### removeHelper(object):void 194 | 195 | ##### updateGridHelper(gridHelper):void 196 | 197 | ##### updateAxesHelper(axesHelper):void 198 | 199 | ##### updatePlaneHelper(planeHelper):void 200 | 201 | ##### clip(enabled):void 202 | 203 | ##### setDefaultCamera():void 204 | 205 | ##### setViewportCamera(uuid):void 206 | 207 | ##### select(object|null):void 208 | 209 | ##### selectNyId(id):void 210 | 211 | ##### selectByUuid(uuid):void 212 | 213 | ##### setHovered(object|null):void 214 | 215 | ##### focus(object):void 216 | 217 | ##### focusById(id):void 218 | 219 | ##### clear():void 220 | 221 | ##### fromJSON(json):void 222 | 223 | ##### toJSON():EditorJson 224 | 225 | ##### objectByUuid(uuid):THREE.Object3d|undefined 226 | 227 | ##### execute(cmd):void 228 | 229 | ##### undo():void 230 | 231 | ##### redo():void 232 | 233 | 234 | ##### Config(config?:[BuildingEditorConfig](#BuildingEditorConfig)). 235 | 236 | ##### BuildingEditorConfig. 237 | 238 | Editor has many properties and methods. Please check [Editor class](https://github.com/baues/building-editor/blob/main/src/Editor.ts) to find them. The documents will be prepared later. 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-editor", 3 | "version": "0.1.2", 4 | "description": "Open Source 3D Building Model Editor", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "prepublishOnly": "npm run build" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/baues/building-editor.git" 13 | }, 14 | "keywords": [ 15 | "building-editor", 16 | "building", 17 | "architecture", 18 | "three.js", 19 | "webgl" 20 | ], 21 | "author": "BAUES (https://github.com/baues)", 22 | "license": "MIT", 23 | "files": [ 24 | "dist" 25 | ], 26 | "bugs": { 27 | "url": "https://github.com/baues/building-editor/issues" 28 | }, 29 | "homepage": "https://github.com/baues/building-editor#readme", 30 | "peerDependencies": { 31 | "three": "^0.127.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.8.4", 35 | "@babel/core": "^7.9.0", 36 | "@babel/preset-env": "^7.9.0", 37 | "@types/node": "^14.14.41", 38 | "@types/three": "^0.127.1", 39 | "babel-loader": "^8.1.0", 40 | "eslint": "^6.8.0", 41 | "eslint-config-standard": "^14.1.1", 42 | "eslint-plugin-import": "^2.20.2", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "eslint-plugin-standard": "^4.0.1", 46 | "three": "^0.127.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Color.ts: -------------------------------------------------------------------------------- 1 | export interface Color { 2 | 'selectionBox': string | number | THREE.Color | undefined; 3 | 'picker': string | number | THREE.Color | undefined; 4 | 'scene/background': string | number | THREE.Color | undefined; 5 | 'gridHelper': number | THREE.Color | undefined; 6 | 'planeHelper': number | undefined; 7 | 'stencilPlane': number | undefined; 8 | } 9 | 10 | export const color: Color = { 11 | 'selectionBox': 0xffffc107, 12 | 'picker': 0xff0000, 13 | 'scene/background': 0xf0f0f0, 14 | 'gridHelper': 0x666666, 15 | 'planeHelper': 0x666666, 16 | 'stencilPlane': 0x00bbff, 17 | }; 18 | -------------------------------------------------------------------------------- /src/Config.ts: -------------------------------------------------------------------------------- 1 | import { defaultConfig } from './defaultConfig'; 2 | 3 | // eslint-disable-next-line max-len 4 | export type Key = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; 5 | 6 | export interface BuildingEditorConfig { 7 | 'exportPrecision': number; 8 | 'control/orbitControls/enable': boolean; 9 | 'control/transformControls/enable': boolean; 10 | 'control/viewCubeControls/visible': boolean; 11 | 'control/viewCubeControls/size'?: number; 12 | 'control/viewCubeControls/style'?: string; 13 | 'control/viewCubeControls/perspective'?: boolean; 14 | 'control/viewCubeControls/northDirection'?: number; // relative north direction [rad] 15 | 'debug': boolean; 16 | 'history': boolean; 17 | 'select/enabled': boolean; 18 | 'redo/enabled': boolean; 19 | 'undo/enabled': boolean; 20 | 'delete/enabled': boolean; 21 | 'contextmenu/enabled': boolean; 22 | 'shortcuts/translate': Key; 23 | 'shortcuts/rotate': Key; 24 | 'shortcuts/scale': Key; 25 | 'shortcuts/undo': Key; 26 | 'shortcuts/focus': Key; 27 | } 28 | 29 | export type EditorConfig = Partial; 30 | 31 | export class Config { 32 | name: string; 33 | config: BuildingEditorConfig; 34 | 35 | constructor(config?: EditorConfig) { 36 | this.name = 'building-editor'; 37 | 38 | const initialConfig: BuildingEditorConfig = defaultConfig; 39 | this.config = initialConfig; 40 | 41 | if (window.localStorage[this.name] === undefined) { 42 | window.localStorage[this.name] = JSON.stringify(initialConfig); 43 | this.config = { 44 | ...initialConfig, 45 | ...config, 46 | }; 47 | } else { 48 | const localStorageData = JSON.parse(window.localStorage[this.name]); 49 | 50 | this.config = { 51 | ...initialConfig, 52 | ...localStorageData, 53 | ...config, 54 | }; 55 | } 56 | } 57 | 58 | getKey(key: K): BuildingEditorConfig[K] { 59 | return this.config[key]; 60 | } 61 | 62 | set(config: EditorConfig): void { 63 | this.config = { 64 | ...this.config, 65 | ...config, 66 | }; 67 | window.localStorage[this.name] = JSON.stringify(this.config); 68 | 69 | if (this.config.debug) { 70 | const dateTime = /\d\d:\d\d:\d\d/.exec(new Date().toString()); 71 | dateTime && console.log('[' + dateTime[0] + ']', 'Saved config to LocalStorage.'); 72 | } 73 | } 74 | 75 | clear(): void { 76 | window.localStorage.clear(); 77 | delete window.localStorage[this.name]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from './Editor'; 2 | 3 | export class ContextMenu { 4 | editor: Editor; 5 | open: boolean; 6 | x: number | null; 7 | y: number | null; 8 | dispose: () => void; 9 | 10 | constructor(editor: Editor) { 11 | this.editor = editor; 12 | this.open = false; 13 | this.x = null; 14 | this.y = null; 15 | 16 | const onContextMenu = (event: MouseEvent): void | boolean => { 17 | if (editor.config.getKey('contextmenu/enabled')) { 18 | event.preventDefault(); 19 | 20 | if (this.open) { 21 | this.hide(); 22 | } else { 23 | this.show(event.clientX, event.clientY); 24 | } 25 | } else { 26 | return true; 27 | } 28 | }; 29 | 30 | this.dispose = (): void => { 31 | editor.renderer.domElement.removeEventListener('contextmenu', onContextMenu, false); 32 | }; 33 | 34 | editor.renderer.domElement.addEventListener('contextmenu', onContextMenu, false); 35 | } 36 | 37 | show(x: number, y: number): void { 38 | this.open = true; 39 | this.x = x; 40 | this.y = y; 41 | this.editor.editorControls.update(); 42 | } 43 | 44 | hide(): void { 45 | this.open = false; 46 | this.x = null; 47 | this.y = null; 48 | this.editor.editorControls.update(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Editor.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; 4 | import { Geometry } from 'three/examples/jsm/deprecated/Geometry'; 5 | 6 | import { THREEJson } from './Types'; 7 | import { Config, EditorConfig } from './Config'; 8 | import { color } from './Color'; 9 | import { ContextMenu } from './ContextMenu'; 10 | import { Event } from './Event'; 11 | import { Exporter } from './Exporter'; 12 | import { Loader } from './Loader'; 13 | import { History, HistoryJson } from './History'; 14 | import { Settings, EditorSettings } from './Settings'; 15 | import { StencilPlane } from './StencilPlane'; 16 | import { EditorControls } from './controls/EditorControls'; 17 | import { ViewCubeControls } from './controls/ViewCubeControls'; 18 | import { Command } from './commands/Command'; 19 | 20 | export type TransformControlsMode = 'translate' | 'rotate' | 'scale'; 21 | export type Helper = THREE.CameraHelper | THREE.PointLightHelper | THREE.DirectionalLightHelper | THREE.SpotLightHelper | THREE.HemisphereLightHelper | THREE.SkeletonHelper; 22 | 23 | export interface EditorJson extends THREEJson { 24 | camera: Record; 25 | scene: THREE.Scene; 26 | history: HistoryJson; 27 | } 28 | 29 | export class Editor { 30 | config: Config; 31 | settings: Settings; 32 | editorControls: EditorControls; 33 | renderer: THREE.WebGLRenderer; 34 | DEFAULT_CAMERA: THREE.Camera; 35 | history: History; 36 | exporter: Exporter; 37 | loader: Loader; 38 | camera: THREE.Camera; 39 | scene: THREE.Scene; 40 | sceneHelpers: THREE.Scene; 41 | objects: THREE.Object3D[]; 42 | INITIAL_OBJECTS: THREE.Object3D[]; 43 | INITIAL_HELPERS: THREE.Object3D[]; 44 | geometries: { [index: string]: Geometry | THREE.BufferGeometry }; 45 | materials: { [index: string]: THREE.Material }; 46 | textures: { [index: string]: THREE.Texture }; 47 | materialsRefCounter: Map; 48 | animations: { [index: string]: THREE.AnimationClip[] }; 49 | mixer: THREE.AnimationMixer; 50 | selected: THREE.Object3D | null; 51 | hovered: THREE.Object3D | null; 52 | helpers: { [index: string]: Helper }; 53 | cameras: { [index: string]: THREE.Camera }; 54 | viewportCamera: THREE.Camera; 55 | orbitControls: OrbitControls; 56 | viewCubeControls: ViewCubeControls; 57 | gridHelper: THREE.GridHelper; 58 | axesHelper: THREE.AxesHelper; 59 | planeHelper: THREE.PlaneHelper; 60 | stencilPlane: StencilPlane; 61 | box: THREE.Box3; 62 | selectionBox: THREE.BoxHelper; 63 | transformControls: TransformControls; 64 | raycaster: THREE.Raycaster; 65 | mouse: THREE.Vector2; 66 | contextMenu: ContextMenu; 67 | event: Event; 68 | 69 | constructor(config?: EditorConfig, settings?: EditorSettings) { 70 | this.config = new Config(config); 71 | this.settings = new Settings(settings); 72 | this.editorControls = new EditorControls(); 73 | this.renderer = this.settings.renderer; 74 | this.DEFAULT_CAMERA = this.settings.camera; 75 | this.camera = this.DEFAULT_CAMERA.clone(); 76 | this.history = new History(this); 77 | this.exporter = new Exporter(this); 78 | this.loader = new Loader(this); 79 | this.scene = this.settings.scene; 80 | this.sceneHelpers = new THREE.Scene(); 81 | this.objects = []; 82 | this.geometries = {}; 83 | this.materials = {}; 84 | this.textures = {}; 85 | this.materialsRefCounter = new Map(); 86 | this.animations = {}; 87 | this.mixer = new THREE.AnimationMixer(this.scene); 88 | this.selected = null; 89 | this.hovered = null; 90 | this.helpers = {}; 91 | this.cameras = {}; 92 | this.viewportCamera = this.camera; 93 | this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement); 94 | this.orbitControls.enabled = this.config.getKey('control/orbitControls/enable'); 95 | this.viewCubeControls = new ViewCubeControls(this.config, this.camera); 96 | this.gridHelper = this.settings.gridHelper; 97 | this.sceneHelpers.add(this.gridHelper); 98 | this.axesHelper = this.settings.axesHelper; 99 | this.sceneHelpers.add(this.axesHelper); 100 | this.planeHelper = this.settings.planeHelper; 101 | this.sceneHelpers.add(this.planeHelper); 102 | this.stencilPlane = new StencilPlane(this.planeHelper.plane); 103 | this.scene.add(this.stencilPlane.stencilPlane); 104 | this.box = new THREE.Box3(); 105 | const selectionBox = new THREE.BoxHelper(undefined as unknown as THREE.Object3D, color.selectionBox); 106 | (selectionBox.material as THREE.Material).depthTest = false; 107 | (selectionBox.material as THREE.Material).transparent = true; 108 | (selectionBox.material as THREE.Material).opacity = 0.7; 109 | selectionBox.visible = false; 110 | selectionBox.name = 'selectionBox'; 111 | this.selectionBox = selectionBox; 112 | this.sceneHelpers.add(this.selectionBox); 113 | this.transformControls = new TransformControls(this.camera, this.renderer.domElement); 114 | this.transformControls.name = 'transformControls'; 115 | this.sceneHelpers.add(this.transformControls); 116 | this.raycaster = new THREE.Raycaster(); 117 | this.mouse = new THREE.Vector2(); 118 | this.contextMenu = new ContextMenu(this); 119 | this.event = new Event(this); 120 | this.addCamera(this.camera); 121 | this.INITIAL_OBJECTS = this.settings.initialObjects; 122 | this.INITIAL_HELPERS = this.settings.initialHelpers; 123 | this.INITIAL_OBJECTS.forEach(object => this.addObject(object)); 124 | this.INITIAL_HELPERS.forEach(object => this.addObjectAsHelper(object)); 125 | } 126 | 127 | setConfig(config: EditorConfig): void { 128 | // apply config 129 | this.config.set(config); 130 | 131 | // apply detail 132 | this.orbitControls.enabled = !!this.config.getKey('control/orbitControls/enable'); 133 | this.transformControls.enabled = !!this.config.getKey('control/transformControls/enable'); 134 | 135 | this.viewCubeControls.remove(); 136 | this.viewCubeControls = new ViewCubeControls(this.config, this.camera); 137 | 138 | if (!this.config.getKey('select/enabled')) { 139 | this.select(null); 140 | } 141 | 142 | this.event.dispose(); 143 | this.event = new Event(this); 144 | 145 | this.render(); 146 | } 147 | 148 | private editorCleared(): void { 149 | this.orbitControls.target.set(0, 0, 0); 150 | this.render(); 151 | } 152 | 153 | private objectSelected(object: THREE.Object3D | null): void { 154 | this.selectionBox.visible = false; 155 | this.transformControls.detach(); 156 | 157 | if (object !== null && object !== this.scene && object !== this.camera) { 158 | this.box.setFromObject(object); 159 | 160 | if (this.box.isEmpty() === false) { 161 | this.selectionBox.setFromObject(object); 162 | this.selectionBox.visible = true; 163 | } 164 | 165 | this.transformControls.enabled && this.transformControls.attach(object); 166 | } 167 | 168 | this.render(); 169 | } 170 | 171 | private objectFocused(target: THREE.Object3D): void { 172 | const delta = new THREE.Vector3(); 173 | const center = this.orbitControls.target; 174 | const sphere = new THREE.Sphere(); 175 | 176 | let distance; 177 | this.box.setFromObject(target); 178 | if (this.box.isEmpty() === false) { 179 | this.box.getCenter(center); 180 | distance = this.box.getBoundingSphere(sphere).radius; 181 | } else { 182 | center.setFromMatrixPosition(target.matrixWorld); 183 | distance = 0.1; 184 | } 185 | delta.set(0, 0, 1); 186 | delta.applyQuaternion(this.camera.quaternion); 187 | delta.multiplyScalar(distance * 4); 188 | this.camera.position.copy(center).add(delta); 189 | this.render(); 190 | } 191 | 192 | objectChanged(object: THREE.Object3D): void { 193 | if (this.selected === object) { 194 | this.selectionBox.setFromObject(object); 195 | } 196 | 197 | if (object instanceof THREE.PerspectiveCamera) { 198 | object.updateProjectionMatrix(); 199 | } 200 | 201 | if (this.helpers[object.id] !== undefined) { 202 | this.helpers[object.id].update(); 203 | } 204 | 205 | this.editorControls.update(); 206 | this.render(); 207 | } 208 | 209 | private objectRemoved(object: THREE.Object3D): void { 210 | const objects = this.objects; 211 | this.orbitControls.enabled = true; 212 | if (object === this.transformControls.object) { 213 | this.transformControls.detach(); 214 | } 215 | 216 | object.traverse((child) => { 217 | objects.splice(objects.indexOf(child), 1); 218 | }); 219 | } 220 | 221 | private helperAdded(object: THREE.Object3D): void { 222 | const picker = object.getObjectByName('picker'); 223 | picker && this.objects.push(picker); 224 | } 225 | 226 | private helperRemoved(object: THREE.Object3D): void { 227 | const picker = object.getObjectByName('picker'); 228 | picker && this.objects.splice(this.objects.indexOf(picker), 1); 229 | } 230 | 231 | private viewportCameraChanged(viewportCamera: THREE.Camera): void { 232 | const camera = this.camera; 233 | viewportCamera.projectionMatrix.copy(camera.projectionMatrix); 234 | 235 | if (camera instanceof THREE.PerspectiveCamera && viewportCamera instanceof THREE.PerspectiveCamera) viewportCamera.aspect = camera.aspect; 236 | 237 | this.camera = viewportCamera; 238 | this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement); 239 | 240 | this.render(); 241 | } 242 | 243 | showGridChanged(showGrid: boolean): void { 244 | this.gridHelper.visible = showGrid; 245 | this.render(); 246 | } 247 | 248 | render(): void { 249 | this.scene.updateMatrixWorld(); 250 | this.renderer.render(this.scene, this.camera); 251 | this.sceneHelpers.updateMatrixWorld(); 252 | this.renderer.render(this.sceneHelpers, this.camera); 253 | } 254 | 255 | setScene(scene: THREE.Scene): void { 256 | this.scene.uuid = scene.uuid; 257 | this.scene.name = scene.name; 258 | 259 | this.scene.background = scene.background !== null ? scene.background.clone() : null; 260 | 261 | if (scene.fog !== null) this.scene.fog = scene.fog.clone(); 262 | 263 | this.scene.userData = JSON.parse(JSON.stringify(scene.userData)); 264 | 265 | this.editorControls.enabled = false; 266 | 267 | while (scene.children.length > 0) { 268 | this.addObject(scene.children[0]); 269 | } 270 | 271 | this.editorControls.enabled = true; 272 | this.editorControls.update(); 273 | } 274 | 275 | changeTransformMode(mode: TransformControlsMode): void { 276 | this.transformControls.enabled && this.transformControls.setMode(mode); 277 | } 278 | 279 | addObject(object: THREE.Object3D, parent?: THREE.Object3D, index?: number): void { 280 | const scope = this; 281 | object.traverse((child) => { 282 | if (child instanceof THREE.Mesh) { 283 | const geometry = child.geometry; 284 | const material = child.material; 285 | if (geometry) scope.addGeometry(geometry); 286 | if (material) scope.addMaterial(material); 287 | } 288 | 289 | if (child instanceof THREE.Camera) scope.addCamera(child); 290 | scope.addHelper(child); 291 | }); 292 | 293 | if (!parent) { 294 | this.scene.add(object); 295 | } else { 296 | index = index || -1; 297 | parent.children.splice(index, 0, object); 298 | object.parent = parent; 299 | } 300 | 301 | object.traverse((child) => { 302 | scope.objects.push(child); 303 | }); 304 | 305 | this.editorControls.update(); 306 | } 307 | 308 | addObjectAsHelper(object: THREE.Object3D): void { 309 | if (object instanceof THREE.AxesHelper) { 310 | this.axesHelper = object; 311 | } else if (object instanceof THREE.GridHelper) { 312 | this.gridHelper = object; 313 | } else if (object instanceof THREE.PlaneHelper) { 314 | this.planeHelper = object; 315 | } 316 | 317 | this.sceneHelpers.add(object); 318 | this.editorControls.update(); 319 | } 320 | 321 | moveObject(object: THREE.Object3D, parent?: THREE.Object3D, before?: THREE.Object3D): void { 322 | if (!parent) { 323 | parent = this.scene; 324 | } 325 | 326 | parent.add(object); 327 | 328 | // sort children array 329 | 330 | if (before) { 331 | const index = parent.children.indexOf(before); 332 | parent.children.splice(index, 0, object); 333 | parent.children.pop(); 334 | } 335 | 336 | this.editorControls.update(); 337 | } 338 | 339 | nameObject(object: THREE.Object3D, name: string): void { 340 | object.name = name; 341 | this.editorControls.update(); 342 | } 343 | 344 | removeObject(object: THREE.Object3D): void { 345 | if (object.parent === null) return; // avoid deleting the camera or scene 346 | 347 | object.traverse((child) => { 348 | (child instanceof THREE.Camera) && this.removeCamera(child); 349 | this.removeHelper(child); 350 | 351 | const material = (child as THREE.Mesh).material; 352 | if (material) this.removeMaterial(material); 353 | }); 354 | 355 | object.parent.remove(object); 356 | 357 | this.objectRemoved(object); 358 | this.editorControls.update(); 359 | } 360 | 361 | addGeometry(geometry: Geometry | THREE.BufferGeometry): void { 362 | this.geometries[geometry.uuid] = geometry; 363 | } 364 | 365 | setGeometryName(geometry: Geometry | THREE.BufferGeometry, name: string): void { 366 | geometry.name = name; 367 | this.editorControls.update(); 368 | } 369 | 370 | addMaterial(material: THREE.Material | THREE.Material[]): void { 371 | if (Array.isArray(material)) { 372 | for (let i = 0, l = material.length; i < l; i++) { 373 | this.addMaterialToRefCounter(material[i]); 374 | } 375 | } else { 376 | this.addMaterialToRefCounter(material); 377 | } 378 | 379 | // this.materialAdded(); 380 | } 381 | 382 | addMaterialToRefCounter(material: THREE.Material): void { 383 | const materialsRefCounter = this.materialsRefCounter; 384 | 385 | let count = materialsRefCounter.get(material); 386 | 387 | if (count === undefined) { 388 | materialsRefCounter.set(material, 1); 389 | this.materials[material.uuid] = material; 390 | } else { 391 | count++; 392 | materialsRefCounter.set(material, count); 393 | } 394 | } 395 | 396 | removeMaterial(material: THREE.Material | THREE.Material[]): void { 397 | if (Array.isArray(material)) { 398 | for (let i = 0, l = material.length; i < l; i++) { 399 | this.removeMaterialFromRefCounter(material[i]); 400 | } 401 | } else { 402 | this.removeMaterialFromRefCounter(material); 403 | } 404 | 405 | // this.materialRemoved(); 406 | } 407 | 408 | removeMaterialFromRefCounter(material: THREE.Material): void { 409 | const materialsRefCounter = this.materialsRefCounter; 410 | 411 | let count = materialsRefCounter.get(material); 412 | count && count--; 413 | 414 | if (count === 0) { 415 | materialsRefCounter.delete(material); 416 | delete this.materials[material.uuid]; 417 | } else { 418 | count && materialsRefCounter.set(material, count); 419 | } 420 | } 421 | 422 | getMaterialById(id: number): THREE.Material | undefined { 423 | let material; 424 | const materials = Object.values(this.materials); 425 | 426 | for (let i = 0; i < materials.length; i++) { 427 | if (materials[i].id === id) { 428 | material = materials[i]; 429 | break; 430 | } 431 | } 432 | 433 | return material; 434 | } 435 | 436 | setMaterialName(material: THREE.Material, name: string): void { 437 | material.name = name; 438 | this.editorControls.update(); 439 | } 440 | 441 | addTexture(texture: THREE.Texture): void { 442 | this.textures[texture.uuid] = texture; 443 | } 444 | 445 | addAnimation(object: THREE.Object3D, animations: THREE.AnimationClip[]): void { 446 | if (animations.length > 0) { 447 | this.animations[object.uuid] = animations; 448 | } 449 | } 450 | 451 | addCamera(camera: THREE.Camera): void { 452 | if (camera.isCamera) { 453 | this.cameras[camera.uuid] = camera; 454 | 455 | // this.cameraAdded(camera); 456 | } 457 | } 458 | 459 | removeCamera(camera: THREE.Camera): void { 460 | if (this.cameras[camera.uuid] !== undefined) { 461 | delete this.cameras[camera.uuid]; 462 | 463 | // this.cameraRemoved(camera); 464 | } 465 | } 466 | 467 | // Helpers 468 | addHelper(object: THREE.Object3D): void { 469 | let helper; 470 | 471 | if (object instanceof THREE.Camera) { 472 | helper = new THREE.CameraHelper(object); 473 | } else if (object instanceof THREE.PointLight) { 474 | helper = new THREE.PointLightHelper(object, 1); 475 | } else if (object instanceof THREE.DirectionalLight) { 476 | helper = new THREE.DirectionalLightHelper(object, 1); 477 | } else if (object instanceof THREE.SpotLight) { 478 | helper = new THREE.SpotLightHelper(object, 1); 479 | } else if (object instanceof THREE.HemisphereLight) { 480 | helper = new THREE.HemisphereLightHelper(object, 1); 481 | } else if (object instanceof THREE.SkinnedMesh) { 482 | helper = new THREE.SkeletonHelper(object.skeleton.bones[0]); 483 | } else { 484 | // no helper for this object type 485 | return; 486 | } 487 | 488 | const geometry = new THREE.SphereBufferGeometry(2, 4, 2); 489 | const material = new THREE.MeshBasicMaterial({ color: color.picker, visible: false }); 490 | const picker = new THREE.Mesh(geometry, material); 491 | picker.name = 'picker'; 492 | picker.userData.object = object; 493 | helper.add(picker); 494 | 495 | this.sceneHelpers.add(helper); 496 | this.helpers[object.id] = helper; 497 | 498 | this.helperAdded(helper); 499 | } 500 | 501 | removeHelper(object: THREE.Object3D): void { 502 | if (this.helpers[object.id] !== undefined) { 503 | const helper = this.helpers[object.id]; 504 | helper.parent && helper.parent.remove(helper); 505 | 506 | delete this.helpers[object.id]; 507 | 508 | this.helperRemoved(helper); 509 | } 510 | } 511 | 512 | updateGridHelper(gridHelper: THREE.GridHelper): void { 513 | this.gridHelper = gridHelper; 514 | } 515 | 516 | updateAxesHelper(axesHelper: THREE.AxesHelper): void { 517 | this.axesHelper = axesHelper; 518 | } 519 | 520 | updatePlaneHelper(planeHelper: THREE.PlaneHelper): void { 521 | this.planeHelper = planeHelper; 522 | } 523 | 524 | clip(enable = true): void { 525 | this.renderer.clippingPlanes = enable ? [this.planeHelper.plane] : []; 526 | } 527 | 528 | setDefaultCamera(): void { 529 | this.camera = this.DEFAULT_CAMERA.clone(); 530 | this.viewportCamera = this.camera; 531 | this.viewportCameraChanged(this.viewportCamera); 532 | } 533 | 534 | setViewportCamera(uuid: string): void { 535 | this.viewportCamera = this.cameras[uuid]; 536 | this.viewportCameraChanged(this.viewportCamera); 537 | } 538 | 539 | select(object: THREE.Object3D | null): void { 540 | const enabled = this.config.getKey('select/enabled'); 541 | if (!enabled && object) return; 542 | if (object && this.selected === object) return; 543 | 544 | this.selected = object; 545 | 546 | this.objectSelected(object); 547 | } 548 | 549 | selectById(id: number): void { 550 | if (id === this.camera.id) { 551 | this.select(this.camera); 552 | return; 553 | } 554 | 555 | const object = this.scene.getObjectById(id); 556 | object && this.select(object); 557 | } 558 | 559 | selectByUuid(uuid: string): void { 560 | this.scene.traverse((child) => { 561 | if (child.uuid === uuid) { 562 | this.select(child); 563 | } 564 | }); 565 | } 566 | 567 | setHovered(object: THREE.Object3D | null): void { 568 | if (object && this.hovered === object) return; 569 | 570 | this.hovered = object; 571 | } 572 | 573 | focus(object: THREE.Object3D): void { 574 | if (object !== undefined) { 575 | this.objectFocused(object); 576 | } 577 | } 578 | 579 | focusById(id: number): void { 580 | const object = this.scene.getObjectById(id); 581 | object && this.focus(object); 582 | } 583 | 584 | clear(): void { 585 | this.history.clear(); 586 | 587 | this.camera.copy(this.DEFAULT_CAMERA); 588 | this.scene = this.settings.scene; 589 | const objects = this.scene.children; 590 | while (objects.length > 0) { 591 | this.removeObject(objects[0]); 592 | } 593 | 594 | this.geometries = {}; 595 | this.materials = {}; 596 | this.textures = {}; 597 | 598 | this.materialsRefCounter.clear(); 599 | 600 | this.animations = {}; 601 | this.mixer.stopAllAction(); 602 | 603 | this.select(null); 604 | 605 | this.editorCleared(); 606 | } 607 | 608 | fromJSON(json: EditorJson): void { 609 | const loader = new THREE.ObjectLoader(); 610 | const camera = loader.parse(json.camera); 611 | 612 | this.camera.copy(camera); 613 | if (this.camera instanceof THREE.PerspectiveCamera && this.DEFAULT_CAMERA instanceof THREE.PerspectiveCamera) { 614 | this.camera.aspect = this.DEFAULT_CAMERA.aspect; 615 | this.camera.updateProjectionMatrix(); 616 | } 617 | 618 | this.history.fromJSON(json.history as HistoryJson); 619 | 620 | loader.parse(json.scene, (scene) => { 621 | this.setScene(scene as THREE.Scene); 622 | }); 623 | } 624 | 625 | toJSON(): EditorJson { 626 | return { 627 | metadata: { 628 | type: 'app', 629 | }, 630 | camera: this.camera.toJSON(), 631 | scene: this.scene.toJSON(), 632 | history: this.history.toJSON(), 633 | }; 634 | } 635 | 636 | objectByUuid(uuid: string | undefined): THREE.Object3D | undefined { 637 | if (!uuid) return undefined; 638 | return this.scene.getObjectByProperty('uuid', uuid); 639 | } 640 | 641 | execute(cmd: Command): void { 642 | this.history.execute(cmd); 643 | } 644 | 645 | undo(): void { 646 | if (this.config.getKey('undo/enabled')) { 647 | this.history.undo(); 648 | } 649 | } 650 | 651 | redo(): void { 652 | if (this.config.getKey('redo/enabled')) { 653 | this.history.redo(); 654 | } 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /src/Event.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Editor } from './Editor'; 3 | import * as Commands from './commands'; 4 | import { throttle } from './utils/throttle'; 5 | import { getViewSize } from './utils/viewportUtils'; 6 | 7 | const IS_MAC = navigator.platform.toUpperCase().indexOf('MAC') >= 0; 8 | const onDownPosition = new THREE.Vector2(); 9 | const onUpPosition = new THREE.Vector2(); 10 | const onDoubleClickPosition = new THREE.Vector2(); 11 | 12 | export class Event { 13 | dispose: () => void; 14 | 15 | constructor(editor: Editor) { 16 | const setSceneSize = (width?: number, height?: number) => { 17 | if (!width || !height) { 18 | const size = getViewSize(); 19 | width = size.width; 20 | height = size.height; 21 | } 22 | 23 | if (editor.camera instanceof THREE.PerspectiveCamera) { 24 | editor.camera.aspect = width / height; 25 | editor.camera.updateProjectionMatrix(); 26 | } 27 | 28 | if (editor.renderer.getPixelRatio() !== window.devicePixelRatio) { 29 | editor.renderer.setPixelRatio(window.devicePixelRatio); 30 | } 31 | editor.renderer.setSize(width, height); 32 | 33 | editor.render(); 34 | }; 35 | setSceneSize(); 36 | 37 | // ResizeListener 38 | const onWindowResize = (): void => { 39 | setSceneSize(); 40 | }; 41 | 42 | // EditorControlsListener 43 | const onUpdateEditorControls = (): void => { 44 | editor.render(); 45 | }; 46 | 47 | // OrbitControlsListener 48 | const onChangeOrbitControls = (): void => { 49 | editor.render(); 50 | editor.viewCubeControls.update(); 51 | }; 52 | 53 | // TransformControlsListener 54 | let objectPositionOnDown: THREE.Vector3 | null = null; 55 | let objectRotationOnDown: THREE.Euler | null = null; 56 | let objectScaleOnDown: THREE.Vector3 | null = null; 57 | 58 | const onChangeTransformControls = (): void => { 59 | const object = editor.transformControls.object; 60 | 61 | if (object) { 62 | editor.selectionBox.setFromObject(object); 63 | 64 | const helper = editor.helpers[object.id]; 65 | 66 | if (helper && !(helper instanceof THREE.SkeletonHelper)) { 67 | helper.update(); 68 | } 69 | } 70 | 71 | editor.render(); 72 | }; 73 | 74 | const onMouseDownTransformControls = (): void => { 75 | const object = editor.transformControls.object; 76 | if (!object) return; 77 | 78 | objectPositionOnDown = object.position.clone(); 79 | objectRotationOnDown = object.rotation.clone(); 80 | objectScaleOnDown = object.scale.clone(); 81 | 82 | editor.orbitControls.enabled = false; 83 | }; 84 | 85 | const onMouseUpTransformControls = (): void => { 86 | const object = editor.transformControls.object; 87 | 88 | if (object !== undefined) { 89 | switch (editor.transformControls.getMode()) { 90 | case 'translate': 91 | if (!objectPositionOnDown) break; 92 | if (!objectPositionOnDown.equals(object.position)) { 93 | editor.execute(new Commands.SetPositionCommand(editor, object, object.position, objectPositionOnDown)); 94 | } 95 | break; 96 | 97 | case 'rotate': 98 | if (!objectRotationOnDown) break; 99 | if (!objectRotationOnDown.equals(object.rotation)) { 100 | editor.execute(new Commands.SetRotationCommand(editor, object, object.rotation, objectRotationOnDown)); 101 | } 102 | break; 103 | 104 | case 'scale': 105 | if (!objectScaleOnDown) break; 106 | if (!objectScaleOnDown.equals(object.scale)) { 107 | editor.execute(new Commands.SetScaleCommand(editor, object, object.scale, objectScaleOnDown)); 108 | } 109 | break; 110 | default: 111 | if (editor.config.getKey('debug')) { 112 | console.error('unknown control mode'); 113 | } 114 | break; 115 | } 116 | } 117 | 118 | editor.orbitControls.enabled = true; 119 | }; 120 | 121 | // ViewCubeControls 122 | const DEGTORAD = THREE.MathUtils.DEG2RAD; 123 | 124 | const rotate = (a: number, b: number, c: number): void => { 125 | const fwd = new THREE.Vector3(); 126 | const euler = new THREE.Euler(a, b, c); 127 | 128 | const finishQuaternion = new THREE.Quaternion() 129 | .copy(editor.camera.quaternion) 130 | .setFromEuler(euler); 131 | fwd.set(0, 0, -1); 132 | fwd.applyQuaternion(finishQuaternion); 133 | fwd.multiplyScalar(100); 134 | editor.camera.position.copy(editor.orbitControls.target).sub(fwd); 135 | editor.orbitControls.update(); 136 | }; 137 | 138 | const onClickViewCubeControls = (e: any): void => { 139 | switch (e.target.id) { 140 | case 'front': 141 | rotate(0, 0, 0); 142 | break; 143 | case 'back': 144 | rotate(0, 180 * DEGTORAD, 0); 145 | break; 146 | case 'top': 147 | rotate(-90 * DEGTORAD, 0, 0); 148 | break; 149 | case 'bottom': 150 | rotate(90 * DEGTORAD, 0, 0); 151 | break; 152 | case 'left': 153 | rotate(0, -90 * DEGTORAD, 0); 154 | break; 155 | case 'right': 156 | rotate(0, 90 * DEGTORAD, 0); 157 | break; 158 | } 159 | }; 160 | 161 | // click 162 | function getMousePosition(dom: HTMLCanvasElement, x: number, y: number): number[] { 163 | const rect = dom.getBoundingClientRect(); 164 | return [(x - rect.left) / rect.width, (y - rect.top) / rect.height]; 165 | } 166 | 167 | const getIntersects = (point: THREE.Vector2, objects: THREE.Object3D[]): THREE.Intersection[] => { 168 | editor.mouse.set(point.x * 2 - 1, -(point.y * 2) + 1); 169 | editor.raycaster.setFromCamera(editor.mouse, editor.camera); 170 | return editor.raycaster.intersectObjects(objects); 171 | }; 172 | 173 | const select = (object: THREE.Object3D | null) => { 174 | editor.select(object); 175 | editor.editorControls.update(); 176 | }; 177 | 178 | const isVisible = (object: THREE.Object3D): boolean => { 179 | let bool = object.visible; 180 | 181 | if (bool) { 182 | object.traverseAncestors((parent) => { 183 | if (!parent.visible) bool = false; 184 | }); 185 | } 186 | return bool; 187 | }; 188 | 189 | const click = () => { 190 | if (!editor.config.getKey('select/enabled')) return; 191 | if (onDownPosition.distanceTo(onUpPosition) < 1e-3) { 192 | const objects: THREE.Object3D[] = []; // editor.objects is not accurate in some condition. should fix 193 | editor.scene.traverse(child => (child instanceof THREE.Mesh && child.name !== 'building-editor-stencilPlane' && isVisible(child)) && objects.push(child)); 194 | const intersects = getIntersects(onUpPosition, objects); 195 | 196 | if (intersects.length > 0) { 197 | let object: THREE.Object3D | null = null; 198 | for (const intersection of intersects) { 199 | const iObject = intersection.object; 200 | object = iObject; 201 | break; 202 | } 203 | 204 | if (object?.userData.object !== undefined) { 205 | select(object.userData.object); 206 | } else { 207 | select(object); 208 | } 209 | } else { 210 | select(null); 211 | } 212 | } 213 | }; 214 | 215 | // PointerUpDownListener 216 | const onPointerDown = (event: MouseEvent): void => { 217 | const array = getMousePosition(editor.renderer.domElement, event.clientX, event.clientY); 218 | onDownPosition.fromArray(array); 219 | }; 220 | 221 | const onPointerUp = (event: MouseEvent): void => { 222 | const array = getMousePosition(editor.renderer.domElement, event.clientX, event.clientY); 223 | onUpPosition.fromArray(array); 224 | 225 | click(); 226 | }; 227 | 228 | // TouchStartEndListener 229 | const onTouchStart = (event: TouchEvent): void => { 230 | const touch = event.changedTouches[0]; 231 | 232 | const array = getMousePosition(editor.renderer.domElement, touch.clientX, touch.clientY); 233 | onDownPosition.fromArray(array); 234 | }; 235 | 236 | const onTouchEnd = (event: TouchEvent): void => { 237 | const touch = event.changedTouches[0]; 238 | 239 | const array = getMousePosition(editor.renderer.domElement, touch.clientX, touch.clientY); 240 | onUpPosition.fromArray(array); 241 | 242 | click(); 243 | }; 244 | 245 | const focus = (object: THREE.Object3D) => { 246 | editor.focus(object); 247 | }; 248 | 249 | // DoubleClickListener 250 | const onDoubleClick = (event: MouseEvent): void => { 251 | const array = getMousePosition(editor.renderer.domElement, event.clientX, event.clientY); 252 | onDoubleClickPosition.fromArray(array); 253 | 254 | const intersects = getIntersects(onDoubleClickPosition, editor.objects); 255 | 256 | if (intersects.length > 0) { 257 | const intersect = intersects[0]; 258 | 259 | focus(intersect.object); 260 | } 261 | }; 262 | 263 | // KeyDownListener 264 | const removeObject = (object: THREE.Object3D) => { 265 | if (object === null) return; 266 | const parent = object.parent; 267 | if (parent !== null) editor.execute(new Commands.RemoveObjectCommand(editor, object)); 268 | editor.editorControls.update(); 269 | }; 270 | 271 | const removeSelected = (): void => { 272 | const object = editor.selected; 273 | if (object === null) return; 274 | removeObject(object); 275 | }; 276 | 277 | const onKeyDown = (event: KeyboardEvent): void => { 278 | switch (event.key?.toLowerCase()) { 279 | case 'backspace': 280 | // event.preventDefault(); 281 | if (editor.config.getKey('delete/enabled')) { 282 | removeSelected(); 283 | } 284 | break; 285 | case 'delete': 286 | if (editor.config.getKey('delete/enabled')) { 287 | removeSelected(); 288 | } 289 | break; 290 | case editor.config.getKey('shortcuts/translate'): 291 | editor.changeTransformMode('translate'); 292 | break; 293 | case editor.config.getKey('shortcuts/rotate'): 294 | editor.changeTransformMode('rotate'); 295 | break; 296 | case editor.config.getKey('shortcuts/scale'): 297 | editor.changeTransformMode('scale'); 298 | break; 299 | case editor.config.getKey('shortcuts/undo'): 300 | if (IS_MAC ? event.metaKey : event.ctrlKey) { 301 | event.preventDefault(); // Prevent browser specific hotkeys 302 | if (event.shiftKey) { 303 | editor.redo(); 304 | } else { 305 | editor.undo(); 306 | } 307 | } 308 | break; 309 | case editor.config.getKey('shortcuts/focus'): 310 | if (editor.selected !== null) { 311 | editor.focus(editor.selected); 312 | } 313 | break; 314 | default: 315 | break; 316 | } 317 | }; 318 | 319 | // Hover 320 | const setHovered = (object: THREE.Object3D | null) => { 321 | editor.setHovered(object); 322 | editor.editorControls.update(); 323 | }; 324 | 325 | const onMouseMove = (event: MouseEvent): void => { 326 | const array = getMousePosition(editor.renderer.domElement, event.clientX, event.clientY); 327 | onDoubleClickPosition.fromArray(array); 328 | 329 | const intersects = getIntersects(onDoubleClickPosition, editor.objects); 330 | 331 | if (intersects.length > 0) { 332 | const intersect = intersects[0]; 333 | setHovered(intersect.object); 334 | } else { 335 | setHovered(null); 336 | } 337 | }; 338 | 339 | // DragDrop 340 | const handleDragOver = (event: DragEvent): void => { 341 | if (!event.dataTransfer) return; 342 | event.preventDefault(); 343 | event.dataTransfer.dropEffect = 'copy'; 344 | }; 345 | 346 | const handleDrop = (event: DragEvent): void => { 347 | if (!event.dataTransfer) return; 348 | event.preventDefault(); 349 | 350 | if (event.dataTransfer.types[0] === 'text/plain') return; // Outliner drop 351 | 352 | if (event.dataTransfer.items) { 353 | // DataTransferItemList supports folders 354 | editor.loader.loadItemList(event.dataTransfer.items as unknown as DataTransferItem[]); 355 | } else { 356 | editor.loader.loadFiles(event.dataTransfer.files as unknown as File[]); 357 | } 358 | }; 359 | 360 | this.dispose = (): void => { 361 | window.removeEventListener('resize', onWindowResize, false); 362 | editor.editorControls.removeEventListener('update', onUpdateEditorControls); 363 | editor.orbitControls.removeEventListener('change', onChangeOrbitControls); 364 | editor.transformControls.removeEventListener('change', onChangeTransformControls); 365 | editor.transformControls.removeEventListener('mouseDown', onMouseDownTransformControls); 366 | editor.transformControls.removeEventListener('mouseUp', onMouseUpTransformControls); 367 | editor.transformControls.removeEventListener('touchstart', onMouseDownTransformControls); 368 | editor.transformControls.removeEventListener('touchend', onMouseUpTransformControls); 369 | editor.viewCubeControls.element.removeEventListener('click', onClickViewCubeControls, false); 370 | editor.renderer.domElement.removeEventListener('pointerdown', onPointerDown, false); 371 | editor.renderer.domElement.removeEventListener('pointerup', onPointerUp, false); 372 | editor.renderer.domElement.removeEventListener('touchstart', onTouchStart, false); 373 | editor.renderer.domElement.removeEventListener('touchend', onTouchEnd, false); 374 | editor.renderer.domElement.removeEventListener('dblclick', onDoubleClick, false); 375 | document.removeEventListener('keydown', onKeyDown, false); 376 | document.removeEventListener('mousemove', (e) => throttle(onMouseMove, 200, e), false); 377 | document.removeEventListener('dragover', handleDragOver, false); 378 | document.removeEventListener('drop', handleDrop, false); 379 | }; 380 | 381 | window.addEventListener('resize', onWindowResize, false); 382 | editor.editorControls.addEventListener('update', onUpdateEditorControls); 383 | editor.orbitControls.addEventListener('change', onChangeOrbitControls); 384 | editor.transformControls.addEventListener('change', onChangeTransformControls); 385 | editor.transformControls.addEventListener('mouseDown', onMouseDownTransformControls); 386 | editor.transformControls.addEventListener('mouseUp', onMouseUpTransformControls); 387 | editor.transformControls.addEventListener('touchstart', onMouseDownTransformControls); 388 | editor.transformControls.addEventListener('touchend', onMouseUpTransformControls); 389 | editor.viewCubeControls.element.addEventListener('click', onClickViewCubeControls, false); 390 | editor.renderer.domElement.addEventListener('pointerdown', onPointerDown, false); 391 | editor.renderer.domElement.addEventListener('pointerup', onPointerUp, false); 392 | editor.renderer.domElement.addEventListener('touchstart', onTouchStart, false); 393 | editor.renderer.domElement.addEventListener('touchend', onTouchEnd, false); 394 | editor.renderer.domElement.addEventListener('dblclick', onDoubleClick, false); 395 | document.addEventListener('keydown', onKeyDown, false); 396 | document.addEventListener('mousemove', (e) => throttle(onMouseMove, 200, e), false); 397 | document.addEventListener('dragover', handleDragOver, false); 398 | document.addEventListener('drop', handleDrop, false); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/Exporter.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { ColladaExporter } from 'three/examples/jsm/exporters/ColladaExporter'; 3 | import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'; 4 | import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'; 5 | import { PLYExporter } from 'three/examples/jsm/exporters/PLYExporter'; 6 | import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'; 7 | import { Editor } from './Editor'; 8 | 9 | export class Exporter { 10 | editor: Editor; 11 | link: HTMLAnchorElement; 12 | 13 | constructor(editor: Editor) { 14 | this.editor = editor; 15 | this.link = document.createElement('a'); 16 | this.link.style.display = 'none'; 17 | } 18 | 19 | parseNumber(key: any, value: number): number { 20 | const precision = this.editor.config.getKey('exportPrecision'); 21 | 22 | return typeof value === 'number' ? parseFloat(value.toFixed(precision)) : value; 23 | } 24 | 25 | // Export Geometry 26 | exportGeometry(): void { 27 | const object = this.editor.selected; 28 | 29 | if (object === null) { 30 | alert('No object selected.'); 31 | return; 32 | } 33 | 34 | if (!(object instanceof THREE.Mesh)) return; 35 | 36 | const geometry = object.geometry; 37 | 38 | if (geometry === undefined) { 39 | alert("The selected object doesn't have geometry."); 40 | return; 41 | } 42 | 43 | let output = geometry.toJSON(); 44 | 45 | try { 46 | output = JSON.stringify(output, this.parseNumber, '\t'); 47 | output = output.replace(/[\n\t]+([\d.e\-[\]]+)/g, '$1'); 48 | } catch (e) { 49 | output = JSON.stringify(output); 50 | } 51 | 52 | this.saveString(output, 'geometry.json'); 53 | } 54 | 55 | // Export Object 56 | exportObject(): void { 57 | const object = this.editor.selected; 58 | 59 | if (object === null) { 60 | alert('No object selected'); 61 | return; 62 | } 63 | 64 | let output = object.toJSON(); 65 | 66 | try { 67 | output = JSON.stringify(output, this.parseNumber, '\t'); 68 | output = output.replace(/[\n\t]+([\d.e\-[\]]+)/g, '$1'); 69 | } catch (e) { 70 | output = JSON.stringify(output); 71 | } 72 | 73 | this.saveString(output, 'model.json'); 74 | } 75 | 76 | // Export Scene 77 | exportScene(): void { 78 | let output = this.editor.scene.toJSON(); 79 | 80 | try { 81 | output = JSON.stringify(output, this.parseNumber, '\t'); 82 | output = output.replace(/[\n\t]+([\d.e\-[\]]+)/g, '$1'); 83 | } catch (e) { 84 | output = JSON.stringify(output); 85 | } 86 | 87 | this.saveString(output, 'scene.json'); 88 | } 89 | 90 | // Export DAE 91 | exportDAE(): void { 92 | const scope = this; 93 | const exporter = new ColladaExporter(); 94 | 95 | exporter.parse(scope.editor.scene, (result) => { 96 | scope.saveString(result.data, 'scene.dae'); 97 | }, {}); 98 | } 99 | 100 | // Export GLB 101 | exportGLB(): void { 102 | const scope = this; 103 | const exporter = new GLTFExporter(); 104 | 105 | exporter.parse( 106 | scope.editor.scene, 107 | (result) => { 108 | scope.saveArrayBuffer(result as BlobPart, 'scene.glb'); 109 | 110 | // forceIndices: true, forcePowerOfTwoTextures: true 111 | // to allow compatibility with facebook 112 | }, 113 | { binary: true, forceIndices: true, forcePowerOfTwoTextures: true }, 114 | ); 115 | } 116 | 117 | // Export GLTF 118 | exportGLTF(): void { 119 | const scope = this; 120 | const exporter = new GLTFExporter(); 121 | 122 | exporter.parse(scope.editor.scene, (result) => { 123 | scope.saveString(JSON.stringify(result, null, 2), 'scene.gltf'); 124 | }, {}); 125 | } 126 | 127 | // Export OBJ 128 | exportOBJ(): void { 129 | const scope = this; 130 | const object = scope.editor.selected; 131 | 132 | if (object === null) { 133 | alert('No object selected.'); 134 | return; 135 | } 136 | 137 | const exporter = new OBJExporter(); 138 | 139 | scope.saveString(exporter.parse(object), 'model.obj'); 140 | } 141 | 142 | // Export PLY (ASCII) 143 | exportPLY(): void { 144 | const scope = this; 145 | const exporter = new PLYExporter(); 146 | 147 | exporter.parse(scope.editor.scene, (result) => { 148 | scope.saveArrayBuffer(result, 'model.ply'); 149 | }, {}); 150 | } 151 | 152 | // Export PLY (Binary) 153 | exportBinaryPLY(): void { 154 | const scope = this; 155 | const exporter = new PLYExporter(); 156 | 157 | exporter.parse( 158 | scope.editor.scene, 159 | (result) => { 160 | scope.saveArrayBuffer(result, 'model-binary.ply'); 161 | }, 162 | { binary: true }, 163 | ); 164 | } 165 | 166 | // Export STL (ASCII) 167 | 168 | exportSTL(): void { 169 | const scope = this; 170 | const exporter = new STLExporter(); 171 | 172 | scope.saveString(exporter.parse(scope.editor.scene), 'model.stl'); 173 | } 174 | 175 | // Export STL (Binary) 176 | 177 | exportBinarySTL(): void { 178 | const scope = this; 179 | const exporter = new STLExporter(); 180 | 181 | scope.saveArrayBuffer(exporter.parse(scope.editor.scene, { binary: true }), 'model-binary.stl'); 182 | } 183 | 184 | save(blob: Blob, filename: string): void { 185 | this.link.href = URL.createObjectURL(blob); 186 | this.link.download = filename || 'data.json'; 187 | this.link.dispatchEvent(new MouseEvent('click')); 188 | // URL.revokeObjectURL( url ); breaks Firefox... 189 | } 190 | 191 | saveArrayBuffer(buffer: BlobPart, filename: string): void { 192 | this.save(new Blob([buffer], { type: 'application/octet-stream' }), filename); 193 | } 194 | 195 | saveString(text: BlobPart, filename: string): void { 196 | this.save(new Blob([text], { type: 'text/plain' }), filename); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/History.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from './Editor'; 2 | import { Config } from './Config'; 3 | import { Command } from './commands/Command'; 4 | import * as Commands from './commands'; 5 | import { CommandTypes } from './commands/types'; 6 | 7 | export interface HistoryJson { 8 | undos: Command[]; 9 | redos: Command[]; 10 | } 11 | 12 | export class History { 13 | editor: Editor; 14 | undos: Command[]; 15 | redos: Command[]; 16 | lastCmdTime: Date; 17 | idCounter: number; 18 | config: Config; 19 | 20 | constructor(editor: Editor) { 21 | this.editor = editor; 22 | this.undos = []; 23 | this.redos = []; 24 | this.lastCmdTime = new Date(); 25 | this.idCounter = 0; 26 | this.config = editor.config; 27 | } 28 | 29 | execute(cmd: Command): void { 30 | const lastCmd = this.undos[this.undos.length - 1]; 31 | const timeDifference = new Date().getTime() - this.lastCmdTime.getTime(); 32 | 33 | const isUpdatableCmd = lastCmd?.updatable; 34 | 35 | if (isUpdatableCmd && timeDifference < 500) { 36 | lastCmd.update(cmd); 37 | cmd = lastCmd; 38 | } else { 39 | this.undos.push(cmd); 40 | cmd.id = ++this.idCounter; 41 | } 42 | 43 | cmd.execute(); 44 | cmd.inMemory = true; 45 | 46 | if (this.config.getKey('history')) { 47 | cmd.json = cmd.toJSON(); 48 | } 49 | this.lastCmdTime = new Date(); 50 | this.redos = []; 51 | } 52 | 53 | undo(): Command | undefined { 54 | let cmd; 55 | 56 | if (this.undos.length > 0) { 57 | cmd = this.undos.pop(); 58 | 59 | if (cmd && !cmd.inMemory && cmd.json) { 60 | cmd.fromJSON(cmd.json); 61 | } 62 | } 63 | 64 | if (cmd) { 65 | cmd.undo(); 66 | this.redos.push(cmd); 67 | } 68 | 69 | return cmd; 70 | } 71 | 72 | redo(): Command | undefined { 73 | let cmd; 74 | 75 | if (this.redos.length > 0) { 76 | cmd = this.redos.pop(); 77 | 78 | if (cmd && !cmd.inMemory && cmd.json) { 79 | cmd.fromJSON(cmd.json); 80 | } 81 | } 82 | 83 | if (cmd !== undefined) { 84 | cmd.execute(); 85 | this.undos.push(cmd); 86 | } 87 | 88 | return cmd; 89 | } 90 | 91 | toJSON(): HistoryJson { 92 | const history = {} as History; 93 | history.undos = []; 94 | history.redos = []; 95 | 96 | if (!this.config.getKey('history')) { 97 | return history; 98 | } 99 | 100 | for (let i = 0; i < this.undos.length; i++) { 101 | const json = this.undos[i].json; 102 | if (json) { 103 | history.undos.push(json); 104 | } 105 | } 106 | 107 | for (let j = 0; j < this.redos.length; j++) { 108 | const json = this.redos[j].json; 109 | if (json) { 110 | history.redos.push(json); 111 | } 112 | } 113 | 114 | return history; 115 | } 116 | 117 | fromJSON(json: HistoryJson): void { 118 | if (json === undefined) return; 119 | 120 | for (let i = 0; i < json.undos.length; i++) { 121 | const cmdJSON = json.undos[i]; 122 | const cmdType = cmdJSON.type as CommandTypes; 123 | // @ts-ignore 124 | const cmd = new Commands[cmdType](this.editor); 125 | cmd.json = cmdJSON; 126 | cmd.id = cmdJSON.id; 127 | cmd.name = cmdJSON.name; 128 | this.undos.push(cmd); 129 | this.idCounter = cmdJSON.id > this.idCounter ? cmdJSON.id : this.idCounter; 130 | } 131 | 132 | for (let j = 0; j < json.redos.length; j++) { 133 | const cmdJSON = json.redos[j]; 134 | const cmdType = cmdJSON.type as CommandTypes; 135 | // @ts-ignore 136 | const cmd = new Commands[cmdType](this.editor); 137 | cmd.json = cmdJSON; 138 | cmd.id = cmdJSON.id; 139 | cmd.name = cmdJSON.name; 140 | this.redos.push(cmd); 141 | this.idCounter = cmdJSON.id > this.idCounter ? cmdJSON.id : this.idCounter; 142 | } 143 | } 144 | 145 | clear(): void { 146 | this.undos = []; 147 | this.redos = []; 148 | this.idCounter = 0; 149 | } 150 | 151 | goToState(id: number): void { 152 | this.editor.editorControls.enabled = false; 153 | 154 | let cmd = this.undos.length > 0 ? this.undos[this.undos.length - 1] : undefined; 155 | 156 | if (cmd === undefined || id > cmd.id) { 157 | cmd = this.redo(); 158 | while (cmd !== undefined && id > cmd.id) { 159 | cmd = this.redo(); 160 | } 161 | } else { 162 | while (true) { 163 | cmd = this.undos[this.undos.length - 1]; 164 | 165 | if (cmd === undefined || id === cmd.id) break; 166 | 167 | this.undo(); 168 | } 169 | } 170 | 171 | this.editor.editorControls.enabled = true; 172 | this.editor.editorControls.update(); 173 | } 174 | 175 | enableSerialization(id: number): void { 176 | this.goToState(-1); 177 | 178 | this.editor.editorControls.enabled = false; 179 | 180 | let cmd = this.redo(); 181 | while (cmd !== undefined) { 182 | if (Object.prototype.hasOwnProperty.call(!cmd, 'json')) { 183 | cmd.json = cmd.toJSON(); 184 | } 185 | cmd = this.redo(); 186 | } 187 | 188 | this.editor.editorControls.enabled = true; 189 | 190 | this.goToState(id); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Loader.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { AMFLoader } from 'three/examples/jsm/loaders/AMFLoader'; 3 | import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader'; 4 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; 5 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'; 6 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; 7 | import { KMZLoader } from 'three/examples/jsm/loaders/KMZLoader'; 8 | import { MD2Loader } from 'three/examples/jsm/loaders/MD2Loader'; 9 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; 10 | import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'; 11 | import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'; 12 | import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader'; 13 | import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader'; 14 | import { VTKLoader } from 'three/examples/jsm/loaders/VTKLoader'; 15 | import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader'; 16 | import { AddObjectCommand } from './commands/AddObjectCommand'; 17 | import { SetSceneCommand } from './commands/SetSceneCommand'; 18 | import { Editor, EditorJson } from './Editor'; 19 | import { THREEJson } from './Types'; 20 | 21 | type FilesMap = {[index: string]: File}; 22 | 23 | export function createFilesMap(files: File[]): FilesMap { 24 | const map: FilesMap = {}; 25 | 26 | for (let i = 0; i < files.length; i++) { 27 | const file = files[i]; 28 | map[file.name] = file; 29 | } 30 | 31 | return map; 32 | } 33 | 34 | export function getFilesFromItemList(items: DataTransferItem[], onDone: (files: File[], filesMap?: FilesMap) => void): void { 35 | let itemsCount = 0; 36 | let itemsTotal = 0; 37 | 38 | const files: File[] = []; 39 | const filesMap: FilesMap = {}; 40 | 41 | function onEntryHandled(): void { 42 | itemsCount++; 43 | 44 | if (itemsCount === itemsTotal) { 45 | onDone(files, filesMap); 46 | } 47 | } 48 | 49 | function handleEntry(entry: any): void { 50 | if (entry?.isDirectory) { 51 | const reader = entry.createReader(); 52 | reader.readEntries((entries: any) => { 53 | for (let i = 0; i < entries.length; i++) { 54 | handleEntry(entries[i]); 55 | } 56 | 57 | onEntryHandled(); 58 | }); 59 | } else if (entry?.isFile) { 60 | entry.file((file: any) => { 61 | files.push(file); 62 | 63 | filesMap[entry.fullPath.substr(1)] = file; 64 | onEntryHandled(); 65 | }); 66 | } 67 | 68 | itemsTotal++; 69 | } 70 | 71 | for (let i = 0; i < items.length; i++) { 72 | handleEntry(items[i].webkitGetAsEntry()); 73 | } 74 | } 75 | 76 | class Loader { 77 | editor: Editor; 78 | texturePath: string; 79 | 80 | constructor(editor: Editor) { 81 | this.editor = editor; 82 | this.texturePath = ''; 83 | } 84 | 85 | loadItemList(items: DataTransferItem[]): void { 86 | const scope = this; 87 | getFilesFromItemList(items, (files: File[], filesMap?: FilesMap) => { 88 | scope.loadFiles(files, filesMap); 89 | }); 90 | } 91 | 92 | loadFiles(files: File[], filesMap?: FilesMap, parent?: THREE.Object3D, onLoad?: (object: THREE.Object3D | undefined) => void, onError?: (error: any) => void): void { 93 | if (files.length > 0) { 94 | filesMap = filesMap || createFilesMap(files); 95 | const manager = new THREE.LoadingManager(); 96 | manager.setURLModifier((url) => { 97 | const file = (filesMap as FilesMap)[url]; 98 | if (file) { 99 | console.log('Loading', url); 100 | return URL.createObjectURL(file); 101 | } 102 | return url; 103 | }); 104 | for (let i = 0; i < files.length; i++) { 105 | this.loadFile(files[i], manager, parent, onLoad, onError); 106 | } 107 | } 108 | } 109 | 110 | loadFile(file: File, manager?: THREE.LoadingManager, parent?: THREE.Object3D, onLoad?: (object: THREE.Object3D | undefined, file: File) => void, onError?: (error: any) => void): void { 111 | const editor = this.editor; 112 | const filename = file.name; 113 | const extension = filename.split('.').pop()?.toLowerCase(); 114 | const reader = new FileReader(); 115 | const throwError = (e: any) => { 116 | if (editor.config.getKey('debug')) console.error('Failed to read file.' + reader.error ? reader.error : e); 117 | reader.abort(); 118 | onError && onError(e); 119 | }; 120 | 121 | reader.addEventListener('progress', (event) => { 122 | const size = '(' + Math.floor(event.total / 1000).toString() + ' KB)'; 123 | const progress = Math.floor((event.loaded / event.total) * 100) + '%'; 124 | console.log('Loading', filename, size, progress); 125 | }); 126 | 127 | reader.addEventListener('error', (event) => { 128 | throwError(event); 129 | }); 130 | 131 | switch (extension) { 132 | case '3ds': 133 | reader.addEventListener('load', (event) => { 134 | try { 135 | const contents = event.target?.result as ArrayBuffer; 136 | const loader = new TDSLoader(manager); 137 | const object = loader.parse(contents, ''); 138 | editor.execute(new AddObjectCommand(editor, object, parent)); 139 | onLoad && onLoad(object, file); 140 | } catch (e) { 141 | throwError(e); 142 | } 143 | }, false); 144 | reader.readAsArrayBuffer(file); 145 | break; 146 | case 'amf': 147 | reader.addEventListener('load', (event) => { 148 | try { 149 | const contents = event.target?.result as ArrayBuffer; 150 | const loader = new AMFLoader(manager); 151 | const amfobject = loader.parse(contents); 152 | editor.execute(new AddObjectCommand(editor, amfobject, parent)); 153 | onLoad && onLoad(amfobject, file); 154 | } catch (e) { 155 | throwError(e); 156 | } 157 | }, false); 158 | reader.readAsArrayBuffer(file); 159 | break; 160 | case 'dae': 161 | reader.addEventListener('load', (event) => { 162 | try { 163 | const contents = event.target?.result as string; 164 | const loader = new ColladaLoader(manager); 165 | const collada = loader.parse(contents, ''); 166 | collada.scene.name = filename; 167 | editor.execute(new AddObjectCommand(editor, collada.scene, parent)); 168 | onLoad && onLoad(collada.scene, file); 169 | } catch (e) { 170 | throwError(e); 171 | } 172 | }, false); 173 | reader.readAsText(file); 174 | break; 175 | case 'fbx': 176 | reader.addEventListener('load', (event) => { 177 | try { 178 | const contents = event.target?.result as ArrayBuffer | string; 179 | const loader = new FBXLoader(manager); 180 | const object = loader.parse(contents, '') as any; 181 | editor.addAnimation(object, object.animations); 182 | editor.execute(new AddObjectCommand(editor, object, parent)); 183 | onLoad && onLoad(object, file); 184 | } catch (e) { 185 | throwError(e); 186 | } 187 | }, false); 188 | reader.readAsArrayBuffer(file); 189 | break; 190 | case 'glb': 191 | reader.addEventListener('load', (event) => { 192 | try { 193 | const contents = event.target?.result as ArrayBuffer; 194 | const dracoLoader = new DRACOLoader(manager); 195 | dracoLoader.setDecoderPath('three/examples/js/libs/draco/gltf/draco_decoder'); 196 | const loader = new GLTFLoader(); 197 | loader.setDRACOLoader(dracoLoader); 198 | loader.parse(contents, '', (result) => { 199 | const scene = result.scene; 200 | scene.name = filename; 201 | editor.addAnimation(scene, result.animations); 202 | editor.execute(new AddObjectCommand(editor, scene, parent)); 203 | onLoad && onLoad(scene, file); 204 | }); 205 | } catch (e) { 206 | throwError(e); 207 | } 208 | }, false); 209 | reader.readAsArrayBuffer(file); 210 | break; 211 | case 'gltf': 212 | reader.addEventListener('load', (event) => { 213 | try { 214 | const contents = event.target?.result as ArrayBuffer; 215 | let loader; 216 | if (this._isGLTF1(contents)) { 217 | alert('Import of glTF asset not possible. Only versions >= 2.0 are supported. Please try to upgrade the file to glTF 2.0 using glTF-Pipeline.'); 218 | } else { 219 | const dracoLoader = new DRACOLoader(manager); 220 | dracoLoader.setDecoderPath('three/examples/js/libs/draco/gltf/draco_decoder'); 221 | loader = new GLTFLoader(manager); 222 | loader.setDRACOLoader(dracoLoader); 223 | } 224 | loader && loader.parse(contents, '', (result) => { 225 | const scene = result.scene; 226 | scene.name = filename; 227 | editor.addAnimation(scene, result.animations); 228 | editor.execute(new AddObjectCommand(editor, scene, parent)); 229 | onLoad && onLoad(scene, file); 230 | }); 231 | } catch (e) { 232 | throwError(e); 233 | } 234 | }, false); 235 | reader.readAsArrayBuffer(file); 236 | break; 237 | case 'js': 238 | case 'json': 239 | case '3geo': 240 | case '3mat': 241 | case '3obj': 242 | case '3scn': 243 | reader.addEventListener('load', (event) => { 244 | try { 245 | const contents = event.target?.result as any; 246 | // 2.0 247 | if (contents.indexOf('postMessage') !== -1) { 248 | const blob = new Blob([contents], { type: 'text/javascript' }); 249 | const url = URL.createObjectURL(blob); 250 | const worker = new Worker(url); 251 | worker.onmessage = (event) => { 252 | event.data.metadata = { version: 2 }; 253 | this._handleJSON(event.data, file, manager, parent); 254 | }; 255 | worker.postMessage(Date.now()); 256 | return; 257 | } 258 | // >= 3.0 259 | const data = JSON.parse(contents); 260 | this._handleJSON(data, file, manager, parent, onLoad); 261 | } catch (e) { 262 | throwError(e); 263 | } 264 | }, false); 265 | reader.readAsText(file); 266 | break; 267 | case 'kmz': 268 | reader.addEventListener('load', (event) => { 269 | try { 270 | const contents = event.target?.result as ArrayBuffer; 271 | const loader = new KMZLoader(manager); 272 | const collada = loader.parse(contents); 273 | collada.scene.name = filename; 274 | editor.execute(new AddObjectCommand(editor, collada.scene, parent)); 275 | onLoad && onLoad(collada.scene, file); 276 | } catch (e) { 277 | throwError(e); 278 | } 279 | }, false); 280 | reader.readAsArrayBuffer(file); 281 | break; 282 | case 'md2': 283 | reader.addEventListener('load', (event) => { 284 | try { 285 | const contents = event.target?.result as ArrayBuffer; 286 | const geometry = new MD2Loader(manager).parse(contents) as any; 287 | const material = new THREE.MeshStandardMaterial({ 288 | morphTargets: true, 289 | morphNormals: true, 290 | }); 291 | const mesh = new THREE.Mesh(geometry, material) as any; 292 | mesh.mixer = new THREE.AnimationMixer(mesh); 293 | mesh.name = filename; 294 | editor.addAnimation(mesh, geometry.animations); 295 | editor.execute(new AddObjectCommand(editor, mesh, parent)); 296 | onLoad && onLoad(mesh, file); 297 | } catch (e) { 298 | throwError(e); 299 | } 300 | }, false); 301 | reader.readAsArrayBuffer(file); 302 | break; 303 | case 'obj': 304 | reader.addEventListener('load', (event) => { 305 | try { 306 | const contents = event.target?.result as string; 307 | const object = new OBJLoader(manager).parse(contents); 308 | object.name = filename; 309 | editor.execute(new AddObjectCommand(editor, object, parent)); 310 | onLoad && onLoad(object, file); 311 | } catch (e) { 312 | throwError(e); 313 | } 314 | }, false); 315 | reader.readAsText(file); 316 | break; 317 | case 'ply': 318 | reader.addEventListener('load', (event) => { 319 | try { 320 | const contents = event.target?.result as ArrayBuffer | string; 321 | const geometry = new PLYLoader(manager).parse(contents) as any; 322 | geometry.sourceType = 'ply'; 323 | geometry.sourceFile = file.name; 324 | const material = new THREE.MeshStandardMaterial(); 325 | const mesh = new THREE.Mesh(geometry, material); 326 | mesh.name = filename; 327 | editor.execute(new AddObjectCommand(editor, mesh, parent)); 328 | onLoad && onLoad(mesh, file); 329 | } catch (e) { 330 | throwError(e); 331 | } 332 | }, false); 333 | reader.readAsArrayBuffer(file); 334 | break; 335 | case 'stl': 336 | reader.addEventListener('load', (event) => { 337 | try { 338 | const contents = event.target?.result as ArrayBuffer; 339 | const geometry = new STLLoader(manager).parse(contents) as any; 340 | geometry.sourceType = 'stl'; 341 | geometry.sourceFile = file.name; 342 | const material = new THREE.MeshStandardMaterial(); 343 | const mesh = new THREE.Mesh(geometry, material); 344 | mesh.name = filename; 345 | editor.execute(new AddObjectCommand(editor, mesh, parent)); 346 | onLoad && onLoad(mesh, file); 347 | } catch (e) { 348 | throwError(e); 349 | } 350 | }, false); 351 | if (reader.readAsBinaryString !== undefined) { 352 | reader.readAsBinaryString(file); 353 | } else { 354 | reader.readAsArrayBuffer(file); 355 | } 356 | break; 357 | case 'svg': 358 | reader.addEventListener('load', (event) => { 359 | try { 360 | const contents = event.target?.result as string; 361 | const loader = new SVGLoader(manager); 362 | const paths = loader.parse(contents).paths; 363 | // 364 | const group = new THREE.Group(); 365 | group.scale.multiplyScalar(0.1); 366 | group.scale.y *= -1; 367 | for (let i = 0; i < paths.length; i++) { 368 | const path = paths[i] as any; 369 | const material = new THREE.MeshBasicMaterial({ 370 | color: path.color, 371 | depthWrite: false, 372 | }); 373 | const shapes = path.toShapes(true); 374 | for (let j = 0; j < shapes.length; j++) { 375 | const shape = shapes[j]; 376 | const geometry = new THREE.ShapeBufferGeometry(shape); 377 | const mesh = new THREE.Mesh(geometry, material); 378 | group.add(mesh); 379 | } 380 | } 381 | editor.execute(new AddObjectCommand(editor, group, parent)); 382 | onLoad && onLoad(group, file); 383 | } catch (e) { 384 | throwError(e); 385 | } 386 | }, false); 387 | reader.readAsText(file); 388 | break; 389 | case 'vtk': 390 | reader.addEventListener('load', (event) => { 391 | try { 392 | const contents = event.target?.result as ArrayBuffer; 393 | const geometry = new VTKLoader(manager).parse(contents, '') as any; 394 | geometry.sourceType = 'vtk'; 395 | geometry.sourceFile = file.name; 396 | const material = new THREE.MeshStandardMaterial(); 397 | const mesh = new THREE.Mesh(geometry, material); 398 | mesh.name = filename; 399 | editor.execute(new AddObjectCommand(editor, mesh, parent)); 400 | onLoad && onLoad(mesh, file); 401 | } catch (e) { 402 | throwError(e); 403 | } 404 | }, false); 405 | reader.readAsText(file); 406 | break; 407 | case 'wrl': 408 | reader.addEventListener('load', (event) => { 409 | try { 410 | const contents = event.target?.result as string; 411 | const result = new VRMLLoader(manager).parse(contents, ''); 412 | editor.execute(new SetSceneCommand(editor, result)); 413 | onLoad && onLoad(result, file); 414 | } catch (e) { 415 | throwError(e); 416 | } 417 | }, false); 418 | reader.readAsText(file); 419 | break; 420 | default: 421 | throwError('Unsupported file format (' + extension + ').'); 422 | break; 423 | } 424 | } 425 | 426 | private _handleJSON( 427 | data: THREEJson, 428 | file: File, 429 | manager?: THREE.LoadingManager, 430 | parent?: THREE.Object3D, 431 | onLoad?: (object: THREE.Object3D | undefined, file: File) => void, 432 | ): void { 433 | const editor = this.editor; 434 | const texturePath = this.texturePath; 435 | if (data.metadata === undefined) { 436 | // 2.0 437 | data.metadata = { type: 'Geometry' }; 438 | } 439 | if (data.metadata.type === undefined) { 440 | // 3.0 441 | data.metadata.type = 'Geometry'; 442 | } 443 | if (data.metadata.formatVersion !== undefined) { 444 | data.metadata.version = data.metadata.formatVersion; 445 | } 446 | let loader; 447 | switch (data.metadata.type.toLowerCase()) { 448 | case 'buffergeometry': 449 | loader = new THREE.BufferGeometryLoader(manager); 450 | const result = loader.parse(data); 451 | const mesh = new THREE.Mesh(result); 452 | editor.execute(new AddObjectCommand(editor, mesh, parent)); 453 | onLoad && onLoad(mesh, file); 454 | break; 455 | case 'geometry': 456 | console.error('Loader: "Geometry" is no longer supported.'); 457 | break; 458 | case 'object': 459 | loader = new THREE.ObjectLoader(); 460 | loader.setResourcePath(texturePath); 461 | loader.parse(data, (result) => { 462 | if (result instanceof THREE.Scene) { 463 | editor.execute(new SetSceneCommand(editor, result)); 464 | onLoad && onLoad(result, file); 465 | } else { 466 | editor.execute(new AddObjectCommand(editor, result, parent)); 467 | onLoad && onLoad(result, file); 468 | } 469 | }); 470 | break; 471 | case 'app': 472 | editor.fromJSON(data as EditorJson); 473 | break; 474 | default: 475 | break; 476 | } 477 | } 478 | 479 | private _isGLTF1(contents: string | ArrayBufferLike): boolean { 480 | let resultContent; 481 | if (typeof contents === 'string') { 482 | // contents is a JSON string 483 | resultContent = contents; 484 | } else { 485 | const magic = THREE.LoaderUtils.decodeText(new Uint8Array(contents, 0, 4)); 486 | if (magic === 'glTF') { 487 | // contents is a .glb file; extract the version 488 | const version = new DataView(contents).getUint32(4, true); 489 | return version < 2; 490 | } else { 491 | // contents is a .gltf file 492 | resultContent = THREE.LoaderUtils.decodeText(new Uint8Array(contents)); 493 | } 494 | } 495 | const json = JSON.parse(resultContent); 496 | return json.asset !== undefined && json.asset.version[0] < 2; 497 | } 498 | } 499 | 500 | export { Loader }; 501 | -------------------------------------------------------------------------------- /src/Settings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import * as THREE from 'three'; 3 | import { defaultSettings } from './defaultSettings'; 4 | 5 | export interface BuildingEditorSettings { 6 | renderer: THREE.WebGLRenderer; 7 | camera: THREE.Camera; 8 | scene: THREE.Scene; 9 | gridHelper: THREE.GridHelper; 10 | axesHelper: THREE.AxesHelper; 11 | planeHelper: THREE.PlaneHelper; 12 | initialObjects: THREE.Object3D[]; 13 | initialHelpers: THREE.Object3D[]; 14 | } 15 | 16 | export type EditorSettings = Partial; 17 | 18 | export class Settings { 19 | renderer: THREE.WebGLRenderer; 20 | camera: THREE.Camera; 21 | scene: THREE.Scene; 22 | gridHelper: THREE.GridHelper; 23 | axesHelper: THREE.AxesHelper; 24 | planeHelper: THREE.PlaneHelper; 25 | initialObjects: THREE.Object3D[]; 26 | initialHelpers: THREE.Object3D[]; 27 | 28 | constructor(settings?: EditorSettings) { 29 | this.renderer = defaultSettings.renderer; 30 | this.camera = defaultSettings.camera; 31 | this.scene = defaultSettings.scene; 32 | this.gridHelper = defaultSettings.gridHelper; 33 | this.axesHelper = defaultSettings.axesHelper; 34 | this.planeHelper = defaultSettings.planeHelper; 35 | this.initialObjects = defaultSettings.initialObjects; 36 | this.initialHelpers = defaultSettings.initialHelpers; 37 | 38 | if (settings) { 39 | Object.assign(this, settings); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/StencilPlane.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { color } from './Color'; 3 | 4 | export class StencilPlane { 5 | plane: THREE.Plane; 6 | stencilPlane: THREE.Mesh; 7 | 8 | constructor(plane: THREE.Plane) { 9 | this.plane = plane; 10 | const planeGeom = new THREE.PlaneBufferGeometry(1000, 1000); 11 | 12 | const planeMat = new THREE.MeshStandardMaterial({ 13 | 14 | color: color.stencilPlane, 15 | metalness: 0.1, 16 | roughness: 0.75, 17 | 18 | stencilWrite: true, 19 | stencilRef: 0, 20 | stencilFunc: THREE.NotEqualStencilFunc, 21 | stencilFail: THREE.ReplaceStencilOp, 22 | stencilZFail: THREE.ReplaceStencilOp, 23 | stencilZPass: THREE.ReplaceStencilOp, 24 | 25 | }); 26 | 27 | this.stencilPlane = new THREE.Mesh(planeGeom, planeMat); 28 | this.stencilPlane.name = 'building-editor-stencilPlane'; 29 | this.stencilPlane.renderOrder = 10; 30 | this.stencilPlane.onAfterRender = function (renderer: { clearStencil: () => void }): void { 31 | renderer.clearStencil(); 32 | }; 33 | } 34 | 35 | update(): void { 36 | const po = this.stencilPlane; 37 | this.plane.coplanarPoint(po.position); 38 | po.lookAt( 39 | po.position.x - this.plane.normal.x, 40 | po.position.y - this.plane.normal.y, 41 | po.position.z - this.plane.normal.z, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | export interface THREEJsonMetaData { 2 | metadata: { 3 | type: string; 4 | version?: number; 5 | formatVersion?: number; 6 | }; 7 | } 8 | export type THREEJson = THREEJsonMetaData & { [index: string]: any }; 9 | -------------------------------------------------------------------------------- /src/commands/AddObjectCommand.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Command } from './Command'; 3 | import { Editor } from '../Editor'; 4 | 5 | class AddObjectCommand extends Command { 6 | type = 'AddObjectCommand'; 7 | parent: THREE.Object3D | undefined; 8 | object: THREE.Object3D | undefined; 9 | index: number | undefined; 10 | 11 | constructor(editor: Editor, object?: THREE.Object3D, parent?: THREE.Object3D, index?: number) { 12 | super(editor); 13 | this.object = object; 14 | this.parent = parent; 15 | this.index = index; 16 | if (object) { 17 | this.name = 'Add Object: ' + object.name; 18 | } 19 | } 20 | 21 | execute(): void { 22 | if (!this.object) return; 23 | this.editor.addObject(this.object, this.parent, this.index); 24 | this.editor.select(this.object); 25 | } 26 | 27 | undo(): void { 28 | if (!this.object) return; 29 | this.editor.removeObject(this.object); 30 | this.editor.select(null); 31 | } 32 | 33 | update(): void {} 34 | 35 | toJSON(): AddObjectCommand { 36 | const output = super.toJSON() as AddObjectCommand; 37 | if (!this.object) return output; 38 | 39 | output.object = this.object.toJSON(); 40 | return output; 41 | } 42 | 43 | fromJSON(json: AddObjectCommand): void { 44 | super.fromJSON(json); 45 | 46 | if (json.object?.uuid) { 47 | this.object = this.editor.objectByUuid(json.object.uuid); 48 | } else { 49 | this.object = undefined; 50 | } 51 | 52 | if (!this.object) { 53 | const loader = new THREE.ObjectLoader(); 54 | this.object = loader.parse(json.object); 55 | } 56 | } 57 | } 58 | 59 | export { AddObjectCommand }; 60 | -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../Editor'; 2 | 3 | export abstract class Command { 4 | editor: Editor; 5 | id: number; 6 | name: string; 7 | inMemory?: boolean; 8 | updatable?: boolean; 9 | json?: Command; 10 | 11 | abstract type: string; 12 | abstract execute(): void; 13 | abstract undo(): void; 14 | abstract update(command: Command): void; 15 | 16 | constructor(editor: Editor) { 17 | this.editor = editor; 18 | this.id = -1; 19 | this.name = ''; 20 | this.inMemory = false; 21 | this.updatable = false; 22 | } 23 | 24 | toJSON(): Command { 25 | const output: Command = { 26 | editor: undefined as unknown as Editor, 27 | type: this.type, 28 | id: this.id, 29 | name: this.name, 30 | execute: this.execute, 31 | undo: this.undo, 32 | update: this.update, 33 | toJSON: this.toJSON, 34 | fromJSON: this.fromJSON, 35 | }; 36 | return output; 37 | } 38 | 39 | fromJSON(json: Command): void { 40 | this.inMemory = true; 41 | this.type = json.type; 42 | this.id = json.id; 43 | this.name = json.name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/RemoveObjectCommand.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Command } from './Command'; 3 | import { Editor } from '../Editor'; 4 | 5 | class RemoveObjectCommand extends Command { 6 | type = 'RemoveObjectCommand'; 7 | parent: THREE.Object3D | undefined; 8 | index: number | undefined; 9 | object: THREE.Object3D | undefined; 10 | parentUuid: string | undefined; 11 | 12 | constructor(editor: Editor, object?: THREE.Object3D) { 13 | super(editor); 14 | this.name = 'Remove Object'; 15 | this.object = object; 16 | this.parent = object?.parent || undefined; 17 | this.index = -1; 18 | if (this.object && this.parent) { 19 | this.index = this.parent.children.indexOf(this.object); 20 | } 21 | } 22 | 23 | execute(): void { 24 | if (!this.object) return; 25 | this.editor.removeObject(this.object); 26 | this.editor.select(null); 27 | } 28 | 29 | undo(): void { 30 | if (!this.object) return; 31 | this.editor.addObject(this.object, this.parent, this.index); 32 | this.editor.select(this.object); 33 | } 34 | 35 | update(): void {} 36 | 37 | toJSON(): RemoveObjectCommand { 38 | const output = super.toJSON() as RemoveObjectCommand; 39 | if (!this.object) return output; 40 | 41 | output.object = this.object.toJSON(); 42 | output.index = this.index; 43 | output.parentUuid = this.parent?.uuid; 44 | 45 | return output; 46 | } 47 | 48 | fromJSON(json: RemoveObjectCommand): void { 49 | super.fromJSON(json); 50 | 51 | this.parent = this.editor.objectByUuid(json.parentUuid); 52 | if (!this.parent) { 53 | this.parent = this.editor.scene; 54 | } 55 | 56 | this.index = json.index; 57 | this.object = this.editor.objectByUuid(json.object?.uuid); 58 | 59 | if (!this.object) { 60 | const loader = new THREE.ObjectLoader(); 61 | this.object = loader.parse(json.object); 62 | } 63 | } 64 | } 65 | 66 | export { RemoveObjectCommand }; 67 | -------------------------------------------------------------------------------- /src/commands/SetPositionCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './Command'; 2 | 3 | import * as THREE from 'three'; 4 | import { Editor } from '../Editor'; 5 | 6 | class SetPositionCommand extends Command { 7 | type = 'SetPositionCommand'; 8 | object: THREE.Object3D; 9 | objectUuid: string; 10 | newPosition: any; 11 | oldPosition: any; 12 | 13 | constructor(editor: Editor, object: THREE.Object3D, newPosition: THREE.Vector3, optionalOldPosition?: THREE.Vector3) { 14 | super(editor); 15 | this.editor = editor; 16 | this.name = 'Set Position'; 17 | this.updatable = true; 18 | this.object = object; 19 | this.objectUuid = this.object.uuid; 20 | this.newPosition = newPosition.clone(); 21 | this.oldPosition = object.position.clone(); 22 | 23 | if (optionalOldPosition !== undefined) { 24 | this.oldPosition = optionalOldPosition.clone(); 25 | } 26 | } 27 | 28 | execute(): void { 29 | this.object.position.copy(this.newPosition); 30 | this.object.updateMatrixWorld(true); 31 | this.editor.objectChanged(this.object); 32 | } 33 | 34 | undo(): void { 35 | this.object.position.copy(this.oldPosition); 36 | this.object.updateMatrixWorld(true); 37 | this.editor.objectChanged(this.object); 38 | } 39 | 40 | update(command: SetPositionCommand): void { 41 | this.newPosition.copy(command.newPosition); 42 | } 43 | 44 | toJSON(): SetPositionCommand { 45 | const output = super.toJSON() as SetPositionCommand; 46 | 47 | output.objectUuid = this.object.uuid; 48 | output.oldPosition = this.oldPosition.toArray(); 49 | output.newPosition = this.newPosition.toArray(); 50 | 51 | return output; 52 | } 53 | 54 | fromJSON(json: SetPositionCommand): void { 55 | super.fromJSON(json); 56 | 57 | this.object = this.editor.objectByUuid(json.objectUuid) as THREE.Object3D; 58 | this.oldPosition = new THREE.Vector3().fromArray(json.oldPosition); 59 | this.newPosition = new THREE.Vector3().fromArray(json.newPosition); 60 | } 61 | } 62 | 63 | export { SetPositionCommand }; 64 | -------------------------------------------------------------------------------- /src/commands/SetRotationCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './Command'; 2 | 3 | import * as THREE from 'three'; 4 | import { Editor } from '../Editor'; 5 | 6 | class SetRotationCommand extends Command { 7 | type = 'SetRotationCommand'; 8 | object: THREE.Object3D 9 | objectUuid: string; 10 | oldRotation: any; 11 | newRotation: any; 12 | 13 | constructor(editor: Editor, object: THREE.Object3D, newRotation: THREE.Euler, optionalOldRotation: THREE.Euler) { 14 | super(editor); 15 | this.name = 'Set Rotation'; 16 | this.updatable = true; 17 | this.object = object; 18 | this.objectUuid = object.uuid; 19 | if (object !== undefined && newRotation !== undefined) { 20 | this.oldRotation = object.rotation.clone(); 21 | this.newRotation = newRotation.clone(); 22 | } 23 | if (optionalOldRotation !== undefined) { 24 | this.oldRotation = optionalOldRotation.clone(); 25 | } 26 | } 27 | 28 | execute(): void { 29 | this.object.rotation.copy(this.newRotation); 30 | this.object.updateMatrixWorld(true); 31 | this.editor.objectChanged(this.object); 32 | } 33 | 34 | undo(): void { 35 | this.object.rotation.copy(this.oldRotation); 36 | this.object.updateMatrixWorld(true); 37 | this.editor.objectChanged(this.object); 38 | } 39 | 40 | update(command: SetRotationCommand): void { 41 | this.newRotation.copy(command.newRotation); 42 | } 43 | 44 | toJSON(): SetRotationCommand { 45 | const output = super.toJSON() as SetRotationCommand; 46 | 47 | output.objectUuid = this.object.uuid; 48 | output.oldRotation = this.oldRotation.toArray(); 49 | output.newRotation = this.newRotation.toArray(); 50 | 51 | return output; 52 | } 53 | 54 | fromJSON(json: SetRotationCommand): void { 55 | super.fromJSON(json); 56 | 57 | this.object = this.editor.objectByUuid(json.objectUuid) as THREE.Object3D; 58 | this.oldRotation = new THREE.Euler().fromArray(json.oldRotation); 59 | this.newRotation = new THREE.Euler().fromArray(json.newRotation); 60 | } 61 | } 62 | 63 | export { SetRotationCommand }; 64 | -------------------------------------------------------------------------------- /src/commands/SetScaleCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './Command'; 2 | 3 | import * as THREE from 'three'; 4 | import { Editor } from '../Editor'; 5 | 6 | class SetScaleCommand extends Command { 7 | type = 'SetScaleCommand'; 8 | object: THREE.Object3D; 9 | objectUuid: string; 10 | oldScale: any; 11 | newScale: any; 12 | 13 | constructor(editor: Editor, object: THREE.Object3D, newScale: THREE.Vector3, optionalOldScale?: THREE.Vector3) { 14 | super(editor); 15 | this.name = 'Set Scale'; 16 | this.updatable = true; 17 | this.object = object; 18 | this.objectUuid = object.uuid; 19 | this.oldScale = object.scale.clone(); 20 | this.newScale = newScale.clone(); 21 | 22 | if (optionalOldScale !== undefined) { 23 | this.oldScale = optionalOldScale.clone(); 24 | } 25 | } 26 | 27 | execute(): void { 28 | this.object.scale.copy(this.newScale); 29 | this.object.updateMatrixWorld(true); 30 | this.editor.objectChanged(this.object); 31 | } 32 | 33 | undo(): void { 34 | this.object.scale.copy(this.oldScale); 35 | this.object.updateMatrixWorld(true); 36 | this.editor.objectChanged(this.object); 37 | } 38 | 39 | update(command: SetScaleCommand): void { 40 | this.newScale.copy(command.newScale); 41 | } 42 | 43 | toJSON(): SetScaleCommand { 44 | const output = super.toJSON() as SetScaleCommand; 45 | 46 | output.objectUuid = this.object.uuid; 47 | output.oldScale = this.oldScale.toArray(); 48 | output.newScale = this.newScale.toArray(); 49 | 50 | return output; 51 | } 52 | 53 | fromJSON(json: SetScaleCommand): void { 54 | super.fromJSON(json); 55 | 56 | this.object = this.editor.objectByUuid(json.objectUuid) as THREE.Object3D; 57 | this.oldScale = new THREE.Vector3().fromArray(json.oldScale); 58 | this.newScale = new THREE.Vector3().fromArray(json.newScale); 59 | } 60 | } 61 | 62 | export { SetScaleCommand }; 63 | -------------------------------------------------------------------------------- /src/commands/SetSceneCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './Command'; 2 | import { SetUuidCommand } from './SetUuidCommand'; 3 | import { SetValueCommand } from './SetValueCommand'; 4 | import { AddObjectCommand } from './AddObjectCommand'; 5 | import { Editor } from '../Editor'; 6 | 7 | class SetSceneCommand extends Command { 8 | type = 'SetSceneCommand'; 9 | cmdArray: Command[]; 10 | cmds?: Command[]; 11 | 12 | constructor(editor: Editor, scene?: THREE.Scene) { 13 | super(editor); 14 | this.name = 'Set Scene'; 15 | this.cmdArray = []; 16 | if (scene !== undefined) { 17 | this.cmdArray.push(new SetUuidCommand(this.editor, this.editor.scene, scene.uuid)); 18 | this.cmdArray.push(new SetValueCommand(this.editor, this.editor.scene, 'name', scene.name)); 19 | this.cmdArray.push(new SetValueCommand(this.editor, this.editor.scene, 'userData', JSON.parse(JSON.stringify(scene.userData)))); 20 | while (scene.children.length > 0) { 21 | const child = scene.children.pop(); 22 | child && this.cmdArray.push(new AddObjectCommand(this.editor, child)); 23 | } 24 | } 25 | } 26 | 27 | execute(): void { 28 | this.editor.editorControls.enabled = false; 29 | 30 | for (let i = 0; i < this.cmdArray.length; i++) { 31 | this.cmdArray[i].execute(); 32 | } 33 | 34 | this.editor.editorControls.enabled = true; 35 | this.editor.editorControls.update(); 36 | } 37 | 38 | undo(): void { 39 | this.editor.editorControls.enabled = false; 40 | 41 | for (let i = this.cmdArray.length - 1; i >= 0; i--) { 42 | this.cmdArray[i].undo(); 43 | } 44 | 45 | this.editor.editorControls.enabled = true; 46 | this.editor.editorControls.update(); 47 | } 48 | 49 | update(): void {} 50 | 51 | toJSON(): SetSceneCommand { 52 | const output = super.toJSON() as SetSceneCommand; 53 | 54 | const cmds = []; 55 | for (let i = 0; i < this.cmdArray.length; i++) { 56 | cmds.push(this.cmdArray[i].toJSON()); 57 | } 58 | output.cmds = cmds; 59 | 60 | return output; 61 | } 62 | 63 | fromJSON(json: SetSceneCommand): void { 64 | super.fromJSON(json); 65 | 66 | const cmds = json.cmds; 67 | if (cmds) { 68 | for (let i = 0; i < cmds.length; i++) { 69 | const cmd = new (window as any)[cmds[i].type](); // creates a new object of type "json.type" 70 | cmd.fromJSON(cmds[i]); 71 | this.cmdArray.push(cmd); 72 | } 73 | } 74 | } 75 | } 76 | 77 | export { SetSceneCommand }; 78 | -------------------------------------------------------------------------------- /src/commands/SetUuidCommand.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../Editor'; 2 | import { Command } from './Command'; 3 | 4 | class SetUuidCommand extends Command { 5 | type = 'SetUuidCommand'; 6 | oldUuid?: string; 7 | newUuid?: string; 8 | object: any; 9 | 10 | constructor(editor: Editor, object: THREE.Object3D, newUuid: string) { 11 | super(editor); 12 | this.name = 'Update UUID'; 13 | this.object = object; 14 | this.oldUuid = object !== undefined ? object.uuid : undefined; 15 | this.newUuid = newUuid; 16 | } 17 | 18 | execute() { 19 | if (!this.object || !this.newUuid) return; 20 | 21 | this.object.uuid = this.newUuid; 22 | this.editor.objectChanged(this.object); 23 | } 24 | 25 | undo() { 26 | if (!this.object || !this.oldUuid) return; 27 | 28 | this.object.uuid = this.oldUuid; 29 | this.editor.objectChanged(this.object); 30 | } 31 | 32 | update(): void {} 33 | 34 | toJSON(): SetUuidCommand { 35 | const output = super.toJSON() as SetUuidCommand; 36 | 37 | output.oldUuid = this.oldUuid; 38 | output.newUuid = this.newUuid; 39 | 40 | return output; 41 | } 42 | 43 | fromJSON(json: SetUuidCommand): void { 44 | super.fromJSON(json); 45 | 46 | this.oldUuid = json.oldUuid; 47 | this.newUuid = json.newUuid; 48 | this.object = this.editor.objectByUuid(json.oldUuid); 49 | 50 | if (this.object === undefined) { 51 | this.object = this.editor.objectByUuid(json.newUuid); 52 | } 53 | } 54 | } 55 | 56 | export { SetUuidCommand }; 57 | -------------------------------------------------------------------------------- /src/commands/SetValueCommand.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../Editor'; 2 | import { Command } from './Command'; 3 | 4 | class SetValueCommand extends Command { 5 | type = 'SetValueCommand'; 6 | oldValue: any; 7 | newValue: any; 8 | objectUuid?: string; 9 | attributeName: string; 10 | object: THREE.Object3D | undefined; 11 | 12 | constructor(editor: Editor, object: THREE.Object3D, attributeName: string, newValue: any) { 13 | super(editor); 14 | this.name = 'Set ' + attributeName; 15 | this.updatable = true; 16 | this.object = object; 17 | this.attributeName = attributeName; 18 | this.oldValue = object ? (object as any)[this.attributeName] : undefined; 19 | this.newValue = newValue; 20 | } 21 | 22 | execute(): void { 23 | if (!this.object || !this.attributeName) return; 24 | 25 | (this.object as any)[this.attributeName] = this.newValue; 26 | this.editor.objectChanged(this.object); 27 | } 28 | 29 | undo(): void { 30 | if (!this.object || !this.attributeName) return; 31 | 32 | (this.object as any)[this.attributeName] = this.oldValue; 33 | this.editor.objectChanged(this.object); 34 | } 35 | 36 | update(cmd: SetValueCommand): void { 37 | this.newValue = cmd.newValue; 38 | } 39 | 40 | toJSON(): SetValueCommand { 41 | const output = super.toJSON() as SetValueCommand; 42 | 43 | output.objectUuid = this.object?.uuid; 44 | output.attributeName = this.attributeName; 45 | output.oldValue = this.oldValue; 46 | output.newValue = this.newValue; 47 | 48 | return output; 49 | } 50 | 51 | fromJSON(json: SetValueCommand): void { 52 | super.fromJSON(json); 53 | 54 | this.attributeName = json.attributeName; 55 | this.oldValue = json.oldValue; 56 | this.newValue = json.newValue; 57 | this.object = this.editor.objectByUuid(json.objectUuid); 58 | } 59 | } 60 | 61 | export { SetValueCommand }; 62 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { AddObjectCommand } from './AddObjectCommand'; 2 | export { RemoveObjectCommand } from './RemoveObjectCommand'; 3 | export { SetPositionCommand } from './SetPositionCommand'; 4 | export { SetRotationCommand } from './SetRotationCommand'; 5 | export { SetScaleCommand } from './SetScaleCommand'; 6 | export { SetSceneCommand } from './SetSceneCommand'; 7 | export { SetUuidCommand } from './SetUuidCommand'; 8 | export { SetValueCommand } from './SetValueCommand'; 9 | -------------------------------------------------------------------------------- /src/commands/types.ts: -------------------------------------------------------------------------------- 1 | const commands = [ 2 | 'AddObjectCommand', 3 | 'RemoveObjectCommand', 4 | 'SetPositionCommand', 5 | 'SetRotationCommand', 6 | 'SetScaleCommand', 7 | 'SetSceneCommand', 8 | 'SetUuidCommand', 9 | 'SetValueCommand', 10 | ] as const; 11 | 12 | export type CommandTypes = typeof commands[number]; 13 | -------------------------------------------------------------------------------- /src/controls/EditorControls.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export class EditorControls extends THREE.EventDispatcher { 4 | enabled: boolean; 5 | updateEvent: { type: string }; 6 | 7 | constructor() { 8 | super(); 9 | 10 | this.enabled = true; 11 | this.updateEvent = { type: 'update' }; 12 | } 13 | 14 | update(): void { 15 | if (!this.enabled) return; 16 | this.dispatchEvent(this.updateEvent); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/controls/ViewCubeControls.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Config } from '../Config'; 3 | 4 | const defaultStyle = ` 5 | .viewCubeControls { 6 | font-family: sans-serif; 7 | transform: scaleY(-1); 8 | -webkit-touch-callout: none; 9 | -webkit-user-select: none; 10 | -khtml-user-select: none; 11 | -moz-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | } 15 | 16 | .viewCubeControls > .box { 17 | position: relative; 18 | transform-style: preserve-3d; 19 | } 20 | 21 | .viewCubeControls > .box > .face { 22 | cursor: grab; 23 | background-color: #fff; 24 | position: absolute; 25 | box-shadow: inset 0 0 0 1px #222; 26 | font-weight: bold; 27 | color: #222; 28 | text-align: center; 29 | } 30 | 31 | .viewCubeControls > .box > .face:hover { 32 | background-color: #ddd; 33 | cursor: pointer; 34 | } 35 | 36 | .viewCubeControls > .box > .ring { 37 | pointer-events: none; 38 | position: absolute; 39 | border-radius: 100%; 40 | box-shadow: inset 0 0 0 1px #222, 0 0 0 1px #ddd; 41 | background-color: #fff; 42 | } 43 | .viewCubeControls > .box > .ring > div { 44 | color: #222; 45 | position: absolute; 46 | font-weight: bold; 47 | } 48 | `; 49 | 50 | export class ViewCubeControls { 51 | static cssElement: HTMLStyleElement; 52 | element: HTMLDivElement; 53 | size: number; 54 | style: string; 55 | perspective: boolean; 56 | visible: boolean; 57 | northDirection: number; 58 | update: () => void; 59 | 60 | constructor(config: Config, camera: THREE.Camera) { 61 | this.size = config.getKey('control/viewCubeControls/size') || 40; 62 | this.style = config.getKey('control/viewCubeControls/style') || defaultStyle; 63 | this.perspective = config.getKey('control/viewCubeControls/perspective') || false; 64 | this.visible = config.getKey('control/viewCubeControls/visible'); 65 | this.northDirection = config.getKey('control/viewCubeControls/northDirection') || 0; 66 | 67 | const size = this.size; 68 | const style = this.style; 69 | 70 | function epsilon(value: number): number { 71 | return Math.abs(value) < 1e-10 ? 0 : value; 72 | } 73 | 74 | function getObjectCSSMatrix(matrix: THREE.Matrix4): string { 75 | const elements = matrix.elements; 76 | const matrix3d = 77 | 'matrix3d(' + 78 | epsilon(elements[0]) + 79 | ',' + 80 | epsilon(elements[1]) + 81 | ',' + 82 | epsilon(elements[2]) + 83 | ',' + 84 | epsilon(elements[3]) + 85 | ',' + 86 | epsilon(-elements[4]) + 87 | ',' + 88 | epsilon(-elements[5]) + 89 | ',' + 90 | epsilon(-elements[6]) + 91 | ',' + 92 | epsilon(-elements[7]) + 93 | ',' + 94 | epsilon(elements[8]) + 95 | ',' + 96 | epsilon(elements[9]) + 97 | ',' + 98 | epsilon(elements[10]) + 99 | ',' + 100 | epsilon(elements[11]) + 101 | ',' + 102 | epsilon(elements[12]) + 103 | ',' + 104 | epsilon(elements[13]) + 105 | ',' + 106 | epsilon(elements[14]) + 107 | ',' + 108 | epsilon(elements[15]) + 109 | ')'; 110 | 111 | return 'translate(-50%,-50%)' + matrix3d; 112 | } 113 | 114 | const matrix = new THREE.Matrix4(); 115 | 116 | const sides = { 117 | front: 'rotateY( 0deg) translateZ(%SIZE)', 118 | right: 'rotateY( 90deg) translateZ(%SIZE)', 119 | back: 'rotateY(180deg) translateZ(%SIZE)', 120 | left: 'rotateY(-90deg) translateZ(%SIZE)', 121 | top: 'rotateX( 90deg) translateZ(%SIZE)', 122 | bottom: 'rotateX(-90deg) translateZ(%SIZE)', 123 | }; 124 | 125 | const offsets = { 126 | n: [0, -1], 127 | e: [1, 0], 128 | s: [0, 1], 129 | w: [-1, 0], 130 | }; 131 | 132 | this.size = size; 133 | 134 | const unit = 'px'; 135 | 136 | if (!ViewCubeControls.cssElement) { 137 | const head = document.head || document.getElementsByTagName('head')[0]; 138 | const element = document.createElement('style'); 139 | 140 | element.id = 'viewCubeControls'; 141 | // element.href = undefined; 142 | element.appendChild(document.createTextNode(style)); 143 | 144 | head.insertBefore(element, head.firstChild); 145 | 146 | ViewCubeControls.cssElement = element; 147 | } 148 | 149 | // Container 150 | const container = document.createElement('div'); 151 | 152 | container.className = 'viewCubeControls'; 153 | 154 | container.style.width = size + unit; 155 | container.style.height = size + unit; 156 | 157 | // Box 158 | const box = document.createElement('div'); 159 | 160 | box.className = 'box'; 161 | box.style.width = size + unit; 162 | box.style.height = size + unit; 163 | box.style.fontSize = size / 6 + unit; 164 | 165 | container.appendChild(box); 166 | 167 | // Ring + cardinal points 168 | const ring = document.createElement('div'); 169 | 170 | const R = Math.PI * 0.8; 171 | const s = (size * R) / 2; 172 | 173 | const dir = this.northDirection; 174 | const rotatedOffsets = { 175 | n: [offsets.n[0] * Math.cos(dir) - offsets.n[1] * Math.sin(dir), offsets.n[0] * Math.sin(dir) + offsets.n[1] * Math.cos(dir)], 176 | e: [offsets.e[0] * Math.cos(dir) - offsets.e[1] * Math.sin(dir), offsets.e[0] * Math.sin(dir) + offsets.e[1] * Math.cos(dir)], 177 | s: [offsets.s[0] * Math.cos(dir) - offsets.s[1] * Math.sin(dir), offsets.s[0] * Math.sin(dir) + offsets.s[1] * Math.cos(dir)], 178 | w: [offsets.w[0] * Math.cos(dir) - offsets.w[1] * Math.sin(dir), offsets.w[0] * Math.sin(dir) + offsets.w[1] * Math.cos(dir)], 179 | }; 180 | 181 | const loc = { 182 | n: [rotatedOffsets.n[0] * s + s, rotatedOffsets.n[1] * s + s], 183 | e: [rotatedOffsets.e[0] * s + s, rotatedOffsets.e[1] * s + s], 184 | s: [rotatedOffsets.s[0] * s + s, rotatedOffsets.s[1] * s + s], 185 | w: [rotatedOffsets.w[0] * s + s, rotatedOffsets.w[1] * s + s], 186 | }; 187 | 188 | const directions = { 189 | n: 'translateX(' + loc.n[0] + unit + ') translateY(' + loc.n[1] + unit + ')', 190 | e: 'translateX(' + loc.e[0] + unit + ') translateY(' + loc.e[1] + unit + ')', 191 | s: 'translateX(' + loc.s[0] + unit + ') translateY(' + loc.s[1] + unit + ')', 192 | w: 'translateX(' + loc.w[0] + unit + ') translateY(' + loc.w[1] + unit + ')', 193 | }; 194 | 195 | function direction(name: 'N' | 'E' | 'S' | 'W'): void { 196 | const e = document.createElement('div'); 197 | 198 | const id = name.toLowerCase() as keyof typeof directions; 199 | 200 | const fs = size / 6; 201 | 202 | e.id = id; 203 | e.textContent = name; 204 | e.style.transform = directions[id]; 205 | e.style.fontSize = fs + unit; 206 | e.style.left = -size / 2 / 6 - rotatedOffsets[id][0] * fs + unit; 207 | e.style.top = -size / 2 / 6 - rotatedOffsets[id][1] * fs + unit; 208 | 209 | ring.appendChild(e); 210 | } 211 | 212 | direction('N'); 213 | direction('E'); 214 | direction('S'); 215 | direction('W'); 216 | 217 | ring.className = 'ring'; 218 | ring.style.transform = 'rotateX(90deg) translateZ(' + (s - size) + unit + ') translateX(' + (-(s * 8 / Math.PI - size) / 3) + unit + ')'; // should calc 219 | ring.style.width = size * R + unit; 220 | ring.style.height = size * R + unit; 221 | 222 | box.appendChild(ring); 223 | 224 | // Sides 225 | function plane(side: 'Front' | 'Right' | 'Back' | 'Left' | 'Top' | 'Bottom'): HTMLDivElement { 226 | const e = document.createElement('div'); 227 | 228 | const id = side.toLowerCase() as keyof typeof sides; 229 | 230 | e.id = id; 231 | e.textContent = side; 232 | e.className = id + ' face'; 233 | 234 | e.style.width = size + unit; 235 | e.style.height = size + unit; 236 | e.style.transform = sides[id].replace('%SIZE', size / 2 + unit); 237 | e.style.lineHeight = size + unit; 238 | 239 | box.appendChild(e); 240 | 241 | return e; 242 | } 243 | 244 | plane('Front'); 245 | plane('Right'); 246 | plane('Back'); 247 | plane('Left'); 248 | plane('Top'); 249 | plane('Bottom'); 250 | 251 | container.style.visibility = this.visible ? 'visible' : 'hidden'; 252 | this.element = container; 253 | 254 | this.update = (): void => { 255 | const size = this.size; 256 | const half = size / 2; 257 | matrix.copy(camera.matrixWorldInverse); 258 | 259 | matrix.elements[12] = half; 260 | matrix.elements[13] = half; 261 | matrix.elements[14] = 0; 262 | 263 | const style = getObjectCSSMatrix(matrix); 264 | 265 | box.style.transform = style; 266 | 267 | container.style.visibility = this.visible ? 'visible' : 'hidden'; 268 | // container.style.perspective = (this.perspective && camera instanceof THREE.PerspectiveCamera 269 | // ? Math.pow(size * size + size * size, 0.5) / Math.tan(((camera.fov / 2) * Math.PI) / 180) : 0) + unit; 270 | }; 271 | } 272 | 273 | remove(): void { 274 | this.element.remove(); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | import { BuildingEditorConfig } from './Config'; 2 | 3 | export const defaultConfig: BuildingEditorConfig = { 4 | 'exportPrecision': 6, 5 | 'control/orbitControls/enable': true, 6 | 'control/transformControls/enable': true, 7 | 'control/viewCubeControls/visible': true, 8 | 'debug': false, 9 | 'history': false, 10 | 'select/enabled': true, 11 | 'redo/enabled': true, 12 | 'undo/enabled': true, 13 | 'delete/enabled': true, 14 | 'contextmenu/enabled': true, 15 | 'shortcuts/translate': 't', 16 | 'shortcuts/rotate': 'r', 17 | 'shortcuts/scale': 's', 18 | 'shortcuts/undo': 'z', 19 | 'shortcuts/focus': 'f', 20 | }; 21 | -------------------------------------------------------------------------------- /src/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { color } from './Color'; 3 | import { BuildingEditorSettings } from './Settings'; 4 | 5 | // renderer 6 | const renderer = new THREE.WebGLRenderer({ alpha: true }); 7 | renderer.autoClear = false; 8 | renderer.shadowMap.autoUpdate = false; 9 | renderer.outputEncoding = THREE.sRGBEncoding; 10 | 11 | // camera 12 | const camera = new THREE.PerspectiveCamera(60, 1, 0.01, 10000); 13 | camera.name = 'camera'; 14 | camera.position.set(0, 20, 50); 15 | camera.lookAt(new THREE.Vector3()); 16 | 17 | // scene 18 | const scene = new THREE.Scene(); 19 | scene.name = 'scene'; 20 | scene.background = new THREE.Color(color['scene/background']); 21 | 22 | // gridHelper 23 | const gridHelper = new THREE.GridHelper(100, 20, color.gridHelper); 24 | gridHelper.name = 'gridHelper'; 25 | 26 | // axesHelper 27 | const axesHelper = new THREE.AxesHelper(50); 28 | axesHelper.name = 'axesHelper'; 29 | 30 | // planeHelper 31 | const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0); 32 | const planeHelper = new THREE.PlaneHelper(plane, 10, color.planeHelper); 33 | planeHelper.renderOrder = 1; 34 | planeHelper.visible = false; 35 | 36 | export const defaultSettings: BuildingEditorSettings = { 37 | renderer, 38 | camera, 39 | scene, 40 | gridHelper, 41 | axesHelper, 42 | planeHelper, 43 | initialObjects: [], 44 | initialHelpers: [], 45 | }; 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Editor, TransformControlsMode } from './Editor'; 2 | import { Config, EditorConfig } from './Config'; 3 | import { Settings, EditorSettings } from './Settings'; 4 | import { StencilPlane } from './StencilPlane'; 5 | import * as Commands from './commands'; 6 | import { EditorControls } from './controls/EditorControls'; 7 | 8 | export { 9 | Editor, 10 | Config, 11 | Settings, 12 | Commands, 13 | StencilPlane, 14 | EditorControls, 15 | }; 16 | 17 | export type { 18 | EditorConfig, 19 | EditorSettings, 20 | TransformControlsMode, 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | let enableCall = true; 2 | 3 | export function throttle(callback: (...args: any[]) => void, ms: number, ...args: any[]): void { 4 | if (!enableCall) return; 5 | 6 | enableCall = false; 7 | callback(...args); 8 | setTimeout(() => { 9 | enableCall = true; 10 | }, ms); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/viewportUtils.ts: -------------------------------------------------------------------------------- 1 | interface ViewSize { 2 | width: number; 3 | height: number; 4 | aspect: number; 5 | } 6 | 7 | export function getViewportSize(): ViewSize | null { 8 | const viewportElement = document.getElementById('building-editor-viewport'); 9 | if (!viewportElement) return null; 10 | const viewport = viewportElement.getBoundingClientRect(); 11 | 12 | const width = viewport.width; 13 | const height = viewport.height; 14 | 15 | return { width, height, aspect: height / width }; 16 | } 17 | 18 | export function getViewSize(): ViewSize { 19 | const viewportSize = getViewportSize(); 20 | const width = viewportSize ? viewportSize.width : window.innerWidth; 21 | const height = viewportSize ? viewportSize.height : window.innerHeight; 22 | 23 | return { width, height, aspect: height / width }; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "pretty": true, 10 | "newLine": "lf", 11 | "outDir": "dist", 12 | "baseUrl": "./" 13 | }, 14 | "include": ["src"], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------