| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- <script setup lang="ts">
- import { onMounted, onUnmounted, ref } from 'vue'
- import * as THREE from 'three'
- import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
- import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
- const containerRef = ref<HTMLDivElement>()
- let scene: THREE.Scene
- let camera: THREE.PerspectiveCamera
- let renderer: THREE.WebGLRenderer
- let labelRenderer: CSS2DRenderer
- let controls: OrbitControls
- let animationId: number
- let flowDots: THREE.Mesh[] = []
- interface DeviceNode {
- id: string; label: string; x: number; z: number; value: string; color: number
- }
- const nodes: DeviceNode[] = [
- { id: 'quShou', label: '渠首', x: -300, z: -70, value: '引水枢纽', color: 0x4dd0e1 },
- { id: 'tiaoXing', label: '条形沉砂池', x: -150, z: -70, value: '12.8万m³', color: 0x4dd0e1 },
- { id: 'gate1', label: '退水闸', x: 0, z: -70, value: '退水', color: 0x4dd0e1 },
- { id: 'chen2', label: '2#沉砂池', x: 150, z: -70, value: '38团二期', color: 0x4dd0e1 },
- { id: 'chen1', label: '1#沉砂池', x: 300, z: -70, value: '38团一期', color: 0x4dd0e1 },
- { id: 'river', label: '莫勒切河', x: -150, z: 70, value: '来水', color: 0x4fc3f7 },
- { id: 'fenShui', label: '节制分水闸', x: 0, z: 70, value: '分水', color: 0x4dd0e1 },
- ]
- const nodeMap = new Map(nodes.map(n => [n.id, n]))
- const pipes = [
- // 线路1:渠首 → 条形沉砂池 → 退水闸 → 2#沉砂池
- { from: 'quShou', to: 'tiaoXing' },
- { from: 'tiaoXing', to: 'gate1' },
- { from: 'gate1', to: 'chen2' },
- // 线路2:莫勒切河 → 节制分水闸 → 2#沉砂池 + 1#沉砂池
- { from: 'river', to: 'fenShui' },
- { from: 'fenShui', to: 'chen2' },
- { from: 'fenShui', to: 'chen1' },
- // 2#沉砂池 → 1#沉砂池
- { from: 'chen2', to: 'chen1' },
- ]
- // 流动管线 Shader
- const pipeVertexShader = `
- attribute float aDistance;
- uniform float uTime;
- varying float vDist;
- void main() {
- vDist = aDistance + uTime * 0.15;
- vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
- gl_Position = projectionMatrix * mvPos;
- }
- `
- const pipeFragmentShader = `
- uniform vec3 uColor;
- uniform float uOpacity;
- varying float vDist;
- void main() {
- // 流动条纹:沿管线方向亮暗交替
- float flow = sin((vDist - 0.5) * 6.283 * 3.0) * 0.5 + 0.5;
- flow = pow(flow, 2.0);
- float glow = 0.3 + 0.7 * flow;
- gl_FragColor = vec4(uColor * glow, uOpacity * (0.5 + 0.5 * flow));
- }
- `
- /** 创建一条带流动效果的管线(Tube 网格,支持线宽) */
- function createFlowLine(pts: THREE.Vector3[]) {
- // 用 CatmullRom 但 tension=0 保持直线
- const curve = new THREE.CatmullRomCurve3(pts, false, 'catmullrom', 0)
- const tubeGeo = new THREE.TubeGeometry(curve, 64, 1.8, 8, false)
- // 重新计算顶点距离属性
- const posAttr = tubeGeo.attributes.position
- const vertCount = posAttr.count
- const distances = new Float32Array(vertCount)
- let totalLen = 0
- // 按 TubeGeometry 的环结构估算距离
- for (let i = 0; i < vertCount; i++) {
- const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i)
- if (i > 0) {
- totalLen += Math.sqrt(
- (x - posAttr.getX(i-1))**2 + (y - posAttr.getY(i-1))**2 + (z - posAttr.getZ(i-1))**2
- )
- }
- distances[i] = totalLen
- }
- const maxLen = distances[vertCount - 1] || 1
- for (let i = 0; i < vertCount; i++) distances[i] /= maxLen
- tubeGeo.setAttribute('aDistance', new THREE.Float32BufferAttribute(distances, 1))
- const mat = new THREE.ShaderMaterial({
- uniforms: {
- uTime: { value: 0 },
- uColor: { value: new THREE.Color(0x4dd0e1) },
- uOpacity: { value: 0.9 },
- },
- vertexShader: pipeVertexShader,
- fragmentShader: pipeFragmentShader,
- transparent: true,
- blending: THREE.AdditiveBlending,
- depthWrite: false,
- })
- const mesh = new THREE.Mesh(tubeGeo, mat);
- (mesh as any)._pipeMat = mat
- return mesh
- }
- // 存储所有管线材质
- const pipeMats: THREE.ShaderMaterial[] = []
- /** 构建折线路径点 */
- function buildPoints(a: DeviceNode, b: DeviceNode, from: string, to: string): THREE.Vector3[] {
- // 分水闸 → 1#沉砂池:先右后下
- if (from === 'fenShui' && to === 'chen1') {
- return [
- new THREE.Vector3(a.x, 14, a.z),
- new THREE.Vector3(b.x, 14, a.z),
- new THREE.Vector3(b.x, 14, b.z),
- ]
- }
- // 同行/同列:直线
- if (a.z === b.z || a.x === b.x) {
- return [
- new THREE.Vector3(a.x, 14, a.z),
- new THREE.Vector3(b.x, 14, b.z),
- ]
- }
- // 标准 L 折线:先水平再垂直
- return [
- new THREE.Vector3(a.x, 14, a.z),
- new THREE.Vector3(b.x, 14, a.z),
- new THREE.Vector3(b.x, 14, b.z),
- ]
- }
- function initScene() {
- const el = containerRef.value
- if (!el) { console.warn('[H3D] no container'); return }
- const w = el.clientWidth, h = el.clientHeight
- console.log('[H3D] init', w, h)
- if (w === 0 || h === 0) return
- scene = new THREE.Scene()
- scene.background = new THREE.Color(0x0a1a2e)
- camera = new THREE.PerspectiveCamera(40, w / h, 1, 2000)
- camera.position.set(300, 350, 450)
- renderer = new THREE.WebGLRenderer({ antialias: true })
- renderer.setSize(w, h)
- renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
- el.appendChild(renderer.domElement)
- labelRenderer = new CSS2DRenderer()
- labelRenderer.setSize(w, h)
- labelRenderer.domElement.style.position = 'absolute'
- labelRenderer.domElement.style.top = '0'
- labelRenderer.domElement.style.left = '0'
- labelRenderer.domElement.style.pointerEvents = 'none'
- el.appendChild(labelRenderer.domElement)
- controls = new OrbitControls(camera, renderer.domElement)
- controls.enableDamping = true
- controls.dampingFactor = 0.1
- controls.target.set(0, 0, 0)
- controls.update()
- // 灯光
- scene.add(new THREE.AmbientLight(0x334466, 0.6))
- const dl = new THREE.DirectionalLight(0xffffcc, 1.2)
- dl.position.set(200, 500, 300)
- scene.add(dl)
- const fl = new THREE.DirectionalLight(0x4488ff, 0.4)
- fl.position.set(-200, 100, -200)
- scene.add(fl)
- // 地面网格
- scene.add(new THREE.GridHelper(600, 20, 0x1a3a5c, 0x0d2a4a))
- // 创建设备节点
- nodes.forEach(n => {
- const g = new THREE.Group()
- // 底座
- const base = new THREE.Mesh(
- new THREE.BoxGeometry(50, 6, 50),
- new THREE.MeshStandardMaterial({ color: 0x1a3050, roughness: 0.6, metalness: 0.3 })
- )
- base.position.y = 3; base.receiveShadow = true; base.castShadow = true
- g.add(base)
- // 主体
- const body = new THREE.Mesh(
- new THREE.BoxGeometry(36, 24, 36),
- new THREE.MeshStandardMaterial({ color: n.color, emissive: 0x0088ff, emissiveIntensity: 0.15, roughness: 0.3, metalness: 0.5 })
- )
- body.position.y = 18; body.castShadow = true
- g.add(body)
- // 顶条
- const rim = new THREE.Mesh(
- new THREE.BoxGeometry(40, 2, 40),
- new THREE.MeshStandardMaterial({ color: n.color, emissive: 0x0088ff, emissiveIntensity: 0.6 })
- )
- rim.position.y = 31
- g.add(rim)
- g.position.set(n.x, 0, n.z)
- scene.add(g)
- })
- // 管道(Shader 流动线)
- pipes.forEach(p => {
- const a = nodeMap.get(p.from)!, b = nodeMap.get(p.to)!
- const pts = buildPoints(a, b, p.from, p.to)
- const line = createFlowLine(pts)
- scene.add(line)
- pipeMats.push((line as any)._pipeMat)
- // 流动球 - 沿折线运动
- const dot = new THREE.Mesh(
- new THREE.SphereGeometry(4, 8, 8),
- new THREE.MeshStandardMaterial({ color: 0x4dd0e1, emissive: 0x4dd0e1, emissiveIntensity: 2 })
- )
- dot.userData = { pts, t: Math.random(), speed: 0.3 + Math.random() * 0.2 }
- scene.add(dot)
- flowDots.push(dot)
- })
- // CSS2D 标签
- nodes.forEach(n => {
- const div = document.createElement('div')
- div.style.cssText = 'text-align:center;pointer-events:none'
- const t = document.createElement('div')
- t.style.cssText = 'color:#fff;font:bold 13px "Microsoft YaHei",sans-serif;text-shadow:0 0 12px rgba(0,0,0,0.95)'
- t.textContent = n.label
- const v = document.createElement('div')
- v.style.cssText = 'color:#4fc3f7;font:11px "Microsoft YaHei",sans-serif;text-shadow:0 0 8px rgba(0,0,0,0.9)'
- v.textContent = n.value
- div.appendChild(t)
- div.appendChild(v)
- const label = new CSS2DObject(div)
- label.position.set(n.x, 42, n.z)
- scene.add(label)
- })
- console.log('[H3D] 初始化完成, 节点:', nodes.length, '管道:', pipes.length)
- window.addEventListener('resize', onResize)
- animate()
- }
- function animate() {
- animationId = requestAnimationFrame(animate)
- const time = performance.now() * 0.001
- pipeMats.forEach(mat => { mat.uniforms.uTime.value = time })
- flowDots.forEach(dot => {
- const { pts, t, speed } = dot.userData
- const nt = (t + 0.004 * speed) % 1
- dot.userData.t = nt
- // 沿折线分段插值
- const segCount = pts.length - 1
- const seg = nt * segCount
- const idx = Math.min(Math.floor(seg), segCount - 1)
- const frac = seg - idx
- const a = pts[idx], b = pts[idx + 1]
- dot.position.set(
- a.x + (b.x - a.x) * frac,
- (a.y + (b.y - a.y) * frac) + 0.5,
- a.z + (b.z - a.z) * frac,
- )
- })
- controls.update()
- renderer.render(scene, camera)
- labelRenderer.render(scene, camera)
- }
- function onResize() {
- if (!containerRef.value) return
- const w = containerRef.value.clientWidth, h = containerRef.value.clientHeight
- camera.aspect = w / h; camera.updateProjectionMatrix()
- renderer.setSize(w, h); labelRenderer.setSize(w, h)
- }
- function disposeScene() {
- window.removeEventListener('resize', onResize)
- cancelAnimationFrame(animationId)
- controls?.dispose()
- renderer?.dispose()
- if (labelRenderer) labelRenderer.domElement.remove()
- pipeMats.length = 0
- flowDots = []
- }
- onMounted(() => { try { initScene() } catch (e) { console.error('[H3D] error:', e) } })
- onUnmounted(() => disposeScene())
- </script>
- <template>
- <div ref="containerRef" class="diagram-container">
- <div class="header">
- <span class="title">38团水利工程组态图</span>
- <span class="hint">拖拽旋转 · 滚轮缩放</span>
- </div>
- </div>
- </template>
- <style scoped>
- .diagram-container {
- width: 100%; height: 100%;
- position: relative; overflow: hidden;
- background: #0a1a2e;
- }
- .header {
- position: absolute; top: 16px; left: 24px; z-index: 10;
- display: flex; align-items: center; gap: 20px;
- }
- .title {
- font: bold 18px "Microsoft YaHei", sans-serif;
- color: #4dd0e1;
- text-shadow: 0 0 20px rgba(77,208,225,0.3);
- }
- .hint {
- color: rgba(255,255,255,0.35);
- font: 12px "Microsoft YaHei", sans-serif;
- }
- </style>
|