import { animation } from "@/core/hook/use-animation"; import { Box3, BoxGeometry, Color, DoubleSide, Mesh, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, Object3D, Quaternion, 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("bay_window/scene.gltf"); gltf.scene.scale.setX(-1); const names = ["01_glass_0", "02_glass_0", "03_glass_0", "04_glass_0"] gltf.scene.traverse((node: any) => { if (names.includes(node.name)) { 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, // 可选:表面清漆层(增强反光) }); } }) let copyModel: Object3D let parent: Object3D gltf.scene.traverse((node: any) => { if (node.name === "01") { copyModel = node.clone() parent = node.parent } }) copyModel!.scale.add({x: 0, y: 0.06, z: -0.1}) const left = copyModel!.clone() left.name = "05" left.rotation.y = -Math.PI / 2 left.position.set(-170, 0, 50) parent!.add(left) const right = copyModel! right.name = "06" right.rotation.y = Math.PI / 2 right.position.set(170, 0, 50) parent!.add(right) const model = await normalized(gltf.scene); // model.scale.add(({x: 0.015, y: 0, z: 0})) // model.position.add({x: 0, y: 0, z: 0}) left.scale.add({x: -0.3, y: 0, z: 0}) left.position.add({x: -7, y: 0, z: -16}) right.scale.add({x: -0.3, y: 0, z: 0}) right.position.add({x: 7, y: 0, z: -16}) return model }, "piaochuang1.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); 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.position.add({x: 0, y: -0.1, 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" ); gltf.scene.rotateY(Math.PI); 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 type ModelSwitch = (open: boolean) => ReturnType; export type SwitchResult = ReturnType; export const switchResources: Record< string, (model: Object3D, render: () => void) => ModelSwitch > = { "men_l.svg": (model, render) => { let node: Object3D; model.traverse((child) => { if (child.name === "Plane001") { node = child; } }); return (open: boolean) => animation( { z: node!.rotation.z }, { z: open ? -Math.PI / 2 : 0 }, (data) => { node.rotation.z = data.z; render(); } ); }, "luodichuang.svg": (model, render) => { let nodes: Object3D[] = []; const initVals = [121.44782257080078, -121.4478]; const finalVals = [58, -58]; model.traverse((child) => { if (child.name === "01") { nodes[0] = child; } else if (child.name === "04") { nodes[1] = child; } }); return (open: boolean) => animation( nodes.map((node) => node.position.x), open ? finalVals : initVals, (data) => { nodes.forEach((node, i) => (node.position.x = data[i])); render(); } ); }, "chuang.svg": (model, render) => { let nodes: Object3D[] = []; model.traverse((child) => { if (child.name === "L") { nodes[0] = child; } else if (child.name === "R") { nodes[1] = child; } }); const initVals = nodes.map((item) => ({ position: item.position.clone(), quat: item.quaternion.clone(), })); const changes = [ { origin: new Vector3(108, 0, 0), angle: -Math.PI / 3 }, { origin: new Vector3(-108, 0, 0), angle: Math.PI / 3 }, ]; const finalVals = initVals.map((item, i) => { const { origin, angle } = changes[i]; const qua = new Quaternion().setFromAxisAngle( { x: 0, y: 1, z: 0 }, angle ); const finalPosition = item.position .clone() .sub(origin) .applyQuaternion(qua) .add(origin); const finalQua = item.quat.clone().multiply(qua); return { position: finalPosition, quat: finalQua, }; }); return (open: boolean) => animation( nodes.map((node) => ({ position: node.position, quat: node.quaternion, })), open ? finalVals : initVals, (data) => { nodes.forEach((node, i) => { node.position.copy(data[i].position); node.quaternion.copy(data[i].quat); }); render(); } ); }, "yimen.svg": (model, render) => { let node: Object3D; model.traverse((child) => { if (child.name === "16668_84x96_Slider_Door-Black_V1001_0") { node = child; } }); const initVal = node!.position.x; const finalVal = initVal - 100; return (open: boolean) => animation( { x: node.position.x }, { x: open ? finalVal : initVal }, (data) => { node.position.setX(data.x); render(); } ); }, "Wardrobe.svg": (model, render) => { let nodes: Object3D[] = []; const initVals = [0, 0]; const finalVals = [Math.PI / 2, Math.PI / 2]; const names = [ 'RootNode', 'VIFS079_NCS_S_1502-Y50R_semigloss_0', 'VIFS077_NCS_S_1502-Y50R_semigloss_0', ] model.traverse((child) => { if (names.includes(child.name)) { nodes.push(child) } }); return (open: boolean) => animation( nodes.map((node) => node.rotation.y), open ? finalVals : initVals, (data) => { nodes.forEach((node, i) => (node.rotateY(data[i]))); console.log(nodes, data) render(); } ); }, "piaochuang.svg": (model, render) => { let nodes: Object3D[] = []; const names = ["01", "02", "03", "04"] model.traverse((child) => { if (names.includes(child.name)) { nodes.push(child) } }); const initVals = nodes.map((item) => ({ position: item.position.clone(), quat: item.quaternion.clone(), })); const changes = [ { origin: new Vector3(80, 0, 0), angle: Math.PI / 3 }, { origin: new Vector3(80, 0, 0), angle: -Math.PI / 3 }, { origin: new Vector3(-80, 0, 0), angle: Math.PI / 3 }, { origin: new Vector3(-80, 0, 0), angle: -Math.PI / 3 }, ]; const finalVals = initVals.map((item, i) => { const { origin, angle } = changes[i]; const qua = new Quaternion().setFromAxisAngle( { x: 0, y: 1, z: 0 }, angle ); const finalPosition = item.position .clone() .sub(origin) .applyQuaternion(qua) .add(origin); const finalQua = item.quat.clone().multiply(qua); return { position: finalPosition, quat: finalQua, // quat: item.quat }; }); return (open: boolean) => animation( nodes.map((node) => ({ position: node.position, quat: node.quaternion, })), open ? finalVals : initVals, (data) => { nodes.forEach((node, i) => { node.position.copy(data[i].position); node.quaternion.copy(data[i].quat); }); render(); } ); }, }; export const getModelSwitch = (type: string) => { const ndx = type.lastIndexOf("/"); if (~ndx) { type = type.substring(ndx + 1); } return switchResources[type]; }; 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;