bill недель назад: 4
Родитель
Сommit
72eb57e135

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "drawing-board-service",
   "private": true,
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "module",
   "scripts": {
     "dev:fuse": "vite --mode=criminaldev",

+ 1 - 1
profile/.env.jmdev

@@ -13,7 +13,7 @@ VITE_MESH_API='/meshAPI/'
 VITE_CLOUD_API='/cloudAPI/'
 VITE_FUSE_API='/fuseAPI/'
 
-VITE_MESH_VIEW='./static/kankan.html?m={m}&lang=zh&token={token}&host=https://4dkk.4dage.com/v4-test/www/'
+VITE_MESH_VIEW='./static/kankan.html?m={m}&lang=zh&token={token}&host=https://survey.4dkankan.com/'
 VITE_CLOUD_VIEW='https://survey.4dkankan.com/swss/index.html?m={m}&lang=zh&token={token}'
 VITE_FUSE_VIEW='https://survey.4dkankan.com/code/'
 VITE_LOGIN_VIEW='https://survey.4dkankan.com/admin/index.html#/login?redirect={redirect}'

+ 18 - 4
public/static/kankan.html

@@ -141,7 +141,7 @@
         $old.classList.remove('active')
       }
       const $item = document.querySelector(`#floors > li[attr-id='${id}']`)
-      kankan.Scene.gotoFloor(Number(id))
+      
       $item && $item.classList.add('active')
 
       document.querySelector('#floor').innerHTML = $item.innerHTML
@@ -154,11 +154,12 @@
       }
 
       $floors.parentElement.style.display = 'block'
-      $floors.innerHTML = floors.map(item => `<li attr-id="${item.id}">${item.name}</li>`).join('')
+      $floors.innerHTML = floors.reverse().map(item => `<li attr-id="${item.id}">${item.name}</li>`).join('')
       $floors.addEventListener('click', ev => {
         const dom = ev.target
         const id = dom.getAttribute('attr-id')
         if (!id) return;
+        kankan.Scene.gotoFloor(Number(id))
         setCurrentFloor(id)
       })
       $floors.parentElement.addEventListener('mouseenter', () => {
@@ -175,11 +176,9 @@
         $old.classList.remove('active')
       }
       const $item = document.querySelector(`#modes > li[attr-id='${id}']`)
-      kankan.Camera[id]()
       $item && $item.classList.add('active')
 
       document.querySelector('#mode').innerHTML = $item.innerHTML
-
     }
 
     const renderModes = () => {
@@ -191,6 +190,7 @@
         const id = dom.getAttribute('attr-id')
         if (!id) return;
         setCurrentMode(id)
+        kankan.Camera[id]()
       })
       $modes.parentElement.addEventListener('mouseenter', () => {
         $modes.style.display = 'block'
@@ -199,6 +199,8 @@
         $modes.style.display = 'none'
       })
       setCurrentMode('panorama')
+
+
     }
 
 
@@ -213,6 +215,7 @@
 
       kankan.store.on('flooruser', floor => {
         renderFloors(floor.floors)
+        console.error(floor.floors)
       })
       kankan.Scene.on('loaded', () => {
         const player = kankan.core.get('Player')
@@ -227,6 +230,17 @@
 
       })
       renderModes()
+
+      kankan.Camera.on('mode.afterChange', ({ toMode, floorIndex }) => {
+        setCurrentFloor(kankan.Scene.floorId)
+        setCurrentMode(toMode)
+        // dollhouse
+        // floorplan
+        // panorama
+      })
+      kankan.Camera.on('flying.ended', () => {
+        setCurrentFloor(kankan.Scene.floorId)
+      })
     }
 
     const init = async () => {

+ 4 - 0
src/core/components/line/attach-server.ts

@@ -602,6 +602,10 @@ export const useLineDescribes = (line: Ref<LineDataLine>) => {
     type: "inputNum",
     label: "线段长度",
     "layout-type": "row",
+    props: {
+      proportion: true,
+    },
+    
     get value() {
       return lineLen(points.value[0], points.value[1]);
     },

+ 16 - 12
src/core/hook/use-selection.ts

@@ -32,7 +32,13 @@ import {
   useViewerInvertTransform,
   useViewerInvertTransformConfig,
 } from "./use-viewer";
-import { debounce, diffArrayChange, mergeFuns, onlyId } from "@/utils/shared";
+import {
+  debounce,
+  diffArrayChange,
+  frameEebounce,
+  mergeFuns,
+  onlyId,
+} from "@/utils/shared";
 import { IRect } from "konva/lib/types";
 import { useMouseShapesStatus } from "./use-mouse-status";
 import Icon from "../components/icon/temp-icon.vue";
@@ -95,7 +101,7 @@ export const useSelection = installGlobalVar(() => {
   const store = useStore();
   const init = (dom: HTMLDivElement, layer: Layer) => {
     store.bus.on("addItemAfter", updateInitData);
-    store.bus.on('dataChangeAfter', updateInitData);
+    store.bus.on("dataChangeAfter", updateInitData);
     const stopListener = dragListener(dom, {
       down(pos) {
         layer.add(box);
@@ -116,7 +122,7 @@ export const useSelection = installGlobalVar(() => {
     });
     return () => {
       store.bus.off("addItemAfter", updateInitData);
-      store.bus.off('dataChangeAfter', updateInitData);
+      store.bus.off("dataChangeAfter", updateInitData);
       stopListener();
       box.remove();
     };
@@ -192,16 +198,14 @@ export const useShapesIcon = (
     for (const addShape of added) {
       const mat = ref(getShapeMat(addShape));
       const data = reactive({ ...iconProps, mat: mat });
+      const update = frameEebounce(() => {
+        data.width = invConfig.value.scaleX * iconProps.width;
+        data.height = invConfig.value.scaleY * iconProps.height;
+        mat.value = getShapeMat(addShape);
+      });
       const unHooks = [
-        on(addShape, () => (mat.value = getShapeMat(addShape))),
-        watch(
-          invConfig,
-          () => {
-            data.width = invConfig.value.scaleX * iconProps.width;
-            data.height = invConfig.value.scaleY * iconProps.height;
-          },
-          { immediate: true }
-        ),
+        on(addShape, update),
+        watch(invConfig, update, { immediate: true, flush: "post" }),
         mParts.add({
           comp: markRaw(Icon),
           props: { data },

+ 5 - 2
src/core/hook/use-status.ts

@@ -1,6 +1,8 @@
 import { computed, reactive } from "vue";
 import { installGlobalVar, stackVar, useDownKeys, useStage } from "./use-global-vars";
 import { Mode } from "@/constant/mode";
+import { useInteractiveAdd } from "./use-draw";
+import { useInteractiveProps } from "./use-interactive";
 
 
 export const useMode = installGlobalVar(() => {
@@ -84,10 +86,11 @@ export const useCan = installGlobalVar(() => {
 
 export const useOperMode = installGlobalVar(() => {
   const keys = useDownKeys()
-
+  const interactiveProps = useInteractiveProps();
+  
   return computed(() => ({
     // 多选模式
-    mulSelection: keys.has('Ctrl') && !keys.has(' ') && !keys.has('Alt'),
+    mulSelection: keys.has('Ctrl') && !keys.has(' ') && !keys.has('Alt') && !interactiveProps.value,
     // mulSelection: keys.has('Meta') && !keys.has(' ') && !keys.has('Alt'),
     // mulSelection: false,
     // 自由移动视图

+ 28 - 2
src/core/renderer-three/components/icon/index.vue

@@ -2,23 +2,26 @@
 
 <script lang="ts" setup>
 import { useRender, useStageProps, useTree } from "../../hook/use-stage";
-import { getLevel, getModel } from "../resource";
-import { Box3, Group, MathUtils, Matrix4, Vector3 } from "three";
+import { getLevel, getModel, getModelSwitch, SwitchResult } from "../resource";
+import { Box3, Group, MathUtils, Matrix4, Object3D, 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";
+import { useInteraction } from "../../hook/use-event";
 
 const props = defineProps<{ data: IconData }>();
 const render = useRender();
 
 const group = new Group();
 const size = shallowRef<Vector3>();
+const model = shallowRef<Object3D>();
 watch(
   () => props.data.url,
   async (type, _, onCleanup) => {
     let typeModel = await getModel(type);
     if (typeModel && type === props.data.url) {
+      model.value = typeModel;
       typeModel = typeModel.clone();
       size.value = new Box3().setFromObject(typeModel).getSize(new Vector3());
       group.add(typeModel);
@@ -26,6 +29,7 @@ watch(
       onCleanup(() => {
         size.value = undefined;
         group.remove(typeModel!);
+        model.value = undefined;
         render();
       });
     }
@@ -73,5 +77,27 @@ watch(
   { immediate: true }
 );
 
+const modelSwitchFactory = computed(() => getModelSwitch(props.data.url));
+let stopAnimation: SwitchResult | null = null;
+if (modelSwitchFactory.value) {
+  const modelSwitch = computed(
+    () => model.value && modelSwitchFactory.value(model.value, render)
+  );
+  let open = false;
+  useInteraction(
+    group,
+    () => {
+      if (modelSwitch.value) {
+        stopAnimation && stopAnimation();
+        stopAnimation = modelSwitch.value!((open = !open));
+        stopAnimation.promise.finally(() => {
+          stopAnimation = null;
+        });
+      }
+    },
+    "dblclick"
+  );
+}
+
 useTree().value = group;
 </script>

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

@@ -8,11 +8,12 @@ import {
 } from "@/core/components/line-icon";
 import { useRender, useStageProps, useTree } from "../../hook/use-stage";
 import { fullMesh, getLevel, getModel, getModelSwitch, SwitchResult } from "../resource";
-import { Group, Matrix4, Object3D } from "three";
+import { Group, Matrix4, Mesh, Object3D } from "three";
 import { computed, ref, shallowRef, watch, watchEffect } from "vue";
 import { lineCenter, lineVector, vector2IncludedAngle } from "@/utils/math";
 import { setMat } from "../../util";
 import { useInteraction } from "../../hook/use-event";
+import { skirtingHeight, skirtingMaterial } from "../line/material";
 
 const props = defineProps<{ data: LineIconData }>();
 const render = useRender();
@@ -102,17 +103,32 @@ watchEffect((onCleanup) => {
   topMesh.scale.set(1, topScale, 1);
   topMesh.position.add({ x: 0, y: 0.5 + topScale / 2, z: 0 });
 
-  const bottomMesh = fullMesh.clone();
-  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 });
+  const botHeight = btHeight.value.bottom - skirtingHeight;
+  const botScale = Math.max(botHeight / height.value, 0);
+  if (botHeight > 0) {
+    const botMesh = fullMesh.clone();
+    botMesh.scale.set(1, botScale, 1);
+    botMesh.position.add({ x: 0, y: -0.5 - botScale / 2, z: 0 });
+    group.add(botMesh);
+    onCleanup(() => {
+      render();
+      group.remove(botMesh);
+    });
+  }
+  const skirtingMesh = new Mesh(fullMesh.geometry, skirtingMaterial);
+  const skHeight = Math.min(btHeight.value.bottom, skirtingHeight);
+  const skScale = skHeight / height.value;
+  skirtingMesh.scale.set(1, skScale, 1);
+
+  skirtingMesh.position.add({ x: 0, y: -0.5 - skScale / 2 - botScale, z: 0 });
+  group.add(skirtingMesh);
 
   group.add(topMesh);
-  group.add(bottomMesh);
+
   render();
   onCleanup(() => {
     group.remove(topMesh);
-    group.remove(bottomMesh);
+    group.remove(skirtingMesh);
     render();
   });
 });

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

@@ -0,0 +1,45 @@
+import { pickPromise } from "@/utils/shared";
+import {
+  FrontSide,
+  MeshPhysicalMaterial,
+  TextureLoader,
+  Vector2,
+} from "three";
+import { EXRLoader } from "three/examples/jsm/Addons.js";
+
+const texLoader = new TextureLoader();
+const exrLoader = new EXRLoader();
+const texloads: Promise<void>[] = [];
+const loadTex = (loader: TextureLoader | EXRLoader, url: string) => {
+  const { promise, resolve, reject } = pickPromise<void>();
+  texloads.push(promise);
+  const tex = loader.load(
+    `/static/models/texture/skirting/${url}`,
+    () => resolve(),
+    undefined,
+    reject
+  );
+tex.repeat.y = 0.5;
+tex.offset.y = 0.0; // UV 从底部开始
+  return tex;
+};
+
+export const skirtingHeight = 20;
+export const skirtingMaterial = new MeshPhysicalMaterial({
+  color: 0xffffff, // 基础颜色(会被贴图覆盖)
+  roughness: 0.7, // 基础粗糙度(会被粗糙度贴图调整)
+  metalness: 0.0, // 木材无金属性
+  clearcoat: 0.1, // 轻微清漆效果
+  clearcoatRoughness: 0.2,
+  displacementScale: 0.01, // 置换强度(根据实际效果调整)
+  displacementBias: -0.02,
+  normalScale: new Vector2(1, 1), // 法线强度
+  transparent: false,
+  premultipliedAlpha: false,
+  side: FrontSide,
+  map: loadTex(texLoader, "oak_veneer_01_diff_1k.jpg"),
+  normalMap: loadTex(exrLoader, "oak_veneer_01_nor_gl_1k.exr"),
+  roughnessMap: loadTex(exrLoader, "oak_veneer_01_rough_1k.exr"),
+  displacementMap: loadTex(texLoader, "oak_veneer_01_disp_1k.png"),
+  aoMap: loadTex(texLoader, "oak_veneer_01_ao_1k.jpg"),
+});

+ 12 - 36
src/core/renderer-three/components/line/single-line.vue

@@ -5,14 +5,12 @@ import { LineData, LineDataLine } from "@/core/components/line";
 import { getLinePoints } from "@/core/components/line/attach-server";
 import {
   BufferGeometry,
-  Color,
   ExtrudeGeometry,
   FrontSide,
   Group,
   Mesh,
   MeshPhongMaterial,
   Shape,
-  Vector2,
 } from "three";
 import { computed, onUnmounted, Ref, ref, watch, watchEffect } from "vue";
 import { useDrawHook, useRender, useStageProps, useTree } from "../../hook/use-stage";
@@ -22,6 +20,7 @@ import {
   useGetExtendPolygon,
 } from "@/core/components/line/renderer/wall/view";
 import { BufferGeometryUtils } from "three/examples/jsm/Addons.js";
+import { skirtingHeight, skirtingMaterial } from "./material";
 
 const props = defineProps<{
   line: LineDataLine;
@@ -35,7 +34,6 @@ const gd = useDrawHook(() => useGetDiffLineIconPolygons(props.line, points));
 const polygons = computed(() => gd.diff(polygon.value));
 const wallGeo = ref() as Ref<BufferGeometry>;
 const skirtingGeo = ref() as Ref<BufferGeometry>;
-const skirtingHeight = 20;
 const sProps = useStageProps();
 
 watch(
@@ -43,40 +41,19 @@ watch(
   debounce(() => {
     wallGeo.value && wallGeo.value.dispose();
     skirtingGeo.value && skirtingGeo.value.dispose();
-
     const polyGeos = polygons.value.map((poly) => {
-      const wallShape = new Shape();
-      const center = new Vector2();
-      const vs = poly.map((p, ndx) => {
-        if (ndx === 0) {
-          wallShape.moveTo(p.x, p.y);
-        } else {
-          wallShape.lineTo(p.x, p.y);
-        }
-        const v = new Vector2(p.x, p.y);
-        center.add(v);
-        return v;
-      });
-      wallShape.closePath();
-      center.divideScalar(vs.length);
-
-      const skirtingShape = new Shape();
-      vs.forEach((v, ndx) => {
-        const p = v.clone().sub(center).multiplyScalar(1.1).add(center);
-        if (ndx === 0) {
-          skirtingShape.moveTo(p.x, p.y);
-        } else {
-          skirtingShape.lineTo(p.x, p.y);
-        }
-      });
-      skirtingShape.closePath();
-
-      const wallGeo = new ExtrudeGeometry(wallShape, {
+      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 wallGeo = new ExtrudeGeometry(shape, {
         depth: sProps.value.height - skirtingHeight,
         bevelEnabled: false,
         steps: 1,
       });
-      const skirtingGeo = new ExtrudeGeometry(skirtingShape, {
+      const skirtingGeo = new ExtrudeGeometry(shape, {
         depth: skirtingHeight,
         bevelEnabled: false,
         steps: 1,
@@ -112,10 +89,9 @@ const wall = new Mesh(
 wall.castShadow = true;
 wall.receiveShadow = true;
 
-const skirting = new Mesh(
-  undefined,
-  new MeshPhongMaterial({ side: FrontSide, color: 0xff0000 })
-);
+const skirting = new Mesh(undefined, skirtingMaterial);
+skirting.castShadow = true;
+skirting.receiveShadow = true;
 
 watchEffect(() => {
   wall.geometry = wallGeo.value;

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

@@ -589,6 +589,33 @@ export const switchResources: Record<
         }
       );
   },
+  "Wardrobe.svg": (model, render) => {
+    let nodes: Object3D[] = [];
+    const initVals = [0, 0];
+    const finalVals = [Math.PI / 2, Math.PI / 2];
+    const names = [
+      'RootNode',
+      'VIFS079_NCS_S_1502-Y50R_semigloss_0',
+      'VIFS077_NCS_S_1502-Y50R_semigloss_0',
+    ]
+
+    model.traverse((child) => {
+      if (names.includes(child.name)) {
+        nodes.push(child)
+      } 
+    });
+
+    return (open: boolean) =>
+      animation(
+        nodes.map((node) => node.rotation.y),
+        open ? finalVals : initVals,
+        (data) => {
+          nodes.forEach((node, i) => (node.rotateY(data[i])));
+          console.log(nodes, data)
+          render();
+        }
+      );
+  },
 
   "piaochuang.svg": (model, render) => {
     let nodes: Object3D[] = [];

+ 9 - 2
src/core/renderer-three/components/text/index.vue

@@ -18,7 +18,7 @@ import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
 import { Transform } from "konva/lib/Util";
 import { Size } from "@/utils/math";
 import { useInteraction } from "../../hook/use-event";
-import { useFlyRoaming } from "../../hook/use-controls";
+import { useControls, useFlyRoaming } from "../../hook/use-controls";
 
 const props = defineProps<{ data: TextData }>();
 
@@ -166,7 +166,14 @@ watch(
 );
 
 const group = new Object3D();
-group.add(text, line);
+
+const type = useControls().type;
+watchEffect((onCleanup) => {
+  if (type.value === "model") {
+    group.add(text, line);
+    onCleanup(() => group.remove(text, line));
+  }
+});
 
 useTree().value = group;
 </script>

+ 7 - 18
src/core/renderer-three/env/ground.vue

@@ -43,42 +43,31 @@ const loadTex = (loader: TextureLoader | EXRLoader, url: string) => {
 
 const texLoader = new TextureLoader();
 const exrLoader = new EXRLoader();
-
-const diffuseMap = loadTex(texLoader, "laminate_floor_02_diff_1k.jpg");
-const displacementMap = loadTex(texLoader, "laminate_floor_02_disp_1k.png");
-const normalMap = loadTex(exrLoader, "laminate_floor_02_nor_gl_1k.exr");
-const roughnessMap = loadTex(exrLoader, "laminate_floor_02_rough_1k.exr");
-
-const render = useRender();
-Promise.all(texloads).then(render);
-
 const material = new MeshPhysicalMaterial({
   // 基础属性
   color: 0xffffff, // 基础颜色(会被贴图覆盖)
-  map: diffuseMap, // 颜色贴图
-
+  map: loadTex(texLoader, "laminate_floor_02_diff_1k.jpg"), // 颜色贴图
   // 物理渲染属性
   roughness: 0.7, // 基础粗糙度(会被粗糙度贴图调整)
   metalness: 0.0, // 木材无金属性
   clearcoat: 0.1, // 轻微清漆效果
   clearcoatRoughness: 0.2,
-
   // 贴图增强
-  normalMap: normalMap, // 法线贴图
+  normalMap: loadTex(exrLoader, "laminate_floor_02_nor_gl_1k.exr"), // 法线贴图
   normalScale: new Vector2(1, 1), // 法线强度
-
-  displacementMap: displacementMap, // 置换贴图
+  displacementMap: loadTex(texLoader, "laminate_floor_02_disp_1k.png"), // 置换贴图
   displacementScale: 0.05, // 置换强度(根据实际效果调整)
   displacementBias: -0.02,
-
-  roughnessMap: roughnessMap, // 粗糙度贴图
-
+  roughnessMap: loadTex(exrLoader, "laminate_floor_02_rough_1k.exr"), // 粗糙度贴图
   // 其他设置
   side: FrontSide,
   transparent: false,
   premultipliedAlpha: false,
 });
 
+const render = useRender();
+Promise.all(texloads).then(render);
+
 const ground = new Mesh(geometry, material);
 ground.receiveShadow = true;
 ground.name = subgroupName;

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

@@ -271,7 +271,7 @@ export const installAutoSwitchControls = installThreeGlobalVar(() => {
   const flyRoaming = useFlyRoaming();
   const flyModel = useFlyModel();
   const sProps = useStageProps();
-  const canFlyTypes = ["line", "icon"] as const;
+  const canFlyTypes = ["line"] as const;
   const canFlyNames = computed(() => {
     const names = canFlyTypes.flatMap((type) =>
       sProps.value.draw.store.getTypeItems(type).map((item) => item.id)

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

@@ -157,6 +157,7 @@ export const useMouseEventRegister = installThreeGlobalVar(() => {
       rf.hitObjects.value = getIntersectObjectByPixel(
         new Vector2(ev.offsetX, ev.offsetY)
       );
+      // console.log(rf.hitObjects.value[0]?.object?.name)
     });
 
     return {

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

@@ -5,3 +5,4 @@ export const setMat = (obj: Object3D, mat: Matrix4) => {
   obj.matrix.copy(mat);
   obj.matrixWorldNeedsUpdate = true;
 };
+

+ 9 - 0
src/example/components/header/actions.ts

@@ -82,6 +82,15 @@ export const getHeaderActions = (draw: Draw) => {
           }
         })
       },
+      disabled: computed(() => {
+        const layer = draw.store.$state.data.layers[draw.store.currentLayer]
+        for (const [_, vals] of Object.entries(layer)) {
+          if (vals.some(val => val.hide)) {
+            return false
+          }
+        }
+        return true
+      }),
       text: "显示全部",
       icon: "a-visible",
     }),

+ 4 - 0
src/example/components/slide/slide.vue

@@ -99,6 +99,10 @@ watch(active, (a) => {
 
 let immedSelect = false;
 const selectHandler = async (val: string, immed = false) => {
+  if (active.value && immedSelect && !immed) {
+    val = active.value;
+  }
+
   if (val === active.value && !immedSelect && immed) {
     return;
   }

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

@@ -179,6 +179,9 @@ const setViewToKanKanCover = async () => {
     fontColor: "#FFFFFF",
   };
   const cSet: any = {
+    arrow: {
+      fill: "#FFFFFF",
+    },
     text: {
       fill: "#FFFFFF",
     },

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

@@ -118,7 +118,7 @@ watch(draw, (draw, _, onCleanup) => {
     });
   }
 });
-const title = computed(() => overviewData.value?.title || "图");
+const title = computed(() => overviewData.value?.title || "平面图");
 watchEffect(() => {
   document.title = title.value;
 });