Browse Source

测量编写ui

bill 3 years ago
parent
commit
56bb41d860
40 changed files with 1348 additions and 140 deletions
  1. 1 1
      src/api/fuse-model.ts
  2. 21 3
      src/app.vue
  3. 12 8
      src/components/actions/index.vue
  4. 7 0
      src/components/bill-ui/assets/scss/components/_message.scss
  5. 47 46
      src/components/bill-ui/components/floating/index.vue
  6. 95 3
      src/components/bill-ui/components/icon/iconfont/demo_index.html
  7. 19 3
      src/components/bill-ui/components/icon/iconfont/iconfont.css
  8. 1 1
      src/components/bill-ui/components/icon/iconfont/iconfont.js
  9. 28 0
      src/components/bill-ui/components/icon/iconfont/iconfont.json
  10. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.ttf
  11. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff
  12. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff2
  13. 16 2
      src/components/bill-ui/components/input/range.vue
  14. 1 0
      src/components/bill-ui/components/input/state.js
  15. 3 3
      src/components/bill-ui/components/message/index.js
  16. 3 1
      src/components/bill-ui/components/message/message.vue
  17. 124 0
      src/components/control-panl/ctrl.vue
  18. 27 0
      src/components/control-panl/index.ts
  19. 105 0
      src/components/control-panl/index.vue
  20. 200 0
      src/components/control-panl/style.scss
  21. 2 0
      src/env/index.ts
  22. 10 0
      src/hook/viewStack.ts
  23. 1 6
      src/layout/left-pano.vue
  24. 10 7
      src/layout/main.vue
  25. 2 9
      src/layout/right-fill-pano.vue
  26. 4 0
      src/layout/slide-menu.vue
  27. 17 0
      src/layout/switch.vue
  28. 35 12
      src/router/config.ts
  29. 23 8
      src/router/constant.ts
  30. 38 0
      src/router/index.ts
  31. 4 4
      src/sdk/association.ts
  32. 2 1
      src/store/index.ts
  33. 37 0
      src/store/measure.ts
  34. 3 0
      src/style.scss
  35. 36 0
      src/views/measure/edit.vue
  36. 87 0
      src/views/measure/index.vue
  37. 99 0
      src/views/measure/sign.vue
  38. 46 22
      src/views/merge/index.vue
  39. 30 0
      src/views/proportion/index.vue
  40. 152 0
      src/views/registration/index.vue

+ 1 - 1
src/api/fuse-model.ts

@@ -53,7 +53,7 @@ const serviceToLocal = (serviceModel: ServiceFuseModel): FuseModel => ({
   fusionNumId: serviceModel.fusionNumId,
   position: serviceModel.transform.position,
   rotation: serviceModel.transform.rotation,
-  id: serviceModel.fusionId.toString(),
+  id: serviceModel.fusionNumId.toString(),
   url: serviceModel.sceneData.type === SceneType.SWSS ? serviceModel.sceneData.num : serviceModel.sceneData.modelGlbUrl,
   title: serviceModel.sceneData.name || serviceModel.sceneData.sceneName || serviceModel.sceneData.modelTitle,
   modelId: serviceModel.sceneData.modelId,

+ 21 - 3
src/app.vue

@@ -3,9 +3,11 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue'
-import { loaded, error, initialStore } from '@/store'
-import { loadComponent, loadPack } from '@/utils'
+import { computed, ref, watch } from 'vue'
+import { loaded, error, initialStore, save, isOld, enterEdit } from '@/store'
+import { loadComponent, loadPack, togetherCallback } from '@/utils'
+import { router, currentMeta } from '@/router'
+import { showLeftPanoStack, showRightPanoStack } from '@/env'
 
 loadPack(initialStore)
 
@@ -17,4 +19,20 @@ const Component = computed(() => {
     return error.value ? Err : Main
   }
 })
+
+router.beforeEach(async (to, from, next) => {
+  if (to.params.save && isOld.value) {
+    await save()
+  }
+  next()
+})
+watch(currentMeta, (meta, _, onClean) => {
+  if (meta && 'full' in meta && meta.full) {
+    enterEdit(() => router.back())
+    onClean(togetherCallback([
+      showLeftPanoStack.push(ref(false)),
+      showRightPanoStack.push(ref(false)),
+    ]))
+  }
+}, { flush: 'post' })
 </script>

+ 12 - 8
src/components/actions/index.vue

@@ -13,26 +13,30 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, toRaw, watchEffect, onBeforeUnmount } from 'vue'
+import { ref, toRaw, watchEffect, onBeforeUnmount, nextTick } from 'vue'
 
-export type ActionsItem = { 
+export type ActionsItem<T = any> = { 
   icon: string, 
-  key?: string, 
+  key?: T, 
   text: string,
-  action: () => (() => void) | void
+  action?: () => (() => void) | void
 }
-export type ActionsProps = { items: ActionsItem[] }
-
-defineProps<ActionsProps>()
+export type ActionsProps = { items: ActionsItem[], current?: ActionsItem | null, single?: boolean, }
 
+const props = defineProps<ActionsProps>()
+const emit = defineEmits<{ (e: 'update:current', data: ActionsItem | null): void }>()
 const equal = (a: ActionsItem | null, b: ActionsItem | null) => toRaw(a) === toRaw(b)
 const selected = ref<ActionsItem | null>(null)
 const clickHandler = (select: ActionsItem) => {
   selected.value = equal(selected.value, select) ? null : select
+  emit('update:current', selected.value)
+  if (props.single) {
+    nextTick(() => selected.value && clickHandler(selected.value))
+  }
 }
 
 watchEffect((onCleanup) => {
-  if (selected.value) {
+  if (selected.value?.action) {
     const cleanup = selected.value.action()
     cleanup && onCleanup(cleanup)
   }

+ 7 - 0
src/components/bill-ui/assets/scss/components/_message.scss

@@ -18,11 +18,18 @@
   transform: translateX(-50%);
   white-space: nowrap;
 
+
   .icon {
     font-size: 16px;
     margin-right: 10px;
   }
 
+  .message-close {
+    font-size: 12px;
+    margin-left: 10px;
+    margin-right: 0;
+  }
+  
   &.success .icon {
     color: #43c665;
   }

+ 47 - 46
src/components/bill-ui/components/floating/index.vue

@@ -37,6 +37,53 @@ const props = defineProps({
 const emit = defineEmits(['leave', 'enter', 'mouseenter', 'mouseleave'])
 const vmRef = ref()
 
+const updateLocation = () => {
+    const pos = getPostionByTarget(props.refer, props.mount, false, props.isTransform)
+    let screenInfo
+    if (!props.isTransform) {
+        screenInfo = scrollParents.value.reduce(
+            (t, c) => {
+                t.y += c.scrollTop
+                t.x += c.scrollLeft
+                return t
+            },
+            { x: 0, y: 0 }
+        )
+    } else {
+        screenInfo = {x: 0, y: 0}
+    }
+
+    const [horizontal, vertical] = dires.value
+    const start = {
+        x: pos.x - screenInfo.x,
+        y: pos.y - screenInfo.y,
+    }
+
+    switch (horizontal) {
+        case Horizontal.left:
+            location.x = start.x
+            break
+        case Horizontal.right:
+            location.x = start.x + pos.width
+            break
+        case Horizontal.center:
+            location.x = start.x + pos.width / 2
+            break
+    }
+
+    switch (vertical) {
+        case Vertical.top:
+            location.y = start.y
+            break
+        case Vertical.bottom:
+            location.y = start.y + pos.height
+            break
+        case Vertical.center:
+            location.y = start.y + pos.height / 2
+            break
+    }
+}
+
 // 确定方向
 const dires = computed(() => {
     const dire = props.dire || `${Vertical.bottom}${Divide}${Horizontal.left}`
@@ -93,52 +140,6 @@ const style = computed(() => ({
     zIndex: zIndex,
 }))
 
-const updateLocation = () => {
-    const pos = getPostionByTarget(props.refer, props.mount, false, props.isTransform)
-    let screenInfo
-    if (!props.isTransform) {
-        screenInfo = scrollParents.value.reduce(
-            (t, c) => {
-                t.y += c.scrollTop
-                t.x += c.scrollLeft
-                return t
-            },
-            { x: 0, y: 0 }
-        )
-    } else {
-        screenInfo = {x: 0, y: 0}
-    }
-
-    const [horizontal, vertical] = dires.value
-    const start = {
-        x: pos.x - screenInfo.x,
-        y: pos.y - screenInfo.y,
-    }
-
-    switch (horizontal) {
-        case Horizontal.left:
-            location.x = start.x
-            break
-        case Horizontal.right:
-            location.x = start.x + pos.width
-            break
-        case Horizontal.center:
-            location.x = start.x + pos.width / 2
-            break
-    }
-
-    switch (vertical) {
-        case Vertical.top:
-            location.y = start.y
-            break
-        case Vertical.bottom:
-            location.y = start.y + pos.height
-            break
-        case Vertical.center:
-            location.y = start.y + pos.height / 2
-            break
-    }
-}
 
 const inSelf = ev => {
     return (props.refer && props.refer.contains(ev.target)) || (vmRef.value && vmRef.value.contains(ev.target))

+ 95 - 3
src/components/bill-ui/components/icon/iconfont/demo_index.html

@@ -55,6 +55,30 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe64a;</span>
+                <div class="name">nav-measure</div>
+                <div class="code-name">&amp;#xe64a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe66f;</span>
+                <div class="name">v-l</div>
+                <div class="code-name">&amp;#xe66f;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe670;</span>
+                <div class="name">h-r</div>
+                <div class="code-name">&amp;#xe670;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe673;</span>
+                <div class="name">f-l</div>
+                <div class="code-name">&amp;#xe673;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe64c;</span>
                 <div class="name">search</div>
                 <div class="code-name">&amp;#xe64c;</div>
@@ -264,9 +288,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1660727466034') format('woff2'),
-       url('iconfont.woff?t=1660727466034') format('woff'),
-       url('iconfont.ttf?t=1660727466034') format('truetype');
+  src: url('iconfont.woff2?t=1660900397303') format('woff2'),
+       url('iconfont.woff?t=1660900397303') format('woff'),
+       url('iconfont.ttf?t=1660900397303') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -293,6 +317,42 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-nav-measure"></span>
+            <div class="name">
+              nav-measure
+            </div>
+            <div class="code-name">.icon-nav-measure
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-v-l"></span>
+            <div class="name">
+              v-l
+            </div>
+            <div class="code-name">.icon-v-l
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-h-r"></span>
+            <div class="name">
+              h-r
+            </div>
+            <div class="code-name">.icon-h-r
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-f-l"></span>
+            <div class="name">
+              f-l
+            </div>
+            <div class="code-name">.icon-f-l
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-search"></span>
             <div class="name">
               search
@@ -609,6 +669,38 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-nav-measure"></use>
+                </svg>
+                <div class="name">nav-measure</div>
+                <div class="code-name">#icon-nav-measure</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-v-l"></use>
+                </svg>
+                <div class="name">v-l</div>
+                <div class="code-name">#icon-v-l</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-h-r"></use>
+                </svg>
+                <div class="name">h-r</div>
+                <div class="code-name">#icon-h-r</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-f-l"></use>
+                </svg>
+                <div class="name">f-l</div>
+                <div class="code-name">#icon-f-l</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-search"></use>
                 </svg>
                 <div class="name">search</div>

+ 19 - 3
src/components/bill-ui/components/icon/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 3549513 */
-  src: url('iconfont.woff2?t=1660727466034') format('woff2'),
-       url('iconfont.woff?t=1660727466034') format('woff'),
-       url('iconfont.ttf?t=1660727466034') format('truetype');
+  src: url('iconfont.woff2?t=1660900397303') format('woff2'),
+       url('iconfont.woff?t=1660900397303') format('woff'),
+       url('iconfont.ttf?t=1660900397303') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,22 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-nav-measure:before {
+  content: "\e64a";
+}
+
+.icon-v-l:before {
+  content: "\e66f";
+}
+
+.icon-h-r:before {
+  content: "\e670";
+}
+
+.icon-f-l:before {
+  content: "\e673";
+}
+
 .icon-search:before {
   content: "\e64c";
 }

File diff suppressed because it is too large
+ 1 - 1
src/components/bill-ui/components/icon/iconfont/iconfont.js


+ 28 - 0
src/components/bill-ui/components/icon/iconfont/iconfont.json

@@ -6,6 +6,34 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "25631400",
+      "name": "nav-measure",
+      "font_class": "nav-measure",
+      "unicode": "e64a",
+      "unicode_decimal": 58954
+    },
+    {
+      "icon_id": "26077352",
+      "name": "v-l",
+      "font_class": "v-l",
+      "unicode": "e66f",
+      "unicode_decimal": 58991
+    },
+    {
+      "icon_id": "26077353",
+      "name": "h-r",
+      "font_class": "h-r",
+      "unicode": "e670",
+      "unicode_decimal": 58992
+    },
+    {
+      "icon_id": "26077356",
+      "name": "f-l",
+      "font_class": "f-l",
+      "unicode": "e673",
+      "unicode_decimal": 58995
+    },
+    {
       "icon_id": "25631464",
       "name": "search",
       "font_class": "search",

BIN
src/components/bill-ui/components/icon/iconfont/iconfont.ttf


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff2


+ 16 - 2
src/components/bill-ui/components/input/range.vue

@@ -81,13 +81,27 @@ const parent = document.documentElement
 const slideDownHandler = ev => {
     ev.preventDefault()
     const moveStartX = ev.clientX || ev.touches[0].clientX
+    const moveStartY = ev.clientY || ev.touches[0].clientY
     const startPercen = percen.value
     mode.value = modeEmun.slide
 
     const moveHandler = ev => {
         ev.preventDefault()
-        const moveX = (ev.clientX || ev.touches[0].clientX) - moveStartX
-        const readyPercen = startPercen + moveX / locusWidth.value
+        const moveCurrentX = ev.clientX || ev.touches[0].clientX
+        const moveCurrentY = ev.clientY || ev.touches[0].clientY
+        const moveX = moveCurrentX - moveStartX
+
+        let readyPercen
+        if (props.moveCallback) {
+            readyPercen = props.moveCallback(
+                {x: moveStartX, y: moveStartY},
+                {x: moveCurrentX, y: moveCurrentY},
+                {start: startPercen, locusWidth: locusWidth.value}
+            )
+        } else {
+            readyPercen = startPercen + moveX / locusWidth.value
+        }
+
 
         percen.value = readyPercen < 0 ? 0 : readyPercen > 1 ? 1 : readyPercen
     }

+ 1 - 0
src/components/bill-ui/components/input/state.js

@@ -184,6 +184,7 @@ export const rangePropsDesc = {
     min: { ...numberPropsDesc.min, require: true },
     min: { ...numberPropsDesc.min, require: true },
     input: { type: Boolean, default: true },
+    moveCallback: { type: Function, require: false }
 }
 
 const summary = {

+ 3 - 3
src/components/bill-ui/components/message/index.js

@@ -12,8 +12,7 @@ Message.use = function use(app) {
             config = { msg: config }
         }
 
-        config.time = config.time || 3000
-        config.type = types.includes(config.type) ? config.type : types[0]
+        config.type = types.includes(config.type) ? config.type : undefined
 
         const instance = ref(null)
         const index = computed(() => (instance.value ? indexs.value.indexOf(instance) : 0))
@@ -32,12 +31,13 @@ Message.use = function use(app) {
         })
         indexs.value.push(instance)
 
-        return config
+        return hide
     }
 
     const existsShows = []
     const oneShow = config => {
         const key = config.type + config.msg
+        console.log(existsShows)
         if (!existsShows.includes(key)) {
             const index = existsShows.length
             existsShows[index] = key

+ 3 - 1
src/components/bill-ui/components/message/message.vue

@@ -5,8 +5,10 @@
         class="ui-message" 
         :style="{ zIndex: zIndex, marginTop: `${index.value * 60}px` }" 
         :class="type" v-if="show">
-        <ui-icon :type="icons[type]" class="icon" />
+        <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" />
       </div>
     </transition>
   </teleport>

+ 124 - 0
src/components/control-panl/ctrl.vue

@@ -0,0 +1,124 @@
+<template>
+  <div 
+    class="ctrl"
+    :class="{
+      active: active,
+      disabled: ctrl.disabled,
+      'in-click': ctrl.inClick,
+      'include-text': ctrl.text,
+      'include-icon': ctrl.icon,
+      'fun-ctrl': true
+    }" 
+    :data-key="ctrl.key"
+    @click.stop="emit('click', ctrl)"
+    ref="ctrlRef"
+  >
+    <template v-if="ctrl.icon">
+      <ui-guide 
+        :msg="ctrl.guide" 
+        :mark="ctrl.key" 
+        type="right" 
+        v-if="ctrl.key"
+      >
+        <template #content="{ show }">
+          <ui-icon 
+            v-if="ctrl.icon" 
+            :svg="ctrl.type === 'svg'" 
+            class="icon" 
+            :tip="show ? '' : ctrl.desc"
+            :type="ctrl.activeIcon && active ? ctrl.activeIcon : ctrl.icon" 
+          />
+        </template>
+      </ui-guide>
+      <ui-icon 
+        v-else 
+        :svg="ctrl.type === 'svg'" 
+        @click.stop="emit('click', ctrl)" 
+        class="icon" 
+        :tip="ctrl.desc"
+        :type="ctrl.activeIcon && active ? ctrl.activeIcon : ctrl.icon" />
+    </template>
+    <span v-if="ctrl.text" class="text">{{ ctrl.text }}</span>
+
+    <ui-floating
+        v-if="ctrl.children"
+        :mount="mountEl"
+        dire="right-top"
+        :refer="ctrlRef"
+        width="160px"
+        :class="{ show: active || showChild }"
+        class="ctrl-child-float"
+        @mouseenter="showChild = true"
+        @mouseleave="showChild = false"
+    >
+      <div class="child-ctrls">
+        <div v-for="ctrl in props.ctrl.children">
+          <Ctrl :ctrl="ctrl" :activeCtrls="activeCtrls" @click="data => emit('click', data)" />
+        </div>
+      </div>
+    </ui-floating>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import type { Item } from './index'
+
+const props = defineProps<{
+    ctrl: Item,
+    activeCtrls: Item[],
+}>()
+const emit = defineEmits<{(e: 'click', data: Item): void }>()
+const active = computed(() => props.activeCtrls?.includes(props.ctrl))
+const mountEl = document.body
+const ctrlRef = ref<HTMLElement>()
+const showChild = ref(false)
+
+</script>
+<script lang="ts">
+export default {
+  name:'Ctrl'
+}
+</script>
+
+<style lang="sass" scoped>
+@import './style.scss'
+</style>
+
+<style lang="scss">
+.control-icon-guide {
+  .guide-bubble {
+    margin-left: 40px !important;
+  }
+
+  .default-msg {
+    font-size: 12px;
+  }
+}
+
+.control-guide {
+  margin-left: 30px;
+  margin-top: -10px;
+}
+
+.ctrl-child-float {
+    position: absolute;
+    top: 0;
+    left: 100%;
+    display: block;
+    width: 160px;
+    background: rgba(26, 26, 26, 0.8);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3), inset 0 0 1px rgb(255 255 255 / 90);
+    border-radius: 4px;
+    border: 1px solid #000000;
+    transform: scaleY(0) translateX(10px);
+    transform-origin: top center;
+    transition: transform 0.3s ease;
+    z-index: 999999 !important;
+
+    &.show {
+        transform: scaleY(1) translateX(10px);
+    }
+}
+</style>

+ 27 - 0
src/components/control-panl/index.ts

@@ -0,0 +1,27 @@
+import ControlPanl from './index.vue'
+
+export type Item = {
+  text?: string
+  icon?: string
+  activeIcon?: string
+  makeup?: boolean
+  def?: boolean
+  inClick?: boolean
+  disabled?: boolean
+  desc?: string
+  type?: string
+  key?: string; 
+  guide?: string
+  children?: Omit<Item, 'children'>[]
+}
+
+export type Items = Item[]
+
+export type Group = { apart?: boolean, label?: string, items: Items }
+export type Groups = Group[]
+
+export type ControlExpose = {dom: HTMLDivElement | null}
+
+export { ControlPanl }
+
+export default ControlPanl

+ 105 - 0
src/components/control-panl/index.vue

@@ -0,0 +1,105 @@
+<template>
+  <transition name="fade">
+    <div 
+      class="control-panl pc" 
+      :class="{ full, 'strengthen-right': full }" 
+      :ref="(dom: any) => exposeConfig.dom = dom" 
+      v-if="!hide"
+    >
+      <div class="control-layer">
+        <div class="scroll-view">
+          <div c
+            lass="panl" 
+            v-for="(groupItem, i) in group" 
+            :class="{ apart: groupItem.apart }"
+          >
+            <p v-if="groupItem.label">{{ groupItem.label }}</p>
+            <VCtrl 
+              v-for="ctrl in groupItem.items" 
+              :activeCtrls="modelValue" 
+              :ctrl="ctrl"
+              :key="ctrl.key"
+              @click="ctrl => clickHandler(ctrl)" 
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script lang="ts" setup>
+import { ref, watchEffect, watch, reactive, toRaw } from 'vue'
+import VCtrl from './ctrl.vue'
+import { useActive } from '@/hook'
+
+import type { Groups, Items, Item, ControlExpose } from './index'
+
+const props =  withDefaults(
+  defineProps<{
+    group: Groups
+    show?: boolean
+    modelValue: Items
+    full?: boolean
+  }>(),
+  {
+    show: true,
+    full: false
+  }
+)
+
+const active = useActive()
+const hide = ref(true)
+watch(
+  () => [active, props.show], 
+  () => {
+    if (active.value) {
+      hide.value = active.value
+    }
+    setTimeout(() => hide.value = !props.show)
+  }, 
+  { immediate: true }
+)
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', items: Item[], oldItems: Item[]): void
+  (e: 'select', item: Item): void
+}>()
+
+const clickHandler = (ctrl: Item) => {
+  if (ctrl.inClick) {
+    return emit('select', ctrl)
+  }
+
+  const newRuns = reactive([...props.modelValue])
+  const index = newRuns.indexOf(reactive(ctrl))
+  if (~index) {
+    newRuns.splice(index, 1)
+  } else if (ctrl.makeup) {
+    newRuns.push(ctrl)
+  } else {
+    const makeupRuns = newRuns.filter(item => item.makeup)
+    newRuns.length = 0
+    newRuns.push(...makeupRuns, ctrl)
+  }
+  emit('update:modelValue', newRuns, props.modelValue)
+}
+
+watchEffect(() => {
+  if (!props.show) {
+    const items: Item[] = []
+    for (const { items } of props.group) {
+      items.push(...items.filter(item => item.def))
+    }
+    emit('update:modelValue', items, props.modelValue)
+  }
+})
+
+ 
+const exposeConfig = reactive<ControlExpose>({ dom: null })
+defineExpose(exposeConfig)
+</script>
+
+<style lang="sass" scoped>
+@import './style.scss'
+</style>

+ 200 - 0
src/components/control-panl/style.scss

@@ -0,0 +1,200 @@
+
+.control-panl {
+  position: absolute;
+  width: 70px;
+  &.pc {
+    padding: 10px 0;
+  }
+  &:not(.pc) {
+    width: 60px;
+    padding: 10px 0;
+  }
+  margin-left: 0;
+  
+
+
+  &.full {
+    top: calc(var(--header-top) + var(--editor-head-height));
+    bottom: 0;
+    left: 0;
+    overflow-y: auto;
+
+    &.pc .control-layer {
+      padding: 0 10px;
+      max-height: 100%;
+    }
+
+    &:not(.pc) .control-layer {
+      padding: 0 5px;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+
+      .pub-panl {
+        flex: none;
+        &.start {
+          margin-bottom: 20px;
+        }
+
+        &.end {
+          margin-top: 20px;
+
+          &.active {
+            color: #00c8af;
+          }
+        }
+
+        .nav-ctrl {
+          position: relative;
+          .icon {
+            position: relative;
+            z-index: 1;
+          }
+
+          span {
+            top: 0;
+            right: 0;
+            position: absolute;
+            width: 22px;
+            height: 18px;
+            border-radius: 9px;
+            text-align: center;
+            line-height: 18px;
+            color: #fff;
+            font-size: 12px;
+            background-color: rgba(0, 200, 175, 1);
+            z-index: 9999;
+          }
+        }
+      }
+
+      .scroll-view {
+        flex: 1;
+        overflow-y: auto;
+        width: calc(100% + 10px);
+        margin-left: -5px;
+        padding: 0 4px;
+      }
+    }
+  }
+
+
+  &:not(.full) {
+    left: 10px;
+    top: 50%;
+    transform: translateY(-50%) translateX(0);
+    border-radius: 10px;
+    padding: 10px;
+    &:not(.pc) {
+      padding: 5px;
+    }
+  }
+  
+
+  backdrop-filter: blur(4px);
+  background: var(--editor-menu-back);
+  pointer-events: all;
+  z-index: 20;
+
+
+
+  .panl {
+    color: #999999;
+    &:not(:first-of-type) {
+      margin-top: 20px;
+    }
+
+    &.apart {
+      border-top: 1px solid rgba(255,255,255,.3);
+      padding-top: 20px;
+      margin-top: 20px;
+    }
+
+
+    p {
+      font-size: 12px;
+      text-align: center;
+      margin: 10px 0;
+    }
+
+    
+  }
+}
+
+.child-ctrls {
+  width: 160px;
+  display: grid;
+  grid-template: auto / repeat(3, 1fr);
+  align-items: center;
+  justify-items: center;
+  justify-content: center;
+  align-content: center;
+  > div {
+    border: 1px solid rgba(255,255,255, .1);
+  }
+}
+
+.ctrl {
+  margin-bottom: 1px;
+  width: 50px;
+  height: 40px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: background-color .3s ease, color .3s ease;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  user-select: none;
+  // background-color: rgba(255,255,255,.1);
+
+  &.fun-ctrl {
+    color: #fff !important;
+    &:hover {
+      color: #00c8af !important;
+    }
+  }
+
+  &.include-text.include-icon {
+
+    .icon {
+      flex: 1;
+      font-size: 16px;
+      text-align: center;
+    }
+    .text {
+      text-align: center;
+      flex: 1;
+      font-size: 12px;
+    }
+  }
+
+
+  &:not(.include-text) {
+    font-size: 20px;
+  }
+
+  &:not(.include-icon) {
+    font-size: 16px;
+  }
+
+
+  &.active {
+    background-color: rgba(var(--colors-primary-base-fill), 0.16);
+  }
+  &.active i {
+    color: var(--colors-primary-base) !important;
+  }
+
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: 
+    opacity .2s ease,
+    margin-left .2s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+  margin-left: -70px;
+}

+ 2 - 0
src/env/index.ts

@@ -10,6 +10,7 @@ export const showLeftPanoStack = stackFactory(ref<boolean>(false))
 export const showLeftCtrlPanoStack = stackFactory(ref<boolean>(true))
 export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true))
 export const showTaggingsStack = stackFactory(ref<boolean>(true))
+export const showMeasuresStack = stackFactory(ref<boolean>(false))
 export const currentModelStack = stackFactory(ref<FuseModel | null>(null))
 export const showModelsMapStack = stackFactory(ref<Map<FuseModel, boolean>>(new Map))
 export const modelsChangeStoreStack = stackFactory(ref<boolean>(false))
@@ -24,6 +25,7 @@ export const custom = flatStacksValue({
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
   showTaggings: showTaggingsStack,
+  showMeasures: showMeasuresStack,
   currentModel: currentModelStack,
   showModelsMap: showModelsMapStack,
   modelsChangeStore: modelsChangeStoreStack,

+ 10 - 0
src/hook/viewStack.ts

@@ -4,6 +4,7 @@ import {
   onDeactivated,
   onMounted,
   onUnmounted,
+  ref,
 } from "vue";
 
 export type ViewStackProps = (...args: any) => (() => any) | void;
@@ -33,3 +34,12 @@ export const useViewStack = (hook: ViewStackProps) => {
   onDeactivated(deHandler);
   onUnmounted(deHandler);
 };
+
+export const useActive = () => {
+  const active = ref(true)
+  useViewStack(() => {
+    active.value = true
+    return () => active.value = false
+  })
+  return active
+}

+ 1 - 6
src/layout/left-pano.vue

@@ -2,7 +2,7 @@
   <span 
     class="ctrl-pano-c fun-ctrl strengthen-right strengthen-top strengthen-bottom"
     v-if="custom.viewMode !== 'full' && custom.showLeftCtrlPano" 
-    @click="hidePano"
+    @click="custom.showLeftPano = !custom.showLeftPano"
     :class="{ active: custom.showLeftPano }">
     <ui-icon type="extend" class="icon"></ui-icon>
   </span>
@@ -13,11 +13,6 @@
 
 <script lang="ts" setup>
 import { custom } from '@/env'
-
-const hidePano = () => {
-  custom.showLeftPano = !custom.showLeftPano
-  custom.showRightPano = !custom.showLeftPano
-}
 </script>
 
 <style lang="scss" scoped>

+ 10 - 7
src/layout/main.vue

@@ -5,10 +5,7 @@
     </div>
 
     <template v-if="loaded" style="height: 100%">
-      <SlideMenu />
       <Header></Header>
-      <ModelList />
-
       <router-view v-slot="{ Component }">
         <keep-alive>
           <component :is="Component" />
@@ -20,12 +17,11 @@
 
 <script lang="ts" setup>
 import { custom } from '@/env'
-import { computed, ref, watchEffect } from 'vue'
-import { isEdit, loaded } from '@/store'
+import { computed, ref, watchEffect, watch } from 'vue'
+import { isEdit, loaded, enterEdit } from '@/store'
 import { initialSDK } from '@/sdk'
-import SlideMenu from './slide-menu.vue'
+import { currentRouteNames, router } from '@/router'
 import Header from './header/index.vue'
-import ModelList from './model-list/index.vue'
 
 const layoutClassNames = computed(() => {
   return {
@@ -44,6 +40,13 @@ const stopSdkInstalWatch = watchEffect(() => {
     stopSdkInstalWatch()
   }
 })
+
+watch(currentRouteNames, () => {
+  if (currentRouteNames.value.includes('full')) {
+    enterEdit(() => router.back())
+  }
+}, { immediate: true })
+
 </script>
 
 <style scoped lang="scss">

+ 2 - 9
src/layout/right-fill-pano.vue

@@ -1,8 +1,8 @@
 <template>
   <span 
     class="ctrl-pano-c fun-ctrl strengthen-left strengthen-top strengthen-bottom"
-    v-if="custom.shwoRightCtrlPano" 
-    @click="hidePano"
+    v-if="custom.shwoRightCtrlPano && custom.viewMode !== 'full'" 
+    @click="custom.showRightPano = !custom.showRightPano"
     :class="{ active: custom.showRightPano }">
     <ui-icon type="extend" class="icon"></ui-icon>
   </span>
@@ -13,13 +13,6 @@
 
 <script setup lang="ts">
 import { custom } from '@/env'
-
-const hidePano = () => {
-  custom.showRightPano = !custom.showRightPano
-  custom.showLeftPano = !custom.showRightPano
-  console.log(custom.showRightPano)
-}
-
 </script>
 
 <style lang="scss" scoped>

+ 4 - 0
src/layout/slide-menu.vue

@@ -21,6 +21,10 @@ const items: Items = [
     ...metas[RoutesName.tagging]
   },
   {
+    name: RoutesName.measure,
+    ...metas[RoutesName.measure]
+  },
+  {
     name: RoutesName.guide,
     ...metas[RoutesName.guide]
   }

+ 17 - 0
src/layout/switch.vue

@@ -0,0 +1,17 @@
+<template>
+  <SlideMenu />
+  <Header></Header>
+  <ModelList />
+
+  <router-view v-slot="{ Component }">
+    <keep-alive>
+      <component :is="Component" />
+    </keep-alive>
+  </router-view>
+</template>
+
+<script lang="ts" setup>
+import SlideMenu from './slide-menu.vue'
+import Header from './header/index.vue'
+import ModelList from './model-list/index.vue'
+</script>

+ 35 - 12
src/router/config.ts

@@ -4,22 +4,45 @@ import type { RouteRecordRaw } from 'vue-router'
 
 export const routes: RouteRecordRaw[] = [
   {
-    path: paths.merge,
-    name: RoutesName.merge,
-    meta: metas.merge,
-    component: () => import('@/views/merge/index.vue')
+    path: paths.switch,
+    name: RoutesName.switch,
+    component: () => import('@/layout/switch.vue'),
+    children: [
+      {
+        path: paths.merge,
+        name: RoutesName.merge,
+        meta: metas.merge,
+        component: () => import('@/views/merge/index.vue')
+      },
+      {
+        path: paths.tagging,
+        name: RoutesName.tagging,
+        meta: metas.tagging,
+        component: () => import('@/views/tagging/index.vue')
+      },
+      {
+        path: paths.measure,
+        name: RoutesName.measure,
+        meta: metas.measure,
+        component: () => import('@/views/measure/index.vue')
+      },
+      {
+        path: paths.guide,
+        name: RoutesName.guide,
+        meta: metas.guide,
+        component: () => import('@/views/guide/index.vue')
+      }
+    ]
   },
   {
-    path: paths.tagging,
-    name: RoutesName.tagging,
-    meta: metas.tagging,
-    component: () => import('@/views/tagging/index.vue')
+    path: paths.registration,
+    name: RoutesName.registration,
+    component: () => import('@/views/registration/index.vue')
   },
   {
-    path: paths.guide,
-    name: RoutesName.guide,
-    meta: metas.guide,
-    component: () => import('@/views/guide/index.vue')
+    path: paths.proportion,
+    name: RoutesName.proportion,
+    component: () => import('@/views/proportion/index.vue')
   }
 ]
 

+ 23 - 8
src/router/constant.ts

@@ -1,24 +1,35 @@
 export enum RoutesName {
   merge = 'merge',
+  registration = 'registration',
+  proportion = 'proportion',
+
   tagging = 'tagging',
-  guide = 'guide'
+  guide = 'guide',
+  measure = 'measure',
+
+  switch = 'switch'
 }
 
-type RouteSeting<T> = {[key in RoutesName]: T}
 
-export const paths: RouteSeting<string> = {
+export const paths = {
   [RoutesName.merge]: '/merge',
+  [RoutesName.registration]: '/registration/:id',
+  [RoutesName.proportion]: '/proportion/:id',
+  
   [RoutesName.tagging]: '/tagging',
-  [RoutesName.guide]: '/path'
-}
+  [RoutesName.guide]: '/path',
+  [RoutesName.measure]: '/measure',
 
-export type Meta = { title: string, icon: string }
+  [RoutesName.switch]: '/',
+}
 
-export const metas: RouteSeting<Meta> = {
+export const metas = {
   [RoutesName.merge]: {
     icon: 'joint',
     title: '拼接'
   },
+  [RoutesName.proportion]: { full: true },
+  [RoutesName.registration]: { full: true },
   [RoutesName.tagging]: {
     icon: 'label',
     title: '标注'
@@ -26,7 +37,11 @@ export const metas: RouteSeting<Meta> = {
   [RoutesName.guide]: {
     icon: 'path',
     title: '路径'
-  }
+  },
+  [RoutesName.measure]: {
+    icon: 'nav-measure',
+    title: '测量'
+  },
 }
 
 export const ViewHome = RoutesName.merge

+ 38 - 0
src/router/index.ts

@@ -1,9 +1,47 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
 import { routes } from './config'
+import { computed } from 'vue'
+import { RoutesName } from './constant'
+import { metas } from './constant'
+
+import type { RouteRecordRaw, RouteRecordName } from 'vue-router'
 
 export const history = createWebHashHistory()
 export const router = createRouter({ history, routes })
 
+export const getRouteNames = (name: RouteRecordName, routes: RouteRecordRaw[]): void | RouteRecordName[] => {
+  for (const route of routes) {
+    if (route.name === name) {
+      return [route.name]
+    } else if (route.children) {
+      const baseNames = getRouteNames(name, route.children)
+      if (baseNames) {
+        return [route.name as string, ...baseNames]
+      }
+    }
+  }
+}
+
+export const currentRouteNames = computed(() => {
+  const currentName = router.currentRoute.value.name
+  return currentName 
+    ? getRouteNames(currentName, routes) || []
+    : []
+})
+
+export const currentLayout = computed(() => {
+  const names = currentRouteNames.value
+  const layoutNames = [RoutesName.switch] as const
+  return layoutNames.find(name => names.includes(name))
+})
+
+export const currentMeta = computed(() => {
+  const currentName = router.currentRoute.value.name
+  if (currentName && currentName in metas) {
+    return (metas as any )[currentName] as ((typeof metas)[keyof typeof metas])
+  }
+})
+
 export * from './config'
 export * from './constant'
 

+ 4 - 4
src/sdk/association.ts

@@ -19,11 +19,11 @@ export const modelRange: ModelAttrRange  = {
   scaleRange: { min: 0, max: 200, step: 0.1 }
 }
 
-import type { SDK, SceneModel, SceneGuidePath } from '.'
-import { Model, Tagging } from '@/store'
+import type { SDK, SceneModel, SceneGuidePath, ModelAttrRange } from '.'
+import { FuseModel, Tagging } from '@/store'
 
-const sceneModelMap = new WeakMap<Model, SceneModel>()
-export const getSceneModel = (model: Model | null) => model && sceneModelMap.get(toRaw(model))
+const sceneModelMap = new WeakMap<FuseModel, SceneModel>()
+export const getSceneModel = (model: FuseModel | null) => model && sceneModelMap.get(toRaw(model))
 
 const associationModels = (sdk: SDK) => {
   const getModels = () => fuseModels.value

+ 2 - 1
src/store/index.ts

@@ -33,4 +33,5 @@ export * from './tagging'
 export * from './tagging-style'
 export * from './guide'
 export * from './guide-path'
-export * from './tagging-positions'
+export * from './tagging-positions'
+export * from './measure'

+ 37 - 0
src/store/measure.ts

@@ -0,0 +1,37 @@
+import { createTemploraryID } from './sys'
+import { ref } from 'vue'
+
+export enum MeasureType {
+  free,
+  vertical,
+  area
+}
+export const MeasureTypeMeta = {
+  [MeasureType.area]: { icon: 'v-l', desc: '自由', unit: '长度' },
+  [MeasureType.free]: { icon: 'f-l', desc: '垂直', unit: '长度' },
+  [MeasureType.vertical]: { icon: 'h-r', desc: '面积', unit: '面积' }
+}
+
+export interface Measure {
+  id: string,
+  desc: string,
+  positions: SceneLocalPos[],
+  type: MeasureType,
+}
+
+export type Measures = Measure[]
+
+export const createMeasure = (measure: Partial<Measure> = {}): Measure => ({
+  id: createTemploraryID(),
+  positions: [],
+  desc: '',
+  type: MeasureType.free,
+  ...measure
+})
+
+
+export const measures = ref<Measures>([
+  createMeasure({ desc: '11' }),
+  createMeasure({ desc: '11', type: MeasureType.vertical }),
+  createMeasure({ desc: '11', type: MeasureType.area })
+])

+ 3 - 0
src/style.scss

@@ -34,6 +34,9 @@ h1, h2, h3, h4, h5, h6 {
 a {
   color: var(--color-main-normal);
 }
+p {
+  margin: 0;
+}
 #app {
   width: 100%;
   height: 100%;

+ 36 - 0
src/views/measure/edit.vue

@@ -0,0 +1,36 @@
+<template>
+
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { enterEdit, useAutoSetMode } from '@/store'
+import { useViewStack } from '@/hook'
+import { togetherCallback } from '@/utils'
+import { showRightCtrlPanoStack, showRightPanoStack, } from '@/env'
+
+import type { Measure } from '@/store'
+
+const props = defineProps<{ measure: Measure }>()
+const emit = defineEmits<{ 
+  (e: 'close'): void
+  (e: 'submit', measure: Measure): void
+}>()
+const measure = reactive(props.measure)
+
+enterEdit(() => emit('close'))
+useViewStack(() => togetherCallback([
+  showRightCtrlPanoStack.push(ref(false)),
+  showRightPanoStack.push(ref(false)),
+]))
+useAutoSetMode(measure, {
+  save: () => emit('submit', measure)
+})
+
+setTimeout(() => {
+  measure.positions = [
+    {x: 1, y: 1, z: 1},
+    {x: 1, y: 1, z: 1},
+  ]
+}, 2000)
+</script>

+ 87 - 0
src/views/measure/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <RightFillPano>
+    <ui-group borderBottom>
+      <template #header>
+        <Actions class="edit-header" :items="options" single />
+      </template>
+    </ui-group>
+    <ui-group title="测量列表">
+      <template #icon>
+        <ui-icon 
+          ctrl
+          :type="custom.showMeasures ? 'eye-s' : 'eye-n'" 
+          @click="custom.showMeasures = !custom.showMeasures" 
+        />
+      </template>
+      <ui-group-option>
+        <ui-input type="text" width="100%" placeholder="搜索" v-model="keyword">
+          <template #preIcon>
+            <ui-icon type="search" />
+          </template>
+        </ui-input>
+      </ui-group-option>
+      <MeasureSign 
+        v-for="measure in filterMeasures" 
+        :key="measure.id" 
+        :measure="measure" 
+        :selected="selectMeasure === measure"
+        @delete="deleteMeasure(measure)"
+        @select="selectMeasure = measure"
+      />
+    </ui-group>
+  </RightFillPano>
+
+  <EditMeasure 
+    v-if="editMeasure" 
+    :measure="editMeasure" 
+    @close="editMeasure = null"
+    @submit="measure => measures.push(measure)"
+  />
+</template>
+
+<script lang="ts" setup>
+import MeasureSign from './sign.vue'
+import Actions from '@/components/actions/index.vue'
+import EditMeasure from './edit.vue'
+import { RightFillPano } from '@/layout'
+import { computed, ref } from 'vue';
+import { custom, showMeasuresStack } from '@/env'
+import { measures, MeasureTypeMeta, MeasureType, createMeasure } from '@/store'
+import { useViewStack } from '@/hook'
+
+import type { Measure } from '@/store'
+import type { ActionsItem } from '@/components/actions/index.vue'
+
+const keyword = ref('')
+const filterMeasures = computed(() => measures.value.filter(measure => measure.desc.includes(keyword.value)))
+const selectMeasure = ref<Measure | null>(null)
+const editMeasure = ref<Measure | null>(null)
+const enterCreateMeasure = (type: MeasureType) => {
+  editMeasure.value = createMeasure({ type })
+}
+const options: ActionsItem[] = [
+  {
+    icon: MeasureTypeMeta[MeasureType.free].icon,
+    text: MeasureTypeMeta[MeasureType.free].desc,
+    action: enterCreateMeasure.bind(null, MeasureType.free)
+  },
+  {
+    icon: MeasureTypeMeta[MeasureType.vertical].icon,
+    text: MeasureTypeMeta[MeasureType.vertical].desc,
+    action: enterCreateMeasure.bind(null, MeasureType.vertical)
+  },
+  {
+    icon: MeasureTypeMeta[MeasureType.area].icon,
+    text: MeasureTypeMeta[MeasureType.area].desc,
+    action: enterCreateMeasure.bind(null, MeasureType.area)
+  }
+]
+
+
+const deleteMeasure = (measure: Measure) => {
+  const index = measures.value.indexOf(measure)
+  measures.value.splice(index, 1)
+}
+
+useViewStack(() => showMeasuresStack.push(ref(true)))
+</script>

+ 99 - 0
src/views/measure/sign.vue

@@ -0,0 +1,99 @@
+<template>
+  <ui-group-option 
+    class="sign-measure" 
+    :class="{active: selected}" 
+    @click="emit('select')"
+  >
+    <div class="info">
+      <ui-icon :type="MeasureTypeMeta[measure.type].icon" class="type" />
+      <div>
+        <p>{{ measure.desc }}</p>
+        <span>{{ MeasureTypeMeta[measure.type].unit }}</span>
+      </div>
+    </div>
+    <div class="actions" @click.stop>
+      <ui-icon type="del" ctrl @click.stop="$emit('delete')" />
+      <ui-icon type="pin" ctrl @click.stop="$emit('fly')" />
+    </div>
+  </ui-group-option>
+</template>
+
+<script setup lang="ts">
+import { MeasureTypeMeta } from '@/store'
+
+import type { Measure } from '@/store'
+
+
+defineProps<{ measure: Measure, selected?: boolean }>()
+
+const emit = defineEmits<{ 
+  (e: 'delete'): void 
+  (e: 'select'): void
+  (e: 'fly'): void
+}>()
+
+</script>
+
+<style lang="scss" scoped>
+.sign-measure {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 0;
+  margin: 0;
+  border-bottom: 1px solid var(--colors-border-color);
+  cursor: pointer;
+  position: relative;
+
+  &.active::after {
+    content: '';
+    position: absolute;
+    pointer-events: none;
+    inset: 0 -20px;
+    background-color: rgba(0, 200, 175, 0.16);
+    z-index: -1;
+  }
+
+  .info {
+    flex: 1;
+
+    display: flex;
+    align-items: center;
+
+    .type {
+      width: 48px;
+      height: 48px;
+      border-radius: 4px;
+      overflow: hidden;
+      display: flex;
+      background: rgba(0,0,0,0.5);
+      font-size: 18px;
+      align-items: center;
+      justify-content: center;
+    }
+
+    div {
+      margin-left: 10px;
+
+      p {
+        color: #fff;
+        font-size: 14px;
+      }
+
+      span {
+        color: rgba(255,255,255,0.6);;
+        font-size: 12px;
+      }
+    }
+  }
+  
+  .actions {
+    flex: none;
+    > * {
+      margin-left: 22px;
+    }
+  }  
+}
+
+
+</style>

+ 46 - 22
src/views/merge/index.vue

@@ -2,29 +2,57 @@
   <RightPano v-if="custom.currentModel && active">
     <ui-group>
       <template #header>
-        <Actions class="edit-header" :items="actionItems" />
+        <Actions class="edit-header" :items="actionItems" v-model:current="currentItem" />
       </template>
       <ui-group-option label="等比缩放">
-        <!-- <template #icon>
-          <a href="">设置比例</a>
-        </template> -->
-        <ui-input type="range" v-model="custom.currentModel.scale" v-bind="modelRange.scaleRange" :ctrl="false" width="100%">
+        <template #icon>
+          <a @click="router.push({ 
+              name: RoutesName.proportion, 
+              params: { id: custom.currentModel!.id, save: '1' },
+            })"
+          >设置比例</a>
+        </template>
+        <ui-input 
+          type="range" 
+          v-model="custom.currentModel.scale" 
+          v-bind="modelRange.scaleRange" 
+          :ctrl="false" 
+          width="100%"
+        >
           <template #icon>%</template>
         </ui-input>
       </ui-group-option>
       <ui-group-option label="离地高度">
-        <ui-input type="range" v-model="custom.currentModel.bottom" v-bind="modelRange.bottomRange" :ctrl="false" width="100%">
+        <ui-input 
+          type="range" 
+          v-model="custom.currentModel.bottom" 
+          v-bind="modelRange.bottomRange" 
+          :ctrl="false" 
+          width="100%"
+        >
           <template #icon>m</template>
         </ui-input>
       </ui-group-option>
       <ui-group-option label="模型不透明度">
-        <ui-input type="range" v-model="custom.currentModel.opacity" v-bind="modelRange.opacityRange" :ctrl="false" width="100%">
+        <ui-input 
+          type="range" 
+          v-model="custom.currentModel.opacity" 
+          v-bind="modelRange.opacityRange" 
+          :ctrl="false" 
+          width="100%"
+        >
           <template #icon>%</template>
         </ui-input>
       </ui-group-option>
-      <!-- <ui-group-option>
-        <ui-button>配准</ui-button>
-      </ui-group-option> -->
+      <ui-group-option>
+        <ui-button 
+          :disabled="currentItem" 
+          @click="router.push({ 
+            name: RoutesName.registration, 
+            params: {id: custom.currentModel!.id, save: '1' } 
+          })"
+        >配准</ui-button>
+      </ui-group-option>
       <ui-group-option>
         <ui-button @click="reset">恢复默认</ui-button>
       </ui-group-option>
@@ -33,33 +61,29 @@
 </template>
 
 <script lang="ts" setup>
+import { RoutesName, router } from '@/router'
 import { RightPano } from '@/layout'
 import { autoSaveFuseModels, defaultFuseModelAttrs } from '@/store'
 import { togetherCallback } from '@/utils'
-import Actions from '@/components/actions/index.vue'
 import { getSceneModel, modelRange } from '@/sdk'
-import { useViewStack } from '@/hook'
+import { useViewStack, useActive } from '@/hook'
 import { showLeftCtrlPanoStack, showLeftPanoStack, custom, modelsChangeStoreStack } from '@/env'
 import { ref, nextTick } from 'vue'
 import { Dialog } from 'bill/expose-common'
 
-import type { ActionsProps } from '@/components/actions/index.vue'
+import Actions from '@/components/actions/index.vue'
+
+import type { ActionsProps, ActionsItem } from '@/components/actions/index.vue'
 
-const active = ref(true)
-useViewStack(() => {
-  active.value = true
-  return () => active.value = false
-})
+const active = useActive()
+const currentItem = ref<ActionsItem | null>(null)
 const actionItems: ActionsProps['items'] = [
   {
     icon: 'move',
     text: '移动',
     action: () => {
       getSceneModel(custom.currentModel)?.enterMoveMode()
-      return () => {
-        console.log(getSceneModel(custom.currentModel), 'leave')
-        getSceneModel(custom.currentModel)?.leaveTransform()
-      }
+      return () => getSceneModel(custom.currentModel)?.leaveTransform()
     }
   },
   {

+ 30 - 0
src/views/proportion/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <ui-editor-toolbar toolbar>
+    <span>长度:</span>
+    <ui-input type="number" width="120px" class="leng-input" :ctrl="false">
+      <template #icon>m</template>
+    </ui-input>
+    <ui-button type="submit" width="160px">重新选点</ui-button>
+  </ui-editor-toolbar>
+</template>
+
+<script lang="ts" setup>
+import { Message } from 'bill/index'
+import { useViewStack } from '@/hook'
+import { getCurrentInstance } from 'vue'
+
+useViewStack(() => {
+  const hide = Message.show({ msg: '请选择两点标记一段已知长度,并输入真实长度' })
+  return () => {
+    console.log(hide)
+    hide()
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.leng-input {
+  margin: 0 20px 0 10px;
+}
+</style>

+ 152 - 0
src/views/registration/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <ControlPanl 
+    :group="[{ items: options }]" 
+    v-model="selectOptions" 
+    ref="selectExpose"
+  />
+  <ui-floating
+    v-if="selectOptions.some(({key}) => key === 'opacity')"
+    :refer="opacityOptionEl"
+    isTransform
+    dire="right-center"
+  >
+    <div class="floating-range strengthen">
+      <div class="range-content">
+        <ui-input 
+          type="range" 
+          v-model="a"
+          v-bind="modelRange.opacityRange" 
+          :ctrl="false" 
+          :input="false"
+          width="100%"
+        />
+        <span class="num" :style="{left: `${a}%`}">{{a}}%</span>
+      </div>
+    </div>
+  </ui-floating>
+
+  <div class="right-range floating-range strengthen">
+    <div class="range-content">
+      <span class="fun-ctrl" @click="a += modelRange.opacityRange.step">+</span>
+      <ui-input 
+        type="range" 
+        v-model="a"
+        v-bind="modelRange.opacityRange" 
+        :moveCallback="changeRange"
+        :ctrl="false" 
+        :input="false"
+        width="100%"
+      />
+      <span class="fun-ctrl" @click="a -= modelRange.opacityRange.step">-</span>
+    </div>
+  </div>
+
+  <div class="ui-message tip-left">请在当前窗口调整水平方向位置</div>
+  <div class="ui-message tip-right">请在当前窗口调整垂直方向位置</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { ControlPanl } from '@/components/control-panl/'
+import { modelRange } from '@/sdk'
+import { diffArrayChange } from '@/utils'
+
+import type { Items, ControlExpose } from '@/components/control-panl'
+
+const options: Items = [
+  { desc: '移动', icon: 'move', key: 'move' },
+  { desc: '旋转', icon: 'flip', key: 'rotate' },
+  { desc: '透明度', icon: 'transparency', key: 'opacity' },
+]
+const selectOptions = ref<Items>([])
+const selectExpose = ref<ControlExpose>()
+const opacityOptionEl = computed(
+  () => selectExpose.value?.dom?.querySelector('div[data-key="opacity"]')
+)
+const a = ref(1)
+
+const changeRange = (sp: ScreenLocalPos, cp: ScreenLocalPos, info: { start: number, locusWidth: number }) => 
+  info.start + ((sp.y - cp.y) / info.locusWidth)
+
+watch(selectOptions, (nOptions, oOptions = []) => {
+  const { added, deleted } = diffArrayChange(nOptions, oOptions)
+  console.log(added, deleted)
+}, { immediate: true })
+
+</script>
+
+<style lang="scss" scoped>
+.floating-range {
+  margin-left: 20px;
+  transform: translateY(-50%);
+  width: 162px;
+  height: 32px;
+  background: rgba(27,27,28,0.8);
+  box-shadow: inset 0px 0px 0px 2px rgba(255,255,255,0.1);
+  border-radius: 16px;
+  padding: 0 10px;
+  .range-content {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    position: relative;
+
+    .num {
+      position: absolute;
+      color: #fff;
+      bottom: 100%;
+      transform: translateX(-50%);
+      background: #000000;
+      border-radius: 4px;
+      padding: 2px 6px;
+      font-size: 12px;
+      pointer-events: none;
+      white-space: nowrap;
+    }
+  }
+}
+
+.right-range {
+  position: absolute;
+  padding: 0;
+  width: 220px;
+  border-radius: 20px;
+  height: 40px;
+  right: 10px;
+  top: 50%;
+  z-index: 1;
+  transform: translateX(40%) rotate(-90deg);
+
+  .range-content {
+    align-items: center;
+
+    span {
+      margin: 0 10px;
+      font-size: 16px;
+      transform: rotate(90deg);
+      flex: none;
+      color: #fff;
+    }
+  }
+}
+
+.tip-left,.tip-right {
+  top: calc(var(--editor-head-height) + var(--header-top) + 11px);
+  z-index: 1;
+}
+
+.tip-left {
+  left: 25%;
+  transform: translateX(-50%);
+}
+.tip-right {
+  left: 75%;
+  transform: translateX(-50%);
+}
+</style>
+
+<style>
+.floating-range .ui-input .range .range-content {
+  --slideSize: calc(var(--height) + 8px) !important;
+}
+</style>