feat(paint-slots): world-space planar UVs for walls (seamless tiling)
Walls used THREE's ExtrudeGeometry UVs, which restart at each wall's local start/end — so finishes seamed at wall joins, showed a mid-face axis-switch stripe, and didn't line up with the roof gable. Re-project the render mesh's UVs in WORLD space (1 unit = 1 m), matching roof-system's pushRoofUv exactly: vertical faces U = ±worldX/Z, V = 1 - worldY. De-indexes so each triangle uses its own face normal (no edge seams). Applied only to the render mesh; collision and floorplan geometry keep the original UVs. Now walls tile continuously across segments and meet the gable seamlessly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c7d0cd3388
commit
65a7fcb74b
@@ -572,7 +572,17 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) {
|
||||
return { ...effective, position: live.position }
|
||||
})
|
||||
|
||||
const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation)
|
||||
const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation)
|
||||
const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0])
|
||||
// World transform the render mesh will apply (position + Y-rotation below).
|
||||
// Reproduce it here so the UVs can be projected in WORLD space — see
|
||||
// `applyWorldPlanarWallUVs`.
|
||||
const wallWorldMatrix = new THREE.Matrix4().compose(
|
||||
new THREE.Vector3(node.start[0], slabElevation, node.start[1]),
|
||||
new THREE.Quaternion().setFromAxisAngle(WALL_UV_Y_AXIS, -wallAngle),
|
||||
WALL_UV_UNIT_SCALE,
|
||||
)
|
||||
const newGeo = applyWorldPlanarWallUVs(builtGeo, wallWorldMatrix)
|
||||
|
||||
mesh.geometry.dispose()
|
||||
mesh.geometry = newGeo
|
||||
@@ -589,6 +599,71 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) {
|
||||
mesh.rotation.y = -angle
|
||||
}
|
||||
|
||||
const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0)
|
||||
const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1)
|
||||
|
||||
/**
|
||||
* Re-project a wall's UVs in WORLD space (1 UV unit = 1 m) so the finish tiles
|
||||
* continuously across adjacent walls and lines up with the roof gable above —
|
||||
* instead of THREE's `ExtrudeGeometry` UVs, which restart at each wall's own
|
||||
* start/end. Matches `roof-system`'s `pushRoofUv` projection exactly: vertical
|
||||
* faces use `U = ±worldX/Z` (the axis across the face normal) and `V = 1 -
|
||||
* worldY`; the thin top/bottom caps use `(worldX, worldZ)`. De-indexes first so
|
||||
* every triangle projects by its own face normal (no shared-vertex seams at
|
||||
* edges). Applied only to the render mesh; collision/floorplan geometry is
|
||||
* untouched.
|
||||
*/
|
||||
function applyWorldPlanarWallUVs(
|
||||
geometry: THREE.BufferGeometry,
|
||||
worldMatrix: THREE.Matrix4,
|
||||
): THREE.BufferGeometry {
|
||||
const target = geometry.index ? geometry.toNonIndexed() : geometry
|
||||
if (target !== geometry) geometry.dispose()
|
||||
|
||||
const position = target.getAttribute('position')
|
||||
if (!position || position.count === 0) return target
|
||||
|
||||
const a = new THREE.Vector3()
|
||||
const b = new THREE.Vector3()
|
||||
const c = new THREE.Vector3()
|
||||
const normal = new THREE.Vector3()
|
||||
const edgeAB = new THREE.Vector3()
|
||||
const edgeAC = new THREE.Vector3()
|
||||
const uvs = new Float32Array(position.count * 2)
|
||||
|
||||
for (let i = 0; i < position.count; i += 3) {
|
||||
a.fromBufferAttribute(position, i).applyMatrix4(worldMatrix)
|
||||
b.fromBufferAttribute(position, i + 1).applyMatrix4(worldMatrix)
|
||||
c.fromBufferAttribute(position, i + 2).applyMatrix4(worldMatrix)
|
||||
edgeAB.subVectors(b, a)
|
||||
edgeAC.subVectors(c, a)
|
||||
normal.crossVectors(edgeAB, edgeAC).normalize()
|
||||
|
||||
const absX = Math.abs(normal.x)
|
||||
const absY = Math.abs(normal.y)
|
||||
const absZ = Math.abs(normal.z)
|
||||
|
||||
for (let k = 0; k < 3; k += 1) {
|
||||
const p = k === 0 ? a : k === 1 ? b : c
|
||||
let u: number
|
||||
let v: number
|
||||
if (absY >= absX && absY >= absZ) {
|
||||
u = p.x
|
||||
v = p.z
|
||||
} else {
|
||||
v = 1 - p.y
|
||||
u = absX >= absZ ? (normal.x >= 0 ? p.z : -p.z) : normal.z >= 0 ? p.x : -p.x
|
||||
}
|
||||
uvs[(i + k) * 2] = u
|
||||
uvs[(i + k) * 2 + 1] = v
|
||||
}
|
||||
}
|
||||
|
||||
target.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
||||
target.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2))
|
||||
return target
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates extruded wall geometry with mitering and cutouts
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user