Kaynağa Gözat

feat: 模型库更新

bill 1 ay önce
ebeveyn
işleme
e6f45a9480

+ 3 - 2
src/core/components/line-icon/index.ts

@@ -28,7 +28,8 @@ export type LineIconData = Omit<IconData, "mat" | "width"> & {
   endLen: number;
   lineId: string;
   openSide: "LEFT" | "RIGHT";
-  type: "full" | "align-bottom";
+  type: "full" | "align-bottom" | 'align-bottom-fix';
+  
   __snapLine?: Pos[],
 };
 
@@ -86,7 +87,7 @@ export const getLineIconMat = (
   const center = lineCenter(line);
 
   const mat = new Transform();
-  if (data.type === "align-bottom") {
+  if (data.type === "align-bottom" || data.type === 'align-bottom-fix') {
     const offset = movev.clone().multiplyScalar(data.height / 2);
     mat.translate(offset.x, offset.y);
   }

+ 40 - 6
src/core/renderer-three/components/icon/index.vue

@@ -2,9 +2,9 @@
 
 <script lang="ts" setup>
 import { useRender, useStageProps, useTree } from "../../hook/use-stage";
-import { fullMesh, getModel } from "../line-icon/resource";
-import { Group, MathUtils, Matrix4 } from "three";
-import { computed, ref, watch, watchEffect } from "vue";
+import { getModel, levelResources } from "../resource";
+import { Box3, Group, MathUtils, Matrix4, Vector3 } from "three";
+import { computed, shallowRef, watch } from "vue";
 import { setMat } from "../../util";
 import { IconData } from "@/core/components/icon";
 import { Transform } from "konva/lib/Util";
@@ -13,15 +13,26 @@ const props = defineProps<{ data: IconData }>();
 const render = useRender();
 
 const group = new Group();
+const size = shallowRef<Vector3>();
+const type = computed(() => {
+  let type = props.data.url;
+  const ndx = type.lastIndexOf("/");
+  if (~ndx) {
+    type = type.substring(ndx + 1);
+  }
+  return type;
+});
 watch(
   () => props.data.url,
   async (type, _, onCleanup) => {
     let typeModel = await getModel(type);
     if (typeModel && type === props.data.url) {
       typeModel = typeModel.clone();
+      size.value = new Box3().setFromObject(typeModel).getSize(new Vector3());
       group.add(typeModel);
       render();
       onCleanup(() => {
+        size.value = undefined;
         group.remove(typeModel!);
         render();
       });
@@ -30,15 +41,38 @@ watch(
   { immediate: true }
 );
 
-const getScaleZ = () => 1;
+const sProps = useStageProps();
 const mat = computed(() => {
+  let height: number | undefined = undefined;
+  if (type.value in levelResources) {
+    if (levelResources[type.value].height === "full") {
+      height = sProps.value.height;
+    } else {
+      height = levelResources[type.value].height as number;
+    }
+  }
+
   const data = props.data;
   const dec = new Transform(data.mat).decompose();
+  if (!height) {
+    height = 30;
+    if (size.value) {
+      const widthScale = (data.width * dec.scaleX) / size.value.x;
+      const thicknessScale = (data.height * dec.scaleY) / size.value.z;
+      if (widthScale > thicknessScale) {
+        height = widthScale * size.value.y;
+      } else {
+        height = thicknessScale * size.value.y;
+      }
+      height = Math.min(height, 90);
+    }
+  }
+
   const mat = new Matrix4()
     .makeTranslation(dec.x, 0, dec.y)
+    .multiply(new Matrix4().makeRotationY(-MathUtils.degToRad(dec.rotation)))
     .multiply(new Matrix4().makeScale(dec.scaleX, 1, dec.scaleY))
-    .multiply(new Matrix4().makeScale(props.data.width, 100, props.data.height))
-    .multiply(new Matrix4().makeRotationY(MathUtils.degToRad(dec.rotation)))
+    .multiply(new Matrix4().makeScale(props.data.width, height, props.data.height))
     .multiply(new Matrix4().makeTranslation(0, 0.5, 0));
   return mat;
 });

+ 1 - 1
src/core/renderer-three/components/line-icon/index.vue

@@ -7,7 +7,7 @@ import {
   LineIconData,
 } from "@/core/components/line-icon";
 import { useRender, useStageProps, useTree } from "../../hook/use-stage";
-import { fullMesh, getModel } from "./resource";
+import { fullMesh, getModel } from "../resource";
 import { Group, Matrix4 } from "three";
 import { computed, ref, watch, watchEffect } from "vue";
 import { lineCenter, lineVector, vector2IncludedAngle } from "@/utils/math";

+ 0 - 129
src/core/renderer-three/components/line-icon/resource.ts

@@ -1,129 +0,0 @@
-import {
-  Box3,
-  BoxGeometry,
-  DoubleSide,
-  Mesh,
-  MeshPhongMaterial,
-  MeshPhysicalMaterial,
-  Object3D,
-  Vector3,
-} from "three";
-import { GLTFLoader, MTLLoader, OBJLoader } from "three/examples/jsm/Addons.js";
-
-const gltfLoader = new GLTFLoader().setPath("/static/models/");
-const objLoader = new OBJLoader().setPath("/static/models/");
-const mtlLoader = new MTLLoader().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<string, () => Promise<Object3D>> = {
-  "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 materials = await mtlLoader.loadAsync("bay_window/3d-model.mtl");
-
-    objLoader.setMaterials(materials);
-    const model = await objLoader.loadAsync("bay_window/3d-model.obj");
-    model.rotateY(Math.PI);
-
-    return await normalized(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");
-    console.log(gltf.scene);
-    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) => {
-      console.log(node.name);
-      if (node.name?.includes("glass_0")) {
-        node.material = new MeshPhysicalMaterial({
-          color: 0xeeeeee, // 浅灰色(可根据需求调整,如0xcccccc)
-          metalness: 0.1, // 轻微金属感(增强反射)
-          roughness: 0.05, // 表面光滑度(0-1,越小越光滑)
-          transmission: 0.9, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
-          opacity: 0.8, // 透明度(与transmission配合使用)
-          transparent: true, // 启用透明
-          side: DoubleSide, // 双面渲染(玻璃通常需要)
-          ior: 1.5, // 折射率(玻璃约为1.5)
-          envMapIntensity: 1, // 环境贴图反射强度(需设置scene.environment)
-          clearcoat: 0.1, // 可选:表面清漆层(增强反光)
-        });
-      }
-    });
-    return await normalized(gltf.scene);
-  },
-  "DoubleBed.svg": async () => {
-    const gltf = await gltfLoader.loadAsync(
-      "bed/scene.gltf"
-    );
-    return await normalized(gltf.scene);
-
-  }
-};
-
-export const getModel = (() => {
-  const typeModels: Record<string, Promise<Object3D | undefined>> = {};
-
-  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;

+ 330 - 0
src/core/renderer-three/components/resource.ts

@@ -0,0 +1,330 @@
+import {
+  Box3,
+  BoxGeometry,
+  Color,
+  DoubleSide,
+  Mesh,
+  MeshPhongMaterial,
+  MeshPhysicalMaterial,
+  Object3D,
+  Vector3,
+} from "three";
+import {  GLTFLoader, MTLLoader, OBJLoader } from "three/examples/jsm/Addons.js";
+
+const gltfLoader = new GLTFLoader().setPath("/static/models/");
+const objLoader = new OBJLoader().setPath("/static/models/");
+const mtlLoader = new MTLLoader().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<string, () => Promise<Object3D>> = {
+  "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 materials = await mtlLoader.loadAsync("bay_window/3d-model.mtl");
+
+    objLoader.setMaterials(materials);
+    const model = await objLoader.loadAsync("bay_window/3d-model.obj");
+    model.rotateY(Math.PI);
+
+    return await normalized(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: 0xeeeeee, // 浅灰色(可根据需求调整,如0xcccccc)
+          metalness: 0.1, // 轻微金属感(增强反射)
+          roughness: 0.05, // 表面光滑度(0-1,越小越光滑)
+          transmission: 0.9, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
+          opacity: 0.8, // 透明度(与transmission配合使用)
+          transparent: true, // 启用透明
+          side: DoubleSide, // 双面渲染(玻璃通常需要)
+          ior: 1.5, // 折射率(玻璃约为1.5)
+          envMapIntensity: 1, // 环境贴图反射强度(需设置scene.environment)
+          clearcoat: 0.1, // 可选:表面清漆层(增强反光)
+        });
+      }
+    });
+    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) => {
+      console.log(child.name)
+      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);
+  },
+  "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;
+  }
+};
+
+export const levelResources: Record<string, {bottom?: number, height?: number | 'full' }> = {
+  'SingleBed.svg': {
+    height: 70,
+  },
+  'ThreeSofa.svg': {
+    height: 90
+  },
+  'SingleSofa.svg': {
+    height: 90
+  },
+  'Desk.svg': {
+    height: 80
+  },
+  '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
+  }
+}
+
+export const getModel = (() => {
+  const typeModels: Record<string, Promise<Object3D | undefined>> = {};
+
+  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;

+ 2 - 8
src/core/renderer-three/env/light.vue

@@ -1,18 +1,12 @@
 <template></template>
 
 <script lang="ts" setup>
-import {
-  AmbientLight,
-  DirectionalLight,
-  Group,
-  HemisphereLight,
-  Vector3,
-} from "three";
+import { AmbientLight, DirectionalLight, Group, HemisphereLight, Vector3 } from "three";
 import { useTree } from "../hook/use-stage";
 
 const group = new Group();
 const direLight = new DirectionalLight(0xffffff, 0.8);
-direLight.position.set(0.1, 1, 0.5);
+direLight.position.set(0.1, 1, -0.5);
 const scale = 500;
 direLight.position.multiplyScalar(scale);
 direLight.lookAt(new Vector3(0, 1, 0));

+ 2 - 2
src/example/constant.ts

@@ -5,7 +5,7 @@ export type IconItem = {
   icon: string;
   name: string;
   color?: string;
-  parse?: { fill?: string; stroke?: string; type?: LineIconData['type'] };
+  parse?: { fill?: string; stroke?: string; type?: LineIconData['type'], height?: number };
 };
 export type IconGroup = {
   name: string;
@@ -52,7 +52,7 @@ export const iconGroups: IconGroup[] = [
             wall: true,
             icon: "piaochuang",
             name: "飘窗",
-            parse: { type: "align-bottom" },
+            parse: { type: "align-bottom-fix", height: 100 },
           },
           {
             wall: true,

+ 2 - 5
src/example/platform/platform-draw.ts

@@ -182,18 +182,15 @@ const getWallTaggingShapes = async (
       break;
     }
 
+    console.log(item)
     const shape: LineIconData = {
       ...getBaseItem(),
       name: item.name,
       createTime: now + ndx,
-      url: item.url,
       zIndex: 1,
-      startLen: item.startLen,
-      endLen: item.endLen,
-      type: 'align-bottom',
-      openSide: item.openSide,
       height: line.strokeWidth,
       lineId: line.id,
+      ...item,
     };
     const iconPoints = getLineIconEndpoints(points, shape)
     reqs.push(

+ 1 - 0
src/example/platform/platform-resource.ts

@@ -46,6 +46,7 @@ export type WallTaggingInfo = {
   url: string,
   pointIds: string[]
   name?: string
+  height?: number,
 } & Pick<LineIconData, 'startLen' | 'endLen' | 'type' | 'openSide'>
 
 export type SceneResource = {

+ 1 - 0
src/example/platform/resource-swkk.ts

@@ -406,6 +406,7 @@ export const getWallAITaggingInfos = async (
         startLen: lineLen(points[0], line[0]),
         endLen: lineLen(points[0], line[1]),
         openSide: item.openSide,
+        height: itemIcon?.parse?.height,
         type: itemIcon?.parse?.type || "full",
         pointIds: points.map((item) => item.key),
       });