├── .gitignore ├── README.md ├── app.js ├── app.json ├── app.wxss ├── package.json ├── pages ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── lists │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── scene │ ├── camera.js │ └── scene.js ├── project.config.json ├── sitemap.json └── utils ├── loadingManager.js ├── mtlLoader.js ├── objLoader.js ├── orbitControls.js ├── pointPick.js ├── requestAnimationFrame.js ├── threeutils.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | .project 4 | node_modules 5 | .idea/ 6 | .vscode/ 7 | npm-debug.log 8 | .jshintrc 9 | .vs/ 10 | test/unit/three.*.unit.js 11 | miniprogram_npm 12 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wx_threeJs_project 2 | 微信小程序,使用three.js引擎,渲染场景 3 | 4 | 基于官方适配的threejs-miniprogram 兼容three.j框架 5 | 6 | 修改兼容Three.js 官方提供的ObjLoader.js 7 | 8 | 完成功能 9 | 1.导入显示obj文件 10 | 2.选种图中实体 11 | 3.给选中的实体更换材质 12 | 4.场景切换 13 | 14 | 15 | 16 | ![image]( https://cdn.jsdelivr.net/gh/ZoeLeee/cdn/123.gif) 17 | ![image]( https://cdn.jsdelivr.net/gh/ZoeLeee/cdn/456.gif) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | wx.getSystemInfo({ 16 | success:(res)=> { 17 | // 获取可使用窗口宽度 18 | this.globalData.height= res.windowHeight; 19 | // 获取可使用窗口高度 20 | this.globalData.width = res.windowWidth; 21 | } 22 | }) 23 | }, 24 | globalData: { 25 | height:0,width:0 26 | }, 27 | THREE:null, 28 | Viewer:null, 29 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/lists/index", 4 | "pages/index/index" 5 | ], 6 | "window": { 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle": "black" 11 | }, 12 | "style": "v2", 13 | "sitemapLocation": "sitemap.json" 14 | } -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | page{ 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookcad", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "three": "^0.113.2", 13 | "threejs-miniprogram": "0.0.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | import { 3 | createScopedThreejs 4 | } from 'threejs-miniprogram' 5 | 6 | import Viewer from '../scene/scene.js'; 7 | import { pointPick } from '../../utils/pointPick.js'; 8 | 9 | //获取应用实例 10 | const app = getApp() 11 | 12 | Page({ 13 | data: { isMove: false,showMtls:"none" }, 14 | onLoad: function (option) { 15 | const eventChannel = this.getOpenerEventChannel(); 16 | eventChannel.on('acceptData', function (data) { 17 | wx.createSelectorQuery().select('#canvas').node().exec((res) => { 18 | const canvas = res[0].node; 19 | let [width, height] = [app.globalData.width, app.globalData.height]; 20 | canvas.width=width; 21 | canvas.height=height; 22 | const THREE = createScopedThreejs(canvas); 23 | const viewer = new Viewer(); 24 | viewer.init(canvas, THREE); 25 | app.Viewer = viewer; 26 | app.THREE = THREE; 27 | if (data.url.endsWith(".mtl")) 28 | viewer.loadObjAndMtl(data.url); 29 | else 30 | viewer.loaderObj(data.url); 31 | }); 32 | }) 33 | }, 34 | touchstart(e) { 35 | app.Viewer.controls.onTouchStart(e); 36 | }, 37 | touchMove(e) { 38 | this.setData({ isMove: true }); 39 | app.Viewer.controls.onTouchMove(e); 40 | }, 41 | touchEnd(e) { 42 | if (!this.data.isMove) { 43 | let selectObjects = app.Viewer.selectObjects; 44 | let o = pointPick({ x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }); 45 | console.log(o); 46 | if (selectObjects.has(o)) { 47 | o.material.emissive = new app.THREE.Color(0x000000); 48 | selectObjects.delete(o) 49 | } 50 | else { 51 | if (o && o.material && o.material.type === "MeshPhongMaterial") { 52 | o.material.emissive = new app.THREE.Color(0x33C541); 53 | selectObjects.add(o); 54 | } 55 | } 56 | } 57 | app.Viewer.controls.onTouchEnd(e); 58 | this.setData({ isMove: false }); 59 | }, 60 | selectMtl(e){ 61 | let viewer=app.Viewer; 62 | let url=e.target.dataset.url; 63 | if(!url) return; 64 | viewer.changeObjectsMaterial(url) 65 | }, 66 | clearSelct(){ 67 | app.Viewer.cancelSelect(); 68 | }, 69 | toggleShowMtls(){ 70 | this.setData({ 71 | showMtls:this.data.showMtls==="none"?"flex":"none" 72 | }) 73 | }, 74 | toggleSceneBackground(){ 75 | let viewer=app.Viewer; 76 | viewer.loadSceneBg(); 77 | }, 78 | onHide() { 79 | app.Viewer.clear(); 80 | } 81 | }) -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | #canvas{ 2 | width:100%; 3 | height:100%; 4 | } 5 | .container{ 6 | height: 100%; 7 | } 8 | 9 | .mtl-list{ 10 | position: fixed; 11 | bottom: 10%; 12 | display: flex; 13 | overflow-x: scroll ; 14 | } 15 | .mtl-list>cover-imgage{ 16 | width: 25%; 17 | height: 25%; 18 | } 19 | 20 | .clear-btn{ 21 | position: fixed; 22 | left: 20rpx; 23 | bottom: 20rpx; 24 | display: flex; 25 | width: 100%; 26 | justify-content: space-between; 27 | } -------------------------------------------------------------------------------- /pages/lists/index.js: -------------------------------------------------------------------------------- 1 | // pages/lists/index.js 2 | Page({ 3 | data: { 4 | url:"" 5 | }, 6 | look(e){ 7 | let url=e.target.dataset.url; 8 | if(!url) 9 | url=this.data.url; 10 | wx.navigateTo({ 11 | url: "/pages/index/index", 12 | success:(res)=> { 13 | this.setData({url:""}); 14 | res.eventChannel.emit('acceptData', { url }) 15 | } 16 | }); 17 | }, 18 | change(e){ 19 | this.setData({url:e.detail.value}); 20 | }, 21 | scanCode(){ 22 | wx.scanCode({ 23 | success: (res) => { 24 | let url=res.result; 25 | if(url&&url.startsWith("http")&&url.endsWith(".obj")){ 26 | wx.navigateTo({ 27 | url: "/pages/index/index", 28 | success:(res)=> { 29 | this.setData({url:""}); 30 | res.eventChannel.emit('acceptData', { url}) 31 | } 32 | }); 33 | } 34 | }, 35 | }) 36 | }, 37 | }) -------------------------------------------------------------------------------- /pages/lists/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/lists/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 柜子 10 | 11 | haha 12 | male02mtl 13 | male02_dds 14 | wenli 15 | m2 16 | m22 17 | 18 | 19 | -------------------------------------------------------------------------------- /pages/lists/index.wxss: -------------------------------------------------------------------------------- 1 | /* pages/lists/index.wxss */ 2 | .container{ 3 | padding: 30rpx; 4 | } 5 | 6 | .input{ 7 | display: flex; 8 | align-content: center; 9 | align-items: center; 10 | align-self: center; 11 | height: 60rpx 12 | } 13 | .input>input{ 14 | outline: 1px solid #ccc; 15 | height: 60rpx; 16 | width: 60%; 17 | } 18 | .input>button{ 19 | flex: 1; 20 | height: 60rpx; 21 | line-height: 60rpx; 22 | font-size: 26rpx; 23 | padding: 0; 24 | } -------------------------------------------------------------------------------- /pages/scene/camera.js: -------------------------------------------------------------------------------- 1 | const app=getApp(); 2 | class Camera { 3 | constructor(THREE) { 4 | this.THREE=THREE; 5 | this.intance=null; 6 | this._size = 1.5; 7 | this.orthCamera=null; 8 | this.persCamera=null; 9 | this.init(); 10 | } 11 | initOrthCamera() { 12 | const THREE=this.THREE; 13 | let [width, height] = [app.globalData.width, app.globalData.height]; 14 | const aspect = height / width; 15 | const size = this._size; 16 | const camera = new THREE.OrthographicCamera(-size, size, size * aspect, -size * aspect, -1000, 1000); 17 | camera.position.set(-1, 1, 1); 18 | const target = new THREE.Vector3(0, 0, 0); 19 | camera.lookAt(target); 20 | this.orthCamera = camera; 21 | this.target=target; 22 | } 23 | initPeCamera(){ 24 | const THREE=this.THREE; 25 | const fov = 75; 26 | let [width, height] = [app.globalData.width, app.globalData.height]; 27 | const aspect = width / height; 28 | const near = 0.1; 29 | const far = 100; 30 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 31 | camera.position.z = 5; 32 | this.persCamera = camera; 33 | } 34 | init(){ 35 | this.initOrthCamera(); 36 | this.initPeCamera(); 37 | this.intance=this.orthCamera; 38 | } 39 | } 40 | export default Camera; -------------------------------------------------------------------------------- /pages/scene/scene.js: -------------------------------------------------------------------------------- 1 | import { getLoader } from '../../utils/objLoader.js'; 2 | import Camera from './camera'; 3 | import { GetOrbitControls } from '../../utils/orbitControls'; 4 | import { getMtlLoader } from '../../utils/mtlLoader.js'; 5 | import { LoadingManager } from '../../utils/loadingManager.js'; 6 | 7 | const uv = "http://cdn.dodream.top/uv_grid_opengl.jpg?key=joelee"; 8 | const app=getApp(); 9 | class Viewer { 10 | constructor() { 11 | this.camera = null; 12 | this.scene = null; 13 | this.renderer = null; 14 | this.objLoader = null; 15 | this.textureLoader = null; 16 | this.mtlLoader=null; 17 | this.plane=null; 18 | this.selectObjects=new Set(); 19 | } 20 | init(canvas, THREE) { 21 | this.THREE = THREE; 22 | this.canvas = canvas; 23 | this.initScene(THREE); 24 | this.initRenderer(canvas, THREE); 25 | 26 | let camera = new Camera(THREE); 27 | 28 | this.scene.add(camera.intance) 29 | 30 | this.camera = camera; 31 | 32 | this.initHelper(THREE); 33 | 34 | this.initPlane(THREE); 35 | this.initLight(THREE); 36 | this.initControl(THREE); 37 | this.initLoader(THREE); 38 | 39 | const ObjLoader = getLoader(THREE); 40 | let objLoader = new ObjLoader(THREE.DefaultLoadingManager) 41 | this.objLoader = objLoader; 42 | 43 | this.textureLoader = new THREE.TextureLoader(); 44 | 45 | const MTLLoader =getMtlLoader(THREE); 46 | 47 | this.mtlLoader=new MTLLoader(new THREE.LoadingManager()); 48 | 49 | // this.testScene(THREE); 50 | // this.loadSceneBg(); 51 | this.animate(); 52 | } 53 | initLoader(THREE){ 54 | console.log(THREE); 55 | THREE.LoadingManager=LoadingManager; 56 | } 57 | initScene(THREE) { 58 | const scene = new THREE.Scene(); 59 | scene.background = new THREE.Color(0xf0f0f0); 60 | this.scene = scene; 61 | 62 | } 63 | initRenderer(canvas, THREE) { 64 | let renderer = new THREE.WebGLRenderer({ 65 | canvas, 66 | antialias: true 67 | }); 68 | this.renderer = renderer; 69 | } 70 | initHelper(THREE) { 71 | const axesHelper = new THREE.AxesHelper(100); 72 | this.scene.add(axesHelper); 73 | 74 | var helper = new THREE.GridHelper(10, 20); 75 | helper.position.y = - 0.1; 76 | helper.material.opacity = 0.25; 77 | helper.material.transparent = true; 78 | this.scene.add(helper); 79 | } 80 | initPlane(THREE) { 81 | var planeGeometry = new THREE.PlaneBufferGeometry(10, 10); 82 | planeGeometry.rotateX(- Math.PI / 2); 83 | var planeMaterial = new THREE.MeshBasicMaterial({ transparent: true, opacity: 0.25 }); 84 | var plane = new THREE.Mesh(planeGeometry, planeMaterial); 85 | plane.name="plane"; 86 | this.plane=plane; 87 | this.scene.add(plane); 88 | } 89 | initLight(THREE) { 90 | this.scene.add(new THREE.AmbientLight(0xf0f0f0)); 91 | } 92 | initControl(THREE) { 93 | const OrbitControls = GetOrbitControls(this.camera.intance, this.renderer.domElement, THREE); 94 | var controls = new OrbitControls(this.camera.intance, this.renderer.domElement); 95 | controls.target.set(0, 0, 0); 96 | controls.update(); 97 | this.controls = controls; 98 | } 99 | loadSceneBg(){ 100 | 101 | if(this.camera.intance===this.camera.orthCamera){ 102 | const THREE=this.THREE; 103 | let texture=new THREE.CubeTextureLoader() 104 | .setPath( 'http://cdn.dodream.top/' ) 105 | .load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] ); 106 | this.scene.background = texture; 107 | this.plane.visiable=false; 108 | } 109 | else{ 110 | this.scene.background =new this.THREE.Color(0xf0f0f0); 111 | this.plane.visiable=true; 112 | } 113 | this.switchCamera(); 114 | 115 | } 116 | switchCamera(){ 117 | if(this.camera.intance===this.camera.orthCamera) 118 | this.camera.intance=this.camera.persCamera; 119 | else 120 | this.camera.intance=this.camera.orthCamera; 121 | 122 | this.controls.object=this.camera.intance; 123 | this.controls.update(); 124 | } 125 | testScene(THREE) { 126 | let objLoader = this.objLoader; 127 | //http://cdn.dodream.top/haha.obj 128 | 129 | objLoader.load( 130 | // resource URL 131 | 'http://cdn.dodream.top/haha.obj', 132 | // called when resource is loaded 133 | (object) => { 134 | object.children.forEach(obj => { 135 | if (obj.material) 136 | obj.material.color = new THREE.Color("#000"); 137 | }); 138 | this.scene.add(object); 139 | }, 140 | // called when loading is in progresses 141 | function (xhr) { 142 | 143 | console.log((xhr.loaded / xhr.total * 100) + '% loaded'); 144 | 145 | }, 146 | // called when loading has errors 147 | function (error) { 148 | 149 | console.log('An error happened'); 150 | 151 | } 152 | ); 153 | } 154 | loaderObj(url) { 155 | console.log(url); 156 | let objLoader = this.objLoader; 157 | const THREE = this.THREE; 158 | 159 | objLoader.load( 160 | // resource URL 161 | url, 162 | // called when resource is loaded 163 | (object) => { 164 | var texture = this.textureLoader.load(uv); 165 | texture.wrapS=THREE.RepeatWrapping; 166 | texture.wrapT=THREE.RepeatWrapping; 167 | texture.repeat.set(1,1); 168 | let hasMesh = false; 169 | object.traverse(function (child) { 170 | if (child.isMesh) { 171 | child.material.map = texture; 172 | hasMesh = true; 173 | } 174 | 175 | if (!hasMesh && child.isLineSegments) 176 | child.material.color = new THREE.Color("#000"); 177 | }); 178 | this.scene.add(object); 179 | }, 180 | // called when loading is in progresses 181 | function (xhr) { 182 | 183 | console.log((xhr.loaded / xhr.total * 100) + '% loaded'); 184 | 185 | }, 186 | // called when loading has errors 187 | function (error) { 188 | 189 | console.log('An error happened'); 190 | 191 | } 192 | ); 193 | } 194 | loadObjAndMtl(mtlUrl){ 195 | let objLoader = this.objLoader; 196 | let url=mtlUrl.replace(".mtl",".obj"); 197 | let mtlLoader=this.mtlLoader; 198 | let self=this; 199 | mtlLoader 200 | .load( mtlUrl, function ( materials ) { 201 | materials.preload(); 202 | objLoader 203 | .setMaterials( materials ) 204 | .load( url, ( object )=> { 205 | self.scene.add( object ); 206 | }); 207 | } ); 208 | } 209 | changeObjectsMaterial(url){ 210 | var texture = this.textureLoader.load(url); 211 | texture.wrapS=this.THREE.RepeatWrapping; 212 | texture.wrapT=this.THREE.RepeatWrapping; 213 | texture.repeat.set(1,1); 214 | 215 | this.selectObjects.forEach(o=>{ 216 | o.material.map=texture; 217 | }); 218 | 219 | } 220 | cancelSelect(){ 221 | this.selectObjects.forEach(o=>{ 222 | o.material.emissive = new this.THREE.Color(0x000000); 223 | }); 224 | this.selectObjects.clear(); 225 | } 226 | clear(obj) { 227 | if (!obj) 228 | obj = this.scene; 229 | 230 | for (let o of obj.children) { 231 | if (o.geometry) 232 | o.geometry.dispose(); 233 | this.clear(o); 234 | o.parent = null; 235 | o.dispatchEvent({ type: "removed" }); 236 | } 237 | obj.children.length = 0; 238 | } 239 | render() { 240 | this.renderer.render(this.scene, this.camera.intance); 241 | } 242 | animate() { 243 | this.canvas.requestAnimationFrame(this.animate.bind(this)); 244 | this.controls.update(); 245 | this.render(); 246 | } 247 | } 248 | 249 | export default Viewer; -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true, 12 | "coverView": true, 13 | "nodeModules": true, 14 | "autoAudits": false, 15 | "showShadowRootInWxmlPanel": true, 16 | "scopeDataCheck": false, 17 | "checkInvalidKey": true, 18 | "checkSiteMap": true, 19 | "uploadWithSourceMap": true, 20 | "babelSetting": { 21 | "ignore": [], 22 | "disablePlugins": [], 23 | "outputPath": "" 24 | } 25 | }, 26 | "compileType": "miniprogram", 27 | "libVersion": "2.10.3", 28 | "appid": "wx4dd9d1cf3942532d", 29 | "projectname": "lookcad", 30 | "debugOptions": { 31 | "hidedInDevtools": [] 32 | }, 33 | "isGameTourist": false, 34 | "simulatorType": "wechat", 35 | "simulatorPluginLibVersion": {}, 36 | "condition": { 37 | "search": { 38 | "current": -1, 39 | "list": [] 40 | }, 41 | "conversation": { 42 | "current": -1, 43 | "list": [] 44 | }, 45 | "game": { 46 | "currentL": -1, 47 | "list": [] 48 | }, 49 | "miniprogram": { 50 | "current": -1, 51 | "list": [] 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /utils/loadingManager.js: -------------------------------------------------------------------------------- 1 | export function LoadingManager( onLoad, onProgress, onError ) { 2 | 3 | var scope = this; 4 | 5 | var isLoading = false; 6 | var itemsLoaded = 0; 7 | var itemsTotal = 0; 8 | var urlModifier = undefined; 9 | var handlers = []; 10 | 11 | // Refer to #5689 for the reason why we don't set .onStart 12 | // in the constructor 13 | 14 | this.onStart = undefined; 15 | this.onLoad = onLoad; 16 | this.onProgress = onProgress; 17 | this.onError = onError; 18 | 19 | this.itemStart = function ( url ) { 20 | 21 | itemsTotal ++; 22 | 23 | if ( isLoading === false ) { 24 | 25 | if ( scope.onStart !== undefined ) { 26 | 27 | scope.onStart( url, itemsLoaded, itemsTotal ); 28 | 29 | } 30 | 31 | } 32 | 33 | isLoading = true; 34 | 35 | }; 36 | 37 | this.itemEnd = function ( url ) { 38 | 39 | itemsLoaded ++; 40 | 41 | if ( scope.onProgress !== undefined ) { 42 | 43 | scope.onProgress( url, itemsLoaded, itemsTotal ); 44 | 45 | } 46 | 47 | if ( itemsLoaded === itemsTotal ) { 48 | 49 | isLoading = false; 50 | 51 | if ( scope.onLoad !== undefined ) { 52 | 53 | scope.onLoad(); 54 | 55 | } 56 | 57 | } 58 | 59 | }; 60 | 61 | this.itemError = function ( url ) { 62 | 63 | if ( scope.onError !== undefined ) { 64 | 65 | scope.onError( url ); 66 | 67 | } 68 | 69 | }; 70 | 71 | this.resolveURL = function ( url ) { 72 | 73 | if ( urlModifier ) { 74 | 75 | return urlModifier( url ); 76 | 77 | } 78 | 79 | return url; 80 | 81 | }; 82 | 83 | this.setURLModifier = function ( transform ) { 84 | 85 | urlModifier = transform; 86 | 87 | return this; 88 | 89 | }; 90 | 91 | this.addHandler = function ( regex, loader ) { 92 | 93 | handlers.push( regex, loader ); 94 | 95 | return this; 96 | 97 | }; 98 | 99 | this.removeHandler = function ( regex ) { 100 | 101 | var index = handlers.indexOf( regex ); 102 | 103 | if ( index !== - 1 ) { 104 | 105 | handlers.splice( index, 2 ); 106 | 107 | } 108 | 109 | return this; 110 | 111 | }; 112 | 113 | this.getHandler = function ( file ) { 114 | 115 | for ( var i = 0, l = handlers.length; i < l; i += 2 ) { 116 | 117 | var regex = handlers[ i ]; 118 | var loader = handlers[ i + 1 ]; 119 | 120 | if ( regex.global ) regex.lastIndex = 0; // see #17920 121 | 122 | if ( regex.test( file ) ) { 123 | 124 | return loader; 125 | 126 | } 127 | 128 | } 129 | 130 | return null; 131 | 132 | }; 133 | 134 | } -------------------------------------------------------------------------------- /utils/mtlLoader.js: -------------------------------------------------------------------------------- 1 | export function getMtlLoader(THREE) { 2 | /** 3 | * Loads a Wavefront .mtl file specifying materials 4 | * 5 | * @author angelxuanchang 6 | */ 7 | function MTLLoader(manager) { 8 | 9 | THREE.Loader.call(this, manager); 10 | 11 | }; 12 | 13 | MTLLoader.prototype = Object.assign(Object.create(THREE.Loader.prototype), { 14 | 15 | constructor: MTLLoader, 16 | 17 | /** 18 | * Loads and parses a MTL asset from a URL. 19 | * 20 | * @param {String} url - URL to the MTL file. 21 | * @param {Function} [onLoad] - Callback invoked with the loaded object. 22 | * @param {Function} [onProgress] - Callback for download progress. 23 | * @param {Function} [onError] - Callback for download errors. 24 | * 25 | * @see setPath setResourcePath 26 | * 27 | * @note In order for relative texture references to resolve correctly 28 | * you must call setResourcePath() explicitly prior to load. 29 | */ 30 | load: function (url, onLoad, onProgress, onError) { 31 | 32 | var scope = this; 33 | 34 | var path = (this.path === '') ? THREE.LoaderUtils.extractUrlBase(url) : this.path; 35 | 36 | var loader = new THREE.FileLoader(this.manager); 37 | loader.setPath(this.path); 38 | loader.load(url, function (text) { 39 | 40 | onLoad(scope.parse(text, path)); 41 | 42 | }, onProgress, onError); 43 | 44 | }, 45 | 46 | setMaterialOptions: function (value) { 47 | 48 | this.materialOptions = value; 49 | return this; 50 | 51 | }, 52 | 53 | /** 54 | * Parses a MTL file. 55 | * 56 | * @param {String} text - Content of MTL file 57 | * @return {THREE.MTLLoader.MaterialCreator} 58 | * 59 | * @see setPath setResourcePath 60 | * 61 | * @note In order for relative texture references to resolve correctly 62 | * you must call setResourcePath() explicitly prior to parse. 63 | */ 64 | parse: function (text, path) { 65 | 66 | var lines = text.split('\n'); 67 | var info = {}; 68 | var delimiter_pattern = /\s+/; 69 | var materialsInfo = {}; 70 | 71 | for (var i = 0; i < lines.length; i++) { 72 | 73 | var line = lines[i]; 74 | line = line.trim(); 75 | 76 | if (line.length === 0 || line.charAt(0) === '#') { 77 | 78 | // Blank line or comment ignore 79 | continue; 80 | 81 | } 82 | 83 | var pos = line.indexOf(' '); 84 | 85 | var key = (pos >= 0) ? line.substring(0, pos) : line; 86 | key = key.toLowerCase(); 87 | 88 | var value = (pos >= 0) ? line.substring(pos + 1) : ''; 89 | value = value.trim(); 90 | 91 | if (key === 'newmtl') { 92 | 93 | // New material 94 | 95 | info = { name: value }; 96 | materialsInfo[value] = info; 97 | 98 | } else { 99 | 100 | if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') { 101 | 102 | var ss = value.split(delimiter_pattern, 3); 103 | info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]; 104 | 105 | } else { 106 | 107 | info[key] = value; 108 | 109 | } 110 | 111 | } 112 | 113 | } 114 | 115 | var materialCreator = new MTLLoader.MaterialCreator(this.resourcePath || path, this.materialOptions); 116 | materialCreator.setCrossOrigin(this.crossOrigin); 117 | materialCreator.setManager(this.manager); 118 | materialCreator.setMaterials(materialsInfo); 119 | return materialCreator; 120 | 121 | } 122 | 123 | }); 124 | 125 | /** 126 | * Create a new THREE.MTLLoader.MaterialCreator 127 | * @param baseUrl - Url relative to which textures are loaded 128 | * @param options - Set of options on how to construct the materials 129 | * side: Which side to apply the material 130 | * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide 131 | * wrap: What type of wrapping to apply for textures 132 | * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping 133 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 134 | * Default: false, assumed to be already normalized 135 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's 136 | * Default: false 137 | * @constructor 138 | */ 139 | 140 | MTLLoader.MaterialCreator = function (baseUrl, options) { 141 | 142 | this.baseUrl = baseUrl || ''; 143 | this.options = options; 144 | this.materialsInfo = {}; 145 | this.materials = {}; 146 | this.materialsArray = []; 147 | this.nameLookup = {}; 148 | 149 | this.side = (this.options && this.options.side) ? this.options.side : THREE.FrontSide; 150 | this.wrap = (this.options && this.options.wrap) ? this.options.wrap : THREE.RepeatWrapping; 151 | 152 | }; 153 | 154 | MTLLoader.MaterialCreator.prototype = { 155 | 156 | constructor: MTLLoader.MaterialCreator, 157 | 158 | crossOrigin: 'anonymous', 159 | 160 | setCrossOrigin: function (value) { 161 | 162 | this.crossOrigin = value; 163 | return this; 164 | 165 | }, 166 | 167 | setManager: function (value) { 168 | 169 | this.manager = value; 170 | 171 | }, 172 | 173 | setMaterials: function (materialsInfo) { 174 | 175 | this.materialsInfo = this.convert(materialsInfo); 176 | this.materials = {}; 177 | this.materialsArray = []; 178 | this.nameLookup = {}; 179 | 180 | }, 181 | 182 | convert: function (materialsInfo) { 183 | 184 | if (!this.options) return materialsInfo; 185 | 186 | var converted = {}; 187 | 188 | for (var mn in materialsInfo) { 189 | 190 | // Convert materials info into normalized form based on options 191 | 192 | var mat = materialsInfo[mn]; 193 | 194 | var covmat = {}; 195 | 196 | converted[mn] = covmat; 197 | 198 | for (var prop in mat) { 199 | 200 | var save = true; 201 | var value = mat[prop]; 202 | var lprop = prop.toLowerCase(); 203 | 204 | switch (lprop) { 205 | 206 | case 'kd': 207 | case 'ka': 208 | case 'ks': 209 | 210 | // Diffuse color (color under white light) using RGB values 211 | 212 | if (this.options && this.options.normalizeRGB) { 213 | 214 | value = [value[0] / 255, value[1] / 255, value[2] / 255]; 215 | 216 | } 217 | 218 | if (this.options && this.options.ignoreZeroRGBs) { 219 | 220 | if (value[0] === 0 && value[1] === 0 && value[2] === 0) { 221 | 222 | // ignore 223 | 224 | save = false; 225 | 226 | } 227 | 228 | } 229 | 230 | break; 231 | 232 | default: 233 | 234 | break; 235 | 236 | } 237 | 238 | if (save) { 239 | 240 | covmat[lprop] = value; 241 | 242 | } 243 | 244 | } 245 | 246 | } 247 | 248 | return converted; 249 | 250 | }, 251 | 252 | preload: function () { 253 | 254 | for (var mn in this.materialsInfo) { 255 | 256 | this.create(mn); 257 | 258 | } 259 | 260 | }, 261 | 262 | getIndex: function (materialName) { 263 | 264 | return this.nameLookup[materialName]; 265 | 266 | }, 267 | 268 | getAsArray: function () { 269 | 270 | var index = 0; 271 | 272 | for (var mn in this.materialsInfo) { 273 | 274 | this.materialsArray[index] = this.create(mn); 275 | this.nameLookup[mn] = index; 276 | index++; 277 | 278 | } 279 | 280 | return this.materialsArray; 281 | 282 | }, 283 | 284 | create: function (materialName) { 285 | 286 | if (this.materials[materialName] === undefined) { 287 | 288 | this.createMaterial_(materialName); 289 | 290 | } 291 | 292 | return this.materials[materialName]; 293 | 294 | }, 295 | 296 | createMaterial_: function (materialName) { 297 | 298 | // Create material 299 | 300 | var scope = this; 301 | var mat = this.materialsInfo[materialName]; 302 | var params = { 303 | 304 | name: materialName, 305 | side: this.side 306 | 307 | }; 308 | 309 | function resolveURL(baseUrl, url) { 310 | 311 | if (typeof url !== 'string' || url === '') 312 | return ''; 313 | 314 | // Absolute URL 315 | if (/^https?:\/\//i.test(url)) return url; 316 | 317 | return baseUrl + url; 318 | 319 | } 320 | 321 | function setMapForType(mapType, value) { 322 | 323 | if (params[mapType]) return; // Keep the first encountered texture 324 | 325 | var texParams = scope.getTextureParams(value, params); 326 | var map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url)); 327 | 328 | map.repeat.copy(texParams.scale); 329 | map.offset.copy(texParams.offset); 330 | 331 | map.wrapS = scope.wrap; 332 | map.wrapT = scope.wrap; 333 | 334 | params[mapType] = map; 335 | 336 | } 337 | 338 | for (var prop in mat) { 339 | 340 | var value = mat[prop]; 341 | var n; 342 | 343 | if (value === '') continue; 344 | 345 | switch (prop.toLowerCase()) { 346 | 347 | // Ns is material specular exponent 348 | 349 | case 'kd': 350 | 351 | // Diffuse color (color under white light) using RGB values 352 | 353 | params.color = new THREE.Color().fromArray(value); 354 | 355 | break; 356 | 357 | case 'ks': 358 | 359 | // Specular color (color when light is reflected from shiny surface) using RGB values 360 | params.specular = new THREE.Color().fromArray(value); 361 | 362 | break; 363 | 364 | case 'ke': 365 | 366 | // Emissive using RGB values 367 | params.emissive = new THREE.Color().fromArray(value); 368 | 369 | break; 370 | 371 | case 'map_kd': 372 | 373 | // Diffuse texture map 374 | 375 | setMapForType("map", value); 376 | 377 | break; 378 | 379 | case 'map_ks': 380 | 381 | // Specular map 382 | 383 | setMapForType("specularMap", value); 384 | 385 | break; 386 | 387 | case 'map_ke': 388 | 389 | // Emissive map 390 | 391 | setMapForType("emissiveMap", value); 392 | 393 | break; 394 | 395 | case 'norm': 396 | 397 | setMapForType("normalMap", value); 398 | 399 | break; 400 | 401 | case 'map_bump': 402 | case 'bump': 403 | 404 | // Bump texture map 405 | 406 | setMapForType("bumpMap", value); 407 | 408 | break; 409 | 410 | case 'map_d': 411 | 412 | // Alpha map 413 | 414 | setMapForType("alphaMap", value); 415 | params.transparent = true; 416 | 417 | break; 418 | 419 | case 'ns': 420 | 421 | // The specular exponent (defines the focus of the specular highlight) 422 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. 423 | 424 | params.shininess = parseFloat(value); 425 | 426 | break; 427 | 428 | case 'd': 429 | n = parseFloat(value); 430 | 431 | if (n < 1) { 432 | 433 | params.opacity = n; 434 | params.transparent = true; 435 | 436 | } 437 | 438 | break; 439 | 440 | case 'tr': 441 | n = parseFloat(value); 442 | 443 | if (this.options && this.options.invertTrProperty) n = 1 - n; 444 | 445 | if (n > 0) { 446 | 447 | params.opacity = 1 - n; 448 | params.transparent = true; 449 | 450 | } 451 | 452 | break; 453 | 454 | default: 455 | break; 456 | 457 | } 458 | 459 | } 460 | 461 | this.materials[materialName] = new THREE.MeshPhongMaterial(params); 462 | return this.materials[materialName]; 463 | 464 | }, 465 | 466 | getTextureParams: function (value, matParams) { 467 | 468 | var texParams = { 469 | 470 | scale: new THREE.Vector2(1, 1), 471 | offset: new THREE.Vector2(0, 0) 472 | 473 | }; 474 | 475 | var items = value.split(/\s+/); 476 | var pos; 477 | 478 | pos = items.indexOf('-bm'); 479 | 480 | if (pos >= 0) { 481 | 482 | matParams.bumpScale = parseFloat(items[pos + 1]); 483 | items.splice(pos, 2); 484 | 485 | } 486 | 487 | pos = items.indexOf('-s'); 488 | 489 | if (pos >= 0) { 490 | 491 | texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); 492 | items.splice(pos, 4); // we expect 3 parameters here! 493 | 494 | } 495 | 496 | pos = items.indexOf('-o'); 497 | 498 | if (pos >= 0) { 499 | 500 | texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); 501 | items.splice(pos, 4); // we expect 3 parameters here! 502 | 503 | } 504 | 505 | texParams.url = items.join(' ').trim(); 506 | return texParams; 507 | 508 | }, 509 | 510 | loadTexture: function (url, mapping, onLoad, onProgress, onError) { 511 | 512 | var texture; 513 | var manager = (this.manager !== undefined) ? this.manager : THREE.DefaultLoadingManager; 514 | var loader = manager.getHandler(url); 515 | 516 | if (loader === null) { 517 | 518 | loader = new THREE.TextureLoader(manager); 519 | 520 | } 521 | 522 | if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin); 523 | texture = loader.load(url, onLoad, onProgress, onError); 524 | 525 | if (mapping !== undefined) texture.mapping = mapping; 526 | 527 | return texture; 528 | 529 | } 530 | 531 | }; 532 | return MTLLoader; 533 | 534 | } -------------------------------------------------------------------------------- /utils/objLoader.js: -------------------------------------------------------------------------------- 1 | export function getLoader(THREE) { 2 | const { 3 | BufferGeometry, 4 | FileLoader, 5 | Float32BufferAttribute, 6 | Group, 7 | LineBasicMaterial, 8 | LineSegments, 9 | Loader, 10 | Material, 11 | Mesh, 12 | MeshPhongMaterial, 13 | Points, 14 | PointsMaterial 15 | } = THREE; 16 | 17 | BufferGeometry.prototype.setAttribute = function (name, attribute) { 18 | this.attributes[name] = attribute; 19 | return this; 20 | } 21 | BufferGeometry.prototype.getAttribute = function (namee) { 22 | return this.attributes[name]; 23 | } 24 | 25 | // o object_name | g group_name 26 | var object_pattern = /^[og]\s*(.+)?/; 27 | // mtllib file_reference 28 | var material_library_pattern = /^mtllib /; 29 | // usemtl material_name 30 | var material_use_pattern = /^usemtl /; 31 | // usemap map_name 32 | var map_use_pattern = /^usemap /; 33 | 34 | function ParserState() { 35 | 36 | var state = { 37 | objects: [], 38 | object: {}, 39 | 40 | vertices: [], 41 | normals: [], 42 | colors: [], 43 | uvs: [], 44 | 45 | materials: {}, 46 | materialLibraries: [], 47 | 48 | startObject: function (name, fromDeclaration) { 49 | 50 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 51 | // file. We need to use it for the first parsed g/o to keep things in sync. 52 | if (this.object && this.object.fromDeclaration === false) { 53 | 54 | this.object.name = name; 55 | this.object.fromDeclaration = (fromDeclaration !== false); 56 | return; 57 | 58 | } 59 | 60 | var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined); 61 | 62 | if (this.object && typeof this.object._finalize === 'function') { 63 | 64 | this.object._finalize(true); 65 | 66 | } 67 | 68 | this.object = { 69 | name: name || '', 70 | fromDeclaration: (fromDeclaration !== false), 71 | 72 | geometry: { 73 | vertices: [], 74 | normals: [], 75 | colors: [], 76 | uvs: [] 77 | }, 78 | materials: [], 79 | smooth: true, 80 | 81 | startMaterial: function (name, libraries) { 82 | 83 | var previous = this._finalize(false); 84 | 85 | // New usemtl declaration overwrites an inherited material, except if faces were declared 86 | // after the material, then it must be preserved for proper MultiMaterial continuation. 87 | if (previous && (previous.inherited || previous.groupCount <= 0)) { 88 | 89 | this.materials.splice(previous.index, 1); 90 | 91 | } 92 | 93 | var material = { 94 | index: this.materials.length, 95 | name: name || '', 96 | mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''), 97 | smooth: (previous !== undefined ? previous.smooth : this.smooth), 98 | groupStart: (previous !== undefined ? previous.groupEnd : 0), 99 | groupEnd: - 1, 100 | groupCount: - 1, 101 | inherited: false, 102 | 103 | clone: function (index) { 104 | 105 | var cloned = { 106 | index: (typeof index === 'number' ? index : this.index), 107 | name: this.name, 108 | mtllib: this.mtllib, 109 | smooth: this.smooth, 110 | groupStart: 0, 111 | groupEnd: - 1, 112 | groupCount: - 1, 113 | inherited: false 114 | }; 115 | cloned.clone = this.clone.bind(cloned); 116 | return cloned; 117 | 118 | } 119 | }; 120 | 121 | this.materials.push(material); 122 | 123 | return material; 124 | 125 | }, 126 | 127 | currentMaterial: function () { 128 | 129 | if (this.materials.length > 0) { 130 | 131 | return this.materials[this.materials.length - 1]; 132 | 133 | } 134 | 135 | return undefined; 136 | 137 | }, 138 | 139 | _finalize: function (end) { 140 | 141 | var lastMultiMaterial = this.currentMaterial(); 142 | if (lastMultiMaterial && lastMultiMaterial.groupEnd === - 1) { 143 | 144 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 145 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 146 | lastMultiMaterial.inherited = false; 147 | 148 | } 149 | 150 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 151 | if (end && this.materials.length > 1) { 152 | 153 | for (var mi = this.materials.length - 1; mi >= 0; mi--) { 154 | 155 | if (this.materials[mi].groupCount <= 0) { 156 | 157 | this.materials.splice(mi, 1); 158 | 159 | } 160 | 161 | } 162 | 163 | } 164 | 165 | // Guarantee at least one empty material, this makes the creation later more straight forward. 166 | if (end && this.materials.length === 0) { 167 | 168 | this.materials.push({ 169 | name: '', 170 | smooth: this.smooth 171 | }); 172 | 173 | } 174 | 175 | return lastMultiMaterial; 176 | 177 | } 178 | }; 179 | 180 | // Inherit previous objects material. 181 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 182 | // If a usemtl declaration is encountered while this new object is being parsed, it will 183 | // overwrite the inherited material. Exception being that there was already face declarations 184 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 185 | 186 | if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') { 187 | 188 | var declared = previousMaterial.clone(0); 189 | declared.inherited = true; 190 | this.object.materials.push(declared); 191 | 192 | } 193 | 194 | this.objects.push(this.object); 195 | 196 | }, 197 | 198 | finalize: function () { 199 | 200 | if (this.object && typeof this.object._finalize === 'function') { 201 | 202 | this.object._finalize(true); 203 | 204 | } 205 | 206 | }, 207 | 208 | parseVertexIndex: function (value, len) { 209 | 210 | var index = parseInt(value, 10); 211 | return (index >= 0 ? index - 1 : index + len / 3) * 3; 212 | 213 | }, 214 | 215 | parseNormalIndex: function (value, len) { 216 | 217 | var index = parseInt(value, 10); 218 | return (index >= 0 ? index - 1 : index + len / 3) * 3; 219 | 220 | }, 221 | 222 | parseUVIndex: function (value, len) { 223 | 224 | var index = parseInt(value, 10); 225 | return (index >= 0 ? index - 1 : index + len / 2) * 2; 226 | 227 | }, 228 | 229 | addVertex: function (a, b, c) { 230 | 231 | var src = this.vertices; 232 | var dst = this.object.geometry.vertices; 233 | 234 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 235 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 236 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 237 | 238 | }, 239 | 240 | addVertexPoint: function (a) { 241 | 242 | var src = this.vertices; 243 | var dst = this.object.geometry.vertices; 244 | 245 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 246 | 247 | }, 248 | 249 | addVertexLine: function (a) { 250 | 251 | var src = this.vertices; 252 | var dst = this.object.geometry.vertices; 253 | 254 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 255 | 256 | }, 257 | 258 | addNormal: function (a, b, c) { 259 | 260 | var src = this.normals; 261 | var dst = this.object.geometry.normals; 262 | 263 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 264 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 265 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 266 | 267 | }, 268 | 269 | addColor: function (a, b, c) { 270 | 271 | var src = this.colors; 272 | var dst = this.object.geometry.colors; 273 | 274 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 275 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 276 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 277 | 278 | }, 279 | 280 | addUV: function (a, b, c) { 281 | 282 | var src = this.uvs; 283 | var dst = this.object.geometry.uvs; 284 | 285 | dst.push(src[a + 0], src[a + 1]); 286 | dst.push(src[b + 0], src[b + 1]); 287 | dst.push(src[c + 0], src[c + 1]); 288 | 289 | }, 290 | 291 | addUVLine: function (a) { 292 | 293 | var src = this.uvs; 294 | var dst = this.object.geometry.uvs; 295 | 296 | dst.push(src[a + 0], src[a + 1]); 297 | 298 | }, 299 | 300 | addFace: function (a, b, c, ua, ub, uc, na, nb, nc) { 301 | 302 | var vLen = this.vertices.length; 303 | 304 | var ia = this.parseVertexIndex(a, vLen); 305 | var ib = this.parseVertexIndex(b, vLen); 306 | var ic = this.parseVertexIndex(c, vLen); 307 | 308 | this.addVertex(ia, ib, ic); 309 | 310 | if (this.colors.length > 0) { 311 | 312 | this.addColor(ia, ib, ic); 313 | 314 | } 315 | 316 | if (ua !== undefined && ua !== '') { 317 | 318 | var uvLen = this.uvs.length; 319 | ia = this.parseUVIndex(ua, uvLen); 320 | ib = this.parseUVIndex(ub, uvLen); 321 | ic = this.parseUVIndex(uc, uvLen); 322 | this.addUV(ia, ib, ic); 323 | 324 | } 325 | 326 | if (na !== undefined && na !== '') { 327 | 328 | // Normals are many times the same. If so, skip function call and parseInt. 329 | var nLen = this.normals.length; 330 | ia = this.parseNormalIndex(na, nLen); 331 | 332 | ib = na === nb ? ia : this.parseNormalIndex(nb, nLen); 333 | ic = na === nc ? ia : this.parseNormalIndex(nc, nLen); 334 | 335 | this.addNormal(ia, ib, ic); 336 | 337 | } 338 | 339 | }, 340 | 341 | addPointGeometry: function (vertices) { 342 | 343 | this.object.geometry.type = 'Points'; 344 | 345 | var vLen = this.vertices.length; 346 | 347 | for (var vi = 0, l = vertices.length; vi < l; vi++) { 348 | 349 | this.addVertexPoint(this.parseVertexIndex(vertices[vi], vLen)); 350 | 351 | } 352 | 353 | }, 354 | 355 | addLineGeometry: function (vertices, uvs) { 356 | 357 | this.object.geometry.type = 'Line'; 358 | 359 | var vLen = this.vertices.length; 360 | var uvLen = this.uvs.length; 361 | 362 | for (var vi = 0, l = vertices.length; vi < l; vi++) { 363 | 364 | this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen)); 365 | 366 | } 367 | 368 | for (var uvi = 0, l = uvs.length; uvi < l; uvi++) { 369 | 370 | this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen)); 371 | 372 | } 373 | 374 | } 375 | 376 | }; 377 | 378 | state.startObject('', false); 379 | 380 | return state; 381 | 382 | } 383 | 384 | // 385 | 386 | function OBJLoader(manager) { 387 | 388 | Loader.call(this, manager); 389 | 390 | this.materials = null; 391 | 392 | } 393 | 394 | OBJLoader.prototype = Object.assign(Object.create(Loader.prototype), { 395 | 396 | constructor: OBJLoader, 397 | 398 | load: function (url, onLoad, onProgress, onError) { 399 | 400 | var scope = this; 401 | 402 | var loader = new FileLoader(scope.manager); 403 | loader.setPath(this.path); 404 | loader.load(url, function (text) { 405 | 406 | onLoad(scope.parse(text)); 407 | 408 | }, onProgress, onError); 409 | 410 | }, 411 | 412 | setMaterials: function (materials) { 413 | 414 | this.materials = materials; 415 | 416 | return this; 417 | 418 | }, 419 | 420 | parse: function (text) { 421 | 422 | var state = new ParserState(); 423 | 424 | if (text.indexOf('\r\n') !== - 1) { 425 | 426 | // This is faster than String.split with regex that splits on both 427 | text = text.replace(/\r\n/g, '\n'); 428 | 429 | } 430 | 431 | if (text.indexOf('\\\n') !== - 1) { 432 | 433 | // join lines separated by a line continuation character (\) 434 | text = text.replace(/\\\n/g, ''); 435 | 436 | } 437 | 438 | var lines = text.split('\n'); 439 | var line = '', lineFirstChar = ''; 440 | var lineLength = 0; 441 | var result = []; 442 | 443 | // Faster to just trim left side of the line. Use if available. 444 | var trimLeft = (typeof ''.trimLeft === 'function'); 445 | 446 | for (var i = 0, l = lines.length; i < l; i++) { 447 | 448 | line = lines[i]; 449 | 450 | line = trimLeft ? line.trimLeft() : line.trim(); 451 | 452 | lineLength = line.length; 453 | 454 | if (lineLength === 0) continue; 455 | 456 | lineFirstChar = line.charAt(0); 457 | 458 | // @todo invoke passed in handler if any 459 | if (lineFirstChar === '#') continue; 460 | 461 | if (lineFirstChar === 'v') { 462 | 463 | var data = line.split(/\s+/); 464 | 465 | switch (data[0]) { 466 | 467 | case 'v': 468 | state.vertices.push( 469 | parseFloat(data[1]), 470 | parseFloat(data[2]), 471 | parseFloat(data[3]) 472 | ); 473 | if (data.length >= 7) { 474 | 475 | state.colors.push( 476 | parseFloat(data[4]), 477 | parseFloat(data[5]), 478 | parseFloat(data[6]) 479 | 480 | ); 481 | 482 | } 483 | break; 484 | case 'vn': 485 | state.normals.push( 486 | parseFloat(data[1]), 487 | parseFloat(data[2]), 488 | parseFloat(data[3]) 489 | ); 490 | break; 491 | case 'vt': 492 | state.uvs.push( 493 | parseFloat(data[1]), 494 | parseFloat(data[2]) 495 | ); 496 | break; 497 | 498 | } 499 | 500 | } else if (lineFirstChar === 'f') { 501 | 502 | var lineData = line.substr(1).trim(); 503 | var vertexData = lineData.split(/\s+/); 504 | var faceVertices = []; 505 | 506 | // Parse the face vertex data into an easy to work with format 507 | 508 | for (var j = 0, jl = vertexData.length; j < jl; j++) { 509 | 510 | var vertex = vertexData[j]; 511 | 512 | if (vertex.length > 0) { 513 | 514 | var vertexParts = vertex.split('/'); 515 | faceVertices.push(vertexParts); 516 | 517 | } 518 | 519 | } 520 | 521 | // Draw an edge between the first vertex and all subsequent vertices to form an n-gon 522 | 523 | var v1 = faceVertices[0]; 524 | 525 | for (var j = 1, jl = faceVertices.length - 1; j < jl; j++) { 526 | 527 | var v2 = faceVertices[j]; 528 | var v3 = faceVertices[j + 1]; 529 | 530 | state.addFace( 531 | v1[0], v2[0], v3[0], 532 | v1[1], v2[1], v3[1], 533 | v1[2], v2[2], v3[2] 534 | ); 535 | 536 | } 537 | 538 | } else if (lineFirstChar === 'l') { 539 | 540 | var lineParts = line.substring(1).trim().split(" "); 541 | var lineVertices = [], lineUVs = []; 542 | 543 | if (line.indexOf("/") === - 1) { 544 | 545 | lineVertices = lineParts; 546 | 547 | } else { 548 | 549 | for (var li = 0, llen = lineParts.length; li < llen; li++) { 550 | 551 | var parts = lineParts[li].split("/"); 552 | 553 | if (parts[0] !== "") lineVertices.push(parts[0]); 554 | if (parts[1] !== "") lineUVs.push(parts[1]); 555 | 556 | } 557 | 558 | } 559 | state.addLineGeometry(lineVertices, lineUVs); 560 | 561 | } else if (lineFirstChar === 'p') { 562 | 563 | var lineData = line.substr(1).trim(); 564 | var pointData = lineData.split(" "); 565 | 566 | state.addPointGeometry(pointData); 567 | 568 | } else if ((result = object_pattern.exec(line)) !== null) { 569 | 570 | // o object_name 571 | // or 572 | // g group_name 573 | 574 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 575 | // var name = result[ 0 ].substr( 1 ).trim(); 576 | var name = (" " + result[0].substr(1).trim()).substr(1); 577 | 578 | state.startObject(name); 579 | 580 | } else if (material_use_pattern.test(line)) { 581 | 582 | // material 583 | 584 | state.object.startMaterial(line.substring(7).trim(), state.materialLibraries); 585 | 586 | } else if (material_library_pattern.test(line)) { 587 | 588 | // mtl file 589 | 590 | state.materialLibraries.push(line.substring(7).trim()); 591 | 592 | } else if (map_use_pattern.test(line)) { 593 | 594 | // the line is parsed but ignored since the loader assumes textures are defined MTL files 595 | // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method) 596 | 597 | console.warn('THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.'); 598 | 599 | } else if (lineFirstChar === 's') { 600 | 601 | result = line.split(' '); 602 | 603 | // smooth shading 604 | 605 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 606 | // but does not define a usemtl for each face set. 607 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 608 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 609 | // where explicit usemtl defines geometry groups. 610 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 611 | 612 | /* 613 | * http://paulbourke.net/dataformats/obj/ 614 | * or 615 | * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf 616 | * 617 | * From chapter "Grouping" Syntax explanation "s group_number": 618 | * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. 619 | * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form 620 | * surfaces, smoothing groups are either turned on or off; there is no difference between values greater 621 | * than 0." 622 | */ 623 | if (result.length > 1) { 624 | 625 | var value = result[1].trim().toLowerCase(); 626 | state.object.smooth = (value !== '0' && value !== 'off'); 627 | 628 | } else { 629 | 630 | // ZBrush can produce "s" lines #11707 631 | state.object.smooth = true; 632 | 633 | } 634 | var material = state.object.currentMaterial(); 635 | if (material) material.smooth = state.object.smooth; 636 | 637 | } else { 638 | 639 | // Handle null terminated files without exception 640 | if (line === '\0') continue; 641 | 642 | console.warn('THREE.OBJLoader: Unexpected line: "' + line + '"'); 643 | 644 | } 645 | 646 | } 647 | state.finalize(); 648 | 649 | var container = new Group(); 650 | container.materialLibraries = [].concat(state.materialLibraries); 651 | 652 | for (var i = 0, l = state.objects.length; i < l; i++) { 653 | 654 | var object = state.objects[i]; 655 | var geometry = object.geometry; 656 | var materials = object.materials; 657 | var isLine = (geometry.type === 'Line'); 658 | var isPoints = (geometry.type === 'Points'); 659 | var hasVertexColors = false; 660 | 661 | // Skip o/g line declarations that did not follow with any faces 662 | if (geometry.vertices.length === 0) continue; 663 | 664 | var buffergeometry = new BufferGeometry(); 665 | 666 | buffergeometry.setAttribute('position', new Float32BufferAttribute(geometry.vertices, 3)); 667 | 668 | if (geometry.normals.length > 0) { 669 | 670 | buffergeometry.setAttribute('normal', new Float32BufferAttribute(geometry.normals, 3)); 671 | 672 | } else { 673 | 674 | buffergeometry.computeVertexNormals(); 675 | 676 | } 677 | 678 | if (geometry.colors.length > 0) { 679 | 680 | hasVertexColors = true; 681 | buffergeometry.setAttribute('color', new Float32BufferAttribute(geometry.colors, 3)); 682 | 683 | } 684 | 685 | if (geometry.uvs.length > 0) { 686 | 687 | buffergeometry.setAttribute('uv', new Float32BufferAttribute(geometry.uvs, 2)); 688 | 689 | } 690 | 691 | // Create materials 692 | 693 | var createdMaterials = []; 694 | 695 | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { 696 | 697 | var sourceMaterial = materials[mi]; 698 | var materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors; 699 | var material = state.materials[materialHash]; 700 | 701 | if (this.materials !== null) { 702 | 703 | material = this.materials.create(sourceMaterial.name); 704 | 705 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 706 | if (isLine && material && !(material instanceof LineBasicMaterial)) { 707 | 708 | var materialLine = new LineBasicMaterial(); 709 | Material.prototype.copy.call(materialLine, material); 710 | materialLine.color.copy(material.color); 711 | material = materialLine; 712 | 713 | } else if (isPoints && material && !(material instanceof PointsMaterial)) { 714 | var materialPoints = new PointsMaterial({ size: 10, sizeAttenuation: false }); 715 | Material.prototype.copy.call(materialPoints, material); 716 | materialPoints.color.copy(material.color); 717 | materialPoints.map = material.map; 718 | material = materialPoints; 719 | 720 | } 721 | 722 | } 723 | 724 | if (material === undefined) { 725 | 726 | if (isLine) { 727 | 728 | material = new LineBasicMaterial(); 729 | 730 | } else if (isPoints) { 731 | 732 | material = new PointsMaterial({ size: 1, sizeAttenuation: false }); 733 | 734 | } else { 735 | 736 | material = new MeshPhongMaterial(); 737 | 738 | } 739 | 740 | material.name = sourceMaterial.name; 741 | material.flatShading = sourceMaterial.smooth ? false : true; 742 | material.vertexColors = hasVertexColors; 743 | 744 | state.materials[materialHash] = material; 745 | 746 | } 747 | 748 | createdMaterials.push(material); 749 | 750 | } 751 | 752 | // Create mesh 753 | var mesh; 754 | 755 | if (createdMaterials.length > 1) { 756 | 757 | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { 758 | 759 | var sourceMaterial = materials[mi]; 760 | buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi); 761 | 762 | } 763 | 764 | if (isLine) { 765 | 766 | mesh = new LineSegments(buffergeometry, createdMaterials); 767 | 768 | } else if (isPoints) { 769 | 770 | mesh = new Points(buffergeometry, createdMaterials); 771 | 772 | } else { 773 | 774 | mesh = new Mesh(buffergeometry, createdMaterials); 775 | 776 | } 777 | 778 | } else { 779 | 780 | if (isLine) { 781 | 782 | mesh = new LineSegments(buffergeometry, createdMaterials[0]); 783 | 784 | } else if (isPoints) { 785 | 786 | mesh = new Points(buffergeometry, createdMaterials[0]); 787 | 788 | } else { 789 | 790 | let mtl=createdMaterials[0].clone(); 791 | 792 | if(mtl.map){ 793 | mtl.map.wrapS=THREE.MirroredRepeatWrapping; 794 | mtl.map.wrapT=THREE.MirroredRepeatWrapping; 795 | } 796 | mesh = new Mesh(buffergeometry,mtl); 797 | 798 | } 799 | 800 | } 801 | 802 | mesh.name = object.name; 803 | 804 | container.add(mesh); 805 | 806 | } 807 | 808 | return container; 809 | 810 | } 811 | 812 | }); 813 | 814 | return OBJLoader; 815 | 816 | } -------------------------------------------------------------------------------- /utils/orbitControls.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function GetOrbitControls(object, domElement, THREE){ 4 | const { 5 | EventDispatcher, 6 | MOUSE, 7 | Quaternion, 8 | Spherical, 9 | TOUCH, 10 | Vector2, 11 | Vector3 12 | } = THREE; 13 | function OrbitControls(object, domElement) { 14 | if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.'); 15 | if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); 16 | 17 | this.object = object; 18 | this.domElement = domElement; 19 | 20 | // Set to false to disable this control 21 | this.enabled = true; 22 | 23 | // "target" sets the location of focus, where the object orbits around 24 | this.target = new Vector3(); 25 | 26 | // How far you can dolly in and out ( PerspectiveCamera only ) 27 | this.minDistance = 0; 28 | this.maxDistance = Infinity; 29 | 30 | // How far you can zoom in and out ( OrthographicCamera only ) 31 | this.minZoom = 0; 32 | this.maxZoom = Infinity; 33 | 34 | // How far you can orbit vertically, upper and lower limits. 35 | // Range is 0 to Math.PI radians. 36 | this.minPolarAngle = 0; // radians 37 | this.maxPolarAngle = Math.PI; // radians 38 | 39 | // How far you can orbit horizontally, upper and lower limits. 40 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 41 | this.minAzimuthAngle = - Infinity; // radians 42 | this.maxAzimuthAngle = Infinity; // radians 43 | 44 | // Set to true to enable damping (inertia) 45 | // If damping is enabled, you must call controls.update() in your animation loop 46 | this.enableDamping = false; 47 | this.dampingFactor = 0.05; 48 | 49 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 50 | // Set to false to disable zooming 51 | this.enableZoom = true; 52 | this.zoomSpeed = 1.0; 53 | 54 | // Set to false to disable rotating 55 | this.enableRotate = true; 56 | this.rotateSpeed = 1.0; 57 | 58 | // Set to false to disable panning 59 | this.enablePan = true; 60 | this.panSpeed = 1.0; 61 | this.screenSpacePanning = false; // if true, pan in screen-space 62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 63 | 64 | // Set to true to automatically rotate around the target 65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 66 | this.autoRotate = false; 67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 68 | 69 | // Set to false to disable use of the keys 70 | this.enableKeys = true; 71 | 72 | // The four arrow keys 73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 74 | 75 | // Mouse buttons 76 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 77 | 78 | // Touch fingers 79 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 80 | 81 | // for reset 82 | this.target0 = this.target.clone(); 83 | this.position0 = this.object.position.clone(); 84 | this.zoom0 = this.object.zoom; 85 | 86 | // 87 | // public methods 88 | // 89 | 90 | this.getPolarAngle = function () { 91 | 92 | return spherical.phi; 93 | 94 | }; 95 | 96 | this.getAzimuthalAngle = function () { 97 | 98 | return spherical.theta; 99 | 100 | }; 101 | 102 | this.saveState = function () { 103 | 104 | scope.target0.copy(scope.target); 105 | scope.position0.copy(scope.object.position); 106 | scope.zoom0 = scope.object.zoom; 107 | 108 | }; 109 | 110 | this.reset = function () { 111 | 112 | scope.target.copy(scope.target0); 113 | scope.object.position.copy(scope.position0); 114 | scope.object.zoom = scope.zoom0; 115 | 116 | scope.object.updateProjectionMatrix(); 117 | scope.dispatchEvent(changeEvent); 118 | 119 | scope.update(); 120 | 121 | state = STATE.NONE; 122 | 123 | }; 124 | 125 | // this method is exposed, but perhaps it would be better if we can make it private... 126 | this.update = function () { 127 | 128 | var offset = new Vector3(); 129 | 130 | // so camera.up is the orbit axis 131 | var quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); 132 | var quatInverse = quat.clone().inverse(); 133 | 134 | var lastPosition = new Vector3(); 135 | var lastQuaternion = new Quaternion(); 136 | 137 | return function update() { 138 | 139 | var position = scope.object.position; 140 | 141 | offset.copy(position).sub(scope.target); 142 | 143 | // rotate offset to "y-axis-is-up" space 144 | offset.applyQuaternion(quat); 145 | 146 | // angle from z-axis around y-axis 147 | spherical.setFromVector3(offset); 148 | 149 | if (scope.autoRotate && state === STATE.NONE) { 150 | 151 | rotateLeft(getAutoRotationAngle()); 152 | 153 | } 154 | 155 | if (scope.enableDamping) { 156 | 157 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 158 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 159 | 160 | } else { 161 | 162 | spherical.theta += sphericalDelta.theta; 163 | spherical.phi += sphericalDelta.phi; 164 | 165 | } 166 | 167 | // restrict theta to be between desired limits 168 | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); 169 | 170 | // restrict phi to be between desired limits 171 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); 172 | 173 | spherical.makeSafe(); 174 | 175 | 176 | spherical.radius *= scale; 177 | 178 | // restrict radius to be between desired limits 179 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); 180 | 181 | // move target to panned location 182 | 183 | if (scope.enableDamping === true) { 184 | 185 | scope.target.addScaledVector(panOffset, scope.dampingFactor); 186 | 187 | } else { 188 | 189 | scope.target.add(panOffset); 190 | 191 | } 192 | 193 | offset.setFromSpherical(spherical); 194 | 195 | // rotate offset back to "camera-up-vector-is-up" space 196 | offset.applyQuaternion(quatInverse); 197 | 198 | position.copy(scope.target).add(offset); 199 | 200 | scope.object.lookAt(scope.target); 201 | 202 | if (scope.enableDamping === true) { 203 | 204 | sphericalDelta.theta *= (1 - scope.dampingFactor); 205 | sphericalDelta.phi *= (1 - scope.dampingFactor); 206 | 207 | panOffset.multiplyScalar(1 - scope.dampingFactor); 208 | 209 | } else { 210 | 211 | sphericalDelta.set(0, 0, 0); 212 | 213 | panOffset.set(0, 0, 0); 214 | 215 | } 216 | 217 | scale = 1; 218 | 219 | // update condition is: 220 | // min(camera displacement, camera rotation in radians)^2 > EPS 221 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 222 | 223 | if (zoomChanged || 224 | lastPosition.distanceToSquared(scope.object.position) > EPS || 225 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { 226 | 227 | scope.dispatchEvent(changeEvent); 228 | 229 | lastPosition.copy(scope.object.position); 230 | lastQuaternion.copy(scope.object.quaternion); 231 | zoomChanged = false; 232 | 233 | return true; 234 | 235 | } 236 | 237 | return false; 238 | 239 | }; 240 | 241 | }(); 242 | 243 | this.dispose = function () { 244 | 245 | // scope.domElement.removeEventListener('contextmenu', onContextMenu, false); 246 | // scope.domElement.removeEventListener('mousedown', onMouseDown, false); 247 | // scope.domElement.removeEventListener('wheel', onMouseWheel, false); 248 | 249 | // scope.domElement.removeEventListener('touchstart', onTouchStart, false); 250 | // scope.domElement.removeEventListener('touchend', onTouchEnd, false); 251 | // scope.domElement.removeEventListener('touchmove', onTouchMove, false); 252 | 253 | // document.removeEventListener('mousemove', onMouseMove, false); 254 | // document.removeEventListener('mouseup', onMouseUp, false); 255 | 256 | // scope.domElement.removeEventListener('keydown', onKeyDown, false); 257 | 258 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 259 | 260 | }; 261 | 262 | // 263 | // internals 264 | // 265 | 266 | var scope = this; 267 | 268 | var changeEvent = { type: 'change' }; 269 | var startEvent = { type: 'start' }; 270 | var endEvent = { type: 'end' }; 271 | 272 | var STATE = { 273 | NONE: - 1, 274 | ROTATE: 0, 275 | DOLLY: 1, 276 | PAN: 2, 277 | TOUCH_ROTATE: 3, 278 | TOUCH_PAN: 4, 279 | TOUCH_DOLLY_PAN: 5, 280 | TOUCH_DOLLY_ROTATE: 6 281 | }; 282 | 283 | var state = STATE.NONE; 284 | 285 | var EPS = 0.000001; 286 | 287 | // current position in spherical coordinates 288 | var spherical = new Spherical(); 289 | var sphericalDelta = new Spherical(); 290 | 291 | var scale = 1; 292 | var panOffset = new Vector3(); 293 | var zoomChanged = false; 294 | 295 | var rotateStart = new Vector2(); 296 | var rotateEnd = new Vector2(); 297 | var rotateDelta = new Vector2(); 298 | 299 | var panStart = new Vector2(); 300 | var panEnd = new Vector2(); 301 | var panDelta = new Vector2(); 302 | 303 | var dollyStart = new Vector2(); 304 | var dollyEnd = new Vector2(); 305 | var dollyDelta = new Vector2(); 306 | 307 | function getAutoRotationAngle() { 308 | 309 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 310 | 311 | } 312 | 313 | function getZoomScale() { 314 | 315 | return Math.pow(0.95, scope.zoomSpeed); 316 | 317 | } 318 | 319 | function rotateLeft(angle) { 320 | 321 | sphericalDelta.theta -= angle; 322 | 323 | } 324 | 325 | function rotateUp(angle) { 326 | 327 | sphericalDelta.phi -= angle; 328 | 329 | } 330 | 331 | var panLeft = function () { 332 | 333 | var v = new Vector3(); 334 | 335 | return function panLeft(distance, objectMatrix) { 336 | 337 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 338 | v.multiplyScalar(- distance); 339 | 340 | panOffset.add(v); 341 | 342 | }; 343 | 344 | }(); 345 | 346 | var panUp = function () { 347 | 348 | var v = new Vector3(); 349 | 350 | return function panUp(distance, objectMatrix) { 351 | 352 | if (scope.screenSpacePanning === true) { 353 | 354 | v.setFromMatrixColumn(objectMatrix, 1); 355 | 356 | } else { 357 | 358 | v.setFromMatrixColumn(objectMatrix, 0); 359 | v.crossVectors(scope.object.up, v); 360 | 361 | } 362 | 363 | v.multiplyScalar(distance); 364 | 365 | panOffset.add(v); 366 | 367 | }; 368 | 369 | }(); 370 | 371 | // deltaX and deltaY are in pixels; right and down are positive 372 | var pan = function () { 373 | 374 | var offset = new Vector3(); 375 | 376 | return function pan(deltaX, deltaY) { 377 | 378 | var element = scope.domElement; 379 | 380 | if (scope.object.isPerspectiveCamera) { 381 | 382 | // perspective 383 | var position = scope.object.position; 384 | offset.copy(position).sub(scope.target); 385 | var targetDistance = offset.length(); 386 | 387 | // half of the fov is center to top of screen 388 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); 389 | 390 | // we use only height here so aspect ratio does not distort speed 391 | panLeft(2 * deltaX * targetDistance / element.height, scope.object.matrix); 392 | panUp(2 * deltaY * targetDistance / element.height, scope.object.matrix); 393 | 394 | } else if (scope.object.isOrthographicCamera) { 395 | 396 | // orthographic 397 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.width, scope.object.matrix); 398 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.height, scope.object.matrix); 399 | 400 | } else { 401 | 402 | // camera neither orthographic nor perspective 403 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); 404 | scope.enablePan = false; 405 | 406 | } 407 | 408 | }; 409 | 410 | }(); 411 | 412 | function dollyOut(dollyScale) { 413 | 414 | if (scope.object.isPerspectiveCamera) { 415 | 416 | scale /= dollyScale; 417 | 418 | } else if (scope.object.isOrthographicCamera) { 419 | 420 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); 421 | scope.object.updateProjectionMatrix(); 422 | zoomChanged = true; 423 | 424 | } else { 425 | 426 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 427 | scope.enableZoom = false; 428 | 429 | } 430 | 431 | } 432 | 433 | function dollyIn(dollyScale) { 434 | 435 | if (scope.object.isPerspectiveCamera) { 436 | 437 | scale *= dollyScale; 438 | 439 | } else if (scope.object.isOrthographicCamera) { 440 | 441 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); 442 | scope.object.updateProjectionMatrix(); 443 | zoomChanged = true; 444 | 445 | } else { 446 | 447 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 448 | scope.enableZoom = false; 449 | 450 | } 451 | 452 | } 453 | 454 | // 455 | // event callbacks - update the object state 456 | // 457 | 458 | function handleMouseDownRotate(event) { 459 | 460 | rotateStart.set(event.clientX, event.clientY); 461 | 462 | } 463 | 464 | function handleMouseDownDolly(event) { 465 | 466 | dollyStart.set(event.clientX, event.clientY); 467 | 468 | } 469 | 470 | function handleMouseDownPan(event) { 471 | 472 | panStart.set(event.clientX, event.clientY); 473 | 474 | } 475 | 476 | function handleMouseMoveRotate(event) { 477 | 478 | rotateEnd.set(event.clientX, event.clientY); 479 | 480 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 481 | 482 | var element = scope.domElement; 483 | 484 | rotateLeft(2 * Math.PI * rotateDelta.x / element.height); // yes, height 485 | 486 | rotateUp(2 * Math.PI * rotateDelta.y / element.height); 487 | 488 | rotateStart.copy(rotateEnd); 489 | 490 | scope.update(); 491 | 492 | } 493 | 494 | function handleMouseMoveDolly(event) { 495 | 496 | dollyEnd.set(event.clientX, event.clientY); 497 | 498 | dollyDelta.subVectors(dollyEnd, dollyStart); 499 | 500 | if (dollyDelta.y > 0) { 501 | 502 | dollyOut(getZoomScale()); 503 | 504 | } else if (dollyDelta.y < 0) { 505 | 506 | dollyIn(getZoomScale()); 507 | 508 | } 509 | 510 | dollyStart.copy(dollyEnd); 511 | 512 | scope.update(); 513 | 514 | } 515 | 516 | function handleMouseMovePan(event) { 517 | 518 | panEnd.set(event.clientX, event.clientY); 519 | 520 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 521 | 522 | pan(panDelta.x, panDelta.y); 523 | 524 | panStart.copy(panEnd); 525 | 526 | scope.update(); 527 | 528 | } 529 | 530 | function handleMouseUp( /*event*/) { 531 | 532 | // no-op 533 | 534 | } 535 | 536 | function handleMouseWheel(event) { 537 | 538 | if (event.deltaY < 0) { 539 | 540 | dollyIn(getZoomScale()); 541 | 542 | } else if (event.deltaY > 0) { 543 | 544 | dollyOut(getZoomScale()); 545 | 546 | } 547 | 548 | scope.update(); 549 | 550 | } 551 | 552 | function handleKeyDown(event) { 553 | 554 | var needsUpdate = false; 555 | 556 | switch (event.keyCode) { 557 | 558 | case scope.keys.UP: 559 | pan(0, scope.keyPanSpeed); 560 | needsUpdate = true; 561 | break; 562 | 563 | case scope.keys.BOTTOM: 564 | pan(0, - scope.keyPanSpeed); 565 | needsUpdate = true; 566 | break; 567 | 568 | case scope.keys.LEFT: 569 | pan(scope.keyPanSpeed, 0); 570 | needsUpdate = true; 571 | break; 572 | 573 | case scope.keys.RIGHT: 574 | pan(- scope.keyPanSpeed, 0); 575 | needsUpdate = true; 576 | break; 577 | 578 | } 579 | 580 | if (needsUpdate) { 581 | 582 | // prevent the browser from scrolling on cursor keys 583 | // event.preventDefault(); 584 | 585 | scope.update(); 586 | 587 | } 588 | 589 | 590 | } 591 | 592 | function handleTouchStartRotate(event) { 593 | 594 | if (event.touches.length == 1) { 595 | 596 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 597 | 598 | } else { 599 | 600 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 601 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 602 | 603 | rotateStart.set(x, y); 604 | 605 | } 606 | 607 | } 608 | 609 | function handleTouchStartPan(event) { 610 | 611 | if (event.touches.length == 1) { 612 | 613 | panStart.set(event.touches[0].pageX, event.touches[0].pageY); 614 | 615 | } else { 616 | 617 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 618 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 619 | 620 | panStart.set(x, y); 621 | 622 | } 623 | 624 | } 625 | 626 | function handleTouchStartDolly(event) { 627 | 628 | var dx = event.touches[0].pageX - event.touches[1].pageX; 629 | var dy = event.touches[0].pageY - event.touches[1].pageY; 630 | 631 | var distance = Math.sqrt(dx * dx + dy * dy); 632 | 633 | dollyStart.set(0, distance); 634 | 635 | } 636 | 637 | function handleTouchStartDollyPan(event) { 638 | 639 | if (scope.enableZoom) handleTouchStartDolly(event); 640 | 641 | if (scope.enablePan) handleTouchStartPan(event); 642 | 643 | } 644 | 645 | function handleTouchStartDollyRotate(event) { 646 | 647 | if (scope.enableZoom) handleTouchStartDolly(event); 648 | 649 | if (scope.enableRotate) handleTouchStartRotate(event); 650 | 651 | } 652 | 653 | function handleTouchMoveRotate(event) { 654 | 655 | if (event.touches.length == 1) { 656 | 657 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 658 | 659 | } else { 660 | 661 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 662 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 663 | 664 | rotateEnd.set(x, y); 665 | 666 | } 667 | 668 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 669 | 670 | var element = scope.domElement; 671 | 672 | rotateLeft(2 * Math.PI * rotateDelta.x / element.height); // yes, height 673 | 674 | rotateUp(2 * Math.PI * rotateDelta.y / element.height); 675 | 676 | rotateStart.copy(rotateEnd); 677 | 678 | } 679 | 680 | function handleTouchMovePan(event) { 681 | 682 | if (event.touches.length == 1) { 683 | 684 | panEnd.set(event.touches[0].pageX, event.touches[0].pageY); 685 | 686 | } else { 687 | 688 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 689 | var y = 0.5 * (event.touches[0].pageY+ event.touches[1].pageY); 690 | 691 | panEnd.set(x, y); 692 | 693 | } 694 | 695 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 696 | 697 | pan(panDelta.x, panDelta.y); 698 | 699 | panStart.copy(panEnd); 700 | 701 | } 702 | 703 | function handleTouchMoveDolly(event) { 704 | 705 | var dx = event.touches[0].pageX - event.touches[1].pageX; 706 | var dy = event.touches[0].pageY - event.touches[1].pageY; 707 | 708 | var distance = Math.sqrt(dx * dx + dy * dy); 709 | 710 | dollyEnd.set(0, distance); 711 | 712 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); 713 | 714 | dollyOut(dollyDelta.y); 715 | 716 | dollyStart.copy(dollyEnd); 717 | 718 | } 719 | 720 | function handleTouchMoveDollyPan(event) { 721 | 722 | if (scope.enableZoom) handleTouchMoveDolly(event); 723 | 724 | if (scope.enablePan) handleTouchMovePan(event); 725 | 726 | } 727 | 728 | function handleTouchMoveDollyRotate(event) { 729 | 730 | if (scope.enableZoom) handleTouchMoveDolly(event); 731 | 732 | if (scope.enableRotate) handleTouchMoveRotate(event); 733 | 734 | } 735 | 736 | function handleTouchEnd( /*event*/) { 737 | 738 | // no-op 739 | 740 | } 741 | 742 | // 743 | // event handlers - FSM: listen for events and reset state 744 | // 745 | 746 | function onMouseDown(event) { 747 | 748 | if (scope.enabled === false) return; 749 | 750 | // Prevent the browser from scrolling. 751 | // event.preventDefault(); 752 | 753 | // Manually set the focus since calling preventDefault above 754 | // prevents the browser from setting it automatically. 755 | 756 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 757 | 758 | var mouseAction; 759 | 760 | switch (event.button) { 761 | 762 | case 0: 763 | 764 | mouseAction = scope.mouseButtons.LEFT; 765 | break; 766 | 767 | case 1: 768 | 769 | mouseAction = scope.mouseButtons.MIDDLE; 770 | break; 771 | 772 | case 2: 773 | 774 | mouseAction = scope.mouseButtons.RIGHT; 775 | break; 776 | 777 | default: 778 | 779 | mouseAction = - 1; 780 | 781 | } 782 | 783 | switch (mouseAction) { 784 | 785 | case MOUSE.DOLLY: 786 | 787 | if (scope.enableZoom === false) return; 788 | 789 | handleMouseDownDolly(event); 790 | 791 | state = STATE.DOLLY; 792 | 793 | break; 794 | 795 | case MOUSE.ROTATE: 796 | 797 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 798 | 799 | if (scope.enablePan === false) return; 800 | 801 | handleMouseDownPan(event); 802 | 803 | state = STATE.PAN; 804 | 805 | } else { 806 | 807 | if (scope.enableRotate === false) return; 808 | 809 | handleMouseDownRotate(event); 810 | 811 | state = STATE.ROTATE; 812 | 813 | } 814 | 815 | break; 816 | 817 | case MOUSE.PAN: 818 | 819 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 820 | 821 | if (scope.enableRotate === false) return; 822 | 823 | handleMouseDownRotate(event); 824 | 825 | state = STATE.ROTATE; 826 | 827 | } else { 828 | 829 | if (scope.enablePan === false) return; 830 | 831 | handleMouseDownPan(event); 832 | 833 | state = STATE.PAN; 834 | 835 | } 836 | 837 | break; 838 | 839 | default: 840 | 841 | state = STATE.NONE; 842 | 843 | } 844 | 845 | if (state !== STATE.NONE) { 846 | 847 | document.addEventListener('mousemove', onMouseMove, false); 848 | document.addEventListener('mouseup', onMouseUp, false); 849 | 850 | scope.dispatchEvent(startEvent); 851 | 852 | } 853 | 854 | } 855 | 856 | function onMouseMove(event) { 857 | 858 | if (scope.enabled === false) return; 859 | 860 | // event.preventDefault(); 861 | 862 | switch (state) { 863 | 864 | case STATE.ROTATE: 865 | 866 | if (scope.enableRotate === false) return; 867 | 868 | handleMouseMoveRotate(event); 869 | 870 | break; 871 | 872 | case STATE.DOLLY: 873 | 874 | if (scope.enableZoom === false) return; 875 | 876 | handleMouseMoveDolly(event); 877 | 878 | break; 879 | 880 | case STATE.PAN: 881 | 882 | if (scope.enablePan === false) return; 883 | 884 | handleMouseMovePan(event); 885 | 886 | break; 887 | 888 | } 889 | 890 | } 891 | 892 | function onMouseUp(event) { 893 | 894 | if (scope.enabled === false) return; 895 | 896 | handleMouseUp(event); 897 | 898 | document.removeEventListener('mousemove', onMouseMove, false); 899 | document.removeEventListener('mouseup', onMouseUp, false); 900 | 901 | scope.dispatchEvent(endEvent); 902 | 903 | state = STATE.NONE; 904 | 905 | } 906 | 907 | function onMouseWheel(event) { 908 | 909 | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return; 910 | 911 | // event.preventDefault(); 912 | // event.stopPropagation(); 913 | 914 | scope.dispatchEvent(startEvent); 915 | 916 | handleMouseWheel(event); 917 | 918 | scope.dispatchEvent(endEvent); 919 | 920 | } 921 | 922 | function onKeyDown(event) { 923 | 924 | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return; 925 | 926 | handleKeyDown(event); 927 | 928 | } 929 | 930 | function onTouchStart(event) { 931 | if (scope.enabled === false) return; 932 | // event.preventDefault(); // prevent scrolling 933 | 934 | switch (event.touches.length) { 935 | 936 | case 1: 937 | 938 | switch (scope.touches.ONE) { 939 | 940 | case TOUCH.ROTATE: 941 | 942 | if (scope.enableRotate === false) return; 943 | 944 | handleTouchStartRotate(event); 945 | 946 | state = STATE.TOUCH_ROTATE; 947 | 948 | break; 949 | 950 | case TOUCH.PAN: 951 | 952 | if (scope.enablePan === false) return; 953 | 954 | handleTouchStartPan(event); 955 | 956 | state = STATE.TOUCH_PAN; 957 | 958 | break; 959 | 960 | default: 961 | 962 | state = STATE.NONE; 963 | 964 | } 965 | 966 | break; 967 | 968 | case 2: 969 | 970 | switch (scope.touches.TWO) { 971 | 972 | case TOUCH.DOLLY_PAN: 973 | 974 | if (scope.enableZoom === false && scope.enablePan === false) return; 975 | 976 | handleTouchStartDollyPan(event); 977 | 978 | state = STATE.TOUCH_DOLLY_PAN; 979 | 980 | break; 981 | 982 | case TOUCH.DOLLY_ROTATE: 983 | 984 | if (scope.enableZoom === false && scope.enableRotate === false) return; 985 | 986 | handleTouchStartDollyRotate(event); 987 | 988 | state = STATE.TOUCH_DOLLY_ROTATE; 989 | 990 | break; 991 | 992 | default: 993 | 994 | state = STATE.NONE; 995 | 996 | } 997 | 998 | break; 999 | 1000 | default: 1001 | 1002 | state = STATE.NONE; 1003 | 1004 | } 1005 | 1006 | if (state !== STATE.NONE) { 1007 | 1008 | scope.dispatchEvent(startEvent); 1009 | 1010 | } 1011 | 1012 | } 1013 | 1014 | function onTouchMove(event) { 1015 | 1016 | if (scope.enabled === false) return; 1017 | 1018 | // event.preventDefault(); // prevent scrolling 1019 | // event.stopPropagation(); 1020 | 1021 | switch (state) { 1022 | 1023 | case STATE.TOUCH_ROTATE: 1024 | 1025 | if (scope.enableRotate === false) return; 1026 | 1027 | handleTouchMoveRotate(event); 1028 | 1029 | scope.update(); 1030 | 1031 | break; 1032 | 1033 | case STATE.TOUCH_PAN: 1034 | 1035 | if (scope.enablePan === false) return; 1036 | 1037 | handleTouchMovePan(event); 1038 | 1039 | scope.update(); 1040 | 1041 | break; 1042 | 1043 | case STATE.TOUCH_DOLLY_PAN: 1044 | 1045 | if (scope.enableZoom === false && scope.enablePan === false) return; 1046 | 1047 | handleTouchMoveDollyPan(event); 1048 | 1049 | scope.update(); 1050 | 1051 | break; 1052 | 1053 | case STATE.TOUCH_DOLLY_ROTATE: 1054 | 1055 | if (scope.enableZoom === false && scope.enableRotate === false) return; 1056 | 1057 | handleTouchMoveDollyRotate(event); 1058 | 1059 | scope.update(); 1060 | 1061 | break; 1062 | 1063 | default: 1064 | 1065 | state = STATE.NONE; 1066 | 1067 | } 1068 | 1069 | } 1070 | 1071 | function onTouchEnd(event) { 1072 | 1073 | if (scope.enabled === false) return; 1074 | 1075 | handleTouchEnd(event); 1076 | 1077 | scope.dispatchEvent(endEvent); 1078 | 1079 | state = STATE.NONE; 1080 | 1081 | } 1082 | 1083 | function onContextMenu(event) { 1084 | 1085 | if (scope.enabled === false) return; 1086 | 1087 | event.preventDefault(); 1088 | 1089 | } 1090 | 1091 | // 1092 | 1093 | // scope.domElement.addEventListener('contextmenu', onContextMenu, false); 1094 | 1095 | // scope.domElement.addEventListener('mousedown', onMouseDown, false); 1096 | // scope.domElement.addEventListener('wheel', onMouseWheel, false); 1097 | // scope.domElement.addEventListener('touchstart', onTouchStart, false); 1098 | // scope.domElement.addEventListener('touchend', onTouchEnd, false); 1099 | // scope.domElement.addEventListener('touchmove', onTouchMove, false); 1100 | 1101 | // scope.domElement.addEventListener('keydown', onKeyDown, false); 1102 | 1103 | scope.onMouseDown=onMouseDown; 1104 | scope.onContextMenu=onContextMenu; 1105 | scope.onMouseWheel=onMouseWheel; 1106 | 1107 | scope.onTouchStart=onTouchStart; 1108 | scope.onTouchEnd=onTouchEnd; 1109 | scope.onTouchMove=onTouchMove; 1110 | 1111 | scope.onKeyDown=onKeyDown; 1112 | 1113 | // make sure element can receive keys. 1114 | 1115 | if (scope.domElement.tabIndex === - 1) { 1116 | 1117 | scope.domElement.tabIndex = 0; 1118 | 1119 | } 1120 | 1121 | // force an update at start 1122 | 1123 | this.update(); 1124 | 1125 | }; 1126 | 1127 | OrbitControls.prototype = Object.create(EventDispatcher.prototype); 1128 | OrbitControls.prototype.constructor = OrbitControls; 1129 | return OrbitControls; 1130 | } -------------------------------------------------------------------------------- /utils/pointPick.js: -------------------------------------------------------------------------------- 1 | const app = getApp(); 2 | 3 | export function onMouseMove(pt, mouse) { 4 | let { width, height } = app.globalData; 5 | // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1) 6 | mouse.x = (pt.x / width) * 2 - 1; 7 | mouse.y = - (pt.y / height) * 2 + 1; 8 | } 9 | 10 | export function pointPick(event, object) { 11 | let THREE = app.THREE; 12 | 13 | if (!object) 14 | object = app.Viewer.scene; 15 | 16 | let selectObjs = []; 17 | object.traverse(o => { 18 | if (o.isMesh&&o.name!=="plane") 19 | selectObjs.push(o); 20 | }) 21 | var raycaster = new THREE.Raycaster(); 22 | var mouse = new THREE.Vector2(); 23 | onMouseMove(event, mouse); 24 | // 通过摄像机和鼠标位置更新射线 25 | raycaster.setFromCamera(mouse, app.Viewer.camera.intance); 26 | 27 | // 计算物体和射线的焦点 28 | var intersects = raycaster.intersectObjects(selectObjs); 29 | for(let inter of intersects){ 30 | if(inter.object) 31 | return inter.object; 32 | } 33 | } -------------------------------------------------------------------------------- /utils/requestAnimationFrame.js: -------------------------------------------------------------------------------- 1 | export const requestAnimationFrame = function (callback, lastTime) { 2 | var lastTime; 3 | if (typeof lastTime === 'undefined') { 4 | lastTime = 0 5 | } 6 | var currTime = new Date().getTime(); 7 | var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); 8 | lastTime = currTime + timeToCall; 9 | var id = setTimeout(function () { 10 | callback(currTime + timeToCall, lastTime); 11 | }, 12 | timeToCall); 13 | return id; 14 | }; 15 | 16 | export const cancelAnimationFrame = function (id) { 17 | clearTimeout(id); 18 | }; 19 | -------------------------------------------------------------------------------- /utils/threeutils.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZoeLeee/wx_threeJs_project/b792adc4b4968bd58ab3142e20f210b5854c4276/utils/threeutils.js -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | --------------------------------------------------------------------------------