Explorar el Código

feat: 提供默认导入场景功能

bill hace 1 mes
padre
commit
d1c84d44fd

+ 3 - 1
.gitignore

@@ -21,4 +21,6 @@ dist-ssr
 *.ntvs*
 *.njsproj
 *.sln
-*.sw?
+
+# 本地模型
+public/static/models/*

+ 23 - 7
src/core/components/line/attach-server.ts

@@ -161,6 +161,21 @@ export const getJoinLine = (
     });
 };
 
+export const foreNormalLineData = (data:LineData) => {
+  for (let i = 0; i < data.lines.length; i++) {
+    const {a, b} = data.lines[i]
+    if (!data.points.some(p => p.id === a) || !data.points.some(p => p.id === b)) {
+      data.lines.splice(i--, 1)
+    }
+  }
+  for (let i = 0; i < data.points.length; i++) {
+    const id = data.points[i].id
+    if (!data.lines.some(l => l.a === id || l.b === id)) {
+      data.points.splice(i, 1)
+    }
+  }
+}
+
 export const normalLineData = (data: LineData, ctx: NLineDataCtx) => {
   const changePoints = [
     ...Object.values(ctx.add.points),
@@ -203,6 +218,7 @@ export const normalLineData = (data: LineData, ctx: NLineDataCtx) => {
       ~ndx && data.points.splice(ndx, 1);
     }
   }
+  // foreNormalLineData(data)
   return deduplicateLines(data);
 };
 
@@ -609,7 +625,7 @@ export const useLineDescribes = (line: Ref<LineDataLine>) => {
 
 export const useDrawLinePoint = (
   data: Ref<LineData>,
-  line: LineDataLine,
+  line: Ref<LineDataLine>,
   callback: (data: {
     prev: LineDataLine;
     next: LineDataLine;
@@ -644,13 +660,13 @@ export const useDrawLinePoint = (
   const stage = useStage();
   const store = useStore();
   const icons = computed(() =>
-    store.getTypeItems("lineIcon").filter((item) => item.lineId === line.id)
+    store.getTypeItems("lineIcon").filter((item) => item.lineId === line.value.id)
   );
   const drawStore = useDrawIngData();
   const cursor = useCursor();
   const enterDraw = () => {
-    const points = getLinePoints(data.value, line)
-    console.log(points, data.value, line)
+    const points = getLinePoints(data.value, line.value)
+    console.log(points, data.value, line.value)
     const cdata: LineData = { ...data.value, points, lines: [] };
     const point = reactive({ ...lineCenter(points), id: onlyId() });
     const cIcons = icons.value.map((icon) => ({ ...icon, id: onlyId() }));
@@ -665,8 +681,8 @@ export const useDrawLinePoint = (
       };
     });
 
-    const prev = { ...line, id: onlyId(), b: point.id };
-    const next = { ...line, id: onlyId(), a: point.id };
+    const prev = { ...line.value, id: onlyId(), b: point.id };
+    const next = { ...line.value, id: onlyId(), a: point.id };
     cdata.lines.push(prev, next);
     cdata.points.push(point);
 
@@ -699,7 +715,7 @@ export const useDrawLinePoint = (
     snapInfos.update([]);
     return mergeFuns(
       cursor.push("./icons/m_add.png"),
-      runHook<() => void>(() =>
+      runHook(() =>
         clickListener(stage.value!.getNode().container(), () => {
           callback({
             prev,

+ 12 - 1
src/core/components/line/line.vue

@@ -3,10 +3,21 @@
 </template>
 
 <script lang="ts" setup>
+import { watch } from "vue";
+import { foreNormalLineData } from "./attach-server.ts";
 import { LineData } from "./index.ts";
 import TempLine from "./temp-line.vue";
 
-defineProps<{ data: LineData }>();
+const props = defineProps<{ data: LineData }>();
+
+watch(
+  () => props.data,
+  (data) => {
+    foreNormalLineData(data);
+  },
+  { immediate: true }
+);
+
 const emit = defineEmits<{
   (e: "updateShape", value: LineData): void;
   (e: "addShape", value: LineData): void;

+ 2 - 2
src/core/components/line/single-line.vue

@@ -83,7 +83,7 @@
 <script lang="ts" setup>
 import EditLine from "../share/edit-line.vue";
 import singlePoint from "./single-point.vue";
-import { computed, ref } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import { getMouseStyle, LineData, LineDataLine, shapeName, renderer } from "./index.ts";
 import { onlyId } from "@/utils/shared.ts";
 import { Pos } from "@/utils/math.ts";
@@ -146,7 +146,7 @@ const store = useStore();
 const history = useHistory();
 const { drawProps, enter: enterDrawLinePoint } = useDrawLinePoint(
   computed(() => props.data),
-  props.line,
+  computed(() => props.line),
   (data) => {
     emit("updateBefore", [props.line.a, props.line.b]);
     emit("addPoint", data.point);

+ 6 - 5
src/core/hook/use-global-vars.ts

@@ -29,9 +29,9 @@ import { ShapeType } from "@/index.ts";
 import { isEditableElement } from "@/utils/dom.ts";
 
 let getInstance = getCurrentInstance;
-export const useRendererInstance = () => {
+export const useRendererInstance = (name = rendererName) => {
   let instance = getInstance()!;
-  while (instance.type.name !== rendererName) {
+  while (instance.type.name !== name) {
     if (instance.parent) {
       instance = instance.parent;
     } else {
@@ -43,10 +43,11 @@ export const useRendererInstance = () => {
 
 export const installGlobalVar = <T>(
   create: () => { var: T; onDestroy: () => void } | T,
-  key = Symbol("globalVar")
+  key = Symbol("globalVar"),
+  instanceName = rendererName
 ) => {
   const useGlobalVar = (): T => {
-    const instance = useRendererInstance() as any;
+    const instance = useRendererInstance(instanceName) as any;
     const { unmounteds } = rendererMap.get(instance)!;
     if (!(key in instance)) {
       let val = create() as any;
@@ -69,7 +70,7 @@ export const installGlobalVar = <T>(
 
 export const useRunHook = installGlobalVar(() => {
   const instance = getCurrentInstance();
-  return <R, T extends () => R = () => R>(hook: T): R => {
+  return <T extends () => any>(hook: T): ReturnType<T> => {
     const back = getInstance;
     getInstance = () => instance;
     const result = hook();

+ 10 - 0
src/core/renderer-three/components/index.ts

@@ -0,0 +1,10 @@
+import { ShapeType } from "@/core/components";
+import Line from './line/index.vue'
+import LineIcon from './line-icon/index.vue'
+
+export const components: {
+  [key in ShapeType]?: (data: {data: any}) => void
+} = {}
+
+components.line = Line as any
+components.lineIcon = LineIcon as any

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

@@ -0,0 +1,77 @@
+<template></template>
+
+<script lang="ts" setup>
+import { getLineIconMat, getSnapLine, LineIconData } from "@/core/components/line-icon";
+import { useRender, useStageProps, useTree } from "../../hook/use-stage";
+import { getModel } from "./resource";
+import { Box3, MathUtils, Matrix4, Object3D, Vector3 } from "three";
+import { computed, ref, watch } from "vue";
+
+const props = defineProps<{ data: LineIconData }>();
+const render = useRender();
+const group = new Object3D();
+const model = ref<Object3D>();
+watch(
+  () => props.data.url,
+  async (type, _, onCleanup) => {
+    let typeModel = await getModel(type);
+    if (typeModel && type === props.data.url) {
+      typeModel = typeModel.clone();
+      model.value = typeModel;
+      group.add(typeModel);
+      const box3 = new Box3();
+      box3.setFromObject(group);
+      render();
+      onCleanup(() => {
+        group.remove(typeModel!);
+        render();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const store = useStageProps().value.draw.store;
+const line = computed(
+  () => store.getTypeItems("line")[0].lines.find((item) => item.id === props.data.lineId)!
+);
+
+const config = computed(() => {
+  const fullTypes = ["men_l", "piaochuang"];
+  const data = { ...props.data };
+  if (fullTypes.some((t) => data.url.includes(t))) {
+    data.type = "full";
+  }
+  const config = getLineIconMat(getSnapLine(store, data)!, data).decompose();
+  return config;
+});
+const width = computed(() => Math.abs(props.data.endLen - props.data.startLen));
+const sProps = useStageProps();
+
+const mat = computed(() => {
+  return new Matrix4()
+    .makeTranslation(new Vector3(config.value.x, sProps.value.height / 2, config.value.y))
+    .multiply(new Matrix4().makeRotationY(MathUtils.degToRad(config.value.rotation)))
+    .multiply(
+      new Matrix4().makeScale(
+        width.value * config.value.scaleX,
+        sProps.value.height,
+        line.value.strokeWidth * config.value.scaleY
+      )
+    );
+});
+
+watch(
+  mat,
+  (mat) => {
+    group.matrixAutoUpdate = false;
+    group.matrix.copy(mat);
+    group.matrixWorldNeedsUpdate = true;
+
+    render();
+  },
+  { immediate: true }
+);
+
+useTree().value = group;
+</script>

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

@@ -0,0 +1,64 @@
+import { Box3, Color, 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) => {
+  const parent = new Object3D();
+  parent.add(model);
+
+  const bbox = new Box3().setFromObject(parent);
+  const size = bbox.getSize(new Vector3());
+  parent.scale.set(1 / size.x, 1 / size.y, 1 / size.z);
+  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");
+    return await normalized(gltf.scene);
+  },
+  "piaochuang.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("window/scene.gltf");
+    // gltf.scene.rotateY(Math.PI / 2);
+    gltf.scene.traverse((node: any) => {
+      if (node.name === '02') {
+        node.parent.remove(node)
+      }
+    })
+    return await normalized(gltf.scene);
+  },
+  "chuang.svg": () => {
+    return getModel('piaochuang.svg') as Promise<Object3D>
+  }
+};
+
+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];
+    }
+  };
+})();

+ 96 - 0
src/core/renderer-three/components/line.vue

@@ -0,0 +1,96 @@
+<template></template>
+
+<script lang="ts" setup>
+import { LineData } from "@/core/components/line";
+import { getLinePoints } from "@/core/components/line/attach-server";
+import {
+  BoxGeometry,
+  Color,
+  DoubleSide,
+  Group,
+  Matrix4,
+  Mesh,
+  MeshPhongMaterial,
+  Vector3,
+} from "three";
+import { computed, ref, watch, watchEffect } from "vue";
+import { useRender, useTree } from "../hook/use-stage";
+import { copy, diffArrayChange, mergeFuns } from "@/utils/shared";
+import { eqPoint, lineCenter, lineLen, lineVector, vectorAngle } from "@/utils/math";
+
+const props = defineProps<{ data: LineData }>();
+const render = useRender();
+const group = new Group();
+
+const destorys: Record<string, () => void> = {};
+const addLine = (id: string) => {
+  const line = computed(() => props.data.lines.find((item) => item.id === id)!);
+  const points = ref(copy(getLinePoints(props.data, line.value)));
+  const width = computed(() => lineLen(points.value[0], points.value[1]));
+
+  const geometry = computed(
+    () => new BoxGeometry(width.value, 100, line.value.strokeWidth)
+  );
+
+  const material = computed(() => {
+    const color = new Color(line.value.stroke);
+    return new MeshPhongMaterial({ color: color, side: DoubleSide });
+  });
+  const mesh = computed(() => {
+    const mesh = new Mesh(geometry.value, material.value);
+    mesh.castShadow = true;
+    mesh.receiveShadow = true;
+    return mesh;
+  });
+
+  const cleanups = [
+    watchEffect(() => {
+      const _points = getLinePoints(props.data, line.value);
+      if (
+        !eqPoint(points.value[0], _points[0]) ||
+        !eqPoint(points.value[1], _points[1])
+      ) {
+        points.value = copy(_points);
+      }
+    }),
+    watchEffect((onCleanup) => {
+      group.add(mesh.value);
+      watchEffect(() => {
+        const center = lineCenter(points.value);
+        const mat = new Matrix4().multiplyMatrices(
+          new Matrix4().makeTranslation(new Vector3(center.x, 50.5, center.y)),
+          new Matrix4().makeRotationY(vectorAngle(lineVector(points.value)))
+        );
+        mesh.value.matrixAutoUpdate = false;
+        mesh.value.matrix.copy(mat);
+        mesh.value.matrixWorldNeedsUpdate = true;
+        render();
+      });
+      onCleanup(() => {
+        group.remove(mesh.value);
+        render();
+      });
+    }),
+  ];
+
+  destorys[line.value.id] = () => {
+    mergeFuns(cleanups)();
+  };
+};
+
+const delLine = (id: string) => {
+  destorys[id]?.();
+};
+
+watch(
+  () => props.data.lines.map((item) => item.id),
+  (newLines, oldLines = []) => {
+    const { added, deleted } = diffArrayChange(newLines, oldLines);
+    deleted.forEach(delLine);
+    added.forEach(addLine);
+  },
+  { immediate: true }
+);
+
+useTree().value = group;
+</script>

+ 23 - 0
src/core/renderer-three/components/line/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <Line
+    v-for="attr in data.lines"
+    :data="data"
+    :line="attr"
+    :key="attr.id"
+    :getExtendPolygon="getExtendPolygon"
+  />
+</template>
+
+<script lang="ts" setup>
+import { LineData } from "@/core/components/line";
+import Line from "./single-line.vue";
+import { computed } from "vue";
+import { useDrawHook } from "../../hook/use-stage";
+import { useGetExtendPolygon } from "@/core/components/line/renderer/wall/view";
+
+const props = defineProps<{ data: LineData }>();
+
+const getExtendPolygon = useDrawHook(() =>
+  useGetExtendPolygon(computed(() => props.data))
+);
+</script>

+ 25 - 0
src/core/renderer-three/components/line/material.ts

@@ -0,0 +1,25 @@
+import { MeshPhongMaterial, MeshPhongMaterialParameters, ShaderLib, UniformsUtils } from "three";
+
+export class StablePhongMaterial extends MeshPhongMaterial {
+  uniforms = {
+    ...UniformsUtils.clone(ShaderLib.phong.uniforms),
+    objectId: { value: 0 }
+  }
+  
+  constructor(options?: MeshPhongMaterialParameters) {
+    super(options)
+
+    this.onBeforeCompile = shader => {
+      shader.uniforms.objectId = this.uniforms.objectId
+      shader.vertexShader = `
+        uniform float objectId;
+        ${shader.vertexShader}
+      `.replace(`#include <project_vertex>`, `
+          #include <project_vertex>
+          gl_Position.z += objectId * 0.0001;
+      `);
+
+      this.userData.shader = shader
+    }
+  }
+}

+ 96 - 0
src/core/renderer-three/components/line/single-line.vue

@@ -0,0 +1,96 @@
+<template></template>
+
+<script lang="ts" setup>
+import { LineData, LineDataLine } from "@/core/components/line";
+import { getLinePoints } from "@/core/components/line/attach-server";
+import {
+  BufferGeometry,
+  Color,
+  DoubleSide,
+  ExtrudeGeometry,
+  Mesh,
+  MeshPhongMaterial,
+  Shape,
+} from "three";
+import { computed, onUnmounted, Ref, ref, watch, watchEffect } from "vue";
+import { useDrawHook, useRender, useStageProps, useTree } from "../../hook/use-stage";
+import { debounce } from "@/utils/shared";
+import {
+  useGetDiffLineIconPolygons,
+  useGetExtendPolygon,
+} from "@/core/components/line/renderer/wall/view";
+import { BufferGeometryUtils } from "three/examples/jsm/Addons.js";
+import { StablePhongMaterial } from "./material";
+
+const props = defineProps<{
+  line: LineDataLine;
+  data: LineData;
+  getExtendPolygon: ReturnType<typeof useGetExtendPolygon>;
+}>();
+
+const polygon = computed(() => props.getExtendPolygon(props.line));
+const points = computed(() => getLinePoints(props.data, props.line));
+const gd = useDrawHook(() => useGetDiffLineIconPolygons(props.line, points));
+const polygons = computed(() => gd.diff(polygon.value));
+const geometry = ref() as Ref<BufferGeometry>;
+const sProps = useStageProps();
+
+watch(
+  polygons,
+  debounce(() => {
+    if (geometry.value) {
+      geometry.value.dispose();
+    }
+    const polyGeos = polygons.value.map((poly) => {
+      const shape = new Shape();
+      shape.moveTo(poly[0].x, poly[0].y);
+      for (let i = 1; i < poly.length; i++) {
+        shape.lineTo(poly[i].x, poly[i].y);
+      }
+      shape.lineTo(poly[poly.length - 1].x, poly[poly.length - 1].y);
+      const geo = new ExtrudeGeometry(shape, {
+        depth: sProps.value.height,
+        bevelEnabled: false,
+        steps: 1,
+      });
+      return geo;
+    });
+
+    geometry.value = BufferGeometryUtils.mergeGeometries(polyGeos);
+    geometry.value
+      .rotateX(Math.PI / 2)
+      .translate(0, sProps.value.height + sProps.value.height * 0.01, 0);
+    polyGeos.forEach((geo) => geo.dispose());
+  }),
+  { immediate: true }
+);
+
+const material = new StablePhongMaterial({ side: DoubleSide });
+material.uniforms.objectId.value = props.data.lines.indexOf(props.line);
+
+const render = useRender();
+watchEffect(() => {
+  material.color = new Color(props.line.stroke);
+  render();
+});
+
+const mesh = new Mesh(undefined, material);
+mesh.castShadow = true;
+mesh.receiveShadow = true;
+watchEffect(() => {
+  mesh.geometry = geometry.value;
+  render();
+});
+
+onUnmounted(() => {
+  material.dispose();
+  mesh.geometry?.dispose();
+});
+
+const tree = useTree();
+watchEffect(() => {
+  if (geometry.value) {
+    tree.value = mesh;
+  }
+});
+</script>

+ 21 - 0
src/core/renderer-three/env/ground.vue

@@ -0,0 +1,21 @@
+<template></template>
+
+<script lang="ts" setup>
+import {
+  DoubleSide,
+  Mesh,
+  MeshPhongMaterial,
+  PlaneGeometry,
+} from "three";
+import { useTree } from "../hook/use-stage";
+
+const geometry = new PlaneGeometry(10000, 10000, 1, 1);
+const material = new MeshPhongMaterial({
+  color: 0xffffff,
+  side: DoubleSide,
+});
+const ground = new Mesh(geometry, material);
+ground.rotateX(-Math.PI / 2);
+ground.receiveShadow = true;
+useTree().value = ground;
+</script>

+ 36 - 0
src/core/renderer-three/env/light.vue

@@ -0,0 +1,36 @@
+<template></template>
+
+<script lang="ts" setup>
+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);
+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));
+
+useTree().value = group;
+</script>

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

@@ -0,0 +1,190 @@
+import { DrawExpose } from "@/core/hook/use-expose";
+import { globalWatch, installGlobalVar } from "@/core/hook/use-global-vars";
+import { listener } from "@/utils/event";
+import { frameEebounce } from "@/utils/shared";
+import mitt, { Emitter } from "mitt";
+import {
+  Color,
+  Object3D,
+  PCFSoftShadowMap,
+  PerspectiveCamera,
+  Scene,
+  Vector3,
+  WebGLRenderer,
+} from "three";
+import { OrbitControls } from "three/addons";
+import {
+  computed,
+  getCurrentInstance,
+  onUnmounted,
+  Ref,
+  ref,
+  shallowRef,
+  watch,
+} from "vue";
+
+export const instanceName = "three-renderer";
+export const installThreeGlobalVar = <T>(
+  create: () => { var: T; onDestroy: () => void } | T,
+  key = Symbol("globalVar")
+) => installGlobalVar(create, key, instanceName);
+
+export const useContainer = installThreeGlobalVar(() => ref<HTMLDivElement>());
+
+export const useRenderer = installThreeGlobalVar(() => {
+  const container = useContainer();
+  const renderer = new WebGLRenderer({ antialias: true }) as WebGLRenderer & {
+    bus: Emitter<{ sizeChange: void }>;
+  };
+
+  renderer.bus = mitt();
+  renderer.shadowMap.enabled = true;
+  renderer.shadowMap.type = PCFSoftShadowMap
+
+  const init = (container: HTMLDivElement) => {
+    container.appendChild(renderer.domElement);
+    const resizeHandler = () => {
+      const w = container.offsetWidth;
+      const h = container.offsetHeight;
+      renderer.setSize(w, h);
+      renderer.bus.emit("sizeChange");
+    };
+
+    resizeHandler();
+    return listener(window, "resize", resizeHandler);
+  };
+
+  return {
+    var: renderer,
+    onDestroy: globalWatch(
+      container,
+      (dom, _, onCleanup) => {
+        dom && onCleanup(init(dom));
+      },
+      { immediate: true }
+    ),
+  };
+});
+
+export type StageProps = {
+  draw: DrawExpose
+  height?: number
+}
+export const useStageProps = installThreeGlobalVar(() => ref() as Ref<Required<StageProps>>)
+
+export type Loop = (time: number) => 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 remove = (fn: Loop) => {
+    const ndx = loops.value.indexOf(fn);
+    if (~ndx) {
+      loops.value.splice(ndx, 1);
+    }
+  };
+  const add = (fn: Loop) => {
+    loops.value.push(fn);
+    return () => remove(fn);
+  };
+
+  return {
+    var: { add, remove },
+    onDestroy: cleanup,
+  };
+});
+
+export const useCamera = installThreeGlobalVar(() => {
+  const camera = new PerspectiveCamera(75, 1, 0.1, 500000);
+  camera.position.set(0, 2, 0)
+  camera.position.multiplyScalar(800)
+  // camera.position.z = 180;
+  // camera.position.y = 200;
+  camera.lookAt(new Vector3(0, 1, 0));
+  return camera;
+});
+
+export const useScene = installThreeGlobalVar(() => {
+  const scene = new Scene()
+  scene.background = new Color('white')
+  return scene
+});
+
+export const useRender = installThreeGlobalVar(() => {
+  const renderer = useRenderer();
+  const scene = useScene();
+  const camera = useCamera();
+  const render = frameEebounce(() => renderer.render(scene, camera));
+
+  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;
+});
+
+export const useTree = () => {
+  const expose = shallowRef();
+  const current = getCurrentInstance() as any;
+  const render = useRender();
+
+  let parent = current.parent;
+  while (parent.type.name !== instanceName) {
+    if (parent.__three_instance) {
+      break;
+    } else if (parent.parent) {
+      parent = parent.parent;
+    }
+  }
+
+  const scene = useScene();
+  const threeParent = computed(
+    () =>
+      (parent.__three_instance
+        ? parent.__three_instance.value
+        : scene) as Object3D
+  );
+
+  watch([threeParent, expose], ([parent, current], _, onCleanup) => {
+    if (parent && current) {
+      parent.add(current);
+      render();
+      onCleanup(() => {
+        parent.remove(current);
+        render();
+      });
+    }
+  });
+
+  current.__three_instance = expose;
+  onUnmounted(() => {
+    expose.value = undefined;
+  });
+  return expose;
+};
+
+export const useDrawHook = <T extends () => any>(hook: T): ReturnType<T> => {
+  const draw = useStageProps().value.draw;
+  return draw.runHook(hook);
+}

+ 43 - 0
src/core/renderer-three/renderer.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="three-container" ref="container">
+    <Ground />
+    <Light />
+    <template v-for="(com, key) in components" :key="key">
+      <component
+        :is="com"
+        :data="item"
+        v-for="item in draw.store.getTypeItems(key)"
+        :key="item.id"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+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 { rendererMap } from "@/constant";
+import { mergeFuns } from "@/utils/shared";
+import { components } from "./components";
+
+const instance = getCurrentInstance();
+defineOptions({ name: instanceName });
+rendererMap.set(instance, { unmounteds: [] });
+onUnmounted(() => {
+  mergeFuns(rendererMap.get(instance)!.unmounteds)();
+});
+
+const props = defineProps<StageProps>();
+useStageProps().value = { ...props, height: props.height || 200 };
+const container = useContainer();
+</script>
+
+<style lang="scss">
+.three-container {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 11 - 1
src/example/components/slide/actions.ts

@@ -11,7 +11,7 @@ import { MenuItem } from "./menu";
 import { copy } from "@/utils/shared";
 import { ElMessage } from "element-plus";
 import { getRealPixel } from "@/example/fuse/views/tabulation/gen-tab";
-import { nextTick } from "vue";
+import { nextTick, ref } from "vue";
 import { Rect } from "konva/lib/shapes/Rect";
 import { Size } from "@/utils/math";
 import { getBaseItem } from "@/core/components/util";
@@ -73,6 +73,7 @@ export const imp: MenuItem = {
       name: "场景",
       handler: async (draw: Draw) => {
         const aiData = await selectAI();
+        console.log(aiData)
         await drawPlatformResource(aiData, draw);
       },
     },
@@ -210,12 +211,21 @@ export const dbImage: MenuItem = {
   ],
 };
 
+export const showThree = ref(false)
 export const test: MenuItem = {
   value: uuid(),
   icon: "debugger",
   name: "测试",
   type: "sub-menu-horizontal",
   children: [
+    {
+      value: uuid(),
+      icon: "",
+      name: "显示三维视角",
+      handler: () => {
+        showThree.value = !showThree.value
+      },
+    },
     ...dbImage.children!,
     {
       value: uuid(),

+ 9 - 2
src/example/env.ts

@@ -1,4 +1,4 @@
-import { computed, ref, watch } from "vue";
+import { computed, ref, watch, nextTick } from "vue";
 
 export const urlUpdateQuery = (
   link: string,
@@ -54,6 +54,11 @@ const updateParams = () => {
 };
 updateParams();
 
+let updateNeedReload = true
+export const preventReload = () => {
+  updateNeedReload = false
+  setTimeout(() => updateNeedReload = true)
+}
 watch(
   () => ({ ...params.value }),
   (nParams, oParams) => {
@@ -61,13 +66,15 @@ watch(
     for (const key in params.value) {
       params.value[key] && sParams.append(key, params.value[key]);
     }
-    console.error(params.value)
     const ndx = location.hash.indexOf("?");
     location.replace(
       location.hash.substring(0, ~ndx ? ndx : undefined) +
         "?" +
         sParams.toString()
     );
+    if (!updateNeedReload) {
+      return;
+    }
     const keys =  new Set([...Object.keys(nParams), ...Object.keys(oParams)])
     for (const key of keys) {
       if (!nParams[key] || (nParams[key] && oParams[key])) {

+ 2 - 0
src/example/fuse/views/overview/header.vue

@@ -41,6 +41,7 @@ const props = defineProps<{ title: string }>();
 const draw = useDraw();
 const emit = defineEmits<{
   (e: "selectVR", v: Scene): void;
+  (e: "saveAfter"): void;
 }>();
 
 const baseActions = getHeaderActions(draw);
@@ -278,6 +279,7 @@ const saveHandler = repeatedlyOnly(async () => {
   });
   tabulationId.value = await window.platform.getTabulationId(overviewId.value);
   console.log("保存完毕");
+  emit("saveAfter");
 });
 
 onUnmounted(

+ 59 - 1
src/example/fuse/views/overview/index.vue

@@ -8,6 +8,7 @@
       <Header
         @selectVR="(scene) => (vrScene = scene)"
         :title="title"
+        @save-after="() => mergeFuns(saveAfterHandlers)()"
         :ref="(r) => (header = r)"
       />
     </template>
@@ -16,6 +17,9 @@
     </template>
     <template #cover>
       <ShowVR :scene="vrScene" v-if="vrScene" ref="vr" @close="vrScene = undefined" />
+      <div class="three-view" v-if="attach && showThree">
+        <component :is="attach" :draw="(draw as any)" />
+      </div>
     </template>
   </Container>
 
@@ -27,12 +31,17 @@ import Header from "./header.vue";
 import Slide from "./slide.vue";
 import Container from "../../../components/container/container.vue";
 import ShowVR from "../../../components/show-vr.vue";
-import { computed, ref, watch, watchEffect } from "vue";
+import { computed, ref, shallowRef, watch, watchEffect } from "vue";
 import { Draw } from "../../../components/container/use-draw";
 import { Scene } from "../../../platform/platform-resource";
 import Dialog from "../../../dialog/dialog.vue";
 import { refreshOverviewData, overviewData } from "../../store";
 import { overviewCustomStyle } from "../defStyle";
+import { showThree } from "@/example/components/slide/actions";
+import { params, preventReload } from "@/example/env";
+import { getFloors } from "@/example/platform/resource-swkk";
+import { mergeFuns } from "@/utils/shared";
+import { drawPlatformResource } from "@/example/platform/platform-draw";
 
 const uploadResourse = window.platform.uploadResourse;
 const full = ref(false);
@@ -40,6 +49,43 @@ const draw = ref<Draw>();
 const vrScene = ref<Scene>();
 const header = ref<any>();
 
+const attach = shallowRef<any>();
+if (import.meta.env.DEV) {
+  import("@/core/renderer-three/renderer.vue").then((res) => {
+    attach.value = res.default;
+  });
+}
+
+const saveAfterHandlers: (() => void)[] = [];
+const initScene = async (draw: Draw) => {
+  const m = params.value.m;
+  const subgroup = params.value.floor;
+  const delHandler = () => {
+    preventReload();
+    delete params.value.m;
+    delete params.value.floor;
+    const ndx = saveAfterHandlers.indexOf(delHandler);
+    ~ndx && saveAfterHandlers.splice(ndx, 1);
+  };
+  saveAfterHandlers.push(delHandler);
+
+  const scenes = await window.platform.getSceneList("");
+  const scene = scenes.find((scene: any) => scene.m === m);
+  const floors = await getFloors(scene);
+  let floor =
+    subgroup && floors.find((floor: any) => floor.subgroup.toString() === subgroup);
+  floor = floor || floors[0];
+
+  await drawPlatformResource(
+    {
+      scene,
+      floorName: floor.name,
+      syncs: ["signage"],
+    },
+    draw
+  );
+};
+
 const init = async (draw: Draw) => {
   await refreshOverviewData();
   draw.config.showCompass = false;
@@ -50,6 +96,8 @@ const init = async (draw: Draw) => {
   // setMap(draw);
   draw.store.setStore(overviewData.value.store);
   overviewData.value.viewport && draw.viewer.setViewMat(overviewData.value.viewport);
+  params.value.m && initScene(draw);
+
   return overviewCustomStyle(draw);
 };
 
@@ -75,3 +123,13 @@ watchEffect(() => {
   document.title = title.value;
 });
 </script>
+
+<style lang="scss">
+.three-view {
+  position: absolute;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  width: 50%;
+}
+</style>

+ 3 - 1
src/example/platform/platform-draw.ts

@@ -252,7 +252,9 @@ const drawSceneResource = async (resource: SceneResource, draw: Draw) => {
   if (oldGeo?.itemName === geoKey) {
     const bound = genBound();
     geo.points.forEach((p) => bound.update(p));
-    offset = await getDrawResourceOffset(draw, bound, thumb);
+    if (bound.get()) {
+      offset = await getDrawResourceOffset(draw, bound, thumb);
+    }
   } else if (oldGeo) {
     oldGeo.itemName = geoKey;
   } else {