[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
4.0 kB
122 lines
1import * as THREE from 'three'
2import { Mesh } from '../core/mesh'
3
4/**
5 * Give a mesh thickness by duplicating it, offsetting along normals,
6 * and stitching boundary edges.
7 */
8export function thicken(mesh: Mesh, thickness: number): Mesh {
9 let geo = mesh.geometry.clone()
10 // Keep indexed if possible — we need shared edge info
11 if (!geo.getIndex()) {
12 // Non-indexed: can't reliably find boundary edges, just do top+bottom
13 return thickenSimple(geo, thickness)
14 }
15
16 geo.computeVertexNormals()
17 const pos = geo.getAttribute('position')
18 const norm = geo.getAttribute('normal')
19 const index = geo.getIndex()!
20 const vertCount = pos.count
21 const half = thickness / 2
22
23 // Build new positions: top (offset +normal) then bottom (offset -normal)
24 const newPos = new Float32Array(vertCount * 2 * 3)
25 for (let i = 0; i < vertCount; i++) {
26 const nx = norm.getX(i), ny = norm.getY(i), nz = norm.getZ(i)
27 // Top
28 newPos[i * 3] = pos.getX(i) + nx * half
29 newPos[i * 3 + 1] = pos.getY(i) + ny * half
30 newPos[i * 3 + 2] = pos.getZ(i) + nz * half
31 // Bottom
32 const bi = (i + vertCount) * 3
33 newPos[bi] = pos.getX(i) - nx * half
34 newPos[bi + 1] = pos.getY(i) - ny * half
35 newPos[bi + 2] = pos.getZ(i) - nz * half
36 }
37
38 const indexArr = index.array
39 const triCount = indexArr.length / 3
40 const newIndices: number[] = []
41
42 // Top faces (same winding)
43 for (let i = 0; i < indexArr.length; i++) {
44 newIndices.push(indexArr[i])
45 }
46
47 // Bottom faces (reversed winding)
48 for (let t = 0; t < triCount; t++) {
49 const i = t * 3
50 newIndices.push(
51 indexArr[i] + vertCount,
52 indexArr[i + 2] + vertCount,
53 indexArr[i + 1] + vertCount,
54 )
55 }
56
57 // Find boundary edges: edges that appear in only one triangle
58 const edgeCount = new Map<string, { a: number; b: number; count: number }>()
59 for (let t = 0; t < triCount; t++) {
60 const i = t * 3
61 const v0 = indexArr[i], v1 = indexArr[i + 1], v2 = indexArr[i + 2]
62 for (const [a, b] of [[v0, v1], [v1, v2], [v2, v0]] as [number, number][]) {
63 const key = a < b ? `${a},${b}` : `${b},${a}`
64 const existing = edgeCount.get(key)
65 if (existing) {
66 existing.count++
67 } else {
68 edgeCount.set(key, { a, b, count: 1 })
69 }
70 }
71 }
72
73 // Stitch boundary edges with side quads
74 for (const { a, b, count } of edgeCount.values()) {
75 if (count === 1) {
76 const a2 = a + vertCount
77 const b2 = b + vertCount
78 newIndices.push(a, b2, b)
79 newIndices.push(a, a2, b2)
80 }
81 }
82
83 const result = new THREE.BufferGeometry()
84 result.setAttribute('position', new THREE.BufferAttribute(newPos, 3))
85 result.setIndex(newIndices)
86 result.computeVertexNormals()
87 return new Mesh(result)
88}
89
90/** Fallback for non-indexed geometry: just top + bottom, no side stitching */
91function thickenSimple(geo: THREE.BufferGeometry, thickness: number): Mesh {
92 geo.computeVertexNormals()
93 const pos = geo.getAttribute('position')
94 const norm = geo.getAttribute('normal')
95 const vertCount = pos.count
96 const half = thickness / 2
97
98 const newPos: number[] = []
99 const indices: number[] = []
100
101 for (let i = 0; i < vertCount; i++) {
102 const nx = norm.getX(i), ny = norm.getY(i), nz = norm.getZ(i)
103 newPos.push(pos.getX(i) + nx * half, pos.getY(i) + ny * half, pos.getZ(i) + nz * half)
104 }
105 for (let i = 0; i < vertCount; i++) {
106 const nx = norm.getX(i), ny = norm.getY(i), nz = norm.getZ(i)
107 newPos.push(pos.getX(i) - nx * half, pos.getY(i) - ny * half, pos.getZ(i) - nz * half)
108 }
109
110 const triCount = vertCount / 3
111 for (let t = 0; t < triCount; t++) {
112 const i = t * 3
113 indices.push(i, i + 1, i + 2)
114 indices.push(i + vertCount, i + 2 + vertCount, i + 1 + vertCount)
115 }
116
117 const result = new THREE.BufferGeometry()
118 result.setAttribute('position', new THREE.BufferAttribute(new Float32Array(newPos), 3))
119 result.setIndex(indices)
120 result.computeVertexNormals()
121 return new Mesh(result)
122}