feat(all): 迁移扩展相关功能
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
import * as THREE from "three";
|
||||
import { PathGeometry, PathPointList, PathTubeGeometry } from "three.path";
|
||||
import App from "@/core/app/App";
|
||||
import { useAddSignal, useRemoveSignal } from "@/hooks";
|
||||
import { deepAssign } from "@/utils";
|
||||
|
||||
export const getDefaultPathOptions = (): IPath.options => ({
|
||||
name: "Path",
|
||||
position: [0, 0, 0],
|
||||
mode: "path",
|
||||
points: [],
|
||||
closed: false,
|
||||
cornerRadius: 0.1,
|
||||
cornerSplit: 10,
|
||||
up: [0, 1, 0],
|
||||
path: {
|
||||
width: 0.4,
|
||||
arrow: false,
|
||||
progress: 1,
|
||||
side: "both",
|
||||
},
|
||||
tube: {
|
||||
radius: 0.1,
|
||||
radialSegments: 8,
|
||||
progress: 1,
|
||||
startRad: 0,
|
||||
},
|
||||
flow: {
|
||||
enabled: false,
|
||||
speed: 0.2,
|
||||
direction: [1, 0],
|
||||
},
|
||||
material: {
|
||||
color: "#ffffff",
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
side: "double",
|
||||
map: "",
|
||||
repeat: [1, 1],
|
||||
offset: [0, 0],
|
||||
rotation: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default class Path extends THREE.Mesh {
|
||||
type = "Path";
|
||||
isPath = true;
|
||||
|
||||
options: IPath.options = getDefaultPathOptions();
|
||||
private pathPointList = new PathPointList();
|
||||
private texture?: THREE.Texture;
|
||||
private static flowPaths = new Set<Path>();
|
||||
private static flowSignalBound = false;
|
||||
private static flowLastTime = 0;
|
||||
private handleAdded = () => {
|
||||
this.updateFlowRegistration();
|
||||
};
|
||||
private handleRemoved = () => {
|
||||
this.updateFlowRegistration();
|
||||
};
|
||||
|
||||
private static bindFlowSignal() {
|
||||
if (Path.flowSignalBound) return;
|
||||
Path.flowSignalBound = true;
|
||||
useAddSignal("sceneRendered", Path.handleFlowTick);
|
||||
}
|
||||
|
||||
private static unbindFlowSignal() {
|
||||
if (!Path.flowSignalBound) return;
|
||||
Path.flowSignalBound = false;
|
||||
Path.flowLastTime = 0;
|
||||
useRemoveSignal("sceneRendered", Path.handleFlowTick);
|
||||
}
|
||||
|
||||
private static handleFlowTick() {
|
||||
if (Path.flowPaths.size === 0) {
|
||||
Path.flowLastTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
const hasPrevious = Path.flowLastTime > 0;
|
||||
const delta = hasPrevious ? (now - Path.flowLastTime) / 1000 : 0;
|
||||
Path.flowLastTime = now;
|
||||
|
||||
if (delta > 0) {
|
||||
Path.flowPaths.forEach(path => path.updateFlow(delta));
|
||||
}
|
||||
(App.viewer as any)?.pluginRequestRender?.(true);
|
||||
}
|
||||
|
||||
constructor(options: Partial<IPath.options> = {}, material?: THREE.Material) {
|
||||
super();
|
||||
|
||||
deepAssign(this.options, options);
|
||||
//if (options.path) this.options.path = { ...this.options.path, ...options.path };
|
||||
//if (options.tube) this.options.tube = { ...this.options.tube, ...options.tube };
|
||||
// if (options.flow) {
|
||||
// this.options.flow = { ...this.options.flow, ...options.flow };
|
||||
// if (Array.isArray(options.flow.direction)) {
|
||||
// this.options.flow.direction = options.flow.direction.slice();
|
||||
// }
|
||||
// }
|
||||
// if (options.material) this.options.material = { ...this.options.material, ...options.material };
|
||||
// if (Array.isArray(options.points)) {
|
||||
// this.options.points = options.points.map(point => ({ ...point }));
|
||||
// }
|
||||
// if (Array.isArray(options.position)) {
|
||||
// this.options.position = options.position.slice();
|
||||
// }
|
||||
// if (Array.isArray(options.up)) {
|
||||
// this.options.up = options.up.slice();
|
||||
// }
|
||||
|
||||
this.name = this.options.name;
|
||||
|
||||
this.updatePathPointList();
|
||||
this.geometry = this.createGeometry();
|
||||
|
||||
if (material) {
|
||||
this.material = material;
|
||||
if (options.material) {
|
||||
this.applyMaterialOptions(this.material);
|
||||
}
|
||||
} else {
|
||||
this.material = this.createMaterial();
|
||||
}
|
||||
|
||||
this.position.fromArray(this.options.position);
|
||||
|
||||
this.addEventListener("added", this.handleAdded);
|
||||
this.addEventListener("removed", this.handleRemoved);
|
||||
this.updateFlowRegistration();
|
||||
}
|
||||
|
||||
private resolveMaterialSide(side?: IPath.MaterialSide) {
|
||||
switch (side) {
|
||||
case "front":
|
||||
return THREE.FrontSide;
|
||||
case "back":
|
||||
return THREE.BackSide;
|
||||
case "double":
|
||||
default:
|
||||
return THREE.DoubleSide;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveMaterialSideName(side?: THREE.Side): IPath.MaterialSide {
|
||||
switch (side) {
|
||||
case THREE.FrontSide:
|
||||
return "front";
|
||||
case THREE.BackSide:
|
||||
return "back";
|
||||
case THREE.DoubleSide:
|
||||
default:
|
||||
return "double";
|
||||
}
|
||||
}
|
||||
|
||||
private createMaterial() {
|
||||
const materialOptions = this.options.material;
|
||||
const color = materialOptions?.color ?? "#ffffff";
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color(color as any),
|
||||
transparent: materialOptions?.transparent ?? true,
|
||||
opacity: materialOptions?.opacity ?? 1,
|
||||
depthWrite: materialOptions?.depthWrite ?? false,
|
||||
depthTest: materialOptions?.depthTest ?? true,
|
||||
side: this.resolveMaterialSide(materialOptions?.side),
|
||||
});
|
||||
|
||||
this.applyMaterialOptions(material);
|
||||
return material;
|
||||
}
|
||||
|
||||
private updatePathPointList() {
|
||||
const points = this.options.points || [];
|
||||
const vectors = points.map(point => new THREE.Vector3(point.x, point.y, point.z));
|
||||
const up = Array.isArray(this.options.up) ? new THREE.Vector3(this.options.up[0], this.options.up[1], this.options.up[2]) : null;
|
||||
this.pathPointList.set(
|
||||
vectors,
|
||||
this.options.cornerRadius ?? 0,
|
||||
this.options.cornerSplit ?? 0,
|
||||
up,
|
||||
Boolean(this.options.closed)
|
||||
);
|
||||
}
|
||||
|
||||
private createGeometry(): PathGeometry | PathTubeGeometry {
|
||||
const mode = this.options.mode;
|
||||
if (mode === "tube") {
|
||||
return new PathTubeGeometry({
|
||||
pathPointList: this.pathPointList,
|
||||
options: this.options.tube,
|
||||
usage: THREE.DynamicDrawUsage,
|
||||
});
|
||||
}
|
||||
|
||||
return new PathGeometry({
|
||||
pathPointList: this.pathPointList,
|
||||
options: this.options.path,
|
||||
usage: THREE.DynamicDrawUsage,
|
||||
});
|
||||
}
|
||||
|
||||
private updateGeometry() {
|
||||
this.updatePathPointList();
|
||||
this.rebuildGeometry();
|
||||
this.geometry.computeBoundingBox();
|
||||
this.geometry.computeBoundingSphere();
|
||||
}
|
||||
|
||||
private rebuildGeometry() {
|
||||
this.geometry.dispose();
|
||||
this.geometry = this.createGeometry();
|
||||
}
|
||||
|
||||
private loadTexture(url: string) {
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load(url, () => {
|
||||
(this.material as THREE.Material).needsUpdate = true;
|
||||
});
|
||||
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
||||
|
||||
const repeat = this.options.material?.repeat;
|
||||
if (Array.isArray(repeat) && repeat.length >= 2) {
|
||||
texture.repeat.set(Number(repeat[0]) || 1, Number(repeat[1]) || 1);
|
||||
}
|
||||
|
||||
const offset = this.options.material?.offset;
|
||||
if (Array.isArray(offset) && offset.length >= 2) {
|
||||
texture.offset.set(Number(offset[0]) || 0, Number(offset[1]) || 0);
|
||||
}
|
||||
|
||||
if (typeof this.options.material?.rotation === "number") {
|
||||
texture.rotation = this.options.material.rotation;
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
private updateFlowRegistration() {
|
||||
if (this.options.flow?.enabled && this.parent) {
|
||||
Path.flowPaths.add(this);
|
||||
Path.bindFlowSignal();
|
||||
} else {
|
||||
Path.flowPaths.delete(this);
|
||||
}
|
||||
if (Path.flowPaths.size === 0) {
|
||||
Path.unbindFlowSignal();
|
||||
}
|
||||
}
|
||||
|
||||
private updateFlow(delta: number) {
|
||||
const flow = this.options.flow;
|
||||
if (!flow?.enabled) return;
|
||||
|
||||
const meshMaterial = this.material as THREE.MeshBasicMaterial;
|
||||
const map = meshMaterial?.map;
|
||||
if (!map) return;
|
||||
|
||||
const speed = typeof flow.speed === "number" ? flow.speed : 0;
|
||||
if (speed === 0) return;
|
||||
|
||||
const direction = Array.isArray(flow.direction) && flow.direction.length >= 2 ? flow.direction : [1, 0];
|
||||
|
||||
map.offset.x += direction[0] * speed * delta;
|
||||
map.offset.y += direction[1] * speed * delta;
|
||||
|
||||
map.offset.x -= Math.floor(map.offset.x);
|
||||
map.offset.y -= Math.floor(map.offset.y);
|
||||
}
|
||||
|
||||
private applyMaterialOptions(material: THREE.Material) {
|
||||
const materialOptions = this.options.material;
|
||||
if (!materialOptions) return;
|
||||
|
||||
const meshMaterial = material as THREE.MeshBasicMaterial;
|
||||
|
||||
if (materialOptions.color !== undefined && meshMaterial.color) {
|
||||
meshMaterial.color = new THREE.Color(materialOptions.color as any);
|
||||
}
|
||||
|
||||
if (typeof materialOptions.opacity === "number") {
|
||||
meshMaterial.opacity = materialOptions.opacity;
|
||||
}
|
||||
if (typeof materialOptions.transparent === "boolean") {
|
||||
meshMaterial.transparent = materialOptions.transparent;
|
||||
}
|
||||
if (typeof materialOptions.depthWrite === "boolean") {
|
||||
meshMaterial.depthWrite = materialOptions.depthWrite;
|
||||
}
|
||||
if (typeof materialOptions.depthTest === "boolean") {
|
||||
meshMaterial.depthTest = materialOptions.depthTest;
|
||||
}
|
||||
if (materialOptions.side) {
|
||||
meshMaterial.side = this.resolveMaterialSide(materialOptions.side);
|
||||
}
|
||||
|
||||
if (typeof materialOptions.map === "string" && materialOptions.map.trim() !== "") {
|
||||
if (this.texture) this.texture.dispose();
|
||||
this.texture = this.loadTexture(materialOptions.map);
|
||||
meshMaterial.map = this.texture;
|
||||
meshMaterial.transparent = true;
|
||||
}
|
||||
|
||||
meshMaterial.needsUpdate = true;
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<IPath.options>) {
|
||||
let needsGeometryUpdate = false;
|
||||
let flowChanged = false;
|
||||
if (options.name !== undefined) {
|
||||
this.options.name = options.name;
|
||||
this.name = options.name;
|
||||
}
|
||||
if (Array.isArray(options.position)) {
|
||||
this.options.position = options.position.slice();
|
||||
this.position.fromArray(this.options.position);
|
||||
}
|
||||
if (Array.isArray(options.points)) {
|
||||
this.options.points = options.points.map(point => ({ ...point }));
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (options.mode) {
|
||||
this.options.mode = options.mode;
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (typeof options.closed === "boolean") {
|
||||
this.options.closed = options.closed;
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (typeof options.cornerRadius === "number") {
|
||||
this.options.cornerRadius = options.cornerRadius;
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (typeof options.cornerSplit === "number") {
|
||||
this.options.cornerSplit = options.cornerSplit;
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (Array.isArray(options.up)) {
|
||||
this.options.up = options.up.slice();
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (options.path) {
|
||||
this.options.path = { ...this.options.path, ...options.path };
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (options.tube) {
|
||||
this.options.tube = { ...this.options.tube, ...options.tube };
|
||||
needsGeometryUpdate = true;
|
||||
}
|
||||
if (options.flow) {
|
||||
this.options.flow = { ...this.options.flow, ...options.flow };
|
||||
if (Array.isArray(options.flow.direction)) {
|
||||
this.options.flow.direction = options.flow.direction.slice();
|
||||
}
|
||||
flowChanged = true;
|
||||
}
|
||||
if (options.material) this.options.material = { ...this.options.material, ...options.material };
|
||||
|
||||
if (needsGeometryUpdate) {
|
||||
this.updateGeometry();
|
||||
}
|
||||
|
||||
if (options.material) {
|
||||
this.applyMaterialOptions(this.material as THREE.Material);
|
||||
}
|
||||
if (flowChanged) {
|
||||
this.updateFlowRegistration();
|
||||
}
|
||||
}
|
||||
|
||||
setPoints(points: IPath.Point[]) {
|
||||
this.options.points = points.map(point => ({ ...point }));
|
||||
this.updateGeometry();
|
||||
}
|
||||
|
||||
toJSON(meta?: THREE.JSONMeta) {
|
||||
const options = JSON.parse(JSON.stringify(this.options));
|
||||
options.position = this.position.toArray();
|
||||
|
||||
const material = this.material as THREE.Material & { color?: THREE.Color };
|
||||
if (!options.material) options.material = {};
|
||||
if (material.color) {
|
||||
options.material.color = `#${material.color.getHexString()}`;
|
||||
}
|
||||
options.material.opacity = material.opacity;
|
||||
options.material.transparent = material.transparent;
|
||||
options.material.depthWrite = material.depthWrite;
|
||||
options.material.depthTest = material.depthTest;
|
||||
options.material.side = this.resolveMaterialSideName(material.side);
|
||||
|
||||
const superJSON = super.toJSON(meta);
|
||||
superJSON.object.type = this.type;
|
||||
superJSON.object.options = options;
|
||||
|
||||
return superJSON;
|
||||
}
|
||||
|
||||
static fromJSON(json: { options: IPath.options; material?: THREE.Material }) {
|
||||
return new Path(json.options, json.material);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.removeEventListener("added", this.handleAdded);
|
||||
this.removeEventListener("removed", this.handleRemoved);
|
||||
this.geometry.dispose();
|
||||
if (Array.isArray(this.material)) {
|
||||
this.material.forEach(mat => mat.dispose());
|
||||
} else {
|
||||
(this.material as THREE.Material).dispose();
|
||||
}
|
||||
if (this.texture) {
|
||||
this.texture.dispose();
|
||||
this.texture = undefined;
|
||||
}
|
||||
Path.flowPaths.delete(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user