feat: add outlet direction (horizontal/vertical), wall light styles, floor textures, and stretch ceiling
- Add configurable outlet direction (horizontal/vertical) stored in metadata - Add wall light style variants (classic, pendant-globe, sconce-up, sconce-down) - Add PBR floor textures including natural oak - Add stretch ceiling offset support with DB migration - Add furniture surface texture selection - Add canvas theme colors utility for dark mode support - Update projection views with improved rendering - Add EN and RU translations for all new properties
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import type { FurnitureItem, FurnitureType } from '@house-plan-maker/shared';
|
||||
import { rotatedAnchorOffsetToCenter } from '@house-plan-maker/shared';
|
||||
import { rotatedAnchorOffsetToCenter, TEXTURABLE_FURNITURE } from '@house-plan-maker/shared';
|
||||
import { getCurtainLeftOpen, getCurtainRightOpen, getCurtainFabricColor } from '../utils/curtainMetadata';
|
||||
import { getFurnitureTexture } from '../utils/furnitureTextureMetadata';
|
||||
import { getFurnitureSurfacePbr, computeTextureRepeat } from './utils/pbrTextures';
|
||||
|
||||
/**
|
||||
* Below this opacity threshold, furniture meshes stop casting shadows. Cast
|
||||
@@ -86,6 +88,37 @@ export function setFurnitureGlobalOpacity(_opacity: number): void {
|
||||
// intentionally empty — per-item material clones own their opacity now
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a material for furniture surfaces: either a PBR textured material
|
||||
* (when the item has a surfaceTexture set) or the solid-color fallback.
|
||||
* The PBR material's UV repeat is scaled for the given surface dimensions.
|
||||
*/
|
||||
function useSurfaceMaterial(
|
||||
item: FurnitureItem,
|
||||
solidColor: string,
|
||||
solidRoughness: number,
|
||||
surfaceWidth: number,
|
||||
surfaceDepth: number,
|
||||
): THREE.MeshStandardMaterial {
|
||||
return useMemo(() => {
|
||||
if (!TEXTURABLE_FURNITURE.includes(item.type)) {
|
||||
return getFurnitureMaterial(solidColor, solidRoughness);
|
||||
}
|
||||
const texture = getFurnitureTexture(item.metadata);
|
||||
const pbr = getFurnitureSurfacePbr(texture);
|
||||
if (!pbr) {
|
||||
return getFurnitureMaterial(solidColor, solidRoughness);
|
||||
}
|
||||
// Clone the PBR material so UV repeat is per-item, not global.
|
||||
const mat = pbr.material.clone();
|
||||
const repeat = computeTextureRepeat(surfaceWidth, surfaceDepth, pbr.tileMeters);
|
||||
if (mat.map) { mat.map = mat.map.clone(); mat.map.repeat.set(repeat.u, repeat.v); }
|
||||
if (mat.normalMap) { mat.normalMap = mat.normalMap.clone(); mat.normalMap.repeat.set(repeat.u, repeat.v); }
|
||||
if (mat.roughnessMap) { mat.roughnessMap = mat.roughnessMap.clone(); mat.roughnessMap.repeat.set(repeat.u, repeat.v); }
|
||||
return mat;
|
||||
}, [item.type, item.metadata, solidColor, solidRoughness, surfaceWidth, surfaceDepth]);
|
||||
}
|
||||
|
||||
// ── Shared geometries for common shapes ──
|
||||
|
||||
const legGeometry = new THREE.CylinderGeometry(LEG_RADIUS, LEG_RADIUS, 1, LEG_SEGMENTS);
|
||||
@@ -101,11 +134,13 @@ function BedMesh({ item, color }: { readonly item: FurnitureItem; readonly color
|
||||
const frameHeight = item.height * 0.3;
|
||||
const headboardHeight = item.height;
|
||||
const mattressMaterial = useMemo(() => getFurnitureMaterial(color, 0.9), [color]);
|
||||
const frameMaterial = useSurfaceMaterial(item, LEG_COLOR, 0.6, item.width, item.depth);
|
||||
const headboardMaterial = useSurfaceMaterial(item, LEG_COLOR, 0.5, item.width, headboardHeight);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Frame */}
|
||||
<mesh position={[0, frameHeight / 2, 0]} castShadow material={legMaterial}>
|
||||
<mesh position={[0, frameHeight / 2, 0]} castShadow material={frameMaterial}>
|
||||
<boxGeometry args={[item.width, frameHeight, item.depth]} />
|
||||
</mesh>
|
||||
{/* Mattress */}
|
||||
@@ -113,7 +148,7 @@ function BedMesh({ item, color }: { readonly item: FurnitureItem; readonly color
|
||||
<boxGeometry args={[item.width * 0.95, mattressHeight, item.depth * 0.95]} />
|
||||
</mesh>
|
||||
{/* Headboard */}
|
||||
<mesh position={[0, headboardHeight / 2, -item.depth / 2 + 0.02]} castShadow material={legMaterialSmooth}>
|
||||
<mesh position={[0, headboardHeight / 2, -item.depth / 2 + 0.02]} castShadow material={headboardMaterial}>
|
||||
<boxGeometry args={[item.width, headboardHeight, 0.04]} />
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -125,7 +160,7 @@ function DeskMesh({ item, color }: { readonly item: FurnitureItem; readonly colo
|
||||
const topThickness = 0.04;
|
||||
const legHeight = item.height - topThickness;
|
||||
const inset = 0.05;
|
||||
const topMaterial = useMemo(() => getFurnitureMaterial(color, 0.5), [color]);
|
||||
const topMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -150,7 +185,7 @@ function DeskMesh({ item, color }: { readonly item: FurnitureItem; readonly colo
|
||||
|
||||
/** Wardrobe: tall box with slight door line */
|
||||
function WardrobeMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
|
||||
const bodyMaterial = useMemo(() => getFurnitureMaterial(color, 0.6), [color]);
|
||||
const bodyMaterial = useSurfaceMaterial(item, color, 0.6, item.width, item.height);
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -207,7 +242,7 @@ function TableMesh({ item, color }: { readonly item: FurnitureItem; readonly col
|
||||
const topThickness = 0.03;
|
||||
const legHeight = item.height - topThickness;
|
||||
const inset = 0.05;
|
||||
const topMaterial = useMemo(() => getFurnitureMaterial(color, 0.5), [color]);
|
||||
const topMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -234,7 +269,7 @@ function ChairMesh({ item, color }: { readonly item: FurnitureItem; readonly col
|
||||
const seatThickness = 0.03;
|
||||
const legHeight = seatHeight - seatThickness;
|
||||
const inset = 0.03;
|
||||
const chairMaterial = useMemo(() => getFurnitureMaterial(color, 0.6), [color]);
|
||||
const chairMaterial = useSurfaceMaterial(item, color, 0.6, item.width, item.depth);
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -263,7 +298,7 @@ function ChairMesh({ item, color }: { readonly item: FurnitureItem; readonly col
|
||||
|
||||
/** Shelf / Bookcase / Nightstand / Dresser / Other: simple box */
|
||||
function SimpleBoxMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
|
||||
const material = useMemo(() => getFurnitureMaterial(color, 0.6), [color]);
|
||||
const material = useSurfaceMaterial(item, color, 0.6, item.width, item.depth);
|
||||
|
||||
return (
|
||||
<mesh position={[0, item.height / 2, 0]} castShadow material={material}>
|
||||
@@ -301,7 +336,7 @@ function BookcaseMesh({ item, color }: { readonly item: FurnitureItem; readonly
|
||||
const hasBackPanel = typeof hasBackPanelRaw === 'boolean' ? hasBackPanelRaw : true;
|
||||
|
||||
const panelThickness = 0.02;
|
||||
const material = useMemo(() => getFurnitureMaterial(color, 0.6), [color]);
|
||||
const material = useSurfaceMaterial(item, color, 0.6, item.width, item.height);
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -589,7 +624,7 @@ function CurtainMesh({ item, color: _defaultColor }: { readonly item: FurnitureI
|
||||
* looking sparse.
|
||||
*/
|
||||
function CribMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
|
||||
const frameMaterial = useMemo(() => getFurnitureMaterial(color, 0.55), [color]);
|
||||
const frameMaterial = useSurfaceMaterial(item, color, 0.55, item.width, item.depth);
|
||||
const mattressMaterial = useMemo(() => getFurnitureMaterial('#f4eadf', 0.9), []);
|
||||
|
||||
const mattressThick = 0.08;
|
||||
@@ -710,7 +745,7 @@ function CribMesh({ item, color }: { readonly item: FurnitureItem; readonly colo
|
||||
* upright panel.
|
||||
*/
|
||||
function DressingTableMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) {
|
||||
const bodyMaterial = useMemo(() => getFurnitureMaterial(color, 0.5), [color]);
|
||||
const bodyMaterial = useSurfaceMaterial(item, color, 0.5, item.width, item.depth);
|
||||
const mirrorMaterial = useMemo(() => getFurnitureMaterial('#b8d0d8', 0.05), []);
|
||||
const mirrorFrameMaterial = useMemo(() => getFurnitureMaterial(color, 0.4), [color]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user