feat(All):Initial
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* byte数组转换成base64 数据
|
||||
* @param {Array<byte>} buffer
|
||||
* @return {string} base64 string
|
||||
*/
|
||||
export function _arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
return window.btoa( binary );
|
||||
}
|
||||
|
||||
/**
|
||||
* base64数据转换成byte数组
|
||||
* @param {string} base64
|
||||
* @return {string} buffer
|
||||
*/
|
||||
export function _base64ToArrayBuffer(base64) {
|
||||
const binary_string = window.atob(base64);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 10进制转rgba
|
||||
export function decToRgb(number:number) {
|
||||
const blue = number & 0xff;
|
||||
const green = number >> 8 & 0xff;
|
||||
const red = number >> 16 & 0xff;
|
||||
return `rgb(${red}, ${green}, ${blue})`;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2024/3/26 9:57
|
||||
* @description 全局常量
|
||||
*/
|
||||
// 支持的模型格式
|
||||
export const MODEL_SUPPORT_TYPE = [
|
||||
// 普通模型格式
|
||||
"gltf","glb","fbx","obj","mtl",
|
||||
"3dm","3ds",
|
||||
"3mf","amf",
|
||||
"dae","drc",
|
||||
"kmz","ldr",
|
||||
"mpd","md2",
|
||||
"pcd","ply",
|
||||
"stl","svg",
|
||||
"usdz","vox","vtk","vtp",
|
||||
"wrl",
|
||||
"xyz",
|
||||
"json","zip",
|
||||
// 自解析格式
|
||||
"rvt","rfa","ifc",
|
||||
"3DTiles",
|
||||
//"osgb",
|
||||
// "rvm",
|
||||
// "dgn",
|
||||
/*"jt"*/
|
||||
// "shp",
|
||||
// "stp","step"
|
||||
];
|
||||
|
||||
// 资源中心支持上传的格式
|
||||
export const ASSET_UPLOAD_SUPPORT_TYPE = {
|
||||
'Model': ['glb','gltf','fbx','obj'],
|
||||
'Material': ['zip'],
|
||||
'Texture': ['png','jpg','jpeg','webp'],
|
||||
'Billboard': ['png','jpg','jpeg','webp','svg'],
|
||||
'HDR': ['hdr','exr'],
|
||||
'Tiles': ['zip'],
|
||||
}
|
||||
|
||||
// 需要转换的BIM模型格式
|
||||
export const NEED_CONVERT_BIM_MODEL = ["rvt", "rfa"];
|
||||
|
||||
// 支持的图纸文件类型
|
||||
export const DRAWING_SUPPORT_TYPE = ["dwg", "dxf", "png", "jpg", "jpeg"];
|
||||
|
||||
// 需要转换的图纸格式
|
||||
export const NEED_CONVERT_DRAWING = ["dwg"];
|
||||
|
||||
// 支持的文档类型
|
||||
export const DOC_SUPPORT_TYPE = ["doc","docx","xls","xlsx","xlsm","ppt",'pptx',"pdf","txt"];
|
||||
|
||||
export const SCENE_TYPE = [
|
||||
{label:"城市",value:"城市"},
|
||||
{label:"园区",value:"园区"},
|
||||
{label:"工厂",value:"工厂"},
|
||||
{label:"楼宇",value:"楼宇"},
|
||||
{label:"设备",value:"设备"},
|
||||
{label:"其他",value:"其他"},
|
||||
]
|
||||
|
||||
// 截屏占位图
|
||||
export const DefaultScreenshot = "/static/images/placeholder/截屏占位图.png";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export function dateTimeFormat(fmt) {
|
||||
let myDate = new Date();
|
||||
let o = {
|
||||
"M+": myDate.getMonth() + 1, //月份
|
||||
"d+": myDate.getDate(), //日
|
||||
"H+": myDate.getHours(), //小时
|
||||
"m+": myDate.getMinutes(), //分
|
||||
"s+": myDate.getSeconds(), //秒
|
||||
"q+": Math.floor((myDate.getMonth() + 3) / 3), //季度
|
||||
"S": myDate.getMilliseconds() //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (myDate.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
for (let k in o)
|
||||
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
return fmt;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {pow1024} from "./utils";
|
||||
|
||||
/**
|
||||
* 通过Fetch API下载文件/图片(支持跨域/Blob处理)
|
||||
* @param {string} url 文件地址
|
||||
* @param {string} filename 保存的文件名
|
||||
*/
|
||||
export async function downloadWithFetch(url:string, filename:string = "") {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('The network is responding abnormally');
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
if(!filename){
|
||||
filename = url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理内存
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
document.body.removeChild(link);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小 字节转换单位
|
||||
* @param {number} size
|
||||
* @returns {string|*}
|
||||
*/
|
||||
export const filterSize = (size:number): string | any => {
|
||||
if (!size) return '0B';
|
||||
if (size < pow1024(1)) return size + ' B';
|
||||
if (size < pow1024(2)) return (size / pow1024(1)).toFixed(2) + ' KB';
|
||||
if (size < pow1024(3)) return (size / pow1024(2)).toFixed(2) + ' MB';
|
||||
if (size < pow1024(4)) return (size / pow1024(3)).toFixed(2) + ' GB';
|
||||
return (size / pow1024(4)).toFixed(2) + ' TB'
|
||||
}
|
||||
|
||||
|
||||
export const getServiceStaticFile = (url:string):string => {
|
||||
if(!url) return "";
|
||||
|
||||
if(url.startsWith("blob:") || url.startsWith("data:") || url.startsWith("http:") || url.startsWith("https:") || url.startsWith('/file/static')) return url;
|
||||
|
||||
return `/file/static` + (url[0] === '/' ? '' : '/') + url;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import "../signals/signalRegister";
|
||||
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
// @ts-ignore
|
||||
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
|
||||
Number.prototype.format = function () {
|
||||
return this.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
};
|
||||
|
||||
/*添加自执行函数,为rem设置自适应的根DOM字体大小,1920px下默认1rem = 20px */
|
||||
(function () {
|
||||
change();
|
||||
|
||||
function change() {
|
||||
const w = document.documentElement.clientWidth > 1280 ? document.documentElement.clientWidth : 1280;
|
||||
document.documentElement.style.fontSize = w * 20 / 1920 + "px";
|
||||
}
|
||||
|
||||
/*监听窗口大小的改变*/
|
||||
window.addEventListener("resize", change, false);
|
||||
})()
|
||||
|
||||
//屏蔽选中
|
||||
document.onselectstart = function (event) {
|
||||
if (window.event) {
|
||||
event = window.event;
|
||||
}
|
||||
try {
|
||||
const the = event.srcElement;
|
||||
// @ts-ignore
|
||||
return (the.tagName === "INPUT" && the.type.toLowerCase() === "text") || the.tagName === "TEXTAREA";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 禁止浏览器默认右键菜单
|
||||
document.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// 保存原始方法
|
||||
const originalStringify = JSON.stringify;
|
||||
const originalParse = JSON.parse;
|
||||
|
||||
// 重写 JSON.stringify
|
||||
JSON.stringify = function (value: any, ...args: any[]) {
|
||||
return originalStringify(value, function (key, val) {
|
||||
// 如果有自定义的 replacer 函数,先执行它
|
||||
if (args[0]) {
|
||||
val = args[0](key, val);
|
||||
}
|
||||
|
||||
if (val === Infinity) return "Infinity";
|
||||
if (val === -Infinity) return "-Infinity";
|
||||
return val;
|
||||
}, args[1]); // args[1] 是 space 参数
|
||||
};
|
||||
|
||||
// 重写 JSON.parse
|
||||
JSON.parse = function (text: string, reviver?: (key: any, value: any) => any) {
|
||||
return originalParse(text, function (key, value) {
|
||||
// 处理 Infinity
|
||||
if (value === "Infinity") return Infinity;
|
||||
if (value === "-Infinity") return -Infinity;
|
||||
|
||||
// 如果有自定义的 reviver 函数,执行它
|
||||
return reviver ? reviver(key, value) : value;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { h } from 'vue';
|
||||
import type { Component } from 'vue'
|
||||
import { NIcon } from 'naive-ui';
|
||||
|
||||
/**
|
||||
* 渲染icon
|
||||
* @param icon vicons
|
||||
* @returns
|
||||
*/
|
||||
export function renderIcon(icon: Component) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) });
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/Addons';
|
||||
import { App } from "@astral3d/engine";
|
||||
|
||||
export interface IModel extends THREE.Object3D {
|
||||
metadata: Object;
|
||||
}
|
||||
|
||||
export function getMaterialName(material) {
|
||||
if (Array.isArray(material)) {
|
||||
const array: any = [];
|
||||
|
||||
for (let i = 0; i < material.length; i++) {
|
||||
array.push(material[i].name);
|
||||
}
|
||||
|
||||
return array.join(',');
|
||||
}
|
||||
|
||||
return material.name;
|
||||
}
|
||||
|
||||
export function getObjectType(object) {
|
||||
if (object.isScene) return 'Scene';
|
||||
if (object.isCamera) return 'Camera';
|
||||
if (object.isLight) return 'Light';
|
||||
if (object.isMesh) return 'Mesh';
|
||||
if (object.isLine) return 'Line';
|
||||
if (object.isPoints) return 'Points';
|
||||
|
||||
return 'Object3D';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中模型 path
|
||||
*/
|
||||
export function getSelectedModelPath() {
|
||||
let pathArr: string[] = [];
|
||||
|
||||
function getPath(obj) {
|
||||
if (obj.parent) {
|
||||
pathArr.unshift(obj.parent.name);
|
||||
getPath(obj.parent);
|
||||
}
|
||||
}
|
||||
|
||||
getPath(App.selected);
|
||||
|
||||
return pathArr.join(' !! ');
|
||||
}
|
||||
|
||||
// 屏幕坐标转世界坐标
|
||||
export function screenToWorld(x: number, y: number) {
|
||||
const vector = new THREE.Vector3();
|
||||
vector.set(
|
||||
(x / window.viewer.container.offsetWidth) * 2 - 1,
|
||||
-(y / window.viewer.container.offsetHeight) * 2 + 1,
|
||||
0.5
|
||||
);
|
||||
vector.unproject(App.camera);
|
||||
|
||||
const dir = vector.sub(App.camera.position).normalize();
|
||||
const distance = -App.camera.position.z / dir.z;
|
||||
return App.camera.position.clone().add(dir.multiplyScalar(distance));
|
||||
}
|
||||
|
||||
export function reBufferGeometryUv(geometry: THREE.BufferGeometry) {
|
||||
const uv = geometry.attributes.uv;
|
||||
if (!uv) return;
|
||||
|
||||
// 获取u和v的范围
|
||||
const box = {
|
||||
min: new THREE.Vector2(Infinity, Infinity),
|
||||
max: new THREE.Vector2(-Infinity, -Infinity),
|
||||
};
|
||||
for (let i = 0; i < uv.count; i++) {
|
||||
const u = uv.getX(i);
|
||||
const v = uv.getY(i);
|
||||
|
||||
box.min.x = Math.min(box.min.x, u);
|
||||
box.min.y = Math.min(box.min.y, v);
|
||||
box.max.x = Math.max(box.max.x, u);
|
||||
box.max.y = Math.max(box.max.y, v);
|
||||
}
|
||||
|
||||
// 计算偏移量和范围
|
||||
const offset = new THREE.Vector2(0 - box.min.x, 0 - box.min.y);
|
||||
const range = new THREE.Vector2(box.max.x - box.min.x, box.max.y - box.min.y);
|
||||
|
||||
// 遍历顶点,修改uv
|
||||
for (let i = 0; i < uv.count; i++) {
|
||||
const u = uv.getX(i);
|
||||
const v = uv.getY(i);
|
||||
|
||||
// 计算新的u和v
|
||||
const newU = (u + offset.x) / range.x;
|
||||
const newV = (v + offset.y) / range.y;
|
||||
|
||||
// 写入新的uv
|
||||
uv.setXY(i, newU, newV);
|
||||
}
|
||||
|
||||
// 通知three.js更新
|
||||
uv.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function setUserData(object: IModel, key: string, value: any) {
|
||||
// key按照.分割,设置到object的userData中
|
||||
const keys = key.split('.');
|
||||
let obj = object.userData;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!obj[k]) {
|
||||
obj[k] = {};
|
||||
}
|
||||
obj = obj[k];
|
||||
}
|
||||
obj[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
export function setMetaData(object: IModel, key: string, value: any) {
|
||||
// key按照.分割,设置到object的metaData中
|
||||
const keys = key.split('.');
|
||||
let metadata = object.metadata;
|
||||
|
||||
if (!metadata) {
|
||||
metadata = {};
|
||||
object.metadata = metadata;
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!metadata[k]) {
|
||||
metadata[k] = {};
|
||||
}
|
||||
metadata = metadata[k];
|
||||
}
|
||||
metadata[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基础场景(会用于轻量展示)
|
||||
*/
|
||||
export function createBasicScene(container: HTMLElement) {
|
||||
let scene = new THREE.Scene();
|
||||
|
||||
let camera = new THREE.PerspectiveCamera(60, container.offsetWidth / container.offsetHeight, 0.1, 1000);
|
||||
camera.position.set(0, 2, 8);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
let renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
powerPreference: "high-performance"
|
||||
});
|
||||
|
||||
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
renderer.setClearColor(0x0c1126);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// 添加光源
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 2);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(5, 5, 5);
|
||||
scene.add(directionalLight);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
backLight.position.set(-5, -5, -5);
|
||||
scene.add(backLight);
|
||||
|
||||
// 初始化轨道控制器
|
||||
let controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = true;
|
||||
|
||||
// @ts-ignore监听视窗变化(节流)
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
let resizeObserver = new ResizeObserver(() => {
|
||||
if (timer) return;
|
||||
timer = setTimeout(function () {
|
||||
camera.aspect = container.offsetWidth / container.offsetHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
||||
|
||||
timer = null;
|
||||
}, 16)
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
const animate = () => {
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
const dispose = () => {
|
||||
resizeObserver.unobserve(container);
|
||||
resizeObserver.disconnect();
|
||||
// @ts-ignore
|
||||
resizeObserver = null;
|
||||
|
||||
renderer.setAnimationLoop(null);
|
||||
renderer.dispose();
|
||||
// @ts-ignore
|
||||
renderer = null;
|
||||
|
||||
controls.dispose();
|
||||
// @ts-ignore
|
||||
controls = null;
|
||||
|
||||
camera.clear();
|
||||
// @ts-ignore
|
||||
camera = null;
|
||||
|
||||
scene.clear();
|
||||
// @ts-ignore
|
||||
scene = null;
|
||||
}
|
||||
|
||||
return {
|
||||
scene,
|
||||
camera,
|
||||
renderer,
|
||||
controls,
|
||||
dispose
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import type { TreeOption } from 'naive-ui';
|
||||
import {BASE64_TYPES} from "@astral3d/engine";
|
||||
import {t} from "@/language";
|
||||
|
||||
// 替换字符
|
||||
export function escapeHTML(html:string) {
|
||||
if (typeof html !== "string") return html;
|
||||
|
||||
return html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* naive UI树结构寻找对应节点位置及所处父节点
|
||||
* @param node 目标节点
|
||||
* @param nodes 树数据
|
||||
*/
|
||||
export function findSiblingsAndIndex(node: TreeOption, nodes?: TreeOption[]): [TreeOption[], number] | [null, null] {
|
||||
if (!nodes) return [null, null];
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
const siblingNode = nodes[i];
|
||||
if (siblingNode.key === node.key) return [nodes, i];
|
||||
|
||||
const [siblings, index] = findSiblingsAndIndex(node, siblingNode.children)
|
||||
if (siblings && index !== null) return [siblings, index];
|
||||
}
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// base64 转 File
|
||||
export function base64ToFile(dataurl, filename) {
|
||||
let arr = dataurl.split(',');
|
||||
let mime = arr[0].match(/:(.*?);/)[1];
|
||||
let bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new File([u8arr], `${filename}.${BASE64_TYPES[arr[0]]}`, {type: mime});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 求次幂
|
||||
*/
|
||||
export function pow1024(num) {
|
||||
return Math.pow(1024, num)
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态添加script
|
||||
* @param src
|
||||
* @param async
|
||||
*/
|
||||
export function loadScript(src:string,async:boolean = true){
|
||||
return new Promise((resolve,reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.async = async;
|
||||
|
||||
script.onload = () => {
|
||||
resolve("");
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
reject(`${src} 加载失败!`);
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪切板
|
||||
* @param text
|
||||
*/
|
||||
export function copyToClipboard(text: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查浏览器是否支持 Clipboard API
|
||||
if (!navigator.clipboard) {
|
||||
reject(t("prompt['Your browser does not support the Clipboard API, please use another browser']"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 Clipboard API 复制文本
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
resolve(t("prompt['Successfully copied to clipboard']"));
|
||||
}).catch(err => {
|
||||
resolve(`${t("prompt['Failed to copy to clipboard']")}: ${err}`);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取rem的px值
|
||||
*/
|
||||
export function remToPxNumber(rem: number): number {
|
||||
const f = parseFloat(document.documentElement.style.fontSize);
|
||||
return f * rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态注入脚本,并监听执行完毕事件
|
||||
* @param {string} src
|
||||
*/
|
||||
export function injectJS(src) {
|
||||
return new Promise(resolve => {
|
||||
// Warn:script.src !== script.getAttribute('src')
|
||||
const loaded = Array.from(document.scripts).some(it => it.getAttribute('src') === src);
|
||||
if (loaded) {
|
||||
resolve(src);
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
document.head.insertBefore(script, document.head.firstElementChild);
|
||||
script.addEventListener('load', () => {
|
||||
resolve(src);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树形结构中查找指定 key 的节点(深度优先搜索)
|
||||
*/
|
||||
export function findTreeNode(tree:TreeOption[], targetKey:string | number):TreeOption | null {
|
||||
// 检查树数据是否为数组
|
||||
if (!Array.isArray(tree)) return null;
|
||||
|
||||
for (const node of tree) {
|
||||
// 检查当前节点是否匹配
|
||||
if (node.key === targetKey) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// 递归搜索子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
const result = findTreeNode(node.children, targetKey);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用深度优先遍历(DFS)递归算法,为没有子节点的节点添加 isLeaf: true 属性
|
||||
*/
|
||||
export function markLeafNodes(tree:TreeOption[]) {
|
||||
if (!tree || !Array.isArray(tree)) return;
|
||||
|
||||
for (const node of tree) {
|
||||
// 如果有children数组且不为空
|
||||
if (node.children && node.children.length > 0) {
|
||||
// 递归处理子节点
|
||||
markLeafNodes(node.children);
|
||||
}
|
||||
// 如果children不存在,或者存在但为空数组
|
||||
else {
|
||||
// 设置 isLeaf 属性(不覆盖已存在的值)
|
||||
if (!node.hasOwnProperty('isLeaf')) {
|
||||
node.isLeaf = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 用于验证的工具函数集合
|
||||
*/
|
||||
import {t} from "@/language";
|
||||
|
||||
export function formItemIsFile(_, value:File | null){
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// 判断value是否是File类型
|
||||
if (!value || !(value instanceof File)) {
|
||||
reject(Error(t("prompt.The entry can not be null")))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function formItemNotNil(_, value:any){
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (value === null || value === undefined) {
|
||||
reject(Error(t("prompt.The entry can not be null")))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
import {App,Utils,Hooks} from "@astral3d/engine";
|
||||
import {getSelectedModelPath} from "@/utils/common/scenes";
|
||||
|
||||
let canvasMouseWheelFn: EventListenerOrEventListenerObject;
|
||||
|
||||
/**
|
||||
* 画布中绘制矩形
|
||||
* @param {HTMLCanvasElement} canvas 画布对象
|
||||
* @param {Array<IDrawingMark>} list 矩形数组
|
||||
**/
|
||||
export class DrawRect {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private parentElement: HTMLDivElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private readonly list: Array<IDrawingMark>;
|
||||
// 当前选中的矩形下标
|
||||
public selectRectIndex: number = -1;
|
||||
// 当前鼠标经过的矩形的下标
|
||||
private hoverRectIndex: number = -1;
|
||||
private sX: number = 0;
|
||||
private sY: number = 0;
|
||||
// 鼠标按下时的clientXY
|
||||
private downClientX: number = 0;
|
||||
private downClientY: number = 0;
|
||||
private zoom: number = 100;
|
||||
// 鼠标左键是否按下
|
||||
private leftMouseDown: boolean = false;
|
||||
// 画布是否处于拖动状态
|
||||
private isCanvasDrag: boolean = false;
|
||||
// 画布拖动后的偏移量
|
||||
private canvasOffsetX: number = 0;
|
||||
private canvasOffsetY: number = 0;
|
||||
// rect是否处于拖动状态
|
||||
private isDrag: boolean = false;
|
||||
private isDraged: boolean = false;
|
||||
|
||||
//杂项
|
||||
public rectColor: string = "#15FF00";
|
||||
public rectSelectColor: string = "#ff0000";
|
||||
|
||||
// 拖动的rect的初始数据
|
||||
private dragRect: IDrawingMark = {x: 0, y: 0, w: 0, h: 0,color:this.rectColor};
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, parentElement: HTMLDivElement) {
|
||||
this.canvas = canvas;
|
||||
this.parentElement = parentElement;
|
||||
|
||||
this.list = App.project.getKey("drawing.markList");
|
||||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
this.ctx.strokeStyle = this.rectColor;
|
||||
this.ctx.lineWidth = 1;
|
||||
|
||||
this.canvas.onmousemove = Utils.throttle(this.onmousemove.bind(this), 10);
|
||||
this.canvas.onmousedown = this.onmousedown.bind(this);
|
||||
this.canvas.onmouseup = this.onmouseup.bind(this);
|
||||
this.canvas.onmouseleave = this.onmouseleave.bind(this);
|
||||
canvasMouseWheelFn = this.onmousewheel.bind(this);
|
||||
this.canvas.addEventListener("mousewheel", canvasMouseWheelFn);
|
||||
|
||||
const drawingDiv = this.parentElement.parentElement as HTMLDivElement;
|
||||
drawingDiv.onmousedown = this.onParentMouseDown.bind(this);
|
||||
drawingDiv.onmousemove = Utils.throttle(this.onParentMouseMove.bind(this),16);
|
||||
drawingDiv.onmouseup = this.onParentMouseUp.bind(this);
|
||||
drawingDiv.onmouseleave = this.onParentMouseLeave.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(){
|
||||
// 若list长度不为0, 则显示已标记框
|
||||
if (this.list.length !== 0) {
|
||||
this.list.forEach((value: IDrawingMark) => {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = value.color as string;
|
||||
// 遍历绘制所有标记框
|
||||
this.ctx.rect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.stroke();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备开始画矩形标记框
|
||||
* @public
|
||||
*/
|
||||
public addRect() {
|
||||
this.canvas.style.cursor = "crosshair";
|
||||
// 进入绘制流程
|
||||
App.project.setKey("drawing.isDrawingRect",true);
|
||||
}
|
||||
|
||||
/*
|
||||
* 退出绘制矩形标记框
|
||||
*/
|
||||
public exitRect() {
|
||||
this.canvas.style.cursor = "default";
|
||||
// 退出绘制流程
|
||||
App.project.setKey("drawing.isDrawingRect",false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除rect
|
||||
*/
|
||||
public deleteRect() {
|
||||
this.list.splice(this.selectRectIndex, 1);
|
||||
this.hoverRectIndex = -1;
|
||||
this.selectRectIndex = -1;
|
||||
this.reDrawCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图纸复位
|
||||
*/
|
||||
public canvasReset(){
|
||||
this.parentElement.style.left = "0px";
|
||||
this.parentElement.style.top = "0px";
|
||||
this.canvasOffsetX = 0;
|
||||
this.canvasOffsetY = 0;
|
||||
this.canvas.style.transform = "scale(1)";
|
||||
|
||||
this.zoom = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改当前绘制的颜色
|
||||
* @param {string} color 颜色
|
||||
*/
|
||||
public setRectColor(color: string) {
|
||||
this.rectColor = color;
|
||||
this.list[this.selectRectIndex].color = color;
|
||||
|
||||
this.reDrawCanvas(true);
|
||||
setTimeout(()=>{
|
||||
this.reDrawCanvas(false);
|
||||
},800)
|
||||
}
|
||||
|
||||
get selectRectColor(){
|
||||
return this.list[this.selectRectIndex]?.color;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮选中的模型对应的rect
|
||||
* @param {string} uuid modelUuid
|
||||
*/
|
||||
public selectRect(uuid: string) {
|
||||
this.selectRectIndex = -1;
|
||||
|
||||
this.list.forEach((item, index) => {
|
||||
if (item.modelUuid !== uuid) return;
|
||||
|
||||
this.selectRectIndex = index;
|
||||
|
||||
if(this.isDrag) return;
|
||||
|
||||
const translate = (zoom:number) => {
|
||||
setTimeout(() => {
|
||||
// 移动画布,使对应标记居中放大显示(按当前缩放比)
|
||||
const x = item.x + item.w / 2;
|
||||
const y = item.y + item.h / 2;
|
||||
this.parentElement.style.transition = "all .6s";
|
||||
this.canvasOffsetX = (-x + this.canvas.offsetWidth / 2) * zoom;
|
||||
this.canvasOffsetY = (-y + this.canvas.offsetHeight / 2) * zoom;
|
||||
this.parentElement.style.left = this.canvasOffsetX + "px";
|
||||
this.parentElement.style.top = this.canvasOffsetY + "px";
|
||||
|
||||
setTimeout(() => {
|
||||
this.parentElement.style.transition = "none";
|
||||
this.canvas.style.transition = "transform 16ms";
|
||||
}, 600)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 对应标记框如果宽高最大值小于100px,则显示为100像素大小
|
||||
const max = Math.max(item.w, item.h);
|
||||
if (max < 100) {
|
||||
const z = 100 * 100 / max;
|
||||
this.zoom = z < 50 ? 50 : (z > 800 ? 800 : z);
|
||||
}else{
|
||||
this.zoom = 100;
|
||||
}
|
||||
|
||||
const zoom = this.zoom / 100;
|
||||
this.canvas.style.transition = "transform .35s";
|
||||
this.canvas.style.transform = "scale(" + zoom + ")";
|
||||
|
||||
translate(zoom);
|
||||
})
|
||||
|
||||
this.reDrawCanvas();
|
||||
|
||||
App.project.setKey("drawing.selectedRectIndex",this.selectRectIndex);
|
||||
}
|
||||
|
||||
private onmousemove(em) {
|
||||
if(em.button !== 0) return;
|
||||
|
||||
if (this.isCanvasDrag)return;
|
||||
|
||||
// 如果处于绘制流程中
|
||||
if (App.project.getKey("drawing.isDrawingRect")) {
|
||||
this.canvas.style.cursor = "crosshair";
|
||||
|
||||
if (this.leftMouseDown) {
|
||||
// 正在绘制矩形
|
||||
// 如果是处于修改矩形流程中
|
||||
if (this.selectRectIndex !== -1) {
|
||||
this.list.splice(this.selectRectIndex, 1, {
|
||||
x: this.sX,
|
||||
y: this.sY,
|
||||
w: em.offsetX - this.sX,
|
||||
h: em.offsetY - this.sY,
|
||||
color:this.list[this.selectRectIndex].color,
|
||||
modelUuid: this.list[this.selectRectIndex].modelUuid,
|
||||
modelPath: this.list[this.selectRectIndex].modelPath
|
||||
});
|
||||
|
||||
this.reDrawCanvas();
|
||||
} else {
|
||||
this.reDrawCanvas();
|
||||
// 设置边框为虚线
|
||||
this.ctx.beginPath();
|
||||
this.ctx.setLineDash([8, 4]);
|
||||
this.ctx.rect(this.sX, this.sY, em.offsetX - this.sX, em.offsetY - this.sY);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/** rect 正在拖动 **/
|
||||
if (this.isDrag) {
|
||||
this.list.splice(this.selectRectIndex, 1, {
|
||||
x: this.dragRect.x + (em.offsetX - this.sX),
|
||||
y: this.dragRect.y + (em.offsetY - this.sY),
|
||||
w: this.dragRect.w,
|
||||
h: this.dragRect.h,
|
||||
color:this.dragRect.color,
|
||||
modelUuid: this.dragRect.modelUuid,
|
||||
modelPath: this.dragRect.modelPath
|
||||
});
|
||||
|
||||
this.reDrawCanvas();
|
||||
this.isDraged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.sX = em.offsetX;
|
||||
this.sY = em.offsetY;
|
||||
|
||||
/** 界面上无矩形 **/
|
||||
if (this.list.length === 0) return;
|
||||
|
||||
/** 界面上有矩形 **/
|
||||
this.list.forEach((item, index) => {
|
||||
let path = new Path2D();
|
||||
|
||||
if (this.selectRectIndex === index) {
|
||||
path.rect(item.x - 4, item.y - 4, item.w + 8, item.h + 8);
|
||||
} else {
|
||||
path.rect(item.x, item.y, item.w, item.h);
|
||||
}
|
||||
|
||||
if (this.ctx.isPointInPath(path, em.offsetX, em.offsetY)) {
|
||||
// 鼠标在矩形内
|
||||
this.hoverRectIndex = index;
|
||||
|
||||
this.canvas.style.cursor = "pointer";
|
||||
} else {
|
||||
// 如果鼠标不在之前所在的矩形内,清除hoverRectIndex
|
||||
if (this.hoverRectIndex === index) {
|
||||
this.hoverRectIndex = -1;
|
||||
}
|
||||
|
||||
this.canvas.style.cursor = "default";
|
||||
}
|
||||
this.reDrawCanvas();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标按下时
|
||||
* @param ed
|
||||
* @private
|
||||
*/
|
||||
private onmousedown(ed) {
|
||||
ed.stopPropagation();
|
||||
|
||||
if(ed.button !== 0) return;
|
||||
|
||||
this.isDraged = false;
|
||||
this.leftMouseDown = true;
|
||||
// 记录按下位置(矩形绘制起始位置)
|
||||
this.sX = ed.offsetX;
|
||||
this.sY = ed.offsetY;
|
||||
|
||||
this.downClientX = ed.clientX;
|
||||
this.downClientY = ed.clientY;
|
||||
|
||||
// 如果处于绘制流程中
|
||||
if (App.project.getKey("drawing.isDrawingRect")) return;
|
||||
|
||||
/** 如果鼠标按下时鼠标在矩形内 **/
|
||||
if (this.hoverRectIndex !== -1) {
|
||||
// 还未选中过模型 或者 此时点击的不是之前选中的模型
|
||||
if (this.selectRectIndex === -1 || this.hoverRectIndex !== this.selectRectIndex) {
|
||||
// 选中矩形 this.hoverRectIndex
|
||||
this.selectRectIndex = this.hoverRectIndex;
|
||||
}
|
||||
this.handleMouseDown(ed.offsetX, ed.offsetY);
|
||||
} else {
|
||||
this.selectRectIndex = -1;
|
||||
|
||||
// 如果鼠标按下时鼠标不在矩形内,拖动画布
|
||||
this.isCanvasDrag = true;
|
||||
}
|
||||
|
||||
App.project.setKey("drawing.selectedRectIndex",this.selectRectIndex);
|
||||
this.reDrawCanvas();
|
||||
}
|
||||
|
||||
private handleMouseDown(offsetX, offsetY) {
|
||||
const selectRect = this.list[this.selectRectIndex];
|
||||
// 判断鼠标点击的是四个角(缩放)还是其他区域(拖动)
|
||||
const x = offsetX - selectRect.x;
|
||||
const y = offsetY - selectRect.y;
|
||||
if (x < 5 && y < 5) {
|
||||
// 左上角
|
||||
this.sX = selectRect.x + selectRect.w;
|
||||
this.sY = selectRect.y + selectRect.h;
|
||||
|
||||
App.project.setKey("drawing.isDrawingRect",true);
|
||||
} else if (x > this.list[this.selectRectIndex].w - 5 && y < 5) {
|
||||
// 右上角
|
||||
this.sX = selectRect.x;
|
||||
this.sY = selectRect.y + selectRect.h;
|
||||
|
||||
App.project.setKey("drawing.isDrawingRect",true);
|
||||
} else if (x < 10 && y > this.list[this.selectRectIndex].h - 5) {
|
||||
// 左下角
|
||||
this.sX = selectRect.x + selectRect.w;
|
||||
this.sY = selectRect.y;
|
||||
|
||||
App.project.setKey("drawing.isDrawingRect",true);
|
||||
} else if (x > this.list[this.selectRectIndex].w - 5 && y > this.list[this.selectRectIndex].h - 5) {
|
||||
// 右下角
|
||||
this.sX = selectRect.x;
|
||||
this.sY = selectRect.y;
|
||||
|
||||
App.project.setKey("drawing.isDrawingRect",true);
|
||||
} else {
|
||||
// 拖动
|
||||
this.canvas.style.cursor = "move";
|
||||
this.dragRect = selectRect;
|
||||
this.isDrag = true;
|
||||
|
||||
// 三维场景定位模型
|
||||
if(selectRect.modelUuid){
|
||||
const model = App.getObjectByUuid(selectRect.modelUuid);
|
||||
Hooks.useDispatchSignal("objectFocused",model);
|
||||
Hooks.useDispatchSignal("objectSelected",model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标抬起时
|
||||
* @private
|
||||
*/
|
||||
private onmouseup(eu) {
|
||||
if(eu.button !== 0) return;
|
||||
|
||||
this.leftMouseDown = false;
|
||||
this.isCanvasDrag = false;
|
||||
|
||||
// 如果处于绘制流程中
|
||||
if (App.project.getKey("drawing.isDrawingRect")) {
|
||||
// 判断是新建矩形还是修改
|
||||
if (this.selectRectIndex === -1) {
|
||||
const w = eu.offsetX - this.sX;
|
||||
const h = eu.offsetY - this.sY;
|
||||
// 矩形w,h都大于5时才添加
|
||||
if (Math.abs(w) > 5 && Math.abs(h) > 5) {
|
||||
// 全取左上角点x,y,使得w,h为正数
|
||||
const rectItem = {
|
||||
x: w > 0 ? this.sX : this.sX + w,
|
||||
y: h > 0 ? this.sY : this.sY + h,
|
||||
w: Math.abs(w),
|
||||
h: Math.abs(h),
|
||||
color:this.rectColor,
|
||||
modelUuid: App.selected?.uuid,
|
||||
modelPath:getSelectedModelPath(),
|
||||
}
|
||||
this.list.push(rectItem);
|
||||
|
||||
Hooks.useDispatchSignal("drawingMarkDone","add",rectItem);
|
||||
}
|
||||
} else {
|
||||
const rectItem = {
|
||||
// 全取左上角点x,y,使得w,h为正数
|
||||
x: this.list[this.selectRectIndex].w > 0 ? this.sX : this.sX + this.list[this.selectRectIndex].w,
|
||||
y: this.list[this.selectRectIndex].h > 0 ? this.sY : this.sY + this.list[this.selectRectIndex].h,
|
||||
w: Math.abs(this.list[this.selectRectIndex].w),
|
||||
h: Math.abs(this.list[this.selectRectIndex].h),
|
||||
color:this.list[this.selectRectIndex].color,
|
||||
modelUuid: this.list[this.selectRectIndex].modelUuid,
|
||||
modelPath: this.list[this.selectRectIndex].modelPath,
|
||||
}
|
||||
this.list.splice(this.selectRectIndex, 1, rectItem);
|
||||
|
||||
Hooks.useDispatchSignal("drawingMarkDone","update",rectItem);
|
||||
}
|
||||
|
||||
this.reDrawCanvas();
|
||||
|
||||
//退出绘制流
|
||||
this.exitRect();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果处于拖动流程中
|
||||
if (this.isDrag) {
|
||||
if(this.isDraged){
|
||||
Hooks.useDispatchSignal("drawingMarkDone","update",this.list[this.selectRectIndex]);
|
||||
}
|
||||
|
||||
this.isDrag = false;
|
||||
this.canvas.style.cursor = "default";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private onmouseleave(){
|
||||
this.isDrag = false;
|
||||
}
|
||||
|
||||
private onmousewheel(event){
|
||||
/**
|
||||
* 获取当前页面的缩放比
|
||||
* 若未设置zoom缩放比,则为默认100%,即1,原图大小
|
||||
*/
|
||||
/* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动 */
|
||||
let z = event.wheelDelta;
|
||||
if (Math.abs(event.wheelDelta) > 120) {
|
||||
z = event.wheelDelta > 0 ? 120 : -120;
|
||||
}
|
||||
|
||||
const lastZoom = this.zoom;
|
||||
this.zoom += z / 12;
|
||||
/* 最小范围 和 最大范围 的图片缩放尺度 */
|
||||
if ( this.zoom >= 50 && this.zoom <= 800) {
|
||||
this.canvas.style.transform = "scale(" + this.zoom / 100 + ")";
|
||||
}else{
|
||||
this.zoom = lastZoom;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新绘制画布
|
||||
*/
|
||||
reDrawCanvas(showSelectLineColor = false) {
|
||||
this.ctx.setLineDash([8, 0]);
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.list.forEach((value, index) => {
|
||||
if (index === this.selectRectIndex) {
|
||||
const r = value.w > 10 ? 4 : 2;
|
||||
/* 绘制选中部分 */
|
||||
/* 绘制方框 */
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = showSelectLineColor ? value.color as string : this.rectSelectColor;
|
||||
this.ctx.rect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.fillStyle = 'RGBA(102,102,102,0.2)'
|
||||
this.ctx.fillRect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.stroke();
|
||||
/* 绘制四个角的点 */
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = this.rectSelectColor;
|
||||
this.ctx.arc(value.x, value.y, r, 0, Math.PI * 2)
|
||||
this.ctx.fillStyle = this.rectSelectColor;
|
||||
this.ctx.fill();// 画起点实心圆
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(value.x, value.y + value.h, r, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = this.rectSelectColor;
|
||||
this.ctx.fill();// 画起点纵向实心圆
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(value.x + value.w, value.y + value.h, r, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = this.rectSelectColor;
|
||||
this.ctx.fill();// 画起点横向实心圆
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(value.x + value.w, value.y, r, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = this.rectSelectColor;
|
||||
this.ctx.fill();// 画终点实心圆
|
||||
this.ctx.stroke();
|
||||
} else if (this.hoverRectIndex === index) {
|
||||
/* 绘制鼠标经过部分 */
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = value.color as string;
|
||||
this.ctx.rect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.fillStyle = 'RGBA(102,102,102,0.2)';
|
||||
this.ctx.fillRect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.stroke();
|
||||
} else {
|
||||
/* 绘制未选中部分 */
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = value.color as string;
|
||||
this.ctx.rect(value.x, value.y, value.w, value.h);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
App.project.setKey("drawing.markList",this.list);
|
||||
}
|
||||
|
||||
/* 父级相关事件监听 */
|
||||
private onParentMouseDown(e){
|
||||
this.downClientX = e.clientX;
|
||||
this.downClientY = e.clientY;
|
||||
|
||||
this.isCanvasDrag = true;
|
||||
}
|
||||
|
||||
private onParentMouseUp(){
|
||||
this.isCanvasDrag = false;
|
||||
|
||||
this.canvasOffsetX = this.parentElement.offsetLeft;
|
||||
this.canvasOffsetY = this.parentElement.offsetTop;
|
||||
}
|
||||
|
||||
private onParentMouseMove(e){
|
||||
/** 画布正在拖动 **/
|
||||
if (this.isCanvasDrag) {
|
||||
this.parentElement.style.left = this.canvasOffsetX + e.clientX - this.downClientX + "px";
|
||||
this.parentElement.style.top = this.canvasOffsetY + e.clientY - this.downClientY + "px";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private onParentMouseLeave(){
|
||||
this.isCanvasDrag = false;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.canvas.onmousemove = null;
|
||||
this.canvas.onmousedown = null;
|
||||
this.canvas.onmouseup = null;
|
||||
this.canvas.onmouseleave = null;
|
||||
this.canvas.removeEventListener("mousewheel", canvasMouseWheelFn);
|
||||
|
||||
const drawingDiv = this.parentElement.parentElement as HTMLDivElement;
|
||||
drawingDiv.onmousedown = null;
|
||||
drawingDiv.onmousemove = null;
|
||||
drawingDiv.onmouseup = null;
|
||||
drawingDiv.onmouseleave = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2024/8/22
|
||||
* @description 预览菜单操作
|
||||
*/
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import {
|
||||
IPreviewOperation,
|
||||
usePreviewOperationStoreWithOut
|
||||
} from "@/store/modules/previewOperation";
|
||||
import * as THREE from 'three';
|
||||
import { App,Hooks,ClippedEdgesBox,Measure,MeasureMode,ModelExplode,Roaming,MiniMap } from "@astral3d/engine";
|
||||
import { t } from "@/language";
|
||||
|
||||
const operationStore = usePreviewOperationStoreWithOut();
|
||||
|
||||
const { enter:enterFullscreen,exit:exitFullscreen } = useFullscreen();
|
||||
|
||||
let roamPdFn, pointerlockFn;
|
||||
|
||||
export class MenuOperation {
|
||||
// 初始化控制器状态
|
||||
static InitControlsState: string = "{}";
|
||||
|
||||
// 开始漫游前的相机位置
|
||||
static lastRoadCameraPos = new THREE.Vector3();
|
||||
|
||||
// 开始漫游前的相机目标位置
|
||||
static lastRoadCameraTarget = new THREE.Vector3();
|
||||
|
||||
// 模型爆炸专用图层
|
||||
static explodeLayer = 10;
|
||||
|
||||
// 当前爆炸的模型
|
||||
static explodeModel:THREE.Object3D | null = null;
|
||||
|
||||
// 剖切盒子
|
||||
static _clippedEdgesBox:ClippedEdgesBox | null = null;
|
||||
|
||||
// 测量类
|
||||
static _measure:Measure | null = null;
|
||||
|
||||
// 爆炸类
|
||||
static _explode:ModelExplode | null = null;
|
||||
|
||||
// 漫游类
|
||||
static _roaming:Roaming | null = null;
|
||||
|
||||
// 小地图
|
||||
static _miniMap:MiniMap | null = null;
|
||||
|
||||
static Init(key: string) {
|
||||
if (MenuOperation[key]) {
|
||||
MenuOperation[key]();
|
||||
} else {
|
||||
window.$message?.warning("相关模块正在开发中...")
|
||||
}
|
||||
}
|
||||
|
||||
static get ClippedEdgesBox(){
|
||||
if(!MenuOperation._clippedEdgesBox){
|
||||
MenuOperation._clippedEdgesBox = new ClippedEdgesBox(window.viewer);
|
||||
}
|
||||
|
||||
return MenuOperation._clippedEdgesBox;
|
||||
}
|
||||
|
||||
static get Measure():Measure{
|
||||
if(!MenuOperation._measure){
|
||||
MenuOperation._measure = new Measure(window.viewer,MeasureMode.Distance);
|
||||
|
||||
MenuOperation._measure.addEventListener("complete",() => {
|
||||
if(!MenuOperation._measure) return;
|
||||
|
||||
// 激活清除测量按钮
|
||||
if (!MenuOperation._measure.isClose && MenuOperation._measure.measureGroup.children.length > 0) {
|
||||
(<{ [key: string]: IPreviewOperation }>operationStore.menuList.measure.children).clearMeasure.disabled = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return MenuOperation._measure;
|
||||
}
|
||||
|
||||
static get Explode():ModelExplode{
|
||||
if(!MenuOperation._explode){
|
||||
MenuOperation._explode = new ModelExplode();
|
||||
}
|
||||
|
||||
return MenuOperation._explode;
|
||||
}
|
||||
|
||||
static get Roaming():Roaming{
|
||||
if(!MenuOperation._roaming){
|
||||
MenuOperation._roaming = new Roaming(window.viewer);
|
||||
}
|
||||
|
||||
return MenuOperation._roaming;
|
||||
}
|
||||
|
||||
static get MiniMap():MiniMap{
|
||||
if(!MenuOperation._miniMap){
|
||||
MenuOperation._miniMap = new MiniMap(window.viewer,{
|
||||
mapSize: 100,
|
||||
mapRenderSize: 350,
|
||||
followTarget: window.viewer.camera,
|
||||
isShow: false,
|
||||
});
|
||||
}
|
||||
|
||||
return MenuOperation._miniMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原视角
|
||||
*/
|
||||
static toHome() {
|
||||
if (MenuOperation.InitControlsState === "{}") {
|
||||
window.$message?.warning("缺失初始视角信息")
|
||||
return;
|
||||
}
|
||||
|
||||
window.viewer.modules.controls.fromJSON(MenuOperation.InitControlsState, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动旋转
|
||||
*/
|
||||
static autoRotate() {
|
||||
operationStore.menuList.autoRotate.active = !operationStore.menuList.autoRotate.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* 剖切
|
||||
*/
|
||||
static cutting() {
|
||||
operationStore.menuList.cutting.active = !operationStore.menuList.cutting.active;
|
||||
|
||||
if (operationStore.menuList.cutting.active) {
|
||||
MenuOperation.ClippedEdgesBox.open();
|
||||
} else {
|
||||
MenuOperation.ClippedEdgesBox.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 测距
|
||||
static distance() {
|
||||
// 上一个测量也许未完成
|
||||
if (!MenuOperation.Measure.isCompleted) {
|
||||
MenuOperation.Measure.complete();
|
||||
}
|
||||
|
||||
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
MenuOperation.Measure.mode = MeasureMode.Distance;
|
||||
MenuOperation.Measure.open();
|
||||
|
||||
operationStore.menuList.measure.active = true;
|
||||
}
|
||||
|
||||
// 测角度
|
||||
static angle() {
|
||||
if (!MenuOperation.Measure.isCompleted) {
|
||||
MenuOperation.Measure.complete();
|
||||
}
|
||||
|
||||
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
MenuOperation.Measure.mode = MeasureMode.Angle;
|
||||
MenuOperation.Measure.open();
|
||||
|
||||
operationStore.menuList.measure.active = true;
|
||||
}
|
||||
|
||||
// 测面积
|
||||
static area() {
|
||||
if (!MenuOperation.Measure.isCompleted) {
|
||||
MenuOperation.Measure.complete();
|
||||
}
|
||||
|
||||
window.$message?.info(t("prompt['Left click to confirm the drawing point, and right click to complete the drawing.']"),{
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
MenuOperation.Measure.mode = MeasureMode.Area;
|
||||
MenuOperation.Measure.open();
|
||||
|
||||
operationStore.menuList.measure.active = true;
|
||||
}
|
||||
|
||||
// 清除测量结果
|
||||
static clearMeasure() {
|
||||
(<{ [key: string]: IPreviewOperation }>operationStore.menuList.measure.children).clearMeasure.disabled = true;
|
||||
|
||||
MenuOperation.Measure.clear();
|
||||
|
||||
operationStore.menuList.measure.active = false;
|
||||
}
|
||||
|
||||
// 爆炸
|
||||
static explode(){
|
||||
if(!App.selected && !MenuOperation.explodeModel){
|
||||
window.$message?.warning(t("prompt['No object selected.']"));
|
||||
return;
|
||||
}
|
||||
|
||||
operationStore.menuList.explode.active = !operationStore.menuList.explode.active;
|
||||
|
||||
if (operationStore.menuList.explode.active){
|
||||
if(!App.selected) return;
|
||||
|
||||
App.selected.traverse(obj => obj.layers.set(MenuOperation.explodeLayer));
|
||||
window.viewer.camera.layers.set(MenuOperation.explodeLayer);
|
||||
|
||||
MenuOperation.Explode.explodeModel(App.selected,operationStore.explodeScalar);
|
||||
MenuOperation.explodeModel = App.selected;
|
||||
}else{
|
||||
if(!MenuOperation.explodeModel) return;
|
||||
|
||||
MenuOperation.explodeModel.traverse(obj => obj.layers.set(0));
|
||||
window.viewer.camera.layers.set(0);
|
||||
|
||||
MenuOperation.Explode.restore();
|
||||
|
||||
MenuOperation.explodeModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 漫游
|
||||
static roaming() {
|
||||
if (!MenuOperation.Roaming) return;
|
||||
|
||||
operationStore.menuList.roaming.active = !operationStore.menuList.roaming.active;
|
||||
|
||||
if (operationStore.menuList.roaming.active) {
|
||||
MenuOperation.enterRoaming();
|
||||
} else {
|
||||
MenuOperation.leaveRoaming();
|
||||
}
|
||||
}
|
||||
|
||||
// 选点进入漫游
|
||||
static enterRoaming() {
|
||||
window.$message?.info(t("preview.Please select initial position"));
|
||||
|
||||
const canvas = window.viewer.renderer.domElement;
|
||||
|
||||
const handlePointerDown = (e: MouseEvent) => {
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
const x = e.offsetX;
|
||||
const y = e.offsetY;
|
||||
const mouse = new THREE.Vector2();
|
||||
mouse.x = (x / canvas.offsetWidth) * 2 - 1;
|
||||
mouse.y = -(y / canvas.offsetHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, window.viewer.camera);
|
||||
raycaster.firstHitOnly = true;
|
||||
|
||||
const intersectObjects:THREE.Object3D[] = [MenuOperation.Roaming.group];
|
||||
if(window.viewer.modules.tilesManage.mergeMesh){
|
||||
intersectObjects.push(window.viewer.modules.tilesManage.mergeMesh);
|
||||
}
|
||||
let intersects = raycaster.intersectObjects(intersectObjects, true) || [];
|
||||
if (intersects && intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
|
||||
// 锁定鼠标指针
|
||||
window.viewer.modules.controls.lockPointer();
|
||||
|
||||
canvas.removeEventListener("pointerdown", roamPdFn);
|
||||
roamPdFn = undefined;
|
||||
|
||||
window.viewer.modules.controls.getTarget(MenuOperation.lastRoadCameraTarget);
|
||||
window.viewer.modules.controls.getPosition(MenuOperation.lastRoadCameraPos);
|
||||
|
||||
const point = new THREE.Vector3(intersect.point.x, intersect.point.y + 2, intersect.point.z);
|
||||
MenuOperation.Roaming.playerInitPos.copy(point);
|
||||
|
||||
MenuOperation.Roaming.startRoaming();
|
||||
|
||||
// 第三人称
|
||||
window.viewer.modules.controls.maxPolarAngle = Math.PI / 2;
|
||||
window.viewer.modules.controls.minDistance = 0.8;
|
||||
window.viewer.modules.controls.maxDistance = 0.8;
|
||||
window.viewer.modules.controls.distance = 0.8;
|
||||
|
||||
Hooks.useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pointerlockFn = () => {
|
||||
if (document.pointerLockElement) {
|
||||
// console.log("指针被锁定到:", document.pointerLockElement);
|
||||
} else {
|
||||
// console.log("指针锁定状态现已解锁");
|
||||
MenuOperation.roaming();
|
||||
}
|
||||
}
|
||||
// 监听鼠标锁定取消
|
||||
document.addEventListener("pointerlockchange", pointerlockFn);
|
||||
|
||||
// 监听选取初始位置
|
||||
roamPdFn = handlePointerDown.bind(this);
|
||||
canvas.addEventListener("pointerdown", roamPdFn);
|
||||
}
|
||||
|
||||
// 退出漫游
|
||||
static leaveRoaming() {
|
||||
MenuOperation.Roaming.exitRoaming(MenuOperation.lastRoadCameraPos,MenuOperation.lastRoadCameraTarget);
|
||||
|
||||
if (roamPdFn) {
|
||||
window.viewer.renderer.domElement.removeEventListener("pointerdown", roamPdFn);
|
||||
roamPdFn = null;
|
||||
}
|
||||
|
||||
if (pointerlockFn) {
|
||||
document.removeEventListener("pointerlockchange", pointerlockFn);
|
||||
pointerlockFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 小地图
|
||||
static miniMap() {
|
||||
if (MenuOperation.MiniMap.isShow) {
|
||||
operationStore.menuList.miniMap.active = false;
|
||||
MenuOperation.MiniMap.close();
|
||||
} else {
|
||||
operationStore.menuList.miniMap.active = true;
|
||||
MenuOperation.MiniMap.open();
|
||||
}
|
||||
}
|
||||
|
||||
// 设置
|
||||
static settings(){
|
||||
operationStore.menuList.settings.active = !operationStore.menuList.settings.active;
|
||||
}
|
||||
|
||||
static fullscreen() {
|
||||
operationStore.menuList.fullscreen.show = false;
|
||||
operationStore.menuList.exitFullscreen.show = true;
|
||||
|
||||
enterFullscreen();
|
||||
}
|
||||
|
||||
static exitFullscreen() {
|
||||
operationStore.menuList.fullscreen.show = true;
|
||||
operationStore.menuList.exitFullscreen.show = false;
|
||||
|
||||
exitFullscreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { AxiosError, AxiosResponse } from 'axios';
|
||||
import {
|
||||
DEFAULT_REQUEST_ERROR_CODE,
|
||||
DEFAULT_REQUEST_ERROR_MSG,
|
||||
ERROR_STATUS,
|
||||
NETWORK_ERROR_CODE,
|
||||
NETWORK_ERROR_MSG,
|
||||
REQUEST_TIMEOUT_CODE,
|
||||
REQUEST_TIMEOUT_MSG
|
||||
} from '@/config/service';
|
||||
import { showErrorMsg } from './msg';
|
||||
import {Service} from "../../../types/network";
|
||||
|
||||
type ErrorStatus = keyof typeof ERROR_STATUS;
|
||||
|
||||
/**
|
||||
* 策略模式
|
||||
* @param actions 每一种可能执行的操作
|
||||
*/
|
||||
export function exeStrategyActions(actions: Common.StrategyAction[]) {
|
||||
actions.some(item => {
|
||||
const [flag, action] = item;
|
||||
if (flag) {
|
||||
action();
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理axios请求失败的错误
|
||||
* @param axiosError - 错误
|
||||
*/
|
||||
export function handleAxiosError(axiosError: AxiosError) {
|
||||
const error: Service.RequestError = {
|
||||
type: 'axios',
|
||||
code: DEFAULT_REQUEST_ERROR_CODE,
|
||||
msg: DEFAULT_REQUEST_ERROR_MSG
|
||||
};
|
||||
|
||||
const actions: Common.StrategyAction[] = [
|
||||
[
|
||||
// 网路错误
|
||||
!window.navigator.onLine || axiosError.message === 'Network Error',
|
||||
() => {
|
||||
Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG });
|
||||
}
|
||||
],
|
||||
[
|
||||
// 超时错误
|
||||
axiosError.code === REQUEST_TIMEOUT_CODE && axiosError.message.includes('timeout'),
|
||||
() => {
|
||||
Object.assign(error, { code: REQUEST_TIMEOUT_CODE, msg: REQUEST_TIMEOUT_MSG });
|
||||
}
|
||||
],
|
||||
[
|
||||
// 请求不成功的错误
|
||||
Boolean(axiosError.response),
|
||||
() => {
|
||||
const errorCode: ErrorStatus = (axiosError.response?.status as ErrorStatus) || 'DEFAULT';
|
||||
// @ts-ignore
|
||||
const msg = axiosError.response?.data?.message || ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG;
|
||||
Object.assign(error, { code: errorCode, msg });
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
exeStrategyActions(actions);
|
||||
|
||||
showErrorMsg(error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求成功后的错误
|
||||
* @param response - 请求的响应
|
||||
*/
|
||||
export function handleResponseError(response: AxiosResponse) {
|
||||
const error: Service.RequestError = {
|
||||
type: 'axios',
|
||||
code: DEFAULT_REQUEST_ERROR_CODE,
|
||||
msg: DEFAULT_REQUEST_ERROR_MSG
|
||||
};
|
||||
|
||||
if (!window.navigator.onLine) {
|
||||
// 网路错误
|
||||
Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG });
|
||||
} else {
|
||||
// 请求成功的状态码非200的错误
|
||||
const errorCode: ErrorStatus = response.status as ErrorStatus;
|
||||
const msg = response.data.message || ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG;
|
||||
Object.assign(error, { type: 'http', code: errorCode, msg });
|
||||
}
|
||||
|
||||
showErrorMsg(error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后端返回的错误(业务错误)
|
||||
* @param backendResult - 后端接口的响应数据
|
||||
*/
|
||||
export function handleBackendError(backendResult: Record<string, any>, config: Service.BackendResultConfig) {
|
||||
const { codeKey, msgKey } = config;
|
||||
const error: Service.RequestError = {
|
||||
type: 'backend',
|
||||
code: backendResult[codeKey],
|
||||
msg: backendResult[msgKey]
|
||||
};
|
||||
|
||||
showErrorMsg(error);
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 创建一个handlerFetch
|
||||
*
|
||||
* @param limit 并发控制
|
||||
* @param timeout 超时设
|
||||
* @return function 返回一个函数
|
||||
*/
|
||||
export function fetchController (limit:number, timeout:number | boolean) {
|
||||
limit = limit || 1;
|
||||
timeout = timeout || false;
|
||||
let count = 0, pool:any = [];
|
||||
|
||||
return function (url:string, options?:any) {
|
||||
// 通过AbortController 控制 取消fetch 请求
|
||||
let controller = new AbortController();
|
||||
let signal = controller.signal;
|
||||
// 判断是否需要超时
|
||||
let isTimeout = options && options.timeout || timeout;
|
||||
|
||||
// 控制请求超时
|
||||
let timeoutPromise = () => {
|
||||
return new Promise((reject) => {
|
||||
setTimeout(() => {
|
||||
// resolve('请求超时');
|
||||
reject('请求超时');
|
||||
controller.abort();
|
||||
}, options?.timeout || timeout)
|
||||
})
|
||||
}
|
||||
|
||||
// 返回fetch 本身
|
||||
let taskPromise = () => new Promise((resolve, reject) => {
|
||||
fetch(url, { signal, ...options }).then(res => {
|
||||
resolve(res);
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
|
||||
// 通过Promise.race可以控制超时,并在访问结果中 去继续调用等待池中的请求
|
||||
let task = () => (isTimeout ? Promise.race([timeoutPromise(), taskPromise()]) : taskPromise())
|
||||
.then((res) => {
|
||||
options.onSuccess && options.onSuccess(res)
|
||||
next();
|
||||
})
|
||||
.catch((err) => {
|
||||
options.onError && options.onError(err)
|
||||
next();
|
||||
});
|
||||
|
||||
// 定一个next 控制等待队列中的请求继续并发调用
|
||||
let next = () => {
|
||||
// 每执行一次next count - 1,然后比较当前的count 与 limit
|
||||
// 如果小于limit 循环执行limit-count 次
|
||||
count--;
|
||||
if (count < limit && pool.length) {
|
||||
let n = limit - count;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let curTask:any = pool.shift();
|
||||
curTask();
|
||||
++count;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
|
||||
if (count >= limit) {
|
||||
pool.push(task);
|
||||
} else {
|
||||
++count;
|
||||
task();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {Service} from "~/network";
|
||||
|
||||
/** 统一失败和成功的请求结果的数据类型 */
|
||||
export async function handleServiceResult(error: Service.RequestError | null, data: any, other?: any) {
|
||||
if (error) {
|
||||
const fail: Service.FailedResult = {
|
||||
error,
|
||||
data: null
|
||||
};
|
||||
return fail;
|
||||
}
|
||||
const success: any = {
|
||||
error: null,
|
||||
data,
|
||||
other
|
||||
};
|
||||
return success;
|
||||
}
|
||||
|
||||
/** 请求结果的适配器:用于接收适配器函数和请求结果 */
|
||||
export function adapter<T extends Service.ServiceAdapter>(
|
||||
adapterFun: T,
|
||||
...args: Service.MultiRequestResult<any>
|
||||
): Service.RequestResult<ReturnType<T>> {
|
||||
let result: Service.RequestResult | undefined;
|
||||
|
||||
const hasError = args.some((item:any) => {
|
||||
const flag = Boolean(item.error);
|
||||
if (flag) {
|
||||
result = {
|
||||
error: item.error,
|
||||
data: null
|
||||
};
|
||||
}
|
||||
return flag;
|
||||
});
|
||||
|
||||
if (!hasError) {
|
||||
const adapterFunArgs = args.map(item => item.data);
|
||||
result = {
|
||||
error: null,
|
||||
data: adapterFun(...adapterFunArgs)
|
||||
};
|
||||
}
|
||||
|
||||
return result!;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './error';
|
||||
export * from './handler';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from '@/config/service';
|
||||
|
||||
/** 错误消息栈,防止同一错误同时出现 */
|
||||
const errorMsgStack = new Map<string | number, string>([]);
|
||||
|
||||
function addErrorMsg(error: Service.RequestError) {
|
||||
errorMsgStack.set(error.code, error.msg);
|
||||
}
|
||||
function removeErrorMsg(error: Service.RequestError) {
|
||||
errorMsgStack.delete(error.code);
|
||||
}
|
||||
function hasErrorMsg(error: Service.RequestError) {
|
||||
return errorMsgStack.has(error.code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
* @param error
|
||||
*/
|
||||
export function showErrorMsg(error: Service.RequestError) {
|
||||
if (!error.msg || NO_ERROR_MSG_CODE.includes(error.code) || hasErrorMsg(error)) return;
|
||||
|
||||
addErrorMsg(error);
|
||||
window.console.warn(error.code, error.msg);
|
||||
window.$message?.error(error.msg, { duration: ERROR_MSG_DURATION });
|
||||
setTimeout(() => {
|
||||
removeErrorMsg(error);
|
||||
}, ERROR_MSG_DURATION);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {Utils} from '@astral3d/engine';
|
||||
|
||||
/**
|
||||
* cesium 相关页面使用的
|
||||
*/
|
||||
const cesiumSignals = [
|
||||
// cesium/viewPort.ts 停止渲染循环
|
||||
"cesium_stopLoop",
|
||||
// cesium融合场景下的threejs scene点击事件
|
||||
"cesium_clickThreeScene",
|
||||
//销毁viewPort
|
||||
"cesium_destroy",
|
||||
// 飞行定位
|
||||
"cesium_flyTo",
|
||||
]
|
||||
|
||||
/**
|
||||
* 其他不便分类的
|
||||
*/
|
||||
const otherSignal = [
|
||||
// 编辑脚本
|
||||
"editScript",
|
||||
// 场景树变化
|
||||
"sceneTreeChange",
|
||||
]
|
||||
|
||||
Utils.SignalsRegisterFn([
|
||||
...cesiumSignals,
|
||||
...otherSignal
|
||||
])
|
||||
@@ -0,0 +1,63 @@
|
||||
import type {Storage} from "@astral3d/engine";
|
||||
|
||||
export default class Config {
|
||||
private static storage: Storage;
|
||||
public static config: { [s: string]: any };
|
||||
|
||||
static initialize(storage: Storage) {
|
||||
Config.storage = storage;
|
||||
Config.config = {
|
||||
// 场景界面与扩展面板分割大小
|
||||
sceneSplitSize: 0.8,
|
||||
// 图纸绘制区
|
||||
cad:{
|
||||
bgColor:0x000000
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
for (let key of Object.keys(Config.config)) {
|
||||
Config.storage.getConfigItem(key).then(_value => {
|
||||
if (_value === null) {
|
||||
Config.storage.setConfigItem(key, Config.config[key]);
|
||||
} else {
|
||||
let newVal = _value;
|
||||
// 有可能会在代码开发过程中增加新的配置项
|
||||
if (Config.config[key] && typeof Config.config[key] === "object") {
|
||||
newVal = Object.assign({}, Config.config[key], _value);
|
||||
}
|
||||
Config.config[key] = newVal;
|
||||
|
||||
if (newVal !== _value) {
|
||||
Config.storage.setConfigItem(key, newVal);
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
Config.storage.setConfigItem(key, Config.config[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static getKey(key: string) {
|
||||
return Config.config[key];
|
||||
}
|
||||
|
||||
static setKey(...args: any[]) {
|
||||
// key, value, key, value ...
|
||||
for (let i = 0, l = args.length; i < l; i += 2) {
|
||||
const key = args[i];
|
||||
const value = args[i + 1];
|
||||
Config.config[key] = value;
|
||||
Config.storage.setConfigItem(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
static clear() {
|
||||
for (let key of Object.keys(Config.config)) {
|
||||
Config.storage.removeConfigItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
export const initializeConfig = (storage) => Config.initialize(storage);
|
||||
Reference in New Issue
Block a user