feat(All):Initial
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<n-dropdown v-bind="attrs" :x="x" :y="y" :options="options" :show="visible" :on-clickoutside="onClickoutside" @select="handleSelect" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAttrs,ref } from "vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
options: any[];
|
||||
}>(), {
|
||||
options:[] as any,
|
||||
})
|
||||
const emits = defineEmits(['select']);
|
||||
|
||||
const attrs = useAttrs();
|
||||
const visible = ref(false);
|
||||
const x = ref(0);
|
||||
const y = ref(0);
|
||||
|
||||
function onClickoutside(){
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleSelect(key:string){
|
||||
emits('select',key)
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function show(_x,_y){
|
||||
x.value = _x;
|
||||
y.value = _y;
|
||||
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
visible:boolean
|
||||
}>(),{
|
||||
visible:false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col justify-center items-center bg-#1D82B8 relative z-9999" v-if="visible">
|
||||
<div class="es-folding-cube bg-#fff">
|
||||
<div class="es-cube1 es-cube"></div>
|
||||
<div class="es-cube2 es-cube"></div>
|
||||
<div class="es-cube4 es-cube"></div>
|
||||
<div class="es-cube3 es-cube"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.es-folding-cube {
|
||||
margin: 20px auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
-webkit-transform: rotateZ(45deg);
|
||||
transform: rotateZ(45deg);
|
||||
}
|
||||
|
||||
.es-folding-cube .es-cube {
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
position: relative;
|
||||
-webkit-transform: scale(1.1);
|
||||
-ms-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.es-folding-cube .es-cube:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1D82B8;
|
||||
-webkit-animation: es-foldCubeAngle 2.4s infinite linear both;
|
||||
animation: es-foldCubeAngle 2.4s infinite linear both;
|
||||
-webkit-transform-origin: 100% 100%;
|
||||
-ms-transform-origin: 100% 100%;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
.es-folding-cube .es-cube2 {
|
||||
-webkit-transform: scale(1.1) rotateZ(90deg);
|
||||
transform: scale(1.1) rotateZ(90deg);
|
||||
}
|
||||
.es-folding-cube .es-cube3 {
|
||||
-webkit-transform: scale(1.1) rotateZ(180deg);
|
||||
transform: scale(1.1) rotateZ(180deg);
|
||||
}
|
||||
.es-folding-cube .es-cube4 {
|
||||
-webkit-transform: scale(1.1) rotateZ(270deg);
|
||||
transform: scale(1.1) rotateZ(270deg);
|
||||
}
|
||||
.es-folding-cube .es-cube2:before {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.es-folding-cube .es-cube3:before {
|
||||
-webkit-animation-delay: 0.6s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
.es-folding-cube .es-cube4:before {
|
||||
-webkit-animation-delay: 0.9s;
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
@-webkit-keyframes es-foldCubeAngle {
|
||||
0%, 10% {
|
||||
-webkit-transform: perspective(140px) rotateX(-180deg);
|
||||
transform: perspective(140px) rotateX(-180deg);
|
||||
opacity: 0;
|
||||
} 25%, 75% {
|
||||
-webkit-transform: perspective(140px) rotateX(0deg);
|
||||
transform: perspective(140px) rotateX(0deg);
|
||||
opacity: 1;
|
||||
} 90%, 100% {
|
||||
-webkit-transform: perspective(140px) rotateY(180deg);
|
||||
transform: perspective(140px) rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes es-foldCubeAngle {
|
||||
0%, 10% {
|
||||
-webkit-transform: perspective(140px) rotateX(-180deg);
|
||||
transform: perspective(140px) rotateX(-180deg);
|
||||
opacity: 0;
|
||||
} 25%, 75% {
|
||||
-webkit-transform: perspective(140px) rotateX(0deg);
|
||||
transform: perspective(140px) rotateX(0deg);
|
||||
opacity: 1;
|
||||
} 90%, 100% {
|
||||
-webkit-transform: perspective(140px) rotateY(180deg);
|
||||
transform: perspective(140px) rotateY(180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<n-alert type="success" :show-icon="false" closable class="mb-2">
|
||||
<div class="flex items-center">
|
||||
<n-icon :size="20">
|
||||
<Link />
|
||||
</n-icon>
|
||||
<a :href="url" target="_blank" class="ml-2">{{ t("other.Related document") }}</a>
|
||||
</div>
|
||||
</n-alert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {t} from "@/language";
|
||||
import {Link} from "@vicons/carbon";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
url: string
|
||||
}>(), {
|
||||
url: "",
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import {Help} from '@vicons/carbon';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
label:string
|
||||
}>(),{
|
||||
label:""
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end">
|
||||
<span>{{ label }}</span>
|
||||
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-icon :size="16" :depth="3" class="ml-5px mb-1px cursor-pointer">
|
||||
<Help />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<slot></slot>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="es-input flex items-center w-full">
|
||||
<label v-if="label">{{ label }}</label>
|
||||
<div v-if="defaultNoBorder && noFocus" @click="handleClickText" class="w-full">{{value}}</div>
|
||||
<n-input v-else ref="inRef" :value="value" @input="handleInput" @change="handleChange" @blur="noFocus = true" type="text" v-bind="attrs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useAttrs,ref,nextTick} from "vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
value: string,
|
||||
label: string,
|
||||
defaultNoBorder: boolean,
|
||||
}>(), {
|
||||
value: '',
|
||||
label: '',
|
||||
defaultNoBorder: false
|
||||
})
|
||||
const emits = defineEmits(["update:value","change"])
|
||||
|
||||
const attrs = useAttrs();
|
||||
const inRef = ref();
|
||||
const noFocus = ref(true);
|
||||
|
||||
async function handleClickText(){
|
||||
noFocus.value = false;
|
||||
await nextTick();
|
||||
inRef.value.focus();
|
||||
}
|
||||
|
||||
function handleInput(value: string) {
|
||||
emits("update:value", value);
|
||||
}
|
||||
|
||||
function handleChange(value: string) {
|
||||
emits("change", value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<n-input-number ref="inRef" v-model:value="esNumber" :step="step" :min="min" :max="max" v-bind="attrs"
|
||||
@update:value="handlerChange" @mousedown.stop="onMouseDown">
|
||||
<template #prefix v-if="label">
|
||||
<n-text>
|
||||
{{ label }} :
|
||||
</n-text>
|
||||
</template>
|
||||
|
||||
<template #suffix v-if="unit !== null">
|
||||
<n-text type="success">
|
||||
{{ unit }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-input-number>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, useAttrs, onMounted, watch,computed} from "vue";
|
||||
import {NInputNumber} from 'naive-ui';
|
||||
import {Utils} from "@astral3d/engine";
|
||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||
|
||||
const globalConfigStore = useGlobalConfigStore();
|
||||
const primaryColor = computed(() => (globalConfigStore.mainColor as IConfig.Color).hex);
|
||||
|
||||
const attrs = useAttrs();
|
||||
let props = withDefaults(defineProps<{
|
||||
value: number,
|
||||
label?: string | null,
|
||||
unit?: string | null,
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?:number,
|
||||
//保留小数位
|
||||
decimal?: number
|
||||
}>(), {
|
||||
value: 0,
|
||||
min: -Infinity,
|
||||
max: Infinity,
|
||||
decimal: 0
|
||||
})
|
||||
const emits = defineEmits(["update:value", "change"])
|
||||
|
||||
const inRef = ref();
|
||||
const step = ref(props.step);
|
||||
const esNumber = ref(0);
|
||||
|
||||
function handlerChange(value: number) {
|
||||
if (value === null) {
|
||||
esNumber.value = props.value;
|
||||
return;
|
||||
}
|
||||
value = parseFloat(value.toFixed(props.decimal));
|
||||
|
||||
emits("update:value", value);
|
||||
emits("change", value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
esNumber.value = props.value;
|
||||
|
||||
if (props.decimal !== 0 && Utils.isNil(step.value)) {
|
||||
step.value = Number(`${Number(0).toFixed(props.decimal - 1)}1`);
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.value, (newVal) => {
|
||||
esNumber.value = newVal;
|
||||
})
|
||||
|
||||
let distance = 0;
|
||||
let onMouseDownValue = 0;
|
||||
const pointer = {x: 0, y: 0};
|
||||
const prevPointer = {x: 0, y: 0};
|
||||
|
||||
function onMouseDown(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if(!Number.isFinite(esNumber.value) || esNumber.value === undefined || esNumber.value === null) return;
|
||||
|
||||
distance = 0;
|
||||
onMouseDownValue = esNumber.value;
|
||||
prevPointer.x = event.clientX;
|
||||
prevPointer.y = event.clientY;
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
pointer.x = event.clientX;
|
||||
pointer.y = event.clientY;
|
||||
|
||||
distance += (pointer.x - prevPointer.x) - (pointer.y - prevPointer.y);
|
||||
|
||||
let value = onMouseDownValue + (distance / (event.shiftKey ? 5 : 50)) * (step.value || 1);
|
||||
value = Math.min(props.max, Math.max(props.min, value));
|
||||
|
||||
if (onMouseDownValue !== value && value !== null) {
|
||||
value = parseFloat(value.toFixed(props.decimal));
|
||||
|
||||
esNumber.value = value;
|
||||
handlerChange(value);
|
||||
}
|
||||
|
||||
prevPointer.x = pointer.x;
|
||||
prevPointer.y = pointer.y;
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-input-number {
|
||||
:deep(.n-input) {
|
||||
cursor: ns-resize;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
|
||||
.n-input-wrapper {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.n-input__suffix{
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.n-input__border{
|
||||
display: none;
|
||||
}
|
||||
.n-input__state-border{
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-input__input-el){
|
||||
color: v-bind(primaryColor) !important;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import {ConditionPoint} from '@vicons/carbon';
|
||||
import {t} from "@/language";
|
||||
import {useAnimationStore} from "@/store/modules/animation";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
label: string,
|
||||
attr: string
|
||||
}>(), {
|
||||
label: "",
|
||||
attr: ""
|
||||
})
|
||||
|
||||
const {addKeyframe} = useAnimationStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="flex items-center">
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="addKeyframe(attr)">
|
||||
<template #icon>
|
||||
<n-icon size="12">
|
||||
<ConditionPoint/>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ t("extra.Add keyframe") }}
|
||||
</n-tooltip>
|
||||
<label>{{ label }}</label>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<n-select size="small" v-bind="attrs" v-model:value="selected" @update:value="update" :options="options"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref,useAttrs} from "vue";
|
||||
import {t} from "@/language";
|
||||
|
||||
const props =withDefaults(defineProps<{
|
||||
value: string
|
||||
}>(), {
|
||||
value: ''
|
||||
})
|
||||
const emits = defineEmits(["update:value"])
|
||||
|
||||
const attrs = useAttrs();
|
||||
const selected = ref(props.value);
|
||||
const options = [
|
||||
{ label: t('layout.sider.particle.Ease linear'), value: 'easeLinear' },
|
||||
{ label: t('layout.sider.particle.Ease in quad'), value: 'easeInQuad' },
|
||||
{ label: t('layout.sider.particle.Ease out quad'), value: 'easeOutQuad' },
|
||||
{ label: t('layout.sider.particle.Ease in out quad'), value: 'easeInOutQuad' },
|
||||
{ label: t('layout.sider.particle.Ease in cubic'), value: 'easeInCubic' },
|
||||
{ label: t('layout.sider.particle.Ease out cubic'), value: 'easeOutCubic' },
|
||||
{ label: t('layout.sider.particle.Ease in out cubic'), value: 'easeInOutCubic' },
|
||||
{ label: t('layout.sider.particle.Ease in quart'), value: 'easeInQuart' },
|
||||
{ label: t('layout.sider.particle.Ease out quart'), value: 'easeOutQuart' },
|
||||
{ label: t('layout.sider.particle.Ease in out quart'), value: 'easeInOutQuart' },
|
||||
{ label: t('layout.sider.particle.Ease in sine'), value: 'easeInSine' },
|
||||
{ label: t('layout.sider.particle.Ease out sine'), value: 'easeOutSine' },
|
||||
{ label: t('layout.sider.particle.Ease in out sine'), value: 'easeInOutSine' },
|
||||
{ label: t('layout.sider.particle.Ease in expo'), value: 'easeInExpo' },
|
||||
{ label: t('layout.sider.particle.Ease out expo'), value: 'easeOutExpo' },
|
||||
{ label: t('layout.sider.particle.Ease in out expo'), value: 'easeInOutExpo' },
|
||||
{ label: t('layout.sider.particle.Ease in circ'), value: 'easeInCirc' },
|
||||
{ label: t('layout.sider.particle.Ease out circ'), value: 'easeOutCirc' },
|
||||
{ label: t('layout.sider.particle.Ease in out circ'), value: 'easeInOutCirc' },
|
||||
{ label: t('layout.sider.particle.Ease in back'), value: 'easeInBack' },
|
||||
{ label: t('layout.sider.particle.Ease out back'), value: 'easeOutBack' },
|
||||
{ label: t('layout.sider.particle.Ease in out back'), value: 'easeInOutBack' }
|
||||
];
|
||||
|
||||
function update(value: string) {
|
||||
emits("update:value", value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue';
|
||||
import {useThemeVars} from 'naive-ui'
|
||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||
import {App} from "@astral3d/engine";
|
||||
import Logo from "@/components/header/Logo.vue";
|
||||
import EsPluginDialog from "@/components/es/plugin/EsPluginDialog.vue";
|
||||
|
||||
const globalConfigStore = useGlobalConfigStore();
|
||||
const primaryColor = computed(() => (globalConfigStore.mainColor as IConfig.Color).hex);
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
const baseColor = computed(() => themeVars.value.baseColor);
|
||||
|
||||
const toolsDialogVisible = ref(false);
|
||||
const pluginRef = ref();
|
||||
const x = ref(315);
|
||||
const y = ref(document.body.clientHeight - 80);
|
||||
const cursor = ref("pointer");
|
||||
|
||||
// 是否正在拖动的标志
|
||||
const isDragging = ref(false);
|
||||
|
||||
function startDrag(downEvent: MouseEvent) {
|
||||
const clientHeight = (document.documentElement.clientHeight || document.body.clientHeight) - 25;
|
||||
const clientWidth = (document.documentElement.clientWidth || document.body.clientWidth) - 25;
|
||||
|
||||
const lastClientX = downEvent.clientX;
|
||||
const lastClientY = downEvent.clientY;
|
||||
const dragging = (moveEvent: MouseEvent) => {
|
||||
if (Math.abs(lastClientX - moveEvent.clientX) > 1 || Math.abs(lastClientY - moveEvent.clientY) > 1) {
|
||||
isDragging.value = true;
|
||||
cursor.value = "grab";
|
||||
|
||||
let boxLeft = moveEvent.clientX;
|
||||
let boxTop = moveEvent.clientY;
|
||||
|
||||
if (boxLeft < 25) {
|
||||
boxLeft = 25;
|
||||
} else if (boxLeft > clientWidth) {
|
||||
boxLeft = clientWidth;
|
||||
}
|
||||
|
||||
if (boxTop < 25) {
|
||||
boxTop = 25;
|
||||
} else if (boxTop > clientHeight) {
|
||||
boxTop = clientHeight;
|
||||
}
|
||||
|
||||
x.value = boxLeft - 25;
|
||||
y.value = boxTop - 25;
|
||||
}
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
if (!isDragging.value) {
|
||||
handleClick()
|
||||
}
|
||||
|
||||
App.storage.setOtherItem("es-plugins-position", {
|
||||
x: x.value,
|
||||
y: y.value
|
||||
})
|
||||
|
||||
isDragging.value = false;
|
||||
cursor.value = "pointer";
|
||||
|
||||
document.removeEventListener('pointermove', dragging);
|
||||
document.removeEventListener('pointerup', stopDrag);
|
||||
}
|
||||
|
||||
// 添加鼠标移动和释放时的事件监听器
|
||||
document.addEventListener('pointermove', dragging);
|
||||
document.addEventListener('pointerup', stopDrag);
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
toolsDialogVisible.value = !toolsDialogVisible.value;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const xy = await App.storage.getOtherItem("es-plugins-position") as {x:number,y:number};
|
||||
if (xy) {
|
||||
x.value = xy.x > document.body.clientWidth ? 315 : xy.x;
|
||||
y.value = xy.y > document.body.clientHeight ? document.body.clientHeight - 120 : xy.y;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="pluginRef" class="es-tools w-40px h-40px fixed z-9000 flex justify-center items-center"
|
||||
:style="{ left: `${x}px`, top: `${y}px`,cursor:`${cursor}` }"
|
||||
@pointerdown="startDrag">
|
||||
<Logo class="w-26px h-26px"/>
|
||||
</div>
|
||||
|
||||
<EsPluginDialog v-model:visible="toolsDialogVisible"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.es-tools {
|
||||
border: 4px solid transparent;
|
||||
border-radius: 50%;
|
||||
background-clip: padding-box, border-box;
|
||||
background-origin: padding-box, border-box;
|
||||
background-image: linear-gradient(to right, v-bind(baseColor), v-bind(baseColor)), linear-gradient(120deg, v-bind(primaryColor), #578AEF);
|
||||
transition: background-image .5s;
|
||||
|
||||
& > svg {
|
||||
transition: transform .5s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: rotate(45deg);
|
||||
|
||||
& > svg {
|
||||
transform: rotate(45deg) scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, nextTick} from 'vue';
|
||||
import type {TreeOption} from 'naive-ui';
|
||||
import {CaretRight, CaretDown} from '@vicons/carbon';
|
||||
import {findTreeNode, markLeafNodes} from "@/utils/common/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
trigger?: 'hover' | 'click' | 'focus' | 'manual',
|
||||
options: TreeOption[],
|
||||
value: string | number
|
||||
}>(), {
|
||||
trigger: "hover",
|
||||
options: () => [],
|
||||
value: ""
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:value', "select", "show","click"]);
|
||||
|
||||
const selectKey = ref(props.value);
|
||||
const selectOption = ref<TreeOption | null>(null);
|
||||
const treeShow = ref(false);
|
||||
const treeNodeProps = ({ option }: { option: TreeOption }) => {
|
||||
return {
|
||||
onClick() {
|
||||
emits("click", option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
markLeafNodes(props.options);
|
||||
|
||||
selectOption.value = findTreeNode(props.options, selectKey.value);
|
||||
})
|
||||
|
||||
function handleTreeShowChange(show: boolean) {
|
||||
treeShow.value = show;
|
||||
|
||||
emits("show", show);
|
||||
}
|
||||
|
||||
function handleTreeSelect(keys: Array<string | number>) {
|
||||
emits("update:value", keys[0]);
|
||||
|
||||
selectKey.value = keys[0];
|
||||
selectOption.value = findTreeNode(props.options, selectKey.value);
|
||||
|
||||
emits("select", selectOption.value, props.options);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-button v-bind="$attrs" @click="emits('click',selectOption)">
|
||||
<template #icon>
|
||||
<n-popover placement="bottom" :trigger="trigger" @update:show="handleTreeShowChange">
|
||||
<template #trigger>
|
||||
<n-icon>
|
||||
<CaretRight v-if="!treeShow"/>
|
||||
<CaretDown v-else/>
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<n-tree :data="options" block-line @update:selected-keys="handleTreeSelect"
|
||||
default-expand-all :default-selected-keys="[value]" :cancelable="false"
|
||||
:indent="18" :node-props="treeNodeProps"/>
|
||||
</n-popover>
|
||||
|
||||
</template>
|
||||
|
||||
{{ selectOption?.label || "" }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,262 @@
|
||||
<script lang="ts" setup>
|
||||
import {ref, onMounted, watch,nextTick} from "vue";
|
||||
import type {UploadFileInfo} from 'naive-ui';
|
||||
import * as THREE from 'three';
|
||||
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader.js';
|
||||
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
|
||||
import { Loader } from "@astral3d/engine";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
texture: THREE.Color | THREE.Texture | THREE.CubeTexture | null,
|
||||
mapping?: THREE.Mapping,
|
||||
width?: string,
|
||||
height?: string,
|
||||
disabled?: boolean
|
||||
}>(),{
|
||||
texture: null,
|
||||
mapping:THREE.Texture.DEFAULT_MAPPING,
|
||||
width: "2rem",
|
||||
height: "2rem",
|
||||
disabled:false
|
||||
})
|
||||
|
||||
const emits = defineEmits(["update:texture", "change"]);
|
||||
|
||||
watch(() => props.texture, () => {
|
||||
if(!props.texture) return;
|
||||
|
||||
setValue(props.texture)
|
||||
})
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
const file = ref<UploadFileInfo[]>([]);
|
||||
const uploadRef = ref();
|
||||
const canvasRef = ref();
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
if(props.texture){
|
||||
setValue(props.texture)
|
||||
}
|
||||
|
||||
canvasRef.value.addEventListener('drop', function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
loadFile(event.dataTransfer.files[0]);
|
||||
});
|
||||
})
|
||||
|
||||
function updateFileList(fList: UploadFileInfo[]) {
|
||||
//永远取最新值
|
||||
file.value = [fList[fList.length - 1]];
|
||||
|
||||
file.value[0].file !== null && loadFile(file.value[0].file as File);
|
||||
}
|
||||
|
||||
function canvasClick() {
|
||||
if(props.disabled) return;
|
||||
|
||||
//uploadRef.value.clear();
|
||||
uploadRef.value.openOpenFileDialog();
|
||||
}
|
||||
|
||||
function setValue(newTexture) {
|
||||
const context = canvasRef.value.getContext('2d');
|
||||
|
||||
// 如果画布不可见,则上下文似乎可以为空
|
||||
if (context) {
|
||||
// 始终在设置新纹理之前清除上下文,因为新纹理可能具有透明度
|
||||
context.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
||||
}
|
||||
|
||||
if (newTexture !== null) {
|
||||
const image = newTexture.image;
|
||||
if (image && image.width > 0) {
|
||||
const scale = canvasRef.value.width / image.width;
|
||||
|
||||
if ( newTexture.isDataTexture || newTexture.isCompressedTexture ) {
|
||||
const canvas2 = renderToCanvas(newTexture);
|
||||
context.drawImage( canvas2, 0, 0, image.width * scale, image.height * scale );
|
||||
} else {
|
||||
context.drawImage( image, 0, 0, image.width * scale, image.height * scale );
|
||||
}
|
||||
} else {
|
||||
canvasRef.value.title = newTexture.sourceFile + ' (error)';
|
||||
}
|
||||
|
||||
if (file.value.length === 0) {
|
||||
file.value = [newTexture];
|
||||
}
|
||||
|
||||
emits("update:texture", newTexture);
|
||||
} else {
|
||||
canvasRef.value.title = 'empty';
|
||||
uploadRef.value.clear();
|
||||
file.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadFile(file) {
|
||||
//文件后缀
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const reader = new FileReader();
|
||||
|
||||
const hash = `${file.lastModified}_${file.size}_${file.name}`;
|
||||
|
||||
if (cache.has(hash)) {
|
||||
const texture = cache.get(hash);
|
||||
|
||||
setValue(texture);
|
||||
emits("change", texture);
|
||||
} else if (extension === 'hdr' || extension === 'pic') {
|
||||
reader.addEventListener('load', function (event) {
|
||||
// 假设RGBE/Radiance HDR图像格式
|
||||
const loader = new RGBELoader();
|
||||
loader.load(event.target?.result as string, function (hdrTexture) {
|
||||
// @ts-ignore
|
||||
hdrTexture.sourceFile = file.name;
|
||||
|
||||
cache.set(hash, hdrTexture);
|
||||
|
||||
setValue(hdrTexture);
|
||||
emits("change", hdrTexture);
|
||||
});
|
||||
});
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
} else if (extension === 'tga') {
|
||||
reader.addEventListener('load', function (event) {
|
||||
const loader = new TGALoader();
|
||||
loader.load(event.target?.result as string, ( texture ) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
// @ts-ignore
|
||||
texture.sourceFile = file.name;
|
||||
|
||||
cache.set(hash, texture);
|
||||
|
||||
setValue(texture);
|
||||
emits("change", texture);
|
||||
});
|
||||
}, false);
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
} else if (extension === 'ktx2') {
|
||||
reader.addEventListener( 'load', function ( event ) {
|
||||
const arrayBuffer = event.target?.result as ArrayBuffer;
|
||||
const blobURL = URL.createObjectURL( new Blob([arrayBuffer]) );
|
||||
const ktx2Loader = Loader.ktx2Loader;
|
||||
|
||||
ktx2Loader.load(blobURL, function ( texture ) {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
// @ts-ignore
|
||||
texture.sourceFile = file.name;
|
||||
texture.needsUpdate = true;
|
||||
|
||||
cache.set(hash, texture);
|
||||
|
||||
setValue( texture );
|
||||
emits("change", texture);
|
||||
ktx2Loader.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
}else if (file.type.match('image.*')) {
|
||||
reader.addEventListener('load', function (event) {
|
||||
const image = document.createElement('img');
|
||||
image.addEventListener('load', function () {
|
||||
const texture = new THREE.Texture(this, props.mapping);
|
||||
// @ts-ignore
|
||||
texture.sourceFile = file.name;
|
||||
texture.needsUpdate = true;
|
||||
|
||||
cache.set(hash, texture);
|
||||
|
||||
setValue(texture);
|
||||
emits("change", texture);
|
||||
}, false);
|
||||
|
||||
image.src = event.target?.result as string;
|
||||
}, false);
|
||||
reader.readAsDataURL(file);
|
||||
}else if(extension === 'exr'){
|
||||
reader.addEventListener( 'load', ( event ) => {
|
||||
const arrayBuffer = event.target?.result as ArrayBuffer;
|
||||
const blobURL = URL.createObjectURL(new Blob([arrayBuffer]));
|
||||
|
||||
Loader.exrLoader.load(blobURL, (texture) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
// @ts-ignore
|
||||
texture.sourceFile = file.name;
|
||||
|
||||
cache.set(hash, texture);
|
||||
|
||||
setValue(texture);
|
||||
emits("change", texture);
|
||||
});
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function setColorSpace(colorSpace: string) {
|
||||
if (props.texture && !(props.texture instanceof THREE.Color)) {
|
||||
props.texture.colorSpace = colorSpace;
|
||||
}
|
||||
}
|
||||
|
||||
let renderer;
|
||||
|
||||
function renderToCanvas(texture) {
|
||||
if (renderer === undefined) {
|
||||
renderer = new THREE.WebGLRenderer();
|
||||
}
|
||||
|
||||
const image = texture.image;
|
||||
renderer.setSize(image.width, image.height, false);
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
const material = new THREE.MeshBasicMaterial({map: texture});
|
||||
const quad = new THREE.PlaneGeometry(2, 2);
|
||||
const mesh = new THREE.Mesh(quad, material);
|
||||
scene.add(mesh);
|
||||
renderer.render(scene, camera);
|
||||
return renderer.domElement;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setColorSpace, setValue
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="es-texture">
|
||||
<n-upload list-type="image-card" ref="uploadRef" v-show="file.length === 0" @update:file-list="updateFileList" :disabled="disabled" />
|
||||
<canvas class="es-texture-canvas" ref="canvasRef" v-show="file.length === 1" @click="canvasClick()" title="Texture"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.n-upload) {
|
||||
.n-upload-trigger.n-upload-trigger--image-card,
|
||||
.n-upload-file--image-card-type {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
}
|
||||
}
|
||||
|
||||
.es-texture {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
|
||||
&-canvas {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import {AlertCircleOutline} from '@vicons/ionicons5';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
size?: string,
|
||||
content?: string,
|
||||
trigger?: 'click' | 'hover' | 'focus' | 'manual',
|
||||
placement?: 'top-start' | 'top' | 'top-end' | 'right-start' | 'right' | 'right-end' | 'bottom-start' | 'bottom' | 'bottom-end' | 'left-start' | 'left' | 'left-end'
|
||||
}>(), {
|
||||
size: '18',
|
||||
content: '',
|
||||
trigger: "hover",
|
||||
placement: 'top'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="flex items-center justify-center cursor-pointer">
|
||||
<span v-if="content" class="mr-2px">{{ content }}</span>
|
||||
<n-tooltip :trigger="trigger" :placement="placement">
|
||||
<template #trigger>
|
||||
<n-icon :size="size">
|
||||
<AlertCircleOutline/>
|
||||
</n-icon>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</n-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {t} from "@/language";
|
||||
import Logo from "@/components/header/Logo.vue";
|
||||
import EsPluginList from "@/components/es/plugin/EsPluginList.vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
visible:boolean
|
||||
}>(),{
|
||||
visible:false
|
||||
})
|
||||
|
||||
const emits = defineEmits(["update:visible"]);
|
||||
|
||||
const searchText = ref("");
|
||||
|
||||
function handleClose(){
|
||||
emits("update:visible",false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal :show="visible" @maskClick="handleClose" @esc="handleClose">
|
||||
<n-card class="es-tools-dialog fixed top-20vh w-90% max-w-800px" :bordered="false" role="dialog" aria-modal="true">
|
||||
<template #header>
|
||||
<n-input v-model:value="searchText" type="text" size="large" autofocus clearable class="font-500"
|
||||
placeholder="Hi, Astral3D Editor">
|
||||
<template #prefix>
|
||||
<Logo class="w-30px h-30px" />
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
|
||||
<n-text depth="3" class="relative bottom-10px left-5px"> {{ t("plugin['Optional plug-in']") }} </n-text>
|
||||
|
||||
<EsPluginList @close="handleClose" :search="searchText" />
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@media screen and (max-width: 800px) {
|
||||
.es-tools-dialog{
|
||||
left:5%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 800px) {
|
||||
.es-tools-dialog{
|
||||
left:calc(50% - 400px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
import {usePluginStore} from "@/store/modules/plugin";
|
||||
import Logo from "@/components/header/Logo.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
search:string
|
||||
}>(),{
|
||||
search:""
|
||||
})
|
||||
|
||||
const emits = defineEmits(['close']);
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const list = computed(() => pluginStore.getPluginsList().value.filter(l => l.name.toLowerCase().indexOf(props.search.toLowerCase()) !== -1))
|
||||
|
||||
function handleRunPlugin(plugin:IPlugin.Item){
|
||||
emits("close");
|
||||
|
||||
window.viewer.modules.plugin.run(plugin.name);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<n-card hoverable :bordered="false" v-for="plugin in list" :key="plugin.name" size="small"
|
||||
@click.stop="handleRunPlugin(plugin)"
|
||||
content-class="flex flex-col justify-center items-center cursor-pointer">
|
||||
<n-image v-if="plugin.icon" :src="plugin.icon" object-fit="cover" width="60"
|
||||
preview-disabled />
|
||||
<Logo v-else class="w-60px" />
|
||||
<span class="mt-8px text-center">{{ plugin.name }}</span>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,354 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref,watchEffect,nextTick} from "vue";
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import {Upload, Delete, Link} from '@vicons/carbon';
|
||||
import {Loader} from "@astral3d/engine";
|
||||
import {t} from "@/language";
|
||||
import GLTFHandlerForm from "./glTFHandler/GLTFHandlerForm.vue";
|
||||
|
||||
const props = defineProps({
|
||||
onOptimize: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onFinish: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
// 优化结果文件
|
||||
let outputFiles:File[] = [];
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
const borderColor = computed(() => themeVars.value.borderColor);
|
||||
const primaryColor = computed(() => themeVars.value.primaryColor);
|
||||
const hoverColor = computed(() => themeVars.value.hoverColor);
|
||||
const textColor3 = computed(() => themeVars.value.textColor3);
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const loadingText = ref<string>(t("plugin.gltfHandler['The model is being optimized...']"));
|
||||
const uploadInputRef = ref();
|
||||
const fileList = ref<File[]>([]);
|
||||
const optimizeDone = ref(false);
|
||||
|
||||
const logInstRef = ref();
|
||||
const log = ref("");
|
||||
|
||||
watchEffect(() => {
|
||||
if (log.value) {
|
||||
nextTick(() => {
|
||||
logInstRef.value?.scrollTo({ position: 'bottom', silent: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function setLoading(boolean:boolean){
|
||||
loading.value = boolean;
|
||||
}
|
||||
|
||||
function addLog(_log:string){
|
||||
log.value += `${_log}\n`;
|
||||
|
||||
loadingText.value = _log;
|
||||
}
|
||||
|
||||
// 拖拽文件进入
|
||||
function handleFilesDrop(event:DragEvent){
|
||||
if (fileList.value.length > 10) {
|
||||
window.$message?.warning(t("plugin.gltfHandler['You can upload a maximum of 10 files']"));
|
||||
return;
|
||||
}
|
||||
|
||||
const originFiles = event.dataTransfer?.files;
|
||||
|
||||
if(!originFiles || !originFiles.length) return;
|
||||
|
||||
const files:File[] = [];
|
||||
for (let i = 0; i < originFiles.length; i++) {
|
||||
const s = originFiles[i].name.split(".");
|
||||
if(["glb","gltf"].includes(s[s.length - 1].toLowerCase())){
|
||||
files.push(originFiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理拖拽进来的文件
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (!fileList.value.find(item => item.lastModified === files[i].lastModified && item.name === files[i].name)) {
|
||||
if (fileList.value.length >= 10) {
|
||||
fileList.value.length = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
fileList.value.push(files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUploadChange(event){
|
||||
const files = event.target.files;
|
||||
|
||||
if (files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (!fileList.value.find(item => item.lastModified === files[i].lastModified && item.name === files[i].name)) {
|
||||
if (fileList.value.length >= 10) {
|
||||
fileList.value.length = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
fileList.value.push(files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:暂时只支持单文件处理
|
||||
// const file = event.target.files[0];
|
||||
// if(file){
|
||||
// fileList.value = [file];
|
||||
// }
|
||||
}
|
||||
|
||||
function handleSelectFile() {
|
||||
if(optimizeDone.value){
|
||||
window.$dialog.warning({
|
||||
title: window.$t('other.warning'),
|
||||
content: window.$t("plugin.gltfHandler['Re-selecting the file will clear the previous optimization result. Do you want to continue?']"),
|
||||
positiveText: window.$t('other.Ok'),
|
||||
negativeText: window.$t('other.Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
outputFiles = [];
|
||||
optimizeDone.value = false;
|
||||
log.value = "";
|
||||
|
||||
uploadInputRef.value.click();
|
||||
},
|
||||
});
|
||||
}else{
|
||||
if (fileList.value.length > 10) {
|
||||
window.$message?.warning(t("plugin.gltfHandler['You can upload a maximum of 10 files']"));
|
||||
return;
|
||||
}
|
||||
|
||||
uploadInputRef.value.click();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteFile(file:File) {
|
||||
if(optimizeDone.value){
|
||||
window.$dialog.warning({
|
||||
title: window.$t('other.warning'),
|
||||
content: window.$t("plugin.gltfHandler['This operation will clear the relevant optimization results. Do you want to continue?']"),
|
||||
positiveText: window.$t('other.Ok'),
|
||||
negativeText: window.$t('other.Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
fileList.value = fileList.value.filter(item => !(item.lastModified === file.lastModified && item.name === file.name));
|
||||
outputFiles = outputFiles.filter(item => !(item.name.replace("_astral3d.optimize","") === file.name));
|
||||
|
||||
if(!fileList.value.length){
|
||||
optimizeDone.value = false;
|
||||
log.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
}else{
|
||||
fileList.value = fileList.value.filter(item => !(item.lastModified === file.lastModified && item.name === file.name));
|
||||
}
|
||||
}
|
||||
|
||||
// 提交优化
|
||||
function handleSubmit(){
|
||||
if(fileList.value.length === 0) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
handleOptimize(formRef.value.model)
|
||||
}
|
||||
|
||||
// 处理优化
|
||||
async function handleOptimize(model:IPlugin.GLTFHandlerOptimizeModel) {
|
||||
for(let file of fileList.value){
|
||||
outputFiles.push(await props.onOptimize(model, file))
|
||||
}
|
||||
|
||||
optimizeDone.value = true;
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// 下载优化结果
|
||||
function handleDownload(){
|
||||
if(outputFiles.length === 0) return;
|
||||
|
||||
outputFiles.forEach(outputFile => {
|
||||
const url = URL.createObjectURL(outputFile);
|
||||
const a = window.document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = outputFile.name;
|
||||
window.document.body.appendChild(a);
|
||||
a.click();
|
||||
window.document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
}
|
||||
|
||||
// 优化结果导入场景
|
||||
function handleImportScene(){
|
||||
if(outputFiles.length === 0) return;
|
||||
|
||||
Loader.loadFiles(outputFiles, undefined).then(() => {
|
||||
window.$message?.success(t("plugin.gltfHandler['Import success!']"));
|
||||
// outputFiles = [];
|
||||
// optimizeDone.value = false;
|
||||
});
|
||||
|
||||
// 关闭插件弹窗
|
||||
// props.onFinish();
|
||||
// log.value = "";
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setLoading,
|
||||
addLog
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-spin :show="loading" >
|
||||
<template #description>
|
||||
{{ loadingText }}
|
||||
</template>
|
||||
|
||||
<div class="optimize-upload h-150px grid gap-2"
|
||||
:style="{gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))'}">
|
||||
<div class="optimize-upload-card" v-if="fileList.length === 0" @click="handleSelectFile" @dragover.stop.prevent @drop.stop.prevent="handleFilesDrop">
|
||||
<input v-show="false" type="file" ref="uploadInputRef" multiple="multiple" accept=".glb,.gltf" @change="handleUploadChange" />
|
||||
|
||||
<n-icon size="48" :depth="3" class="mb-12px">
|
||||
<Upload/>
|
||||
</n-icon>
|
||||
<n-text class="mb-6px text-16px"> {{ t("plugin.gltfHandler['Select the.glb/.gltf file']") }} </n-text>
|
||||
<n-text depth="3"> {{ t("plugin.gltfHandler['Batch processing is supported (up to 10)']") }} </n-text>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="optimize-upload-list">
|
||||
<div v-for="(file,index) in fileList" :key="index" class="optimize-upload-list-item">
|
||||
<n-icon size="16">
|
||||
<Link/>
|
||||
</n-icon>
|
||||
<span class="optimize-upload-list-item-name" :title="file.name">{{ file.name }}</span>
|
||||
<n-icon size="16" class="optimize-upload-list-item-del" @click.stop="handleDeleteFile(file)">
|
||||
<Delete/>
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-log ref="logInstRef" :log="log" trim class="!h-150px" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<GLTFHandlerForm ref="formRef" />
|
||||
|
||||
<div class="w-full flex justify-center mt-10px">
|
||||
<n-button v-if="!optimizeDone" type="primary" @click="handleSubmit">
|
||||
{{ t("plugin.gltfHandler.Optimize") }}
|
||||
</n-button>
|
||||
|
||||
<template v-else>
|
||||
<n-button type="primary" @click="handleDownload">
|
||||
{{ t("plugin.gltfHandler.Download") }}
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleImportScene" class="ml-4">
|
||||
{{ t("plugin.gltfHandler.Import scene") }}
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</n-spin>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.optimize-upload {
|
||||
&-card {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px dashed v-bind(borderColor);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: border-color 0.3s, width 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: v-bind(primaryColor);
|
||||
}
|
||||
}
|
||||
|
||||
&-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
transition: opacity 0.3s, height 0.3s;
|
||||
|
||||
&::before {
|
||||
display: table;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&-item {
|
||||
color: v-bind(primaryColor);
|
||||
position: relative;
|
||||
height: 22px;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color .3s;
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: v-bind(hoverColor);
|
||||
|
||||
.optimize-upload-list-item-del{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 6px;
|
||||
line-height: 1.57;
|
||||
flex: auto;
|
||||
max-width: 85%;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
&-del {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
color: v-bind(textColor3);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,419 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, nextTick, useTemplateRef, reactive } from "vue";
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { Upload, Delete, Link } from '@vicons/carbon';
|
||||
import * as THREE from 'three';
|
||||
import { App,Loader, PointCloudReconstructor,AddObjectCommand } from "@astral3d/engine";
|
||||
import { t } from "@/language";
|
||||
import { createBasicScene } from "@/utils/common/scenes";
|
||||
import ReconstructorForm from "./pointCloudReconstructor/ReconstructorForm.vue";
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
const borderColor = computed(() => themeVars.value.borderColor);
|
||||
const primaryColor = computed(() => themeVars.value.primaryColor);
|
||||
const hoverColor = computed(() => themeVars.value.hoverColor);
|
||||
const textColor3 = computed(() => themeVars.value.textColor3);
|
||||
|
||||
const formRef = useTemplateRef("formRef");
|
||||
const viewerRef = useTemplateRef("viewerRef");
|
||||
const uploadInputRef = useTemplateRef("uploadInputRef");
|
||||
|
||||
const loading = ref(false);
|
||||
const loadingText = ref<string>(t("plugin.pointCloudReconstructor['The point cloud is being reconstructed...']"));
|
||||
|
||||
const fileList = ref<File[]>([]);
|
||||
const stats = reactive({
|
||||
reconstructorDone: false,
|
||||
percentage:0,
|
||||
message:"",
|
||||
// 下面是统计信息
|
||||
totalPoints: "0",
|
||||
reconstructedObjects: 0,
|
||||
processingTime: "0s"
|
||||
})
|
||||
|
||||
let tmpScene, tmpDispose;
|
||||
let reconstructor, startTime;
|
||||
// 点云模型对象,重建好的对象
|
||||
let pointCloud,reconstructedGroup;
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
const p = createBasicScene(viewerRef.value as HTMLDivElement);
|
||||
tmpScene = p.scene;
|
||||
tmpDispose = p.dispose;
|
||||
})
|
||||
|
||||
// 拖拽文件进入
|
||||
function handleFilesDrop(event: DragEvent) {
|
||||
const originFiles = event.dataTransfer?.files;
|
||||
|
||||
if (!originFiles || !originFiles.length) return;
|
||||
|
||||
const files: File[] = [];
|
||||
for (let i = 0; i < originFiles.length; i++) {
|
||||
const s = originFiles[i].name.split(".");
|
||||
if (["glb", "gltf", "ply", "pcd"].includes(s[s.length - 1].toLowerCase())) {
|
||||
files.push(originFiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
// 暂时只支持单个模型处理
|
||||
fileList.value = [files[0]];
|
||||
handleFileLoad();
|
||||
}
|
||||
}
|
||||
|
||||
// 手动选择文件
|
||||
function handleSelectFile() {
|
||||
if (stats.reconstructorDone) {
|
||||
window.$dialog.warning({
|
||||
title: window.$t('other.warning'),
|
||||
content: window.$t("plugin.pointCloudReconstructor['Re-selecting the file will empty the previous reconstruction. Do you want to continue?']"),
|
||||
positiveText: window.$t('other.Ok'),
|
||||
negativeText: window.$t('other.Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
fileList.value = [];
|
||||
|
||||
stats.reconstructorDone = false;
|
||||
|
||||
uploadInputRef.value?.click();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
|
||||
uploadInputRef.value?.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 文件变更
|
||||
function handleUploadChange(event) {
|
||||
const files = event.target.files;
|
||||
|
||||
if (files) {
|
||||
fileList.value = [files[0]];
|
||||
|
||||
handleFileLoad();
|
||||
}
|
||||
}
|
||||
|
||||
// 文件模型加载
|
||||
async function handleFileLoad() {
|
||||
clearScene();
|
||||
|
||||
const verifyExec = (object3D:THREE.Points) => {
|
||||
pointCloud = object3D;
|
||||
|
||||
stats.totalPoints = object3D.geometry.attributes.position.count.toLocaleString();
|
||||
|
||||
tmpScene.add(pointCloud);
|
||||
}
|
||||
|
||||
const object3D = await Loader.loadFile(fileList.value[0], new THREE.LoadingManager(), null, false) as THREE.Points;
|
||||
if (object3D.type === "Points") {
|
||||
verifyExec(object3D);
|
||||
} else if (object3D.children.length === 1 && object3D.children[0].type === "Points") {
|
||||
verifyExec(object3D.children[0] as THREE.Points);
|
||||
} else {
|
||||
window.$message?.error(t("plugin.pointCloudReconstructor['The model is not a point cloud']"));
|
||||
|
||||
fileList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 删除已选择文件
|
||||
function handleDeleteFile() {
|
||||
if (stats.reconstructorDone) {
|
||||
window.$dialog.warning({
|
||||
title: window.$t('other.warning'),
|
||||
content: window.$t("plugin.pointCloudReconstructor['This operation will empty the associated reconstructions. Do you want to continue?']"),
|
||||
positiveText: window.$t('other.Ok'),
|
||||
negativeText: window.$t('other.Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
fileList.value = [];
|
||||
|
||||
stats.reconstructorDone = false;
|
||||
|
||||
clearScene();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
|
||||
clearScene();
|
||||
}
|
||||
}
|
||||
|
||||
// 提交重建
|
||||
function handleSubmit() {
|
||||
if (fileList.value.length === 0 || !pointCloud) return;
|
||||
|
||||
const model = formRef.value?.model;
|
||||
if (!model) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 防止二次重建
|
||||
if(stats.reconstructorDone && reconstructedGroup){
|
||||
// 移除原来的重建结果
|
||||
tmpScene.remove(reconstructedGroup);
|
||||
reconstructedGroup = null;
|
||||
|
||||
// 显示点云
|
||||
pointCloud.visible = true;
|
||||
|
||||
stats.reconstructedObjects = 0;
|
||||
stats.processingTime = "0s";
|
||||
}
|
||||
|
||||
if (!reconstructor) {
|
||||
reconstructor = new PointCloudReconstructor();
|
||||
}
|
||||
|
||||
reconstructor.colorTolerance = model.colorTolerance;
|
||||
reconstructor.distanceThreshold = model.distanceThreshold;
|
||||
reconstructor.minClusterSize = model.minClusterSize;
|
||||
reconstructor.downsampleResolution = model.downsampleResolution;
|
||||
|
||||
// 处理几何数据
|
||||
reconstructor.processGeometry(pointCloud.geometry);
|
||||
|
||||
startTime = performance.now();
|
||||
|
||||
// 开始重建
|
||||
reconstructor.reconstruct(
|
||||
(progress, status) => {
|
||||
stats.percentage = Math.round(progress);
|
||||
|
||||
stats.message = status;
|
||||
},
|
||||
(objectGroup, totalObjects) => {
|
||||
reconstructedGroup = objectGroup;
|
||||
|
||||
tmpScene.add(reconstructedGroup);
|
||||
|
||||
// 更新统计
|
||||
stats.reconstructedObjects = totalObjects;
|
||||
|
||||
// 计算处理时间
|
||||
const endTime = performance.now();
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(1);
|
||||
stats.processingTime = `${duration}s`;
|
||||
|
||||
// 隐藏点云
|
||||
pointCloud.visible = false;
|
||||
|
||||
stats.reconstructorDone = true;
|
||||
loading.value = false;
|
||||
|
||||
// 更新状态
|
||||
stats.message = `重建完成: ${totalObjects}个对象 (${duration}秒)`;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 重建结果导入场景
|
||||
function handleImportScene() {
|
||||
if(!tmpScene || !reconstructedGroup) return;
|
||||
|
||||
App.execute(new AddObjectCommand(reconstructedGroup.clone()));
|
||||
}
|
||||
|
||||
// 关闭插件
|
||||
function handleClose(){
|
||||
clearScene();
|
||||
|
||||
tmpDispose && tmpDispose();
|
||||
|
||||
reconstructor && reconstructor.dispose();
|
||||
}
|
||||
|
||||
// 清除场景在此添加的内容
|
||||
function clearScene(){
|
||||
pointCloud && tmpScene.remove(pointCloud);
|
||||
reconstructedGroup && tmpScene.remove(reconstructedGroup);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleClose
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-spin :show="loading">
|
||||
<template #description>
|
||||
{{ loadingText }}
|
||||
</template>
|
||||
|
||||
<n-progress type="line" :percentage="stats.percentage" indicator-placement="inside" :processing="loading" class="mb-3" />
|
||||
|
||||
<div class="reconstructor-upload grid gap-2"
|
||||
:style="{ gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))' }">
|
||||
<div class="reconstructor-upload-card !h-150px" v-if="fileList.length === 0" @click="handleSelectFile"
|
||||
@dragover.stop.prevent @drop.stop.prevent="handleFilesDrop">
|
||||
<input v-show="false" type="file" ref="uploadInputRef" accept=".glb,.gltf,.ply,.pcd"
|
||||
@change="handleUploadChange" />
|
||||
|
||||
<n-icon size="48" :depth="3" class="mb-12px">
|
||||
<Upload />
|
||||
</n-icon>
|
||||
<n-text class="mb-6px text-16px">
|
||||
{{ t("plugin.pointCloudReconstructor['Select the Point Cloud model file']") + ":.glb,.gltf,.ply,.pcd" }}
|
||||
</n-text>
|
||||
<n-text depth="3">
|
||||
{{ t("plugin.pointCloudReconstructor['Please semantically segment the point cloud first']") }}
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="reconstructor-upload-list">
|
||||
<div v-for="(file, index) in fileList" :key="index" class="reconstructor-upload-list-item">
|
||||
<n-icon size="16">
|
||||
<Link />
|
||||
</n-icon>
|
||||
<span class="reconstructor-upload-list-item-name" :title="file.name">{{ file.name }}</span>
|
||||
<n-icon size="16" class="reconstructor-upload-list-item-del" @click.stop="handleDeleteFile()">
|
||||
<Delete />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div ref="viewerRef" class="w-full h-400px mb-2 relative">
|
||||
<span class="absolute bottom-2 left-4">{{ stats.message }}</span>
|
||||
</div>
|
||||
|
||||
<n-grid :x-gap="50" :cols="3">
|
||||
<n-gi>
|
||||
<n-card size="small" embedded hoverable content-class="flex flex-col justify-center items-center">
|
||||
<div>总点数</div>
|
||||
<div class="stats-text text-1.2rem mt-2">{{ stats.totalPoints }}</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<n-card size="small" embedded hoverable content-class="flex flex-col justify-center items-center">
|
||||
<div>重建对象</div>
|
||||
<div class="stats-text text-1.2rem mt-2">{{ stats.reconstructedObjects }}</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<n-card size="small" embedded hoverable content-class="flex flex-col justify-center items-center">
|
||||
<div>处理时间</div>
|
||||
<div class="stats-text text-1.2rem mt-2">{{ stats.processingTime }}</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<ReconstructorForm ref="formRef" />
|
||||
|
||||
<div class="w-full flex justify-center mt-10px">
|
||||
<n-button type="primary" :loading="loading" @click="handleSubmit">
|
||||
{{ t("plugin.pointCloudReconstructor.Reconstruction") }}
|
||||
</n-button>
|
||||
|
||||
<template v-if="stats.reconstructorDone">
|
||||
<n-button type="primary" @click="handleImportScene" class="ml-4">
|
||||
{{ t("plugin.gltfHandler.Import scene") }}
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</n-spin>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.reconstructor-upload {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&-card {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px dashed v-bind(borderColor);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: border-color 0.3s, width 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: v-bind(primaryColor);
|
||||
}
|
||||
}
|
||||
|
||||
&-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
transition: opacity 0.3s, height 0.3s;
|
||||
|
||||
&::before {
|
||||
display: table;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&-item {
|
||||
color: v-bind(primaryColor);
|
||||
position: relative;
|
||||
height: 22px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color .3s;
|
||||
cursor: pointer;
|
||||
padding: 0.4rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: v-bind(hoverColor);
|
||||
|
||||
.reconstructor-upload-list-item-del {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 6px;
|
||||
line-height: 1.57;
|
||||
flex: auto;
|
||||
max-width: 85%;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
&-del {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
color: v-bind(textColor3);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
color: v-bind(primaryColor);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {t} from "@/language";
|
||||
import EsFormItemHelpLabel from "@/components/es/EsFormItemHelpLabel.vue";
|
||||
|
||||
const model = ref<IPlugin.GLTFHandlerOptimizeModel>({
|
||||
'compress': "draco",
|
||||
'meshoptLevel': "high",
|
||||
'instance': true,
|
||||
'instanceMin': 5,
|
||||
'flatten': true,
|
||||
'join': true,
|
||||
'palette': true,
|
||||
'paletteMin': 5,
|
||||
'prune': true,
|
||||
'pruneAttributes': true,
|
||||
'pruneLeaves': true,
|
||||
'pruneSolidTextures': true,
|
||||
'weld': true,
|
||||
'simplify': true,
|
||||
'simplifyError': 0.01,
|
||||
'simplifyLockBorder': false,
|
||||
'simplifyRatio': 0,
|
||||
'textureCompress': "webp",
|
||||
'textureSize': 512,
|
||||
})
|
||||
|
||||
function handleKeyword(keyword: string, p: string) {
|
||||
const strs = p.split(keyword);
|
||||
|
||||
return `${strs[0]}<span class="color-red">${keyword}</span>${strs[1]}`;
|
||||
}
|
||||
|
||||
// 合并网格(开启合并网格必开启展平场景树)
|
||||
function handleJoinChange(checked: boolean) {
|
||||
if (checked) {
|
||||
model.value.flatten = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 简化网格(开启简化网格必开启合并同位顶点)
|
||||
function handleSimplifyChange(checked: boolean) {
|
||||
if (checked) {
|
||||
model.value.weld = true;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
model
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" label-placement="left" label-width="auto" class="optimize-form">
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<!-- 压缩方式 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Compress')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['Floating point compression method.']") }}<br/>
|
||||
<span class="color-red">Draco</span>:{{ t('plugin.gltfHandler.compresses') }} geometry <br/>
|
||||
<span class="color-red">Meshopt & Quantization</span>:{{ t('plugin.gltfHandler.compresses') }} geometry &
|
||||
animation
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-select v-model:value="model.compress" class="!w-full" :options="[
|
||||
{label:'Draco',value:'draco'},
|
||||
{label:'Meshopt',value:'meshopt'},
|
||||
{label:'Quantization',value:'quantize'},
|
||||
{label:'None',value:'false'}
|
||||
]"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- Meshopt压缩级别 -->
|
||||
<n-form-item-gi v-if="model.compress === 'meshopt'" :span="12" :label="t('plugin.gltfHandler.Meshopt compression level')">
|
||||
<n-select v-model:value="model.meshoptLevel" class="!w-full" :options="[
|
||||
{label:'medium',value:'medium'},
|
||||
{label:'high',value:'high'}
|
||||
]"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 实例化网格 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Instance')">
|
||||
{{ t("plugin.gltfHandler['Use GPU instancing with shared mesh references.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.instance"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 实例化网格界限 -->
|
||||
<n-form-item-gi v-if="model.instance" :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Instance min')">
|
||||
{{ t("plugin.gltfHandler['Number of instances required for instancing.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-input-number v-model:value="model.instanceMin" :min="2" :max="Infinity" class="!w-full"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 展平场景树 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Flatten')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['Flatten scene graph.']") }}<br/>
|
||||
<div
|
||||
v-html="handleKeyword(t('plugin.gltfHandler.Join'),t('plugin.gltfHandler.This item cannot be closed when opening Join'))"></div>
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.flatten" :disabled="model.join"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 合并网格 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Join')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['Join meshes and reduce draw calls.']") }}<br/>
|
||||
<div
|
||||
v-html="handleKeyword(t('plugin.gltfHandler.Flatten'),t('plugin.gltfHandler[\'Prerequisites: Flatten is enabled\']'))"></div>
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.join" @update:value="handleJoinChange"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 合并纹理 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Palette')">
|
||||
{{ t("plugin.gltfHandler['Creates palette textures and merges materials.']") }}<br/>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.palette"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 合并纹理界限 -->
|
||||
<n-form-item-gi v-if="model.palette" :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Palette min')">
|
||||
{{
|
||||
t("plugin.gltfHandler['Minimum number of blocks in the palette texture. If fewer unique material values are found, no palettes will be generated.']")
|
||||
}}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-input-number v-model:value="model.paletteMin" :min="2" :max="Infinity" class="!w-full"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 修剪 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Prune')">
|
||||
{{
|
||||
t("plugin.gltfHandler['Removes properties from the file if they are not referenced by a Scene.']")
|
||||
}}<br/>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.prune"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<template v-if="model.prune">
|
||||
<!-- 修剪顶点 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Prune attributes')">
|
||||
{{ t("plugin.gltfHandler['Whether to prune unused vertex attributes.']") }}<br/>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.pruneAttributes"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 修剪子节点 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Prune leaves')">
|
||||
{{ t("plugin.gltfHandler['Whether to prune empty leaf nodes.']") }}<br/>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.pruneLeaves"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 修剪纹理 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Prune solid textures')">
|
||||
{{
|
||||
t("plugin.gltfHandler['Whether to prune solid (single-color) textures,converting them to material factors.']")
|
||||
}}<br/>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.pruneSolidTextures"/>
|
||||
</n-form-item-gi>
|
||||
</template>
|
||||
|
||||
<!-- 合并同位顶点 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Weld')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['Merge equivalent vertices.']") }}<br/>
|
||||
<div
|
||||
v-html="handleKeyword(t('plugin.gltfHandler.Simplify'),t('plugin.gltfHandler.This item cannot be closed when opening Simplify'))"></div>
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.weld" :disabled="model.simplify"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 简化网格 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Simplify')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['Simplify mesh geometry with meshoptimizer.']") }}<br/>
|
||||
<div
|
||||
v-html="handleKeyword(t('plugin.gltfHandler.Weld'),t('plugin.gltfHandler[\'Prerequisites: Weld is enabled\']'))"></div>
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.simplify" @update:value="handleSimplifyChange"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<template v-if="model.simplify">
|
||||
<!-- 简化误差界限 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Simplify error')">
|
||||
{{ t("plugin.gltfHandler['Simplification error tolerance, as a fraction of mesh extent.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-input-number v-model:value="model.simplifyError" :min="0.01" :max="100" :step="0.01" :precision="2"
|
||||
class="!w-full">
|
||||
<template #suffix> %</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 简化锁定边界 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Simplify lock border')">
|
||||
{{ t("plugin.gltfHandler['Whether to lock topological borders of the mesh.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-switch v-model:value="model.simplifyLockBorder"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 简化比率 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Simplify ratio')">
|
||||
{{ t("plugin.gltfHandler['Target ratio (0-1) of vertices to keep.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
|
||||
<n-slider v-model:value="model.simplifyRatio" :min="0" :max="1" :step="0.01"/>
|
||||
</n-form-item-gi>
|
||||
</template>
|
||||
|
||||
<!-- 纹理压缩 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Texture compress')">
|
||||
<div>
|
||||
{{ t("plugin.gltfHandler['AVIF and WebP optimize transfer size;']") }}<br/>
|
||||
{{ t("plugin.gltfHandler['Auto Compresses in the original format;']") }}<br/>
|
||||
{{ t("plugin.gltfHandler['none Does not compress.']") }}
|
||||
</div>
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-select v-model:value="model.textureCompress" class="!w-full" :options="[
|
||||
{label:'WebP',value:'webp'},
|
||||
{label:'AVIF',value:'avif'},
|
||||
{label:'Auto',value:'auto'},
|
||||
{label:'None',value:'none'}
|
||||
]"/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 纹理最大尺寸 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.gltfHandler.Texture size')">
|
||||
{{ t("plugin.gltfHandler['Maximum texture dimensions, in pixels.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
<n-input-number v-model:value="model.textureSize" :min="2" :step="2" :max="Infinity" class="!w-full">
|
||||
<template #suffix> px</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.optimize-form {
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { t } from "@/language";
|
||||
import EsFormItemHelpLabel from "@/components/es/EsFormItemHelpLabel.vue";
|
||||
|
||||
const model = ref<IPlugin.PointCloudReconstructorModel>({
|
||||
'colorTolerance': 5,
|
||||
'distanceThreshold': 0.3,
|
||||
'minClusterSize': 10,
|
||||
'downsampleResolution': 0.05,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
model
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" label-placement="left" label-width="auto" class="overflow-auto mt-2">
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<!-- 颜色容差 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.pointCloudReconstructor.Color tolerance')">
|
||||
{{ t("plugin.pointCloudReconstructor['Color similarity processing, the larger the value, the wider the range of cluster color values.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
|
||||
<n-slider v-model:value="model.colorTolerance" :min="1" :max="20" :step="1" />
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 距离阈值 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.pointCloudReconstructor.Distance threshold')">
|
||||
{{ t("plugin.pointCloudReconstructor['Control the sensitivity of object separation, that is, spatial clustering distance threshold.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
|
||||
<n-slider v-model:value="model.distanceThreshold" :min="0.05" :max="0.5" :step="0.05" />
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 最小簇大小 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.pointCloudReconstructor.Min cluster size')">
|
||||
{{ t("plugin.pointCloudReconstructor['Minimum number of cluster points, filtering noise points and small clusters.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
|
||||
<n-slider v-model:value="model.minClusterSize" :min="5" :max="100" :step="5" />
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 点云抽稀 -->
|
||||
<n-form-item-gi :span="12">
|
||||
<template #label>
|
||||
<EsFormItemHelpLabel :label="t('plugin.pointCloudReconstructor.Downsample resolution')">
|
||||
{{ t("plugin.pointCloudReconstructor['The grid space division method is used to reduce the number of points, so as to reduce the amount of calculation in the subsequent processing.']") }}
|
||||
</EsFormItemHelpLabel>
|
||||
</template>
|
||||
|
||||
<n-slider v-model:value="model.downsampleResolution" :min="0.01" :max="0.2" :step="0.01" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user