Add roof material targets and UV mapping
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user