import { Box3, BoxGeometry, Color, DirectionalLight, DoubleSide, Mesh, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, Object3D, Vector3, } from "three"; import { GLTFLoader } from "three/examples/jsm/Addons.js"; const gltfLoader = new GLTFLoader().setPath("/static/models/"); const normalized = async (model: Object3D, pub = true) => { const parent = new Object3D(); parent.add(model); const bbox = new Box3().setFromObject(parent); const size = bbox.getSize(new Vector3()); if (pub) { parent.scale.set(1 / size.x, 1 / size.y, 1 / size.z); } else { const min = Math.max(size.x, size.y, size.z); parent.scale.set(1 / min, 1 / min, 1 / min); } model.traverse((child: any) => { if (child.isMesh) { child.receiveShadow = true; child.castShadow = true; } }); const center = new Box3().setFromObject(parent).getCenter(new Vector3()); parent.position.sub({ x: center.x, y: center.y, z: center.z }); return parent; }; const resources: Record Promise> = { "men_l.svg": async () => { const gltf = await gltfLoader.loadAsync("door_with_frame/scene.gltf"); gltf.scene.rotateY(Math.PI); gltf.scene.scale.setX(-1); return await normalized(gltf.scene); }, "piaochuang.svg": async () => { const gltf = await gltfLoader.loadAsync("window_1/scene.gltf"); gltf.scene.rotateY(Math.PI); gltf.scene.traverse((node: any) => { if (!node.isMesh) return; if (node.name.includes("Object")) { node.material = new MeshPhysicalMaterial({ color: 0xffffff, // 浅灰色(可根据需求调整,如0xcccccc) metalness: 0.1, // 轻微金属感(增强反射) roughness: 0.01, // 表面光滑度(0-1,越小越光滑) transmission: 1, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持) opacity: 1, // 透明度(与transmission配合使用) transparent: true, // 启用透明 side: DoubleSide, // 双面渲染(玻璃通常需要) ior: 0, // 折射率(玻璃约为1.5) clearcoat: 0.5, // 可选:表面清漆层(增强反光) }); } else if (node.name.includes("_111111_white_plastic")) { node.material = new MeshStandardMaterial({ color: 0xffffff, // 浅灰色 metalness: 0.9, // 高金属度 roughness: 0.3, // 中等粗糙度 side: DoubleSide, }); } else if ( node.name.includes("_111111_seam_0") || node.name.includes("_111111__15_0") || node.name.includes("_111111_Aluminium_profile_0") ) { node.material = new MeshStandardMaterial({ color: 0xffffff, metalness: 0.8, roughness: 0.4, aoMapIntensity: 1.0, side: DoubleSide, }); } else { node.material = new MeshPhongMaterial({ side: DoubleSide, color: 0xffffff, }); } }); const model = await normalized(gltf.scene); model.scale.add({ x: 0.00015, y: 0.0001, z: 0 }); model.position.add({ x: -0.01, y: -0.005, z: 0.02 }); return model; }, "chuang.svg": async () => { const gltf = await gltfLoader.loadAsync("window (3)/scene.gltf"); return await normalized(gltf.scene); }, "yimen.svg": async () => { const gltf = await gltfLoader.loadAsync("sliding_door/scene.gltf"); return await normalized(gltf.scene); }, "shuangkaimen.svg": async () => { const gltf = await gltfLoader.loadAsync( "white_double_windowed_door/scene.gltf" ); return await normalized(gltf.scene); }, "luodichuang.svg": async () => { const gltf = await gltfLoader.loadAsync("window2/scene.gltf"); gltf.scene.traverse((node: any) => { if (node.name?.includes("glass_0")) { node.material = new MeshPhysicalMaterial({ color: 0xffffff, // 浅灰色(可根据需求调整,如0xcccccc) metalness: 0.1, // 轻微金属感(增强反射) roughness: 0.01, // 表面光滑度(0-1,越小越光滑) transmission: 1, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持) opacity: 1, // 透明度(与transmission配合使用) transparent: true, // 启用透明 side: DoubleSide, // 双面渲染(玻璃通常需要) ior: 0, // 折射率(玻璃约为1.5) clearcoat: 0.5, // 可选:表面清漆层(增强反光) }); } }); return await normalized(gltf.scene); }, "DoubleBed.svg": async () => { const gltf = await gltfLoader.loadAsync("bed/scene.gltf"); const models: Object3D[] = []; const delModelName = ["Pillow_2002", "Plane002"]; gltf.scene.traverse((child: any) => { if (delModelName.some((n) => n === child.name)) { models.push(child); } }); models.forEach((m) => m.parent?.remove(m)); const model = await normalized(gltf.scene); model.position.setY(model.position.y - 0.131); return model; }, "SingleBed.svg": async () => { const gltf = await gltfLoader.loadAsync("woodbed/scene.gltf"); const model = await normalized(gltf.scene); model.rotateY(Math.PI / 2); return model; }, sf: async () => { const gltf = await gltfLoader.loadAsync( "sofa_set_-_4_type_of_sofa_lowpoly./scene.gltf" ); return gltf.scene; }, "ThreeSofa.svg": async () => { const gltf = await gltfLoader.loadAsync( "sofa_-_game_ready_model/scene.gltf" ); const model = await normalized(gltf.scene, undefined); model.traverse((child: any) => { if (child.isMesh) { child.material.color = new Color(0x444444); } }); return model; }, "SingleSofa.svg": async () => { const scene = (await getModel("sf"))!; const models: Object3D[] = []; const pickModelName = ["Cube026"]; scene.traverse((child: any) => { if (pickModelName.some((n) => n === child.name)) { models.push(child); } }); const model = new Object3D().add(...models.map((item) => item.clone())); model.rotateY(Math.PI / 2); return await normalized(model); }, "Desk.svg": async () => { const scene = (await getModel("sf"))!; const models: Object3D[] = []; const pickModelName = ["Cube004"]; scene.traverse((child: any) => { if (pickModelName.some((n) => n === child.name)) { models.push(child); } }); const model = new Object3D().add(...models.map((item) => item.clone())); model.rotateY(Math.PI / 2); return await normalized(model); }, "TeaTable.svg": async () => { return (await getModel("Desk.svg"))!.clone(); }, "DiningTable.svg": async () => { const desk = new Object3D().add((await getModel("Desk.svg"))!.clone()); const chair = (await getModel("Chair.svg"))!; const model = new Object3D(); const lt = chair.clone(); lt.position.set(-0.14, -0.5, 0.25); lt.scale.set(0.5, 1.2, 0.8); lt.rotateY(Math.PI); model.add(lt); const rt = chair.clone(); rt.position.set(0.14, -0.5, 0.25); rt.scale.set(0.5, 1.2, 0.8); rt.rotateY(Math.PI); model.add(rt); const lb = chair.clone(); lb.position.set(-0.14, -0.5, -0.25); lb.scale.set(0.5, 1.2, 0.8); model.add(lb); const rb = chair.clone(); rb.position.set(0.14, -0.5, -0.25); rb.scale.set(0.5, 1.2, 0.8); model.add(rb); desk.scale.set(1.2, 1, 0.55); model.add(desk); const nModel = await normalized(model); nModel.position.sub({ x: 0, y: 0.075, z: 0 }); return nModel; }, "Chair.svg": async () => { const gltf = await gltfLoader.loadAsync("psx_chair/scene.gltf"); const model = await normalized(gltf.scene, undefined); model.scale.add({ x: 0, y: 0.3, z: 0 }); return model; }, "TV.svg": async () => { const gltf = await gltfLoader.loadAsync("tv_and_tv_stand/scene.gltf"); const model = await normalized(gltf.scene, undefined); return model; }, "Plant.svg": async () => { const gltf = await gltfLoader.loadAsync("pothos_plant/scene.gltf"); const model = await normalized(gltf.scene, undefined); return model; }, "Washstand.svg": async () => { const gltf = await gltfLoader.loadAsync("washbasin/scene.gltf"); gltf.scene.rotateY(Math.PI); const model = await normalized(gltf.scene, undefined); return model; }, "Closestool.svg": async () => { const gltf = await gltfLoader.loadAsync("toilet/scene.gltf"); const model = await normalized(gltf.scene, undefined); model.traverse((child: any) => { if (child.isMesh) { child.material.color = new Color(0xffffff); } }); return model; }, "Wardrobe.svg": async () => { const gltf = await gltfLoader.loadAsync("wardrobe_14722-22/scene.gltf"); const model = await normalized(gltf.scene, undefined); model.traverse((child: any) => { if (child.isMesh) { child.material.color = new Color(0xcbc3b3); } }); return model; }, "BedsideCupboard.svg": async () => { const gltf = await gltfLoader.loadAsync( "low_poly_bedside_table/scene.gltf" ); const model = await normalized(gltf.scene, undefined); model.traverse((child: any) => { if (child.isMesh) { child.material.color = new Color(0xffffff); } }); return model; }, "CombinationSofa.svg": async () => { const tsofa = (await getModel("ThreeSofa.svg"))!.clone(); const ssofa = (await getModel("SingleSofa.svg"))!.clone(); const tea = (await getModel("TeaTable.svg"))!.clone(); const model = new Object3D(); // tsofa.rotateY(-Math.PI / 2) tsofa.scale.multiply({ x: 0.8, y: 1, z: 0.4 }); tsofa.position.add({ x: -0, y: 0, z: -0.6 }); model.add(tsofa); ssofa.rotateY(-Math.PI / 2); ssofa.scale.multiply({ x: 0.4, y: 1, z: 0.4 }); ssofa.position.add({ x: -0.15, y: 0, z: -2.2 }); model.add(ssofa); tea.scale.multiply({ x: 0.8, y: 0.5, z: 0.4 }); tea.position.add({ x: -0, y: -0.13, z: 0 }); model.add(tea); return normalized(model); }, kitchen: async () => { const gltf = await gltfLoader.loadAsync( "basic_kitchen_cabinets_and_counter/scene.gltf" ); gltf.scene.rotateY(-Math.PI); return gltf.scene; }, "Cupboard.svg": async () => { const gltf = await gltfLoader.loadAsync("kitchen_cabinets (1)/scene.gltf"); gltf.scene.rotateY(Math.PI / 2); const model = await normalized(gltf.scene); model.traverse((child: any) => { if ( child.isMesh && ["pCube1_cor_0", "pCube8_cor_0"].includes(child.name) ) { child.material.color = new Color(0xffffff); } }); return model; }, "GasStove.svg": async () => { const gltf = await gltfLoader.loadAsync("burner_gas_stove/scene.gltf"); const model = await normalized(gltf.scene); model.traverse((child: any) => { if (child.isMesh) { child.material.emissive = new Color(0x222222) } }); return model; }, }; export const levelResources: Record< string, { bottom?: number | string; height?: number | string | "full"; top?: number | string; } > = { "SingleBed.svg": { height: 70, }, "ThreeSofa.svg": { height: 90, }, "SingleSofa.svg": { height: 90, }, "CombinationSofa.svg": { height: 90, }, "Desk.svg": { height: 80, }, "TeaTable.svg": { height: 50, }, "DiningTable.svg": { height: 100, }, "Chair.svg": { height: 80, }, "TV.svg": { height: 120, }, "Washstand.svg": { height: 100, }, "Closestool.svg": { height: 45, }, "Wardrobe.svg": { height: "full", }, "BedsideCupboard.svg": { height: 50, }, "piaochuang.svg": { top: 4, bottom: 40, }, "men_l.svg": { height: "full", }, "yimen.svg": { height: "full", }, "shuangkaimen.svg": { height: "full", }, "luodichuang.svg": { height: "full", }, "Cupboard.svg": { height: "full", }, "GasStove.svg": { height: 10, bottom: "0.335", }, }; export const getLevel = (type: string, fullHeight: number) => { const ndx = type.lastIndexOf("/"); if (~ndx) { type = type.substring(ndx + 1); } const transform = (data: any): Record => { const tdata: Record = {}; for (const key of Object.keys(data)) { if (data[key] === "full") { tdata[key] = fullHeight; } else if (typeof data[key] === "string") { tdata[key] = parseFloat(data[key]) * fullHeight; } else { tdata[key] = data[key]; } } return tdata; }; if (!levelResources[type]) { return {}; } const data = transform(levelResources[type]); if (!data.height && "top" in data && "bottom" in data) { data.height = fullHeight - data.top - data.bottom; } return data; }; export const getModel = (() => { const typeModels: Record> = {}; return (type: string) => { const ndx = type.lastIndexOf("/"); if (~ndx) { type = type.substring(ndx + 1); } if (type in typeModels) { return typeModels[type]; } if (type in resources) { typeModels[type] = resources[type](); typeModels[type].catch(() => { delete typeModels[type]; }); return typeModels[type]; } }; })(); export const fullMesh = new Mesh( new BoxGeometry(1, 1, 1), new MeshPhongMaterial({ color: 0xffffff }) ); fullMesh.receiveShadow = fullMesh.castShadow = true;