Browse Source

3D模型查看器 / 对话框定义 / pnpm 管理器

master
修宁 7 months ago
parent
commit
c663be998e
  1. 11
      package.json
  2. 3117
      pnpm-lock.yaml
  3. 16
      src/App.vue
  4. 167
      src/components/ShowDialogWrap.vue
  5. 38
      src/components/el-drawer-drag-width/index.ts
  6. 82
      src/components/element-dialog-resize/index.ts
  7. 45
      src/components/element-dialog-resize/use-css-variable.ts
  8. 85
      src/components/element-dialog-resize/use-draggable.ts
  9. 82
      src/components/element-dialog-resize/use-fullscreen.ts
  10. 9
      src/components/element-dialog-resize/use-parse-translate.ts
  11. 101
      src/components/element-dialog-resize/use-resizer.ts
  12. 194
      src/designer/Model3DView.vue
  13. 26
      src/designer/menus/Model3DView.ts
  14. 11
      src/designer/menus/Tools.ts
  15. 59
      src/runtime/System.ts
  16. 4
      src/views/ModelMainInit.ts
  17. 19
      vite.config.ts
  18. 2220
      yarn.lock

11
package.json

@ -12,7 +12,8 @@
"format": "prettier --write src/"
},
"dependencies": {
"@vicons/antd": "^0.13.0",
"@vueuse/core": "^13.2.0",
"ag-grid-community": "^28.2.1",
"ag-grid-enterprise": "^28.2.1",
"ag-grid-vue3": "^28.2.1",
"axios": "^1.9.0",
@ -22,11 +23,11 @@
"element-plus": "^2.9.10",
"hotkeys-js": "^3.13.10",
"jquery": "^3.6.0",
"json5": "^2.2.3",
"less": "^4.2.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"pinia": "^3.0.1",
"rimraf": "^3.0.2",
"sortablejs": "1.15.6",
"split.js": "^1.6.4",
"three": "^0.176.0",
@ -36,17 +37,21 @@
"vue3-menus": "^1.1.2"
},
"devDependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@rolldown/pluginutils": "1.0.0-beta.8-commit.56abf23",
"@tsconfig/node22": "^22.0.1",
"@types/jquery": "^3.3.31",
"@types/lodash-es": "^4.17.7",
"@types/node": "^22.14.0",
"@types/three": "^0.176.0",
"@vicons/antd": "^0.13.0",
"@vicons/fa": "^0.12.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vitejs/plugin-vue-jsx": "^4.2.0",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.3",
"rimraf": "^6.0.1",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",

3117
pnpm-lock.yaml

File diff suppressed because it is too large

16
src/App.vue

@ -1,11 +1,23 @@
<template>
<div class="main">
<RouterView />
<div id="yv-hide-container" style="display: none;">
<template v-for="item in dialogList">
<component :is="item.cmp" v-bind="item.props" />
</template>
</div>
</div>
</template>
<script setup lang="ts">
<script>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
export default {
computed: {
dialogList() {
return system.rootElementList
}
}
}
</script>
<style scoped>
header {

167
src/components/ShowDialogWrap.vue

@ -0,0 +1,167 @@
<template>
<div v-element-dialog-resize="{ draggable: draggable, fullscreen: draggable }">
<el-dialog :title="title" :width="width" v-model="isShow" :modal="modal"
:class="['yv-dialog-wrap', ...calcClass]"
:style="calcStyle"
:closeOnClickModal="closeOnClickModal"
:closeOnPressEscape="closeOnPressEscape"
append-to-body destroyOnClose
@closed="onClosed"
@opened="onOpened">
<component v-if="childCmp" :is="childCmp"
v-bind="_input"
ref="childCmpRef"
@ok="onOk"
@closeDialog="onClose"
/>
<template #footer>
<div class="footer">
<el-button v-if="showCancelButton" @click="onClose">{{ cancelButtonText }}</el-button>
<el-button v-if="showOkButton" type="primary" @click="onOkHandle">{{ okButtonText }}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import { markRaw } from 'vue'
import ElementDialogResize from '@/components/element-dialog-resize'
export default {
directives: { ElementDialogResize },
props: {
title: { type: String, default: '对话框', required: false },
draggable: { type: Boolean, default: () => false, required: false },
width: { type: [String, Number], default: () => 600, required: false },
height: { type: Number, defaults: () => 0, required: false },
showClose: { type: Boolean, default: () => true, required: false },
showMax: { type: Boolean, default: () => false, required: false },
modal: { type: Boolean, default: () => true, required: false },
showCancelButton: { type: Boolean, default: () => true, required: false },
showOkButton: { type: Boolean, default: () => true, required: false },
cancelButtonText: { type: String, default: () => '取消', required: false },
okButtonText: { type: String, default: () => '确定', required: false },
dialogClass: { type: String, default: '', required: false },
closeOnClickModal: { type: Boolean, default: () => false, required: false },
closeOnPressEscape: { type: Boolean, default: () => true, required: false },
_insId: { type: String, required: true },
_input: { type: Object, defaults: {}, required: true },
dialogResolve: { type: Function, required: true },
dialogReject: { type: Function, required: true },
childCmp: { type: Object, default: undefined, required: false }
},
mounted() {
},
data() {
return {
isShow: true,
returnOk: false
}
},
methods: {
onOk(result) {
// bizMixin.js OK
this.isShow = false
this.returnOk = true
this.$nextTick(() => {
this.dialogResolve(result)
})
},
onOkHandle() {
if (typeof this.$refs.childCmpRef['okClick'] === 'function') {
const data = this.$refs.childCmpRef['okClick']()
if (data) {
this.onOk(data)
}
}
},
onClose() {
this.isShow = false
},
onClosed() {
if (!this.returnOk) {
// returnOk Promise.reject
this.dialogReject()
}
_.remove(system.rootElementList, (item) => item.props._insId === this._insId)
},
onOpened() {
}
},
computed: {
calcClass() {
const clazz = []
if (this.draggable) {
clazz.push('resize-dialog')
}
if (this.dialogClass) {
clazz.push(this.dialogClass)
}
return clazz
},
calcStyle() {
const style = {}
if (this.height > 0) {
style.height = this.height + 'px'
}
return style
}
}
}
</script>
<style lang="less">
.yv-dialog-wrap {
}
.resize-dialog {
min-height: 200px;
min-width: 300px;
padding: 5px !important;
//height: 400px;
display: flex;
flex-direction: column;
.fullbtn-container {
height: 100%;
line-height: 45px;
width: 30px;
text-align: center;
&:hover {
color: var(--frist-menu-active-color)
}
}
.closebtn-container {
height: 100%;
line-height: 45px;
width: 30px;
text-align: center;
&:hover {
color: var(--frist-menu-active-color)
}
}
&.is-draggable {
& > .el-dialog__header {
cursor: move;
}
}
.el-dialog__body {
flex-grow: 1;
padding: 0 !important;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
</style>

38
src/components/el-drawer-drag-width/index.ts

@ -0,0 +1,38 @@
import $ from 'jquery'
import _ from 'lodash'
/**
* el-drawer
*/
export default {
mounted(el, binding, vnode, oldVnode) {
const drawerEle = $(el).closest('.el-drawer')[0]
// 创建触发拖拽的元素
const dragItem = document.createElement('div')
// 将元素放置到抽屉的左边边缘
dragItem.style.cssText = 'height: 100%;width: 5px;cursor: w-resize;position: absolute;left: 0;'
drawerEle?.append(dragItem)
dragItem.onmousedown = (downEvent) => {
// 拖拽时禁用文本选中
document.body.style.userSelect = 'none'
document.onmousemove = _.debounce(function (moveEvent) {
// 获取鼠标距离浏览器右边缘的距离
let realWidth = document.body.clientWidth - moveEvent.pageX
const width30 = document.body.clientWidth * 0.2
const width80 = document.body.clientWidth * 0.8
// 宽度不能大于浏览器宽度 80%,不能小于宽度的 20%
realWidth = realWidth > width80 ? width80 : realWidth < width30 ? width30 : realWidth
drawerEle.style.width = realWidth + 'px'
}, 0)
document.onmouseup = function (e) {
// 拖拽时结束时,取消禁用文本选中
document.body.style.userSelect = 'initial'
document.onmousemove = null
document.onmouseup = null
}
}
}
}

82
src/components/element-dialog-resize/index.ts

@ -0,0 +1,82 @@
import {computed, nextTick, watchEffect} from 'vue'
import {isArray} from 'lodash'
import useDraggable, {addUnit} from './use-draggable'
import useResizer from './use-resizer'
import useFullscreen from './use-fullscreen'
import type {DirectiveBinding} from 'vue'
import useCssVariable from './use-css-variable'
const beforeMountedFun: Array<(...args: any) => any> = []
function handle(el: HTMLElement, binding: DirectiveBinding, vnode: any) {
if (!(isArray(vnode.children) &&
(vnode.children[0] as any)?.props?.modelValue &&
(vnode.children[0] as any)?.component?.subTree?.children?.default)) {
// console.log(vnode.children)
// console.log((vnode.children[0] as any)?.props?.modelValue)
// console.log((vnode.children[0] as any)?.component?.subTree)
// console.log((vnode.children[0] as any)?.component?.subTree.children[0])
return undefined
}
nextTick(() => {
const dialogVnode = vnode.children[0].component
// const rootEl = dialogVnode.subTree.children[0].el as HTMLElement //luoyifan element-ui 2.9 升级
const rootEl = dialogVnode.subTree.component.subTree.children[0].el.nextElementSibling as HTMLElement
if (rootEl.dataset.initialized === 'ok') return undefined
rootEl.dataset.initialized = 'ok'
const dialogOverlay: HTMLElement = rootEl.querySelector('.el-overlay-dialog')!
// 隐藏element这个奇怪设计导致拉伸太大而出现滚动条
dialogOverlay.style.overflow = 'hidden'
const dialogEl: HTMLElement = rootEl.querySelector('div.el-dialog')!
dialogEl.style.display = 'flex'
dialogEl.style.flexDirection = 'column'
const dialogBodyEl: HTMLElement = dialogEl.querySelector('.el-dialog__body')!
dialogBodyEl.style.flexGrow = '1'
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')!
const isEnableResizer = computed(() => true)
const isDraggable = computed(() => binding.value?.draggable ?? false)
if (binding.value?.closeIsHidden) {
watchEffect(() => {
const isShow = binding.value?.isShowFn()
if (isShow) {
dialogEl.style.display = 'flex'
rootEl.style.display = 'block'
} else {
dialogEl.style.display = 'none'
rootEl.style.display = 'none'
}
})
}
useCssVariable(dialogEl)
useResizer(dialogEl, isEnableResizer, beforeMountedFun)
useDraggable(dialogEl, dialogHeaderEl, isDraggable, beforeMountedFun)
if (binding.value?.fullscreen) useFullscreen(dialogEl, dialogVnode, binding.value)
})
}
/**
* element-plus dialog组件大小缩放功能
* 使ElDialog自带的 draggable fullscreen
* @param draggable Boolean
* @example
* <div v-element-dialog-resize="{ draggable: true, fullscreen: true }">
* <el-dialog v-model="dialogVisible" title="Tips" width="30%">
* </el-dialog>
* </div>
*/
export default {
mounted: handle,
updated: handle,
beforeUnmount: () => {
for (const fun of beforeMountedFun) {
if (typeof fun === 'function') {
fun()
}
}
},
}

45
src/components/element-dialog-resize/use-css-variable.ts

@ -0,0 +1,45 @@
import {watch, reactive} from 'vue'
import {useElementBounding} from '@vueuse/core'
import {addUnit} from './use-draggable'
export default function useCssVariable(dialogEl: HTMLElement) {
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')!
{
const dialogHeaderRect = useElementBounding(dialogHeaderEl)
watch(
() => dialogHeaderRect.height,
(height) => {
requestAnimationFrame(() =>
dialogEl.style.setProperty('--el-dialog-header-height', addUnit(height.value)!)
)
},
{immediate: true}
)
}
const dialogFooterEl: HTMLElement | null = dialogEl.querySelector('footer.el-dialog__footer')
if (dialogFooterEl) {
const dialogFooterRect = useElementBounding(dialogFooterEl)
watch(
() => dialogFooterRect.height,
(height) => {
requestAnimationFrame(() =>
dialogEl.style.setProperty('--el-dialog-footer-height', addUnit(height.value)!)
)
},
{immediate: true}
)
}
const dialogRect = useElementBounding(dialogEl)
watch(
() => reactive([dialogRect.width.value, dialogRect.height.value]),
([width, height]) => {
requestAnimationFrame(() => {
dialogEl.style.setProperty('--el-dialog-width', addUnit(width)!)
dialogEl.style.setProperty('--el-dialog-height', addUnit(height)!)
})
},
{immediate: true, deep: true}
)
}

85
src/components/element-dialog-resize/use-draggable.ts

@ -0,0 +1,85 @@
import {watchEffect, unref} from 'vue'
import {isNumber, isString} from 'lodash'
import type {ComputedRef, Ref} from 'vue'
import useParseTranslate from './use-parse-translate'
export function addUnit(value?: string | number, defaultUnit = 'px') {
if (!value) return ''
if (isString(value)) {
return value
} else if (isNumber(value)) {
return `${value}${defaultUnit}`
}
}
const useDraggable = (
targetRef: Ref<HTMLElement | undefined> | HTMLElement,
dragRef: Ref<HTMLElement | undefined> | HTMLElement,
draggable: ComputedRef<boolean>,
beforeMountedFun: Array<(...args: any) => any>
) => {
const targetRef_ = unref(targetRef)!
const dragRef_ = unref(dragRef)!
const onMousedown = (e: MouseEvent) => {
const downX = e.clientX
const downY = e.clientY
const {x: offsetX, y: offsetY} = useParseTranslate(targetRef_!.style.transform)
const targetRect = targetRef_!.getBoundingClientRect()
const targetLeft = targetRect.left
const targetTop = targetRect.top
const targetWidth = targetRect.width
const targetHeight = targetRect.height
const clientWidth = document.documentElement.clientWidth
const clientHeight = document.documentElement.clientHeight
const minLeft = -targetLeft + offsetX
const minTop = -targetTop + offsetY
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX
const maxTop = clientHeight - targetTop - targetHeight + offsetY
const onMousemove = (e: MouseEvent) => {
requestAnimationFrame(() => {
const moveX = Math.min(Math.max(offsetX + e.clientX - downX, minLeft), maxLeft)
const moveY = Math.min(Math.max(offsetY + e.clientY - downY, minTop), maxTop)
targetRef_!.style.transform = `translate(${addUnit(moveX)}, ${addUnit(moveY)})`
})
}
const onMouseup = () => {
document.removeEventListener('mousemove', onMousemove)
document.removeEventListener('mouseup', onMouseup)
}
document.addEventListener('mousemove', onMousemove)
document.addEventListener('mouseup', onMouseup)
}
const onDraggable = () => {
if (dragRef_ && targetRef_) {
targetRef_.className = `${targetRef_.className} is-draggable`
dragRef_.addEventListener('mousedown', onMousedown)
}
}
const offDraggable = () => {
if (dragRef_ && targetRef_) {
targetRef_.className = targetRef_.className.replace('is-draggable', '')
dragRef_.removeEventListener('mousedown', onMousedown)
}
}
watchEffect(() => {
if (draggable.value) {
onDraggable()
} else {
offDraggable()
}
})
beforeMountedFun.push(offDraggable)
}
export default useDraggable

82
src/components/element-dialog-resize/use-fullscreen.ts

@ -0,0 +1,82 @@
import { render, nextTick, h } from 'vue'
//@ts-ignore
import { FullScreen as IconFullScreen, Close as IconClose } from '@element-plus/icons-vue'
import { renderIcon } from '@/utils/webutils'
export default function useFullscreen(dialogEl: HTMLElement, dialogVnode: any, bindingValue: any) {
//@ts-ignore
const dialogHeaderEl: HTMLElement = dialogEl.querySelector('header.el-dialog__header')!
let allowDraggable = false
let widthBeforeFullscreen: number | undefined = undefined
function onToggleFullScreen() {
if (dialogEl.className.includes('is-draggable')) allowDraggable = true
if (!dialogVnode.props.fullscreen) {
// 准备切换全屏,记录当前宽度
widthBeforeFullscreen = dialogHeaderEl.getBoundingClientRect().width
}
if (dialogVnode.props.fullscreen) {
if (!widthBeforeFullscreen) return undefined
// 准备缩小,恢复记录的宽度
dialogEl.style.setProperty('--el-dialog-width', `${widthBeforeFullscreen}px`)
dialogEl.style.width = `${widthBeforeFullscreen}px`
widthBeforeFullscreen = undefined
}
dialogVnode.props.fullscreen = !dialogVnode.props.fullscreen
if (!dialogVnode.props.fullscreen && allowDraggable) {
// 从全屏变回窗口,要加上 is-draggable
nextTick(() => {
dialogEl.className = `${dialogEl.className} is-draggable`
})
}
}
function onCloseDialog() {
if (bindingValue.closeIsHidden) {
// 关闭只是隐藏,就抛出 close 事件
dialogVnode.emit('close')
} else {
dialogVnode.props.modelValue = false
}
}
dialogHeaderEl.ondblclick = onToggleFullScreen
const dialogHeaderButtonEl: HTMLElement | null =
dialogHeaderEl.querySelector('.el-dialog__headerbtn')
if (dialogHeaderButtonEl) {
dialogHeaderEl.removeChild(dialogHeaderButtonEl)
}
const customDialogHeaderButtonEl = document.createElement('div')
customDialogHeaderButtonEl.className = 'el-dialog__headerbtn'
customDialogHeaderButtonEl.style.display = 'flex'
customDialogHeaderButtonEl.style.alignItems = 'center'
customDialogHeaderButtonEl.style.width = 'auto'
customDialogHeaderButtonEl.style.marginRight = '5px'
const iconFullscreenContainer = document.createElement('div')
iconFullscreenContainer.className = 'fullbtn-container'
iconFullscreenContainer.onclick = onToggleFullScreen
const iconFullscreenVnode = h(renderIcon('FullScreen'))
// <IconFullScreen onClick={onToggleFullScreen}/>
render(iconFullscreenVnode, iconFullscreenContainer)
customDialogHeaderButtonEl.appendChild(iconFullscreenContainer)
const iconCloseContainer = document.createElement('div')
iconCloseContainer.className = 'closebtn-container'
iconCloseContainer.onclick = onCloseDialog
const iconCloseVnode = h(renderIcon('Close'))
// <IconClose onClick={onCloseDialog}/>
render(iconCloseVnode, iconCloseContainer)
customDialogHeaderButtonEl.appendChild(iconCloseContainer)
dialogHeaderEl.appendChild(customDialogHeaderButtonEl)
// 加入覆盖样式
const style = document.createElement('style')
style.type = 'text/css'
// --el-dialog-width: 100%!important;
style.innerHTML = '.el-dialog.is-fullscreen {width:100%!important;height:100%!important;}'
document.querySelector('head')!.appendChild(style)
}

9
src/components/element-dialog-resize/use-parse-translate.ts

@ -0,0 +1,9 @@
export default function useParseTranslate(translate: string | undefined) {
if (translate === '') return {x: 0, y: 0}
return {
x: translate ? Number(translate.slice(translate.indexOf('(') + 1, translate.indexOf('px'))) : 0,
y: translate
? Number(translate.slice(translate.indexOf(',') + 1, translate.indexOf(')') - 2))
: 0,
}
}

101
src/components/element-dialog-resize/use-resizer.ts

@ -0,0 +1,101 @@
import {watchEffect, unref} from 'vue'
import type {ComputedRef, Ref} from 'vue'
import useParseTranslate from './use-parse-translate'
import {addUnit} from './use-draggable'
export default function useResizer(
dialogRef: Ref<HTMLElement | undefined> | HTMLElement,
isEnable: ComputedRef<boolean>,
beforeMountedFun: Array<(...args: any) => any>
) {
const dialogEl = unref(dialogRef)!
// dialogEl.style.setProperty('--el-dialog-width', addUnit(dialogEl.getBoundingClientRect().width)!)
// dialogEl.style.setProperty(
// '--el-dialog-height',
// addUnit(dialogEl.getBoundingClientRect().height)!
// )
const resizerEl = document.createElement('div')
resizerEl.className = 'el-dialog-resizer'
resizerEl.style.width = '15px'
resizerEl.style.height = '15px'
resizerEl.style.zIndex = '3000'
resizerEl.style.background = 'transparent'
resizerEl.style.position = 'absolute'
resizerEl.style.bottom = '0'
resizerEl.style.right = '0'
resizerEl.style.cursor = 'se-resize'
let dialogDefaultRect: DOMRect | undefined = undefined
let resizerDefaultRect: DOMRect | undefined = undefined
const mouseOffsetInResizer = {x: 0, y: 0}
let dialogDefaultTranslate: string | undefined = undefined
function onMouseMove(ev: MouseEvent) {
requestAnimationFrame(() => {
const deltaWidth = ev.clientX - dialogDefaultRect!.right + mouseOffsetInResizer.x
const deltaHeight = ev.clientY - dialogDefaultRect!.bottom + mouseOffsetInResizer.y
const {x: translateX, y: translateY} = useParseTranslate(dialogDefaultTranslate)
const newWidth = `${dialogDefaultRect!.width + deltaWidth}px`
const newHeight = `${dialogDefaultRect!.height + deltaHeight}px`
// dialogEl.style.setProperty('--el-dialog-width', newWidth)
// dialogEl.style.setProperty('--el-dialog-height', newHeight)
dialogEl.style.width = newWidth
dialogEl.style.height = newHeight
dialogEl.style.transform = `translate(${translateX + deltaWidth / 2}px,${translateY}px)`
})
}
const onMouseDown = (ev: MouseEvent) => {
document.body.style.userSelect = 'none'
document.body.style.cursor = 'se-resize'
dialogDefaultRect = dialogEl.getBoundingClientRect()
resizerDefaultRect = resizerEl.getBoundingClientRect()
mouseOffsetInResizer.x = resizerDefaultRect.right - ev.clientX
mouseOffsetInResizer.y = resizerDefaultRect.bottom - ev.clientY
dialogDefaultTranslate = dialogEl.style.transform === '' ? undefined : dialogEl.style.transform
function onMouseUp() {
requestAnimationFrame(() => {
dialogDefaultRect = undefined
resizerDefaultRect = undefined
dialogDefaultTranslate = undefined
document.body.style.userSelect = ''
document.body.style.cursor = ''
})
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onResizer = () => {
if (resizerEl && dialogEl) {
resizerEl.addEventListener('mousedown', onMouseDown)
dialogEl.appendChild(resizerEl)
}
}
const offResizer = () => {
if (resizerEl && dialogEl) {
resizerEl.removeEventListener('mousedown', onMouseDown)
try {
dialogEl.removeChild(resizerEl)
} catch (e) {
}
}
}
watchEffect(() => {
if (isEnable.value) {
onResizer()
} else {
offResizer()
}
})
beforeMountedFun.push(offResizer)
}

194
src/designer/Model3DView.vue

@ -0,0 +1,194 @@
<template>
<div class="model3d-view">
<div class="toolbar">
<input type="file" @change="handleFileUpload" accept=".fbx,.obj,.mtl" />
<span>文件上传</span>
</div>
<div class="main-content">
<!-- Three.js 渲染画布 -->
<div class="canvas-container" ref="canvasContainer"></div>
<!-- 右侧面板 -->
<div class="gui-panel" ref="guiPanel"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js'
// DOM refs
const canvasContainer = ref(null)
const guiPanel = ref(null)
// Three.js
let scene, camera, renderer, controls
let modelGroup = new THREE.Group()
let gui
//
const state = {
autoRotate: false,
showAxesHelper: true,
showGridHelper: true,
cameraPosition: [0, 5, 10]
}
onMounted(() => {
initThree()
initGUI()
})
function initThree() {
//
scene = new THREE.Scene()
scene.background = new THREE.Color(0xeeeeee)
//
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / 2,
0.1,
1000
)
camera.position.set(...state.cameraPosition)
//
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8)
canvasContainer.value.appendChild(renderer.domElement)
//
controls = new OrbitControls(camera, renderer.domElement)
// 线
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20)
scene.add(gridHelper)
//
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 10, 10)
scene.add(directionalLight)
//
function animate() {
requestAnimationFrame(animate)
if (state.autoRotate) {
modelGroup.rotation.y += 0.01
}
controls.update()
renderer.render(scene, camera)
}
animate()
//
window.addEventListener('resize', () => {
camera.aspect = canvasContainer.value.clientWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8)
})
}
function initGUI() {
gui = new dat.GUI({ autoPlace: false })
guiPanel.value.appendChild(gui.domElement)
//
gui.add(state, 'autoRotate').name('自动旋转')
// 线
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => {
scene.getObjectByName('AxesHelper').visible = val
})
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => {
scene.getObjectByName('GridHelper').visible = val
})
//
const cameraFolder = gui.addFolder('相机位置')
cameraFolder.add(state.cameraPosition, 0, -10, 10).name('X')
cameraFolder.add(state.cameraPosition, 1, -10, 10).name('Y')
cameraFolder.add(state.cameraPosition, 2, 5, 30).name('Z').onChange(() => {
camera.position.set(...state.cameraPosition)
})
cameraFolder.open()
}
function handleFileUpload(event) {
const file = event.target.files[0]
if (!file) return
//
if (modelGroup.children.length > 0) {
modelGroup.children.forEach(child => modelGroup.remove(child))
}
const fileName = file.name.toLowerCase()
const reader = new FileReader()
if (fileName.endsWith('.fbx')) {
reader.readAsArrayBuffer(file)
reader.onload = () => {
const loader = new FBXLoader()
const content = loader.parse(reader.result, '')
modelGroup.add(content)
scene.add(modelGroup)
}
} else if (fileName.endsWith('.obj')) {
reader.readAsText(file)
reader.onload = () => {
const loader = new OBJLoader()
const content = loader.parse(reader.result)
modelGroup.add(content)
scene.add(modelGroup)
}
} else if (fileName.endsWith('.mtl')) {
alert('需要同时上传 .obj 和 .mtl 文件,请先实现多文件上传处理逻辑。')
} else {
alert('不支持的文件类型!')
}
}
</script>
<style scoped>
.dialog-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
padding: 10px;
background-color: #f0f0f0;
border-bottom: 1px solid #ccc;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.canvas-container {
flex: 3;
position: relative;
}
.gui-panel {
flex: 1;
border-left: 1px solid #ccc;
background: #fff;
padding: 10px;
overflow-y: auto;
}
</style>

26
src/designer/menus/Model3DView.ts

@ -0,0 +1,26 @@
import { defineMenu } from '@/runtime/DefineMenu.ts'
import Model3DView from '@/designer/Model3DView.vue'
export default defineMenu((menus) => {
menus.insertChildren('tool',
{
name: 'tool', label: '小工具', order: 3, disabled: false
},
[
{
name: 'model3dview', label: '模型查看器', order: 1,
click: () => {
system.showDialog(Model3DView, {
title: '模型查看器',
width: 800,
height: 400,
showClose: true,
showMax: true,
showCancelButton: false,
showOkButton: false
})
}
}
]
)
})

11
src/designer/menus/Tools.ts

@ -0,0 +1,11 @@
import { defineMenu } from '@/runtime/DefineMenu.ts'
import { renderIcon } from '@/utils/webutils.ts'
export default defineMenu((menus) => {
menus.insertChildren('file',
{
name: 'tool', label: '小工具', order: 3, disabled: false
},
[]
)
})

59
src/runtime/System.ts

@ -3,11 +3,12 @@ import _ from 'lodash'
import localforage from 'localforage'
import JSON5 from 'json5'
import hotkeys from 'hotkeys-js'
import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp } from 'vue'
import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp, type Component } from 'vue'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { renderIcon } from '@/utils/webutils.ts'
import type { showDialogOption } from '@/SystemOption'
import ShowDialogWrap from '@/components/ShowDialogWrap.vue'
export default class System {
_ = _
@ -31,6 +32,11 @@ export default class System {
errorDialogContent: string[] = reactive([])
errorDialogIsShowing: boolean = false
/**
*
*/
rootElementList: { cmp: Component, props: any }[] = reactive([])
constructor(app: App) {
this.app = app
}
@ -188,4 +194,55 @@ export default class System {
})
})
}
showDialog(childCmp: Component, param: ShowDialogOption = {}) {
return new Promise<any>((resolve, reject) => {
const fullProps: any = {
title: '未命名对话框',
draggable: true,
width: 800,
height: 600,
showClose: true,
showMax: true,
modal: true,
dialogClass: '',
closeOnClickModal: false,
closeOnPressEscape: true,
_insId: _.uniqueId('tmp_dlg_'),
showCancelButton: true,
showOkButton: true,
cancelButtonText: '取消',
okButtonText: '确定',
...param,
dialogResolve: resolve,
dialogReject: reject,
childCmp: markRaw(childCmp),
_input: param.data || {}
}
system.rootElementList.push({
cmp: h(ShowDialogWrap, fullProps),
props: fullProps
})
})
}
}
export interface ShowDialogOption {
title?: string
draggable?: boolean
width?: number
height?: number
showClose?: boolean
showMax?: boolean
modal?: boolean
dialogClass?: string
closeOnClickModal?: boolean
closeOnPressEscape?: boolean
data?: any
showCancelButton?: boolean
showOkButton?: boolean
cancelButtonText?: string
okButtonText?: string
}

4
src/views/ModelMainInit.ts

@ -11,6 +11,8 @@ import ToolboxMeta from '@/designer/viewWidgets/toolbox/ToolboxMeta'
import FileMenu from '@/designer/menus/FileMenu.ts'
import EditMenu from '@/designer/menus/EditMenu.ts'
import ToolsMenu from '@/designer/menus/Tools.ts'
import Model3DView from '@/designer/menus/Model3DView.ts'
import { forEachMenu } from '@/runtime/DefineMenu.ts'
import { normalizeShortKey } from '@/utils/webutils.ts'
@ -29,6 +31,8 @@ export function ModelMainInit() {
FileMenu.install()
EditMenu.install()
ToolsMenu.install()
Model3DView.install()
// hotkeys('ctrl+s', (event) => {
// system.msg('ctrl+s')

19
vite.config.ts

@ -2,22 +2,31 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
// vueJsx(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 7791,
open: false,
proxy: {
}
},
optimizeDeps: {
include: ['lodash', 'axios', 'three', 'dat.gui'],
},
include: [
'lodash', 'axios', 'three', 'dat.gui',
'element-plus', 'ag-grid-community', 'ag-grid-enterprise', 'ag-grid-vue3',
'codemirror'
]
}
})

2220
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save