| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- /** Original low-poly demonstration meshes; no external assets/dependencies. */
- import { mkdir, writeFile } from 'node:fs/promises'
- import { enuToGltf } from '../../src/modules/cockpit/services/pipeInstanceGltf.js'
- const out = new URL('../../public/cockpit/models/pipes/', import.meta.url)
- await mkdir(out, { recursive: true })
- function geometry() {
- const positions = [], normals = [], indices = []
- // Canonical ENU xyz -> standard glTF Y-up, Z-forward (Cesium corrects to ENU).
- const convert = enuToGltf
- function vertex(p, n) {
- const index = positions.length / 3
- positions.push(...convert(p)); normals.push(...convert(n))
- return index
- }
- function tube(path, radius, sides = 12) {
- const rings = []
- for (let i = 0; i < path.length; i += 1) {
- const before = path[Math.max(0, i - 1)], after = path[Math.min(path.length - 1, i + 1)]
- const d = after.map((v, j) => v - before[j]), length = Math.hypot(...d)
- const tangent = d.map((v) => v / length)
- const axis = Math.abs(tangent[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0]
- const cross = (a, b) => [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]
- const u0 = cross(tangent, axis), uLength = Math.hypot(...u0), u = u0.map((v) => v/uLength), v = cross(tangent, u)
- const ring = []
- for (let j = 0; j < sides; j += 1) {
- const angle = j * Math.PI * 2 / sides
- const n = u.map((value, k) => value * Math.cos(angle) + v[k] * Math.sin(angle))
- ring.push(vertex(path[i].map((value, k) => value + radius*n[k]), n))
- }
- rings.push(ring)
- }
- for (let i = 1; i < rings.length; i += 1) for (let j = 0; j < sides; j += 1) {
- const k = (j+1)%sides, a = rings[i-1][j], b = rings[i-1][k], c = rings[i][j], d = rings[i][k]
- indices.push(a,b,c,b,d,c)
- }
- // Flat end caps use separate vertices for correct normals.
- for (const end of [0, path.length-1]) {
- const other = end === 0 ? 1 : end-1
- const delta = path[end].map((value,k) => value-path[other][k]), len = Math.hypot(...delta), normal = delta.map((value)=>value/len)
- const center = vertex(path[end],normal), ring = []
- for (const index of rings[end]) {
- // Reverse conversion of the already stored position.
- ring.push(vertex([positions[index*3+2],positions[index*3],positions[index*3+1]],normal))
- }
- for (let j=0;j<sides;j+=1) indices.push(center, ring[j], ring[(j+1)%sides])
- }
- }
- return { positions, normals, indices, tube }
- }
- function document(kind) {
- const g = geometry()
- if (kind === 'spline') g.tube([[0,0,0],[1,0,0]],0.5)
- if (kind === 'elbow') {
- const path = Array.from({length:9},(_,i)=>{ const a=-Math.PI/2+i*Math.PI/16; return [-.25+.5*Math.cos(a),.25+.5*Math.sin(a),0] })
- g.tube(path,.12)
- }
- if (['branch','cross'].includes(kind)) {
- g.tube([[-.5,0,0],[.5,0,0]],.12)
- g.tube([[0,kind==='cross'?-.5:0,0],[0,.5,0]],.12)
- }
- if (kind === 'valve') {
- g.tube([[-.5,0,0],[.5,0,0]],.12)
- g.tube([[-.16,0,0],[.16,0,0]],.21)
- g.tube([[0,0,0],[0,0,.38]],.045)
- const circle = Array.from({length:17},(_,i)=>[.22*Math.cos(i*Math.PI/8),.22*Math.sin(i*Math.PI/8),.38])
- g.tube(circle,.035,8)
- g.tube([[-.22,0,.38],[.22,0,.38]],.025,8)
- g.tube([[0,-.22,.38],[0,.22,.38]],.025,8)
- }
- const arrays = [new Float32Array(g.positions),new Float32Array(g.normals),new Uint16Array(g.indices)]
- const bytes = Buffer.concat(arrays.map((array)=>Buffer.from(array.buffer)))
- const min=[Infinity,Infinity,Infinity],max=[-Infinity,-Infinity,-Infinity]
- for(let i=0;i<g.positions.length;i+=1){const k=i%3;min[k]=Math.min(min[k],g.positions[i]);max[k]=Math.max(max[k],g.positions[i])}
- const colors={spline:[.9,.9,.9,1],elbow:[1,.72,.18,1],valve:[1,.2,.2,1],branch:[.25,.9,.35,1],cross:[.25,.9,.35,1]}
- return { bytes, gltf: {
- asset:{version:'2.0',generator:'City Lifeline original demonstration pipe meshes'},
- scene:0, scenes:[{nodes:[0]}], nodes:[{mesh:0,name:kind}],
- meshes:[{primitives:[{attributes:{POSITION:0,NORMAL:1},indices:2,material:0}]}],
- materials:[{doubleSided:true,pbrMetallicRoughness:{baseColorFactor:colors[kind],metallicFactor:.15,roughnessFactor:.65}}],
- buffers:[{byteLength:bytes.length,uri:`${kind}.bin`}],
- bufferViews:[{buffer:0,byteOffset:0,byteLength:arrays[0].byteLength,target:34962},{buffer:0,byteOffset:arrays[0].byteLength,byteLength:arrays[1].byteLength,target:34962},{buffer:0,byteOffset:arrays[0].byteLength+arrays[1].byteLength,byteLength:arrays[2].byteLength,target:34963}],
- accessors:[{bufferView:0,componentType:5126,count:g.positions.length/3,type:'VEC3',min,max},{bufferView:1,componentType:5126,count:g.normals.length/3,type:'VEC3'},{bufferView:2,componentType:5123,count:g.indices.length,type:'SCALAR'}],
- extras:{demonstration:true,kind,units:'metres',splineAxis:'glTF +Z, start 0, end 1',installationOrientation:'unknown; fixed illustrative ENU orientation'},
- } }
- }
- for (const kind of ['spline','elbow','valve','branch','cross']) {
- const { gltf, bytes } = document(kind)
- await writeFile(new URL(`${kind}.bin`,out),bytes)
- await writeFile(new URL(`${kind}.gltf`,out),JSON.stringify(gltf))
- console.log(`${kind}: ${gltf.accessors[0].count} vertices, ${gltf.accessors[2].count/3} triangles`)
- }
|