bd-admin
Version:
一款能根据需求快速配置vue后台管理的脚手架
656 lines (643 loc) • 20 kB
text/typescript
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";
import {
Vector3,
SetModel,
ModelParams,
ModelMap,
ShapeGeometry,
Params,
} from "./interface/index";
import { MoveTo } from "./utils/index";
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { addData, getData, openDB } from "./indexdb";
const rgbaToRgb = (color: string) => {
const rgbaAttr = color.match(/[\d.]+/g);
if (rgbaAttr && rgbaAttr.length >= 3) {
const r = rgbaAttr[0];
const g = rgbaAttr[1];
const b = rgbaAttr[2];
return "rgb(" + r + "," + g + "," + b + ")";
}
return color;
};
// 根据rgba获取透明度
const getOpacity = (input: string) => {
const regex = /\((.*?)\)/;
const result = input.match(regex);
if (result && result[1]) {
const resultStr = result[1].split(",");
if (resultStr.length === 4) {
return Number(resultStr[3]);
}
}
return null;
};
// 创建线
const createLineMesh = (color: string, linewidth: number, points: any) => {
const material = new THREE.LineBasicMaterial({
color: rgbaToRgb(color),
linewidth,
linecap: "round",
});
const geometry = new THREE.BufferGeometry().setFromPoints(points!);
return new THREE.Line(geometry, material);
};
// 创建面模型的材质
const createFaceMaterial = (color: string) => {
const opacity = getOpacity(color);
const material = new THREE.MeshBasicMaterial({
color: rgbaToRgb(color),
side: THREE.DoubleSide,
transparent: true,
opacity: opacity ? opacity : 1,
});
return material;
};
// 创建面
const createFaceMesh = (array: THREE.Vector3[], color: string) => {
const heartShape = new THREE.Shape(
array.map((item) => new THREE.Vector2(item.x, item.z)),
);
const geometry = new THREE.ShapeGeometry(heartShape);
const material = createFaceMaterial(color);
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI / 2;
return mesh;
};
const getModelType = (url: string): string => {
let pathname = "";
if (URL) {
const addr = new URL(url);
pathname = addr.pathname;
} else {
pathname = url.split("?")[0];
}
return pathname.split(".")[1];
};
// 获取模型动画
const getActions = (
animations: THREE.AnimationClip[],
model: THREE.Object3D,
activeAction: string = "",
) => {
const actions: { [key: string]: THREE.AnimationAction } = {};
const mixerArray: THREE.AnimationMixer[] = [];
if (animations && animations.length > 0) {
const mixer: THREE.AnimationMixer = new THREE.AnimationMixer(model);
mixerArray.push(mixer);
for (let i = 0; i < animations.length; i++) {
const clip = animations[i];
const action: THREE.AnimationAction = mixer.clipAction(clip);
actions[clip.name] = action;
}
if (activeAction) {
const activeActionPlay: THREE.AnimationAction = actions[activeAction];
activeActionPlay.play();
}
}
return {
actions,
mixerArray,
};
};
// 设置阴影
const setShadow = (shadow: boolean | null | undefined, model: any) => {
if (shadow) {
model.traverse(function (object: {
isMesh: any;
castShadow: boolean;
receiveShadow: boolean;
}) {
if (object.isMesh) object.castShadow = true;
object.receiveShadow = true;
});
}
};
class Xhr {
loaderStartTime: number;
loaderEndTime: number;
loader: number;
renderStatus: boolean;
renderTime: number | null;
constructor() {
this.loaderStartTime = +new Date(); //加载开始时间
this.loaderEndTime = 0; // 加载结束时间
this.loader = 0; // 加载进度
this.renderStatus = false; // 状态
this.renderTime = null; // 渲染时间
}
setLoader(loaded: number, total: number) {
this.loader = (loaded / total) * 100;
}
setLoaderEndTime() {
this.loaderEndTime = +new Date() - this.loaderStartTime;
}
setRenderTime() {
this.renderTime = +new Date() - this.loaderStartTime - this.loaderEndTime;
this.renderStatus = true;
}
}
export default class ModelThree {
private readonly modelMap: Map<string, ModelMap>; // 模型
private readonly modelGroup: THREE.Group; // 模型组
private readonly electronRailGroup: THREE.Group;
private readonly scene: THREE.Scene;
private readonly camera: THREE.PerspectiveCamera;
private readonly renderer: THREE.WebGLRenderer;
private readonly params: Params;
private readonly electronRail: Map<string, number[][]>; // 电子围栏坐标组
private readonly loadingMap: Map<string, Xhr>;
private db: any;
constructor(
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
renderer: THREE.WebGLRenderer,
params: Params,
db: any,
) {
this.db = db;
this.params = params;
this.scene = scene;
this.camera = camera;
this.modelGroup = new THREE.Group();
this.modelGroup.name = "modelGroup";
this.scene.add(this.modelGroup);
this.electronRailGroup = new THREE.Group();
this.electronRailGroup.name = "electronRailGroup";
this.scene.add(this.electronRailGroup);
this.renderer = renderer;
this.modelMap = new Map<string, ModelMap>();
this.loadingMap = new Map<string, Xhr>();
this.electronRail = new Map<string, number[][]>();
}
render = (dt: any) => {
if (this.modelMap) {
this.modelMap.forEach((actionsValue: any) => {
if (Object.hasOwn(actionsValue, "mixerArray")) {
actionsValue["mixerArray"].forEach((item: any) => {
item.update(dt);
});
}
});
}
};
/**加载模型
* @param {ModelParams} modelParams // 加载模型参数
* @param {Function} callBack // 加载状态回调
*/
loadModel(modelParams: ModelParams, callBack?: Function) {
const { url, name } = modelParams;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
const load = (db: any) => {
getData(db, "model", url)
.then(async (v: any) => {
let path = url;
if (v) {
path = URL.createObjectURL(v.model);
that.dbLoad(
{
...modelParams,
url: path,
},
callBack,
);
} else {
const loadXhr: Xhr | undefined = new Xhr();
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = () => {
if (xhr.status === 200) {
const arrayBuffer = xhr.response;
const model = new Blob([arrayBuffer]);
addData(db, "model", {
path: url,
model: model,
});
path = URL.createObjectURL(model);
that.dbLoad(
{
...modelParams,
url: path,
},
callBack,
loadXhr,
);
} else {
that.dbLoad(modelParams, callBack);
}
};
// 监听 onprogress 事件来获取请求进度
xhr.onprogress = function (event) {
if (event.lengthComputable) {
loadXhr!.setLoaderEndTime();
loadXhr!.setLoader(event.loaded, event.total);
that.loadingMap.set(name, loadXhr!);
if (callBack) {
callBack(that.loading());
}
} else {
console.log("无法计算进度", event);
}
};
xhr.onerror = function () {
console.error("请求发生错误");
};
xhr.send(); // 发送请求
}
})
.catch(() => {
that.dbLoad(modelParams, callBack);
});
};
if (this.db) {
load(this.db);
} else {
openDB("threeDB", "model", 2)!.then((db) => {
if (db) {
this.db = db;
load(db);
} else {
that.dbLoad(modelParams, callBack);
}
});
}
}
// 加载模型
dbLoad(modelParams: ModelParams, callBack?: Function, load?: Xhr) {
const {
KTX2,
url,
position: { x = 0, y = 0, z = 0 },
scale,
name,
rotation,
activeAction,
DRACO,
} = modelParams;
let loader = null;
const type: string = getModelType(url);
if (type === "fbx") {
loader = new FBXLoader();
} else {
loader = new GLTFLoader();
if (DRACO) {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(DRACO);
loader.setDRACOLoader(dracoLoader);
}
}
if (KTX2) {
const ktx2Loader = new KTX2Loader()
.setTranscoderPath(KTX2)
.detectSupport(this.renderer);
loader.setKTX2Loader(ktx2Loader);
loader.setMeshoptDecoder(MeshoptDecoder);
}
let loadXhr: Xhr | null | undefined = null;
if (!load) {
loadXhr = new Xhr();
} else {
loadXhr = load;
}
loader.load(
url,
(gltf: {
scene: THREE.Group<THREE.Object3DEventMap>;
animations: THREE.AnimationClip[];
}) => {
let model: any = null;
if (gltf.scene) {
model = gltf.scene;
} else {
model = gltf;
}
const { actions, mixerArray } = getActions(
gltf.animations,
model,
activeAction,
);
setShadow(this.params.shadow, model);
model.position.set(x, y, z);
if (scale) {
model.scale.set(scale.x, scale.y, scale.z);
}
if (rotation) {
model.rotation.set(rotation.x, rotation.y, rotation.z);
}
this.modelMap.set(name, {
mode: model,
actions,
mixerArray,
});
model.name = name;
this.modelGroup.add(model);
loadXhr!.setRenderTime();
this.loadingMap.set(name, loadXhr!);
if (callBack) {
callBack(this.loading());
}
},
(xhr: { loaded: number; total: number }) => {
loadXhr!.setLoaderEndTime();
loadXhr!.setLoader(xhr.loaded, xhr.total);
this.loadingMap.set(name, loadXhr!);
if (callBack) {
callBack(this.loading());
}
},
(err: any) => {
console.log(err);
if (callBack) {
this.loadingMap.delete(name);
callBack(null);
}
},
);
}
/**播放模型动画
* @param name 模型名称
* @param playKey 播放动画的名称
* @param flag // 暂停 false or 播放 true
* @returns
*/
playActiveAction(name: string, playKey: string, flag: boolean = true) {
const model = this.modelMap.get(name);
if (!model) return;
const activeAction = model.actions[playKey];
if (!activeAction) return;
activeAction.clampWhenFinished = true;
activeAction.loop = THREE.LoopOnce;
activeAction.reset();
flag ? activeAction.fadeIn(0.1).play() : activeAction.fadeIn(0.3).stop();
}
// 设置模型位置
setModelPosition(params: SetModel) {
const { name, position } = params;
const model = this.getModel(name);
if (!model) return;
model.position.set(position.x, position.y, position.z);
}
/**判断是否在模型内
* @param params 参数
* @param {string} params.name 模型名
* @param {Vector3} params.point 点位置
* @param {string} params.childrenName 子模型
* @returns boolen
*/
isInModel(params: {
name: string;
point: Vector3;
childrenName: string | undefined;
}) {
const { name, point, childrenName } = params;
const model = this.getModel(name, childrenName);
if (!model) return false;
const boundingBox = new THREE.Box3().setFromObject(model);
return boundingBox.containsPoint(
new THREE.Vector3(point.x, point.y, point.z),
);
}
getBoxHelp(name: string, childrenName: string | undefined = "") {
const model = this.getModel(name, childrenName);
if (!model) return;
const BoxHelper = new THREE.BoxHelper(model);
this.scene.add(BoxHelper);
return BoxHelper.geometry.attributes.position.array;
}
/**模型移动
* @param {string} name 模型名
* @param {Vector3} position 移动目的坐标
* @param {boolean} animation 是否开启移动动画
* @param {number} speed 移动速度
* @param {Function} callback 移动完成回调
* @returns
*/
modelMove(
name: string,
position: Vector3,
animation?: boolean,
speed?: number,
callback?: Function,
run?: Function,
) {
const model = this.getModel(name);
if (!model) return;
if (!animation) {
model.position.set(position.x, position.y, position.z);
if (callback) callback();
return;
}
const endPosition = new THREE.Vector3(position.x, position.y, position.z);
const M = new MoveTo(model.position, endPosition, speed);
const fn = () => {
if (run) run(model.position);
const manhattanDistanceTo = M.move();
if (manhattanDistanceTo <= 1) {
model.position.set(position.x, position.y, position.z);
if (callback) callback();
} else {
requestAnimationFrame(fn);
}
};
fn();
}
clickModelMove(modelPosition: THREE.Vector3, Position: THREE.Vector3) {
const startPosition = new THREE.Vector3(
modelPosition.x,
0,
modelPosition.z,
); // 起始位置
const endPosition = new THREE.Vector3(Position.x, 0, Position.z); // 结束位置
//计算移动的向量
const delta = new THREE.Vector3().subVectors(endPosition, startPosition);
return delta.normalize();
}
// 模型朝向
modelLookAt(name: string, Position: THREE.Vector3) {
const model = this.getModel(name);
if (!model) return;
const target = new THREE.Vector3(Position.x, 1, Position.z); // x, y, z 是你想要模型朝向的点的坐标
model.lookAt(target);
}
/**查找模型
* @param {string} name 模型名称
* @param {string} childrenName 模型内子模型名称
* @returns
*/
getModel = (
name: string,
childrenName: string | undefined = "",
): THREE.Object3D<THREE.Object3DEventMap> | undefined => {
let model = this.modelGroup.getObjectByName(name);
if (!model)
model = this.scene.getObjectByName(name) as
| THREE.Object3D<THREE.Object3DEventMap>
| undefined;
if (childrenName && model) {
const modelChildren = model.getObjectByName(childrenName);
return modelChildren;
}
return model;
};
/** 模型显示隐藏
* @param showParams 参数
* @param {string} showParams.name 模型名称
* @param {boolean} showParams.visible 模型显示状态
* @param {string} showParams.childrenName 查找模型内子模型 仅查找一层
*/
showModel(showParams: {
name: string;
visible: boolean;
childrenName?: string;
}) {
const { name = "", childrenName = "", visible = true } = showParams;
const model = this.getModel(name, childrenName);
if (model) {
model.visible = visible;
}
}
deleteModel(name: string) {
const model = this.getModel(name);
if (model) {
this.modelGroup.remove(model);
}
}
/**添加多边形围栏
* @param {[x: number, y: number, z: number][] | [x: number, z: number][] }pathArray 点坐标数组
* @param { ShapeGeometry }params 模型参数
* @param { Function}callBack 创建失败回调
* @returns void
*/
addShapeGeometry(
pathArray: [x: number, y: number, z: number][] | [x: number, z: number][],
params: ShapeGeometry,
callBack?: Function,
) {
const {
name,
color = "#FF962C",
linewidth = 4,
type = "line",
visible = true,
} = params;
if (this.electronRail.get(name)) return;
const array: THREE.Vector3[] = [];
const length = pathArray.length;
const maxXYZLength = pathArray[0].length;
const position = pathArray[0];
for (let i = 0; i < length; i++) {
const item = pathArray[i];
const itemIndex = item.length;
if (itemIndex !== maxXYZLength || itemIndex > 3 || itemIndex < 2) {
if (callBack) callBack();
return;
}
const [x, y, z] = item;
itemIndex === 2
? array.push(new THREE.Vector3(x, 0, y))
: array.push(new THREE.Vector3(x, position[1], z));
}
this.electronRail.set(name, pathArray);
let mesh = null;
if (type === "face") {
mesh = createLineMesh(color, linewidth, array);
const faceMesh = createFaceMesh(array, color);
faceMesh.position.y = position[1];
mesh.add(faceMesh);
} else {
let points = array;
if (type === "curve") {
points = new THREE.CatmullRomCurve3(array).getPoints(array.length * 10);
}
mesh = createLineMesh(color, linewidth, points);
}
mesh.name = name;
mesh.visible = visible;
this.electronRailGroup.add(mesh);
console.log("添加", mesh);
}
// 删除多边形围栏
deletGeometry(name: string) {
const model: any = this.electronRailGroup.getObjectByName(name);
if (model) {
this.modelGroup.remove(model);
this.electronRail.delete(name);
}
}
// 多边形围栏显示隐藏
showGeometry(name: string, visible: boolean = true) {
const model: any = this.electronRailGroup.getObjectByName(name);
if (!model) return;
model.visible = visible;
}
// 设置多边形围栏样式
setGeometryStyle(params: ShapeGeometry) {
const { name, color = "#FF962C", linewidth = 4 } = params;
const model: any = this.electronRailGroup.getObjectByName(name);
if (!model) return;
model.material = new THREE.LineBasicMaterial({
color: rgbaToRgb(color),
linewidth,
linecap: "round",
});
if (model.children.length > 0) {
model.children[0].material = createFaceMaterial(rgbaToRgb(color));
}
}
loading = () => {
const index = this.loadingMap.size;
let loader = 0;
let status = true;
this.loadingMap.forEach((value) => {
loader += value.loader;
if (!value.renderStatus) {
status = value.renderStatus;
}
});
return {
loader: Math.floor(loader / index),
renderStatus: status,
};
};
/**
* p :[x,y] ,带判定的P点
* poly: [[x0,y0],[x1,y1]......] 多边形的路径
*/
rayCasting(p: number[], poly: number[][]) {
// px,py为p点的x和y坐标
const [px, py] = p;
let flag = false;
const length = poly.length;
// 这个for循环是为了遍历多边形的每一个线段
for (let i = 0, l = length, j = l - 1; i < l; j = i, i++) {
const [sx, sy] = poly[i];
const [tx, ty] = poly[j];
// 点与多边形顶点重合
if ((sx === px && sy === py) || (tx === px && ty === py)) return true;
// 点的射线和多边形的一条边重合,并且点在边上
if (
sy === ty &&
sy === py &&
((sx > px && tx < px) || (sx < px && tx > px))
)
return true;
// 判断线段两端点是否在射线两侧
if ((sy < py && ty >= py) || (sy >= py && ty < py)) {
// 求射线和线段的交点x坐标,交点y坐标当然是py
const x = sx + ((py - sy) * (tx - sx)) / (ty - sy);
// 点在多边形的边上
if (x === px) return true;
// x大于px来保证射线是朝右的,往一个方向射,假如射线穿过多边形的边界,flag取反一下
if (x > px) flag = !flag;
}
}
// 射线穿过多边形边界的次数为奇数时点在多边形内
return flag ? true : false;
}
}