Browse Source

制作场景内功能模块搭建测量固定点灯

bill 2 years ago
parent
commit
c0c64472d9
38 changed files with 1020 additions and 37 deletions
  1. BIN
      public/static/t.png
  2. 6 7
      src/components/base/assets/scss/components/_message.scss
  3. 2 2
      src/components/base/components/message/index.js
  4. 1 1
      src/components/base/components/message/message.vue
  5. 1 1
      src/components/button-pane/index.vue
  6. 13 2
      src/components/group-button/index.vue
  7. 1 2
      src/hook/custom/index.ts
  8. 2 0
      src/hook/custom/setup.ts
  9. 1 1
      src/sdk/carry/inject/atom.vue
  10. 7 6
      src/sdk/carry/measures/index.vue
  11. 1 1
      src/sdk/carry/setup.vue
  12. 4 0
      src/sdk/types/store.ts
  13. 10 0
      src/store/baseLine.ts
  14. 12 0
      src/store/basePoint.ts
  15. 12 0
      src/store/fixPoint.ts
  16. 2 1
      src/store/index.ts
  17. 3 2
      src/utils/menus.ts
  18. 3 2
      src/views/graphic/menus.ts
  19. 2 2
      src/views/graphic/vectorMenus.vue
  20. 1 1
      src/views/measure/constant.ts
  21. 53 0
      src/views/scene/covers/actions.vue
  22. 31 0
      src/views/scene/covers/basePoint.vue
  23. 36 0
      src/views/scene/covers/basePoints.vue
  24. 101 0
      src/views/scene/covers/cover.vue
  25. 30 0
      src/views/scene/covers/fixPoint.vue
  26. 44 0
      src/views/scene/covers/fixPoints.vue
  27. 34 0
      src/views/scene/covers/measure.vue
  28. 35 0
      src/views/scene/covers/measures.vue
  29. BIN
      src/views/scene/covers/point1.png
  30. 37 5
      src/views/scene/index.vue
  31. 24 0
      src/views/scene/linkage/cover.ts
  32. 91 0
      src/views/scene/linkage/measure.ts
  33. 0 0
      src/views/scene/menus.ts
  34. 140 0
      src/views/scene/menus/actions.ts
  35. 113 0
      src/views/scene/menus/menus.ts
  36. 54 0
      src/views/scene/menus/pane.vue
  37. 0 1
      src/views/scene/mode.vue
  38. 113 0
      src/views/scene/photo.vue

BIN
public/static/t.png


+ 6 - 7
src/components/base/assets/scss/components/_message.scss

@@ -1,15 +1,13 @@
 .ui-message {
   position: fixed;
   left: 50%;
-  top: 110px;
+  top: 24px;
   height: 40px;
   padding: 0 20px;
-  background: rgba(20,20,20,0.7);
-  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
-  border-radius: 4px;
-  border: 1px solid #000000;
-  backdrop-filter: blur(4px);
-  color: #fff;
+  background: #fff;
+  border-radius: 20px;
+  box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.5);
+  color: #333;
   display: flex;
   font-size: 14px;
   align-items: center;
@@ -17,6 +15,7 @@
   opacity: 1;
   transform: translateX(-50%);
   white-space: nowrap;
+  pointer-events: none;
 
   .icon {
     font-size: 16px;

+ 2 - 2
src/components/base/components/message/index.js

@@ -31,7 +31,7 @@ Message.use = function use(app) {
         })
         indexs.value.push(instance)
 
-        return config
+        return hide
     }
 
     const existsShows = []
@@ -49,7 +49,7 @@ Message.use = function use(app) {
     }
 
     for (const type of types) {
-        Message[type] = (config, isOne = true) => {
+        Message[type] = (config, isOne = false) => {
             if (toRawType(config) === 'String') {
                 config = {
                     msg: config,

+ 1 - 1
src/components/base/components/message/message.vue

@@ -5,7 +5,7 @@
         class="ui-message" 
         :style="{ zIndex: zIndex, marginTop: `${index.value * 60}px` }" 
         :class="type" v-if="show">
-        <ui-icon :type="icons[type]" class="icon" v-if="type" />
+<!--        <ui-icon :type="icons[type]" class="icon" v-if="type" />-->
         <p>{{ msg }}</p>
 
         <ui-icon ctrl type="close" v-if="!time" @click="destroy" class="message-close" />

+ 1 - 1
src/components/button-pane/index.vue

@@ -14,7 +14,7 @@ const props = withDefaults(
 
 const style = computed(() => {
   const isRow = props.dire === 'row';
-  const bound = isRow ? { height: `${64}px` } : { width: `${64}px` }
+  const bound = isRow ? { height: `${props.size}px` } : { width: `${props.size}px` }
 
   return {
     padding: isRow ? `4px ${props.size / 2}px` : `${props.size / 2}px 4px`,

+ 13 - 2
src/components/group-button/index.vue

@@ -33,13 +33,22 @@ const props = withDefaults(
 
 const menuStyle = computed(() => {
   const offset = props.size / 4;
-  return props.dire === 'row' ? { marginRight: offset + "px" } : { marginBottom: offset + 'px' }
+  return props.dire === 'row'
+    ? {
+        padding: `0 10px`,
+        marginRight: '8px'
+      }
+    :
+      {
+        padding: `10px 0 `,
+        marginBottom: '8px'
+      }
 })
 </script>
 
 <style lang="scss" scoped>
 .menu {
-  width: 56px;
+  min-width: 56px;
   display: flex;
   flex-direction: column;
   cursor: pointer;
@@ -47,6 +56,7 @@ const menuStyle = computed(() => {
   text-align: center;
   transition: color .3s ease;
   color: #fff;
+  border-radius: 4px;
 
   &.active,
   &:hover {
@@ -68,6 +78,7 @@ const menuStyle = computed(() => {
   p {
     line-height: 17px;
     font-size: 12px;
+    white-space:nowrap;
   }
 }
 </style>

+ 1 - 2
src/hook/custom/index.ts

@@ -6,9 +6,8 @@ import { SDK } from '@/sdk'
 import { Router } from 'vue-router'
 import { RouteCustomConfig, setupRouteCustom, setupSDK } from './setup'
 
-export const customSetup = (sdk: SDK, router: Router, config: RouteCustomConfig) => {
+export const customSetup = (sdk: SDK) => {
     setupSDK(sdk)
-    setupRouteCustom(router, config)
 }
 
 export default customSetup

+ 2 - 0
src/hook/custom/setup.ts

@@ -89,9 +89,11 @@ export const setupSDK = (sdk: SDK) => {
   watchEffect(() => sdk.scene.changeMode(customMap[CustomCom.LaserMode]));
   watchEffect(() => sdk.carry.setShowHots(!disabledMap[DisabledCom.Hot]));
   watchEffect(() =>
+
     sdk.carry.setShowMeasures(!disabledMap[DisabledCom.Measure])
   );
 
+
   fullScreenSetting(sdk);
   spliceSetting(sdk);
   magnifierSetting(sdk);

+ 1 - 1
src/sdk/carry/inject/atom.vue

@@ -37,9 +37,9 @@ const updatePos = () => {
 }
 
 sdk.on('posChange', updatePos)
-sdk.isMap && sdk.on('mapZoomLevelChange', updatePos)
 watch(props, updatePos)
 updatePos()
+sdk.isMap && sdk.on('mapZoomLevelChange', updatePos)
 </script>
 
 <style lang="scss" scoped>

+ 7 - 6
src/sdk/carry/measures/index.vue

@@ -1,9 +1,9 @@
 <template>
   <MeasureItem
-    v-for="(item, i) in props.store.measure.list"
+    v-for="(item, i) in measure"
     :key="Math.random() * 100"
     :data="item"
-    :unit="props.store.measure.unit"
+    :unit="MeasureUnit.meter"
     :ref="canvas => (refs[i] = canvas)"
   />
 </template>
@@ -12,20 +12,21 @@
 import MeasureItem from './item.vue'
 import { propsKey, laserKey } from '../constant'
 import { inject, computed, ref, watch } from 'vue'
+import {MeasureUnit} from "@/sdk";
 
 const props = inject(propsKey)
 const laser = inject(laserKey)
 const refs = ref([])
 const measureMap = props.measureMap
 
+const measure = computed(() => [...props.store.measure.list, ...props.store.baseLine.baseLines])
+
 watch(
   refs,
   () => {
-    const list = props.store.measure.list
-    for (let i = 0; i < list.length; i++) {
+    for (let i = 0; i < measure.value.length; i++) {
       const vm = refs.value[i]
-
-      vm && measureMap.set(list[i], vm.canvas)
+      vm && measureMap.set(measure.value[i], vm.canvas)
     }
   },
   { deep: true }

+ 1 - 1
src/sdk/carry/setup.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="carry-layer" @click.right.stop.prevent>
     <Hots />
-    <Measures v-if="props.showMeasures && sdk.isScene" />
+    <Measures v-if="props.showMeasures" />
     <InjectComponent />
   </div>
 </template>

+ 4 - 0
src/sdk/types/store.ts

@@ -16,6 +16,10 @@ export type Store = {
     measure?: {
         list: MeasuresRaw
         unit: MeasureUnit
+    },
+    baseLine?: {
+        baseLines: MeasuresRaw
+        unit: MeasureUnit
     }
     navigation: NavigationAtom
     showNavpanel?: Ref<boolean>

+ 10 - 0
src/store/baseLine.ts

@@ -0,0 +1,10 @@
+import { MeasuresRaw } from "@/sdk/types/measure";
+import { Ref, ref } from "vue";
+
+export type Measures = Ref<MeasuresRaw>;
+
+export const baseLines: Measures = ref([]);
+
+export default {
+  baseLines
+}

+ 12 - 0
src/store/basePoint.ts

@@ -0,0 +1,12 @@
+import {ref} from "vue";
+
+export type BasePoint = {
+  id: string
+  pos: {
+    x: number,
+    y: number,
+    z: number
+  }
+}
+
+export const basePoints = ref<BasePoint[]>([])

+ 12 - 0
src/store/fixPoint.ts

@@ -0,0 +1,12 @@
+import {ref} from "vue";
+
+export type FixPoint = {
+  id: string
+  pos: {
+    x: number,
+    y: number,
+    z: number
+  }
+}
+
+export const fixPoints = ref<FixPoint[]>([])

+ 2 - 1
src/store/index.ts

@@ -3,12 +3,13 @@ import measure from "./measure";
 import setup from "./setup";
 import setting from "./setting";
 import { DataSetAtom } from "@/sdk";
-
+import baseLine from "@/store/baseLine";
 export const store = {
   hot,
   measure,
   setup,
   setting,
+  baseLine
 };
 
 export * from "./sys";

+ 3 - 2
src/utils/menus.ts

@@ -44,7 +44,8 @@ export const generateMixMenus = <
   childKey: K,
   generateFn: (men: T) => R,
   mainMenus: T[],
-  itemAction: (men: MenuRaw) => void
+  itemAction: (men: T) => void,
+  getActiveItem: () => any
 ): GenerateMinMenusResult<T[K], R> => {
   const child = ref();
   const menus = generateByMenus((menu) => ({
@@ -65,7 +66,7 @@ export const generateMixMenus = <
   const activeMenuKey = computed(() =>
     child.value
       ? findMenuByAttr(child.value, childKey, mainMenus)?.key
-      : uiType.current
+      : getActiveItem()
   )
 
   return {

+ 3 - 2
src/views/graphic/menus.ts

@@ -153,7 +153,7 @@ export const generateByMenus = <T extends MenuRaw>(
 export const findMainMenuByAttr = (
   value: MenusRaw,
   attr = "extend" as const,
-  mainMenus = mainMenusRaw
+  mainMenus = mainMenusRaw,
 ) => findMenuByAttr(value, attr, mainMenus)
 
 export const generateMixMenus = <T extends {}, K extends keyof MenuRaw>(
@@ -164,5 +164,6 @@ export const generateMixMenus = <T extends {}, K extends keyof MenuRaw>(
   childKey,
   generateFn,
   mainMenus,
-    menu => uiType.change(menu.key as any)
+    menu => uiType.change(menu.key as any),
+  () => uiType.current
 );

+ 2 - 2
src/views/graphic/vectorMenus.vue

@@ -8,7 +8,7 @@
   />
   <ui-graphic-vector-menus.vue
     v-if="store.child.value"
-    :menus="store.child.value"
+    :menus="store.child.value as MenusRaw"
     :level="level + 1"
   />
 </template>
@@ -23,7 +23,7 @@ const props = withDefaults(
   { level: 1 }
 )
 const store = computed(() => generateMixMenus(
-  "children",
+  "children" as const,
   m => m,
   props.menus
 ))

+ 1 - 1
src/views/measure/constant.ts

@@ -135,8 +135,8 @@ export const ctrolGroup = reactive([
   },
 ]) as Group<CtrlAtom>;
 
-export type CanvasMeasureAtom = { raw: MeasureAtom; canvas: Measure };
 export type CanvasMeasures = Array<CanvasMeasureAtom>;
+export type CanvasMeasureAtom = { raw: MeasureAtom; canvas: Measure };
 export type TempCanvasMeasureAtom = Omit<CanvasMeasureAtom, "canvas"> & {
   canvas: StartMeasure;
 };

+ 53 - 0
src/views/scene/covers/actions.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="action-layout">
+    <ButtonPane
+        v-for="menu in menus"
+        :key="menu.key"
+        @click="menu.action"
+        class="action"
+        :style="{backgroundColor: menu.color}">
+      <UiIcon :type="menu.icon" class="icon" :style="{color: menu.iconColor}"/>
+    </ButtonPane>
+  </div>
+</template>
+<script lang="ts" setup>
+import ButtonPane from '@/components/button-pane/index.vue'
+import UiIcon from "@/components/base/components/icon/index.vue";
+import {backgroundColor} from "html2canvas/dist/types/css/property-descriptors/background-color";
+
+type ActionItem = {
+  key: string
+  icon: string,
+  iconColor: string,
+  color: string,
+  action: () => void
+}
+
+defineProps<{ menus: ActionItem[] }>()
+</script>
+
+<style lang="scss" scoped>
+.action-layout {
+  position: absolute;
+  bottom: var(--boundMargin);
+  left: 50%;
+  transform: translateX(-50%);
+  display: flex;
+
+  .action {
+    position: static;
+    cursor: pointer;
+
+    &:not(:last-child) {
+      margin-right: 24px;
+    }
+  }
+
+  .icon {
+    font-size: 20px;
+    color: #fff;
+    position: absolute;
+    transform: translateX(-50%);
+  }
+}
+</style>

+ 31 - 0
src/views/scene/covers/basePoint.vue

@@ -0,0 +1,31 @@
+<template>
+  <Cover
+      @change-pos="pos => $emit('changePos', pos)"
+      :pos="pos"
+      @focus="$emit('focus')"
+      @blur="$emit('blur')"
+  >
+    <img :src="icon" class="label" />
+  </Cover>
+
+</template>
+
+<script setup lang="ts">
+import Cover from './cover.vue'
+import icon from './point1.png'
+import {Pos3D} from '@/sdk'
+import {watchEffect} from "vue";
+
+const props = defineProps<{ pos: Pos3D }>()
+defineEmits<{
+  (m: 'changePos', pos: Pos3D): void,
+  (m: 'focus'): void,
+  (m: 'blur'): void
+}>()
+</script>
+
+<style scoped lang="scss">
+.label {
+  width: 32px;
+}
+</style>

+ 36 - 0
src/views/scene/covers/basePoints.vue

@@ -0,0 +1,36 @@
+<template>
+  <BasePoint
+    v-for="point in basePoints"
+    :key="point.id"
+    :pos="point.pos"
+    @change-pos="pos => point.pos = pos"
+    @blur="() => active = active === point ? null : active"
+    @focus="() => active = point"
+  />
+
+  <ActionsPanel :menus="activeActionMenus" v-if="active" />
+</template>
+
+<script setup lang="ts">
+import { basePoints } from '@/store/basePoint'
+import BasePoint from './basePoint.vue'
+import {ref} from "vue";
+import {FixPoint} from "@/store/fixPoint";
+import ActionsPanel from "@/views/scene/covers/actions.vue";
+
+const active = ref<FixPoint>()
+const activeActionMenus = [
+  {
+    key: "delete",
+    icon: "del",
+    color: "#FF4D4F",
+    iconColor: "#fff",
+    action() {
+      const index = basePoints.value.indexOf(active.value)
+      if (~index) {
+        basePoints.value.splice(index, 1)
+      }
+    }
+  }
+]
+</script>

+ 101 - 0
src/views/scene/covers/cover.vue

@@ -0,0 +1,101 @@
+<template>
+  <div
+      class="cover-layout"
+      @click.stop.prevent="clickHandler"
+      @mousedown.stop.prevent="downHandler"
+      :class="{ move: move }"
+      :style="style"
+  >
+    <slot />
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
+import {Pos, Pos3D} from '@/sdk'
+import {useSDK} from '@/hook'
+
+const props = defineProps<{ pos: Pos3D }>()
+const emit = defineEmits<{
+  (m: 'changePos', pos: Pos3D): void
+  (m: 'focus'): void
+  (m: 'blur'): void
+}>()
+const sdk = useSDK()
+
+const screen = ref<Pos>(null)
+const style = computed(
+  () =>
+    screen.value && {
+      left: screen.value.x + 'px',
+      top: screen.value.y + 'px'
+    }
+)
+const updatePos = () => {
+  if (props.pos) {
+    const data = sdk.scene.getScreenByPoint(props.pos)
+    screen.value = data.trueSide ? data.pos : null
+  }
+}
+
+sdk.scene.on('posChange', updatePos)
+watch(props, updatePos)
+updatePos()
+
+const move = ref(false)
+const downHandler = (sev: MouseEvent) => {
+  const el = sev.target as HTMLElement
+  const mountEl = document.documentElement
+  const preset = {
+    x: el.offsetWidth / 2 - sev.offsetX,
+    y: el.offsetHeight - sev.offsetY
+  }
+
+  const moveHandler = (ev: MouseEvent) => {
+    move.value = true
+    const pos = sdk.scene.getPointByScreen({
+      x: ev.pageX + preset.x,
+      y: ev.pageY + preset.y,
+      inDrag: true
+    })
+    if (pos.position) {
+      emit('changePos', pos.position);
+    }
+  }
+  const upHandler = (ev: MouseEvent) => {
+    mountEl.removeEventListener('mousemove', moveHandler)
+    mountEl.removeEventListener('mouseup', upHandler)
+    move.value = false
+  }
+
+  mountEl.addEventListener('mousemove', moveHandler)
+  mountEl.addEventListener('mouseup', upHandler)
+}
+
+const clickHandler = ev => {
+  emit("focus")
+
+  const handler = () => {
+    emit("blur")
+    document.documentElement.removeEventListener("click", handler)
+  }
+  document.documentElement.addEventListener("click", handler, { passive: true })
+}
+
+onUnmounted(() => {
+  emit("blur")
+})
+</script>
+
+<style scoped lang="scss">
+.cover-layout {
+  position: absolute;
+  z-index: 999;
+  transform: translate(-50%, -100%);
+  cursor: move;
+}
+
+.move {
+  pointer-events: none;
+}
+</style>

+ 30 - 0
src/views/scene/covers/fixPoint.vue

@@ -0,0 +1,30 @@
+<template>
+  <Cover
+      @change-pos="pos => $emit('changePos', pos)"
+      :pos="pos"
+      @focus="$emit('focus')"
+      @blur="$emit('blur')"
+  >
+    <img :src="icon" class="label" />
+  </Cover>
+
+</template>
+
+<script setup lang="ts">
+import Cover from './cover.vue'
+import icon from './point1.png'
+import {Pos3D} from '@/sdk'
+
+const props = defineProps<{ pos: Pos3D }>()
+defineEmits<{
+  (m: 'changePos', pos: Pos3D): void,
+  (m: 'focus'): void,
+  (m: 'blur'): void,
+}>()
+</script>
+
+<style scoped lang="scss">
+.label {
+  width: 32px;
+}
+</style>

+ 44 - 0
src/views/scene/covers/fixPoints.vue

@@ -0,0 +1,44 @@
+<template>
+  <FixPointPanel
+    v-for="point in fixPoints"
+    :key="point.id"
+    :pos="point.pos"
+    @change-pos="pos => point.pos = pos"
+    @blur="() => active = active === point ? null : active"
+    @focus="() => active = point"
+  />
+
+  <ActionsPanel :menus="activeActionMenus" v-if="active" />
+</template>
+
+<script setup lang="ts">
+import { fixPoints, FixPoint } from '@/store/fixPoint'
+import FixPointPanel from './fixPoint.vue'
+import ActionsPanel from './actions.vue'
+import {ref} from "vue";
+
+const active = ref<FixPoint>()
+const activeActionMenus = [
+  {
+    key: "edit",
+    icon: "edit",
+    color: "#161A1A",
+    iconColor: "#2F8FFF",
+    action() {
+
+    }
+  },
+  {
+    key: "delete",
+    icon: "del",
+    color: "#FF4D4F",
+    iconColor: "#fff",
+    action() {
+      const index = fixPoints.value.indexOf(active.value)
+      if (~index) {
+        fixPoints.value.splice(index, 1)
+      }
+    }
+  }
+]
+</script>

+ 34 - 0
src/views/scene/covers/measure.vue

@@ -0,0 +1,34 @@
+<template></template>
+
+<script setup lang="ts">
+import {useSDK} from "@/hook";
+import { MeasureAtom } from '@/store/measure'
+import {computed, watchEffect} from "vue";
+
+const props = defineProps<{ data: MeasureAtom }>()
+const emit = defineEmits<{
+  (m: 'changePoints', data: MeasureAtom['points']): void,
+  (m: 'focus'): void,
+  (m: 'blur'): void
+}>()
+
+const sdk = useSDK()
+const map = sdk.carry.measureMap
+const measure = computed(() => map.get(props.data))
+
+watchEffect(() => {
+  if (measure.value) {
+    measure.value.bus.on("highlight", (focus) => {
+      if (focus) {
+        emit('focus')
+      } else {
+        emit('blur')
+      }
+    })
+
+    measure.value.bus.on("update", () => {
+      emit("changePoints", measure.value.getPoints())
+    })
+  }
+})
+</script>

+ 35 - 0
src/views/scene/covers/measures.vue

@@ -0,0 +1,35 @@
+<template>
+  <Measure
+      v-for="measure in list"
+      :key="measure.id"
+      :data="measure"
+      @change-points="points => measure.points = points"
+      @blur="() => active = active === measure ? null : active"
+      @focus="() => active = measure"
+  />
+
+  <ActionsPanel :menus="activeActionMenus" v-if="active" />
+</template>
+
+<script setup lang="ts">
+import { list, MeasureAtom } from '@/store/measure'
+import Measure from './measure.vue'
+import {ref} from "vue";
+import ActionsPanel from "@/views/scene/covers/actions.vue";
+
+const active = ref<MeasureAtom>()
+const activeActionMenus = [
+  {
+    key: "delete",
+    icon: "del",
+    color: "#FF4D4F",
+    iconColor: "#fff",
+    action() {
+      const index = list.value.indexOf(active.value)
+      if (~index) {
+        list.value.splice(index, 1)
+      }
+    }
+  }
+]
+</script>

BIN
src/views/scene/covers/point1.png


+ 37 - 5
src/views/scene/index.vue

@@ -1,16 +1,48 @@
 <template>
   <Container />
   <Mode />
+  <Menus />
+  <BasePoints />
+  <FixPoints />
+  <Measures />
+  <Photo />
+
+  <ButtonPane class="back fun-ctrl" size="48">
+    <ui-icon type="close" class="icon" />
+  </ButtonPane>
 </template>
 
 <script lang="ts" setup>
 import Container from "./container.vue"
 import Mode from './mode.vue'
-import {watchEffect} from "vue";
-import {useAsyncSDK, CustomCom, customMap} from "@/hook";
+import Menus from './menus/pane.vue'
+import BasePoints from "@/views/scene/covers/basePoints.vue";
+import FixPoints from "@/views/scene/covers/fixPoints.vue";
+import Measures from "@/views/scene/covers/measures.vue";
+import Photo from './photo.vue'
+import ButtonPane from '@/components/button-pane'
+import {useAsyncSDK, disabledMap} from "@/hook";
+import customSetup from "../../hook/custom";
+import UiIcon from "@/components/base/components/icon/index.vue";
 
-useAsyncSDK().then(sdk => {
-  watchEffect(() => sdk.scene.changeMode(customMap[CustomCom.LaserMode]));
 
+useAsyncSDK().then(sdk => {
+  customSetup(sdk)
+  disabledMap.measure = false
 })
-</script>
+</script>
+
+<style scoped lang="scss">
+.back {
+  left: var(--boundMargin);
+  top: var(--boundMargin);
+  color: #fff;
+  font-size: 20px;
+  transition: color .3s ease;
+
+  .icon {
+    position: absolute;
+    transform: translateX(-50%);
+  }
+}
+</style>

+ 24 - 0
src/views/scene/linkage/cover.ts

@@ -0,0 +1,24 @@
+import {Component, Ref, watchEffect} from "vue";
+import {useSDK} from "@/hook";
+import {CoordType, Pos, Pos3D} from "@/sdk";
+
+
+export const getCoverPos = (onComplete: (pos: Pos3D) => void) => {
+  const sdk = useSDK();
+  const canvas = sdk.scene.el
+  const layout = canvas.parentElement
+
+  const downHandler = ev => {
+    const screenPos = { x: ev.offsetX, y: ev.offsetY }
+    const pos = sdk.coordTransform(CoordType.SCENE_SCREEN, screenPos, CoordType.LOCAL)
+    onComplete(pos as Pos3D)
+  }
+  const upHandler = () => {
+    canvas.removeEventListener("mousedown", downHandler)
+    canvas.removeEventListener("mouseup", upHandler)
+  }
+  canvas.addEventListener("mousedown", downHandler)
+  canvas.addEventListener("mouseup", upHandler)
+
+  return upHandler
+}

+ 91 - 0
src/views/scene/linkage/measure.ts

@@ -0,0 +1,91 @@
+import {useSDK} from "@/hook";
+import {MeasureAtom, MeasureType, TemploraryID, unTemp} from "@/store";
+import {Measure, MeasureUnit, StartMeasure} from "@/sdk";
+import {Message} from "@/components/base";
+
+
+type CanvasMeasureAtom = { raw: MeasureAtom; canvas: Measure };
+type TempCanvasMeasureAtom = Omit<CanvasMeasureAtom, "canvas"> & {
+  canvas: StartMeasure;
+};
+
+
+let prevMeasure: TempCanvasMeasureAtom
+
+export const startMeasure = (type: MeasureType) => {
+  if (prevMeasure) {
+    prevMeasure.canvas.bus.emit("quit")
+    prevMeasure = null;
+  }
+
+  const laser = useSDK();
+  const measure = laser.scene.startMeasure(type, MeasureUnit.meter);
+  const raw = {
+    id: TemploraryID,
+    dataSet: unTemp,
+    type: type,
+    points: [],
+    show: true,
+    dataset_points: null,
+    datasetIds: null,
+  };
+
+  const currentMeasure: TempCanvasMeasureAtom = {
+    raw: raw,
+    canvas: measure,
+  };
+
+  return new Promise<MeasureAtom>(resolve => {
+    const endHandler = () => {
+      raw.points = currentMeasure.canvas.getPoints();
+      raw.dataset_points = currentMeasure.canvas.getDatasetLocations();
+      raw.datasetIds = currentMeasure.canvas.getDatasets();
+      raw.dataSet = unTemp;
+      endMeasure();
+      resolve(raw);
+    }
+    const quitHandler = () => {
+      currentMeasure.canvas.quit();
+      endMeasure()
+      resolve(null);
+    }
+    const invalidPointHandler = (msg = '点云为空,无法测量') => {
+      Message.warning(msg);
+    }
+    const endMeasure = () => {
+      currentMeasure.canvas.clear();
+      currentMeasure.canvas.bus.off("end", endHandler);
+      currentMeasure.canvas.bus.off("quit", quitHandler);
+      currentMeasure.canvas.bus.off("invalidPoint", invalidPointHandler);
+      if (prevMeasure === currentMeasure) {
+        prevMeasure = null;
+      }
+    }
+
+    currentMeasure.canvas.bus.on("quit", quitHandler);
+    currentMeasure.canvas.bus.on("end", endHandler);
+    currentMeasure.canvas.bus.on("invalidPoint", invalidPointHandler);
+    prevMeasure = currentMeasure
+  })
+}
+
+export const continuedMeasure = (
+  type: MeasureType,
+  onAddMeasure: (data: MeasureAtom) => void
+) => {
+
+  startMeasure(type)
+    .then(data => {
+      if (data) {
+        onAddMeasure(data);
+        continuedMeasure(type, onAddMeasure)
+      }
+    })
+}
+
+export const stopMeasure = () => {
+  if (prevMeasure) {
+    prevMeasure.canvas.bus.emit("quit")
+    prevMeasure = null;
+  }
+}

+ 0 - 0
src/views/scene/menus.ts


+ 140 - 0
src/views/scene/menus/actions.ts

@@ -0,0 +1,140 @@
+import {findMenuByKey, menuEnum, MenuRaw} from "@/views/scene/menus/menus";
+import {continuedMeasure, startMeasure, stopMeasure as stopMeasureRaw} from "@/views/scene/linkage/measure";
+import {list, MeasureAtom, MeasureType} from "@/store/measure";
+import {baseLines} from '@/store/baseLine'
+import {basePoints} from "@/store/basePoint";
+import {Ref, watch} from "vue";
+import {customMap} from "@/hook";
+import {getCoverPos} from '../linkage/cover'
+import {Pos3D} from "@/sdk";
+import {fixPoints} from "@/store/fixPoint";
+import Message from "@/components/base/components/message/message.vue";
+
+const trackPosMenuAction = (onComplete: () => void, onAddStore: (pos: Pos3D) => void) => {
+  customMap.magnifier = true
+  const onCleanup = getCoverPos(pos => {
+    onComplete()
+    onAddStore(pos)
+  })
+  return () => {
+    customMap.magnifier = false
+    onCleanup()
+  }
+}
+
+const stopMeasure = () => {
+  stopMeasureRaw()
+  customMap.magnifier = false
+}
+const trackMeasureMenuAction = (
+  measureType: MeasureType,
+  menu: MenuRaw,
+  onAddStore: (data: MeasureAtom) => void,
+  onComplete,
+  name: string
+) => {
+  const hide = Message.success({ msg: "请选择一个位置单击,确定基准线的起点" })
+  customMap.magnifier = true
+  const onAddMeasure = (data: MeasureAtom) => {
+    if (data) {
+      onAddStore(data)
+      onComplete()
+    }
+    hide()
+  }
+
+  if (menu.continued) {
+    continuedMeasure(measureType as any, onAddMeasure)
+  } else {
+    startMeasure(measureType as any).then(onAddMeasure)
+  }
+
+  return () => {
+    stopMeasure()
+  }
+}
+
+const menuActions = {
+  [menuEnum.BASE_POINT]: (_, onComplete) => {
+    const hide = Message.success({ msg: "请单击选择基准点位置" })
+    return trackPosMenuAction(
+      () => {
+        hide()
+        onComplete()
+      },
+      pos => basePoints.value.push({ id: "aa", pos })
+    )
+  },
+  [menuEnum.FIX_POINT]:  (_, onComplete) => {
+    const hide = Message.success({ msg: "请单击选择固定点位置" })
+    return trackPosMenuAction(
+      () => {
+        hide()
+        onComplete()
+      },
+      pos => fixPoints.value.push({ id: "aa", pos })
+    )
+  },
+  [menuEnum.MEASURE_ROW]: (menu, onComplete) => {
+    return trackMeasureMenuAction(
+      'L_LINE',
+      menu,
+      (data) => list.value.push(data),
+      onComplete,
+      "测量线"
+    )
+  },
+  [menuEnum.MEASURE_COLUMN]: (menu, onComplete) => {
+    return trackMeasureMenuAction(
+      'V_LINE',
+      menu,
+      (data) => list.value.push(data),
+      onComplete,
+      "测量线"
+    )
+  },
+  [menuEnum.MEASURE_FREE]: (menu, onComplete) => {
+    return trackMeasureMenuAction(
+      'LINE',
+      menu,
+      (data) => list.value.push(data),
+      onComplete,
+      "测量线"
+    )
+  },
+  [menuEnum.BASE_LINE]: (menu, onComplete) => {
+    return trackMeasureMenuAction(
+      'LINE',
+      menu,
+      (data) => baseLines.value.push(data),
+      onComplete,
+      "基准线"
+    )
+  },
+  [menuEnum.CLEAR]: (menu, onComplete) => {
+    list.value = []
+    baseLines.value = []
+    basePoints.value = []
+    fixPoints.value = []
+    onComplete()
+  },
+}
+
+export const joinActions = (activeKey: Ref<string>) => {
+  return watch(
+    () => activeKey.value,
+    (key, oldKey, onCleanup) => {
+      if (key) {
+        const menu = findMenuByKey(key as any)
+        const cleanup = menuActions[key](menu, () => {
+          if (!menu.continued) {
+            activeKey.value = null;
+          }
+        });
+        if (typeof cleanup === 'function') {
+          onCleanup(cleanup);
+        }
+      }
+    }
+  )
+}

+ 113 - 0
src/views/scene/menus/menus.ts

@@ -0,0 +1,113 @@
+import {findMenuByAttr, generateMixMenus as generateMixMenusRaw} from '@/utils/menus'
+import {ref} from "vue";
+
+export type MenuRaw = {
+  key: string,
+  text: string,
+  continued?: boolean
+  icon?: string,
+  children?: MenuRaw[],
+  onClick?: () => void
+}
+
+export enum menuEnum {
+  SAVE = 'save',
+  CLEAR = 'clear',
+  MEASURE_ROW = 'L_LINE',
+  MEASURE_COLUMN = 'V_LINE',
+  MEASURE_FREE = 'LINE',
+  FIX_POINT = 'fixPoint',
+  BASE_LINE = 'baseLine',
+  BASE_POINT = 'basePoint',
+}
+
+export const menus: MenuRaw[] = [
+  {
+    icon: "",
+    text: "保存",
+    key: menuEnum.SAVE
+  },
+  {
+    icon: "",
+    text: "清除",
+    key: menuEnum.CLEAR
+  },
+  {
+    icon: "",
+    text: "测量",
+    key: "measure",
+    children: [
+      {
+        icon: "",
+        // continued: true,
+        text: "水平",
+        key: menuEnum.MEASURE_ROW
+      },
+      {
+        icon: "",
+        continued: true,
+        text: "垂直",
+        key: menuEnum.MEASURE_COLUMN
+      },
+      {
+        icon: "",
+        continued: true,
+        text: "自由",
+        key: menuEnum.MEASURE_FREE
+      }
+    ]
+  },
+  {
+    icon: "",
+    text: "固定点",
+    key: "fixPointMenu",
+    children: [
+      {
+        icon: "",
+        text: "固定点",
+        key: menuEnum.FIX_POINT
+      },
+    ]
+  },
+  {
+    icon: "",
+    text: "基准线/点",
+    key: "baseLineOrPoint",
+    children: [
+      {
+        icon: "",
+        text: "基准线",
+        key: menuEnum.BASE_LINE
+      },
+      {
+        icon: "",
+        text: "基准点",
+        key: menuEnum.BASE_POINT
+      },
+    ]
+  }
+]
+
+export const generateMixMenus = <T extends {}, K extends keyof MenuRaw>(
+  childKey: K,
+  generateFn: (men: MenuRaw) => T,
+  mainMenus: MenuRaw[] = menus
+) => {
+  const itemActiveKey = ref("")
+  return {
+    ...generateMixMenusRaw(
+        childKey,
+        generateFn,
+        mainMenus,
+        menu => {
+          menu.onClick && menu.onClick()
+          itemActiveKey.value = menu.key
+        },
+        () => itemActiveKey.value
+      ),
+    itemActiveKey
+  }
+}
+
+export const findMenuByKey = (value: menuEnum) =>
+  findMenuByAttr(value, 'key', menus)

+ 54 - 0
src/views/scene/menus/pane.vue

@@ -0,0 +1,54 @@
+<template>
+  <ActionMenus
+      v-if="!store.child.value"
+      class="menus"
+      :menus="store.menus as any"
+      :active-key="store.activeMenuKey.value"
+      dire="column"
+  />
+  <scene-menus
+      v-if="store.child.value"
+      @back="store.child.value = null"
+      :menus="store.child.value as MenuRaw[]"
+      :level="level + 1"
+  />
+</template>
+
+<script lang="ts" setup>
+import ActionMenus from "@/components/group-button/index.vue";
+import {generateMixMenus, MenuRaw, menus} from './menus'
+import {joinActions} from './actions'
+import {computed, onUnmounted} from "vue";
+
+const props = withDefaults(
+  defineProps<{ menus?: MenuRaw[], level?: number }>(),
+  {level: 1}
+)
+const emit = defineEmits<{ (e: 'back'): void }>();
+const backMenu = {
+  icon: "",
+  text: "返回",
+  key: "back",
+  onClick: () => emit("back")
+}
+
+const menusMix = computed(() =>
+  props.level === 1 ? menus : [backMenu, ...props.menus]
+)
+const store = generateMixMenus(
+  'children',
+  m => m,
+  menusMix.value
+)
+onUnmounted(joinActions(store.itemActiveKey));
+</script>
+<script lang="ts"> export default {name: 'scene-menus'}</script>
+
+<style lang="scss" scoped>
+.menus {
+  position: absolute;
+  left: var(--boundMargin);
+  top: 50%;
+  transform: translateY(-50%);
+}
+</style>

+ 0 - 1
src/views/scene/mode.vue

@@ -35,7 +35,6 @@ const menus = computed(() =>
 )
 
 watchEffect(() => {
-  console.log(activeKey.value)
   customMap.mode = activeKey.value
 })
 </script>

+ 113 - 0
src/views/scene/photo.vue

@@ -0,0 +1,113 @@
+<template>
+  <img :src="tempPhoto" class="face-animation" v-if="tempPhoto" ref="coverRef">
+
+  <div class="photo-layout">
+    <ButtonPane class="photo-btn fun-ctrl" size="80" @click="photo">
+      <ui-icon type="close" class="icon" />
+    </ButtonPane>
+
+    <img src="/static/t.png" class="cover">
+  </div>
+</template>
+
+<script setup lang="ts">
+import UiIcon from "@/components/base/components/icon/index.vue";
+import ButtonPane from "@/components/button-pane/index.vue";
+import { useSDK } from '@/hook/useLaser'
+import {genUseLoading} from "@/hook";
+import {base64ToBlob} from "@/utils";
+import {nextTick, ref} from "vue";
+
+const tempPhoto = ref<string>();
+const coverRef = ref<HTMLImageElement>()
+const photo = genUseLoading(async () => {
+  const sdk = useSDK()
+  const dom = sdk.scene.el
+  const {dataUrl: base64} = await sdk.scene.screenshot(dom.offsetWidth, dom.offsetHeight)
+  const blob = base64ToBlob(base64)
+  tempPhoto.value = URL.createObjectURL(blob)
+  await nextTick();
+  const handler = () => {
+    coverRef.value.removeEventListener("animationend", handler)
+  }
+  coverRef.value.addEventListener("animationend", handler)
+})
+</script>
+
+<style scoped lang="scss">
+.photo-layout {
+  position: absolute;
+  right: var(--boundMargin);
+  top: 50%;
+  transform: translateY(-50%);
+  text-align: center;
+  color: #fff;
+  font-size: 20px;
+
+  .icon {
+    position: absolute;
+    transform: translateX(-50%);
+  }
+}
+
+.photo-btn {
+  position: static;
+  margin-bottom: 16px;
+}
+
+.cover {
+  width: 48px;
+  height: 48px;
+  border: 1px solid #fff;
+  object-fit: cover;
+  border-radius: 24px;
+  overflow: hidden;
+}
+
+.face-animation {
+  pointer-events: none;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  animation: 1s linear 1 both photo-face;
+
+}
+.face-animation.start {
+}
+
+@keyframes photo-face {
+  from {
+    left: 0;
+    top: 0;
+    width: 100vw;
+    height: 100vh;
+    margin-left: 0;
+    margin-top: 0;
+    border-radius: 0;
+  }
+  30%,
+  40%,
+  to {
+    left: 50vw;
+    top: 100px;
+    width: 48px;
+    height: 48px;
+    margin-left: -24px;
+    border-radius: 24px;
+    z-index: 2
+  }
+
+  70% {
+    left: calc(80vw - 60px);
+    top: calc(calc(50% - 48px) / 1.5);
+  }
+
+  to {
+    left: calc(100vw - 63px);
+    top: calc(calc(50%) + 20px);
+  }
+
+}
+</style>