Browse Source

Merge branch 'ga-2.1.0' into v1.3.0

bill 3 tuần trước cách đây
mục cha
commit
9d75d16794

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 7149 - 0
package-lock.json


+ 3 - 0
package.json

@@ -10,6 +10,7 @@
     "build:fusetest": "vite build --mode=firetest && vite build --mode=criminaltest && vite build --mode=cjzfiretest && vite build --mode=xmfiretest",
     "build:fuse": "vite build --mode=fire && vite build --mode=criminal && vite build --mode=cjzfire && vite build --mode=xmfire",
     "dev:hx": "vite --mode=hxdev",
+    "build:dali": "vite build --mode=dalitest",
     "build:hx": "vite build --mode=hx",
     "dev:jm": "vite --mode=jmdev",
     "build:jm": "vite build --mode=jm",
@@ -24,10 +25,12 @@
     "@types/svg-path-parser": "^1.1.6",
     "@types/three": "^0.169.0",
     "clipper-lib": "^6.4.2",
+    "coordtransform": "^2.1.2",
     "dxf-writer": "^1.18.4",
     "element-plus": "^2.8.6",
     "flatten-svg": "^0.3.0",
     "html2canvas": "^1.4.1",
+    "js-base64": "^3.7.8",
     "jspdf": "^3.0.1",
     "jszip": "^3.10.1",
     "konva": "9.3.20",

+ 17 - 0
profile/.env.dali

@@ -0,0 +1,17 @@
+VITE_PRIMARY='#0960bd'
+VITE_TITLE='绘图'
+VITE_ENTRY='/example/fuse/enter-mix.ts'
+VITE_ENTRY_EXAMPLE='./main.ts'
+
+VITE_MESH_OSS='https://4dkk.4dage.com/'
+VITE_MESH_API='https://www.4dkankan.com/'
+VITE_CLOUD_API='https://laser.4dkankan.com/backend/'
+VITE_FUSE_API='https://mix3d.4dkankan.com/'
+
+VITE_MESH_VIEW='./static/kankan.html?m={m}&lang=zh&host=https://4dkk.4dage.com/v4/www&server=https://www.4dkankan.com&oss=https://4dkk.4dage.com/'
+VITE_CLOUD_VIEW='https://laser.4dkankan.com/index.html?m={m}&lang=zh'
+VITE_FUSE_VIEW='https://mix3d.4dkankan.com/'
+VITE_LOGIN_VIEW='https://mix3d.4dkankan.com/criminal/index.html?redirect={redirect}#login'
+
+VITE_BUILD_DIR="criminal"
+VITE_ICO="./icos/criminal.ico"

+ 17 - 0
profile/.env.dalidev

@@ -0,0 +1,17 @@
+VITE_PRIMARY='#0960bd'
+VITE_TITLE='绘图'
+VITE_ENTRY='/example/fuse/enter-mix.ts'
+VITE_ENTRY_EXAMPLE='./main.ts'
+VITE_MOCK_ENV=criminaltest
+
+VITE_MESH_OSS='/meshOSS/'
+VITE_MESH_API='/meshAPI/'
+VITE_CLOUD_API='/cloudAPI/'
+VITE_FUSE_API='/fuseAPI/'
+
+VITE_MESH_VIEW='./static/kankan.html?m={m}&lang=zh&host=https://4dkk.4dage.com/v4-test/www&server=https://test.4dkankan.com&oss=https://4dkk.4dage.com/'
+VITE_CLOUD_VIEW='https://uat-laser.4dkankan.com/uat/index.html?m={m}&lang=zh'
+VITE_FUSE_VIEW='https://test-mix3d.4dkankan.com/'
+VITE_LOGIN_VIEW='https://test-mix3d.4dkankan.com/criminal/index.html?redirect={redirect}#login'
+
+VITE_ICO="./icos/criminal.ico"

+ 18 - 0
profile/.env.dalitest

@@ -0,0 +1,18 @@
+VITE_PRIMARY='#0960bd'
+VITE_TITLE='绘图'
+VITE_ENTRY='/example/fuse/enter-mix.ts'
+VITE_ENTRY_EXAMPLE='./main.ts'
+
+VITE_OSS_ROOT="https://4dkk.4dage.com/"
+VITE_MESH_OSS='https://4dkk.4dage.com/'
+VITE_MESH_API='https://test.4dkankan.com/'
+VITE_CLOUD_API='https://uat-laser.4dkankan.com/uat/'
+VITE_FUSE_API='http://192.168.0.93/'
+
+VITE_MESH_VIEW='./static/kankan.html?m={m}&lang=zh&host=https://4dkk.4dage.com/v4-test/www&server=https://test.4dkankan.com&oss=https://4dkk.4dage.com/'
+VITE_CLOUD_VIEW='https://uat-laser.4dkankan.com/uat/index.html?m={m}&lang=zh'
+VITE_FUSE_VIEW='http://192.168.0.93/'
+VITE_LOGIN_VIEW='https://test-mix3d.4dkankan.com/criminal/index.html?redirect={redirect}#login'
+
+VITE_BUILD_DIR="criminal"
+VITE_ICO="./icos/criminal.ico"

+ 1 - 1
profile/.env.firedev

@@ -2,7 +2,7 @@ VITE_PRIMARY='#D8000A'
 VITE_TITLE='绘图'
 VITE_ENTRY='/example/fuse/enter-mix.ts'
 VITE_ENTRY_EXAMPLE='./main.ts'
-VITE_MOCK_ENV=fire
+VITE_MOCK_ENV=firetest
 
 VITE_MESH_OSS='/meshOSS/'
 VITE_MESH_API='/meshAPI/'

+ 6 - 3
src/example/dialog/basemap/leaflet/useLeaflet.ts

@@ -1,6 +1,7 @@
 import { mergeFuns } from "@/utils/shared";
 import { map, Map, tileLayer, marker } from "leaflet";
 import "leaflet/dist/leaflet.css";
+import coordtransform from 'coordtransform'
 import { onUnmounted, Ref, shallowRef, watch, watchEffect } from "vue";
 
 export const useLMap = (domRef: Ref<HTMLDivElement | undefined>) => {
@@ -118,10 +119,11 @@ export function isValidLatLng(latlng: LatLng) {
 }
 
 export const latlngStrTransform = (latlng: string) => {
-  const [lat, lng] = latlng.split(",").map((s) => Number(s.trim()));
+  let [lat, lng] = latlng.split(",").map((s) => Number(s.trim()));
   if (!isValidLatLng({ lat, lng })) {
     return null;
   } else {
+    [lng, lat] = coordtransform.wgs84togcj02(lng, lat)
     return { lat, lng };
   }
 };
@@ -134,8 +136,9 @@ export const getCurrentLatlng = async () => {
     navigator.geolocation.getCurrentPosition(
       (position) => {
         // 成功回调
-        const lat = position.coords.latitude; // 纬度
-        const lng = position.coords.longitude; // 经度
+        let lat = position.coords.latitude; // 纬度
+        let lng = position.coords.longitude; // 经度
+        
         resolve({ lat, lng })
       },
       (error) => {

+ 1 - 1
src/example/env.ts

@@ -85,7 +85,7 @@ watch(
       if (!nParams[key] || (nParams[key] && oParams[key])) {
         if (nParams[key] !== oParams[key]) {
           setTimeout(() => {
-            location.reload();
+            // location.reload();
           }, 100);
           return;
         }

+ 8 - 29
src/example/fuse/enter-case.ts

@@ -3,6 +3,7 @@ import type { Scene } from "../../example/platform/platform-resource";
 import { token, params } from "../env";
 import { genLoading } from "../loadding";
 import * as shadred from "./enter-shared";
+import { encodePwd } from "@/utils/encode";
 
 window.platform = { ...shadred };
 
@@ -21,9 +22,13 @@ window.platform.login = (isBack = true) => {
     if (params.value.caseId) {
       shadred
         .post("/service/manage/login", {
-          password: "JwiuK95dExMjM0NTY=7nHGf5ySQWSuC4G1An",
-          username: "super-admin",
-          userName: "super-admin",
+          // password: "JwiuK95dExMjM0NTY=7nHGf5ySQWSuC4G1An",
+          // username: "super-admin",
+          // userName: "super-admin",
+
+          password: encodePwd('Aa123456'),
+          username: "liliy",
+          userName: "liliy",
         })
         .then((res) => {
           params.value.token = res.token;
@@ -36,31 +41,5 @@ window.platform.login = (isBack = true) => {
   return shadred.login(isBack);
 };
 
-window.platform.getSceneList = genLoading(
-  async (keyword: string): Promise<Scene[]> => {
-    const list = await shadred.post(`fusion/case/sceneListPost`, {
-      caseId: params.value.caseId,
-      isMesh: 1,
-      sceneName: keyword,
-    });
-    return list.map((item: any) => ({
-      type: shadred.SCENE_TYPE.mesh,
-      m: item.num,
-      title: item.name,
-      id: item.id.toString(),
-      token,
-    }));
-  }
-);
-
 /* @vite-ignore */
 import(import.meta.env.VITE_ENTRY_EXAMPLE);
-
-setTimeout(() => {
-  if (!params.value.caseId || !token) {
-    ElMessage.error("当前项目号不存在!");
-    window.platform.login(!!params.value.caseId);
-  } else {
-    window.platform.getSceneList("");
-  }
-}, 500);

+ 0 - 1
src/example/fuse/enter-mix.ts

@@ -31,7 +31,6 @@ window.platform.login = (isBack = true) => {
 setTimeout(() => {
   if (!params.value.caseId || !token) {
     ElMessage.error("当前项目号不存在!");
-    window.platform.login(!!params.value.caseId);
   } else {
     window.platform.getSceneList("");
   }

+ 83 - 21
src/example/fuse/enter-shared.ts

@@ -1,6 +1,6 @@
-import type { TabCover } from "./store";
+import { type TabCover } from "./store";
 import type { Scene } from "../../example/platform/platform-resource";
-import type { StoreData } from "@/core/store/store";
+import { getEmptyStoreData, type StoreData } from "@/core/store/store";
 import { token, params, urlUpdateQuery, urlGetQuery } from "../env";
 import { genLoading } from "../loadding";
 import { formatDate, tempStrFill } from "@/utils/shared";
@@ -9,6 +9,12 @@ import {
   latlngStrTransform,
 } from "../dialog/basemap/leaflet/useLeaflet";
 import mitt from "mitt";
+import { defaultLayer } from "@/constant";
+import { tableTitleKey } from '../constant'
+import { getPaperConfig, paperConfigs } from "../components/slide/actions";
+import { getBaseItem } from "@/core/components/util";
+import { getRealPixel } from "./views/tabulation/gen-tab";
+
 
 export const SCENE_TYPE = {
   fuse: "fuse",
@@ -128,12 +134,13 @@ const after = async (fet: Promise<Response>) => {
   }
 
   if (res.code === 8034) {
-    if (!import.meta.env.DEV) {
-      setTimeout(() => {
-        history.back();
-      }, 1000);
-    }
-    throw `${res.message},即将退出`;
+    // if (!import.meta.env.DEV) {
+    //   setTimeout(() => {
+    //     history.back();
+    //   }, 1000);
+    // }
+    // throw `${res.message},即将退出`;
+    res.code = 8032
   }
 
   if ([4008, 4010, 7012].includes(res.code)) {
@@ -144,7 +151,7 @@ const after = async (fet: Promise<Response>) => {
   }
   // 特殊code 不跳转
   if ([8035].includes(res.code)) {
-    throw '存储路径不可用,请前往设置/文件管理修改原始数据最新路径后重试。'
+    throw "存储路径不可用,请前往设置/文件管理修改原始数据最新路径后重试。";
   }
 
   if (res.code !== 0) {
@@ -178,7 +185,7 @@ export const getSceneList = (keyword: string) => {
           m: item.num,
           title: item.sceneName,
           id: item.id.toString(),
-          mapping: item.mapping || '',
+          mapping: item.mapping || "",
           token,
         } as Scene)
     );
@@ -266,7 +273,7 @@ export const getTabulationId = async (id: string) => {
   return list[0]?.id;
 };
 
-export const getTabulationData = genLoading(async (id: string) => {
+export const _getTabulationData = genLoading(async (id: string) => {
   if (!id) {
     return {
       store: {
@@ -295,7 +302,56 @@ export const getTabulationData = genLoading(async (id: string) => {
   };
 });
 
-export const saveTabulationData = genLoading(
+export const getTabulationData = async (...args: any[]) => {
+  const result = await _getTabulationData.apply(this, args as any);
+  const temp = await getTableTemp();
+  if (!result.title && temp.title) {
+    result.title = temp.title;
+  }
+  if (!result.store) {
+    result.store = {
+      ...getEmptyStoreData(),
+    };
+  }
+  if (result.title) {
+    let texts = result.store.layers[defaultLayer].text;
+    if (!texts) {
+      texts = result.store.layers[defaultLayer].text = [];
+    }
+    let title = texts.find((title: any) => title.key === tableTitleKey);
+    if (title) {
+      title.content = result.title;
+    } else {
+      const { margin, size } = getPaperConfig(
+        (paperConfigs as any)[result.paperKey].size,
+        (paperConfigs as any)[result.paperKey].scale
+      );
+      const title = {
+        ...getBaseItem(),
+        content: result.title,
+        width: getRealPixel(90, result.paperKey),
+        heihgt: getRealPixel(14.4, result.paperKey),
+        fontSize: getRealPixel(12, result.paperKey),
+        key: tableTitleKey,
+        align: "center",
+        mat: [1, 0, 0, 1, 0, 0],
+      };
+      const pos = {
+        x:
+          (size.width - margin[3]) / 2 -
+          getRealPixel(40, result.paperKey) +
+          margin[3],
+        y: getRealPixel(15, result.paperKey) + margin[0],
+      };
+      title.mat[4] = pos.x;
+      title.mat[5] = pos.y;
+      texts.push(title);
+    }
+  }
+  return result;
+};
+
+export const _saveTabulationData = genLoading(
   async (
     id: string,
     data: {
@@ -322,15 +378,21 @@ export const saveTabulationData = genLoading(
   }
 );
 
-export const uploadResourse = genLoading(
-  async (file: File) => {
-    try {
-      return await postFile(`fusion/upload/file`, { file })
-    } catch (e) {
-      throw e
-    }
+export const saveTabulationData = (id: any, data: any) => {
+  const texts = data.store.layers[defaultLayer].text;
+  let title = texts.find((title: any) => title.key === tableTitleKey);
+  let content = title ? title.content : "";
+
+  return _saveTabulationData(id, { ...data, title: content });
+};
+
+export const uploadResourse = genLoading(async (file: File) => {
+  try {
+    return await postFile(`fusion/upload/file`, { file });
+  } catch (e) {
+    throw e;
   }
-);
+});
 
 export const getResource = (url: string) => {
   return url;
@@ -409,7 +471,7 @@ export const searchAddress = async (keyword: string, mapId: number) => {
   const data = await post(`fusion/mapConfig/geocode`, {
     address: keyword,
     mapId,
-  });
+  }) || [];
   return data.map((item: any) => ({
     ...item,
     latlng: latlngStrTransform(item.location.split(",").reverse().join(",")),

+ 8 - 66
src/example/fuse/enter.ts

@@ -2,12 +2,7 @@ import { ElMessage } from "element-plus";
 import { params, preventReload } from "../env";
 import * as platform from "./enter-shared";
 import { asyncTimeout } from "@/utils/shared";
-import { getEmptyStoreData } from "@/core/store/store";
-import { defaultLayer } from "@/constant";
-import { tableTitleKey } from "../constant";
-import { getBaseItem } from "@/core/components/util";
-import { getRealPixel } from "./views/tabulation/gen-tab";
-import { getPaperConfig, paperConfigs } from "../components/slide/actions";
+import { encodePwd } from "@/utils/encode";
 
 window.platform = { ...platform };
 
@@ -20,9 +15,13 @@ window.platform.login = (isBack = true) => {
   if (import.meta.env.DEV) {
     platform
       .post("/service/manage/login", {
-        password: "SeJhJBbaExMjM0NTY=SoWosNzcQWC8T7xb06",
-        username: "super-admin",
-        userName: "super-admin",
+        // password: "Di8r5tFpExMjM0NTY=F39Vd0znQWfBY7W9iG",
+        // username: "W测试2",
+        // userName: "W测试2",
+
+        password: encodePwd("Aa123456"),
+        username: "liliy",
+        userName: "liliy",
       })
       .then((res) => {
         params.value.token = res.token;
@@ -93,63 +92,6 @@ if (!!params.value.sceneDraw) {
   import(import.meta.env.VITE_ENTRY_EXAMPLE);
 }
 
-window.platform.getTabulationData = async (...args: any[]) => {
-  const result = await platform.getTabulationData.apply(this, args as any);
-  const temp = await platform.getTableTemp();
-  if (!result.title && temp.title) {
-    result.title = temp.title;
-  }
-  if (!result.store) {
-    result.store = {
-      ...getEmptyStoreData(),
-    };
-  }
-  if (result.title) {
-    let texts = result.store.layers[defaultLayer].text;
-    if (!texts) {
-      texts = result.store.layers[defaultLayer].text = [];
-    }
-    let title = texts.find((title: any) => title.key === tableTitleKey);
-    if (title) {
-      title.content = result.title;
-    } else {
-      const { margin, size } = getPaperConfig(
-        (paperConfigs as any)[result.paperKey].size,
-        (paperConfigs as any)[result.paperKey].scale
-      );
-      const title = {
-        ...getBaseItem(),
-        content: result.title,
-        width: getRealPixel(90, result.paperKey),
-        heihgt: getRealPixel(14.4, result.paperKey),
-        fontSize: getRealPixel(12, result.paperKey),
-        key: tableTitleKey,
-        align: "center",
-        mat: [1, 0, 0, 1, 0, 0],
-      };
-      const pos = {
-        x:
-          (size.width - margin[3]) / 2 -
-          getRealPixel(40, result.paperKey) +
-          margin[3],
-        y: getRealPixel(15, result.paperKey) + margin[0],
-      };
-      title.mat[4] = pos.x;
-      title.mat[5] = pos.y;
-      texts.push(title);
-    }
-  }
-  return result;
-};
-
-window.platform.saveTabulationData = (id: any, data: any) => {
-  const texts = data.store.layers[defaultLayer].text;
-  let title = texts.find((title: any) => title.key === tableTitleKey);
-  let content = title ? title.content : "";
-
-  return platform.saveTabulationData(id, { ...data, title: content });
-};
-
 // 登录检测
 setTimeout(() => {
   if (!window.platform.preventLogin) {

+ 1 - 4
src/example/fuse/views/overview/header.vue

@@ -33,6 +33,7 @@ import { listener } from "@/utils/event.ts";
 import { asyncTimeout, mergeFuns, repeatedlyOnly } from "@/utils/shared.ts";
 import saveAs from "@/utils/file-serve.ts";
 import { setViewToTableCover } from "./actions.ts";
+import { genLoading } from "@/example/loadding.ts";
 
 const props = defineProps<{ title: string }>();
 const draw = useDraw();
@@ -186,10 +187,6 @@ const saveHandler = repeatedlyOnly(async () => {
       window.platform.uploadResourse(new File([kkBlob], `kankan-cover.png`)),
     ]);
   }
-
-  tabulationId.value = await window.platform.getTabulationId(overviewId.value);
-  await refreshTabulationData();
-
   const tabStore = await repTabulationStore(
     tabulationData.value.paperKey,
     storeData.config.compass.rotation,

+ 10 - 4
src/example/loadding.ts

@@ -11,12 +11,18 @@ export const genLoading = <T, K extends PFN<T>>(fn: K, options?: Options): K =>
 const loadingStack = ref<Array<Options | undefined>>([]);
 const tokens: string[] = [];
 let instance: ReturnType<typeof ElLoading.service> | null = null;
+let timeout: any;
 watchEffect(() => {
+  clearTimeout(timeout)
   if (!loadingStack.value.length && instance) {
-    instance.close();
-    instance = null;
+    timeout = setTimeout(() => {
+      instance?.close();
+      instance = null;
+      console.log('clear')
+    }, 500);
   }
-  if (loadingStack.value.length && !instance && !import.meta.env.DEV) {
+  if (loadingStack.value.length && !instance) {
+    console.log('gen')
     instance = ElLoading.service(loadingStack.value[0]);
   }
 });
@@ -58,4 +64,4 @@ export const loading = <T, K extends PFN<T>>(
       }, 50);
     });
   return ret;
-};
+};

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

@@ -224,6 +224,7 @@ const getTaggingShapes = async (taggings: SceneResource["taggings"]) => {
         .catch(() => {})
     );
   }
+
   await Promise.all(reqs);
   return {
     texts,
@@ -241,6 +242,7 @@ const getBorderTaggingShapes = (taggings: SceneResource["borderTaggings"]) => {
     rects.push({
       ...getBaseItem(),
       ...rectDefaultStyle,
+      strokeWidth: 1,
       points: taggings[i].points,
       attitude: [1, 0, 0, 1, 0, 0],
     });
@@ -378,7 +380,6 @@ const drawSceneResource = async (resource: SceneResource, draw: Draw) => {
   const { icons, images, texts } = await getTaggingShapes(resource.taggings);
 
   // 这版本的icons先不用 用border
-  icons.length = 0
   const border = getBorderTaggingShapes(resource.borderTaggings)
   texts.push(...border.texts)
   const rects = border.rects

+ 4 - 2
src/example/platform/resource-swkk.ts

@@ -262,6 +262,8 @@ export const getTraceTaggingInfos = async (
     )
       continue;
 
+    
+
     const isSys = trace.icon.indexOf("/") > -1;
     const icon = isSys
       ? trace.icon.substring(trace.icon.lastIndexOf("/") + 1)
@@ -695,9 +697,9 @@ export const getResource = async ({
   }
   if (syncs.includes("traces")) {
     reqs.push(
-      getTraceTaggingInfos(scene, scale, floorIndex).then((ts) =>
+      getTraceTaggingInfos(scene, scale, floorIndex).then((ts) => {
         taggings.push(...ts)
-      )
+      })
     );
   }
 

+ 101 - 0
src/utils/encode.ts

@@ -0,0 +1,101 @@
+import { Base64 } from "js-base64";
+
+function randomWord(randomFlag: boolean, min: number, max?: number) {
+  let str = "";
+  let range = min;
+  const arr = [
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "H",
+    "I",
+    "J",
+    "K",
+    "L",
+    "M",
+    "N",
+    "O",
+    "P",
+    "Q",
+    "R",
+    "S",
+    "T",
+    "U",
+    "V",
+    "W",
+    "X",
+    "Y",
+    "Z",
+  ];
+  // 随机产生
+  if (randomFlag && max) {
+    range = Math.round(Math.random() * (max - min)) + min;
+  }
+  for (let i = 0; i < range; i++) {
+    const pos = Math.round(Math.random() * (arr.length - 1));
+    str += arr[pos];
+  }
+  return str;
+}
+
+export function encodePwd(str: string, strv = "") {
+  str = Base64.encode(str);
+  const NUM = 2;
+  const front = randomWord(false, 8);
+  const middle = randomWord(false, 8);
+  const end = randomWord(false, 8);
+
+  const str1 = str.substring(0, NUM);
+  const str2 = str.substring(NUM);
+
+  if (strv) {
+    const strv1 = strv.substring(0, NUM);
+    const strv2 = strv.substring(NUM);
+    return [
+      front + str2 + middle + str1 + end,
+      front + strv2 + middle + strv1 + end,
+    ];
+  }
+
+  return front + str2 + middle + str1 + end;
+}

+ 55 - 100
src/utils/polygon.ts

@@ -1,117 +1,73 @@
 /**
- * 多条选段 由点id组合 确定多边形
- * @param segments 
- * @returns 
+ * 高效提取连通段(包括闭合多边形和开放折线)
  */
 export function extractConnectedSegments(
   segments: { a: number; b: number }[]
 ): number[][] {
-  // 1. 构建无向图邻接表
-  const graph: Record<number, number[]> = {};
+  if (segments.length === 0) return [];
+
+  // 1. 构建邻接表
+  const adj: Map<number, Set<number>> = new Map();
   for (const { a, b } of segments) {
-    if (!graph[a]) graph[a] = [];
-    if (!graph[b]) graph[b] = [];
-    graph[a].push(b);
-    graph[b].push(a);
+    if (a === b) continue; // 忽略自环
+    if (!adj.has(a)) adj.set(a, new Set());
+    if (!adj.has(b)) adj.set(b, new Set());
+    adj.get(a)!.add(b);
+    adj.get(b)!.add(a);
   }
 
-  // 2. 边访问记录
-  const edgeKey = (a: number, b: number) => (a < b ? `${a},${b}` : `${b},${a}`);
-
-  // 3. 结果收集
   const result: number[][] = [];
-  const polygonSet = new Set<string>();
-
-  // 4. 多边形标准化函数(按点集合的排序字符串标识)
-  const normalizePolygon = (polygon: number[]) => {
-    const points = [...new Set(polygon.slice(0, -1))].sort((a, b) => a - b);
-    return JSON.stringify(points);
-  };
-
-  // 5. 查找闭合环的迭代DFS实现
-  function findPolygons(start: number) {
-    const stack: {
-      current: number;
-      path: number[];
-      visited: Set<string>;
-    }[] = [{ current: start, path: [start], visited: new Set() }];
-
-    while (stack.length > 0) {
-      const { current, path, visited } = stack.pop()!;
-
-      for (const neighbor of graph[current]) {
-        const edge = edgeKey(current, neighbor);
-
-        // 跳过已访问的边
-        if (visited.has(edge)) continue;
 
-        // 发现闭合环
-        if (neighbor === start && path.length > 2) {
-          const closedPath = [...path, start];
-          const polyKey = normalizePolygon(closedPath);
-
-          if (!polygonSet.has(polyKey)) {
-            polygonSet.add(polyKey);
-            result.push(closedPath);
-          }
-          continue;
-        }
-
-        // 避免重复访问点(起点除外)
-        if (neighbor !== start && path.includes(neighbor)) continue;
-
-        const newVisited = new Set(visited);
-        newVisited.add(edge);
-        stack.push({
-          current: neighbor,
-          path: [...path, neighbor],
-          visited: newVisited,
-        });
+  // 2. 首先提取所有“开放路径” (从度数为 1 的点开始)
+  // 这样做可以消除干扰,剩下的一定是纯粹的环结构
+  for (const [node, neighbors] of adj.entries()) {
+    if (neighbors.size === 1) {
+      const path: number[] = [node];
+      let curr = node;
+      
+      while (adj.has(curr) && adj.get(curr)!.size > 0) {
+        const next = adj.get(curr)!.values().next().value!;
+        path.push(next);
+        
+        // 删除已经处理的边
+        adj.get(curr)!.delete(next);
+        adj.get(next)!.delete(curr);
+        
+        // 如果 next 点没边了,从图中移除
+        if (adj.get(curr)!.size === 0) adj.delete(curr);
+        curr = next;
+        
+        // 如果遇到了分叉点(度数大于1)或终点,停止
+        if (!adj.has(curr) || adj.get(curr)!.size !== 1) break;
       }
+      if (path.length > 1) result.push(path);
     }
   }
 
-  // 6. 查找所有闭合环
-  for (const node in graph) {
-    const nodeId = Number(node);
-    if (graph[nodeId].length >= 2) {
-      // 至少需要两个连接才能形成环
-      findPolygons(nodeId);
+  // 3. 处理剩下的部分(此时图中只剩下闭合环)
+  // 采用简单的深度优先遍历提取环
+  const nodes = Array.from(adj.keys());
+  for (const startNode of nodes) {
+    if (!adj.has(startNode)) continue;
+
+    const path: number[] = [startNode];
+    let curr = startNode;
+
+    while (adj.has(curr) && adj.get(curr)!.size > 0) {
+      const next = adj.get(curr)!.values().next().value!;
+      path.push(next);
+
+      adj.get(curr)!.delete(next);
+      adj.get(next)!.delete(curr);
+      
+      if (adj.get(curr)!.size === 0) adj.delete(curr);
+      
+      curr = next;
+      if (curr === startNode) break; // 环闭合
     }
-  }
-
-  // 7. 查找开放路径
-  const visitedNodes = new Set<number>();
-  for (const node in graph) {
-    const nodeId = Number(node);
-    if (visitedNodes.has(nodeId)) continue;
-
-    // 开放路径必须从端点开始(度数为1的点)
-    if (graph[nodeId].length === 1) {
-      const path: number[] = [];
-      let current: number | null = nodeId;
-      let prev: number | null = null;
-
-      while (current !== null) {
-        visitedNodes.add(current);
-        path.push(current);
-
-        // 找到下一个未访问的相邻点
-        let next: number | null = null;
-        for (const neighbor of graph[current]) {
-          if (neighbor !== prev && !visitedNodes.has(neighbor)) {
-            next = neighbor;
-            break;
-          }
-        }
-
-        prev = current;
-        current = next;
-      }
-
-      if (path.length >= 2) {
-        result.push(path);
-      }
+    
+    if (path.length > 1) {
+      result.push(path);
     }
   }
 
@@ -119,7 +75,6 @@ export function extractConnectedSegments(
 }
 
 
-
 /**
  * 从无向线段中提取所有闭合环(多边形)和开放路径
  * @param segments 线段数组,每条线段由两个点组成(无方向性)

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

@@ -8,6 +8,7 @@ interface ImportMeta {
   readonly env: ImportMetaEnv
 }
 
+declare module 'coordtransform';
 
  module 'virtual:svg-icons-register' {
   const content: any;