Browse Source

Model2DEditor

master
修宁 7 months ago
parent
commit
9d4d06ec1a
  1. 8
      src/designer/Designer.ts
  2. 68
      src/designer/Model2DEditor.vue
  3. 4
      src/designer/menus/Model3DView.ts
  4. 63
      src/designer/model2DEditor/Model2DEditor.vue
  5. 40
      src/designer/model2DEditor/Model2DEditorJs.ts
  6. 330
      src/designer/model2DEditor/ThreeJsEditor.vue
  7. 0
      src/designer/model3DView/Model3DView.vue
  8. 292
      src/views/ModelMain.less
  9. 8
      src/views/ModelMain.vue

8
src/designer/Designer.ts

@ -10,6 +10,14 @@ export default class Designer {
allLevels: any = null
currentFloor: string = null
editorState = reactive({
ready: false,
camera: {
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 }
}
})
constructor() {
this.init()
this.open()

68
src/designer/Model2DEditor.vue

@ -1,68 +0,0 @@
<template>
<div class="section-canvas">
<div class="section-top-toolbar section-toolbar">
<el-button type="primary" :icon="renderIcon('element TopLeft')" link></el-button>
<span class="section-toolbar-line"></span>
<el-cascader placeholder="选择楼层" size="small" v-model="currentLevel" :options="allLevels" filterable />
</div>
<div class="section-content"></div>
<div class="section-bottom-toolbar section-toolbar">
<div class="section-toolbar-left">
<el-button type="primary" :icon="renderIcon('fa MousePointer')" link></el-button>
<span class="section-toolbar-line"></span>
<el-button type="primary" :icon="renderIcon('element Aim')" link></el-button>
<span class="section-toolbar-line"></span>
<el-button type="primary" :icon="renderIcon('antd LineOutlined')" link></el-button>
<span class="section-toolbar-line"></span>
<el-button type="primary" :icon="renderIcon('icon5 BandageSharp')" link></el-button>
<span class="section-toolbar-line"></span>
<el-button type="primary" :icon="renderIcon('antd InsertRowLeftOutlined')" link></el-button>
</div>
<div class="section-toolbar-right">
<el-input v-model="searchKeyword" size="small" style="width: 110px" placeholder="Search">
<template #prefix>
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<span class="section-toolbar-line"></span>
<el-text type="warning">1</el-text>
<span class="section-toolbar-line"></span>
<el-text type="danger">0</el-text>
<div class="infor">
X=14.091,Y=12.397
</div>
</div>
</div>
</div>
</template>
<script>
import { renderIcon } from '@/utils/webutils.ts'
export default {
name: 'Model2DEditor',
components: {
renderIcon
},
data() {
return {
currentLevel: '',
searchKeyword: '',
sectionLeftSearch: '',
sectionRightSize: 0,
sectionBottomSize: 0,
hideRight: false,
hideBottom: false,
calcRightPanel: null,
calcBottomPanel: null
}
},
methods: {
renderIcon
},
computed: {
allLevels() {
return designer.allLevels
}
}
}
</script>

4
src/designer/menus/Model3DView.ts

@ -1,5 +1,5 @@
import { defineMenu } from '@/runtime/DefineMenu.ts'
import Model3DView from '@/designer/Model3DView.vue'
import Model3DView from '@/designer/model3DView/Model3DView.vue'
export default defineMenu((menus) => {
menus.insertChildren('tool',
@ -18,7 +18,7 @@ export default defineMenu((menus) => {
showMax: true,
showCancelButton: false,
showOkButton: false,
dialogClass:'model-3d-view-wrap',
dialogClass: 'model-3d-view-wrap'
})
}
}

63
src/designer/model2DEditor/Model2DEditor.vue

@ -0,0 +1,63 @@
<template>
<div class="section-canvas">
<div class="section-top-toolbar section-toolbar">
<span class="section-toolbar-line" style="margin-left: 85px;"></span>
<el-button :icon="renderIcon('antd ClusterOutlined')" link></el-button>
<span class="section-toolbar-line"></span>
<el-cascader placeholder="选择楼层" size="small" v-model="view.currentLevel" :options="allLevels" filterable />
</div>
<div class="section-content">
<ThreeJsEditor v-if="view.currentLevel" :key="view.currentLevel" />
</div>
<div class="section-bottom-toolbar section-toolbar">
<div class="section-toolbar-left">
<el-button title="鼠标状态 (ESC)" :icon="renderIcon('fa MousePointer')" link
:type="oper.currentMode==='normal'?'primary':''"
@click="()=>oper.currentMode = 'normal'"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="框选模式 (T)" :icon="renderIcon('FullScreen')" link
:type="oper.currentMode==='selectByRec'?'primary':''"
@click="()=>oper.currentMode = 'selectByRec'"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="物理流动线 (Z)" :icon="renderIcon('antd EnterOutlined')" link
:type="oper.currentMode==='ALink'?'primary':''"
@click="()=>oper.currentMode = 'ALink'"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="逻辑关联 (X)" :icon="renderIcon('antd LinkOutlined')" link
:type="oper.currentMode==='SLink'?'primary':''"
@click="()=>oper.currentMode = 'SLink'"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="测量工具" :icon="renderIcon('fa Ruler')" link
:type="oper.currentMode==='Ruler'?'primary':''"
@click="()=>oper.currentMode = 'Ruler'"></el-button>
</div>
<div class="section-toolbar-right">
<el-input v-model="view.searchKeyword" size="small" style="width: 110px; margin-right: 5px;"
placeholder="Search">
<template #prefix>
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<el-text type="warning">00001</el-text>
<span class="section-toolbar-line"></span>
<el-text type="danger">00011</el-text>
<div v-if="editorState.ready">
{{ editorState.camera.position.x.toFixed(2) }},{{ editorState.camera.position.y.toFixed(2) }},{{ editorState.camera.position.z.toFixed(2)
}}
</div>
<div class="infor">
X=14.091,Y=12.397
</div>
</div>
</div>
</div>
</template>
<script>
import Model2DEditorJs from './Model2DEditorJs.js'
export default Model2DEditorJs
</script>

40
src/designer/model2DEditor/Model2DEditorJs.ts

@ -0,0 +1,40 @@
import { renderIcon } from '@/utils/webutils.ts'
import { defineComponent } from 'vue'
import ThreeJsEditor from './ThreeJsEditor.vue'
export default defineComponent({
name: 'Model2DEditor',
components: { ThreeJsEditor },
data() {
return {
oper: {
currentMode: 'normal'
},
view: {
currentLevel: '',
searchKeyword: ''
}
} as IData
},
methods: {
renderIcon
},
computed: {
editorState() {
return designer.editorState
},
allLevels() {
return designer.allLevels
}
}
})
export interface IData {
oper: {
currentMode: 'normal' | 'ALink' | 'SLink' | 'PointCallback' | 'PointAdd' | 'LinkAdd' | 'LinkAdd2' | 'Ruler' | 'selectByRec',
},
view: {
currentLevel: string,
searchKeyword: string,
},
}

330
src/designer/model2DEditor/ThreeJsEditor.vue

@ -0,0 +1,330 @@
<template>
<div class="canvas-container" ref="canvasContainer" tabindex="1" />
</template>
<script setup lang="ts">
import * as THREE from 'three'
import $ from 'jquery'
import Stats from 'three/examples/jsm/libs/stats.module'
import {
ref,
onMounted,
nextTick,
reactive,
watch,
getCurrentInstance,
onUnmounted,
onBeforeUnmount,
defineExpose
} from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// DOM refs
const canvasContainer = ref(null)
//
let resizeObserver
// Three.js
let scene, camera, renderer, controls
let statsControls, axesHelper, gridHelper
const state = designer.editorState
function initMode2DCamera() {
if (camera) {
scene.remove(camera)
}
// ============================
const viewerDom = canvasContainer.value
const cameraNew = new THREE.OrthographicCamera(
viewerDom.clientWidth / -2,
viewerDom.clientWidth / 2,
viewerDom.clientHeight / 2,
viewerDom.clientHeight / -2,
1,
500
)
cameraNew.position.set(0, 100, 0)
cameraNew.lookAt(0, 0, 0)
cameraNew.zoom = 30
camera = cameraNew
scene.add(camera)
// ============================
const controlsNew = new OrbitControls(
camera,
renderer.domElement
)
controlsNew.enableDamping = false
controlsNew.enableZoom = true
controlsNew.enableRotate = false
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN } //
controlsNew.screenSpacePanning = false //
controlsNew.listenToKeyEvents(viewerDom) //
controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' }
controlsNew.panSpeed = 1
controlsNew.keyPanSpeed = 20 // normal 7
controlsNew.minDistance = 0.1
controlsNew.maxDistance = 1000
controls = controlsNew
controlsNew.addEventListener('change', syncCameraState)
camera.updateProjectionMatrix()
syncCameraState()
}
function initThree() {
const viewerDom = canvasContainer.value
//
scene = new THREE.Scene()
scene.background = new THREE.Color(0xeeeeee)
//
renderer = new THREE.WebGLRenderer({
logarithmicDepthBuffer: true,
antialias: true,
alpha: true,
precision: 'mediump',
premultipliedAlpha: true,
preserveDrawingBuffer: false,
powerPreference: 'high-performance'
})
renderer.outputEncoding = THREE.SRGBColorSpace
renderer.clearDepth()
renderer.shadowMap.enabled = true
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height)
viewerDom.appendChild(renderer.domElement)
//
initMode2DCamera()
// 线
axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
gridHelper = new THREE.GridHelper(500, 500)
gridHelper.material = new THREE.LineBasicMaterial({
color: 0x888888,
opacity: 1,
transparent: true
})
scene.add(gridHelper)
//
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
directionalLight.position.set(5, 5, 5).multiplyScalar(3)
directionalLight.castShadow = true
scene.add(directionalLight)
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1)
scene.add(hemisphereLight)
//
statsControls = new Stats()
statsControls.showPanel(0)
statsControls.dom.style.position = 'absolute'
statsControls.dom.style.top = '2px'
statsControls.dom.style.left = '0'
viewerDom.appendChild(statsControls.dom)
$(statsControls.dom).children().css('height', '28px')
//
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.9,
roughness: 0.1
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
camera.position.z = 5
animate()
}
//
function animate() {
requestAnimationFrame(animate)
renderView()
}
function renderView() {
statsControls?.update()
renderer?.render(scene, camera)
}
onMounted(() => {
const editor = getCurrentInstance()
nextTick(() => {
initThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(viewerDom)
state.ready = true
window['editor'] = editor
window['renderer'] = renderer
window['scene'] = scene
window['camera'] = camera
window['renderer'] = renderer
window['controls'] = controls
viewerDom?.focus()
})
})
onBeforeUnmount(() => {
state.ready = false
cleanupThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
delete window['editor']
delete window['renderer']
delete window['scene']
delete window['camera']
delete window['renderer']
delete window['controls']
})
function handleResize(entries) {
for (let entry of entries) {
// entry.contentRect
console.log('Element size changed:', entry.contentRect)
const width = entry.contentRect.width
const height = entry.contentRect.height
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height
camera.updateProjectionMatrix()
} else if (camera instanceof THREE.OrthographicCamera) {
camera.left = width / -2
camera.right = width / 2
camera.top = height / 2
camera.bottom = height / -2
camera.updateProjectionMatrix()
}
renderer.setSize(width, height)
break
}
}
watch(() => state.camera.position.y, (newVal) => {
if (state.ready) {
updateGridVisibility()
}
}, { deep: true })
/**
* 根据可视化范围更新网格的透明度
*/
function updateGridVisibility() {
const cameraDistance = state.camera.position.y
const maxVisibleDistance = 60 //
const fadeStartDistance = 10 //
// 0~1
let opacity = 1
if (cameraDistance > fadeStartDistance) {
opacity = 1 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance), 1)
}
//
gridHelper.material.opacity = opacity
gridHelper.material.transparent = opacity < 1
console.log('opacity', opacity)
}
function cleanupThree() {
//
if (scene) {
scene.traverse((obj) => {
//
if (obj.geometry) {
obj.geometry.dispose()
}
//
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose())
} else {
obj.material.dispose()
}
}
//
if (obj.texture) {
obj.texture.dispose()
}
//
if (obj.renderTarget) {
obj.renderTarget.dispose()
}
// OrbitControls
if (obj.dispose) {
obj.dispose()
}
})
//
scene.children = []
}
if (statsControls) {
statsControls.dom.remove()
}
if (renderer) {
renderer.dispose()
renderer.forceContextLoss()
console.log('WebGL disposed, memory:', renderer.info.memory)
renderer.domElement = null
}
}
function getEffectiveViewDistance() {
// 1.
const targetDistance = controls.target.distanceTo(camera.position)
// 2.
const viewHeight = (camera.top - camera.bottom) / camera.zoom
// 3.
// 使45fov
const referenceFOV = 45 //
return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2))
}
function syncCameraState() {
if (camera) {
state.camera.position.x = camera.position.x
state.camera.position.y = getEffectiveViewDistance()
state.camera.position.z = camera.position.z
}
}
</script>

0
src/designer/Model3DView.vue → src/designer/model3DView/Model3DView.vue

292
src/views/ModelMain.less

@ -1,271 +1,329 @@
.app-wrap{
.app-wrap {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.app-header{
height:50px;
.app-header {
height: 50px;
background: #545c64;
flex-shrink: 0;
display: flex;
flex-direction: row;
overflow: hidden;
.logo{
.logo {
display: flex;
align-items: center;
margin: 0 20px;
}
.app-header-menu-wrap{
flex:1;
.app-header-menu-wrap {
flex: 1;
display: flex;
flex-direction: row;
.app-header-menu{
.app-header-menu {
height: 100%;
padding:0 16px;
color:#fff;
padding: 0 16px;
color: #fff;
display: flex;
align-items: center;
cursor: pointer;
.el-icon{
margin-left:8px;
.el-icon {
margin-left: 8px;
font-size: 12px;
}
&:hover{
&:hover {
background-color: #494d52;
color: #fff
}
}
}
.user{
.user {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 10px;
&>span{
& > span {
display: inline-flex;
padding:5px;
padding: 5px;
background: #f4c521;
border-radius:15px;
color:#fff;
border-radius: 15px;
color: #fff;
}
}
}
.app-section{
flex:1;
.app-section {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
.btns-toolbar{
.btns-toolbar {
display: flex;
flex-direction: column;
.btns{
.item{
.btns {
.item {
height: 48px;
line-height: 48px;
text-align: center;
cursor: pointer;
font-size: 26px;
&:hover{
&:hover {
background: #cccccc;
}
&.selected{
&.selected {
background: #e8e8e8;
position: relative;
&:before{
&:before {
content: '';
position: absolute;
width: 3px;
height: 100%;
background: #f4c521;
left:0;
top:0;
left: 0;
top: 0;
}
}
}
}
.btns-top{
flex:1;
.btns-top {
flex: 1;
}
.btns-bottom{
.btns-bottom {
}
}
.btns-toolbar-left{
.btns-toolbar-left {
flex-shrink: 0;
width:50px;
border-right:1px solid #dcdcdc
width: 50px;
border-right: 1px solid #dcdcdc
}
.btns-toolbar-right{
.btns-toolbar-right {
flex-shrink: 0;
width:50px;
border-left:1px solid #dcdcdc;
&.btns-toolbar{
.btns .item.selected:before{
right:0;
left:auto;
width: 50px;
border-left: 1px solid #dcdcdc;
&.btns-toolbar {
.btns .item.selected:before {
right: 0;
left: auto;
}
}
}
.section{
flex:1;
.section {
flex: 1;
overflow: hidden;
.section-item-wrap{
.section-item-wrap {
height: 100%;
display: flex;
flex-direction: column;
&>.title{
border-bottom:1px solid #dcdcdc;
& > .title {
border-bottom: 1px solid #dcdcdc;
height: 35px;
line-height: 35px;
padding:0 0 0 10px;
padding: 0 0 0 10px;
font-size: 14px;
position: relative;
display: flex;
align-items: center;
&>.el-icon{
margin-right:3px;
& > .el-icon {
margin-right: 3px;
position: relative;
top:1px;
top: 1px;
}
&>.el-input{
flex:1;
margin:0 30px 0 10px;
& > .el-input {
flex: 1;
margin: 0 30px 0 10px;
}
.close{
.close {
position: absolute;
right:0;
right: 0;
display: inline-flex;
padding:10px;
cursor:pointer;
&:hover{
color:var(--el-color-primary)
padding: 10px;
cursor: pointer;
&:hover {
color: var(--el-color-primary)
}
}
}
.calc-left-panel{
flex:1;
.calc-left-panel {
flex: 1;
overflow: auto;
}
.calc-right-panel{
flex:1;
.calc-right-panel {
flex: 1;
overflow: auto;
}
.calc-bottom-panel{
flex:1;
.calc-bottom-panel {
flex: 1;
overflow: auto;
}
}
.section-bottom{
.section-item-wrap{
&>.title >.el-input{
flex:none;
.section-bottom {
.section-item-wrap {
& > .title > .el-input {
flex: none;
}
}
}
.section-tabs.el-tabs--card{
.section-tabs.el-tabs--card {
height: 100%;
&>.el-tabs__header{
& > .el-tabs__header {
box-sizing: border-box;
z-index: 0;
margin:0;
&>.el-tabs__nav-wrap{
margin-bottom:0
margin: 0;
& > .el-tabs__nav-wrap {
margin-bottom: 0
}
.el-tabs__item.is-active{
.el-tabs__item.is-active {
position: relative;
z-index: 1;
&:before{
&:before {
content: '';
width: 100%;
height: 1px;
background: #c61429;
position: absolute;
left:0;
top:0;
left: 0;
top: 0;
z-index: 999;
}
&:after{
&:after {
content: '';
width: 100%;
height: 1px;
background: #fff;
position: absolute;
left:0;
bottom:0;
left: 0;
bottom: 0;
z-index: 999;
}
&:hover{
&:after{
background:#c5c5c5;
&:hover {
&:after {
background: #c5c5c5;
}
}
}
.el-tabs__item{
border-bottom:0;
.el-tabs__item {
border-bottom: 0;
}
.el-tabs__nav-prev{
.el-tabs__nav-prev {
height: 40px;
background: #c9c9c9;
.el-icon{
color:#c61429
.el-icon {
color: #c61429
}
}
.el-tabs__nav-next{
.el-tabs__nav-next {
height: 40px;
background: #c9c9c9;
.el-icon{
color:#c61429
.el-icon {
color: #c61429
}
}
}
&>.el-tabs__content{
flex:1;
&>.el-tab-pane{
& > .el-tabs__content {
flex: 1;
& > .el-tab-pane {
height: 100%;
}
.section-canvas{
.section-canvas {
height: 100%;
display: flex;
flex-direction: column;
.section-toolbar{
.section-toolbar {
flex-shrink: 0;
height: 30px;
display: flex;
align-items: center;
.el-button{
.el-button {
margin-left: 5px;
}
.section-toolbar-line{
.section-toolbar-line {
width: 1px;
height: 16px;
background: #dcdcdc;
margin:0 5px;
margin: 0 5px;
}
&.section-bottom-toolbar{
&.section-bottom-toolbar {
justify-content: space-between;
.section-toolbar-left{
.section-toolbar-left {
display: flex;
align-items: center;
}
.section-toolbar-right{
.section-toolbar-right {
display: flex;
flex-direction: row;
align-items: center;
.infor{
.infor {
background: #000;
margin:0 5px;
color:#fff;
margin: 0 5px;
color: #fff;
font-size: 12px;
min-width: 120px;
text-align: center;
padding:3px 5px;
padding: 3px 5px;
}
}
}
}
.section-content{
flex:1;
.section-content {
flex: 1;
background: #e0e0e0;
display: flex;
& > .canvas-container {
flex: 1;
}
}
}
}
@ -274,20 +332,22 @@
}
}
.el-popper .el-divider--horizontal{
margin:5px 0;
border-color:#656668
.el-popper .el-divider--horizontal {
margin: 5px 0;
border-color: #656668
}
.model-3d-view-wrap.el-dialog.resize-dialog{
padding:0!important;
.el-dialog__footer{
.model-3d-view-wrap.el-dialog.resize-dialog {
padding: 0 !important;
.el-dialog__footer {
//padding-top:8px;
display: none;
}
.el-dialog__header{
.el-dialog__header {
display: flex;
align-items: center;
padding:8px 0 8px 8px;
padding: 8px 0 8px 8px;
}
}

8
src/views/ModelMain.vue

@ -98,16 +98,16 @@
</div>
</template>
<script>
import Logo from '@/assets/images/logo.png'
import './ModelMain.less'
import { renderIcon } from '@/utils/webutils.js'
import Split from '@/components/split/split.vue'
import SplitArea from '@/components/split/split-area.vue'
import Logo from '@/assets/images/logo.png'
import './ModelMain.less'
import { ModelMainInit, ModelMainMounted, ModelMainUnmounted } from '@/views/ModelMainInit.js'
import { getRootMenu } from '@/runtime/DefineMenu.js'
import { getWidgetByName, getWidgetBySide, getAllWidget } from '@/runtime/DefineWidget.js'
import Model2DEditor from '@/designer/Model2DEditor.vue'
import Model2DEditor from '@/designer/model2DEditor/Model2DEditor.vue'
import ModelView from '@/designer/ModelView.vue'
import { normalizeShortKey } from '@/utils/webutils.ts'

Loading…
Cancel
Save