目录
一、组件封装
<template>
<div class="stp-viewer-wrapper" id="myCanvas">
</div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, watch, ref } from "vue";
import type { PropType } from "vue";
import type { WaterMark } from '@/type';
import * as THREE from "three";
//@ts-ignore
import occtimportjs from 'occt-import-js'
// control help
import { TransformControls } from "three/examples/jsm/controls/TransformControls"
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls"
import { renderScene, throttle } from '@/utils'
import { Xhr } from "@/request/http";
const props = defineProps({
fileName: {
type: String,
required: true
},
fileIndex: {
type: String,
required: true
},
slug: {
type: String,
required: true
},
password: {
type: String,
required: true
},
waterMark: {
type: Object as PropType<WaterMark>,
required: true
}
})
let occtImportPromise: any = null
let fileBuffer: Uint8Array
// 定义渲染器
const renderer = ref<THREE.WebGLRenderer>(new THREE.WebGLRenderer({ antialias: true }))
// 定义摄像头
const camera = ref<THREE.PerspectiveCamera | null>(null)
// 定义场景
let scene: THREE.Scene
const emits = defineEmits(['on-request-error'])
watch(() => props.fileName, async (val, oldVal) => {
if(!await downloadFile()) return
await loadStep()
})
// view step
onMounted(async () => {
if(!await downloadFile()) return
await loadStep()
window.addEventListener("resize", () => resizeStp())
})
const resizeStp = throttle(() => {
if(camera.value !== null && renderer.value !== null) {
camera.value.aspect = window.innerWidth / window.innerHeight
camera.value?.updateProjectionMatrix()
renderer.value.setSize(window.innerWidth, window.innerHeight)
if(props.waterMark.authname) renderScene(window.innerWidth, window.innerHeight, props.waterMark, scene);
}
})
onBeforeUnmount(() => window.removeEventListener("resize", () => resizeStp()))
const downloadFile = async () => {
try {
const response = await Xhr.request('previewer', {
method: 'GET',
params: {
slug: props.slug,
index: props.fileIndex,
password: props.password,
},
responseType: 'arraybuffer'
})
fileBuffer = new Uint8Array(response.data)
return true
} catch (e) {
emits('on-request-error', (e as Error).message)
return false
}
}
// get occt
const loadOcctImport = (): Promise<any> => {
if (occtImportPromise) {
return occtImportPromise
}
const occt = occtimportjs()
occtImportPromise = occt
return occtImportPromise
}
const LoadGeometry = async (targetObject: THREE.Object3D) => {
// init occt-import-js
const occt = await loadOcctImport()
const result = occt.ReadStepFile(fileBuffer)
// process the geometries of the result
const group = new THREE.Group();
for (let resultMesh of result.meshes) {
const { mesh, edges } = buildMesh(resultMesh, true);
group.add(mesh);
if (edges) group.add(edges);
}
targetObject.add(group);
}
const buildMesh = (geometryMesh: any, showEdges: any) => {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute(geometryMesh.attributes.position.array, 3));
if (geometryMesh.attributes.normal)
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(geometryMesh.attributes.normal.array, 3));
geometry.name = geometryMesh.name;
const index = Uint32Array.from(geometryMesh.index.array);
geometry.setIndex(new THREE.BufferAttribute(index, 1));
const outlineMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
const defaultMaterial = new THREE.MeshPhongMaterial({
color: geometryMesh.color ? new THREE.Color(geometryMesh.color[0], geometryMesh.color[1], geometryMesh.color[2]) : 0xcccccc,
specular: 0,
});
let materials = [ defaultMaterial ];
const edges = showEdges ? new THREE.Group() : null;
if (geometryMesh.brep_faces && geometryMesh.brep_faces.length > 0) {
for (let faceColor of geometryMesh.brep_faces) {
const color = faceColor.color ? new THREE.Color(faceColor.color[0], faceColor.color[1], faceColor.color[2]) : defaultMaterial.color;
materials.push(new THREE.MeshPhongMaterial({ color: color, specular: 0 }));
}
const triangleCount = geometryMesh.index.array.length / 3;
let triangleIndex = 0;
let faceColorGroupIndex = 0;
while (triangleIndex < triangleCount) {
const firstIndex = triangleIndex;
let lastIndex = null;
let materialIndex = null;
if (faceColorGroupIndex >= geometryMesh.brep_faces.length) {
lastIndex = triangleCount;
materialIndex = 0;
} else if (triangleIndex < geometryMesh.brep_faces[faceColorGroupIndex].first) {
lastIndex = geometryMesh.brep_faces[faceColorGroupIndex].first;
materialIndex = 0;
} else {
lastIndex = geometryMesh.brep_faces[faceColorGroupIndex].last + 1;
materialIndex = faceColorGroupIndex + 1;
faceColorGroupIndex++;
}
geometry.addGroup(firstIndex * 3, (lastIndex - firstIndex) * 3, materialIndex);
triangleIndex = lastIndex;
if (edges) {
const innerGeometry = new THREE.BufferGeometry();
innerGeometry.setAttribute("position", geometry.attributes.position);
if (geometryMesh.attributes.normal) innerGeometry.setAttribute("normal", geometry.attributes.normal);
innerGeometry.setIndex(new THREE.BufferAttribute(index.slice(firstIndex * 3, lastIndex * 3), 1));
const innerEdgesGeometry = new THREE.EdgesGeometry(innerGeometry, 180);
const edge = new THREE.LineSegments(innerEdgesGeometry, outlineMaterial);
edges.add(edge);
}
}
}
const mesh = new THREE.Mesh(geometry, materials.length > 1 ? materials : materials[0]);
mesh.name = geometryMesh.name;
if (edges) edges.renderOrder = mesh.renderOrder + 1;
return { mesh, geometry, edges };
}
// 预览
const loadStep = async () => {
const width = window.innerWidth;
const height = window.innerHeight;
// generate html element
renderer.value.setSize(width, height);
renderer.value.setClearColor(0xfafafa);
const myCanvas = document.getElementById('myCanvas')
if(!myCanvas) return
const renderDom = renderer.value.domElement
myCanvas!.appendChild(renderDom);
camera.value = new THREE.PerspectiveCamera(50, width / height, 1.0, 100000.0);
camera.value.position.set(5000.0, 15000.0, 10000.0);
camera.value.up.set(0.0, 0.0, 1.0);
camera.value.lookAt(new THREE.Vector3 (1, 0, 1));
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0x444444);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0x888888);
directionalLight.position.set(camera.value.position.x, camera.value.position.y, camera.value.position.z);
scene.add(directionalLight);
const trackBall = new TrackballControls(camera.value, renderDom);
trackBall.update();
trackBall.addEventListener('change', render => renderer.value.render(scene, camera.value as THREE.PerspectiveCamera));
const control = new TransformControls(camera.value, renderDom);
control.addEventListener('change', () => renderer.value.render(scene, camera.value as THREE.PerspectiveCamera));
control.addEventListener('dragging-changed', function (event) {
trackBall.enabled = !event.value;
});
const mainObject = new THREE.Object3D();
LoadGeometry(mainObject);
scene.add(mainObject);
control.attach(mainObject);
scene.add(control);
if(props.waterMark.authname) renderScene(width, height, props.waterMark, scene);
renderer.value.render(scene, camera.value);
// renderer.setAnimationLoop((time) => {
// mainObject.rotation.x = time / 2000;
// mainObject.rotation.y = time / 1000;
// renderer.render(scene, camera);
// });
renderer.value.setAnimationLoop((time) => {
trackBall.update();
renderer.value.render(scene, camera.value as THREE.PerspectiveCamera);
});
}
</script>
<style scoped lang="less">
.stp-viewer-wrapper {
.set-height-width(100%, 100%);
}
</style>
二、创建scene与水印添加
import type { WaterMark } from '@/type'
import * as THREE from 'three'
/**
* 创建水印
* @param waterMark 水印内容
* @returns HTMLCanvasElement
*/
export const waterMarkPattern = (waterMark: WaterMark): HTMLCanvasElement => {
const canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.width = 200
canvas.height = 100
const ctx = canvas.getContext("2d")! as CanvasRenderingContext2D
ctx.rotate((-18 * Math.PI) / 180)
ctx.font = "20px Vedana"
ctx.fillStyle = "rgba(0, 0, 0, .5)"
ctx.textAlign = "left"
ctx.textBaseline = "middle"
ctx.fillText(waterMark.authname, 10, 60)
return canvas
}
/**
* 在目标元素上渲染水印
* @param targetElementId 目标元素id
* @param width 目标元素宽度
* @param height 目标元素高度
* @param waterMark 水印内容
*/
export const renderWatermark = ( targetElementId: string, width: number, height: number, waterMark: WaterMark) => {
const canvas: HTMLCanvasElement = document.getElementById(targetElementId) as HTMLCanvasElement
if(!canvas) return
const ctx = canvas.getContext("2d")!
// 平铺水印
const pattern = ctx.createPattern(waterMarkPattern(waterMark), "repeat") as CanvasPattern
ctx.rect(0, 0, width, height)
ctx.fillStyle = pattern
ctx.fill()
}
/**
* 创建空canvas
* @param width
* @param height
* @param fillStyle
* @returns
*/
export const emptyCanvas = (width: number, height: number, fillStyle: string): HTMLCanvasElement => {
const canvas = document.createElement("canvas") as HTMLCanvasElement // 默认size 300px * 150px
const ctx = canvas.getContext("2d")!
canvas.width = width
canvas.height = height
ctx.fillStyle = fillStyle
ctx.fillRect(0, 0, width, height); // First rectangle
return canvas
}
/**
* 创建纹理图片
* @param width 纹理图片宽度
* @param height 纹理图片高度
* @param waterMark 水印
*/
export const create3DTexture = (width: number, height: number, waterMark: WaterMark): string => {
const canvas = emptyCanvas(width, height, 'skyblue')
const ctx = canvas.getContext("2d")!
// 平铺水印
const pattern = ctx.createPattern(waterMarkPattern(waterMark), "repeat") as CanvasPattern
ctx.rect(0, 0, width, height)
ctx.fillStyle = pattern
ctx.fill()
return canvas.toDataURL("image/jpeg", 1.0)
}
/**
* 3d 添加背景
* @param scene three.scene
*/
export const renderScene = (width: number, height: number, waterMark: WaterMark, scene: THREE.Scene) => {
const texture = new THREE.Texture()
const image = new Image()
image.src = create3DTexture(width, height, waterMark)
texture.image = image
image.onload = () => texture.needsUpdate = true
scene.background = texture
}
/**
* 水印信息
*/
export type WaterMark = {
/**
* eg. 张三1234
*/
authname: string;
}