10 changed files with 1367 additions and 934 deletions
@ -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> |
|||
@ -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> |
|||
@ -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, |
|||
}, |
|||
} |
|||
@ -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. 计算等效的透视相机距离 |
|||
// 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考) |
|||
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> |
|||
Loading…
Reference in new issue