import { BoxGeometry } from "three"; import type { Vec2, Vec3 } from "../core/vector"; import { BOX_FACE_IDS, createDefaultFaceUvState, type Brush, type BoxFaceId, type FaceUvState, type WhiteboxFaceId } from "../document/brushes"; import { getBrushFaceBasis, getBrushLocalVertexPosition } from "./whitebox-brush"; import { getBrushFaceVertexIds } from "./whitebox-topology"; interface BoxBrushUvProjectionSource { size: Vec3; faces: Record; } function computeGenericBrushFaceSize(brush: Brush, faceId: WhiteboxFaceId): Vec2 { const basis = getBrushFaceBasis(brush, faceId); const projectedVertices = getBrushFaceVertexIds(brush, faceId).map( (vertexId) => { const vertex = getBrushLocalVertexPosition(brush, vertexId); const relative = { x: vertex.x - basis.origin.x, y: vertex.y - basis.origin.y, z: vertex.z - basis.origin.z }; return { x: relative.x * basis.uAxis.x + relative.y * basis.uAxis.y + relative.z * basis.uAxis.z, y: relative.x * basis.vAxis.x + relative.y * basis.vAxis.y + relative.z * basis.vAxis.z }; } ); const firstVertex = projectedVertices[0]; const min = { ...firstVertex }; const max = { ...firstVertex }; for (const projectedVertex of projectedVertices.slice(1)) { min.x = Math.min(min.x, projectedVertex.x); min.y = Math.min(min.y, projectedVertex.y); max.x = Math.max(max.x, projectedVertex.x); max.y = Math.max(max.y, projectedVertex.y); } return { x: max.x - min.x, y: max.y - min.y }; } export function getBoxBrushFaceSize( brush: BoxBrushUvProjectionSource | Brush, faceId: BoxFaceId | WhiteboxFaceId ): Vec2 { if ("kind" in brush) { if (brush.kind !== "box") { return computeGenericBrushFaceSize(brush, faceId); } } switch (faceId) { case "posX": case "negX": return { x: brush.size.z, y: brush.size.y }; case "posY": case "negY": return { x: brush.size.x, y: brush.size.z }; case "posZ": case "negZ": return { x: brush.size.x, y: brush.size.y }; } if ("kind" in brush) { return computeGenericBrushFaceSize(brush, faceId); } throw new Error(`Unsupported box face id ${faceId}.`); } export function createFitToFaceBoxBrushFaceUvState( brush: Brush, faceId: WhiteboxFaceId ): FaceUvState { const faceSize = getBoxBrushFaceSize(brush, faceId); return { ...createDefaultFaceUvState(), scale: { x: 1 / faceSize.x, y: 1 / faceSize.y } }; } export function createFitToMaterialTileBoxBrushFaceUvState( brush: Brush, faceId: WhiteboxFaceId, tileSize: Vec2 ): FaceUvState { const faceSize = getBoxBrushFaceSize(brush, faceId); return { ...createDefaultFaceUvState(), scale: { x: tileSize.x / faceSize.x, y: tileSize.y / faceSize.y } }; } export function projectBoxFaceVertexToUv(vertexPosition: Vec3, brush: BoxBrushUvProjectionSource, faceId: BoxFaceId): Vec2 { const halfSize = { x: brush.size.x * 0.5, y: brush.size.y * 0.5, z: brush.size.z * 0.5 }; switch (faceId) { case "posX": return { x: halfSize.z - vertexPosition.z, y: vertexPosition.y + halfSize.y }; case "negX": return { x: vertexPosition.z + halfSize.z, y: vertexPosition.y + halfSize.y }; case "posY": return { x: vertexPosition.x + halfSize.x, y: halfSize.z - vertexPosition.z }; case "negY": return { x: vertexPosition.x + halfSize.x, y: vertexPosition.z + halfSize.z }; case "posZ": return { x: vertexPosition.x + halfSize.x, y: vertexPosition.y + halfSize.y }; case "negZ": return { x: halfSize.x - vertexPosition.x, y: vertexPosition.y + halfSize.y }; } } export function transformProjectedFaceUv(baseUv: Vec2, faceSize: Vec2, uvState: FaceUvState): Vec2 { let u = (baseUv.x - faceSize.x * 0.5) * uvState.scale.x; let v = (baseUv.y - faceSize.y * 0.5) * uvState.scale.y; if (uvState.flipU) { u *= -1; } if (uvState.flipV) { v *= -1; } switch (uvState.rotationQuarterTurns) { case 1: { const nextU = -v; v = u; u = nextU; break; } case 2: u *= -1; v *= -1; break; case 3: { const nextU = v; v = -u; u = nextU; break; } } return { x: u + faceSize.x * 0.5 * uvState.scale.x + uvState.offset.x, y: v + faceSize.y * 0.5 * uvState.scale.y + uvState.offset.y }; } export function applyBoxBrushFaceUvsToGeometry(geometry: BoxGeometry, brush: BoxBrushUvProjectionSource): void { const positionAttribute = geometry.getAttribute("position"); const uvAttribute = geometry.getAttribute("uv"); const indexAttribute = geometry.getIndex(); if (indexAttribute === null) { throw new Error("BoxGeometry is expected to be indexed for face UV projection."); } // BoxGeometry groups follow the same px, nx, py, ny, pz, nz order as the canonical face ids. for (const [materialIndex, faceId] of BOX_FACE_IDS.entries()) { const group = geometry.groups.find((candidate) => candidate.materialIndex === materialIndex); if (group === undefined) { continue; } const faceSize = getBoxBrushFaceSize(brush, faceId); const vertexIndices = new Set(); for (let indexOffset = group.start; indexOffset < group.start + group.count; indexOffset += 1) { vertexIndices.add(indexAttribute.getX(indexOffset)); } for (const vertexIndex of vertexIndices) { const localVertexPosition = { x: positionAttribute.getX(vertexIndex), y: positionAttribute.getY(vertexIndex), z: positionAttribute.getZ(vertexIndex) }; const projectedUv = projectBoxFaceVertexToUv(localVertexPosition, brush, faceId); const transformedUv = transformProjectedFaceUv(projectedUv, faceSize, brush.faces[faceId].uv); uvAttribute.setXY(vertexIndex, transformedUv.x, transformedUv.y); } } uvAttribute.needsUpdate = true; }