build-assets.mjs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. /** Original low-poly demonstration meshes; no external assets/dependencies. */
  2. import { mkdir, writeFile } from 'node:fs/promises'
  3. import { enuToGltf } from '../../src/modules/cockpit/services/pipeInstanceGltf.js'
  4. const out = new URL('../../public/cockpit/models/pipes/', import.meta.url)
  5. await mkdir(out, { recursive: true })
  6. function geometry() {
  7. const positions = [], normals = [], indices = []
  8. // Canonical ENU xyz -> standard glTF Y-up, Z-forward (Cesium corrects to ENU).
  9. const convert = enuToGltf
  10. function vertex(p, n) {
  11. const index = positions.length / 3
  12. positions.push(...convert(p)); normals.push(...convert(n))
  13. return index
  14. }
  15. function tube(path, radius, sides = 12) {
  16. const rings = []
  17. for (let i = 0; i < path.length; i += 1) {
  18. const before = path[Math.max(0, i - 1)], after = path[Math.min(path.length - 1, i + 1)]
  19. const d = after.map((v, j) => v - before[j]), length = Math.hypot(...d)
  20. const tangent = d.map((v) => v / length)
  21. const axis = Math.abs(tangent[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0]
  22. 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]]
  23. const u0 = cross(tangent, axis), uLength = Math.hypot(...u0), u = u0.map((v) => v/uLength), v = cross(tangent, u)
  24. const ring = []
  25. for (let j = 0; j < sides; j += 1) {
  26. const angle = j * Math.PI * 2 / sides
  27. const n = u.map((value, k) => value * Math.cos(angle) + v[k] * Math.sin(angle))
  28. ring.push(vertex(path[i].map((value, k) => value + radius*n[k]), n))
  29. }
  30. rings.push(ring)
  31. }
  32. for (let i = 1; i < rings.length; i += 1) for (let j = 0; j < sides; j += 1) {
  33. const k = (j+1)%sides, a = rings[i-1][j], b = rings[i-1][k], c = rings[i][j], d = rings[i][k]
  34. indices.push(a,b,c,b,d,c)
  35. }
  36. // Flat end caps use separate vertices for correct normals.
  37. for (const end of [0, path.length-1]) {
  38. const other = end === 0 ? 1 : end-1
  39. const delta = path[end].map((value,k) => value-path[other][k]), len = Math.hypot(...delta), normal = delta.map((value)=>value/len)
  40. const center = vertex(path[end],normal), ring = []
  41. for (const index of rings[end]) {
  42. // Reverse conversion of the already stored position.
  43. ring.push(vertex([positions[index*3+2],positions[index*3],positions[index*3+1]],normal))
  44. }
  45. for (let j=0;j<sides;j+=1) indices.push(center, ring[j], ring[(j+1)%sides])
  46. }
  47. }
  48. return { positions, normals, indices, tube }
  49. }
  50. function document(kind) {
  51. const g = geometry()
  52. if (kind === 'spline') g.tube([[0,0,0],[1,0,0]],0.5)
  53. if (kind === 'elbow') {
  54. 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] })
  55. g.tube(path,.12)
  56. }
  57. if (['branch','cross'].includes(kind)) {
  58. g.tube([[-.5,0,0],[.5,0,0]],.12)
  59. g.tube([[0,kind==='cross'?-.5:0,0],[0,.5,0]],.12)
  60. }
  61. if (kind === 'valve') {
  62. g.tube([[-.5,0,0],[.5,0,0]],.12)
  63. g.tube([[-.16,0,0],[.16,0,0]],.21)
  64. g.tube([[0,0,0],[0,0,.38]],.045)
  65. const circle = Array.from({length:17},(_,i)=>[.22*Math.cos(i*Math.PI/8),.22*Math.sin(i*Math.PI/8),.38])
  66. g.tube(circle,.035,8)
  67. g.tube([[-.22,0,.38],[.22,0,.38]],.025,8)
  68. g.tube([[0,-.22,.38],[0,.22,.38]],.025,8)
  69. }
  70. const arrays = [new Float32Array(g.positions),new Float32Array(g.normals),new Uint16Array(g.indices)]
  71. const bytes = Buffer.concat(arrays.map((array)=>Buffer.from(array.buffer)))
  72. const min=[Infinity,Infinity,Infinity],max=[-Infinity,-Infinity,-Infinity]
  73. 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])}
  74. 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]}
  75. return { bytes, gltf: {
  76. asset:{version:'2.0',generator:'City Lifeline original demonstration pipe meshes'},
  77. scene:0, scenes:[{nodes:[0]}], nodes:[{mesh:0,name:kind}],
  78. meshes:[{primitives:[{attributes:{POSITION:0,NORMAL:1},indices:2,material:0}]}],
  79. materials:[{doubleSided:true,pbrMetallicRoughness:{baseColorFactor:colors[kind],metallicFactor:.15,roughnessFactor:.65}}],
  80. buffers:[{byteLength:bytes.length,uri:`${kind}.bin`}],
  81. 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}],
  82. 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'}],
  83. extras:{demonstration:true,kind,units:'metres',splineAxis:'glTF +Z, start 0, end 1',installationOrientation:'unknown; fixed illustrative ENU orientation'},
  84. } }
  85. }
  86. for (const kind of ['spline','elbow','valve','branch','cross']) {
  87. const { gltf, bytes } = document(kind)
  88. await writeFile(new URL(`${kind}.bin`,out),bytes)
  89. await writeFile(new URL(`${kind}.gltf`,out),JSON.stringify(gltf))
  90. console.log(`${kind}: ${gltf.accessors[0].count} vertices, ${gltf.accessors[2].count/3} triangles`)
  91. }