Add roof material targets and UV mapping

This commit is contained in:
sudhir
2026-04-20 12:57:06 +05:30
parent 05002a7865
commit bbc351d3a4
11 changed files with 428 additions and 47 deletions
+3 -3
View File
@@ -51,7 +51,7 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
id: 'wall-wood1',
label: 'Wood',
description: 'Warm wood finish',
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
previewThumbnailUrl: '/material/wood1/wood1_thumbnail.webp',
preset: {
maps: {
@@ -86,7 +86,7 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
id: 'wall-wood2',
label: 'Wood',
description: 'Textured wood finish',
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
previewThumbnailUrl: '/material/wood2/wood2_thumbnail.webp',
preset: {
maps: {
@@ -122,7 +122,7 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
id: 'wall-wood3',
label: 'Wood',
description: 'Knotted timber finish',
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
previewThumbnailUrl: '/material/wood3/wood3_thumbnail.webp',
preset: {
maps: {
+2 -1
View File
@@ -43,7 +43,8 @@ export type {
} from './nodes/item'
export { getScaledDimensions, ItemNode } from './nodes/item'
export { LevelNode } from './nodes/level'
export { RoofNode } from './nodes/roof'
export { getEffectiveRoofSurfaceMaterial, RoofNode } from './nodes/roof'
export type { RoofSurfaceMaterialRole, RoofSurfaceMaterialSpec } from './nodes/roof'
export { RoofSegmentNode, RoofType } from './nodes/roof-segment'
export { ScanNode } from './nodes/scan'
// Nodes
+77 -2
View File
@@ -1,14 +1,26 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'
import { type MaterialSchema, MaterialSchema as MaterialSchemaSchema } from '../material'
import { RoofSegmentNode } from './roof-segment'
export type RoofSurfaceMaterialRole = 'top' | 'edge' | 'wall'
export type RoofSurfaceMaterialSpec = {
material?: MaterialSchema
materialPreset?: string
}
export const RoofNode = BaseNode.extend({
id: objectId('roof'),
type: nodeType('roof'),
material: MaterialSchema.optional(),
material: MaterialSchemaSchema.optional(),
materialPreset: z.string().optional(),
topMaterial: MaterialSchemaSchema.optional(),
topMaterialPreset: z.string().optional(),
edgeMaterial: MaterialSchemaSchema.optional(),
edgeMaterialPreset: z.string().optional(),
wallMaterial: MaterialSchemaSchema.optional(),
wallMaterialPreset: z.string().optional(),
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
// Rotation around Y axis in radians
rotation: z.number().default(0),
@@ -26,3 +38,66 @@ export const RoofNode = BaseNode.extend({
)
export type RoofNode = z.infer<typeof RoofNode>
function getLegacyRoofSurfaceMaterial(node: RoofNode): RoofSurfaceMaterialSpec {
return {
material: node.material,
materialPreset: node.materialPreset,
}
}
export function getEffectiveRoofSurfaceMaterial(
node: RoofNode,
role: RoofSurfaceMaterialRole,
): RoofSurfaceMaterialSpec {
if (role === 'top') {
if (node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string') {
return {
material: node.topMaterial,
materialPreset: typeof node.topMaterialPreset === 'string' ? node.topMaterialPreset : undefined,
}
}
}
if (role === 'edge') {
if (node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string') {
return {
material: node.edgeMaterial,
materialPreset:
typeof node.edgeMaterialPreset === 'string' ? node.edgeMaterialPreset : undefined,
}
}
}
if (role === 'wall') {
if (node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string') {
return {
material: node.wallMaterial,
materialPreset:
typeof node.wallMaterialPreset === 'string' ? node.wallMaterialPreset : undefined,
}
}
}
if (role === 'edge') {
if (node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string') {
return {
material: node.wallMaterial,
materialPreset:
typeof node.wallMaterialPreset === 'string' ? node.wallMaterialPreset : undefined,
}
}
}
if (role === 'wall') {
if (node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string') {
return {
material: node.edgeMaterial,
materialPreset:
typeof node.edgeMaterialPreset === 'string' ? node.edgeMaterialPreset : undefined,
}
}
}
return getLegacyRoofSurfaceMaterial(node)
}
+61
View File
@@ -224,6 +224,63 @@ function migrateStairSurfaceMaterials(node: Record<string, any>) {
return next
}
function migrateRoofSurfaceMaterials(node: Record<string, any>) {
const hasTop = node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string'
const hasEdge = node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string'
const hasWall = node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string'
const legacyFinish = {
material: node.material,
materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
}
if (!hasTop && !hasEdge && !hasWall) {
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
return node
}
return {
...node,
topMaterial: legacyFinish.material,
topMaterialPreset: legacyFinish.materialPreset,
edgeMaterial: legacyFinish.material,
edgeMaterialPreset: legacyFinish.materialPreset,
wallMaterial: legacyFinish.material,
wallMaterialPreset: legacyFinish.materialPreset,
}
}
const next = { ...node }
if (!hasTop) {
next.topMaterial = legacyFinish.material
next.topMaterialPreset = legacyFinish.materialPreset
}
if (!hasEdge) {
if (node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string') {
next.edgeMaterial = node.wallMaterial
next.edgeMaterialPreset =
typeof node.wallMaterialPreset === 'string' ? node.wallMaterialPreset : undefined
} else {
next.edgeMaterial = legacyFinish.material
next.edgeMaterialPreset = legacyFinish.materialPreset
}
}
if (!hasWall) {
if (node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string') {
next.wallMaterial = node.edgeMaterial
next.wallMaterialPreset =
typeof node.edgeMaterialPreset === 'string' ? node.edgeMaterialPreset : undefined
} else {
next.wallMaterial = legacyFinish.material
next.wallMaterialPreset = legacyFinish.materialPreset
}
}
return next
}
function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
const patchedNodes = { ...nodes }
for (const [id, node] of Object.entries(patchedNodes)) {
@@ -281,6 +338,10 @@ function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
if (node.type === 'wall') {
patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id])
}
if (node.type === 'roof') {
patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id])
}
}
return patchedNodes as Record<string, AnyNode>
}
+38 -1
View File
@@ -11,7 +11,7 @@ import useScene from '../../store/use-scene'
const csgEvaluator = new Evaluator()
csgEvaluator.useGroups = true
;(csgEvaluator as any).consolidateGroups = false // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
csgEvaluator.attributes = ['position', 'normal']
csgEvaluator.attributes = ['position', 'normal', 'uv']
function prepareBrushForCSG(brush: Brush) {
brush.geometry.computeBoundsTree = computeBoundsTree
@@ -25,6 +25,7 @@ const _position = new THREE.Vector3()
const _quaternion = new THREE.Quaternion()
const _scale = new THREE.Vector3(1, 1, 1)
const _yAxis = new THREE.Vector3(0, 1, 0)
const _uvFaceNormal = new THREE.Vector3()
// Pending merged-roof updates carried across frames (for throttling)
const pendingRoofUpdates = new Set<AnyNodeId>()
@@ -251,6 +252,7 @@ function updateMergedRoofGeometry(
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex)
}
ensureUv2Attribute(resultGeo)
resultGeo.computeVertexNormals()
mergedMesh.geometry.dispose()
mergedMesh.geometry = resultGeo
@@ -641,6 +643,7 @@ export function generateRoofSegmentGeometry(node: RoofSegmentNode): THREE.Buffer
wallBrush.geometry.dispose()
innerBrush.geometry.dispose()
ensureUv2Attribute(resultGeo)
resultGeo.computeVertexNormals()
return resultGeo
}
@@ -936,6 +939,7 @@ function createGeometryFromFaces(
): THREE.BufferGeometry {
const positions: number[] = []
const normals: number[] = []
const uvs: number[] = []
const indices: number[] = []
const groups: { start: number; count: number; materialIndex: number }[] = []
let vertexCount = 0
@@ -974,6 +978,10 @@ function createGeometryFromFaces(
normals.push(normal.x, normal.y, normal.z)
normals.push(normal.x, normal.y, normal.z)
pushRoofUv(uvs, p0, normal)
pushRoofUv(uvs, fi, normal)
pushRoofUv(uvs, fi1, normal)
indices.push(vertexCount, vertexCount + 1, vertexCount + 2)
faceVertexCount += 3
@@ -990,6 +998,7 @@ function createGeometryFromFaces(
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
geometry.setIndex(indices)
for (const g of groups) {
@@ -999,6 +1008,34 @@ function createGeometryFromFaces(
// Merge identical vertices to optimize geometry for CSG and create clean topology
const mergedGeo = mergeVertices(geometry, 1e-4)
geometry.dispose()
ensureUv2Attribute(mergedGeo)
return mergedGeo
}
function pushRoofUv(uvs: number[], point: THREE.Vector3, normal: THREE.Vector3) {
_uvFaceNormal.copy(normal).normalize()
const absX = Math.abs(_uvFaceNormal.x)
const absY = Math.abs(_uvFaceNormal.y)
const absZ = Math.abs(_uvFaceNormal.z)
if (absY >= absX && absY >= absZ) {
uvs.push(point.x, point.z)
return
}
if (absX >= absZ) {
uvs.push(point.z, point.y)
return
}
uvs.push(point.x, point.y)
}
function ensureUv2Attribute(geometry: THREE.BufferGeometry) {
const uv = geometry.getAttribute('uv')
if (!uv) return
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2))
}