import * as THREE from 'three' export const cardDimensions = { width: 0.063, height: 0.0882, thickness: 0.0004, cornerRadius: 0.0028, } as const const sceneHeight = 4 const sceneScale = sceneHeight / cardDimensions.height export const cardSceneDimensions = { width: cardDimensions.width * sceneScale, height: cardDimensions.height * sceneScale, thickness: cardDimensions.thickness * sceneScale, cornerRadius: cardDimensions.cornerRadius * sceneScale, } as const function createRoundedRectangle(width: number, height: number, radius: number) { const halfWidth = width / 2 const halfHeight = height / 2 const shape = new THREE.Shape() shape.moveTo(-halfWidth + radius, -halfHeight) shape.lineTo(halfWidth - radius, -halfHeight) shape.quadraticCurveTo(halfWidth, -halfHeight, halfWidth, -halfHeight + radius) shape.lineTo(halfWidth, halfHeight - radius) shape.quadraticCurveTo(halfWidth, halfHeight, halfWidth - radius, halfHeight) shape.lineTo(-halfWidth + radius, halfHeight) shape.quadraticCurveTo(-halfWidth, halfHeight, -halfWidth, halfHeight - radius) shape.lineTo(-halfWidth, -halfHeight + radius) shape.quadraticCurveTo(-halfWidth, -halfHeight, -halfWidth + radius, -halfHeight) return shape } export function createCardGeometry( frontMaterial: THREE.Material, backMaterial: THREE.Material, edgeMaterial: THREE.Material, ) { const { width, height, thickness, cornerRadius } = cardSceneDimensions const shape = createRoundedRectangle(width, height, cornerRadius) const bodyGeometry = new THREE.ExtrudeGeometry(shape, { curveSegments: 16, depth: thickness, steps: 1, bevelEnabled: false, }) bodyGeometry.translate(0, 0, -thickness / 2) // Extrusion caps would duplicate the dedicated faces and z-fight at Pack's camera distances. const sideWalls = bodyGeometry.groups.find((group) => group.materialIndex === 1) if (!sideWalls) throw new Error('Card extrusion is missing its side-wall geometry') bodyGeometry.setDrawRange(sideWalls.start, sideWalls.count) const faceGeometry = new THREE.ShapeGeometry(shape, 16) const positions = faceGeometry.getAttribute('position') const uvs = faceGeometry.getAttribute('uv') for (let index = 0; index < positions.count; index += 1) { uvs.setXY( index, positions.getX(index) / width + 0.5, positions.getY(index) / height + 0.5, ) } uvs.needsUpdate = true const faceOffset = thickness / 2 const body = new THREE.Mesh(bodyGeometry, edgeMaterial) body.name = 'CARD_EDGE' const front = new THREE.Mesh(faceGeometry, frontMaterial) front.name = 'CARD_FRONT' front.position.z = faceOffset const back = new THREE.Mesh(faceGeometry.clone(), backMaterial) back.name = 'CARD_BACK' back.position.z = -faceOffset back.rotation.y = Math.PI const card = new THREE.Group() card.name = 'CARD_PROCEDURAL' card.add(body, front, back) for (const mesh of [body, front, back]) { mesh.castShadow = true mesh.receiveShadow = true } return card }