瀏覽代碼

feat: 三维关联架构搭建

bill 1 月之前
父節點
當前提交
2e7385fab8

+ 5 - 18
src/core/renderer-three/components/icon/index.vue

@@ -2,7 +2,7 @@
 
 <script lang="ts" setup>
 import { useRender, useStageProps, useTree } from "../../hook/use-stage";
-import { getModel, levelResources } from "../resource";
+import { getLevel, getModel } from "../resource";
 import { Box3, Group, MathUtils, Matrix4, Vector3 } from "three";
 import { computed, shallowRef, watch } from "vue";
 import { setMat } from "../../util";
@@ -14,14 +14,6 @@ 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) => {
@@ -42,15 +34,10 @@ watch(
 );
 
 const sProps = useStageProps();
+const level = computed(() => getLevel(props.data.url, sProps.value.height));
 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;
-    }
-  }
+  let height = level.value.height;
+  let bottom = level.value.bottom || 0;
 
   const data = props.data;
   const dec = new Transform(data.mat).decompose();
@@ -69,7 +56,7 @@ const mat = computed(() => {
   }
 
   const mat = new Matrix4()
-    .makeTranslation(dec.x, 0, dec.y)
+    .makeTranslation(dec.x, bottom, 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, height, props.data.height))

+ 3 - 1
src/core/renderer-three/components/index.ts

@@ -2,6 +2,7 @@ import { ShapeType } from "@/core/components";
 import Line from './line/index.vue'
 import LineIcon from './line-icon/index.vue'
 import Icon from './icon/index.vue'
+import Text from './text/index.vue'
 
 export const components: {
   [key in ShapeType]?: (data: {data: any}) => void
@@ -9,4 +10,5 @@ export const components: {
 
 components.line = Line as any
 components.lineIcon = LineIcon as any
-components.icon = Icon as any
+components.icon = Icon as any
+components.text = Text as any

+ 26 - 13
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, getLevel, getModel } from "../resource";
 import { Group, Matrix4 } from "three";
 import { computed, ref, watch, watchEffect } from "vue";
 import { lineCenter, lineVector, vector2IncludedAngle } from "@/utils/math";
@@ -38,16 +38,28 @@ const store = useStageProps().value.draw.store;
 const line = computed(
   () => store.getTypeItems("line")[0].lines.find((item) => item.id === props.data.lineId)!
 );
-const fullHeight = ["men_l", "yimen", "shuangkaimen", "luodichuang"];
 const fullThickness = ["men_l", "yimen", "shuangkaimen", "luodichuang"];
 
-const height = computed(() => {
-  const isFullHeight = fullHeight.some((t) => props.data.url.includes(t));
-  return isFullHeight ? sProps.value.height : sProps.value.height / 2;
-});
-const bottom = computed(
-  () => height.value / 2 + (sProps.value.height - height.value) / 2
+const level = computed(() => getLevel(props.data.url, sProps.value.height));
+const height = computed(() =>
+  level.value.height ? level.value.height : sProps.value.height / 2
 );
+const btHeight = computed(() => {
+  let bot = 0;
+  if ("bottom" in level.value) {
+    bot = level.value.bottom!;
+  } else if ("top" in level.value) {
+    bot = sProps.value.height - height.value - level.value.top!;
+  } else {
+    bot = (sProps.value.height - height.value) / 2;
+  }
+  return {
+    bottom: bot,
+    top: sProps.value.height - height.value - bot,
+  };
+});
+const bottom = computed(() => height.value / 2 + btHeight.value.bottom);
+
 const thickness = computed(() => {
   const isFullThickness = fullThickness.some((t) => props.data.url.includes(t));
   return isFullThickness ? line.value.strokeWidth : props.data.height;
@@ -81,14 +93,15 @@ watchEffect((onCleanup) => {
   if (height.value === sProps.value.height) {
     return;
   }
-  const scale = bottom.value / sProps.value.height;
   const topMesh = fullMesh.clone();
-  topMesh.scale.set(1, scale, 1);
-  topMesh.position.add({ x: 0, y: 0.75, z: 0 });
+  const topScale = btHeight.value.top / height.value;
+  topMesh.scale.set(1, topScale, 1);
+  topMesh.position.add({ x: 0, y: 0.5 + topScale / 2, z: 0 });
 
   const bottomMesh = fullMesh.clone();
-  bottomMesh.scale.set(1, scale, 1);
-  bottomMesh.position.add({ x: 0, y: -0.75, z: 0 });
+  const bottomScale = btHeight.value.bottom / height.value;
+  bottomMesh.scale.set(1, bottomScale, 1);
+  bottomMesh.position.add({ x: 0, y: -0.5 - bottomScale / 2, z: 0 });
 
   group.add(topMesh);
   group.add(bottomMesh);

+ 260 - 114
src/core/renderer-three/components/resource.ts

@@ -2,18 +2,18 @@ import {
   Box3,
   BoxGeometry,
   Color,
+  DirectionalLight,
   DoubleSide,
   Mesh,
   MeshPhongMaterial,
   MeshPhysicalMaterial,
+  MeshStandardMaterial,
   Object3D,
   Vector3,
 } from "three";
-import {  GLTFLoader, MTLLoader, OBJLoader } from "three/examples/jsm/Addons.js";
+import { GLTFLoader } 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();
@@ -48,13 +48,53 @@ const resources: Record<string, () => Promise<Object3D>> = {
     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);
+    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,
+        });
+      }
+    });
 
-    return await normalized(model);
+    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");
@@ -76,16 +116,15 @@ const resources: Record<string, () => Promise<Object3D>> = {
     gltf.scene.traverse((node: any) => {
       if (node.name?.includes("glass_0")) {
         node.material = new MeshPhysicalMaterial({
-          color: 0xeeeeee, // 浅灰色(可根据需求调整,如0xcccccc)
+          color: 0xffffff, // 浅灰色(可根据需求调整,如0xcccccc)
           metalness: 0.1, // 轻微金属感(增强反射)
-          roughness: 0.05, // 表面光滑度(0-1,越小越光滑)
-          transmission: 0.9, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
-          opacity: 0.8, // 透明度(与transmission配合使用)
+          roughness: 0.01, // 表面光滑度(0-1,越小越光滑)
+          transmission: 1, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
+          opacity: 1, // 透明度(与transmission配合使用)
           transparent: true, // 启用透明
           side: DoubleSide, // 双面渲染(玻璃通常需要)
-          ior: 1.5, // 折射率(玻璃约为1.5)
-          envMapIntensity: 1, // 环境贴图反射强度(需设置scene.environment)
-          clearcoat: 0.1, // 可选:表面清漆层(增强反光)
+          ior: 0, // 折射率(玻璃约为1.5)
+          clearcoat: 0.5, // 可选:表面清漆层(增强反光)
         });
       }
     });
@@ -94,15 +133,14 @@ const resources: Record<string, () => Promise<Object3D>> = {
   "DoubleBed.svg": async () => {
     const gltf = await gltfLoader.loadAsync("bed/scene.gltf");
 
-    const models: Object3D[] = []
-    const delModelName = ['Pillow_2002', 'Plane002']
+    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)
+      if (delModelName.some((n) => n === child.name)) {
+        models.push(child);
       }
     });
-    models.forEach(m => m.parent?.remove(m))
+    models.forEach((m) => m.parent?.remove(m));
 
     const model = await normalized(gltf.scene);
     model.position.setY(model.position.y - 0.131);
@@ -114,11 +152,11 @@ const resources: Record<string, () => Promise<Object3D>> = {
     model.rotateY(Math.PI / 2);
     return model;
   },
-  "sf": async () => {
+  sf: async () => {
     const gltf = await gltfLoader.loadAsync(
       "sofa_set_-_4_type_of_sofa_lowpoly./scene.gltf"
     );
-    return gltf.scene
+    return gltf.scene;
   },
   "ThreeSofa.svg": async () => {
     const gltf = await gltfLoader.loadAsync(
@@ -135,101 +173,92 @@ const resources: Record<string, () => Promise<Object3D>> = {
     return model;
   },
   "SingleSofa.svg": async () => {
-    const scene = (await getModel('sf'))!
-    const models: Object3D[] = []
-    const pickModelName = ['Cube026']
+    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)
+      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)
+    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']
+    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)
+      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)
+    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 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 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 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 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 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 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})
+    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 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"
-    );
+  "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 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 gltf = await gltfLoader.loadAsync("toilet/scene.gltf");
     const model = await normalized(gltf.scene, undefined);
     model.traverse((child: any) => {
       if (child.isMesh) {
@@ -239,9 +268,7 @@ const resources: Record<string, () => Promise<Object3D>> = {
     return model;
   },
   "Wardrobe.svg": async () => {
-    const gltf = await gltfLoader.loadAsync(
-      "wardrobe_14722-22/scene.gltf"
-    );
+    const gltf = await gltfLoader.loadAsync("wardrobe_14722-22/scene.gltf");
     const model = await normalized(gltf.scene, undefined);
     model.traverse((child: any) => {
       if (child.isMesh) {
@@ -249,7 +276,6 @@ const resources: Record<string, () => Promise<Object3D>> = {
       }
     });
     return model;
-
   },
   "BedsideCupboard.svg": async () => {
     const gltf = await gltfLoader.loadAsync(
@@ -262,44 +288,164 @@ const resources: Record<string, () => Promise<Object3D>> = {
       }
     });
     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, height?: number | 'full' }> = {
-  'SingleBed.svg': {
+export const levelResources: Record<
+  string,
+  {
+    bottom?: number | string;
+    height?: number | string | "full";
+    top?: number | string;
+  }
+> = {
+  "SingleBed.svg": {
     height: 70,
   },
-  'ThreeSofa.svg': {
-    height: 90
+  "ThreeSofa.svg": {
+    height: 90,
   },
-  'SingleSofa.svg': {
-    height: 90
+  "SingleSofa.svg": {
+    height: 90,
   },
-  'Desk.svg': {
-    height: 80
+  "CombinationSofa.svg": {
+    height: 90,
   },
-  'DiningTable.svg': {
-    height: 100
+  "Desk.svg": {
+    height: 80,
   },
-  'Chair.svg': {
-    height: 80
+  "TeaTable.svg": {
+    height: 50,
   },
-  'TV.svg': {
-    height: 120
+  "DiningTable.svg": {
+    height: 100,
   },
-  'Washstand.svg': {
-    height: 100
+  "Chair.svg": {
+    height: 80,
   },
-  'Closestool.svg': {
-    height: 45
+  "TV.svg": {
+    height: 120,
   },
-  'Wardrobe.svg': {
-    height: 'full'
+  "Washstand.svg": {
+    height: 100,
   },
-  'BedsideCupboard.svg': {
-    height: 50
+  "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<string, number> => {
+    const tdata: Record<string, number> = {};
+    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<string, Promise<Object3D | undefined>> = {};

+ 192 - 0
src/core/renderer-three/components/text/index.vue

@@ -0,0 +1,192 @@
+<template></template>
+
+<script lang="ts" setup>
+import {
+  BufferGeometry,
+  CanvasTexture,
+  DoubleSide,
+  Line,
+  LineBasicMaterial,
+  Mesh,
+  MeshBasicMaterial,
+  Object3D,
+  PlaneGeometry,
+  Vector3,
+} from "three";
+import {
+  useCamera,
+  useCursor,
+  useRender,
+  useStageProps,
+  useTree,
+} from "../../hook/use-stage";
+import { TextData } from "@/core/components/text";
+import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
+import { Transform } from "konva/lib/Util";
+import { Size } from "@/utils/math";
+import { useHoverEventRegister, useMouseEventRegister } from "../../hook/use-event";
+import { useFlyRoaming } from "../../hook/use-controls";
+
+const props = defineProps<{ data: TextData }>();
+
+const fontSize = computed(() => props.data.fontSize || 12);
+const size = ref<Size>({ width: 1, height: 1 });
+
+const getTextTexture = () => {
+  const pixelScale = 1;
+  let $canvas: HTMLCanvasElement;
+  $canvas = document.createElement("canvas");
+  const ctx = $canvas.getContext("2d")!;
+  const pixelFontSize = fontSize.value * pixelScale;
+  ctx.font = `${pixelFontSize}px ${props.data.fontFamily} ${props.data.fontStyle}`;
+
+  const contents = props.data.content.split("\n");
+  const textMetrics = contents.map((content, ndx) => {
+    const textMetric = ctx.measureText(content);
+    const width = textMetric.width;
+    let height = textMetric.fontBoundingBoxAscent;
+    if (ndx === contents.length - 1) {
+      height += textMetric.fontBoundingBoxDescent;
+    }
+    return {
+      width,
+      height,
+      fontBoundingBoxAscent: textMetric.fontBoundingBoxAscent,
+    };
+  });
+
+  const textSize = textMetrics.reduce(
+    (t, c) => ({
+      width: Math.max(t.width, c.width),
+      height: t.height + c.height,
+    }),
+    { width: 0, height: 0 }
+  );
+  const padding = 0.2 * pixelFontSize;
+  $canvas.width = textSize.width + 2 * padding;
+  $canvas.height = textSize.height + 2 * padding;
+
+  ctx.font = `${props.data.fontStyle} ${pixelFontSize}px "${props.data.fontFamily}"`;
+  ctx.fillStyle = props.data.fill || "#000000";
+  ctx.strokeStyle = props.data.stroke || "#ffffff";
+  ctx.lineWidth = padding;
+  ctx.textAlign = props.data.align as CanvasTextAlign;
+  let top = padding;
+  contents.forEach((content, ndx) => {
+    const met = textMetrics[ndx];
+    const b = top + met.fontBoundingBoxAscent;
+    if (ctx.textAlign === "center") {
+      ctx.strokeText(content, textSize.width / 2 + padding, b);
+    } else if (ctx.textAlign === "right") {
+      ctx.strokeText(content, textSize.width + padding, b);
+    } else if (ctx.textAlign === "left") {
+      ctx.strokeText(content, padding, b);
+    }
+    top += met.height;
+  });
+  top = padding;
+  contents.forEach((content, ndx) => {
+    const met = textMetrics[ndx];
+    const b = top + met.fontBoundingBoxAscent;
+    if (ctx.textAlign === "center") {
+      ctx.fillText(content, textSize.width / 2 + padding, b);
+    } else if (ctx.textAlign === "right") {
+      ctx.fillText(content, textSize.width + padding, b);
+    } else if (ctx.textAlign === "left") {
+      ctx.fillText(content, padding, b);
+    }
+    top += met.height;
+  });
+
+  size.value = {
+    width: textSize.width / pixelScale,
+    height: textSize.height / pixelScale,
+  };
+
+  const texture = new CanvasTexture($canvas);
+  texture.needsUpdate = true;
+  return texture;
+};
+
+const geo = new PlaneGeometry(1, 1);
+const material = new MeshBasicMaterial({
+  transparent: true,
+  side: DoubleSide,
+});
+const text = new Mesh(geo, material);
+
+const hoverRegister = useHoverEventRegister();
+const cursor = useCursor();
+let leave: () => void;
+hoverRegister(text, (hover) => {
+  if (hover) {
+    leave = cursor.push("pointer");
+  } else {
+    leave && leave();
+  }
+});
+
+const flyRoaming = useFlyRoaming();
+const mouseRegister = useMouseEventRegister();
+mouseRegister(text, "click", ({ point }) => {
+  flyRoaming(point.clone().setY(0));
+});
+
+const sProps = useStageProps();
+const dec = computed(() => new Transform(props.data.mat).decompose());
+const position = computed(() => {
+  const center = new Vector3(dec.value.x, sProps.value.height + 50, dec.value.y);
+
+  if (props.data.width && props.data.align !== "left") {
+    if (props.data.align === "center") {
+      center.x += (props.data.width - size.value.width) / 2;
+    } else {
+      center.x += props.data.width - size.value.width;
+    }
+  }
+  center.x += size.value.width / 2;
+  center.z += size.value.height / 2;
+
+  return center;
+});
+
+watchEffect(() => {
+  material.map = getTextTexture();
+});
+
+const lineGeo = new BufferGeometry();
+const line = new Line(lineGeo, new LineBasicMaterial({ color: 0xffffff }));
+
+const camera = useCamera();
+const updateTextMat = () => {
+  const distance = camera.position.distanceTo(position.value) / 400;
+  text.lookAt(camera.position);
+  const scaleX = (dec.value.scaleX * size.value.width * distance) / 1.5;
+  const scaleY = (size.value.height / size.value.width) * scaleX;
+
+  text.scale.set(scaleX, scaleY, 1);
+  text.position.copy(position.value);
+
+  const end = position.value.clone();
+  end.y -= scaleY / 2;
+  lineGeo.setFromPoints([end, new Vector3(end.x, 0, end.z)]);
+};
+
+camera.bus.on("change", updateTextMat);
+onUnmounted(() => camera.bus.off("change", updateTextMat));
+
+const render = useRender();
+watch(
+  [size, position],
+  () => {
+    updateTextMat();
+    render();
+  },
+  { immediate: true }
+);
+
+const group = new Object3D();
+group.add(text, line);
+
+useTree().value = group;
+</script>

+ 1 - 0
src/core/renderer-three/container.ts

@@ -0,0 +1 @@
+export const subgroupName = 'subgroup'

+ 3 - 6
src/core/renderer-three/env/ground.vue

@@ -1,13 +1,9 @@
 <template></template>
 
 <script lang="ts" setup>
-import {
-  DoubleSide,
-  Mesh,
-  MeshPhongMaterial,
-  PlaneGeometry,
-} from "three";
+import { DoubleSide, Mesh, MeshPhongMaterial, PlaneGeometry } from "three";
 import { useTree } from "../hook/use-stage";
+import { subgroupName } from "../container";
 
 const geometry = new PlaneGeometry(10000, 10000, 1, 1);
 const material = new MeshPhongMaterial({
@@ -17,5 +13,6 @@ const material = new MeshPhongMaterial({
 const ground = new Mesh(geometry, material);
 ground.rotateX(-Math.PI / 2);
 ground.receiveShadow = true;
+ground.name = subgroupName;
 useTree().value = ground;
 </script>

+ 22 - 20
src/core/renderer-three/env/light.vue

@@ -5,26 +5,28 @@ import { AmbientLight, DirectionalLight, Group, HemisphereLight, Vector3 } from
 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);
-const scale = 500;
-direLight.position.multiplyScalar(scale);
-direLight.lookAt(new Vector3(0, 1, 0));
-direLight.castShadow = true;
-group.add(
-  new AmbientLight(0x404040),
-  direLight,
-  new HemisphereLight(0xffffff, 0x080820, 2)
-);
-direLight.shadow.camera.left = -scale * 1.5;
-direLight.shadow.camera.right = scale * 1.5;
-direLight.shadow.camera.top = -scale * 1.5;
-direLight.shadow.camera.bottom = scale * 1.5;
-direLight.shadow.camera.far = scale * 2;
-direLight.shadow.mapSize.width = 2048 * 2;
-direLight.shadow.mapSize.height = 2048 * 2;
-direLight.shadow.camera.updateProjectionMatrix();
-// group.add(new CameraHelper(direLight.shadow.camera));
+group.add(new AmbientLight(0x404040), new HemisphereLight(0xffffff, 0xcccccc, 2));
+
+const addDire = (position: Vector3) => {
+  const direLight = new DirectionalLight(0xffffff, 1);
+  direLight.position.copy(position);
+  const scale = 500;
+  direLight.position.multiplyScalar(scale);
+  direLight.lookAt(new Vector3(0, 0, 0));
+
+  direLight.castShadow = true;
+  direLight.shadow.camera.left = -scale * 1.5;
+  direLight.shadow.camera.right = scale * 1.5;
+  direLight.shadow.camera.top = -scale * 1.5;
+  direLight.shadow.camera.bottom = scale * 1.5;
+  direLight.shadow.camera.far = scale * 2;
+  direLight.shadow.mapSize.width = 2048 * 2;
+  direLight.shadow.mapSize.height = 2048 * 2;
+  direLight.shadow.camera.updateProjectionMatrix();
+  // group.add(new CameraHelper(direLight.shadow.camera));
+  group.add(direLight);
+};
+addDire(new Vector3(0.1, 1, -0.5));
 
 useTree().value = group;
 </script>

+ 0 - 0
src/core/renderer-three/hook/use-animation.ts


+ 217 - 0
src/core/renderer-three/hook/use-controls.ts

@@ -0,0 +1,217 @@
+import { ref, watch } from "vue";
+import {
+  installThreeGlobalVar,
+  useCamera,
+  useContainer,
+  useRender,
+} from "./use-stage";
+import { OrbitControls } from "three/examples/jsm/Addons.js";
+import { listener } from "@/utils/event";
+import { PerspectiveCamera, Vector3 } from "three";
+import { mergeFuns } from "@/utils/shared";
+import { useMouseEventRegister } from "./use-event";
+import { subgroupName } from "../container";
+
+const getModelControls = (
+  container: HTMLDivElement,
+  camera: PerspectiveCamera,
+  render: () => void
+) => {
+  const controls = new OrbitControls(camera, container);
+  controls.target.set(0, 5, 0);
+  controls.update();
+
+  const unListener = listener(controls as any, "change", render);
+  let prevOrigin: Vector3 | null = null;
+  let prevDire: Vector3 | null = null;
+
+  return {
+    controls,
+    onDestory() {
+      controls.dispose();
+      unListener();
+    },
+    syncCamera() {
+      controls.update();
+    },
+    disable() {
+      prevOrigin = camera.position.clone();
+      prevDire = camera.getWorldDirection(new Vector3());
+      controls.enabled = false;
+    },
+    enable() {
+      controls.enabled = true;
+    },
+    recover() {
+      if (prevOrigin && prevDire) {
+        camera.position.copy(prevOrigin);
+        camera.lookAt(prevOrigin.add(prevDire));
+        controls.update();
+        render();
+      }
+    },
+  };
+};
+
+const getRoamingControls = (
+container: HTMLDivElement,
+  camera: PerspectiveCamera,
+  render: () => void
+) => {
+  const initDirection = camera.getWorldDirection(new Vector3());
+  const controls = new OrbitControls(camera, container);
+  controls.rotateSpeed = -0.3;
+  controls.enableZoom = false;
+
+  const syncCamera = (direction = camera.getWorldDirection(new Vector3())) => {
+    controls.target.copy(direction.add(camera.position));
+    controls.update();
+  };
+  syncCamera(initDirection);
+  render();
+  let prevOrigin: Vector3 | null = null;
+  let prevDire: Vector3 | null = null;
+
+  const unListener = listener(controls as any, "change", render);
+
+  return {
+    controls,
+    onDestory() {
+      controls.dispose();
+      unListener();
+    },
+    syncCamera,
+
+    disable() {
+      prevOrigin = camera.position.clone();
+      prevDire = camera.getWorldDirection(new Vector3());
+      controls.enabled = false;
+    },
+    enable() {
+      controls.enabled = true;
+      render();
+    },
+    recover() {
+      if (prevOrigin && prevDire) {
+        camera.position.copy(prevOrigin);
+        camera.lookAt(prevOrigin.add(prevDire));
+        prevOrigin = prevDire = null;
+
+        syncCamera();
+        render();
+      }
+    },
+  };
+};
+
+const controlsFactory = {
+  model: getModelControls,
+  roaming: getRoamingControls,
+};
+export type ControlsType = keyof typeof controlsFactory;
+export type Controls = ReturnType<(typeof controlsFactory)[ControlsType]>;
+
+export const useControls = installThreeGlobalVar(() => {
+  const container = useContainer();
+  const camera = useCamera();
+  const render = useRender();
+  const type = ref<ControlsType>();
+  const controls = ref<Controls>();
+  let controlsMap: Partial<Record<ControlsType, Controls>> = {};
+
+  const ctRender = () => {
+    camera.bus.emit("change");
+    render();
+  };
+  const stopWatch = watch(
+    [container, type],
+    ([container, type], _, onCleanup) => {
+      if (!(type && container)) return;
+
+      let ct: Controls;
+      if (type in controlsMap) {
+        ct = controlsMap[type]!;
+        ct.enable();
+      } else {
+        ct = controlsMap[type] = controlsFactory[type](
+          container,
+          camera,
+          ctRender
+        );
+      }
+
+      controls.value = ct;
+      onCleanup(() => {
+        ct.disable();
+        controls.value = undefined;
+      });
+    },
+    { immediate: true, flush: "sync" }
+  );
+
+  return {
+    var: { type, value: controls },
+    onDestroy: () => {
+      stopWatch();
+      for (const controls of Object.values(controlsMap)) {
+        controls.onDestory();
+      }
+      controlsMap = {};
+    },
+  };
+});
+
+export const useFlyRoaming = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const { type, value: controls } = useControls();
+
+  return (point: Vector3) => {
+    type.value = undefined;
+    const position = point.clone().add({ x: 0, y: 100, z: 0 });
+    const direction = camera.getWorldDirection(new Vector3());
+    const target = position.clone().add(direction);
+    camera.position.copy(position);
+    camera.lookAt(target);
+
+    type.value = "roaming";
+    controls.value!.syncCamera();
+  };
+})
+
+export const useFlyModel = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const { type, value: controls } = useControls();
+
+  return (set?: {point: Vector3, direction: Vector3}) => {
+    type.value = "model";
+    if (set) {
+      const position = set.point.clone().add({ x: 0, y: 100, z: 0 });
+      const direction = set.direction
+      const target = position.clone().add(direction);
+      camera.position.copy(position);
+      camera.lookAt(target);
+      controls.value?.syncCamera()
+    } else {
+      controls.value!.recover();
+    }
+  }
+})
+
+export const installAutoSwitchControls = installThreeGlobalVar(() => {
+  const { type } = useControls();
+  const mouseRegister = useMouseEventRegister();
+  const flyRoaming = useFlyRoaming()
+  const flyModel = useFlyModel()
+
+  type.value = "model";
+  const onDestroy = mergeFuns(
+    listener(document.documentElement, "keydown", (ev) => {
+      if (ev.key === "Escape" && type.value !== "model") {
+        flyModel()
+      }
+    }),
+    mouseRegister(subgroupName, "dblclick", ({ point }) => flyRoaming(point)),
+  );
+
+  return { onDestroy };
+});

+ 204 - 0
src/core/renderer-three/hook/use-event.ts

@@ -0,0 +1,204 @@
+import { Intersection, Object3D, Vector2 } from "three";
+import { installThreeGlobalVar, useContainer, useScene } from "./use-stage";
+import { listener } from "@/utils/event";
+import { markRaw, reactive, shallowRef, watch } from "vue";
+import { useGetIntersectObjectByPixel } from "./use-getter";
+import { diffArrayChange, mergeFuns } from "@/utils/shared";
+import { globalWatch } from "@/core/hook/use-global-vars";
+
+type ExtractMouseEventKeys<T, E> = {
+  [K in keyof T]: T[K] extends E ? K : never;
+}[keyof T];
+
+export const treeIncludes = (tree: Object3D | string, node: Object3D) => {
+  let current: Object3D | null = node;
+  while (current) {
+    if (current === tree || current.name === tree) {
+      return true;
+    }
+    current = current.parent;
+  }
+  return false;
+};
+
+const useRegisterFactory = <T extends UIEvent>(
+  hitInit: (container: HTMLDivElement) => {
+    destory: () => void;
+    mountRegister: (
+      name: ExtractMouseEventKeys<GlobalEventHandlersEventMap, T>,
+      cb: (ev: T) => void
+    ) => () => void;
+  }
+) => {
+  type Name = ExtractMouseEventKeys<GlobalEventHandlersEventMap, T>;
+  type CB<T extends Name> = (
+    object: Intersection,
+    event: GlobalEventHandlersEventMap[T],
+    objects: Intersection[]
+  ) => void;
+  type RegVal = [Object3D | string, CB<Name>, boolean];
+
+  const hitObjects = shallowRef<Intersection[]>();
+  const currentEvent = shallowRef<[Name, T]>();
+  const scene = useScene();
+  const registered = reactive({}) as {
+    [key in Name]?: RegVal[];
+  };
+  const container = useContainer();
+
+  const hasHit = (object: Intersection, regval: RegVal) => {
+    if (regval[0] === scene) {
+      return true;
+    } else {
+      return treeIncludes(regval[0], object.object);
+    }
+  };
+
+  const init = (container: HTMLDivElement) => {
+    const { destory, mountRegister } = hitInit(container);
+    const mounted: Partial<Record<Name, () => void>> = {};
+
+    const stopRegWatch = watch(
+      () => Object.keys(registered) as Name[],
+      (keys, oldKey = []) => {
+        const { added, deleted } = diffArrayChange(keys, oldKey);
+        added.forEach((key) => {
+          mounted[key] = mountRegister(
+            key,
+            (e) => (currentEvent.value = [key, e])
+          );
+        });
+        deleted.forEach((key) => {
+          if (key in mounted) {
+            mounted[key]!();
+            delete mounted[key];
+          }
+        });
+      },
+      { immediate: true }
+    );
+
+    const stopEmitWatch = watch(
+      [hitObjects, currentEvent, registered],
+      ([hitObjects, event]) => {
+        if (!hitObjects?.length || !event) return;
+        let regvals = registered[event[0]];
+        if (!regvals?.length) return;
+
+        regvals = [...regvals];
+        for (const hitObject of hitObjects) {
+          for (let i = 0; i < regvals!.length; i++) {
+            if (hasHit(hitObject, regvals[i])) {
+              regvals[i][1](hitObject, event[1] as any, hitObjects);
+              regvals.splice(i--, 1);
+            } else if (!regvals[i][2]) {
+              regvals.splice(i--, 1);
+            }
+          }
+        }
+        currentEvent.value = undefined;
+      }
+    );
+
+    return () => {
+      stopRegWatch();
+      stopEmitWatch();
+      for (const key in mounted) {
+        mounted[key as Name]!();
+      }
+      destory();
+    };
+  };
+
+  const register = <T extends Name>(
+    object: Object3D | string,
+    name: T,
+    cb: CB<T>,
+    penetrate = false
+  ) => {
+    if (!(name in registered)) {
+      registered[name] = [];
+    }
+    const regVal: RegVal = markRaw([object, cb as any, penetrate]);
+    registered[name]!.push(regVal);
+
+    return () => {
+      if (!registered[name]) return;
+      const ndx = registered[name].indexOf(regVal);
+      if (~ndx) {
+        registered[name].splice(ndx, 1);
+      }
+    };
+  };
+
+  return {
+    register,
+    registered,
+    hitObjects,
+    destory: globalWatch(
+      container,
+      (container, _, onCleanup) => {
+        container && onCleanup(init(container));
+      },
+      { immediate: true }
+    ),
+  };
+};
+
+export const useMouseEventRegister = installThreeGlobalVar(() => {
+  const getIntersectObjectByPixel = useGetIntersectObjectByPixel();
+  const rf = useRegisterFactory<MouseEvent>((container) => {
+    const unHitListener = listener(container, "mousemove", (ev) => {
+      if (!Object.keys(rf.registered).length) return;
+      rf.hitObjects.value = getIntersectObjectByPixel(
+        new Vector2(ev.offsetX, ev.offsetY)
+      );
+    });
+
+    return {
+      destory: mergeFuns(unHitListener),
+      mountRegister: (name, cb) => listener(container, name, cb),
+    };
+  });
+  return {
+    var: rf.register,
+    onDestroy: rf.destory,
+  };
+});
+
+export const useHoverEventRegister = installThreeGlobalVar(() => {
+  type Item =  [string | Object3D, (hover: boolean) => void]
+  const register = useMouseEventRegister();
+  const scene = useScene();
+  const checks = [] as Array<Item>
+  const prevs = [] as boolean[]
+
+  const stopListener = register(scene, "mousemove", ({ object }) => {
+    checks.forEach(([node, cb], ndx) => {
+      const hover = treeIncludes(node, object);
+      if (prevs[ndx] !== hover) {
+        cb(hover)
+        prevs[ndx] = hover
+      }
+    })
+  });
+
+  const hoverRegister = (tree: string | Object3D, cb: (hover: boolean) => void) => {
+    const item: Item = [tree, cb]
+    checks.push(item)
+    prevs.push(false)
+
+    return () => {
+      const ndx = checks.indexOf(item)
+      if (~ndx) {
+        checks.splice(ndx, 1)
+        prevs.splice(ndx, 1)
+      }
+    }
+  }
+
+  return {
+    var: hoverRegister,
+    onDestroy: stopListener
+  }
+});

+ 31 - 0
src/core/renderer-three/hook/use-getter.ts

@@ -0,0 +1,31 @@
+import { Raycaster, Vector2, Vector3 } from "three";
+import { installThreeGlobalVar, useCamera, useContainer, useScene } from "./use-stage";
+
+export const useRaycaster = installThreeGlobalVar(() => new Raycaster());
+
+export const useGetIntersectObject = () => {
+  const scene = useScene();
+  const raycaster = useRaycaster();
+
+  return (origin: Vector3, direction: Vector3) => {
+    raycaster.set(origin, direction);
+    return raycaster.intersectObject(scene);
+  };
+};
+
+export const useGetIntersectObjectByPixel = () => {
+  const scene = useScene();
+  const raycaster = useRaycaster();
+  const camera = useCamera();
+  const container = useContainer()
+
+  return (pixel: Vector2) => {
+    if (container.value) {
+      pixel = pixel.clone()
+      pixel.setX((pixel.x / container.value.offsetWidth) * 2 - 1)
+      pixel.setY(-(pixel.y / container.value.offsetHeight) * 2 + 1)
+    }
+    raycaster.setFromCamera(pixel, camera);
+    return raycaster.intersectObject(scene);
+  };
+};

+ 67 - 39
src/core/renderer-three/hook/use-stage.ts

@@ -1,5 +1,10 @@
 import { DrawExpose } from "@/core/hook/use-expose";
-import { globalWatch, installGlobalVar } from "@/core/hook/use-global-vars";
+import {
+  globalWatch,
+  globalWatchEffect,
+  installGlobalVar,
+  stackVar,
+} from "@/core/hook/use-global-vars";
 import { listener } from "@/utils/event";
 import { frameEebounce } from "@/utils/shared";
 import mitt, { Emitter } from "mitt";
@@ -9,10 +14,10 @@ import {
   PCFSoftShadowMap,
   PerspectiveCamera,
   Scene,
+  Vector2,
   Vector3,
   WebGLRenderer,
 } from "three";
-import { OrbitControls } from "three/addons";
 import {
   computed,
   getCurrentInstance,
@@ -78,24 +83,15 @@ export const useStageProps = installThreeGlobalVar(
   () => ref() as Ref<Required<StageProps>>
 );
 
-export type Loop = (time: number) => void;
+export type Loop = () => void;
 export const useAnimationLoop = installThreeGlobalVar(() => {
-  const renderer = useRenderer();
   const loops = ref<Loop[]>([]);
-  const cleanup = globalWatch(
-    () => loops.value.length > 0,
-    (canLoop) => {
-      if (canLoop) {
-        renderer.setAnimationLoop((time) => {
-          for (const loop of loops.value) {
-            loop(time);
-          }
-        });
-      } else {
-        renderer.setAnimationLoop(null);
-      }
+  const trigger = () => {
+    for (const loop of loops.value) {
+      loop();
     }
-  );
+  };
+
   const remove = (fn: Loop) => {
     const ndx = loops.value.indexOf(fn);
     if (~ndx) {
@@ -107,17 +103,28 @@ export const useAnimationLoop = installThreeGlobalVar(() => {
     return () => remove(fn);
   };
 
-  return {
-    var: { add, remove },
-    onDestroy: cleanup,
-  };
+  return { add, remove, trigger };
 });
 
 export const useCamera = installThreeGlobalVar(() => {
-  const camera = new PerspectiveCamera(75, 1, 0.1, 500000);
+  const renderer = useRenderer();
+  const camera = new PerspectiveCamera(
+    75,
+    1,
+    0.1,
+    500000
+  ) as PerspectiveCamera & { bus: Emitter<{ change: void }> };
   camera.position.set(0, 2, 0);
   camera.position.multiplyScalar(800);
   camera.lookAt(new Vector3(0, 1, 0));
+  camera.bus = mitt();
+
+  renderer.bus.on("sizeChange", () => {
+    const size = renderer.getSize(new Vector2());
+    camera.aspect = size.width / size.height;
+    camera.updateProjectionMatrix();
+    camera.bus.emit("change");
+  });
   return camera;
 });
 
@@ -128,24 +135,16 @@ export const useScene = installThreeGlobalVar(() => {
 });
 
 export const useRender = installThreeGlobalVar(() => {
+  const loop = useAnimationLoop();
   const renderer = useRenderer();
   const scene = useScene();
   const camera = useCamera();
-  const render = frameEebounce(() => renderer.render(scene, camera));
+  const render = frameEebounce(() => {
+    loop.trigger();
+    renderer.render(scene, camera);
+  });
 
   renderer.bus.on("sizeChange", render);
-  const container = useContainer();
-  watch(container, (container, _, onCleanup) => {
-    if (container) {
-      const controls = new OrbitControls(camera, container);
-      controls.target.set(0, 5, 0);
-      controls.update();
-      controls.addEventListener("change", render);
-      onCleanup(() => {
-        controls.removeEventListener("change", render);
-      });
-    }
-  });
   return render;
 });
 
@@ -171,12 +170,15 @@ export const useTree = () => {
         : scene) as Object3D
   );
 
-  watch([threeParent, expose], ([parent, current], _, onCleanup) => {
-    if (parent && current) {
-      parent.add(current);
+  watch([threeParent, expose], ([parent, expose], _, onCleanup) => {
+    if (current.props?.data?.id) {
+      expose.name = current.props.data.id;
+    }
+    if (parent && expose) {
+      parent.add(expose);
       render();
       onCleanup(() => {
-        parent.remove(current);
+        parent.remove(expose);
         render();
       });
     }
@@ -193,3 +195,29 @@ export const useDrawHook = <T extends () => any>(hook: T): ReturnType<T> => {
   const draw = useStageProps().value.draw;
   return draw.runHook(hook);
 };
+
+export const useCursor = installThreeGlobalVar(
+  () => stackVar("default"),
+  Symbol("cursor")
+);
+
+export const installCursorStyle = installThreeGlobalVar(() => {
+  const cursor = useCursor();
+  const container = useContainer()
+  const cursorStyle = computed(() => {
+    if (cursor.value.includes(".")) {
+      return `url(${cursor.value}) 12 12, auto`;
+    } else {
+      return cursor.value;
+    }
+  });
+
+  const stop = globalWatchEffect((onCleanup) => {
+    const dom = container.value
+    if (dom) {
+      dom.style.cursor = cursorStyle.value
+      onCleanup(() => dom.style.cursor = 'initial')
+    }
+  })
+  return { onDestroy: stop }
+});

+ 10 - 2
src/core/renderer-three/renderer.vue

@@ -17,11 +17,17 @@
 import Ground from "./env/ground.vue";
 import Light from "./env/light.vue";
 import { getCurrentInstance, onUnmounted } from "vue";
-import { DrawExpose } from "../hook/use-expose";
-import { instanceName, StageProps, useContainer, useStageProps } from "./hook/use-stage";
+import {
+  installCursorStyle,
+  instanceName,
+  StageProps,
+  useContainer,
+  useStageProps,
+} from "./hook/use-stage";
 import { rendererMap } from "@/constant";
 import { mergeFuns } from "@/utils/shared";
 import { components } from "./components";
+import { installAutoSwitchControls } from "./hook/use-controls";
 
 const instance = getCurrentInstance();
 defineOptions({ name: instanceName });
@@ -32,6 +38,8 @@ onUnmounted(() => {
 
 const props = defineProps<StageProps>();
 useStageProps().value = { ...props, height: props.height || 200 };
+installAutoSwitchControls();
+installCursorStyle();
 const container = useContainer();
 </script>
 

+ 1 - 3
src/example/components/slide/slide-icons.vue

@@ -50,13 +50,12 @@ const drawIcon = async (item: IconItem) => {
   const type = item.wall ? "lineIcon" : "icon";
   const parset: any = await getIconStyle(url);
   parset.isIcon = true;
-
   props.draw.enterDrawShape(
     type,
     {
       ...defaultStyle,
-      ...(item.parse || {}),
       ...parset,
+      ...(item.parse || {}),
       url,
       name,
     },
@@ -68,7 +67,6 @@ const drawIcon = async (item: IconItem) => {
 const activeName = computed(() => (props.draw.presetAdd?.preset as any)?.name);
 
 const activeGroups = ref(props.groups.map((item) => item.name));
-console.log(activeGroups.value);
 const keyword = ref("");
 const searchGroups = computed(() => {
   return props.groups

+ 1 - 1
src/example/constant.ts

@@ -52,7 +52,7 @@ export const iconGroups: IconGroup[] = [
             wall: true,
             icon: "piaochuang",
             name: "飘窗",
-            parse: { type: "align-bottom-fix", height: 100 },
+            parse: { type: "align-bottom-fix", height: 70 },
           },
           {
             wall: true,

+ 1 - 1
src/example/fuse/enter.ts

@@ -268,7 +268,7 @@ const saveTabulationData = genLoading(
     return item.id;
   }
 );
-
+// caseId=6&overviewId=195
 const uploadResourse = genLoading(async (file: File) => {
   const url = await postFile(`fusion/upload/file`, { file });
   if (url.includes("//")) {