bill 8 kuukautta sitten
vanhempi
commit
d87b9dd5dd

+ 2 - 1
package.json

@@ -9,7 +9,8 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "ant-design-vue": "^3.3.0-beta.3",
+    "@ant-design/icons-vue": "^7.0.1",
+    "ant-design-vue": "^4.2.6",
     "axios": "^0.27.2",
     "coordtransform": "^2.1.2",
     "less": "^4.1.3",

+ 39 - 11
pnpm-lock.yaml

@@ -1,9 +1,10 @@
 lockfileVersion: 5.4
 
 specifiers:
+  '@ant-design/icons-vue': ^7.0.1
   '@types/node': ^18.6.5
   '@vitejs/plugin-vue': ^3.0.0
-  ant-design-vue: ^3.3.0-beta.3
+  ant-design-vue: ^4.2.6
   axios: ^0.27.2
   coordtransform: ^2.1.2
   less: ^4.1.3
@@ -20,7 +21,8 @@ specifiers:
   vuedraggable: ^4.1.0
 
 dependencies:
-  ant-design-vue: 3.3.0-beta.3_vue@3.2.37
+  '@ant-design/icons-vue': 7.0.1_vue@3.2.37
+  ant-design-vue: 4.2.6_vue@3.2.37
   axios: 0.27.2
   coordtransform: 2.1.2
   less: 4.1.3
@@ -45,15 +47,15 @@ packages:
   /@ant-design/colors/6.0.0:
     resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==}
     dependencies:
-      '@ctrl/tinycolor': 3.4.1
+      '@ctrl/tinycolor': 3.6.1
     dev: false
 
   /@ant-design/icons-svg/4.2.1:
     resolution: {integrity: sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==}
     dev: false
 
-  /@ant-design/icons-vue/6.1.0_vue@3.2.37:
-    resolution: {integrity: sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==}
+  /@ant-design/icons-vue/7.0.1_vue@3.2.37:
+    resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==}
     peerDependencies:
       vue: '>=3.0.3'
     dependencies:
@@ -92,11 +94,19 @@ packages:
       '@babel/helper-validator-identifier': 7.18.6
       to-fast-properties: 2.0.0
 
-  /@ctrl/tinycolor/3.4.1:
-    resolution: {integrity: sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==}
+  /@ctrl/tinycolor/3.6.1:
+    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
     engines: {node: '>=10'}
     dev: false
 
+  /@emotion/hash/0.9.2:
+    resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
+    dev: false
+
+  /@emotion/unitless/0.8.1:
+    resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
+    dev: false
+
   /@esbuild/linux-loong64/0.14.54:
     resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==}
     engines: {node: '>=12'}
@@ -350,19 +360,22 @@ packages:
   /@vue/shared/3.2.37:
     resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==}
 
-  /ant-design-vue/3.3.0-beta.3_vue@3.2.37:
-    resolution: {integrity: sha512-xA7clMwKRw2S6E8etiq7FRN8qktRW6xr9HBuEuUDQwsbxLIy1kVKfiTbdbh+CklmasKGd9qvEJbzc65U/HJeOw==}
+  /ant-design-vue/4.2.6_vue@3.2.37:
+    resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
     engines: {node: '>=12.22.0'}
     peerDependencies:
       vue: '>=3.2.0'
     dependencies:
       '@ant-design/colors': 6.0.0
-      '@ant-design/icons-vue': 6.1.0_vue@3.2.37
+      '@ant-design/icons-vue': 7.0.1_vue@3.2.37
       '@babel/runtime': 7.18.9
-      '@ctrl/tinycolor': 3.4.1
+      '@ctrl/tinycolor': 3.6.1
+      '@emotion/hash': 0.9.2
+      '@emotion/unitless': 0.8.1
       '@simonwep/pickr': 1.8.2
       array-tree-filter: 2.1.0
       async-validator: 4.2.5
+      csstype: 3.1.3
       dayjs: 1.11.5
       dom-align: 1.12.3
       dom-scroll-into-view: 2.0.1
@@ -371,6 +384,8 @@ packages:
       resize-observer-polyfill: 1.5.1
       scroll-into-view-if-needed: 2.2.29
       shallow-equal: 1.2.1
+      stylis: 4.3.4
+      throttle-debounce: 5.0.2
       vue: 3.2.37
       vue-types: 3.0.2_vue@3.2.37
       warning: 4.0.3
@@ -468,6 +483,10 @@ packages:
   /csstype/2.6.20:
     resolution: {integrity: sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==}
 
+  /csstype/3.1.3:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+    dev: false
+
   /dayjs/1.11.5:
     resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
     dev: false
@@ -1071,10 +1090,19 @@ packages:
   /sourcemap-codec/1.4.8:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
 
+  /stylis/4.3.4:
+    resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==}
+    dev: false
+
   /supports-preserve-symlinks-flag/1.0.0:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
 
+  /throttle-debounce/5.0.2:
+    resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
+    engines: {node: '>=12.22'}
+    dev: false
+
   /to-fast-properties/2.0.0:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}

+ 6 - 0
src/api/constant.ts

@@ -32,6 +32,12 @@ export const INSERT_TAGGING = `/fusion/caseTag/add`
 export const UPDATE_TAGGING = `/fusion/caseTag/update`
 export const DELETE_TAGGING = `/fusion/caseTag/delete`
 
+// 路线列表
+export const PATH_LIST = `/fusion/path/allList`
+export const INSERT_PATH = `/fusion/path/add`
+export const UPDATE_PATH = `/fusion/path/update`
+export const DELETE_PATH = `/fusion/path/delete`
+
 // 标签放置列表
 export const TAGGING_POINT_LIST = `/fusion/caseTagPoint/allList`
 export const INSERT_TAGGING_POINT = `/fusion/caseTagPoint/place`

+ 2 - 1
src/api/index.ts

@@ -34,4 +34,5 @@ export * from './record-fragment'
 export * from './view'
 export * from './folder-type'
 export * from './floder'
-export * from './setting'
+export * from './setting'
+export * from './path'

+ 68 - 0
src/api/path.ts

@@ -0,0 +1,68 @@
+import axios from "./instance";
+import { params } from "@/env";
+import { PATH_LIST, DELETE_PATH, INSERT_PATH, UPDATE_PATH } from "./constant";
+
+interface ServerPath {
+  id: number;
+  path: string;
+}
+
+export interface Path {
+  id: string;
+  name: string;
+  linePosition?: {
+    loc: SceneLocalPos;
+    modelId: string;
+  };
+  lineWidth: number;
+  lineColor: string;
+  lineAltitudeAboveGround: number
+  fontSize: number;
+  autoAdjust: boolean;
+  globalVisibility: boolean;
+  points: {
+    name?: string;
+    position: {
+      loc: SceneLocalPos;
+      modelId: string;
+    };
+  }[];
+}
+
+export type Paths = Path[];
+
+const serviceToLocal = (servicePath: ServerPath): Path => ({
+  ...JSON.parse(servicePath.path),
+  id: servicePath.id.toString(),
+});
+
+const localToService = (path: Path): ServerPath => ({
+  id: Number(path.id),
+  path: JSON.stringify(path),
+});
+
+export const fetchPaths = async () => {
+  const staggings = await axios.get<ServerPath[]>(PATH_LIST, {
+    params: { caseId: params.caseId },
+  });
+  return staggings.map(serviceToLocal);
+};
+
+export const postAddPath = async (path: Path) => {
+  const stagging = await axios.post<ServerPath>(INSERT_PATH, {
+    ...localToService(path),
+    caseId: params.caseId,
+  });
+  return serviceToLocal(stagging);
+};
+
+export const postUpdatePath = (path: Path) => {
+  return axios.post<undefined>(UPDATE_PATH, {
+    ...localToService(path),
+    caseId: params.caseId,
+  });
+};
+
+export const postDeletePath = (id: Path["id"]) => {
+  return axios.post<undefined>(DELETE_PATH, { pathId: id });
+};

+ 31 - 1
src/api/tagging-position.ts

@@ -8,6 +8,7 @@ import {
 
 import type { FuseModel } from './fuse-model'
 import type { Tagging } from './tagging'
+import { Tagging3DProps } from '@/sdk'
 
 interface ServicePosition {
   "tagPointId": number,
@@ -15,13 +16,29 @@ interface ServicePosition {
   "fusionNumId": number
   "modelId"?: number
   "tagPoint": string,
+  normal: string
+  
+  globalVisibility: boolean
+  altitudeAboveGround: number
+  type: string,
+  mat: string
 }
 
+export enum TaggingPositionType {
+  '2d' = '2d',
+  '3d' = '3d'
+}
 export interface TaggingPosition {
   id: string,
   taggingId: Tagging['id']
   modelId: FuseModel['id']
+  normal: SceneLocalPos,
   localPos: SceneLocalPos
+  globalVisibility: boolean
+
+  altitudeAboveGround: number
+  type: TaggingPositionType
+  mat: Tagging3DProps['mat']
 }
 
 export type TaggingPositions = TaggingPosition[]
@@ -31,7 +48,15 @@ const serviceToLocal = (position: ServicePosition, taggingId?: Tagging['id']): T
   id: position.tagPointId.toString(),
   modelId: position.fusionNumId ? position.fusionNumId.toString() : position.modelId ? position.modelId.toString() : '123123',
   taggingId: taggingId || position.tagId.toString(),
-  localPos: JSON.parse(position.tagPoint)
+  localPos: JSON.parse(position.tagPoint),
+  type: (position.type || TaggingPositionType['2d']) as TaggingPositionType,
+  globalVisibility: position.globalVisibility,
+  normal: position.normal ? JSON.parse(position.normal) : { x: 0, y: 0, z: 1 },
+  mat: position.mat ? JSON.parse(position.mat) : {
+    scale: {x: 1, y: 1, z: 1},
+    rotation: {x: 0, y: 0, z: 0, w: 1}
+  },
+  altitudeAboveGround: position.altitudeAboveGround
 })
 
 const localToService = (position: TaggingPosition, update = false): PartialProps<ServicePosition, 'tagPointId'> => ({
@@ -39,6 +64,11 @@ const localToService = (position: TaggingPosition, update = false): PartialProps
   "tagId": Number(position.taggingId),
   "fusionNumId": Number(position.modelId),
   "tagPoint": JSON.stringify(position.localPos),
+  globalVisibility: position.globalVisibility,
+  type: position.type,
+  mat: position.mat && JSON.stringify(position.mat),
+  altitudeAboveGround: position.altitudeAboveGround,
+  normal: JSON.stringify(position.normal)
 })
 
 

+ 88 - 39
src/api/tagging-style.ts

@@ -1,63 +1,112 @@
-import axios from './instance'
-import { TAGGING_STYLE_LIST, INSERT_TAGGING_STYLE, DELETE_TAGGING_STYLE, UPLOAD_HEADS } from './constant'
-import { jsonToForm } from '@/utils'
-import { params } from '@/env'
+import axios from "./instance";
+import {
+  TAGGING_STYLE_LIST,
+  INSERT_TAGGING_STYLE,
+  DELETE_TAGGING_STYLE,
+  UPLOAD_HEADS,
+} from "./constant";
+import { jsonToForm } from "@/utils";
+import { params } from "@/env";
 interface ServiceStyle {
-  iconId: number,
-  iconTitle: string,
-  iconUrl: string,
-  isSystem: number
-  lastUse: 1 | 0
+  iconId: number;
+  iconTitle: string;
+  iconUrl: string;
+  isSystem: number;
+  lastUse: 1 | 0;
+}
+
+export const defStyleType = {
+  id: 8,
+  name: "其他",
+}
+export const styleTypes = [
+  {
+    id: 1,
+    name: "痕迹",
+    children: [
+      { id: 2, name: "手印" },
+      { id: 3, name: "足迹" },
+      { id: 4, name: "血迹 " },
+      { id: 5, name: "尸体" },
+      { id: 6, name: "其他" },
+    ],
+  },
+  {
+    id: 7,
+    name: "物证",
+  },
+  defStyleType
+];
+
+export const getStyleTypeName = (id: number, all = styleTypes): string => {
+  for (const item of all) {
+    if (id === item.id) {
+      return item.name
+    } else if ('children' in item) {
+      const cname = getStyleTypeName(id, item.children)
+      if (cname) {
+        return cname
+      }
+    }
+  }
+  if (all === styleTypes) {
+    return defStyleType.name
+  } else {
+    return ''
+  }
 }
 
 export interface TaggingStyle {
-  id: string
-  icon: string
-  name: string
-  lastUse: 1 | 0
-  default: boolean
+  id: string;
+  icon: string;
+  typeId: number;
+  lastUse: 1 | 0;
+  default: boolean;
 }
 
-const toLocal = (serviceStyle: ServiceStyle) : TaggingStyle => ({
+const toLocal = (serviceStyle: ServiceStyle): TaggingStyle => ({
   id: serviceStyle.iconId.toString(),
   lastUse: serviceStyle.lastUse,
-  name: serviceStyle.iconTitle,
+  typeId: Number(serviceStyle.iconTitle) || defStyleType.id,
   icon: serviceStyle.iconUrl,
-  default: Boolean(serviceStyle.isSystem)
-})
+  default: Boolean(serviceStyle.isSystem),
+});
 
 const toService = (style: TaggingStyle): ServiceStyle => ({
   iconId: Number(style.id),
-  iconTitle: style.name,
+  iconTitle: style.typeId.toString(),
   lastUse: style.lastUse,
   iconUrl: style.icon,
-  isSystem: Number(style.default)
+  isSystem: Number(style.default),
+});
 
-})
-
-export type TaggingStyles = TaggingStyle[]
+export type TaggingStyles = TaggingStyle[];
 
 export const fetchTaggingStyles = async () => {
-  const reqParams = params.share ? { caseId: params.caseId } : { }
-  const data = await axios.get<ServiceStyle[]>(TAGGING_STYLE_LIST, { params: reqParams })
-  return data.map(toLocal)
-}
+  const reqParams = params.share ? { caseId: params.caseId } : {};
+  const data = await axios.get<ServiceStyle[]>(TAGGING_STYLE_LIST, {
+    params: reqParams,
+  });
+  return data.map(toLocal);
+};
 
-export const postAddTaggingStyle = async (props: {file: Blob, iconTitle: string}) => {
-  
+export const postAddTaggingStyle = async (props: {
+  file: Blob;
+  iconTitle: string;
+}) => {
   const data = await axios<ServiceStyle>({
-    method: 'POST',
+    method: "POST",
     headers: UPLOAD_HEADS,
-    url: INSERT_TAGGING_STYLE, 
+    url: INSERT_TAGGING_STYLE,
     data: jsonToForm({
       file: new File([props.file], `${props.iconTitle}.png`),
       iconTitle: props.iconTitle,
-      caseId: params.caseId
-    })
-  })
-  return toLocal(data)
-}
+      caseId: params.caseId,
+    }),
+  });
+  return toLocal(data);
+};
 
-export const postDeleteTaggingStyle = async (id: TaggingStyle['id']) => {
-  await axios.post(DELETE_TAGGING_STYLE, { iconId: Number(id) })
-}
+export const postDeleteTaggingStyle = async (id: TaggingStyle["id"]) => {
+  await axios.post(DELETE_TAGGING_STYLE, { iconId: Number(id) });
+};

+ 9 - 3
src/api/tagging.ts

@@ -8,8 +8,9 @@ import {
 } from './constant'
 
 import type { FuseModel } from './fuse-model'
-import type { TaggingPosition } from './tagging-position'
+import { TaggingPosition, TaggingPositionType } from './tagging-position'
 import type { TaggingStyle } from './tagging-style'
+import { Tagging3DProps } from '@/sdk'
 
 interface ServerTagging {
   "hotIconId": number,
@@ -21,12 +22,16 @@ interface ServerTagging {
   "leaveBehind": string,
   "tagDescribe": string,
   "tagTitle": string,
+
+  show3dTitle: boolean
+
 }
 
 export interface Tagging {
   id: string
   styleId: TaggingStyle['id']
   title: string,
+  show3dTitle: boolean
   desc: string
   part: string
   method: string
@@ -34,7 +39,6 @@ export interface Tagging {
   images: string[],
 }
 
-
 export type Taggings = Tagging[]
 
 
@@ -44,15 +48,17 @@ const serviceToLocal = (serviceTagging: ServerTagging): Tagging => ({
   title: serviceTagging.tagTitle,
   desc: serviceTagging.tagDescribe,
   part: serviceTagging.leaveBehind,
+  show3dTitle: serviceTagging.show3dTitle,
   method: serviceTagging.getMethod,
   principal: serviceTagging.getUser,
-  images: JSON.parse(serviceTagging.tagImgUrl)
+  images: JSON.parse(serviceTagging.tagImgUrl),
 })
 
 const localToService = (tagging: Tagging, update = false): PartialProps<ServerTagging, 'tagId' | 'hotIconUrl'> & { fusionId: number } => ({
   "hotIconId": Number(tagging.styleId),
   "fusionId": params.caseId,
   "getMethod": tagging.method,
+  show3dTitle: tagging.show3dTitle,
   "getUser": tagging.principal,
   "hotIconUrl": "static/img_default/lQLPDhrvVzvNvTswMLAOU-UNqYnnZQG1YPJUwLwA_48_48.png",
   "tagId": update ? Number(tagging.id) : undefined,

+ 31 - 17
src/app.vue

@@ -1,21 +1,23 @@
 <template>
-  <ui-editor-layout
-    @click.stop
-    id="layout-app"
-    class="editor-layout"
-    :style="layoutStyles"
-    :class="layoutClassNames"
-  >
-    <div :ref="(el: any) => appEl = (el as HTMLDivElement)" v-if="loaded">
-      <router-view v-slot="{ Component }">
-        <keep-alive>
-          <component :is="Component" />
-        </keep-alive>
-      </router-view>
-    </div>
-  </ui-editor-layout>
-
-  <PwdModel v-if="inputPwd" @close="inputPwd = false" />
+  <ConfigProvider :theme="token">
+    <ui-editor-layout
+      @click.stop
+      id="layout-app"
+      class="editor-layout"
+      :style="layoutStyles"
+      :class="layoutClassNames"
+    >
+      <div :ref="(el: any) => appEl = (el as HTMLDivElement)" v-if="loaded">
+        <router-view v-slot="{ Component }">
+          <keep-alive>
+            <component :is="Component" />
+          </keep-alive>
+        </router-view>
+      </div>
+    </ui-editor-layout>
+
+    <PwdModel v-if="inputPwd" @close="inputPwd = false" />
+  </ConfigProvider>
 </template>
 
 <script lang="ts" setup>
@@ -24,7 +26,19 @@ import { computed, ref, watch, watchEffect, nextTick } from "vue";
 import { isEdit, prefix, appEl, initialSetting, caseProject, refreshCase } from "@/store";
 import router, { currentLayout, RoutesName } from "./router";
 import { loadPack } from "@/utils";
+import { ConfigProvider } from "ant-design-vue";
 import PwdModel from "@/layout/pwd.vue";
+import { theme } from "ant-design-vue";
+
+const token = {
+  algorithm: theme.darkAlgorithm,
+  token: {
+    colorPrimary: "#00c8af",
+    fontSize: 14,
+    wireframe: true,
+    colorInfo: "#00c8af",
+  },
+};
 
 const loaded = ref(false);
 const inputPwd = ref(false);

+ 3 - 0
src/assets/style/fire.css

@@ -0,0 +1,3 @@
+.ant-modal-root .model-header .header-desc span {
+  color: #D8000A;
+}

+ 1 - 1
src/assets/style/fire.less

@@ -1,5 +1,5 @@
 .ant-modal-root {
-  @import 'ant-design-vue/dist/antd.less';
+  // @import 'ant-design-vue/dist/antd.less';
   @primary-color: #D8000A;
   @menu-item-active-bg: #E6F7FF;
   @table-selected-row-bg: #E6F7FF;

+ 11 - 0
src/assets/style/global.css

@@ -0,0 +1,11 @@
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  color: #fff;
+}
+:where(.css-dev-only-do-not-override-cf1j00).ant-popover .ant-popover-inner-content {
+  padding: 0;
+}

+ 18 - 0
src/assets/style/global.less

@@ -0,0 +1,18 @@
+// @import 'ant-design-vue/dist/antd.less';
+
+// @primary-color: #00c8af;
+// @menu-item-active-bg: #E6F7FF;
+// @table-selected-row-bg: #E6F7FF;
+// .ant-table-tbody > tr.ant-table-row-selected > td {
+//   background: none;
+// }
+// .model-header .header-desc span {
+//   color: @primary-color;
+// }
+
+h1, h2, h3, h4, h5, h6 {
+  color: #fff;
+}
+:where(.css-dev-only-do-not-override-cf1j00).ant-popover .ant-popover-inner-content {
+  padding: 0;
+}

+ 26 - 0
src/components/path/list.vue

@@ -0,0 +1,26 @@
+<template>
+  <template v-for="(path, index) in paths" :key="path.id">
+    <Sign
+      v-if="getPathIsShow(path) && path.points.length"
+      @delete="deletePath(path)"
+      @changePosition="(data: any) => updatePosition(index, data)"
+      :path="path"
+      :key="path.id"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import Sign from "./sign.vue";
+import { paths, getPathIsShow } from "@/store";
+
+import type { Path } from "@/store";
+
+const deletePath = (path: Path) => {
+  const index = paths.value.indexOf(path);
+  paths.value.splice(index, 1);
+};
+const updatePosition = (ndx: number, data: Path) => {
+  paths.value[ndx] = data;
+};
+</script>

+ 111 - 0
src/components/path/sign.vue

@@ -0,0 +1,111 @@
+<template>
+  <template v-if="show">
+    <div :style="linePixel">
+      {{ path.name }}
+    </div>
+    <template v-for="(p, i) in pointPixels">
+      <span :style="p" v-if="p && path.points[i].name">
+        {{ path.points[i].name }}
+      </span>
+    </template>
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch, watchEffect } from "vue";
+import { router, RoutesName } from "@/router";
+import { sdk } from "@/sdk";
+
+import type { Path } from "@/store";
+import { usePixel, usePixels } from "@/hook/use-pixel";
+
+export type SignProps = { path: Path };
+
+const props = defineProps<SignProps>();
+const emit = defineEmits<{
+  (e: "delete"): void;
+  (e: "updatePoints", val: Path["points"]): void;
+  (e: "updateLinePosition", val: SceneLocalPos): void;
+}>();
+
+const path = sdk.createPath({
+  line: {
+    width: props.path.lineWidth,
+    color: props.path.lineColor,
+    altitudeAboveGround: props.path.lineAltitudeAboveGround,
+    position: props.path.linePosition?.loc || props.path.points[0].position.loc,
+    modelId: props.path.linePosition?.modelId || props.path.points[0].position.modelId,
+  },
+  points: props.path.points.map((item) => ({
+    position: item.position.loc,
+    modelId: item.position.modelId,
+  })),
+});
+
+watchEffect(() => {
+  path.changeCanEdit(router.currentRoute.value.name === RoutesName.paths);
+});
+
+watch(
+  () => ({
+    width: props.path.lineWidth,
+    color: props.path.lineColor,
+    altitudeAboveGround: props.path.lineAltitudeAboveGround,
+    position: props.path.linePosition?.loc || props.path.points[0].position.loc,
+    modelId: props.path.linePosition?.modelId || props.path.points[0].position.modelId,
+  }),
+  (val) => {
+    path.changeLine(val);
+  },
+  { deep: true }
+);
+
+const toCameraDistance = ref(path.toCameraDistance);
+path.bus.on("toCameraDistanceChange", (v) => (toCameraDistance.value = v));
+const show = computed(() => props.path.globalVisibility || toCameraDistance.value <= 30);
+watchEffect(() => path.visibility(show.value));
+
+path.bus.on("changePoints", (points) => {
+  pathPoints.value = points.map((p) => ({
+    localPos: p.position,
+    modelId: p.modelId,
+  }));
+
+  emit(
+    "updatePoints",
+    points.map((p, i) => ({
+      ...props.path.points[i],
+      position: {
+        loc: p.position,
+        modelId: p.modelId,
+      },
+    }))
+  );
+});
+
+const isHover = ref(false);
+path.bus.on("enter", () => {
+  isHover.value = true;
+});
+path.bus.on("leave", () => {
+  isHover.value = false;
+});
+
+const [linePixel] = usePixel(() => ({
+  localPos: props.path.linePosition?.loc || props.path.points[0].position.loc,
+  modelId: props.path.linePosition?.modelId || props.path.points[0].position.modelId,
+}));
+
+const [pointPixels, pathPoints] = usePixels(() => {
+  return props.path.points.map((item) => ({
+    localPos: item.position.loc,
+    modelId: item.position.modelId,
+  }));
+});
+
+path.bus.on("lineTopPositionChange", (pos) => {
+  emit("updateLinePosition", pos);
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 20 - 15
src/components/tagging/list.vue

@@ -1,8 +1,9 @@
 <template>
   <template v-for="(pos, index) in positions" :key="pos.id">
-    <Sign 
+    <Sign
       v-if="getTaggingPositionIsShow(pos)"
       @delete="deletePosition(pos)"
+      @changePosition="(data: any) => updatePosition(index, data)"
       :tagging="tagging"
       :scene-pos="pos"
       :key="pos.id"
@@ -11,21 +12,25 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue'
-import Sign from './sign.vue'
-import { 
-  getTaggingPositions, 
-  taggingPositions,
-  getTaggingPositionIsShow
-} from '@/store';
+import { computed } from "vue";
+import Sign from "./sign-new.vue";
+import { getTaggingPositions, taggingPositions, getTaggingPositionIsShow } from "@/store";
 
-import type { Tagging, TaggingPosition } from '@/store';
+import type { Tagging, TaggingPosition } from "@/store";
 
-const props = defineProps<{ tagging: Tagging }>()
-const positions = computed(() => getTaggingPositions(props.tagging))
+const props = defineProps<{ tagging: Tagging }>();
+const positions = computed(() => getTaggingPositions(props.tagging));
 
 const deletePosition = (pos: TaggingPosition) => {
-  const index = taggingPositions.value.indexOf(pos)
-  taggingPositions.value.splice(index, 1)
-}
-</script>
+  const index = taggingPositions.value.indexOf(pos);
+  taggingPositions.value.splice(index, 1);
+};
+const updatePosition = (
+  ndx: number,
+  data: { pos: SceneLocalPos; modelId: string; normal: SceneLocalPos }
+) => {
+  taggingPositions.value[ndx].localPos = data.pos;
+  taggingPositions.value[ndx].modelId = data.modelId;
+  taggingPositions.value[ndx].normal = data.normal;
+};
+</script>

+ 254 - 0
src/components/tagging/sign-new.vue

@@ -0,0 +1,254 @@
+<template>
+  <div
+    v-if="show && posStyle"
+    class="hot-item pc"
+    :style="posStyle"
+    @mouseenter="isHover = true"
+    @mouseleave="isHover = false"
+  >
+    <!-- <div class="title tag-tip">{{ tagging.title }}</div> -->
+    <div @click.stop>
+      <UIBubble class="hot-bubble pc" :show="showContent" type="left" level="center">
+        <h2>{{ tagging.title }}</h2>
+        <div class="content">
+          <p><span>特征描述:</span>{{ tagging.desc }}</p>
+          <p><span>遗留部位:</span>{{ tagging.part }}</p>
+          <p><span>提取方法:</span>{{ tagging.method }}</p>
+          <p><span>提取人:</span>{{ tagging.principal }}</p>
+        </div>
+        <Images
+          :tagging="tagging"
+          :in-full="true"
+          @pull="(index) => (pullIndex = index)"
+        />
+        <div
+          class="edit-hot"
+          v-if="router.currentRoute.value.name === RoutesName.tagging"
+        >
+          <span @click="$emit('delete')" class="fun-ctrl">
+            <ui-icon type="del" />
+            删除
+          </span>
+        </div>
+      </UIBubble>
+
+      <Preview
+        @close="pullIndex = -1"
+        :current="pullIndex"
+        :items="queryItems"
+        v-if="!!~pullIndex"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch, watchEffect } from "vue";
+import { router, RoutesName } from "@/router";
+import UIBubble from "bill/components/bubble/index.vue";
+import Images from "@/views/tagging/images.vue";
+import Preview, { MediaType } from "../static-preview/index.vue";
+import { getTaggingStyle } from "@/store";
+import { getFileUrl } from "@/utils";
+import { sdk } from "@/sdk";
+import { custom, getResource } from "@/env";
+
+import type { Tagging, TaggingPosition } from "@/store";
+import { usePixel } from "@/hook/use-pixel";
+
+export type SignProps = { tagging: Tagging; scenePos: TaggingPosition; show?: boolean };
+
+const props = defineProps<SignProps>();
+const emit = defineEmits<{
+  (e: "delete"): void;
+  (
+    e: "changePosition",
+    val: { pos: SceneLocalPos; modelId: string; normal: SceneLocalPos }
+  ): void;
+}>();
+
+const queryItems = computed(() =>
+  props.tagging.images.map((image) => ({
+    type: MediaType.img,
+    url: getResource(getFileUrl(image)),
+  }))
+);
+const taggingStyle = computed(() => getTaggingStyle(props.tagging.styleId));
+
+const tag = sdk.createTagging({
+  title: props.tagging.title,
+  type: props.scenePos.type,
+  mat: props.scenePos.mat,
+  altitudeAboveGround: props.scenePos.altitudeAboveGround,
+  position: props.scenePos.localPos,
+  modelId: props.scenePos.modelId,
+  normal: props.scenePos.normal,
+  canMove: false,
+  image: getFileUrl(taggingStyle.value!.icon),
+});
+
+watch(taggingStyle, (icon) => icon && tag.changeImage(getFileUrl(icon.icon)));
+watchEffect(() => {
+  tag.changeCanMove(router.currentRoute.value.name === RoutesName.tagging);
+});
+watch(
+  () => props.scenePos.mat,
+  (val) => {
+    tag.changeMat(val);
+  },
+  { deep: true }
+);
+watch(
+  () => props.scenePos.altitudeAboveGround,
+  (val) => {
+    tag.changeAltitudeAboveGround(val);
+  },
+  { deep: true }
+);
+watch(
+  () => props.tagging.title,
+  (val) => {
+    tag.changeTitle(val);
+  },
+  { deep: true }
+);
+watch(
+  () => props.tagging.show3dTitle,
+  (val) => {
+    tag.visibilityTitle(val);
+  },
+  { deep: true }
+);
+watch(
+  () => props.scenePos.type,
+  (val) => {
+    tag.changeType(val);
+    changePos();
+  },
+  { deep: true }
+);
+
+const toCameraDistance = ref(tag.toCameraDistance);
+tag.bus.on("toCameraDistanceChange", (v) => (toCameraDistance.value = v));
+
+const show = computed(
+  () => props.scenePos.globalVisibility || toCameraDistance.value <= 30
+);
+watchEffect(() => tag.visibility(show.value));
+
+tag.bus.on("changePosition", (data) => {
+  emit("changePosition", data);
+  changePos();
+});
+
+const isHover = ref(false);
+tag.bus.on("enter", () => {
+  isHover.value = true;
+});
+tag.bus.on("leave", () => {
+  isHover.value = false;
+});
+tag.bus.on("click", () => iconClickHandler());
+
+const [posStyle, pos] = usePixel(() => ({
+  localPos: { x: 0, y: 0, z: 0 },
+  modelId: "0",
+}));
+
+const changePos = () => {
+  const c = tag.getImageCenter();
+  pos.value = {
+    localPos: c.pos,
+    modelId: c.modelId,
+  };
+};
+changePos();
+
+const showContent = computed(() => {
+  return (
+    !~pullIndex.value &&
+    (isHover.value || custom.showTaggingPositions.has(props.scenePos))
+  );
+});
+
+const pullIndex = ref(-1);
+const iconClickHandler = () => {
+  if (custom.showTaggingPositions.has(props.scenePos)) {
+    custom.showTaggingPositions.delete(props.scenePos);
+  } else {
+    custom.showTaggingPositions.add(props.scenePos);
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.hot-item {
+  pointer-events: all;
+  position: absolute;
+  transform: translate(-50%, -100%);
+  cursor: pointer;
+
+  .tag-img {
+    width: 32px;
+    height: 32px;
+  }
+
+  .hot-bubble {
+    cursor: initial;
+
+    &.pc {
+      width: 400px;
+    }
+    &:not(.pc) {
+      width: 80vw;
+      --bottom-left: 40vw;
+    }
+
+    h2 {
+      font-size: 20px;
+      margin-bottom: 10px;
+      color: #ffffff;
+      position: relative;
+    }
+
+    .content {
+      font-size: 14px;
+      font-family: MicrosoftYaHei;
+      color: #999999;
+      line-height: 1.35em;
+      margin-bottom: 20px;
+      word-break: break-all;
+
+      p {
+        margin-bottom: 10px;
+      }
+    }
+  }
+
+  &.active,
+  &:hover {
+    z-index: 3;
+  }
+}
+
+.edit-hot {
+  margin-top: 20px;
+  text-align: right;
+
+  span {
+    font-size: 14px;
+    color: rgba(255, 255, 255, 0.6);
+    cursor: pointer;
+  }
+}
+</style>
+
+<style>
+.tag-tip {
+  z-index: 8 !important;
+}
+.tag-tip p {
+  padding: 6px 10px !important;
+  margin: 5px 0 !important;
+}
+</style>

+ 71 - 72
src/components/tagging/sign.vue

@@ -1,40 +1,38 @@
 <template>
-  <div 
+  <div
     v-if="posStyle"
-    class="hot-item pc" 
-    :style="posStyle" 
+    class="hot-item pc"
+    :style="posStyle"
     @mouseenter="isHover = true"
     @mouseleave="isHover = false"
-    :class="{active: showContent}"
+    :class="{ active: showContent }"
   >
     <ui-tip :tip="tagging.title" foreShow tipV="top" class="tag-tip">
-      <img 
+      <img
         class="tag-img"
-        :src="getResource(getFileUrl(taggingStyle.icon))" 
-        @click="iconClickHandler" 
-        v-if="taggingStyle" 
+        :src="getResource(getFileUrl(taggingStyle.icon))"
+        @click="iconClickHandler"
+        v-if="taggingStyle"
       />
     </ui-tip>
     <div @click.stop>
-      <UIBubble
-        class="hot-bubble pc" 
-        :show="showContent" 
-        type="left" 
-        level="center"
-      >
-        <h2>{{ tagging.title }} </h2>
+      <UIBubble class="hot-bubble pc" :show="showContent" type="left" level="center">
+        <h2>{{ tagging.title }}</h2>
         <div class="content">
           <p><span>特征描述:</span>{{ tagging.desc }}</p>
           <p><span>遗留部位:</span>{{ tagging.part }}</p>
           <p><span>提取方法:</span>{{ tagging.method }}</p>
           <p><span>提取人:</span>{{ tagging.principal }}</p>
         </div>
-        <Images 
-          :tagging="tagging" 
-          :in-full="true" 
-          @pull="index => (pullIndex = index)" 
+        <Images
+          :tagging="tagging"
+          :in-full="true"
+          @pull="(index) => (pullIndex = index)"
         />
-        <div class="edit-hot" v-if="router.currentRoute.value.name === RoutesName.tagging">
+        <div
+          class="edit-hot"
+          v-if="router.currentRoute.value.name === RoutesName.tagging"
+        >
           <span @click="$emit('delete')" class="fun-ctrl">
             <ui-icon type="del" />
             删除
@@ -42,85 +40,87 @@
         </div>
       </UIBubble>
 
-      <Preview 
+      <Preview
         @close="pullIndex = -1"
         :current="pullIndex"
         :items="queryItems"
-        v-if="!!~pullIndex" 
+        v-if="!!~pullIndex"
       />
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watchEffect, watch, onUnmounted } from 'vue'
-import { router, RoutesName } from '@/router'
-import UIBubble from 'bill/components/bubble/index.vue'
-import Images from '@/views/tagging/images.vue'
-import Preview, { MediaType } from '../static-preview/index.vue'
-import { getTaggingStyle, getFuseModel } from '@/store';
-import { getFileUrl } from '@/utils'
-import { sdk } from '@/sdk'
-import { custom, getResource } from '@/env'
-import { useViewStack } from '@/hook'
+import { computed, ref, watchEffect, watch, onUnmounted } from "vue";
+import { router, RoutesName } from "@/router";
+import UIBubble from "bill/components/bubble/index.vue";
+import Images from "@/views/tagging/images.vue";
+import Preview, { MediaType } from "../static-preview/index.vue";
+import { getTaggingStyle, getFuseModel } from "@/store";
+import { getFileUrl } from "@/utils";
+import { sdk } from "@/sdk";
+import { custom, getResource } from "@/env";
+import { useViewStack } from "@/hook";
 
-import type { Tagging, TaggingPosition } from '@/store';
+import type { Tagging, TaggingPosition } from "@/store";
 
-export type SignProps = { tagging: Tagging, scenePos: TaggingPosition, show?: boolean }
+export type SignProps = { tagging: Tagging; scenePos: TaggingPosition; show?: boolean };
 
-defineEmits<{ (e: 'delete'): void }>()
+defineEmits<{ (e: "delete"): void }>();
 
-const props = defineProps<SignProps>()
-const posStyle = ref<null | { left: string, top: string}>(null)
+const props = defineProps<SignProps>();
+const posStyle = ref<null | { left: string; top: string }>(null);
 const updatePosStyle = () => {
-  const screenPos = sdk.getScreenByPosition(props.scenePos.localPos, props.scenePos.modelId)
+  const screenPos = sdk.getScreenByPosition(
+    props.scenePos.localPos,
+    props.scenePos.modelId
+  );
   if (!screenPos?.trueSide) {
-    posStyle.value = null
+    posStyle.value = null;
   } else {
     posStyle.value = {
-      left: screenPos.pos.x + 'px',
-      top: screenPos.pos.y + 'px',
-    }
+      left: screenPos.pos.x + "px",
+      top: screenPos.pos.y + "px",
+    };
   }
-}
-
+};
 
 useViewStack(() => {
-  sdk.sceneBus.on('cameraChange', updatePosStyle)
+  sdk.sceneBus.on("cameraChange", updatePosStyle);
   return () => {
-    sdk.sceneBus.off('cameraChange', updatePosStyle)
-  }
-})
-watchEffect(updatePosStyle)
-
-const model = getFuseModel(props.scenePos.modelId)
-model && watch(model, updatePosStyle, { deep: true })
+    sdk.sceneBus.off("cameraChange", updatePosStyle);
+  };
+});
+watchEffect(updatePosStyle);
 
+const model = getFuseModel(props.scenePos.modelId);
+model && watch(model, updatePosStyle, { deep: true });
 
 const showContent = computed(() => {
-  return !~pullIndex.value 
-    && (isHover.value || custom.showTaggingPositions.has(props.scenePos))
-})
-
-const taggingStyle = computed(() => getTaggingStyle(props.tagging.styleId)) 
-
-const pullIndex = ref(-1)
-const isHover = ref(false)
-const queryItems = computed(() => 
-  props.tagging.images.map(image => ({
-    type: MediaType.img, 
-    url: getResource(getFileUrl(image))
+  return (
+    !~pullIndex.value &&
+    (isHover.value || custom.showTaggingPositions.has(props.scenePos))
+  );
+});
+
+const taggingStyle = computed(() => getTaggingStyle(props.tagging.styleId));
+
+const pullIndex = ref(-1);
+const isHover = ref(false);
+const queryItems = computed(() =>
+  props.tagging.images.map((image) => ({
+    type: MediaType.img,
+    url: getResource(getFileUrl(image)),
   }))
-)
+);
 
 const iconClickHandler = () => {
   if (custom.showTaggingPositions.has(props.scenePos)) {
-    custom.showTaggingPositions.delete(props.scenePos)
+    custom.showTaggingPositions.delete(props.scenePos);
   } else {
-    custom.showTaggingPositions.add(props.scenePos)
+    custom.showTaggingPositions.add(props.scenePos);
   }
-}
-
+};
 </script>
 
 <style lang="scss" scoped>
@@ -149,7 +149,7 @@ const iconClickHandler = () => {
     h2 {
       font-size: 20px;
       margin-bottom: 10px;
-      color: #FFFFFF;
+      color: #ffffff;
       position: relative;
     }
 
@@ -183,7 +183,6 @@ const iconClickHandler = () => {
     cursor: pointer;
   }
 }
-
 </style>
 
 <style>
@@ -194,4 +193,4 @@ const iconClickHandler = () => {
   padding: 6px 10px !important;
   margin: 5px 0 !important;
 }
-</style>
+</style>

+ 2 - 0
src/env/index.ts

@@ -14,6 +14,7 @@ export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true))
 export const showBottomBarStack = stackFactory(ref<boolean>(false), true)
 export const bottomBarHeightStack = stackFactory(ref<string>('60px'))
 export const showTaggingsStack = stackFactory(ref<boolean>(true))
+export const showPathsStack = stackFactory(ref<boolean>(true))
 export const showMeasuresStack = stackFactory(ref<boolean>(true))
 export const currentModelStack = stackFactory(ref<FuseModel | null>(null))
 export const showModelsMapStack = stackFactory(ref<WeakMap<FuseModel, boolean>>(new WeakMap()), true)
@@ -29,6 +30,7 @@ export const custom = flatStacksValue({
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
   showTaggings: showTaggingsStack,
+  showPaths: showPathsStack,
   showMeasures: showMeasuresStack,
   currentModel: currentModelStack,
   showModelsMap: showModelsMapStack,

+ 79 - 0
src/hook/use-pixel.ts

@@ -0,0 +1,79 @@
+import sdk from "@/sdk";
+import { ref, Ref, watch, watchEffect } from "vue";
+import { useViewStack } from "./viewStack";
+
+export const usePixel = (
+  getter: () => ({ localPos: SceneLocalPos; modelId: string } | undefined)
+) => {
+  const pos = ref(getter())
+  watch(getter, val => pos.value = val)
+  const pixel = ref<{ left: string; top: string }>();
+  const updatePosStyle = () => {
+    if (!pos.value) {
+      pixel.value = void 0;
+      return;
+    }
+    const screenPos = sdk.getScreenByPosition(
+      pos.value.localPos,
+      pos.value.modelId
+    );
+    if (!screenPos) {
+      pixel.value = void 0;
+      return;
+    }
+
+    pixel.value = {
+      left: screenPos.pos.x + "px",
+      top: screenPos.pos.y + "px",
+    };
+  };
+  watch(pos, updatePosStyle, {deep: true})
+
+  useViewStack(() => {
+    sdk.sceneBus.on("cameraChange", updatePosStyle);
+    return () => {
+      sdk.sceneBus.off("cameraChange", updatePosStyle);
+    };
+  });
+
+  return [pixel, pos] as const;
+};
+
+
+export const usePixels = (
+  getter: () => ({ localPos: SceneLocalPos; modelId: string }[])
+) => {
+  const positions = ref(getter())
+  watch(getter, val => {
+    positions.value = val
+  })
+  const pixels = ref<({ left: string; top: string } | null)[]>();
+  const updatePosStyle = () => {
+    pixels.value = []
+    for (let i = 0; i < positions.value.length; i++) {
+      const pos = positions.value[i]
+      const screenPos = sdk.getScreenByPosition(
+        pos.localPos,
+        pos.modelId
+      );
+      if (screenPos) {
+        pixels.value[i] = {
+          left: screenPos.pos.x + "px",
+          top: screenPos.pos.y + "px",
+        };
+      } else {
+        pixels.value[i] = screenPos
+      }
+    }
+  };
+  watch(positions, updatePosStyle, {deep: true})
+
+  useViewStack(() => {
+    sdk.sceneBus.on("cameraChange", updatePosStyle);
+    return () => {
+      sdk.sceneBus.off("cameraChange", updatePosStyle);
+    };
+  });
+
+  return [pixels, positions] as const;
+};

+ 3 - 2
src/main.ts

@@ -7,7 +7,8 @@ import { appStyleImport, appType, params } from '@/env'
 import { addHook, addUnsetTokenURLS, delHook, delUnsetTokenURLS } from '@/api'
 import { currentLayout, RoutesName } from './router';
 import * as URL from '@/api/constant'
-
+// import 'ant-design-vue/dist/reset.css';
+import '@/assets/style/global.less'
 
 const app = createApp(App)
 app.use(Components)
@@ -18,7 +19,7 @@ if (import.meta.env.DEV) {
   import('@/hook/notice')
 }
 
-appStyleImport[params.app]()
+appStyleImport[params.app] && appStyleImport[params.app]()
 watchEffect((onCleanup) => {
   if ([RoutesName.show, RoutesName.signModel].includes(currentLayout.value!)) {
 

+ 1 - 0
src/router/constant.ts

@@ -10,6 +10,7 @@ export enum RoutesName {
   fuseEditSwitch = "fuseEditSwitch",
   merge = "merge",
   tagging = "tagging",
+  paths = "paths",
   taggingPosition = "taggingPosition",
   guide = "guide",
   measure = "measure",

+ 2 - 0
src/sdk/association/index.ts

@@ -11,6 +11,7 @@ import { associationTaggings } from "./tagging";
 import { associationSetting } from "./setting";
 import { associationMessaures } from "./measure";
 import { custom } from "@/env";
+import { associationPaths } from "./path";
 
 export const getSupperPanoModel = () => {
   const supperModel = ref<FuseModel | null>(null);
@@ -117,6 +118,7 @@ export const setupAssociation = (mountEl: HTMLDivElement, sdk: SDK) => {
       associationTaggings(sdk, mountEl);
       associationMessaures(sdk);
       associationSetting(sdk, mountEl);
+      associationPaths(sdk, mountEl)
       nextTick(() => stopWatch());
     }
   });

+ 9 - 0
src/sdk/association/path.ts

@@ -0,0 +1,9 @@
+import { mount } from "@/utils";
+import TaggingComponent from "@/components/path/list.vue";
+import { SDK } from "../sdk";
+
+// -----------------标签关联--------------------
+
+export const associationPaths = (sdk: SDK, el: HTMLDivElement) => {
+  mount(el, TaggingComponent);
+};

+ 108 - 0
src/sdk/sdk.ts

@@ -13,6 +13,7 @@ import {
   Scene,
 } from "@/store";
 import type { Emitter } from "mitt";
+import { TaggingPositionType } from "@/api";
 export enum SettingResourceType {
   map = "map",
   color = "color",
@@ -152,6 +153,7 @@ export interface SDK {
     panoModelChange: SceneModel;
     modeChange: { mode: "pano" | "fuse"; active: SceneModel };
   }>;
+
   setBackdrop: (
     drop: string,
     type: SettingResourceType,
@@ -192,6 +194,112 @@ export interface SDK {
     modelIds: MeasurePosition["modelId"][]
   ): Measure<T>;
   startMeasure<T extends StoreMeasure["type"]>(type: T): StartMeasure<T>;
+
+  createTagging: (props: Tagging3DProps) => Tagging3D
+
+  createPath: (props: PathProps) => Path
+}
+
+export type PathProps = {
+  line: {
+    width: number,
+    color: string,
+    altitudeAboveGround: number
+    position: SceneLocalPos,
+    modelId: string
+  },
+  points: {
+    position: SceneLocalPos,
+    modelId: string,
+  }[]
+}
+export type Path = {
+  bus: Emitter<{
+    // 标注点击事件
+    click: void;
+    // 鼠标移入标注事件
+    enter: void;
+    // 鼠标移出标注事件
+    leave: void;
+    // 顶部标注中心点像素位置更改事件, 传出像素位置
+    lineTopPositionChange: SceneLocalPos
+    // 路径点位置变更
+    changePoints: PathProps['points']
+    // 距离相机位置变更
+    toCameraDistanceChange: number
+  }>;
+  // 是否可编辑
+  changeCanEdit: (canMove: boolean) => void
+  // 标注可见性
+  visibility: (visibility: boolean) => void
+  // 更改标题气泡属性
+  changeLine: (props: Partial<PathProps['line']>) => void
+  // 顶标注中心像素位置 
+  lineTopPosition: ScreenLocalPos
+  // 距离相机位置
+  toCameraDistance: number
+  // 线段销毁
+  destory: () => void
+}
+
+export type Tagging3DProps = {
+  // 标题
+  title: string
+  // 标注类型 2d | 3d
+  type: TaggingPositionType,
+  // 模型id
+  modelId: string,
+  // 贴地射线获取的位置
+  position: SceneLocalPos
+  normal: SceneLocalPos
+  // 是否可以移动
+  canMove: boolean,
+  // 贴地图片url
+  image: string
+  // 离地高度
+  altitudeAboveGround: number,
+  
+  // 贴地图片的变换
+  mat: {
+    scale: SceneLocalPos,
+    rotation: Rotation,
+  }
+}
+export type Tagging3D = {
+  bus: Emitter<{
+    // 标注点击事件
+    click: void;
+    // 鼠标移入标注事件
+    enter: void;
+    // 鼠标移出标注事件
+    leave: void;
+    // 位置变更
+    changePosition: {pos: SceneLocalPos, modelId: string, normal: SceneLocalPos}
+    // 距离相机位置变更
+    toCameraDistanceChange: number
+  }>;
+  // 设置标题
+  changeTitle: (title: string) => void
+  // 标题是否可见
+  visibilityTitle: (visibility: boolean) => void
+  // 更改可拖拽移动
+  changeCanMove: (canMove: boolean) => void
+  // 获取图标中心三维坐标
+  getImageCenter: () => ({pos: SceneLocalPos, modelId: string})
+  // 更改图标
+  changeImage: (url: string) => void
+  // 标注可见性
+  visibility: (visibility: boolean) => void
+  // 标注图片变换,传入变换
+  changeMat: (props: Tagging3DProps['mat']) => void
+  // 更改离地高度
+  changeAltitudeAboveGround: (val: number) => void
+  // 更改热点类型
+  changeType: (val: TaggingPositionType) => void
+  // 距离相机位置
+  toCameraDistance: number
+  // 标注销毁
+  destory: () => void
 }
 
 export let sdk: SDK;

+ 2 - 1
src/store/index.ts

@@ -13,4 +13,5 @@ export * from './record-fragment'
 export * from './floder'
 export * from './floder-type'
 export * from './setting'
-export * from './case'
+export * from './case'
+export * from './path'

+ 91 - 0
src/store/path.ts

@@ -0,0 +1,91 @@
+import { ref } from 'vue'
+import { autoSetModeCallback, createTemploraryID } from './sys'
+import { getFuseModel, getFuseModelShowVariable } from './fuse-model'
+import { 
+  fetchPaths,
+  postAddPath,
+  postDeletePath,
+  postUpdatePath, 
+} from '@/api'
+import { 
+  deleteStoreItem, 
+  addStoreItem, 
+  updateStoreItem, 
+  fetchStoreItems,
+  saveStoreItems,
+  recoverStoreItems
+} from '@/utils'
+
+
+import type { Path } from '@/api'
+import { custom, params } from '@/env'
+import { Message } from 'bill/expose-common'
+
+export  type { Path } from '@/api'
+export type Paths = Path[]
+
+export const paths = ref<Paths>([])
+export const getPath = (id: Path['id']) => paths.value.find(path => path.id === id)
+
+export const getPathIsShow = (path: Path) => {
+  if (!custom.showPaths) return false;
+  const modelIds = path.points.map(item => item.position.modelId)
+  if (path.linePosition?.modelId) {
+    modelIds.push(path.linePosition.modelId)
+  }
+  return modelIds.every(modelId =>  {
+    const model = getFuseModel(modelId)
+    return model && getFuseModelShowVariable(model).value
+  })
+}
+
+export const createPath = (path: Partial<Path> = {}): Path => {
+  return {
+    id: createTemploraryID(),
+    lineColor: 'ff0000',
+    lineWidth: 3,
+    name: '',
+    fontSize: 1,
+    autoAdjust: false,
+    globalVisibility: false,
+    points: [],
+    ...path
+  }
+}
+
+
+let bcPaths: Paths = []
+export const getBackupPaths = () => bcPaths
+export const backupPaths = () => {
+  bcPaths = paths.value.map(path => ({
+    ...path,
+    points: path.points.map(item => ({...item})),
+  }))
+}
+
+export const initialPath = fetchStoreItems(paths, fetchPaths, backupPaths)
+export const recoverPaths = recoverStoreItems(paths, () => bcPaths)
+export const addPath = addStoreItem(paths, postAddPath)
+export const deletePath = deleteStoreItem(paths, path => postDeletePath(path.id))
+export const updatePath = updateStoreItem(paths, postUpdatePath)
+
+export const savePaths = saveStoreItems(
+  paths, 
+  getBackupPaths,
+  {
+    add: addPath,
+    delete: deletePath,
+    update: updatePath
+  }
+)
+export const autoSavePaths = autoSetModeCallback(paths, {
+  backup: backupPaths,
+  recovery: recoverPaths,
+  save: async () => {
+    if (!paths.value.every(record => record.name)) {
+      Message.warning('路径名称不可为空')
+      throw '路径名称不可为空'
+    }
+    await savePaths()
+  }
+})

+ 9 - 0
src/store/tagging-positions.ts

@@ -7,6 +7,7 @@ import {
   postAddTaggingPosition,
   postDeleteTaggingPosition,
   postUpdateTaggingPosition,
+  TaggingPositionType,
 } from '@/api'
 import { 
   deleteStoreItem, 
@@ -25,6 +26,14 @@ export const taggingPositions = ref<TaggingPositions>([])
 export const createTaggingPosition = (position: Partial<TaggingPosition> = {}): TaggingPosition => ({
   id: createTemploraryID(),
   taggingId: '',
+  globalVisibility: false,
+  altitudeAboveGround: 0.5,
+  mat: {
+    scale: {x: 1, y: 1, z: 1},
+    rotation: {x: 0, y: 0, z: 0, w: 1}
+  },
+  normal: {x: 0, y: 0, z: 1},
+  type: TaggingPositionType['2d'],
   modelId: '',
   localPos: { x: 0, y: 0, z: 0 },
   ...position

+ 3 - 2
src/store/tagging-style.ts

@@ -1,6 +1,7 @@
 import { ref, computed } from 'vue'
 import { autoSetModeCallback, createTemploraryID } from './sys'
 import { 
+  defStyleType,
   fetchTaggingStyles,
   postAddTaggingStyle,
   postDeleteTaggingStyle
@@ -28,7 +29,7 @@ export const createTaggingStyle = (style: Partial<TaggingStyle> = {}): TaggingSt
   icon: '',
   lastUse: 0,
   default: false,
-  name: '',
+  typeId: defStyleType.id,
   ...style
 })
 
@@ -50,7 +51,7 @@ export const addTaggingStyle = addStoreItem(taggingStyles, async (tagging) => {
   if (typeof tagging.icon === 'string') {
     return tagging
   } else {
-    return postAddTaggingStyle({ file: tagging.icon.blob, iconTitle: tagging.name })
+    return postAddTaggingStyle({ file: tagging.icon.blob, iconTitle: tagging.typeId.toString() })
   }
 })
 export const deleteTaggingStyle = deleteStoreItem(taggingStyles, style => postDeleteTaggingStyle(style.id))

+ 2 - 0
src/store/tagging.ts

@@ -22,6 +22,7 @@ import {
   postAddTagging,
   postDeleteTagging,
   postUpdateTagging, 
+  TaggingPositionType, 
   uploadFile
 } from '@/api'
 import { 
@@ -54,6 +55,7 @@ export const createTagging = (tagging: Partial<Tagging> = {}): Tagging => {
     desc: '',
     part: '',
     method: '',
+    show3dTitle: false,
     principal: '',
     images: [],
     ...tagging

+ 38 - 13
src/views/tagging/edit.vue

@@ -5,9 +5,16 @@
         标签
         <ui-icon type="close" ctrl @click.stop="$emit('quit')" class="edit-close" />
       </h3>
+      <div class="style-select">
+        <span>图标样式</span>
+        <span>
+          <StyleTypeSelect v-model:value="type" />
+        </span>
+      </div>
+
       <StylesManage
         :styles="styles"
-        :active="(getTaggingStyle(tagging.styleId) as TaggingStyle)"
+        :active="activeStyle!"
         @change="(style) => (tagging.styleId = style.id)"
         @delete="deleteStyle"
         @uploadStyle="uploadStyle"
@@ -21,6 +28,10 @@
         v-model="tagging.title"
         maxlength="15"
       />
+      <div style="margin-bottom: 10px">
+        <ui-input type="checkbox" label="标题长驻显示" v-model="tagging.show3dTitle" />
+      </div>
+
       <ui-input
         class="input"
         width="100%"
@@ -60,6 +71,7 @@
       >
         <template #preIcon><span>提取人:</span></template>
       </ui-input>
+
       <ui-input
         class="input"
         type="file"
@@ -97,6 +109,7 @@
 </template>
 
 <script lang="ts" setup>
+import StyleTypeSelect from "./style-type-select.vue";
 import StylesManage from "./styles.vue";
 import Images from "./images.vue";
 import { computed, ref, watchEffect } from "vue";
@@ -110,6 +123,7 @@ import {
   isTemploraryID,
   defaultStyle,
 } from "@/store";
+import { styleTypes } from "@/api";
 
 export type EditProps = {
   data: Tagging;
@@ -119,15 +133,16 @@ const props = defineProps<EditProps>();
 const emit = defineEmits<{ (e: "quit"): void; (e: "save", data: Tagging): void }>();
 const tagging = ref<Tagging>({ ...props.data, images: [...props.data.images] });
 const activeStyle = computed(() => getTaggingStyle(tagging.value.styleId));
-
-watchEffect(() => {
-  if (!activeStyle.value && defaultStyle.value) {
-    tagging.value.styleId = defaultStyle.value.id;
-  }
-});
+// 去除默认
+if (!activeStyle.value && defaultStyle.value) {
+  tagging.value.styleId = defaultStyle.value.id;
+}
+const type = ref(activeStyle.value ? activeStyle.value.typeId : styleTypes[0].id);
 
 const submitHandler = () => {
-  if (!tagging.value.title.trim()) {
+  if (activeStyle.value?.typeId !== type.value) {
+    Message.error("请选择图标样式!");
+  } else if (!tagging.value.title.trim()) {
     Message.error("标签标题必须填写!");
   }
   //  else if (!tagging.value.images.length) {
@@ -138,8 +153,10 @@ const submitHandler = () => {
   }
 };
 
-const styles = computed(() =>
-  [...taggingStyles.value].sort((a, b) =>
+const styles = computed(() => {
+  const fStyles = taggingStyles.value.filter((item) => item.typeId === type.value);
+  console.log(fStyles);
+  return fStyles.sort((a, b) =>
     a.default
       ? -1
       : b.default
@@ -153,8 +170,8 @@ const styles = computed(() =>
       : isTemploraryID(b.id)
       ? 1
       : 0
-  )
-);
+  );
+});
 
 const deleteStyle = (style: TaggingStyle) => {
   const index = taggingStyles.value.indexOf(style);
@@ -174,6 +191,8 @@ const deleteStyle = (style: TaggingStyle) => {
 };
 
 const uploadStyle = (style: TaggingStyle) => {
+  style.typeId = type.value;
+  console.log(style.typeId);
   taggingStyles.value.push(style);
   tagging.value.styleId = style.id;
 };
@@ -208,7 +227,7 @@ const delImageHandler = async (file: Tagging["images"][number]) => {
   inset: 0;
   background: rgba(0, 0, 0, 0.3);
   backdrop-filter: blur(4px);
-  z-index: 2000;
+  z-index: 2;
   padding: 20px;
   overflow-y: auto;
 }
@@ -233,6 +252,12 @@ const delImageHandler = async (file: Tagging["images"][number]) => {
   transform: translateY(-50%);
 }
 
+.style-select {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
 .edit-title {
   padding-bottom: 18px;
   margin-bottom: 18px;

+ 45 - 0
src/views/tagging/style-type-select.vue

@@ -0,0 +1,45 @@
+<template>
+  <!-- <Menu style="width: 256px" mode="vertical" :items="getItems()" @click="handleClick" /> -->
+  <Dropdown placement="bottom">
+    <span>{{ text }} <DownOutlined /></span>
+    <template #overlay>
+      <Menu
+        style="width: 120px"
+        :active-key="value.toString()"
+        mode="vertical"
+        :items="getItems()"
+        @click="handleClick"
+      />
+    </template>
+  </Dropdown>
+</template>
+
+<script lang="ts" setup>
+import { getStyleTypeName, styleTypes } from "@/api";
+import { computed } from "vue";
+import { Menu, Dropdown } from "ant-design-vue";
+import { DownOutlined } from "@ant-design/icons-vue";
+
+const props = defineProps<{ value: number }>();
+const emit = defineEmits<{ (e: "update:value", v: number): void }>();
+const text = computed(() => getStyleTypeName(props.value));
+
+const getItems = (types = styleTypes): any => {
+  return types.map((item) => ({
+    label: item.name,
+    title: item.name,
+    key: item.id,
+    children: "children" in item ? getItems(item.children) : null,
+  }));
+};
+const handleClick = (info: any) => {
+  emit("update:value", info.key);
+};
+</script>
+
+<style scoped>
+span {
+  font-size: 14px;
+  cursor: pointer;
+}
+</style>

+ 6 - 2
src/views/tagging/styles.vue

@@ -18,6 +18,7 @@
     <div
       v-for="hotStyle in styleAll"
       class="item"
+      :key="hotStyle.id"
       :class="{ active: active === hotStyle }"
       @click="clickHandler(hotStyle)"
     >
@@ -91,7 +92,11 @@ const styleAll = computed(() => {
       0,
       props.styles.length > maxShowLen.value ? maxShowLen.value : maxShowLen.value + 1
     );
-    if (!styles.includes(props.active) && props.active) {
+    if (
+      props.styles.includes(props.active) &&
+      !styles.includes(props.active) &&
+      props.active
+    ) {
       styles[styles.length - 1] = props.active;
     }
     return styles;
@@ -104,7 +109,6 @@ const iconUpload = async ({ file, preview }: { file: File; preview: string }) =>
     emit(
       "uploadStyle",
       createTaggingStyle({
-        name: file.name,
         icon: { url: data[1], blob: data[0] },
       })
     );

+ 1 - 0
src/vite-env.d.ts

@@ -12,6 +12,7 @@ type ToChangeAPI<T extends Record<string, any>> = {
 
 type SceneLocalPos = { x: number, y: number, z: number }
 type ScreenLocalPos = { x: number, y: number }
+type Rotation = { x: number, y: number, z: number, w: number }
 
 type LocalFile = { url: string, blob: Blob }
 type LocalMode<T, K> = T extends any[]