HuangKai 2 months ago
commit
01a29b2dd6

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# hello-world
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Lints and fixes files
+```
+npm run lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 19 - 0
jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

File diff suppressed because it is too large
+ 19648 - 0
package-lock.json


+ 45 - 0
package.json

@@ -0,0 +1,45 @@
+{
+  "name": "FeiTuMapEditor",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "core-js": "^3.8.3",
+    "element-ui": "^2.15.14",
+    "three": "^0.171.0",
+    "vue": "^2.6.14"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "vue-template-compiler": "^2.6.14"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "@babel/eslint-parser"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


File diff suppressed because it is too large
+ 1 - 0
public/favicon.svg


+ 18 - 0
public/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.svg">
+    <link rel="stylesheet" type="text/css" href="style.css">
+    <title>飞兔地图编辑器</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 3 - 0
public/style.css

@@ -0,0 +1,3 @@
+body {
+    margin: 0;
+}

+ 23 - 0
src/App.vue

@@ -0,0 +1,23 @@
+<template>
+    <div id="app">
+        <MapEditor />
+    </div>
+</template>
+
+<script>
+import MapEditor from '@/view/MapEditor.vue'
+
+export default {
+    name: 'App',
+    components: {
+        MapEditor
+    }
+}
+</script>
+
+<style>
+#app {
+    width: 100vw;
+    height: 100vh;
+}
+</style>

File diff suppressed because it is too large
+ 1 - 0
src/assets/logo.svg


+ 58 - 0
src/components/BottomToolBar.vue

@@ -0,0 +1,58 @@
+<template>
+    <el-row :gutter="12" id="bottom-tool">
+        <el-col :span="3">
+            <el-card shadow="always" :body-style="{ padding: '0px', height: '50px' }">
+            </el-card>
+        </el-col>
+        <el-col :span="13">
+            <el-card shadow="always" :body-style="{ padding: '10px', height: '30px', horizontalAlign: 'center' }">
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <el-divider direction="vertical"></el-divider>
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <el-divider direction="vertical"></el-divider>
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <el-divider direction="vertical"></el-divider>
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+            </el-card>
+        </el-col>
+        <el-col :span="8">
+            <el-card shadow="always" :body-style="{ padding: '0px', height: '50px'  }">
+                从不显示
+            </el-card>
+        </el-col>
+    </el-row>
+</template>
+
+<script>
+export default {
+    name: 'BottomToolBar',
+    props: {
+
+    }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#bottom-tool {
+    position: absolute;
+    /* background-color: aquamarine; */
+    bottom: 20px;
+    right: 20px;
+    width: 1000px;
+}
+
+.tool-icon {
+    width: 30px;
+    height: 30px;
+}
+
+.el-divider--vertical {
+    display: inline-block;
+    width: 1px;
+    height: 1.5em;
+    margin: 0 8px;
+    vertical-align: baseline;
+    position: relative;
+}
+</style>

+ 96 - 0
src/components/FeatureMenu.vue

@@ -0,0 +1,96 @@
+<template>
+    <div>
+        <el-row>
+            <el-col :span="1.5">
+                <img src="../assets/logo.svg" alt="" title="飞兔地图首页">
+            </el-col>
+            <el-col :span="2.5">
+                <h3>飞兔地图</h3>
+            </el-col>
+            <el-col :span="20">
+                <el-menu class="el-menu-demo" mode="horizontal" default-active="1">
+                    <el-menu-item index="1">房间编辑</el-menu-item>
+                    <el-menu-item index="2">POI编辑</el-menu-item>
+                    <el-menu-item index="3">导航编辑</el-menu-item>
+                    <el-menu-item index="4">楼层编辑</el-menu-item>
+                </el-menu>
+            </el-col>
+        </el-row>
+        <div class="top-tool">
+            <el-row>
+                <el-col :span="9">
+                    <div>
+                        <el-input size="small" placeholder="请输入内容" v-model="input3" class="input-with-select" default-first-option="true">
+                            <el-select v-model="select" slot="prepend" placeholder="请选择">
+                                <el-option label="房间" value="1"></el-option>
+                                <el-option label="模型" value="2"></el-option>
+                                <el-option label="POI" value="3"></el-option>
+                                <el-option label="道路" value="4"></el-option>
+                            </el-select>
+                            <!-- <el-button slot="append" icon="el-icon-search"></el-button> -->
+                        </el-input>
+                    </div>
+                </el-col>
+                <el-col :span="5">
+                    <div class="text">
+                        <el-button type="text">我的场景</el-button>
+                    </div>
+                </el-col>
+                <el-col :span="5">
+                    <div class="text">
+                        <el-popover
+    placement="top-start"
+    title="标题"
+    width="200"
+    trigger="hover"
+    content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。">
+    <el-button type="text" slot="reference">帮助</el-button>
+  </el-popover>
+                    </div>
+                </el-col>
+                <el-col :span="5">
+                    <div class="text">
+                        <el-button type="text">交流群</el-button>
+                    </div>
+                </el-col>
+            </el-row>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'FeatureMenu',
+    props: {
+
+    }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+img {
+    width: 60px;
+    height: 60px;
+}
+
+.top-tool {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    width: 600px;
+    vertical-align: middle;
+}
+
+.top-tool .text{
+    font-size: 16px;
+}
+
+/* .top-tool .text :hover{
+    color: azure;
+} */
+
+.el-menu.el-menu--horizontal {
+    border-bottom: 0;/*solid 1px #e6e6e6;*/
+}
+</style>

+ 33 - 0
src/components/features/NavEdit.vue

@@ -0,0 +1,33 @@
+<template>
+    <div v-if="mode==='nav'">
+        <div class="bottom-border content-box">
+            <div v-for="o in 4" :key="o" class="text item">
+                {{ '列表内容 ' + o }}
+            </div>
+        </div>
+        <div class="content-box">
+            <div v-for="o in [88,89,80]" :key="o" class="text item">
+                {{ '列表内容 ' + o }}
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'NavEdit',
+    props: {
+        mode: String
+    },
+}
+</script>
+
+<style scoped>
+.bottom-border{
+    border-bottom: 1px solid #eee;
+}
+
+.content-box{
+    padding: 10px;
+}
+</style>

+ 35 - 0
src/components/features/POIEdit.vue

@@ -0,0 +1,35 @@
+<template>  
+    <div v-if="mode==='poi'">
+        <div class="bottom-border content-box">
+            <div v-for="o in 10" :key="o" class="text item">
+                {{ '列表内容 ' + o }}
+            </div>
+        </div>
+        <div class="content-box">
+            <div v-for="o in [88,89,80]" :key="o" class="text item">
+                {{ '列表内容 ' + o }}
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'POIEdit',
+    props: {
+        mode: String
+    },
+}
+</script>
+
+<style scoped>
+
+
+.bottom-border{
+    border-bottom: 1px solid #eee;
+}
+
+.content-box{
+    padding: 10px;
+}
+</style>

+ 108 - 0
src/components/features/RoomEdit.vue

@@ -0,0 +1,108 @@
+<template>
+    <el-container v-if="mode==='room'">
+        <el-aside width="60px">
+            <div class="menu-item">
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <div>绘制</div>
+            </div>
+            <el-divider></el-divider>
+            <div class="menu-item">
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <div>绘制</div>
+            </div>
+            <el-divider></el-divider>
+            <div class="menu-item">
+                <img src="@/assets/logo.svg" alt="" class="tool-icon">
+                <div>绘制</div>
+            </div>
+        </el-aside>
+        <el-main class="feature-box">
+            <el-collapse class="content-box" v-model="activeNames" @change="handleChange">
+                <el-collapse-item title="一致性 Consistency" name="1">
+                    <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
+                    <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
+                </el-collapse-item>
+                <el-collapse-item title="反馈 Feedback" name="2">
+                    <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
+                    <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
+                </el-collapse-item>
+                <el-collapse-item title="效率 Efficiency" name="3">
+                    <div>简化流程:设计简洁直观的操作流程;</div>
+                    <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
+                    <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
+                </el-collapse-item>
+                <el-collapse-item title="可控 Controllability" name="4">
+                    <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
+                    <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-container>
+</template>
+
+<script>
+export default {
+    name: 'RoomEdit',
+    props: {
+        mode: String
+    },
+    created() {
+        console.log(this);
+    }
+}
+</script>
+
+<style scoped>
+/* 
+.bottom-border{
+    border-bottom: 1px solid #eee;
+} */
+
+.content-box{
+    padding: 10px;
+}
+
+.feature-box{
+    padding: 2px;
+    /* margin-right: 7px; */
+    /* height: 100%; */
+    max-height: 399px;
+    overflow:auto;
+}
+
+.tool-icon {
+    width: 30px;
+    height: 30px;
+}
+
+.menu-item{
+    padding: 10px 0;
+    margin: 0 6px;
+    cursor: pointer;
+    font-size: 12px;
+    text-align: center;
+    color: #2c3e50;
+}
+
+.menu-item :hover{
+    background-color: #afc2d4;
+}
+  
+.el-aside {
+    background-color: #D3DCE6;
+    color: #333;
+    text-align: top;
+    /* line-height: 50px; */
+    height: 400px;
+    padding: 10px;
+}
+
+.el-divider--horizontal {
+    display: block;
+    height: 1px;
+    width: 100%;
+    margin:2px 0;
+    background-color: dimgray;
+}
+
+</style>

+ 21 - 0
src/js/Editor.js

@@ -0,0 +1,21 @@
+import * as THREE from 'three';
+
+var _DEFAULT_CAMERA = new THREE.PerspectiveCamera( 50, 1, 0.01, 1000 );
+_DEFAULT_CAMERA.name = 'Camera';
+_DEFAULT_CAMERA.position.set( 0, 5, 10 );
+_DEFAULT_CAMERA.lookAt( new THREE.Vector3() );
+
+class Editor {
+    constructor(){
+        console.log("Editor init");
+
+        this.camera = _DEFAULT_CAMERA.clone();
+
+        this.scene = new THREE.Scene();
+        this.scene.name = 'Scene';
+        
+        this.viewportCamera = this.camera;
+    }
+}
+
+export default Editor;

+ 438 - 0
src/js/EditorControls.js

@@ -0,0 +1,438 @@
+import * as THREE from 'three';
+
+class EditorControls extends THREE.EventDispatcher {
+
+	constructor( object, domElement ) {
+
+		super();
+
+		// API
+
+		this.enabled = true;
+		this.center = new THREE.Vector3();
+		this.panSpeed = 0.002;
+		this.zoomSpeed = 0.1;
+		this.rotationSpeed = 0.005;
+
+		// internals
+
+		var scope = this;
+		var vector = new THREE.Vector3();
+		var delta = new THREE.Vector3();
+		var box = new THREE.Box3();
+
+		var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
+		var state = STATE.NONE;
+
+		var center = this.center;
+		var normalMatrix = new THREE.Matrix3();
+		var pointer = new THREE.Vector2();
+		var pointerOld = new THREE.Vector2();
+		var spherical = new THREE.Spherical();
+		var sphere = new THREE.Sphere();
+
+		var pointers = [];
+		var pointerPositions = {};
+
+		// events
+
+		var changeEvent = { type: 'change' };
+
+		this.focus = function ( target ) {
+
+			var distance;
+
+			box.setFromObject( target );
+
+			if ( box.isEmpty() === false ) {
+
+				box.getCenter( center );
+				distance = box.getBoundingSphere( sphere ).radius;
+
+			} else {
+
+				// Focusing on an Group, AmbientLight, etc
+
+				center.setFromMatrixPosition( target.matrixWorld );
+				distance = 0.1;
+
+			}
+
+			delta.set( 0, 0, 1 );
+			delta.applyQuaternion( object.quaternion );
+			delta.multiplyScalar( distance * 4 );
+
+			object.position.copy( center ).add( delta );
+
+			scope.dispatchEvent( changeEvent );
+
+		};
+
+		this.pan = function ( delta ) {
+
+			var distance = object.position.distanceTo( center );
+
+			delta.multiplyScalar( distance * scope.panSpeed );
+			delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
+
+			object.position.add( delta );
+			center.add( delta );
+
+			scope.dispatchEvent( changeEvent );
+
+		};
+
+		this.zoom = function ( delta ) {
+
+			var distance = object.position.distanceTo( center );
+
+			delta.multiplyScalar( distance * scope.zoomSpeed );
+
+			if ( delta.length() > distance ) return;
+
+			delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
+
+			object.position.add( delta );
+
+			scope.dispatchEvent( changeEvent );
+
+		};
+
+		this.rotate = function ( delta ) {
+
+			vector.copy( object.position ).sub( center );
+
+			spherical.setFromVector3( vector );
+
+			spherical.theta += delta.x * scope.rotationSpeed;
+			spherical.phi += delta.y * scope.rotationSpeed;
+
+			spherical.makeSafe();
+
+			vector.setFromSpherical( spherical );
+
+			object.position.copy( center ).add( vector );
+
+			object.lookAt( center );
+
+			scope.dispatchEvent( changeEvent );
+
+		};
+
+		//
+
+		function onPointerDown( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( pointers.length === 0 ) {
+
+				domElement.setPointerCapture( event.pointerId );
+
+				domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
+				domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
+
+			}
+
+			//
+
+			if ( isTrackingPointer( event ) ) return;
+
+			//
+
+			addPointer( event );
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchStart( event );
+
+			} else {
+
+				onMouseDown( event );
+
+			}
+
+		}
+
+		function onPointerMove( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchMove( event );
+
+			} else {
+
+				onMouseMove( event );
+
+			}
+
+		}
+
+		function onPointerUp( event ) {
+
+			removePointer( event );
+
+			switch ( pointers.length ) {
+
+				case 0:
+
+					domElement.releasePointerCapture( event.pointerId );
+
+					domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
+					domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
+
+					break;
+
+				case 1:
+
+					var pointerId = pointers[ 0 ];
+					var position = pointerPositions[ pointerId ];
+
+					// minimal placeholder event - allows state correction on pointer-up
+					onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
+
+					break;
+
+			}
+
+		}
+
+		// mouse
+
+		function onMouseDown( event ) {
+
+			if ( event.button === 0 ) {
+
+				state = STATE.ROTATE;
+
+			} else if ( event.button === 1 ) {
+
+				state = STATE.ZOOM;
+
+			} else if ( event.button === 2 ) {
+
+				state = STATE.PAN;
+
+			}
+
+			pointerOld.set( event.clientX, event.clientY );
+
+		}
+
+		function onMouseMove( event ) {
+
+			pointer.set( event.clientX, event.clientY );
+
+			var movementX = pointer.x - pointerOld.x;
+			var movementY = pointer.y - pointerOld.y;
+
+			if ( state === STATE.ROTATE ) {
+
+				scope.rotate( delta.set( - movementX, - movementY, 0 ) );
+
+			} else if ( state === STATE.ZOOM ) {
+
+				scope.zoom( delta.set( 0, 0, movementY ) );
+
+			} else if ( state === STATE.PAN ) {
+
+				scope.pan( delta.set( - movementX, movementY, 0 ) );
+
+			}
+
+			pointerOld.set( event.clientX, event.clientY );
+
+		}
+
+		function onMouseUp() {
+
+			state = STATE.NONE;
+
+		}
+
+		function onMouseWheel( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			event.preventDefault();
+
+			// Normalize deltaY due to https://bugzilla.mozilla.org/show_bug.cgi?id=1392460
+			scope.zoom( delta.set( 0, 0, event.deltaY > 0 ? 1 : - 1 ) );
+
+		}
+
+		function contextmenu( event ) {
+
+			event.preventDefault();
+
+		}
+
+		this.dispose = function () {
+
+			domElement.removeEventListener( 'contextmenu', contextmenu );
+			domElement.removeEventListener( 'dblclick', onMouseUp );
+			domElement.removeEventListener( 'wheel', onMouseWheel );
+
+			domElement.removeEventListener( 'pointerdown', onPointerDown );
+
+		};
+
+		domElement.addEventListener( 'contextmenu', contextmenu );
+		domElement.addEventListener( 'dblclick', onMouseUp );
+		domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
+
+		domElement.addEventListener( 'pointerdown', onPointerDown );
+
+		// touch
+
+		var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
+		var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
+
+		var prevDistance = null;
+
+		function onTouchStart( event ) {
+
+			trackPointer( event );
+
+			switch ( pointers.length ) {
+
+				case 1:
+					touches[ 0 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					touches[ 1 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					break;
+
+				case 2:
+
+					var position = getSecondPointerPosition( event );
+
+					touches[ 0 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					touches[ 1 ].set( position.x, position.y, 0 ).divideScalar( window.devicePixelRatio );
+					prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
+					break;
+
+			}
+
+			prevTouches[ 0 ].copy( touches[ 0 ] );
+			prevTouches[ 1 ].copy( touches[ 1 ] );
+
+		}
+
+
+		function onTouchMove( event ) {
+
+			trackPointer( event );
+
+			function getClosest( touch, touches ) {
+
+				var closest = touches[ 0 ];
+
+				for ( var touch2 of touches ) {
+
+					if ( closest.distanceTo( touch ) > touch2.distanceTo( touch ) ) closest = touch2;
+
+				}
+
+				return closest;
+
+			}
+
+			switch ( pointers.length ) {
+
+				case 1:
+					touches[ 0 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					touches[ 1 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - 1 ) );
+					break;
+
+				case 2:
+
+					var position = getSecondPointerPosition( event );
+
+					touches[ 0 ].set( event.pageX, event.pageY, 0 ).divideScalar( window.devicePixelRatio );
+					touches[ 1 ].set( position.x, position.y, 0 ).divideScalar( window.devicePixelRatio );
+					var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
+					scope.zoom( delta.set( 0, 0, prevDistance - distance ) );
+					prevDistance = distance;
+
+
+					var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
+					var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
+					offset0.x = - offset0.x;
+					offset1.x = - offset1.x;
+
+					scope.pan( offset0.add( offset1 ) );
+
+					break;
+
+			}
+
+			prevTouches[ 0 ].copy( touches[ 0 ] );
+			prevTouches[ 1 ].copy( touches[ 1 ] );
+
+		}
+
+		function addPointer( event ) {
+
+			pointers.push( event.pointerId );
+
+		}
+
+		function removePointer( event ) {
+
+			delete pointerPositions[ event.pointerId ];
+
+			for ( var i = 0; i < pointers.length; i ++ ) {
+
+				if ( pointers[ i ] == event.pointerId ) {
+
+					pointers.splice( i, 1 );
+					return;
+
+				}
+
+			}
+
+		}
+
+		function isTrackingPointer( event ) {
+
+			for ( var i = 0; i < pointers.length; i ++ ) {
+
+				if ( pointers[ i ] == event.pointerId ) return true;
+
+			}
+
+			return false;
+
+		}
+
+		function trackPointer( event ) {
+
+			var position = pointerPositions[ event.pointerId ];
+
+			if ( position === undefined ) {
+
+				position = new THREE.Vector2();
+				pointerPositions[ event.pointerId ] = position;
+
+			}
+
+			position.set( event.pageX, event.pageY );
+
+		}
+
+		function getSecondPointerPosition( event ) {
+
+			var pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ];
+
+			return pointerPositions[ pointerId ];
+
+		}
+
+	}
+
+}
+
+export { EditorControls };

+ 37 - 0
src/js/ViewHelper.js

@@ -0,0 +1,37 @@
+import { ViewHelper as ViewHelperBase } from 'three/addons/helpers/ViewHelper.js';
+
+class ViewHelper extends ViewHelperBase {
+
+	constructor( editorCamera, dom ) {
+
+		super( editorCamera, dom );
+
+		// const panel = new UIPanel();
+		// panel.setId( 'viewHelper' );
+		// panel.setPosition( 'absolute' );
+		// panel.setRight( '0px' );
+		// panel.setBottom( '0px' );
+		// panel.setHeight( '128px' );
+		// panel.setWidth( '128px' );
+
+		// panel.dom.addEventListener( 'pointerup', ( event ) => {
+
+		// 	event.stopPropagation();
+
+		// 	this.handleClick( event );
+
+		// } );
+
+		// panel.dom.addEventListener( 'pointerdown', function ( event ) {
+
+		// 	event.stopPropagation();
+
+		// } );
+
+		// container.add( panel );
+
+	}
+
+}
+
+export { ViewHelper };

+ 119 - 0
src/js/Viewport.js

@@ -0,0 +1,119 @@
+import * as THREE from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+// import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+class Viewport {
+    constructor(editor){
+        console.log("Viewport init ", editor);
+        let dom = document.getElementById("viewport");
+        this.editor = editor;
+        console.log(dom);
+        
+        const GRID_COLORS_LIGHT = [ 0x999999, 0x777777 ];
+        // const GRID_COLORS_DARK = [ 0x555555, 0x888888 ];
+
+        this.renderer = new THREE.WebGLRenderer();
+
+        this.camera = editor.camera;
+        this.scene = editor.scene;
+        
+        const grid = new THREE.Group();
+
+        const grid1 = new THREE.GridHelper( 30, 30 );
+        grid1.material.color.setHex( GRID_COLORS_LIGHT[ 0 ] );
+        grid1.material.vertexColors = false;
+        grid.add( grid1 );
+
+        const grid2 = new THREE.GridHelper( 30, 6 );
+        grid2.material.color.setHex( GRID_COLORS_LIGHT[ 1 ] );
+        grid2.material.vertexColors = false;
+        grid.add( grid2 );
+
+        const pointsArr = [
+            // 三维向量Vector3表示的坐标值
+            new THREE.Vector3(0,0,0),
+            new THREE.Vector3(0,0,1),
+            new THREE.Vector3(1,0,5),
+            new THREE.Vector3(3,0,0),
+        ];
+        const material = new THREE.LineBasicMaterial({
+            color: 0x00ff00,
+            linewidth: 1,
+        });
+        const geometry = new THREE.BufferGeometry();
+        geometry.setFromPoints(pointsArr);
+        const line = new THREE.Line(geometry, material);
+        grid.add(line)
+
+        // 假设我们已经有了由鼠标点击生成的points数组
+// const points = [new THREE.Vector2(0,0),new THREE.Vector2(1,1),new THREE.Vector2(2,2)]; // 用户通过点击收集的点
+
+// const lineWidth = 0.1; // 折线的宽度
+
+// function createWideLineMesh(points, width) {
+//     const geometries = [];
+//     for (let i = 0; i < points.length - 1; i++) {
+//         const start = points[i];
+//         const end = points[i + 1];
+
+//         // 计算线段的长度和方向
+//         const direction = new THREE.Vector3().subVectors(end, start).normalize();
+//         const length = start.distanceTo(end);
+
+//         // 创建矩形网格
+//         const geometry = new THREE.PlaneGeometry(width, length);
+//         const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x0033ff, side: THREE.DoubleSide }));
+//         // grid.add(mesh)
+//         // 设置矩形的位置和旋转
+//         console.log(direction, length, geometry, geometry.center());
+//         mesh.position.copy(start.clone().add(end).multiplyScalar(0.5));
+//         const rotationAxis = new THREE.Vector3(0, 0, 1).cross(direction);
+//         const angle = Math.acos(new THREE.Vector3(0, 1, 0).dot(direction));
+//         mesh.setRotationFromAxisAngle(rotationAxis, angle);
+//         // console.log(direction, length, geometry.rotateZ(angle));
+
+//         // 添加到集合
+//         geometries.push(mesh);
+//     }
+
+//     // 合并所有几何体为一个
+//     const combinedGeometry = BufferGeometryUtils.mergeGeometries(
+//         geometries.map(g => g.geometry)
+//     );
+
+//     // 创建最终的材料和网格
+//     const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
+//     const lineMesh = new THREE.Mesh(combinedGeometry, material);
+
+//     return lineMesh;
+// }
+
+// // 创建折线mesh并添加到场景
+// const wideLine = createWideLineMesh(points, lineWidth);
+//         grid.add(wideLine)
+
+        this.scene.add(grid);
+        this.scene.add(this.camera);
+        this.renderer.setSize(dom.clientWidth, dom.clientHeight);
+        this.camera.aspect = dom.clientWidth / dom.clientHeight;
+        this.camera.updateProjectionMatrix();
+        dom.appendChild(this.renderer.domElement)
+
+        this.control = new OrbitControls(this.camera, this.renderer.domElement)
+
+        this.animate()
+    }
+
+    animate() {
+
+        requestAnimationFrame( () => this.animate() );
+    
+        // required if controls.enableDamping or controls.autoRotate are set to true
+        this.control.update();
+    
+        this.renderer.render( this.scene, this.editor.viewportCamera );
+    
+    }
+}
+
+export default Viewport;

File diff suppressed because it is too large
+ 14 - 0
src/js/libs/signals.min.js


+ 11 - 0
src/main.js

@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+import App from './App.vue';
+
+Vue.use(ElementUI);
+
+new Vue({
+  el: '#app',
+  render: h => h(App)
+});

+ 35 - 0
src/view/EditorViewport.vue

@@ -0,0 +1,35 @@
+<template>
+   <div id="viewport" /> 
+</template>
+
+<script>
+import Editor from '@/js/Editor'
+import Viewport from '@/js/Viewport'
+
+export default {
+    name: 'EditorViewport',
+    props: {
+        editor: Editor
+    },
+    data() {
+        return {
+            viewport: null,
+        }
+    },
+    mounted(){
+        console.log(this);
+        this._data.viewport = new Viewport(this._props.editor)
+
+        console.log(this._props.editor);
+    }
+}
+
+
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#viewport {
+    height: 100%;
+}
+</style>

+ 65 - 0
src/view/LeftEditPanel.vue

@@ -0,0 +1,65 @@
+<template>
+    <el-card  v-if="editMode !== 'floor'" shadow="always" class="box-card" id="left-tool">
+        <div slot="header" class="clearfix">
+            <span>卡片名称</span>
+        </div>
+        <RoomEdit :mode="editMode"/>
+        <POIEdit :mode="editMode"/>
+        <NavEdit :mode="editMode"/>
+    </el-card>
+</template>
+
+<script>
+import RoomEdit from '@/components/features/RoomEdit.vue'
+import POIEdit from '@/components/features/POIEdit.vue'
+import NavEdit from '@/components/features/NavEdit.vue'
+
+export default {
+    name: 'LeftEditPanel',
+    props: {
+
+    },
+    comments:{
+        RoomEdit,
+        POIEdit,
+        NavEdit
+    },
+    data() {
+        return {
+            editMode: 'room',
+            editModes: ['room', 'poi', 'nav', 'floor'],
+        }
+    },
+    beforeCreate() {
+        this.$options.components.RoomEdit= require('@/components/features/RoomEdit.vue').default
+        this.$options.components.POIEdit= require('@/components/features/POIEdit.vue').default
+        this.$options.components.NavEdit= require('@/components/features/NavEdit.vue').default
+        console.log(RoomEdit, POIEdit, NavEdit);
+    },
+    created() {
+        console.log(RoomEdit, POIEdit, NavEdit);
+    }
+}
+
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#left-tool {
+    position: absolute;
+    top: 80px;
+    left: 20px;
+    min-width: 288px;
+    max-width: 288px;
+    min-height: 450px;
+    max-height: 700px;
+}
+
+.el-card ::v-deep .el-card__header {
+    background-color: dodgerblue;
+}
+
+.el-card ::v-deep .el-card__body {
+    padding: 0;
+}
+</style>

+ 53 - 0
src/view/MapEditor.vue

@@ -0,0 +1,53 @@
+<template>
+    <el-container id="editor">
+        <el-header><FeatureMenu id="features" /></el-header>
+        <el-main id="viewport-container">            
+            <EditorViewport :editor="editor"/>
+        </el-main>
+        
+        <LeftEditPanel />
+        <RightEditPanel />
+        <BottomToolBar />
+    </el-container>
+</template>
+
+<script>
+import Editor from '@/js/Editor.js'
+import FeatureMenu from '@/components/FeatureMenu.vue'
+import LeftEditPanel from '@/view/LeftEditPanel.vue'
+import RightEditPanel from '@/view/RightEditPanel.vue'
+import BottomToolBar from '@/components/BottomToolBar.vue'
+import EditorViewport from '@/view/EditorViewport.vue'
+export default {
+    name: 'MapEditor',
+    props: {
+
+    },
+    data() {
+        return {
+            editor: new Editor(),
+        }
+    },
+    components: {
+        FeatureMenu,
+        LeftEditPanel,
+        RightEditPanel,
+        BottomToolBar,
+        EditorViewport,
+    },
+    created() {
+        console.log(this.editor);
+    }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#editor {
+    width: 100%;
+    height: 100%;
+}
+#viewport-container {
+    padding: 0;
+}
+</style>

+ 38 - 0
src/view/RightEditPanel.vue

@@ -0,0 +1,38 @@
+<template>
+    <el-card shadow="always" class="box-card" id="right-edit">
+        <div slot="header" class="clearfix">
+            <span>卡片名称</span>
+            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
+        </div>
+        <div v-for="o in 4" :key="o" class="text item">
+            {{ '列表内容 ' + o }}
+        </div>
+    </el-card>
+</template>
+
+<script>
+export default {
+    name: 'RightEditPanel',
+    props: {
+
+    }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+#right-edit {
+    position: absolute;
+    /* background-color: aquamarine; */
+    top: 80px;
+    right: 20px;
+    min-width: 288px;
+    max-width: 288px;
+    min-height: 450px;
+    max-height: 450px;
+}
+
+.el-card ::v-deep .el-card__header {
+    background-color: dodgerblue;
+}
+</style>

+ 4 - 0
vue.config.js

@@ -0,0 +1,4 @@
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  transpileDependencies: true
+})