feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
@@ -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;
}
+57
View File
@@ -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;
}
+70
View File
@@ -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) });
}
+231
View File
@@ -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
}
}
+171
View File
@@ -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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* 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 => {
// Warnscript.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()
}
})
}