Browse Source

Merge branch 'merge-selection'

bill 1 month ago
parent
commit
c218fba4ca
100 changed files with 5886 additions and 1651 deletions
  1. 4 4
      .env.jmdev
  2. 2 1
      .gitignore
  3. 2 1
      package.json
  4. 35 0
      pnpm-lock.yaml
  5. 3 6
      public/icons/chuang.svg
  6. 21 17
      public/icons/luodichuang.svg
  7. 2 4
      public/icons/men_l.svg
  8. 6 0
      public/icons/men_p_l.svg
  9. 2 5
      public/icons/piaochuang.svg
  10. 3 5
      public/icons/shuangkaimen.svg
  11. 5 4
      public/icons/yakou.svg
  12. 2 4
      public/icons/yimen.svg
  13. 156 54
      public/static/kankan.html
  14. BIN
      src/core/assert/cursor/pic_pen.ico
  15. BIN
      src/core/assert/cursor/pic_pen_a.ico
  16. BIN
      src/core/assert/cursor/pic_pen_r.ico
  17. 5 2
      src/core/components/arrow/arrow.vue
  18. 1 0
      src/core/components/arrow/temp-arrow.vue
  19. 1 0
      src/core/components/circle/temp-circle.vue
  20. 69 6
      src/core/components/group/group.vue
  21. 53 21
      src/core/components/group/index.ts
  22. 1 1
      src/core/components/group/temp-group.vue
  23. 2 18
      src/core/components/icon/icon.ts
  24. 6 35
      src/core/components/icon/icon.vue
  25. 1 2
      src/core/components/icon/index.ts
  26. 10 19
      src/core/components/icon/temp-icon.vue
  27. 34 0
      src/core/components/icon/temp-path.vue
  28. 1 1
      src/core/components/image/index.ts
  29. 15 1
      src/core/components/index.ts
  30. 233 0
      src/core/components/line-icon/icon.vue
  31. 264 0
      src/core/components/line-icon/index.ts
  32. 48 0
      src/core/components/line-icon/temp-icon.vue
  33. 765 0
      src/core/components/line/attach-server.ts
  34. 98 25
      src/core/components/line/index.ts
  35. 12 7
      src/core/components/line/line.vue
  36. 5 0
      src/core/components/line/renderer/wall/index.ts
  37. 54 0
      src/core/components/line/renderer/wall/index.vue
  38. 380 0
      src/core/components/line/renderer/wall/view.ts
  39. 107 132
      src/core/components/line/single-line.vue
  40. 109 0
      src/core/components/line/single-point.vue
  41. 54 37
      src/core/components/line/temp-line.vue
  42. 48 129
      src/core/components/line/use-draw.ts
  43. 1 0
      src/core/components/polygon/temp-polygon.vue
  44. 1 0
      src/core/components/sequent-line/temp-line.vue
  45. 1 0
      src/core/components/serial/temp-serial.vue
  46. 4 2
      src/core/components/share/edit-line.vue
  47. 1 0
      src/core/components/table/temp-table.vue
  48. 1 0
      src/core/components/triangle/temp-triangle.vue
  49. 18 10
      src/core/helper/compass.vue
  50. 16 1
      src/core/helper/debugger.vue
  51. 3 6
      src/core/helper/split-line.vue
  52. 119 100
      src/core/hook/use-animation.ts
  53. 96 16
      src/core/hook/use-component.ts
  54. 1 1
      src/core/hook/use-debugger.ts
  55. 34 18
      src/core/hook/use-draw.ts
  56. 14 0
      src/core/hook/use-dxf.ts
  57. 0 3
      src/core/hook/use-event.ts
  58. 42 55
      src/core/hook/use-expose.ts
  59. 100 91
      src/core/hook/use-global-vars.ts
  60. 115 0
      src/core/hook/use-group.ts
  61. 25 42
      src/core/hook/use-history.ts
  62. 23 14
      src/core/hook/use-interactive.ts
  63. 11 0
      src/core/hook/use-layer.ts
  64. 0 1
      src/core/hook/use-mouse-status.ts
  65. 0 320
      src/core/hook/use-selection-n.ts
  66. 169 225
      src/core/hook/use-selection.ts
  67. 2 1
      src/core/hook/use-status.ts
  68. 1 1
      src/core/hook/use-transformer.ts
  69. 0 1
      src/core/html-mount/propertys/components/color.vue
  70. 2 2
      src/core/html-mount/propertys/components/input-num.vue
  71. 2 4
      src/core/html-mount/propertys/components/num.vue
  72. 2 0
      src/core/html-mount/propertys/index.ts
  73. 189 0
      src/core/html-mount/propertys/mount-describes.vue
  74. 20 181
      src/core/html-mount/propertys/mount.vue
  75. 77 0
      src/core/renderer-three/components/icon/index.vue
  76. 14 0
      src/core/renderer-three/components/index.ts
  77. 126 0
      src/core/renderer-three/components/line-icon/index.vue
  78. 96 0
      src/core/renderer-three/components/line.vue
  79. 23 0
      src/core/renderer-three/components/line/index.vue
  80. 25 0
      src/core/renderer-three/components/line/material.ts
  81. 95 0
      src/core/renderer-three/components/line/single-line.vue
  82. 476 0
      src/core/renderer-three/components/resource.ts
  83. 192 0
      src/core/renderer-three/components/text/index.vue
  84. 1 0
      src/core/renderer-three/container.ts
  85. 18 0
      src/core/renderer-three/env/ground.vue
  86. 32 0
      src/core/renderer-three/env/light.vue
  87. 84 0
      src/core/renderer-three/hook/use-animation.ts
  88. 247 0
      src/core/renderer-three/hook/use-controls.ts
  89. 204 0
      src/core/renderer-three/hook/use-event.ts
  90. 33 0
      src/core/renderer-three/hook/use-getter.ts
  91. 200 0
      src/core/renderer-three/hook/use-move.ts
  92. 224 0
      src/core/renderer-three/hook/use-stage.ts
  93. 51 0
      src/core/renderer-three/renderer.vue
  94. 7 0
      src/core/renderer-three/util.ts
  95. 7 10
      src/core/renderer/draw-group.vue
  96. 1 1
      src/core/renderer/draw-shape.vue
  97. 15 0
      src/core/renderer/group-temp.vue
  98. 4 2
      src/core/renderer/renderer.vue
  99. 2 2
      src/core/store/store.ts
  100. 0 0
      src/deconstruction.d.ts

+ 4 - 4
.env.jmdev

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

+ 2 - 1
.gitignore

@@ -23,5 +23,6 @@ dist-ssr
 *.sln
 *.sw?
 
+
 # 本地模型
-public/static/models/*
+public/static/models/*

+ 2 - 1
package.json

@@ -9,7 +9,6 @@
     "build:fuse": "vite build --mode=fuse",
     "dev:hx": "vite --mode=hxdev",
     "build:hx": "vite build --mode=hx",
-
     "dev:jm": "vite --mode=jmdev",
     "build:jm": "vite build --mode=jm"
   },
@@ -20,6 +19,7 @@
     "@types/node": "^22.9.0",
     "@types/svg-path-parser": "^1.1.6",
     "@types/three": "^0.169.0",
+    "clipper-lib": "^6.4.2",
     "dxf-writer": "^1.18.4",
     "element-plus": "^2.8.6",
     "html2canvas": "^1.4.1",
@@ -27,6 +27,7 @@
     "jszip": "^3.10.1",
     "konva": "^9.3.18",
     "localforage": "^1.10.0",
+    "martinez-polygon-clipping": "^0.7.4",
     "mitt": "^3.0.1",
     "pinia": "^2.2.4",
     "sass": "^1.80.4",

+ 35 - 0
pnpm-lock.yaml

@@ -26,6 +26,9 @@ importers:
       '@types/three':
         specifier: ^0.169.0
         version: 0.169.0
+      clipper-lib:
+        specifier: ^6.4.2
+        version: 6.4.2
       dxf-writer:
         specifier: ^1.18.4
         version: 1.18.4
@@ -47,6 +50,9 @@ importers:
       localforage:
         specifier: ^1.10.0
         version: 1.10.0
+      martinez-polygon-clipping:
+        specifier: ^0.7.4
+        version: 0.7.4
       mitt:
         specifier: ^3.0.1
         version: 3.0.1
@@ -780,6 +786,9 @@ packages:
     resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
     engines: {node: '>= 10.0'}
 
+  clipper-lib@6.4.2:
+    resolution: {integrity: sha512-knglhjQX5ihNj/XCIs6zCHrTemdvHY3LPZP9XB2nq2/3igyYMFueFXtfp84baJvEE+f8pO1ZS4UVeEgmLnAprQ==}
+
   clone@2.1.2:
     resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
     engines: {node: '>=0.8'}
@@ -1450,6 +1459,9 @@ packages:
     resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
     engines: {node: '>=0.10.0'}
 
+  martinez-polygon-clipping@0.7.4:
+    resolution: {integrity: sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw==}
+
   math-intrinsics@1.1.0:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
@@ -1708,6 +1720,9 @@ packages:
     resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
     engines: {node: '>= 0.8.15'}
 
+  robust-predicates@2.0.4:
+    resolution: {integrity: sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==}
+
   rollup@4.40.2:
     resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1940,6 +1955,9 @@ packages:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}
 
+  splaytree@0.1.4:
+    resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==}
+
   split-string@3.1.0:
     resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
     engines: {node: '>=0.10.0'}
@@ -2032,6 +2050,9 @@ packages:
   three@0.169.0:
     resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==}
 
+  tinyqueue@1.2.3:
+    resolution: {integrity: sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==}
+
   to-object-path@0.3.0:
     resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
     engines: {node: '>=0.10.0'}
@@ -2826,6 +2847,8 @@ snapshots:
     dependencies:
       source-map: 0.6.1
 
+  clipper-lib@6.4.2: {}
+
   clone@2.1.2: {}
 
   collection-visit@1.0.0:
@@ -3616,6 +3639,12 @@ snapshots:
     dependencies:
       object-visit: 1.0.1
 
+  martinez-polygon-clipping@0.7.4:
+    dependencies:
+      robust-predicates: 2.0.4
+      splaytree: 0.1.4
+      tinyqueue: 1.2.3
+
   math-intrinsics@1.1.0: {}
 
   mdn-data@2.0.14: {}
@@ -3906,6 +3935,8 @@ snapshots:
   rgbcolor@1.0.1:
     optional: true
 
+  robust-predicates@2.0.4: {}
+
   rollup@4.40.2:
     dependencies:
       '@types/estree': 1.0.7
@@ -4170,6 +4201,8 @@ snapshots:
 
   source-map@0.6.1: {}
 
+  splaytree@0.1.4: {}
+
   split-string@3.1.0:
     dependencies:
       extend-shallow: 3.0.2
@@ -4287,6 +4320,8 @@ snapshots:
 
   three@0.169.0: {}
 
+  tinyqueue@1.2.3: {}
+
   to-object-path@0.3.0:
     dependencies:
       kind-of: 3.2.2

+ 3 - 6
public/icons/chuang.svg

@@ -1,7 +1,4 @@
-<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.5 13.5H6.5V18.5H0.5L0.5 13.5Z" stroke="black"/>
-<path d="M26 13.5H32V18.5H26L26 13.5Z" stroke="black"/>
-<path d="M6.5 14.5L26 14.5" stroke="black"/>
-<path d="M6.63379 16.5L26.1338 16.5" stroke="black"/>
-<path d="M6.63379 18.5L26.1338 18.5" stroke="black"/>
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0.5 12.5H31.5V19.5H0.5V12.5Z" stroke="black"/>
+<path d="M0.5 16H31.5" stroke="black"/>
 </svg>

+ 21 - 17
public/icons/luodichuang.svg

@@ -1,19 +1,23 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.5 13.5H5.5V18.5H0.5L0.5 13.5Z" stroke="black"/>
-<path d="M26.5 13.5H31.5V18.5H26.5L26.5 13.5Z" stroke="black"/>
-<path d="M5.5 16.5H26.5" stroke="black"/>
-<path d="M5.5 18.5H7" stroke="black"/>
-<path d="M8 18.5H10" stroke="black"/>
-<path d="M11 18.5H13" stroke="black"/>
-<path d="M14 18.5H16" stroke="black"/>
-<path d="M17 18.5H19" stroke="black"/>
-<path d="M20 18.5H22" stroke="black"/>
-<path d="M23 18.5H25" stroke="black"/>
-<path d="M5.5 14.5H7" stroke="black"/>
-<path d="M8 14.5H10" stroke="black"/>
-<path d="M11 14.5H13" stroke="black"/>
-<path d="M14 14.5H16" stroke="black"/>
-<path d="M17 14.5H19" stroke="black"/>
-<path d="M20 14.5H22" stroke="black"/>
-<path d="M23 14.5H25" stroke="black"/>
+<path d="M0.5 16H31.5" stroke="black"/>
+<path d="M6 19H8" stroke="black"/>
+<path d="M3 19H5" stroke="black"/>
+<path d="M2 13H0.5V19H2" stroke="black"/>
+<path d="M30 13H31.5V19H30" stroke="black"/>
+<path d="M9 19H11" stroke="black"/>
+<path d="M12 19H14" stroke="black"/>
+<path d="M15 19H17" stroke="black"/>
+<path d="M18 19H20" stroke="black"/>
+<path d="M21 19H23" stroke="black"/>
+<path d="M24 19H26" stroke="black"/>
+<path d="M27 19H29" stroke="black"/>
+<path d="M6 13H8" stroke="black"/>
+<path d="M3 13H5" stroke="black"/>
+<path d="M9 13H11" stroke="black"/>
+<path d="M12 13H14" stroke="black"/>
+<path d="M15 13H17" stroke="black"/>
+<path d="M18 13H20" stroke="black"/>
+<path d="M21 13H23" stroke="black"/>
+<path d="M24 13H26" stroke="black"/>
+<path d="M27 13H29" stroke="black"/>
 </svg>

+ 2 - 4
public/icons/men_l.svg

@@ -1,6 +1,4 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M26.5 19.5C26.5 19.5 24.2143 12 19.5 9C14.7857 5.99996 7.5 6.5 7.5 6.5" stroke="black"/>
-<path d="M31.5 19.5H26.5V24.5H31.5L31.5 19.5Z" stroke="black"/>
-<path d="M5.5 19.5H0.5V24.5H5.5L5.5 19.5Z" stroke="black"/>
-<path d="M7.5 6.5H5.5V20.5H7.5L7.5 6.5Z" stroke="black"/>
+<path d="M27.5 25.5C27.5 24.5 26 16 19.9999 11.5C13.9999 7.00005 8.00007 7 6.5 7" stroke="black"/>
+<path d="M6.5 7.00003H4.5V25H6.5L6.5 7.00003Z" stroke="black"/>
 </svg>

+ 6 - 0
public/icons/men_p_l.svg

@@ -0,0 +1,6 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M26.5 19.5C26.5 19.5 24.2143 12 19.5 9C14.7857 5.99996 7.5 6.5 7.5 6.5" stroke="black"/>
+<path fix-center="RB" fix="true" d="M31.5 19.5H26.5V24.5H31.5L31.5 19.5Z" stroke="black"/>
+<path d="M5.5 19.5H0.5V24.5H5.5L5.5 19.5Z" stroke="black"/>
+<path d="M7.5 6.5H5.5V20.5H7.5L7.5 6.5Z" stroke="black"/>
+</svg>

+ 2 - 5
public/icons/piaochuang.svg

@@ -1,7 +1,4 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.5 18.5V8.5H27.5V18.5" stroke="black"/>
-<path d="M6.5 18.5V10.5H25.5V18.5" stroke="black"/>
-<path d="M1.5 18.5H6.5V23.5H1.5L1.5 18.5Z" stroke="black"/>
-<path d="M25.5 18.5H30.5V23.5H25.5L25.5 18.5Z" stroke="black"/>
-<path d="M6.5 23.5H25.5" stroke="black"/>
+<path d="M0.5 8V23.5H31.5V8H0.5Z" stroke="black"/>
+<path d="M2.5 23.5V10H29.5V23.5" stroke="black"/>
 </svg>

+ 3 - 5
public/icons/shuangkaimen.svg

@@ -1,7 +1,5 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.5 19.5H5.5V24.5H0.5L0.5 19.5Z" stroke="black"/>
-<path d="M26.5 19.5H31.5V24.5H26.5L26.5 19.5Z" stroke="black"/>
-<path d="M24.5 6.5H26.5V20.5H24.5L24.5 6.5Z" stroke="black"/>
-<path d="M5.5 6.5H7.5V20.5H5.5L5.5 6.5Z" stroke="black"/>
-<path d="M7.5 6.50176C7.5 6.50176 10.6667 6.50176 13.5 9.50176C16.3333 12.5018 16 15.5018 16 15.5018C16 15.5018 15.5 12.6782 18.5 9.50176C21.5 6.3253 24.5 6.50176 24.5 6.50176" stroke="black"/>
+<path d="M29.5 7H31.5V25H29.5L29.5 7Z" stroke="black"/>
+<path d="M0.5 7H2.5V25H0.5L0.5 7Z" stroke="black"/>
+<path d="M2.5 7C6 7 10 8.78571 12.5 12C15 15.2143 16 19.5 16 19.5C16 19.5 17 15 19.5 12C22 9 26 7 29.5 7" stroke="black"/>
 </svg>

+ 5 - 4
public/icons/yakou.svg

@@ -1,14 +1,15 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.5 13.5H6.5V18.5H0.5L0.5 13.5Z" stroke="black"/>
-<path d="M25.5 13.5L31.5 13.5V18.5L25.5 18.5L25.5 13.5Z" stroke="black"/>
-<path d="M27.5 13.5L27.4999 11.5L4.5 11.5002L4.50007 13.5002" stroke="black"/>
-<path d="M4.49982 18.5L4.49989 20.5L27.4998 20.4997L27.4998 18.4997" stroke="black"/>
+<path d="M31.5 11.5L0.5 11.5002V20.5L31.5 20.5V11.5Z" stroke="black"/>
 <path d="M9 14.5H11" stroke="black"/>
+<path d="M5 14.5H7" stroke="black"/>
 <path d="M13 14.5H15" stroke="black"/>
 <path d="M17 14.5H19" stroke="black"/>
 <path d="M21 14.5H23" stroke="black"/>
+<path d="M25 14.5H27" stroke="black"/>
 <path d="M9 17.5H11" stroke="black"/>
+<path d="M5 17.5H7" stroke="black"/>
 <path d="M13 17.5H15" stroke="black"/>
 <path d="M17 17.5H19" stroke="black"/>
 <path d="M21 17.5H23" stroke="black"/>
+<path d="M25 17.5H27" stroke="black"/>
 </svg>

+ 2 - 4
public/icons/yimen.svg

@@ -1,6 +1,4 @@
 <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.5 13.5H5.5V18.5H0.5L0.5 13.5Z" stroke="black"/>
-<path d="M26.5 13.5H31.5V18.5H26.5L26.5 13.5Z" stroke="black"/>
-<path d="M13.4999 16.5H26.5L26.5001 18.5H13.5L13.4999 16.5Z" stroke="black"/>
-<path d="M5.5 13.5H18.5001L18.5002 15.5H5.50007L5.5 13.5Z" stroke="black"/>
+<path d="M13.5 16H31.5L31.5001 18H13.5001L13.5 16Z" stroke="black"/>
+<path d="M0.5 14H18.5L18.5001 16H0.500068L0.5 14Z" stroke="black"/>
 </svg>

+ 156 - 54
public/static/kankan.html

@@ -6,6 +6,20 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
   <style>
+    .select .option::-webkit-scrollbar {
+      width: 10px;
+    }
+
+    .select .option::-webkit-scrollbar-track {
+      background: rgba(0, 0, 0, .3);
+      border-radius: 10px;
+    }
+
+    .select .option::-webkit-scrollbar-thumb {
+      background: rgba(255, 255, 255, .3);
+      border-radius: 10px;
+    }
+
     html,
     body {
       width: 100%;
@@ -19,36 +33,68 @@
       height: 100%;
     }
 
-    #floors {
+    .map {
+      margin: 0;
       pointer-events: all;
       position: fixed;
-      left: 20px;
+      left: 15px;
       z-index: 10;
-      top: 50%;
-      transform: translateY(-50%);
-      background: rgba(0, 0, 0, .6);
+      bottom: 15px;
+      display: flex;
+      align-items: center;
+
+    }
+
+    .map div {
+      margin-right: 5px;
+    }
+
+    .select {
+      display: none;
+    }
+
+    .select .option {
+      background: rgba(0, 0, 0, .3);
       border-radius: 6px 6px 6px 6px;
       overflow: hidden;
       transition: all .3s;
-      padding: 5px 13px;
+      padding: 5px 8px;
       min-width: 30px;
+      position: absolute;
+      bottom: 100%;
+      margin-bottom: 0;
+      max-height: calc(100vh - 80px);
+      overflow-y: auto;
     }
 
-    #floors li {
+    .select .value {
+      background: rgba(0, 0, 0, .3);
+      border-radius: 6px 6px 6px 6px;
+      min-width: 30px;
+      padding: 5px 8px;
+    }
+
+    .select .place {
+      height: 5px;
+    }
+
+    .select .value,
+    .select .option li {
       height: 36px;
       cursor: pointer;
-      color: hsla(0, 0%, 100%, .7);
       font-size: 14px;
       line-height: 40px;
       max-width: 83vw;
-      text-align: center;
+      text-align: left;
       position: relative;
       color: #fff;
       text-overflow: ellipsis;
       overflow: hidden;
       white-space: nowrap;
     }
-    .active {
+
+    .select .option li.active {
+      display: none;
       color: #00c8af !important;
     }
   </style>
@@ -56,64 +102,112 @@
 
 <body>
   <div id="scene" class="scene"></div>
-  <div class="map"></div>
-  <ul id="floors" style="display: none;"></ul>
+  <div class="map">
+    <div class="select">
+      <ul class="option" id="floors" style="display: none;"></ul>
+      <div class="place"></div>
+      <div class="value" id="floor"></div>
+    </div>
+    <div class="select">
+      <ul class="option" id="modes" style="display: none;">
+        <li attr-id="panorama">漫游</li>
+        <li attr-id="floorplan">平面图</li>
+        <li attr-id="dollhouse">3D</li>
+        <!-- <li attr-id="panorama">漫游</li>
+        <li attr-id="floorplan">平面图</li>
+        <li attr-id="dollhouse">3D</li> -->
+      </ul>
+      <div class="place"></div>
+      <div class="value" id="mode"></div>
+    </div>
+  </div>
   <script>
     const params = new URLSearchParams(location.search)
     let host = params.get('host') || ''
 
-    const $script1 = document.createElement('script')
-    $script1.src = `${host}/sdk/kankan-sdk-deps.js`
-    document.documentElement.appendChild($script1)
+    const loadLib = (url) => {
+      const $script = document.createElement('script')
+      $script.src = `${host}${url}`
+      document.documentElement.appendChild($script)
+      return new Promise(resolve => $script.onload = resolve)
+    }
+
+    let kankan;
+    const setCurrentFloor = (id) => {
+      const $old = document.querySelector(`#floors > .active`)
+      if ($old) {
+        $old.classList.remove('active')
+      }
+      const $item = document.querySelector(`#floors > li[attr-id='${id}']`)
+      kankan.Scene.gotoFloor(Number(id))
+      $item && $item.classList.add('active')
+
+      document.querySelector('#floor').innerHTML = $item.innerHTML
+    }
+
+    const renderFloors = (floors) => {
+      const $floors = document.querySelector('#floors')
+      if (floors.length <= 1) {
+        return;
+      }
+
+      $floors.parentElement.style.display = 'block'
+      $floors.innerHTML = floors.map(item => `<li attr-id="${item.id}">${item.name}</li>`).join('')
+      $floors.addEventListener('click', ev => {
+        const dom = ev.target
+        const id = dom.getAttribute('attr-id')
+        if (!id) return;
+        setCurrentFloor(id)
+      })
+      $floors.parentElement.addEventListener('mouseenter', () => {
+        $floors.style.display = 'block'
+      })
+      $floors.parentElement.addEventListener('mouseleave', () => {
+        $floors.style.display = 'none'
+      })
+    }
 
-    const $script2 = document.createElement('script')
-    $script2.src = `${host}/sdk/kankan-sdk.js`
-    document.documentElement.appendChild($script2)
+    const setCurrentMode = id => {
+      const $old = document.querySelector(`#modes > .active`)
+      if ($old) {
+        $old.classList.remove('active')
+      }
+      const $item = document.querySelector(`#modes > li[attr-id='${id}']`)
+      kankan.Camera[id]()
+      $item && $item.classList.add('active')
 
+      document.querySelector('#mode').innerHTML = $item.innerHTML
 
-    let loadPackCount = 0
-    $script1.onload = () => {
-      ++loadPackCount
-      init()
     }
-    $script2.onload = () => {
-      ++loadPackCount
-      init()
+
+    const renderModes = () => {
+      const $modes = document.querySelector('#modes')
+
+      $modes.parentElement.style.display = 'block'
+      $modes.addEventListener('click', ev => {
+        const dom = ev.target
+        const id = dom.getAttribute('attr-id')
+        if (!id) return;
+        setCurrentMode(id)
+      })
+      $modes.parentElement.addEventListener('mouseenter', () => {
+        $modes.style.display = 'block'
+      })
+      $modes.parentElement.addEventListener('mouseleave', () => {
+        $modes.style.display = 'none'
+      })
+      setCurrentMode('panorama')
     }
 
-    const init = async () => {
-      if (loadPackCount < 2) return;
-      await setTimeout(() => { }, 1000)
-      var kankan = new KanKan({
+
+    const initKankan = () => {
+      kankan = new KanKan({
         dom: '#scene',
         num: params.get('m'),
         server: host,
         resource: host + '/oss/'
       })
-
-      const setCurrentFloor = (id) => {
-        const $old = document.querySelector(`#floors > .active`)
-        if ($old) {
-          $old.classList.remove('active')
-        }
-        const $item = document.querySelector(`#floors > li[attr-id='${id}']`)
-        kankan.Scene.gotoFloor(Number(id))
-        $item && $item.classList.add('active')
-      }
-
-      const renderFloors = (floors) => {
-        const $floors = document.querySelector('#floors')
-        if (floors.length <= 1) return;
-        $floors.style.display = 'block'
-        $floors.innerHTML = floors.map(item => `<li attr-id="${item.id}">${item.name}</li>`).join('')
-        $floors.addEventListener('click', ev => {
-          const dom = ev.target
-          const id = dom.getAttribute('attr-id')
-          if (!id) return;
-          setCurrentFloor(id)
-        })
-      }
-
+      kankan.render()
 
       kankan.store.on('flooruser', floor => {
         renderFloors(floor.floors)
@@ -130,8 +224,16 @@
         metadata.surveillances = 0
 
       })
-      kankan.render()
+      renderModes()
+    }
+
+    const init = async () => {
+      await loadLib(`/sdk/kankan-sdk-deps.js`)
+      await loadLib(`/sdk/kankan-sdk.js`)
+      await setTimeout(() => {}, 300)
+      initKankan()
     }
+    init()
   </script>
 
   <script>

BIN
src/core/assert/cursor/pic_pen.ico


BIN
src/core/assert/cursor/pic_pen_a.ico


BIN
src/core/assert/cursor/pic_pen_r.ico


+ 5 - 2
src/core/components/arrow/arrow.vue

@@ -29,6 +29,7 @@ import { Pos } from "@/utils/math.ts";
 import { Group } from "konva/lib/Group";
 import { flatPositions } from "@/utils/shared.ts";
 import { themeColor } from "@/constant";
+import { watch } from "vue";
 
 const props = defineProps<{ data: ArrowData }>();
 const emit = defineEmits<{
@@ -86,8 +87,10 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
     // "zIndex",
   ],
 });
-describes.fill.label = "颜色";
-describes.strokeWidth.label = "粗细";
+watch(describes, describes => {
+  describes.fill.label = "颜色";
+  describes.strokeWidth.label = "粗细";
+}, {immediate: true})
 
 // const draw = useInteractiveDrawShapeAPI();
 // const store = useStore();

+ 1 - 0
src/core/components/arrow/temp-arrow.vue

@@ -12,6 +12,7 @@
         hitStrokeWidth: data.strokeWidth + 10,
         pointerWidth: data.pointerLength,
         closed: false,
+        id: void 0,
         points: flatPositions([data.points[ndx], data.points[ndx + 1]]),
         ...eConfig,
         opacity: addMode ? 0.3 : data.opacity,

+ 1 - 0
src/core/components/circle/temp-circle.vue

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         ...matConfig,
+        id: void 0,
         zIndex: undefined,
         hitStrokeWidth: data.strokeWidth,
         opacity: addMode ? 0.3 : data.opacity,

+ 69 - 6
src/core/components/group/group.vue

@@ -2,18 +2,32 @@
   <template v-if="show">
     <TempGroup
       :data="{ ...tData, listening }"
-      :ref="(e: any) => {shape = e?.shape; getGroupShapes = e?.getGroupShapes}"
+      :ref="(e: any) => { shape = e?.shape; getGroupShapes = e?.getGroupShapes }"
       :autoUpdate="autoUpdate"
     />
     <Operate :target="shape" :menus="operateMenus" />
+    <PropertyUpdate
+      show
+      :name="propertyName"
+      :describes="descs"
+      @change="changePropertyHandler"
+      v-if="setPropertyDatas.length"
+    />
   </template>
 </template>
 
 <script lang="ts" setup>
 import TempGroup from "./temp-group.vue";
 import { Operate } from "../../html-mount/propertys/index.ts";
-import { GroupData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
-import { useComponentStatus } from "@/core/hook/use-component.ts";
+import PropertyUpdate from "../../html-mount/propertys/mount-describes.vue";
+import {
+  GroupData,
+  getMouseStyle,
+  defaultStyle,
+  matResponse,
+  GroupMatCtxItem,
+} from "./index.ts";
+import { useComponentStatus, useGetShapeBelong } from "@/core/hook/use-component.ts";
 import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 import { useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Rect } from "konva/lib/shapes/Rect";
@@ -24,9 +38,11 @@ import { useHistory } from "@/core/hook/use-history.ts";
 import { computed, nextTick, onUnmounted, ref, shallowRef } from "vue";
 import { useOperMode } from "@/core/hook/use-status.ts";
 import { EntityShape } from "@/deconstruction.js";
-import { useForciblyShowItemIds } from "@/core/hook/use-global-vars.ts";
+import { useForciblyShowItemIds, useStage } from "@/core/hook/use-global-vars.ts";
 import { themeColor } from "@/constant";
 import { debounce } from "@/utils/shared.ts";
+import { useComponentsDescribes } from "@/core/hook/use-group.ts";
+import { components } from "../index.ts";
 
 const props = defineProps<{ data: GroupData }>();
 const emit = defineEmits<{
@@ -38,6 +54,50 @@ const emit = defineEmits<{
 const store = useStore();
 const history = useHistory();
 const autoUpdate = ref(true);
+const stage = useStage();
+const _getShapeBelong = useGetShapeBelong();
+const getShapeBelong = (id: string) => {
+  const shape = stage.value?.getNode().findOne(`#${id}`);
+  if (!shape) return;
+  const belong = _getShapeBelong(shape as EntityShape);
+  if (belong) {
+    return {
+      type: belong.type,
+      item: belong.item,
+      childrenId: belong.isSelf ? undefined : belong.curId,
+    } as GroupMatCtxItem;
+  }
+};
+
+const selectIds = computed(() => props.data.ids);
+const descs = useComponentsDescribes(selectIds);
+const setPropertyDatas = computed(() => {
+  const keys = Object.keys(descs.value);
+  return keys.length === 0 ? [] : descs.value[keys[0]].joins;
+});
+const propertyName = computed(() => {
+  const names = new Set<string>();
+  for (const id of selectIds.value) {
+    const belong = getShapeBelong(id);
+    if (belong?.type) {
+      names.add(components[belong.type].shapeName);
+    }
+  }
+  return [...names].join("、") || "多实现";
+});
+
+const changePropertyHandler = () => {
+  history.onceTrack(() => {
+    for (const item of setPropertyDatas.value) {
+      const belong = getShapeBelong(item.id);
+      // getShapeBelong(item.id)
+      if (belong?.type) {
+        store.setItem(belong.type, { value: belong.item, id: belong.item.id });
+      }
+    }
+  });
+};
+
 const { shape, tData, data } = useComponentStatus<Rect, GroupData>({
   emit,
   props,
@@ -53,9 +113,12 @@ const { shape, tData, data } = useComponentStatus<Rect, GroupData>({
     const updateChildren = debounce((data, mat) => {
       unUpdate = false;
       setShapeTransform(shape.value!.getNode(), mat);
-      matResponse({ data, mat, store }, prevMat);
+      const ctxs = data.ids.map(getShapeBelong).filter((item: any) => !!item);
+      matResponse({ data, mat, store }, prevMat, ctxs);
       prevMat = mat;
-      getGroupShapes!().forEach((shape) => nextTick(() => shape.fire("bound-change")));
+      getGroupShapes!().forEach((shape) => {
+        nextTick(() => shape.fire("bound-change"));
+      });
       if (autoUpdate.value) {
         transformResolve && transformResolve();
         transformResolve = null;

+ 53 - 21
src/core/components/group/index.ts

@@ -1,16 +1,21 @@
 import { BaseItem, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { DrawItem, InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import {
+  DrawItem,
+  InteractiveFix,
+  InteractiveTo,
+  MatResponseProps,
+  ShapeType,
+} from "../index.ts";
 import { Transform } from "konva/lib/Util";
 import { components } from "../index";
-import { normalSelectItems } from "@/core/hook/use-selection.ts";
 
 export { default as Component } from "./group.vue";
 export { default as TempComponent } from "./temp-group.vue";
 
 export const shapeName = "分组";
 export const defaultStyle = {
-  stroke: '#cccccc',
+  stroke: "#cccccc",
   strokeWidth: 2,
   opacity: 0.8,
   dash: [10, 10],
@@ -23,10 +28,10 @@ export const getMouseStyle = (data: GroupData) => {
   return {
     default: {
       stroke: strokeStatus.pub,
-      dash: data.dash || defaultStyle.dash
+      dash: data.dash || defaultStyle.dash,
     },
     hover: { stroke: strokeStatus.hover },
-    select: { stroke: strokeStatus.select, dash: [10, 0], },
+    select: { stroke: strokeStatus.select, dash: [10, 0] },
     press: { stroke: strokeStatus.press },
   };
 };
@@ -63,28 +68,55 @@ export const interactiveFixData: InteractiveFix<"group"> = ({ data }) => {
   return data;
 };
 
-
-
-export const matResponse = ({data, mat, store}: MatResponseProps<'group'>, prevMat?: Transform) => {
+export type GroupMatCtxItem<T extends ShapeType = ShapeType> = {
+  type: T,
+  item: DrawItem<T>,
+  childrenId?: string,
+};
+export const matResponse = (
+  { data, mat, store }: MatResponseProps<"group">,
+  prevMat?: Transform,
+  ctxs?: GroupMatCtxItem[]
+) => {
   if (!store) {
     return;
   }
   const incMat = prevMat ? mat.copy().multiply(prevMat.invert()) : mat;
   prevMat = mat;
 
-  const items: DrawItem[] = []
-  for (const id of data.ids) {
-    const item = store.getItemById(id);
-    if (!item) continue;
-    items.push(item)
+  if (!ctxs) {
+    ctxs = [];
+    for (const id of data.ids) {
+      const item = store.getItemById(id);
+      if (!item) continue;
+      const type = store.getType(item.id)!;
+      ctxs.push({ type, item })
+    }
   }
-  let fitems = normalSelectItems(store, items, false)
 
-  fitems = fitems.length ? fitems : items
-  for (let i = 0; i < fitems.length; i++) {
-    const type = store.getType(fitems[i].id)!
-    const item = fitems[i]
-    components[type].matResponse({ data: item as any, mat: incMat, increment: true, store });
-    store.setItem(type, { value: item, id: item.id });
+  const startTypes: ShapeType[] = []
+  for (let i = 0; i < ctxs.length; i++) {
+    const type = ctxs[i].type
+    if (!startTypes.includes(type)) {
+      components[type].startMatResponse && components[type].startMatResponse()  
+      startTypes.push(type)
+    }
+    components[type].matResponse({
+      data: ctxs[i].item as any,
+      mat: incMat,
+      increment: true,
+      store,
+      operId: ctxs[i].childrenId
+    });
+  }
+  for (const type of startTypes) {
+    components[type].endMatResponse && components[type].endMatResponse!()  
   }
-}
+  for (let i = 0; i < ctxs.length; i++) {
+    store.setItem(ctxs[i].type, { value: ctxs[i].item, id: ctxs[i].item.id });
+  }
+};
+
+export const useGetSelectionManage = () => {
+  return { canSelect: () => false };
+};

+ 1 - 1
src/core/components/group/temp-group.vue

@@ -29,7 +29,7 @@ const getGroupShapes = () => {
   const shapes: EntityShape[] = [];
   for (const id of data.value.ids) {
     const $shape = $stage!.findOne(`#${id}`)!;
-    if (!$shape) return;
+    if (!$shape || $shape.attrs.disableGroupOper) continue;
     shapes.push($shape as EntityShape);
   }
   return shapes;

+ 2 - 18
src/core/components/icon/icon.ts

@@ -6,7 +6,6 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { FixScreen } from "@/utils/bound.ts";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Size } from "@/utils/math.ts";
 import { getSvgContent, parseSvgContent } from "@/utils/resource.ts";
@@ -127,7 +126,6 @@ export type IconData = Partial<typeof defaultStyle> &
     coverStrokeWidth?: number;
     mat: number[];
     url: string;
-    fixScreen?: FixScreen;
   };
 
 export const dataToConfig = (data: IconData) => {
@@ -144,7 +142,6 @@ export const interactiveToData: InteractiveTo<"icon"> = ({
   ...args
 }) => {
   if (info.cur) {
-    console.error(preset);
     return interactiveFixData({
       ...args,
       viewTransform,
@@ -157,22 +154,9 @@ export const interactiveToData: InteractiveTo<"icon"> = ({
 export const interactiveFixData: InteractiveFix<"icon"> = ({
   data,
   info,
-  viewTransform,
 }) => {
-  if (data.fixScreen) {
-    if ("x" in info.cur! && "y" in info.cur!) {
-      // 存储屏幕坐标
-      const screen = viewTransform.point(info.cur!);
-      data.fixScreen = {
-        left: screen.x,
-        top: screen.y,
-      };
-    }
-    data.mat = [1, 0, 0, 1, 0, 0];
-  } else {
-    const mat = new Transform().translate(info.cur!.x, info.cur!.y);
-    data.mat = mat.m;
-  }
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
+  data.mat = mat.m;
   return data;
 };
 

+ 6 - 35
src/core/components/icon/icon.vue

@@ -14,20 +14,12 @@
 import TempIcon from "./temp-icon.vue";
 import { IconData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import {
-  PropertyUpdate,
-  Operate,
-  mergeDescribes,
-} from "../../html-mount/propertys/index.ts";
+import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
 import { Transform } from "konva/lib/Util";
-import originDescribes from "../../html-mount/propertys/describes.json";
-import { ref, watchEffect } from "vue";
-import { MathUtils } from "three";
 import { useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Group } from "konva/lib/Group";
 import { Rect } from "konva/lib/shapes/Rect";
 import { setShapeTransform } from "@/utils/shape.ts";
-import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 
 const props = defineProps<{ data: IconData }>();
 const emit = defineEmits<{
@@ -40,7 +32,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
-  transformType: props.data.fixScreen ? undefined : "custom",
+  transformType: "custom",
   customTransform(callback, shape, data) {
     let prevInvMat: Transform;
     return useCustomTransformer(shape, data, {
@@ -73,6 +65,9 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
       },
       callback,
       openSnap: false,
+      transformerConfig: {
+        flipEnabled: true,
+      },
     });
   },
   defaultStyle,
@@ -82,30 +77,6 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   copyHandler(mat, data) {
     return matResponse({ data, mat, increment: true });
   },
-  propertys: [
-    "name",
-    "fill",
-    "stroke",
-    "strokeWidth",
-    //  "dash", "opacity",
-    "strokeScaleEnabled",
-  ],
+  propertys: ["name", "fill", "stroke", "strokeWidth"],
 });
-
-const status = useMouseShapeStatus(shape);
-if (props.data.fixScreen) {
-  const other = ref({ rotate: 0 });
-  mergeDescribes(other, {}, ["rotate"], describes);
-  describes.rotate = {
-    ...originDescribes.rotate,
-    sort: 3,
-    get value() {
-      const deg = new Transform(data.value.mat).decompose().rotation % 360;
-      return Math.round(deg);
-    },
-    set value(val) {
-      data.value.mat = new Transform().rotate(MathUtils.degToRad(val)).m;
-    },
-  } as any;
-}
 </script>

+ 1 - 2
src/core/components/icon/index.ts

@@ -1,5 +1,4 @@
 export * from './icon.ts'
 import TempComponent from "./temp-icon.vue";
 import Component from "./icon.vue";
-console.log(TempComponent)
-export { TempComponent, Component }
+export { TempComponent, Component }

+ 10 - 19
src/core/components/icon/temp-icon.vue

@@ -2,7 +2,13 @@
   <v-group :config="groupConfig" v-if="groupConfig && svg" ref="shape">
     <v-group :config="initDecMat" name="rep-position">
       <v-rect :config="rectConfig" name="repShape" />
-      <v-path v-for="config in pathConfigs" :config="config" name="icon-path" />
+      <Path
+        v-for="config in pathConfigs"
+        :config="(config as any)"
+        :svg="svg"
+        :mat="mat"
+      />
+      <!-- <v-path v-for="config in pathConfigs" :config="config" name="icon-path" /> -->
     </v-group>
   </v-group>
 </template>
@@ -14,10 +20,9 @@ import { getSvgContent, parseSvgContent, SVGParseResult } from "@/utils/resource
 import { Group } from "konva/lib/Group";
 import { DC } from "@/deconstruction.js";
 import { Transform } from "konva/lib/Util";
-import { useViewerInvertTransform, useViewSize } from "@/core/hook/use-viewer.ts";
-import { getFixPosition } from "@/utils/bound.ts";
 import { useStore } from "@/core/store/index.ts";
 import { useHistory } from "@/core/hook/use-history.ts";
+import Path from "./temp-path.vue";
 
 const props = defineProps<{ data: IconData; addMode?: boolean }>();
 const svg = ref<SVGParseResult | null>(null);
@@ -37,11 +42,9 @@ watch(
   async (url) => {
     svg.value = null;
     const svgContent = await getSvgContent(url);
-
     const content = parseSvgContent(svgContent);
     if (content.paths.length === 0) {
       svg.value = null;
-      console.error(props.data.url, content, "路径数据不正确不是svg");
       history.preventTrack(() => store.delItem("icon", props.data.id));
     } else {
       svg.value = content;
@@ -71,7 +74,6 @@ const pathConfigs = computed(() => {
     ...path,
     ...data.value,
     id: undefined,
-    lineWidth: 1000,
     zIndex: undefined,
     offset: { x: svg.value!.x, y: svg.value!.y },
   }));
@@ -84,21 +86,10 @@ const initDecMat = computed(() => {
     .decompose();
 });
 
-const viewInvTransform = useViewerInvertTransform();
-const size = useViewSize();
+const mat = computed(() => new Transform(data.value.mat));
 const groupConfig = computed(() => {
-  let mat = new Transform(data.value.mat);
-
-  if (data.value.fixScreen) {
-    if (!size.value) return {};
-    const pos = getFixPosition(data.value.fixScreen, data.value, size.value);
-    pos.x += data.value.width / 2;
-    pos.y += data.value.height / 2;
-    mat = viewInvTransform.value.copy().translate(pos.x, pos.y).multiply(mat);
-  }
-
   return {
-    ...mat.decompose(),
+    ...mat.value.decompose(),
     zIndex: undefined,
     listening: data.value.listening,
     id: data.value.id,

+ 34 - 0
src/core/components/icon/temp-path.vue

@@ -0,0 +1,34 @@
+<template>
+  <v-path :config="config" name="icon-path" />
+
+  <!-- <v-path :config="{ ...props.config, x: 32 }" name="icon-path" /> -->
+</template>
+
+<script lang="ts" setup>
+import { parseSvgContent } from "@/utils/resource";
+import { Transform } from "konva/lib/Util";
+import { computed } from "vue";
+
+const props = defineProps<{
+  svg: ReturnType<typeof parseSvgContent>;
+  config: ReturnType<typeof parseSvgContent>["paths"][0];
+  mat: Transform;
+}>();
+
+const config = computed(() => {
+  if (!props.config.fix) {
+    return props.config;
+  }
+  // return props.config;
+  const dec = props.mat.decompose();
+  // console.log(props.svg.width, props.svg.height, props.config.box);
+  const config = {
+    ...props.config,
+    scaleX: 1 / dec.scaleX,
+    scaleY: 1 / dec.scaleY,
+    // x: props.config.box.x - props.svg.width / 2,
+    // y: props.config.box.y - props.svg.height / 2,
+  };
+  return config;
+});
+</script>

+ 1 - 1
src/core/components/image/index.ts

@@ -62,7 +62,7 @@ export type ImageData = Partial<typeof defaultStyle> &
     };
     heightRaw?: number
     strokeWidth?: number;
-    cornerRadius: number;
+    cornerRadius?: number;
     url: string;
     mat: number[];
   };

+ 15 - 1
src/core/components/index.ts

@@ -4,6 +4,7 @@ import * as circle from "./circle";
 import * as triangle from "./triangle";
 import * as polygon from "./polygon";
 import * as line from "./line";
+import * as lineIcon from "./line-icon/index";
 import * as text from "./text";
 import * as icon from "./icon";
 import * as image from "./image";
@@ -12,6 +13,7 @@ import * as serial from "./serial";
 import * as group from "./group";
 import * as sequentLine from "./sequent-line";
 
+
 import { SLineData } from "./sequent-line";
 import { ArrowData } from "./arrow";
 import { TableData } from "./table";
@@ -20,6 +22,7 @@ import { CircleData } from "./circle";
 import { TriangleData } from "./triangle";
 import { PolygonData } from "./polygon";
 import { LineData } from "./line";
+import { LineIconData } from "./line-icon/index";
 import { TextData } from "./text";
 import { IconData } from "./icon";
 import { ImageData } from "./image";
@@ -31,6 +34,7 @@ import { Transform } from "konva/lib/Util";
 import { DrawStore } from "../store";
 import { DrawHistory } from "../hook/use-history";
 import { TransformerVectorType } from "../hook/use-transformer";
+import { UseGetSelectionManage } from "../hook/use-selection";
 
 const _components = {
   arrow,
@@ -39,6 +43,7 @@ const _components = {
   triangle,
   polygon,
   line,
+  lineIcon,
   text,
   icon,
   image,
@@ -55,10 +60,15 @@ type CompAttach<key extends ShapeType> = {
     data: DrawItem<key>,
     childrenId?: string
   ) => void;
+  checkItemData?: (item: DrawItem<key>) => boolean
   useDraw?: () => void;
   getSnapInfos?: (items: DrawItem<key>) => ComponentSnapInfo[];
   GroupComponent: (props: { data: DrawItem[] }) => any;
   getPredefine?: (attrKey: keyof DrawItem<key>) => any;
+  useGetSelectionManage?: UseGetSelectionManage
+  startMatResponse?: () => void
+  endMatResponse?: () => void
+  childrenDataGetter?: (data: DrawItem<key>, id: string) => any
 };
 type _Components = {
   [key in keyof typeof _components]: (typeof _components)[key];
@@ -84,6 +94,7 @@ export type DrawDataItem = {
   triangle: TriangleData;
   polygon: PolygonData;
   line: LineData;
+  lineIcon: LineIconData,
   text: TextData;
   icon: IconData;
   image: ImageData;
@@ -124,7 +135,7 @@ export type InteractiveFix<T extends ShapeType> = (args: {
   viewTransform: Transform;
   store: DrawStore;
   history: DrawHistory;
-}) => DrawItem<T>;
+}) => DrawItem<T> | undefined;
 
 export type MatResponseProps<T extends ShapeType> = {
   data: DrawItem<T>;
@@ -132,4 +143,7 @@ export type MatResponseProps<T extends ShapeType> = {
   increment?: boolean;
   operType?: TransformerVectorType;
   store?: DrawStore;
+  operId?: string
 };
+
+export const shapeTypes = Object.keys(components) as ShapeType[]

+ 233 - 0
src/core/components/line-icon/icon.vue

@@ -0,0 +1,233 @@
+<template>
+  <TempIcon :data="tData" :ref="(e: any) => shape = e?.shape" />
+  <PropertyUpdate
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @delete="emit('delShape')"
+    @change="emit('updateShape', { ...data })"
+  />
+  <Operate :target="shape" :menus="operateMenus" />
+</template>
+
+<script lang="ts" setup>
+import TempIcon from "./temp-icon.vue";
+import {
+  LineIconData,
+  getMouseStyle,
+  defaultStyle,
+  matResponse,
+  getLineIconMat,
+  getSnapLine,
+  getLineIconEndpoints,
+} from "./index.ts";
+import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
+import { Transform } from "konva/lib/Util";
+import {
+  useCustomTransformer,
+  useGetTransformerOperType,
+} from "@/core/hook/use-transformer.ts";
+import { Group } from "konva/lib/Group";
+import { Rect } from "konva/lib/shapes/Rect";
+import { setShapeTransform } from "@/utils/shape.ts";
+import { useStore } from "@/core/store/index.ts";
+import { usePointerPos } from "@/core/hook/use-global-vars.ts";
+import { useViewerInvertTransform } from "@/core/hook/use-viewer.ts";
+import { computed, nextTick, watch } from "vue";
+import { useHistory } from "@/core/hook/use-history.ts";
+import {
+  eqPoint,
+  line2IncludedAngle,
+  lineInner,
+  lineLen,
+  lineVector,
+  Pos,
+} from "@/utils/math.ts";
+import { copy } from "@/utils/shared.ts";
+import { useTestPoints } from "@/core/hook/use-debugger.ts";
+
+const props = defineProps<{ data: LineIconData }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: LineIconData): void;
+  (e: "addShape", value: LineIconData): void;
+  (e: "delShape"): void;
+}>();
+
+const store = useStore();
+const getOperType = useGetTransformerOperType();
+const viewMat = useViewerInvertTransform();
+const pos = usePointerPos();
+const testPoints = useTestPoints();
+const { shape, tData, data, operateMenus, describes } = useComponentStatus({
+  emit,
+  props,
+  getMouseStyle,
+  transformType: "custom",
+  selfData: true,
+  customTransform(callback, shape, data) {
+    let prevInvMat: Transform;
+    let posOffset: Pos | null = null;
+    return useCustomTransformer(shape, data, {
+      getRepShape() {
+        const group = new Group();
+        const rect = new Rect();
+        group.add(rect);
+        const update = () => {
+          const mat = getLineIconMat(getSnapLine(store, data.value)!, data.value);
+          const width = Math.abs(data.value.endLen - data.value.startLen);
+          const height = data.value.height;
+          prevInvMat = mat;
+          rect.width(width);
+          rect.height(height);
+          rect.offset({ x: width / 2, y: height / 2 });
+          setShapeTransform(group, mat);
+        };
+        update();
+        return { shape: group, update };
+      },
+      handler(data, mat) {
+        if (pos.value && !getOperType()) {
+          // if (!posOffset) {
+          //   const real = viewMat.value.point(pos.value);
+          //   const prevDec = prevInvMat.decompose();
+          //   posOffset = {
+          //     x: real.x - prevDec.x,
+          //     y: real.y - prevDec.y,
+          //   };
+          //   testPoints.value = [
+          //     {
+          //       x: real.x - posOffset.x,
+          //       y: real.y - posOffset.y,
+          //     },
+          //   ];
+          // }
+          const rpos = viewMat.value.point({
+            x: pos.value.x,
+            y: pos.value.y,
+          });
+          const m = mat.m;
+          m[4] = rpos.x;
+          m[5] = rpos.y;
+          // m[4] = rpos.x - posOffset.x;
+          // m[5] = rpos.y - posOffset.y;
+          mat = new Transform(m);
+        }
+        matResponse({
+          data,
+          mat: mat,
+          operType: getOperType(),
+          store,
+        });
+
+        return true;
+      },
+      callback() {
+        posOffset = null;
+        callback();
+      },
+      openSnap: false,
+      transformerConfig: {
+        flipEnabled: true,
+        rotateEnabled: false,
+        enabledAnchors: ["middle-left", "middle-right"],
+        boundBoxFunc: (oldBox, newBox) => {
+          if (newBox.width < 5) {
+            return oldBox;
+          } else {
+            return newBox;
+          }
+        },
+      },
+    });
+  },
+  defaultStyle,
+  copyHandler(_, data) {
+    const snapLine = getSnapLine(store, data)!;
+    const line = getLineIconEndpoints(getSnapLine(store, data)!, data);
+    const vector = lineVector(line);
+    const move = vector.multiplyScalar(lineLen(line[0], line[1]));
+    const mat = new Transform()
+      .translate(move.x, move.y)
+      .multiply(getLineIconMat(snapLine, data));
+    return matResponse({ data, mat, store });
+  },
+  propertys: ["name", "fill", "stroke", "strokeWidth"],
+});
+
+const line = computed(() => getSnapLine(store, props.data));
+const history = useHistory();
+watch(
+  () => line.value && (copy(line.value) as Pos[]),
+  (line, oldLine) => {
+    history.preventTrack(() => {
+      if (!line) {
+        console.error("找不到line", props.data);
+        return emit("delShape");
+      }
+      if (!oldLine) return;
+
+      const eq0 = eqPoint(oldLine[0], line[0]);
+      const eq1 = eqPoint(oldLine[1], line[1]);
+      if (eq0 !== eq1) {
+        // 联动
+        const startNdx = eq0 ? 0 : 1;
+        const endNdx = eq0 ? 1 : 0;
+        const rotate = line2IncludedAngle(
+          [oldLine[startNdx], oldLine[endNdx]],
+          [line[startNdx], line[endNdx]]
+        );
+        const mat = new Transform()
+          .translate(line[startNdx].x, line[startNdx].y)
+          .rotate(rotate)
+          .translate(-line[startNdx].x, -line[startNdx].y);
+        const endPoints = getLineIconEndpoints(oldLine, data.value).map((p) =>
+          mat.point(p)
+        );
+
+        if (lineInner(line, endPoints[0]) && lineInner(line, endPoints[1])) {
+          emit("updateShape", {
+            ...data.value,
+            startLen: lineLen(line[0], endPoints[0]),
+            endLen: lineLen(line[0], endPoints[1]),
+          });
+        } else {
+          emit("delShape");
+          return;
+        }
+      }
+      nextTick(() => {
+        shape.value?.getNode().fire("bound-change");
+      });
+    });
+  },
+  { immediate: true, flush: "post" }
+);
+
+if (props.data.type === "align-bottom") {
+  operateMenus.splice(
+    operateMenus.length - 1,
+    0,
+    {
+      label: "内外翻转",
+      handler: () => {
+        emit("updateShape", {
+          ...data.value,
+          openSide: data.value.openSide === "LEFT" ? "RIGHT" : "LEFT",
+        });
+      },
+    },
+    {
+      label: "左右翻转",
+      handler: () => {
+        emit("updateShape", {
+          ...data.value,
+          openSide: data.value.openSide === "LEFT" ? "RIGHT" : "LEFT",
+          startLen: data.value.endLen,
+          endLen: data.value.startLen,
+        });
+      },
+    }
+  );
+}
+</script>

+ 264 - 0
src/core/components/line-icon/index.ts

@@ -0,0 +1,264 @@
+import { Transform } from "konva/lib/Util";
+import { getBaseItem } from "../util.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import TempComponent from "./temp-icon.vue";
+import Component from "./icon.vue";
+import { defaultStyle, addMode, IconData } from "../icon/index.ts";
+import {
+  eqPoint,
+  lineCenter,
+  lineInner,
+  lineLen,
+  linePointProjection,
+  lineVector,
+  Pos,
+  Size,
+  vector2IncludedAngle,
+} from "@/utils/math.ts";
+import { Vector2 } from "three";
+import { LineData } from "../line/index.ts";
+import { DrawStore } from "@/core/store/index.ts";
+
+export { defaultStyle, addMode, TempComponent, Component };
+export { getMouseStyle, getPredefine } from "../icon/index.ts";
+
+export const shapeName = "线段图例";
+export type LineIconData = Omit<IconData, "mat" | "width"> & {
+  startLen: number;
+  endLen: number;
+  lineId: string;
+  openSide: "LEFT" | "RIGHT";
+  type: "full" | "align-bottom" | 'align-bottom-fix';
+  
+  __snapLine?: Pos[],
+};
+
+export const getSnapLine = (
+  store: DrawStore,
+  data: Pick<LineIconData, "lineId" | '__snapLine'>,
+  lineData?: LineData
+) => {
+  lineData = lineData || store.getTypeItems("line")[0];
+  if (!lineData) return null;
+
+  const wall = lineData.lines.find((line) => line.id === data.lineId);
+  if (!wall) return data.__snapLine
+
+  return [
+    lineData.points.find((p) => p.id === wall.a)!,
+    lineData.points.find((p) => p.id === wall.b)!,
+  ];
+};
+
+export const getLineIconEndpoints = (
+  snapLine: Pos[],
+  data: Pick<LineIconData, "startLen" | "endLen">
+) => {
+  const linev = lineVector(snapLine);
+  return [
+    linev.clone().multiplyScalar(data.startLen).add(snapLine[0]),
+    linev.clone().multiplyScalar(data.endLen).add(snapLine[0]),
+  ];
+};
+
+export const isRangInner = (line: Pos[], data: LineIconData) => {
+  const len = lineLen(line[0], line[1]);
+  return (
+    data.startLen >= 0 &&
+    data.endLen >= 0 &&
+    data.startLen <= len &&
+    data.endLen <= len
+  );
+};
+
+export const getLineIconMat = (
+  snapLine: Pos[],
+  data: Pick<
+    LineIconData,
+    "height" | "startLen" | "endLen" | "openSide" | "type"
+  >
+) => {
+  const line = getLineIconEndpoints(snapLine, data);
+  const lineRotate = vector2IncludedAngle(lineVector(line), { x: 1, y: 0 });
+  const isLeft = data.openSide === "LEFT";
+  const moveRotate = lineRotate + ((isLeft ? 1 : -1) * Math.PI) / 2;
+  const movev = new Vector2(Math.cos(moveRotate), -Math.sin(moveRotate));
+  const shapeRotate = vector2IncludedAngle({ x: 0, y: -1 }, movev);
+  const center = lineCenter(line);
+
+  const mat = new Transform();
+  if (data.type === "align-bottom" || data.type === 'align-bottom-fix') {
+    const offset = movev.clone().multiplyScalar(data.height / 2);
+    mat.translate(offset.x, offset.y);
+  }
+
+  mat.translate(center.x, center.y).rotate(shapeRotate);
+
+  const afterStart = mat.point({
+    x: -Math.abs(data.endLen - data.startLen) / 2,
+    y: -data.height / 2,
+  });
+
+  if (lineLen(afterStart, line[0]) > lineLen(afterStart, line[1])) {
+    mat.scale(-1, 1);
+  }
+  // console.log(data.id, copy(data), copy(snapLine), mat.m)
+  return mat;
+};
+
+export const matResponse = ({
+  data,
+  mat,
+  store,
+  operType,
+  increment
+}: MatResponseProps<"lineIcon">, lineData?: LineData) => {
+  if (!store || increment) return data;
+  lineData = lineData || store.getTypeItems("line")[0];
+  if (!lineData) return data;
+
+  // 简单移动
+  if (!operType) {
+    const position = { x: mat.m[4], y: mat.m[5] };
+    const getLineIconAttach = genGetLineIconAttach(lineData, {
+      width: data.endLen - data.startLen,
+      height: data.height,
+    });
+    const attach = getLineIconAttach(position);
+    attach && Object.assign(data, attach);
+  } else {
+    const line = getSnapLine(store, data)!;
+    const oldMat = getLineIconMat(line, data);
+    const incMat = mat.copy().multiply(oldMat.invert());
+    const points = getLineIconEndpoints(line, data);
+    const oldWidth = Math.abs(data.endLen - data.startLen);
+    let startLen = data.startLen;
+    let endLen = data.endLen;
+    if (operType === "middle-left") {
+      const startPoint = incMat.point(points[0]);
+      startLen = lineLen(line[0], startPoint);
+      if (!eqPoint(lineVector([line[0], startPoint]), lineVector(line))) {
+        startLen *= -1;
+      }
+    } else if (operType === "middle-right") {
+      const endPoint = incMat.point(points[1]);
+      endLen = lineLen(line[0], endPoint);
+      if (!eqPoint(lineVector([line[0], endPoint]), lineVector(line))) {
+        endLen *= -1;
+      }
+    }
+
+    if (isRangInner(line, { ...data, startLen, endLen })) {
+      const width = Math.abs(endLen - startLen);
+      data.startLen = startLen;
+      data.endLen = endLen;
+      if (data.type === "align-bottom") {
+        data.height = (data.height / oldWidth) * width;
+      }
+    }
+  }
+  if (data.type === "full") {
+    const wall = lineData.lines.find((line) => line.id === data.lineId);
+    wall && (data.height = wall.strokeWidth);
+  }
+  return data;
+};
+
+export const genGetLineIconAttach = (lineData: LineData, size: Size, minLen = 200) => {
+  const lines = lineData.lines.map((line) => [
+    lineData.points.find((p) => p.id === line.a)!,
+    lineData.points.find((p) => p.id === line.b)!,
+  ]);
+  const linevs = lines.map(lineVector);
+
+  return (position: Pos) => {
+    const shapeLines = lines.map((line, ndx) => {
+      const pjPoint = linePointProjection(line, position);
+      const offset = linevs[ndx].clone().multiplyScalar(size.width / 2);
+      const end = pjPoint.clone().add(offset);
+      const start = pjPoint.clone().add(offset.multiplyScalar(-1));
+      return { start, end, pjPoint, len: lineLen(pjPoint, position) };
+    });
+
+    let ndx = -1;
+    for (let i = 0; i < shapeLines.length; i++) {
+      if (
+        lineInner(lines[i], shapeLines[i].start) &&
+        lineInner(lines[i], shapeLines[i].end) &&
+        shapeLines[i].len < minLen
+      ) {
+        minLen = shapeLines[i].len;
+        ndx = i;
+      }
+    }
+    if (!~ndx) {
+      return null;
+    }
+
+    const attrib = shapeLines[ndx];
+    const shapev = lineVector([position, attrib.pjPoint]);
+    const angle = vector2IncludedAngle(
+      lineVector([attrib.start, attrib.end]),
+      shapev
+    );
+
+    return {
+      openSide: angle < 0 ? "RIGHT" : "LEFT",
+      lineId: lineData.lines[ndx].id,
+      startLen: lineLen(lines[ndx][0], shapeLines[ndx].start),
+      endLen: lineLen(lines[ndx][0], shapeLines[ndx].end),
+      addLen: shapeLines[ndx].len,
+    } as Pick<LineIconData, "openSide" | "lineId" | "startLen" | "endLen"> & {
+      addLen: number;
+    };
+  };
+};
+
+export const interactiveToData: InteractiveTo<"lineIcon"> = ({
+  info,
+  preset = {},
+  viewTransform,
+  ...args
+}) => {
+  if (info.cur) {
+    return interactiveFixData({
+      ...args,
+      viewTransform,
+      info,
+      data: {
+        ...getBaseItem(),
+        type: "align-bottom",
+        ...preset,
+      } as unknown as LineIconData,
+    });
+  }
+};
+
+export const interactiveFixData: InteractiveFix<"lineIcon"> = ({
+  data,
+  info,
+  store,
+}) => {
+  const lineData = store.getTypeItems("line")[0];
+  if (!lineData) throw "没有线段数据,无法添加icon";
+  const width = (data as any).width || data.height;
+
+  const getLineIconAttach = genGetLineIconAttach(lineData, {
+    width,
+    height: data.height,
+  });
+  const attach = getLineIconAttach(info.cur!);
+  attach && Object.assign(data, attach);
+
+  if (data.type === "full") {
+    const wall = lineData.lines.find((line) => line.id === data.lineId);
+    wall && (data.height = wall.strokeWidth);
+  }
+
+  return data;
+};
+
+export const checkItemData = (item: LineIconData) => !!item.lineId
+
+export const getSnapPoints = () => [];
+export const getSnapInfos = () => [];

+ 48 - 0
src/core/components/line-icon/temp-icon.vue

@@ -0,0 +1,48 @@
+<template>
+  <TempIcon
+    :data="data"
+    :disableGroupOper="true"
+    :add-mode="addMode"
+    v-if="data && props.data.openSide"
+    :ref="(e: any) => shape = e?.shape"
+  />
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import { getLineIconMat, getSnapLine, isRangInner, LineIconData } from ".";
+import TempIcon from "../icon/temp-icon.vue";
+import { useStore } from "@/core/store";
+import { Group } from "konva/lib/Group";
+import { DC } from "@/deconstruction";
+
+const shape = ref<DC<Group>>();
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+});
+
+const props = defineProps<{ data: LineIconData; addMode?: boolean }>();
+const store = useStore();
+const line = computed(() => getSnapLine(store, props.data));
+const mat = computed(() => {
+  if (!line.value) return;
+  return getLineIconMat(line.value, props.data).m;
+});
+
+const rangInner = computed(() => line.value && isRangInner(line.value, props.data));
+
+const data = computed(() => {
+  if (mat.value) {
+    const iconData = {
+      ...props.data,
+      mat: mat.value,
+      width: Math.abs(props.data.endLen - props.data.startLen),
+      coverFill: rangInner.value ? undefined : "red",
+      coverOpcatiy: rangInner.value ? 0 : 0.3,
+    };
+    return iconData;
+  }
+});
+</script>

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

@@ -0,0 +1,765 @@
+import {
+  SnapResultInfo,
+  useCustomSnapInfos,
+  useSnap,
+  useSnapConfig,
+} from "@/core/hook/use-snap";
+import { defaultStyle, getSnapInfos, LineData, LineDataLine } from ".";
+import {
+  eqPoint,
+  getVectorLine,
+  lineCenter,
+  lineIntersection,
+  lineLen,
+  linePointLen,
+  linePointProjection,
+  lineVector,
+  Pos,
+  vector2IncludedAngle,
+  verticalVector,
+  zeroEq,
+} from "@/utils/math";
+import {
+  copy,
+  frameEebounce,
+  mergeFuns,
+  onlyId,
+  rangMod,
+} from "@/utils/shared";
+import { MathUtils, Vector2 } from "three";
+import { generateSnapInfos, getBaseItem } from "../util";
+import { ComponentSnapInfo } from "..";
+import { useStore } from "@/core/store";
+import { computed, onUnmounted, reactive, ref, Ref, watch } from "vue";
+import {
+  useCursor,
+  usePointerPos,
+  useRunHook,
+  useStage,
+} from "@/core/hook/use-global-vars";
+import { PropertyDescribes } from "@/core/html-mount/propertys";
+import { useMode } from "@/core/hook/use-status";
+import { useListener } from "@/core/hook/use-event";
+import { Mode } from "@/constant/mode";
+import { useViewerInvertTransform } from "@/core/hook/use-viewer";
+import { clickListener } from "@/utils/event";
+import {
+  genGetLineIconAttach,
+  getLineIconMat,
+  LineIconData,
+} from "../line-icon";
+import { useDrawIngData } from "@/core/hook/use-draw";
+import { useComponentDescribes } from "@/core/hook/use-component";
+
+export type NLineDataCtx = {
+  del: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineDataLine>;
+  };
+  add: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineDataLine>;
+  };
+  update: {
+    points: Record<string, LineData["points"][0]>;
+    lines: Record<string, LineDataLine>;
+  };
+};
+export const getInitCtx = (): NLineDataCtx => ({
+  del: {
+    points: {},
+    lines: {},
+  },
+  add: {
+    points: {},
+    lines: {},
+  },
+  update: {
+    points: {},
+    lines: {},
+  },
+});
+
+export const repPointRef = (
+  data: LineData,
+  delId: string,
+  repId: string,
+  queUpdate = true
+) => {
+  for (let i = 0; i < data.lines.length; i++) {
+    const line = data.lines[i];
+    if (line.a === delId) {
+      if (queUpdate) {
+        data.lines[i] = { ...line, a: repId };
+      } else {
+        data.lines[i].a = repId;
+      }
+    }
+    if (line.b === delId) {
+      if (queUpdate) {
+        data.lines[i] = { ...line, b: repId };
+      } else {
+        data.lines[i].b = repId;
+      }
+    }
+  }
+  return data;
+};
+
+export const getLinePoints = (data: LineData, line: LineDataLine) => [
+  data.points.find((p) => p.id === line.a)!,
+  data.points.find((p) => p.id === line.b)!,
+];
+
+export const deduplicateLines = (data: LineData) => {
+  const seen = new Map<string, LineDataLine>();
+  let isChange = false;
+  for (const line of data.lines) {
+    if (line.a === line.b) continue;
+    // 生成标准化键:确保 (a,b) 和 (b,a) 被视为相同,并且 a === b 时也去重
+    const key1 = `${line.a},${line.b}`;
+    const key2 = `${line.b},${line.a}`;
+
+    // 检查是否已存在相同键
+    const existingKey = seen.has(key1) ? key1 : seen.has(key2) ? key2 : null;
+
+    if (existingKey) {
+      // 如果存在重复键,覆盖旧值(保留尾部元素)
+      seen.delete(existingKey);
+      seen.set(key1, line); // 统一存储为 key1 格式
+      isChange = true;
+    } else {
+      // 新记录,直接存储
+      seen.set(key1, line);
+    }
+  }
+  if (isChange) {
+    data.lines = Array.from(seen.values());
+  }
+
+  return data;
+};
+
+export const getJoinLine = (
+  data: LineData,
+  line: LineDataLine,
+  pId: string
+) => {
+  const pointIds = [line.a, line.b];
+  return data.lines
+    .filter(
+      (item) =>
+        (item.a === pId || item.b === pId) &&
+        !(pointIds.includes(item.a) && pointIds.includes(item.b))
+    )
+    .map((line) => {
+      const pointIds = pId === line.a ? [line.a, line.b] : [line.b, line.a];
+      return {
+        ...line,
+        points: pointIds.map((id) => data.points.find((p) => p.id === id)!),
+      };
+    });
+};
+
+export const foreNormalLineData = (data:LineData) => {
+  for (let i = 0; i < data.lines.length; i++) {
+    const {a, b} = data.lines[i]
+    if (!data.points.some(p => p.id === a) || !data.points.some(p => p.id === b)) {
+      data.lines.splice(i--, 1)
+    }
+  }
+  for (let i = 0; i < data.points.length; i++) {
+    const id = data.points[i].id
+    if (!data.lines.some(l => l.a === id || l.b === id)) {
+      data.points.splice(i, 1)
+    }
+  }
+}
+
+export const normalLineData = (data: LineData, ctx: NLineDataCtx) => {
+  const changePoints = [
+    ...Object.values(ctx.add.points),
+    ...Object.values(ctx.update.points),
+  ];
+
+  // 合并相同点
+  for (const p2 of changePoints) {
+    const ndx = data.points.findIndex((item) => item.id === p2.id);
+    if (!~ndx) continue;
+
+    for (let i = 0; i < data.points.length; i++) {
+      const p1 = data.points[i];
+      if (p1.id !== p2.id && eqPoint(p1, p2)) {
+        repPointRef(data, p1.id, p2.id);
+        data.points.splice(i, 1);
+        i--;
+      }
+    }
+  }
+
+  // 删除线a b 点一样的线段
+  for (let i = 0; i < data.lines.length; i++) {
+    const line = data.lines[i];
+    if (line.a === line.b) {
+      data.lines.splice(i--, 1);
+    }
+  }
+
+  // 删除游离点
+  const pointIds = Object.values(ctx.del.lines).flatMap((item) => [
+    item.a,
+    item.b,
+  ]);
+  pointIds.push(...Object.keys(ctx.add.points));
+  const linePointIds = data.lines.flatMap((item) => [item.a, item.b]);
+  for (let id of pointIds) {
+    if (!linePointIds.includes(id)) {
+      const ndx = data.points.findIndex((p) => p.id === id);
+      ~ndx && data.points.splice(ndx, 1);
+    }
+  }
+  // foreNormalLineData(data)
+  return deduplicateLines(data);
+};
+
+export const genMoveLineHandler = (
+  data: LineData,
+  lineId: string,
+  snapConfig: ReturnType<typeof useSnapConfig>,
+  snapResult: SnapResultInfo,
+  ctx = getInitCtx()
+) => {
+  const line = data.lines.find((line) => line.id === lineId)!;
+  const pointIds = [line.a, line.b];
+  const points = pointIds.map((id) => data.points.find((p) => p.id === id)!);
+  const lineDire = lineVector(points);
+  const initPoints = copy(points);
+  const angleRange = [MathUtils.degToRad(10), MathUtils.degToRad(170)];
+
+  const getRefInfo = (moveDire: Vector2, ndx: number) => {
+    const joinLines = getJoinLine(data, line, pointIds[ndx]);
+    const joinLineDires: Vector2[] = [];
+    const linePoints = [points[ndx], points[Number(!ndx)]];
+    const lineDire = lineVector(linePoints);
+    const joinPoints: LineData["points"] = [];
+
+    let invAngle = Number.MAX_VALUE;
+    let invSelectLineId: string;
+    let invSelectLineDire: Vector2 | null = null;
+
+    let alongAngle = -Number.MAX_VALUE;
+    let alongSelectLineId: string;
+    let alongSelectLineDire: Vector2 | null = null;
+
+    for (const line of joinLines) {
+      joinPoints.push(...line.points.filter((p) => !points.includes(p)));
+      const joinDire = lineVector(line.points);
+      joinLineDires.push(joinDire);
+
+      const angle = vector2IncludedAngle(lineDire, joinDire);
+      if (angle > 0) {
+        if (angle < invAngle) {
+          invAngle = angle;
+          invSelectLineId = line.id;
+          invSelectLineDire = joinDire;
+        }
+      } else {
+        if (angle > alongAngle) {
+          alongAngle = angle;
+          alongSelectLineId = line.id;
+          alongSelectLineDire = joinDire;
+        }
+      }
+    }
+
+    if (!invSelectLineDire && !alongSelectLineDire) {
+      return;
+    }
+    let isAlong = !invSelectLineDire;
+    if (!isAlong && alongSelectLineDire) {
+      const invMoveAngle = Math.abs(
+        vector2IncludedAngle(moveDire, invSelectLineDire!)
+      );
+      const alongMoveAngle = Math.abs(
+        vector2IncludedAngle(moveDire, alongSelectLineDire!)
+      );
+      isAlong = alongMoveAngle! < invMoveAngle!;
+    }
+    let info = isAlong
+      ? {
+          lineDire,
+          selectLineDire: alongSelectLineDire!,
+          selectLineId: alongSelectLineId!,
+          angle: alongAngle!,
+        }
+      : {
+          lineDire,
+          selectLineDire: invSelectLineDire!,
+          selectLineId: invSelectLineId!,
+          angle: invAngle!,
+        };
+
+    info.angle = rangMod(info.angle, Math.PI);
+    const needVertical =
+      info.angle > angleRange[1] || info.angle < angleRange[0];
+    const needSplit =
+      needVertical ||
+      joinLineDires.some(
+        (dire) =>
+          dire !== info.selectLineDire &&
+          !zeroEq(
+            rangMod(vector2IncludedAngle(dire, info.selectLineDire), Math.PI)
+          )
+      );
+    return { ...info, needSplit, needVertical, joinPoints };
+  };
+
+  let refInfos: ReturnType<typeof getRefInfo>[];
+  let snapLines: (null | Pos[])[];
+  let inited = false;
+  let norNdx = -1;
+
+  const init = (moveDires: Vector2[]) => {
+    refInfos = [getRefInfo(moveDires[0], 0), getRefInfo(moveDires[0], 1)];
+    snapLines = [];
+    let minAngle = Math.PI / 2;
+    const vLineDire = verticalVector(lineDire);
+    for (let i = 0; i < refInfos.length; i++) {
+      const refInfo = refInfos[i];
+      if (!refInfo) {
+        continue;
+      }
+      if (refInfo.needSplit) {
+        // 拆分点
+        const point = points[i];
+        const newPoint = { ...point, id: onlyId() };
+        data.points.push(newPoint);
+        repPointRef(data, point.id, newPoint.id, false);
+        const newLine = {
+          ...getBaseItem(),
+          ...defaultStyle,
+          a: point.id,
+          b: newPoint.id,
+        };
+        data.lines.push(newLine);
+        ctx.add.lines[newLine.id] = newLine;
+        ctx.add.points[newPoint.id] = newPoint;
+
+        if (i) {
+          line.b = point.id;
+        } else {
+          line.a = point.id;
+        }
+      }
+      const dire = refInfo.needVertical
+        ? verticalVector(refInfo.selectLineDire)
+        : refInfo.selectLineDire;
+
+      const angle = rangMod(vector2IncludedAngle(dire, vLineDire), Math.PI / 2);
+      if (angle < minAngle) {
+        norNdx = i;
+        minAngle = angle;
+      }
+      snapLines[i] = getVectorLine(dire, copy(points[i]), 10);
+    }
+  };
+  const assignPos = (origin: LineData["points"][0], target: Pos) => {
+    origin.x = target.x;
+    origin.y = target.y;
+    ctx.update.points[origin.id] = origin;
+  };
+
+  const updateOtPoint = (ndx: number) => {
+    const uNdx = ndx === 1 ? 0 : 1;
+    const move = new Vector2(
+      points[ndx].x - initPoints[ndx].x,
+      points[ndx].y - initPoints[ndx].y
+    );
+    if (!snapLines[uNdx]) {
+      assignPos(points[uNdx], move.add(initPoints[uNdx]));
+    } else {
+      assignPos(
+        points[uNdx],
+        lineIntersection(getVectorLine(lineDire, points[ndx]), snapLines[uNdx])!
+      );
+    }
+  };
+
+  const move = (finalPoss: Pos[]) => {
+    if (!inited) {
+      const moveDires = finalPoss.map((pos, ndx) =>
+        lineVector([initPoints[ndx], pos])
+      );
+      inited = true;
+      init(moveDires);
+    }
+
+    if (!snapLines[0] && !snapLines[1]) {
+      assignPos(points[0], finalPoss[0]);
+      assignPos(points[1], finalPoss[1]);
+    } else if (!snapLines[0]) {
+      const pos = linePointProjection(snapLines[1]!, finalPoss[1]);
+      assignPos(points[1], pos);
+      updateOtPoint(1);
+    } else if (!snapLines[1]) {
+      const pos = linePointProjection(snapLines[0]!, finalPoss[0]);
+      assignPos(points[0], pos);
+      updateOtPoint(0);
+    } else {
+      const pos = linePointProjection(snapLines[norNdx]!, finalPoss[norNdx]);
+      assignPos(points[norNdx], pos);
+      updateOtPoint(norNdx);
+    }
+  };
+
+  const getSnapRefPoint = (
+    point: Pos,
+    refPoints: Pos[],
+    line: Pos[] | null
+  ) => {
+    for (const refPoint of refPoints) {
+      if (
+        lineLen(refPoint, point) < snapConfig.snapOffset &&
+        (!line || zeroEq(linePointLen(line, refPoint)))
+      ) {
+        return refPoint;
+      }
+    }
+  };
+
+  const snap = () => {
+    snapResult.clear();
+    let refPoint: Pos | undefined = undefined;
+    let ndx = -1;
+
+    const useRefPoint = () => {
+      const hv = [
+        { join: refPoint, refDirection: { x: 0, y: 1 } },
+        { join: refPoint, refDirection: { x: 1, y: 0 } },
+      ];
+      snapResult.attractSnaps.push(...(hv as any));
+      assignPos(points[ndx], refPoint!);
+      updateOtPoint(ndx);
+    };
+
+    if (refInfos[0]?.joinPoints) {
+      refPoint = getSnapRefPoint(
+        points[0],
+        refInfos[0]?.joinPoints,
+        snapLines[0]
+      );
+      if (refPoint) {
+        ndx = 0;
+        return useRefPoint();
+      }
+    }
+    if (refInfos[1]?.joinPoints) {
+      refPoint = getSnapRefPoint(
+        points[1],
+        refInfos[1]?.joinPoints,
+        snapLines[1]
+      );
+      if (refPoint) {
+        ndx = 1;
+        return useRefPoint();
+      }
+    }
+
+    const usedPoints = [
+      ...(refInfos[0]?.joinPoints || []),
+      ...(refInfos[1]?.joinPoints || []),
+      ...points,
+    ];
+    const refPoints = data.points.filter((p) => !usedPoints.includes(p));
+    for (let i = 0; i < points.length; i++) {
+      refPoint = getSnapRefPoint(points[i], refPoints, snapLines[i]);
+      if (refPoint) {
+        ndx = i;
+        return useRefPoint();
+      }
+    }
+  };
+
+  const end = () => {
+    snapResult.clear();
+  };
+
+  return {
+    move: (ps: Pos[]) => {
+      move(ps);
+      snap();
+    },
+    end,
+  };
+};
+
+export const useLineDataSnapInfos = () => {
+  const infos = useCustomSnapInfos();
+  const store = useStore();
+  const lineData = computed(() => store.getTypeItems("line")[0]);
+  let snapInfos: ComponentSnapInfo[];
+
+  const updateSnapInfos = (pointIds: string[]) => {
+    clear();
+    snapInfos = getSnapInfos({
+      ...lineData.value,
+      lines: lineData.value.lines.filter(
+        (item) => !(pointIds.includes(item.a) || pointIds.includes(item.b))
+      ),
+      points: lineData.value.points.filter(
+        (item) => !pointIds.includes(item.id)
+      ),
+    });
+    snapInfos.forEach((item) => {
+      infos.add(item);
+    });
+  };
+
+  const clear = () => {
+    snapInfos && snapInfos.forEach((item) => infos.remove(item));
+  };
+
+  return {
+    update: updateSnapInfos,
+    clear,
+  };
+};
+
+export const updateLineLength = (
+  lineData: LineData,
+  line: LineDataLine,
+  length: number,
+  flex?: "a" | "b" | "both",
+  vector?: Pos
+) => {
+  const points = [
+    lineData.points.find((p) => p.id === line.a)!,
+    lineData.points.find((p) => p.id === line.b)!,
+  ];
+  vector = vector || lineVector(points);
+
+  if (!flex) {
+    const aCount = lineData.lines.filter(
+      (line) => line.a === points[0].id || line.b === points[0].id
+    ).length;
+    const bCount = lineData.lines.filter(
+      (line) => line.a === points[1].id || line.b === points[1].id
+    ).length;
+    if (aCount === bCount || (aCount > 1 && bCount > 1)) {
+      flex = "both";
+    } else {
+      flex = aCount > 1 ? "b" : "a";
+    }
+  }
+
+  let moveVector = new Vector2(vector.x, vector.y);
+  let npoints: Pos[];
+  if (flex === "both") {
+    const center = lineCenter(points);
+    const l1 = getVectorLine(
+      moveVector.clone().multiplyScalar(-1),
+      center,
+      length / 2
+    );
+    const l2 = getVectorLine(moveVector, center, length / 2);
+    npoints = [l1[1], l2[1]];
+  } else {
+    const fNdx = flex === "a" ? 1 : 0;
+    const mNdx = flex === "a" ? 0 : 1;
+    const line = getVectorLine(
+      mNdx === 1 ? moveVector : moveVector.multiplyScalar(-1),
+      points[fNdx],
+      length
+    );
+    const nPoints: Pos[] = [];
+    nPoints[fNdx] = points[fNdx];
+    nPoints[mNdx] = line[1];
+    npoints = nPoints;
+  }
+  Object.assign(points[0], npoints[0]);
+  Object.assign(points[1], npoints[1]);
+};
+
+export const useLineDescribes = (line: Ref<LineDataLine>) => {
+  const d: any = useComponentDescribes(line, ["stroke", "strokeWidth"], {});
+  const store = useStore();
+  const lineData = computed(() => store.getTypeItems("line")[0]);
+  const points = computed(() => [
+    lineData.value.points.find((p) => p.id === line.value.a)!,
+    lineData.value.points.find((p) => p.id === line.value.b)!,
+  ]);
+  let setLineVector: Vector2;
+
+  watch(d, (d) => {
+    d.strokeWidth.props = {
+      ...d.strokeWidth.props,
+      proportion: true,
+    };
+    d.strokeWidth.label = "粗细";
+    d.stroke.label = "颜色";
+
+  d.length = {
+    type: "inputNum",
+    label: "线段长度",
+    "layout-type": "row",
+    get value() {
+      return lineLen(points.value[0], points.value[1]);
+    },
+    set value(val) {
+      console.log(val, d.length.isChange);
+      if (!d.isChange) {
+        setLineVector = lineVector(points.value);
+      }
+      updateLineLength(
+        lineData.value,
+        line.value,
+        val,
+        undefined,
+        setLineVector
+      );
+    },
+  };
+  }, {immediate: true});
+  return d as PropertyDescribes;
+};
+
+export const useDrawLinePoint = (
+  data: Ref<LineData>,
+  line: Ref<LineDataLine>,
+  callback: (data: {
+    prev: LineDataLine;
+    next: LineDataLine;
+    point: LineData["points"][0];
+    oldIcons: LineIconData[];
+    newIcons: LineIconData[];
+  }) => void
+) => {
+  const mode = useMode();
+  let __leave: (() => void) | null;
+  const leave = () => {
+    if (__leave) {
+      __leave();
+      __leave = null;
+    }
+  };
+
+  useListener("contextmenu", (ev) => ev.button === 2 && setTimeout(leave));
+  onUnmounted(leave);
+
+  const pos = usePointerPos();
+  const viewInvMat = useViewerInvertTransform();
+  const drawProps = ref<{
+    data: LineData;
+    prev: LineDataLine;
+    next: LineDataLine;
+    point: LineData["points"][0];
+  }>();
+  const runHook = useRunHook();
+  const snapInfos = useLineDataSnapInfos();
+  const snap = useSnap();
+  const stage = useStage();
+  const store = useStore();
+  const icons = computed(() =>
+    store.getTypeItems("lineIcon").filter((item) => item.lineId === line.value.id)
+  );
+  const drawStore = useDrawIngData();
+  const cursor = useCursor();
+  const enterDraw = () => {
+    const points = getLinePoints(data.value, line.value)
+    console.log(points, data.value, line.value)
+    const cdata: LineData = { ...data.value, points, lines: [] };
+    const point = reactive({ ...lineCenter(points), id: onlyId() });
+    const cIcons = icons.value.map((icon) => ({ ...icon, id: onlyId() }));
+    const iconInfos = icons.value.map((icon) => {
+      const mat = getLineIconMat(points, icon);
+      return {
+        position: { x: mat.m[4], y: mat.m[5] },
+        size: {
+          width: icon.endLen - icon.startLen,
+          height: icon.height,
+        },
+      };
+    });
+
+    const prev = { ...line.value, id: onlyId(), b: point.id };
+    const next = { ...line.value, id: onlyId(), a: point.id };
+    cdata.lines.push(prev, next);
+    cdata.points.push(point);
+
+    drawProps.value = { data: cdata, prev, next, point };
+    let isStop = false;
+    const afterUpdate = frameEebounce((position: Pos) => {
+      if (isStop) return;
+      snap.clear();
+      position = viewInvMat.value.point(position);
+
+      const mat = snap.move(generateSnapInfos([position], true, true));
+      Object.assign(point, mat ? mat.point(position) : position);
+
+      drawStore.lineIcon = [];
+      cIcons.forEach((icon, ndx) => {
+        const getAttach = genGetLineIconAttach(cdata, iconInfos[ndx].size, 200);
+        const attach = getAttach(iconInfos[ndx].position);
+        if (attach) {
+          const line = cdata.lines.find((item) => item.id === attach.lineId)!;
+          const snapLine = [
+            cdata.points.find((p) => p.id === line.a)!,
+            cdata.points.find((p) => p.id === line.b)!,
+          ];
+          const iconData = { ...icon, ...attach, __snapLine: snapLine };
+          drawStore.lineIcon!.push(iconData);
+        }
+      });
+    });
+    pos.replay();
+    snapInfos.update([]);
+    return mergeFuns(
+      cursor.push("./icons/m_add.png"),
+      runHook(() =>
+        clickListener(stage.value!.getNode().container(), () => {
+          callback({
+            prev,
+            next,
+            point,
+            oldIcons: icons.value,
+            newIcons: drawStore.lineIcon!,
+          });
+          leave();
+        })
+      ),
+      watch(
+        pos,
+        (pos) => {
+          pos && afterUpdate(pos);
+        },
+        { immediate: true }
+      ),
+      () => {
+        drawProps.value = undefined;
+        snapInfos.clear();
+        snap.clear();
+        isStop = true;
+      }
+    );
+  };
+
+  const enter = () => {
+    __leave = mergeFuns(
+      () => (__leave = null),
+      mode.push(Mode.draw),
+      watch(
+        () => mode.include(Mode.draw),
+        (hasDraw, _, onCleanup) => {
+          hasDraw ? onCleanup(enterDraw()) : leave();
+        },
+        { immediate: true }
+      )
+    );
+  };
+
+  return {
+    leave,
+    enter,
+    drawProps,
+  };
+};

+ 98 - 25
src/core/components/line/index.ts

@@ -2,11 +2,18 @@ import { lineVector, Pos, vectorAngle, verticalVector } from "@/utils/math.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
-import { Transform } from "konva/lib/Util";
 import { inRevise, onlyId, rangMod } from "@/utils/shared.ts";
 import { MathUtils } from "three";
-import { DrawStore } from "@/core/store/index.ts";
-import { getInitCtx, normalLineData } from "./use-draw.ts";
+import { DrawStore, useStore } from "@/core/store/index.ts";
+import {
+  SelectionManageBus,
+  UseGetSelectionManage,
+} from "@/core/hook/use-selection.ts";
+import { EntityShape } from "@/deconstruction.js";
+import mitt from "mitt";
+import { Ref, ref, watch } from "vue";
+import { getInitCtx, NLineDataCtx, normalLineData } from "./attach-server.ts";
+import * as wallRenderer from "./renderer/wall";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
@@ -19,6 +26,19 @@ export const defaultStyle = {
   dash: [30, 0],
 };
 
+export const renderer = ref<{
+  genGetShapeAttrib: (data: Ref<LineData | undefined>) => any;
+  Component: any;
+}>();
+export const setRendererType = async (type?: "wall" | "db-line") => {
+  if (type === "wall") {
+    renderer.value = wallRenderer;
+  } else {
+    renderer.value = undefined;
+  }
+};
+setRendererType("wall");
+
 export const addMode = "single-dots";
 
 export const getMouseStyle = (data: LineData) => {
@@ -55,19 +75,20 @@ export const getSnapPoints = (data: LineData) => {
   return data.points;
 };
 
+export type LineDataLine = {
+  id: string;
+  a: string;
+  b: string;
+  strokeWidth: number;
+  stroke: string;
+  dash: number[];
+};
+export type LineDataPoint = Pos & { id: string };
 export type LineData = Partial<typeof defaultStyle> &
   BaseItem & {
-    points: (Pos & { id: string })[];
-    lines: {
-      id: string;
-      a: string;
-      b: string;
-      strokeWidth: number;
-      stroke: string;
-      dash: number[];
-    }[];
+    points: LineDataPoint[];
+    lines: LineDataLine[];
     polygon: { points: string[]; id: string }[];
-    attitude: number[];
     updateTime?: number;
     calcTime?: number;
   };
@@ -89,7 +110,6 @@ export const interactiveToData: InteractiveTo<"line"> = ({
         lines: [],
         points: [],
         polygon: [],
-        attitude: [1, 0, 0, 1, 0, 0],
       },
     });
   }
@@ -130,31 +150,59 @@ export const interactiveFixData: InteractiveFix<"line"> = ({ data, info }) => {
   return data;
 };
 
+const matResPoints = new Set<string>();
+let matCtx: NLineDataCtx | null;
+export const startMatResponse = () => {
+  matCtx = getInitCtx();
+};
+
 export const matResponse = ({
   data,
   mat,
-  increment,
+  operId,
 }: MatResponseProps<"line">) => {
-  let transfrom: Transform;
-  const attitude = new Transform(data.attitude);
-  if (!increment) {
-    const inverMat = attitude.copy().invert();
-    transfrom = mat.copy().multiply(inverMat);
-  } else {
-    transfrom = mat;
+  const line = data.lines.find((item) => item.id === operId);
+  if (!line) return;
+  const ids = [line.a, line.b];
+  for (const id of ids) {
+    if (matResPoints.has(id)) {
+      continue;
+    }
+    const ndx = data.points.findIndex((item) => item.id === id);
+    if (~ndx) {
+      const point = data.points[ndx];
+      data.points[ndx] = {
+        ...point,
+        ...mat.point(point),
+      };
+      matCtx!.update.points[point.id] = data.points[ndx];
+      matResPoints.add(id);
+    }
   }
-
-  data.points = data.points.map((v) => ({ ...v, ...transfrom.point(v) }));
-  data.attitude = transfrom.copy().multiply(attitude).m;
   return data;
 };
 
+export const endMatResponse = () => {
+  matResPoints.clear();
+  // matCtx && normalLineData(matData, matCtx)
+  // console.log(matData, matCtx)
+  matCtx = null;
+};
+
 export const getPredefine = (key: keyof LineData) => {
   if (key === "strokeWidth") {
     return { proportion: true };
   }
 };
 
+export const childrenDataGetter = (data: LineData, id: string) => {
+  const line = data.lines.find((item) => item.id === id);
+  if (!line) return;
+
+  const ids = [line.a, line.b];
+  return data.points.filter((p) => ids.includes(p.id));
+};
+
 export const delItem = (store: DrawStore, data: LineData, childId: string) => {
   if (!childId) {
     store.delItem("line", data.id);
@@ -216,3 +264,28 @@ export const delPoint = (data: LineData, id: string, ctx = getInitCtx()) => {
   ~ndx && data.points.splice(ndx, 1);
   return { data, ctx };
 };
+
+export const useGetSelectionManage: UseGetSelectionManage = () => {
+  const store = useStore();
+
+  const canSelect = (shape: EntityShape) => {
+    const id = shape.id();
+    const line = store.getTypeItems("line")[0];
+    return !!(id && line.lines.some((item) => item.id === id));
+  };
+  const listener = (shape: EntityShape) => {
+    const bus: SelectionManageBus = mitt();
+    const stop = watch(
+      () => canSelect(shape),
+      (exixts, _) => {
+        if (!exixts) {
+          bus.emit("del", shape);
+        }
+      },
+      { immediate: true }
+    );
+    return { stop, bus };
+  };
+
+  return { canSelect, listener };
+};

+ 12 - 7
src/core/components/line/line.vue

@@ -1,18 +1,23 @@
 <template>
-  <TempLine
-    :data="data"
-    @updateShape="emit('updateShape', { ...data })"
-    :canEdit="true"
-  />
+  <TempLine :data="data" @updateShape="emit('updateShape', data)" />
 </template>
 
 <script lang="ts" setup>
-import { useAutomaticData } from "@/core/hook/use-automatic-data.ts";
+import { watch } from "vue";
+import { foreNormalLineData } from "./attach-server.ts";
 import { LineData } from "./index.ts";
 import TempLine from "./temp-line.vue";
 
 const props = defineProps<{ data: LineData }>();
-const data = useAutomaticData(() => props.data);
+
+watch(
+  () => props.data,
+  (data) => {
+    foreNormalLineData(data);
+  },
+  { immediate: true }
+);
+
 const emit = defineEmits<{
   (e: "updateShape", value: LineData): void;
   (e: "addShape", value: LineData): void;

+ 5 - 0
src/core/components/line/renderer/wall/index.ts

@@ -0,0 +1,5 @@
+import { useGetExtendPolygon } from "./view";
+import Component from './index.vue'
+
+export const genGetShapeAttrib = useGetExtendPolygon
+export { Component }

+ 54 - 0
src/core/components/line/renderer/wall/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <v-line
+    v-for="polygon in polygons"
+    :config="{
+      opacity: opacity,
+      points: flatPositions(polygon),
+      fill: stroke,
+      closed: true,
+      listening: false,
+    }"
+  />
+
+  <template v-if="showLabel">
+    <v-group>
+      <template v-if="gd.steps.value.length">
+        <SizeLine
+          :points="line"
+          :strokeWidth="props.line.strokeWidth"
+          :stroke="props.line.stroke"
+          v-for="line in [...gd.steps.value, ...gd.subSteps.value]"
+        />
+      </template>
+      <SizeLine
+        :points="points"
+        :strokeWidth="props.line.strokeWidth"
+        :stroke="props.line.stroke"
+        v-else
+      />
+    </v-group>
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed } from "vue";
+import { useGetDiffLineIconPolygons, useGetExtendPolygon } from "./view";
+import { LineData, LineDataLine } from "../..";
+import { getLinePoints } from "../../attach-server";
+import { flatPositions } from "@/utils/shared";
+import SizeLine from "../../../share/size-line.vue";
+
+const props = defineProps<{
+  getShapeAttrib: ReturnType<typeof useGetExtendPolygon>;
+  data: LineData;
+  line: LineDataLine;
+  opacity?: number;
+  stroke?: string;
+  showLabel: any;
+}>();
+
+const polygon = computed(() => props.getShapeAttrib(props.line));
+const points = computed(() => getLinePoints(props.data, props.line));
+const gd = useGetDiffLineIconPolygons(props.line, points);
+const polygons = computed(() => gd.diff(polygon.value));
+</script>

+ 380 - 0
src/core/components/line/renderer/wall/view.ts

@@ -0,0 +1,380 @@
+import {
+  eqPoint,
+  getLEJJoinNdxs,
+  getLEJLineAngle,
+  getLineEdgeJoinInfo,
+  getLineEdges,
+  LEJInfo,
+  LEJLine,
+  lineVector,
+  Pos,
+  verticalVector,
+} from "@/utils/math";
+import { LineData, LineDataLine } from "../..";
+import { getJoinLine, getLinePoints } from "../../attach-server";
+import { MathUtils } from "three";
+import { diffArrayChange, round } from "@/utils/shared";
+import { useStore } from "@/core/store";
+import { computed, nextTick, reactive, Ref, watch } from "vue";
+import { getLineIconEndpoints } from "../../../line-icon";
+import { useDrawIngData } from "@/core/hook/use-draw";
+import { polygonDifference, polygonDifferenceOnly } from "@/utils/math-clip";
+
+export const useGetExtendPolygon = (lineData: Ref<LineData | undefined>) => {
+  const minAngle = MathUtils.degToRad(0.1);
+  const palAngle = MathUtils.degToRad(20);
+
+  type JInfo = { lej: LEJInfo | undefined; diffPolygons?: Pos[][] };
+  const joinInfos = reactive({}) as Record<string, Record<string, JInfo>>;
+
+  const getInfoKey = (line: LEJLine) =>
+    line.points.reduce((t, p) => round(p.x, 3) + round(p.y, 3) + t, "") + line.width;
+
+  const setLEJInfo = (
+    data: LineData,
+    originLine: LineDataLine,
+    targetLine: LineDataLine
+  ) => {
+    const origin = {
+      points: getLinePoints(data, originLine),
+      width: originLine.strokeWidth,
+    };
+    const target = {
+      points: getLinePoints(data, targetLine),
+      width: targetLine.strokeWidth,
+    };
+    const { originNdx } = getLEJJoinNdxs(origin.points, target.points);
+    const lej = getLineEdgeJoinInfo(origin, target, minAngle, palAngle);
+    const originKey = getInfoKey(origin);
+
+    if (!(originKey in joinInfos)) {
+      joinInfos[originKey] = {};
+    }
+    if (!(originNdx in joinInfos[originKey])) {
+      joinInfos[originKey][originNdx] = { lej };
+    }
+    return joinInfos[originKey][originNdx];
+  };
+
+  const getLEJPolygon = (data: LineData, originLine: LineDataLine) => {
+    const origin = {
+      points: getLinePoints(data, originLine),
+      width: originLine.strokeWidth,
+    };
+    if (!origin.points[0] || !origin.points[1]) return [];
+    const key = getInfoKey(origin);
+    let originEdges: Pos[] = getLineEdges(origin.points, origin.width);
+    const initOriginEdges = [...originEdges];
+
+    const jInfos: (JInfo | undefined)[] = [
+      joinInfos[key]?.[0],
+      joinInfos[key]?.[1],
+    ];
+
+    if (!(key in joinInfos)) {
+      return originEdges;
+    }
+    for (const info of jInfos) {
+      if (!info?.lej) continue;
+      for (const rep of info.lej) {
+        const ndx = originEdges.indexOf(initOriginEdges[rep.rep]);
+        originEdges.splice(ndx, 1, ...rep.points);
+      }
+    }
+
+    for (const info of jInfos) {
+      if (!info?.diffPolygons) continue;
+      originEdges = polygonDifferenceOnly(originEdges, info.diffPolygons);
+    }
+
+    return originEdges;
+  };
+
+  const setManyJoinInfo = (data: LineData, lines: LineDataLine[]) => {
+    type Select = ReturnType<typeof getLEJLineAngle> & {
+      origin: LineDataLine;
+      target: LineDataLine;
+    };
+    const selectLEJLines = (lines: LineDataLine[]) => {
+      let select: Select;
+      let maxAngle = -999;
+      for (let i = 0; i < lines.length; i++) {
+        const line1 = getLinePoints(data, lines[i]);
+        for (let j = i + 1; j < lines.length; j++) {
+          const line2 = getLinePoints(data, lines[j]);
+          const ejlAngle = getLEJLineAngle(line1, line2);
+          if (ejlAngle.norAngle > maxAngle) {
+            maxAngle = ejlAngle.norAngle;
+            select = {
+              ...ejlAngle,
+              origin: lines[i],
+              target: lines[j],
+            };
+          }
+        }
+      }
+      return select!;
+    };
+
+    let diffPolygons: Pos[][] = [];
+    const pointIds = lines.flatMap(l => [l.a, l.b])
+    const lineCount = lines.length
+    while (lines.length) {
+      if (lines.length > 1) {
+        const select = selectLEJLines(lines)!;
+        const origin = setLEJInfo(data, select.origin, select.target);
+        const target = setLEJInfo(data, select.target, select.origin);
+
+        lines = lines.filter(
+          (line) => line !== select.origin && line !== select.target
+        );
+        origin.diffPolygons = diffPolygons
+        target.diffPolygons = diffPolygons
+
+        diffPolygons = [
+          ...diffPolygons,
+          getLEJPolygon(data, select.origin),
+          getLEJPolygon(data, select.target),
+        ];
+      } else {
+        const key = getInfoKey({
+          points: getLinePoints(data, lines[0]),
+          width: lines[0].strokeWidth,
+        });
+        if (!(key in joinInfos)) {
+          joinInfos[key] = {};
+        }
+        const ndx = [lines[0].a, lines[0].b].findIndex(
+          (id) => pointIds.filter(pid => id === pid).length === lineCount
+        );
+        joinInfos[key][ndx] = { lej: undefined, diffPolygons };
+        lines = [];
+      }
+    }
+  };
+
+  const genLEJLine = (lineData: LineData, line: LineDataLine) => ({
+    points: getLinePoints(lineData, line),
+    width: line.strokeWidth,
+  });
+
+  const init = (data: LineData) => {
+    const watchLine = (line: LineDataLine) => {
+      const joina = computed(() => getJoinLine(data, line, line.a));
+      const joinb = computed(() => getJoinLine(data, line, line.b));
+      const self = computed(() => genLEJLine(data, line));
+      const getWatchKey = () => {
+        const lines = [
+          ...joina.value.map((l) => genLEJLine(data, l)),
+          ...joinb.value.map((l) => genLEJLine(data, l)),
+          self.value,
+        ];
+        if (lines.some((l) => !l.points[0] || !l.points[1])) {
+          return null;
+        } else {
+          return lines.map(getInfoKey).join(",");
+        }
+      };
+
+      return watch(
+        getWatchKey,
+        (wkey, _2, onCleanup) => {
+          if (!wkey) return;
+          const key = getInfoKey(self.value);
+          const calcNdxs: number[] = [];
+
+          if (!(key in joinInfos)) {
+            joinInfos[key] = {};
+            calcNdxs.push(0, 1);
+          } else {
+            "0" in joinInfos[key] || calcNdxs.push(0);
+            "1" in joinInfos[key] || calcNdxs.push(1);
+          }
+
+          for (const ndx of calcNdxs) {
+            const joins = ndx === 0 ? joina.value : joinb.value;
+            if (joins.length === 0) {
+              joinInfos[key][ndx] = { lej: undefined };
+            } else if (joins.length !== 1) {
+              setManyJoinInfo(data, [line, ...joins]);
+            } else {
+              setLEJInfo(data, line, joins[0]);
+              setLEJInfo(data, joins[0], line);
+            }
+          }
+
+          onCleanup(() => {
+            delete joinInfos[key];
+          });
+        },
+        { immediate: true, flush: 'post' }
+      );
+    };
+
+    let isStop = false;
+    const stopMap = new WeakMap<LineDataLine, () => void>();
+    const stopWatch = watch(
+      () => [...data.lines],
+      async (newLines, oldLines = []) => {
+        const { added, deleted } = diffArrayChange(newLines, oldLines);
+        deleted.forEach((line) => {
+          const fn = stopMap.get(line);
+          fn && fn();
+        });
+        await nextTick();
+        if (!isStop) {
+          added.forEach((line) => {
+            stopMap.set(line, watchLine(line));
+          });
+        }
+        deleted;
+      },
+      { immediate: true }
+    );
+
+    return () => {
+      isStop = true;
+      stopWatch();
+      data.lines.forEach((line) => {
+        const fn = stopMap.get(line);
+        fn && fn();
+      });
+    };
+  };
+
+  watch(
+    lineData,
+    (data, _, onCleanup) => {
+      data && onCleanup(init(data));
+    },
+    { immediate: true }
+  );
+
+  return (line: LineDataLine) => {
+    const polygon = lineData.value ? getLEJPolygon(lineData.value, line) : [];
+    return polygon
+  };
+};
+
+// 计算与icon相差的多边形
+export const useGetDiffLineIconPolygons = (
+  line: LineDataLine,
+  linePoints: Ref<Pos[]>
+) => {
+  const store = useStore();
+  const drawStore = useDrawIngData();
+  const linevv = computed(() => verticalVector(lineVector(linePoints.value!)));
+  const icons = computed(() => {
+    const icons = store
+      .getTypeItems("lineIcon")
+      .concat(drawStore.lineIcon || []);
+    return icons.filter((item) => !item.hide);
+  });
+  const lineIcons = computed(() =>
+    icons.value.filter((icon) => icon.lineId === line.id)
+  );
+
+  const interSteps = computed(() => {
+    const lineSteps = lineIcons.value
+      .map((icon) => {
+        const openSide = icon.openSide;
+        const endLen = icon.endLen || 0.001;
+        const startLen = icon.startLen || 0.001;
+        const inv = endLen < startLen;
+        return {
+          lens: inv ? [endLen, startLen] : [startLen, endLen],
+          openSide: inv ? (openSide === "LEFT" ? "RIGHT" : "LEFT") : openSide,
+        };
+      })
+      .sort((line1, line2) => line1.lens[0] - line2.lens[0]);
+    if (!lineSteps.length) return [];
+
+    const interSteps: { lens: number[]; openSide: "LEFT" | "RIGHT" }[] = [];
+    let i = 0;
+    do {
+      const startStep = lineSteps[i].lens[0];
+      const openSide = lineSteps[i].openSide;
+      let endStep = lineSteps[i].lens[1];
+      for (i++; i < lineSteps.length; i++) {
+        if (lineSteps[i].lens[0] <= endStep) {
+          if (lineSteps[i].lens[1] > endStep) {
+            endStep = lineSteps[i].lens[1];
+          }
+        } else {
+          break;
+        }
+      }
+      interSteps.push({
+        lens: [startStep, endStep],
+        openSide,
+      });
+    } while (i < lineSteps.length);
+
+    return interSteps;
+  });
+
+  const interLines = computed(() =>
+    interSteps.value.map((steps) => ({
+      openSide: steps.openSide,
+      points: getLineIconEndpoints(linePoints.value!, {
+        startLen: steps.lens[0],
+        endLen: steps.lens[1],
+      }),
+    }))
+  );
+
+  const stepLines = computed(() => {
+    if (!linePoints.value?.length || !interLines.value.length) return [];
+
+    const steps: Pos[][] = [];
+    if (!eqPoint(linePoints.value[0], interLines.value[0].points[0])) {
+      steps.push([linePoints.value[0], interLines.value[0].points[0]]);
+    }
+    let start = interLines.value[0].points[1];
+
+    let i = 1;
+    for (; i < interLines.value.length; i++) {
+      const iLine = interLines.value[i];
+      steps.push([start, iLine.points[0]]);
+      start = iLine.points[1];
+    }
+
+    if (!eqPoint(start, linePoints.value[1])) {
+      steps.push([start, linePoints.value[1]]);
+    }
+
+    return steps;
+  });
+
+  const subStepsLines = computed(() => {
+    return interLines.value.map((il) => {
+      return il.openSide === "RIGHT" ? il.points : [...il.points].reverse();
+    });
+  });
+
+  const interPolygons = computed(() => {
+    return interLines.value.map((il) => {
+      const interLine = il.points;
+      const topOffset = linevv.value.clone().multiplyScalar(line.strokeWidth);
+      const botOffset = topOffset.clone().multiplyScalar(-1);
+      return [
+        topOffset.clone().add(interLine[0]),
+        topOffset.clone().add(interLine[1]),
+        botOffset.clone().add(interLine[1]),
+        botOffset.clone().add(interLine[0]),
+      ];
+    });
+  });
+
+  return {
+    diff: (polygon: Pos[]) => {
+      const result = interPolygons.value.length
+        ? polygonDifference(polygon, interPolygons.value)
+        : [polygon];
+
+      return result;
+    },
+    subSteps: subStepsLines,
+    steps: stepLines,
+  };
+};

+ 107 - 132
src/core/components/line/single-line.vue

@@ -1,12 +1,15 @@
 <template>
+  <!-- @dragstart="emit('dragLineStart', props.line)"
+    @update:line="(ps) => emit('dragLine', props.line, ps)"
+    @dragend="emit('dragLineEnd', props.line)" -->
   <EditLine
     :ref="(d: any) => shape = d?.shape"
-    :data="{ ...line, ...style }"
-    :opacity="isDrawIng ? 0.5 : 1"
+    :data="lineData"
+    :opacity="renderer ? 0 : 1"
     :points="points"
     :closed="false"
     :id="line.id"
-    :disablePoint="!canEdit || mode.include(Mode.readonly)"
+    :disablePoint="true"
     :ndx="0"
     @dragstart="dragstartHandler(points.map((item) => item.id))"
     @update:line="
@@ -19,39 +22,22 @@
     @add-point="addPoint"
   />
 
-  <SizeLine
-    v-if="
+  <component
+    v-if="renderer"
+    :is="renderer?.Component"
+    :opacity="drawProps ? 0.7 : 1"
+    :stroke="isDrawIng ? themeColor : style.stroke"
+    :getShapeAttrib="getShapeAttrib"
+    :data="data"
+    :line="line"
+    :showLabel="
       status.active ||
       config.showComponentSize ||
       isDrawIng ||
       dragPointIds?.includes(line.a) ||
       dragPointIds?.includes(line.b)
     "
-    :points="points"
-    :strokeWidth="style.strokeWidth"
-    :stroke="style.stroke"
   />
-
-  <template v-if="(!mode.include(Mode.readonly) && canEdit) || isDrawIng">
-    <EditPoint
-      v-for="(point, ndx) in points"
-      :key="point.id"
-      :opacity="1"
-      :size="line.strokeWidth"
-      :points="points"
-      :drawIng="ndx === 0 && isDrawIng"
-      :ndx="ndx"
-      :closed="false"
-      :id="line.id"
-      :disable="addMode"
-      :color="isDrawIng ? themeColor : style.stroke"
-      @dragstart="dragstartHandler([point.id])"
-      @update:position="(p) => emit('updatePoint', { ...point, ...p })"
-      @dragend="dragendHandler"
-      @delete="delPoint(point)"
-    />
-  </template>
-
   <PropertyUpdate
     :describes="describes"
     :data="line"
@@ -59,143 +45,143 @@
     :name="shapeName"
     @change="
       () => {
-        isStartChange || emit('updateBefore', []);
+        emit('updateBefore', []);
         emit('updateLine', { ...line });
         emit('update');
-        isStartChange = false;
       }
     "
     @delete="delHandler"
   />
   <Operate :target="shape" :menus="menus" />
+
+  <template v-if="drawProps">
+    <SingleLine
+      :data="drawProps.data"
+      :line="drawProps.prev"
+      :drawMode="drawProps.point"
+      :getShapeAttrib="drawGetShapeAttrib"
+    />
+    <singlePoint
+      :data="drawProps.data"
+      :line="drawProps.prev"
+      :drawMode="drawProps.point"
+    />
+    <SingleLine
+      :data="drawProps.data"
+      :line="drawProps.next"
+      :drawMode="drawProps.point"
+      :getShapeAttrib="drawGetShapeAttrib"
+    />
+    <singlePoint
+      :data="drawProps.data"
+      :line="drawProps.next"
+      :drawMode="drawProps.point"
+    />
+  </template>
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from "vue";
-import { getMouseStyle, getSnapInfos, LineData, shapeName } from "./index.ts";
-import { onlyId } from "@/utils/shared.ts";
 import EditLine from "../share/edit-line.vue";
-import { getVectorLine, lineCenter, lineLen, lineVector, Pos } from "@/utils/math.ts";
-import EditPoint from "../share/edit-point.vue";
+import singlePoint from "./single-point.vue";
+import { computed, ref, watchEffect } from "vue";
+import { getMouseStyle, LineData, LineDataLine, shapeName, renderer } from "./index.ts";
+import { onlyId } from "@/utils/shared.ts";
+import { Pos } from "@/utils/math.ts";
 import { Line } from "konva/lib/shapes/Line";
 import { DC } from "@/deconstruction.js";
-import { useMode } from "@/core/hook/use-status.ts";
-import { Mode } from "@/constant/mode.ts";
-import SizeLine from "../share/size-line.vue";
 import { useConfig } from "@/core/hook/use-config.ts";
-import { ComponentSnapInfo } from "../index.ts";
-import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
-import { mergeDescribes } from "@/core/html-mount/propertys/index.ts";
 import { PropertyUpdate, Operate } from "../../html-mount/propertys/index.ts";
 import {
   useAnimationMouseStyle,
   useMouseShapeStatus,
 } from "@/core/hook/use-mouse-status.ts";
 import { themeColor } from "@/constant";
-import { Vector2 } from "three";
-
-const mode = useMode();
+import {
+  getLinePoints,
+  useDrawLinePoint,
+  useLineDataSnapInfos,
+  useLineDescribes,
+} from "./attach-server.ts";
+import { useStore } from "@/core/store/index.ts";
+import { useHistory } from "@/core/hook/use-history.ts";
 
 const props = defineProps<{
-  line: LineData["lines"][number];
+  line: LineDataLine;
   addMode?: boolean;
   canEdit?: boolean;
   data: LineData;
   dragPointIds?: string[];
+  drawMode?: LineData["points"][number];
+  getShapeAttrib?: (line: LineDataLine) => any;
 }>();
 
 const emit = defineEmits<{
   (e: "updatePoint", value: LineData["points"][number]): void;
   (e: "addPoint", value: LineData["points"][number]): void;
-  (e: "delPoint", value: LineData["points"][number]): void;
+  (e: "addLine", value: LineDataLine): void;
   (e: "delLine"): void;
-  (e: "updateLine", value: LineData["lines"][number]): void;
+  (e: "updateLine", value: LineDataLine): void;
   (e: "updateBefore", value: string[]): void;
   (e: "update"): void;
+
+  (e: "dragLineStart", value: LineDataLine): void;
+  (e: "dragLine", line: LineDataLine, move: Pos[]): void;
+  (e: "dragLineEnd", value: LineDataLine): void;
 }>();
 
+const line = computed(() => props.line);
+const points = computed(() => getLinePoints(props.data, props.line));
+
 const shape = ref<DC<Line>>();
-const points = computed(() => [
-  props.data.points.find((p) => p.id === props.line.a)!,
-  props.data.points.find((p) => p.id === props.line.b)!,
-]);
 const lineData = computed(() => props.line);
-const describes = mergeDescribes(lineData, {}, ["stroke", "strokeWidth"]);
-const d = describes as any;
-d.strokeWidth.props = {
-  ...d.strokeWidth.props,
-  proportion: true,
-};
-d.strokeWidth.label = "粗细";
-d.stroke.label = "颜色";
-
-let isStartChange = false;
-let setLineVector: Vector2;
-describes.length = {
-  type: "inputNum",
-  label: "线段长度",
-  "layout-type": "row",
-  get value() {
-    return lineLen(points.value[0], points.value[1]);
-  },
-  set value(val) {
-    if (!isStartChange) {
-      emit("updateBefore", [props.line.a, props.line.b]);
-      setLineVector = lineVector(points.value);
-    }
-    isStartChange = true;
-    const aCount = props.data.lines.filter(
-      (line) => line.a === points.value[0].id || line.b === points.value[0].id
-    ).length;
-    const bCount = props.data.lines.filter(
-      (line) => line.a === points.value[1].id || line.b === points.value[1].id
-    ).length;
-
-    if (aCount === bCount || (aCount > 1 && bCount > 1)) {
-      // 两端伸展
-      const center = lineCenter(points.value);
-      const l1 = getVectorLine(setLineVector.clone().multiplyScalar(-1), center, val / 2);
-      const l2 = getVectorLine(setLineVector, center, val / 2);
-      emit("updatePoint", { ...points.value[0], ...l1[1] });
-      emit("updatePoint", { ...points.value[1], ...l2[1] });
-    } else {
-      // 单端伸展
-      const changeNdx = aCount > 1 ? 1 : 0;
-      const start = points.value[aCount > 1 ? 0 : 1];
-      const lineVec =
-        aCount > 1 ? setLineVector : setLineVector.clone().multiplyScalar(-1);
-      const line = getVectorLine(lineVec, start, val);
-      emit("updatePoint", { ...points.value[changeNdx], ...line[1] });
-    }
-  },
-  props: { proportion: true },
-};
+const describes = useLineDescribes(lineData);
 
 const delHandler = () => {
   emit("updateBefore", [props.line.a, props.line.b]);
   emit("delLine");
   emit("update");
 };
+
+const store = useStore();
+const history = useHistory();
+const { drawProps, enter: enterDrawLinePoint } = useDrawLinePoint(
+  computed(() => props.data),
+  computed(() => props.line),
+  (data) => {
+    emit("updateBefore", [props.line.a, props.line.b]);
+    emit("addPoint", data.point);
+    emit("addLine", data.prev);
+    emit("addLine", data.next);
+    emit("delLine");
+    history.preventTrack(() => {
+      data.oldIcons.forEach((icon) => store.delItem("lineIcon", icon.id));
+      store.addItems("lineIcon", data.newIcons);
+    });
+    emit("update");
+  }
+);
+
+const drawData = computed(() => drawProps.value?.data);
+const drawGetShapeAttrib = computed(
+  () => renderer.value && renderer.value.genGetShapeAttrib(drawData)
+);
 const menus = [
-  {
-    label: "删除",
-    handler: delHandler,
-  },
+  { label: "加点", handler: enterDrawLinePoint },
+  { label: "删除", handler: delHandler },
 ];
 
 const status = useMouseShapeStatus(shape);
-const [mstyle] = useAnimationMouseStyle({
+const [style] = useAnimationMouseStyle({
   shape,
   getMouseStyle,
   data: lineData as any,
 });
+
 const isDrawIng = computed(
   () =>
-    props.addMode && props.data.lines.indexOf(props.line) === props.data.lines.length - 1
-);
-
-const style = computed(() =>
-  isDrawIng.value ? { ...mstyle.value, stroke: themeColor } : mstyle.value
+    (props.addMode &&
+      props.data.lines.indexOf(props.line) === props.data.lines.length - 1) ||
+    props.drawMode
 );
 
 const addPoint = (pos: Pos) => {
@@ -204,31 +190,20 @@ const addPoint = (pos: Pos) => {
   emit("update");
 };
 
+const lDataSnap = useLineDataSnapInfos();
 const config = useConfig();
-const delPoint = (point: LineData["points"][number]) => {
-  emit("updateBefore", []);
-  emit("delPoint", point);
-  emit("update");
-};
-
-const infos = useCustomSnapInfos();
-let snapInfos: ComponentSnapInfo[];
 const dragstartHandler = (eIds: string[]) => {
   emit("updateBefore", eIds);
-
-  snapInfos = getSnapInfos({
-    ...props.data,
-    lines: props.data.lines.filter(
-      (item) => !(eIds.includes(item.a) || eIds.includes(item.b))
-    ),
-    points: props.data.points.filter((item) => !eIds.includes(item.id)),
-  });
-  snapInfos.forEach((item) => {
-    infos.add(item);
-  });
+  lDataSnap.update(eIds);
 };
 const dragendHandler = () => {
   emit("update");
-  snapInfos.forEach((item) => infos.remove(item));
+  lDataSnap.clear();
 };
+
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+});
 </script>

+ 109 - 0
src/core/components/line/single-point.vue

@@ -0,0 +1,109 @@
+<template>
+  <template v-for="(point, ndx) in points" :key="point.id">
+    <!-- <v-text :text="point.id" :x="point.x" :y="point.y" :fontSize="18" /> -->
+    <EditPoint
+      v-if="showEditPoint"
+      :ref="(r: any) => shapes[ndx] = r?.shape"
+      :size="line.strokeWidth"
+      :points="points"
+      :opacity="1"
+      :drawIng="isDrawIng ? (drawMode ? point === drawMode : ndx === 1) : false"
+      :ndx="ndx"
+      :closed="false"
+      :id="line.id"
+      :disable="addMode || !!drawMode"
+      :color="isDrawIng ? themeColor : style.stroke"
+      @dragstart="dragstartHandler([point.id])"
+      @update:position="(p) => emit('updatePoint', { ...point, ...p })"
+      @dragend="dragendHandler"
+      @delete="delPoint(point)"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import { getMouseStyle, LineData } from "./index.ts";
+import { useAnimationMouseStyle, useShapeIsHover } from "@/core/hook/use-mouse-status.ts";
+import { themeColor } from "@/constant";
+import EditPoint from "../share/edit-point.vue";
+import { DC } from "@/deconstruction.js";
+import { Line } from "konva/lib/shapes/Line";
+import { useLineDataSnapInfos } from "./attach-server.ts";
+import { Circle } from "konva/lib/shapes/Circle";
+
+const props = defineProps<{
+  lineShape?: DC<Line>;
+  line: LineData["lines"][number];
+  addMode?: boolean;
+  data: LineData;
+  dragPointIds?: string[];
+  drawMode?: LineData["points"][number];
+}>();
+const shapes = ref<DC<Circle>[]>([]);
+
+const emit = defineEmits<{
+  (e: "updatePoint", value: LineData["points"][number]): void;
+  (e: "delPoint", value: LineData["points"][number]): void;
+  (e: "updateBefore", value: string[]): void;
+  (e: "update"): void;
+}>();
+
+const points = computed(() => [
+  props.data.points.find((p) => p.id === props.line.a)!,
+  props.data.points.find((p) => p.id === props.line.b)!,
+]);
+const lineData = computed(() => props.line);
+const lineShape = computed(() => props.lineShape);
+
+const [style] = useAnimationMouseStyle({
+  shape: lineShape,
+  getMouseStyle,
+  data: lineData as any,
+});
+const isLineHover = useShapeIsHover(lineShape)[0];
+const isPointHovera = useShapeIsHover(computed(() => shapes.value[0]))[0];
+const isPointHoverb = useShapeIsHover(computed(() => shapes.value[1]))[0];
+const isDrawIng = computed(
+  () =>
+    (props.addMode &&
+      props.data.lines.indexOf(props.line) === props.data.lines.length - 1) ||
+    !!props.drawMode
+);
+const showEditPoint = computed(
+  () =>
+    (props.dragPointIds &&
+      props.dragPointIds.includes(props.line.a) &&
+      props.dragPointIds.includes(props.line.b)) ||
+    isLineHover.value ||
+    isPointHovera.value ||
+    isPointHoverb.value ||
+    dragIng.value ||
+    isDrawIng.value
+);
+
+const delPoint = (point: LineData["points"][number]) => {
+  emit("updateBefore", []);
+  emit("delPoint", point);
+  emit("update");
+};
+
+const lDataSnap = useLineDataSnapInfos();
+const dragIng = ref(false);
+const dragstartHandler = (eIds: string[]) => {
+  emit("updateBefore", eIds);
+  lDataSnap.update(eIds);
+  dragIng.value = true;
+};
+const dragendHandler = () => {
+  dragIng.value = false;
+  emit("update");
+  lDataSnap.clear();
+};
+
+defineExpose({
+  get shape() {
+    return shapes.value;
+  },
+});
+</script>

+ 54 - 37
src/core/components/line/temp-line.vue

@@ -2,40 +2,64 @@
   <v-group :id="data.id" ref="shape">
     <v-group>
       <singleLine
-        v-for="item in data.lines"
+        v-for="(item, ndx) in data.lines"
+        :ref="(r: any) => lineShapes[ndx] = r?.shape"
         :key="item.id"
         :line="item"
         :data="data"
         :add-mode="addMode"
-        :can-edit="!initData"
+        :can-edit="!initData && !operMode.mulSelection"
         :dragPointIds="dragPointIds"
         @add-point="(p) => addPointHandler(p, item)"
-        @del-point="delPointHandler"
+        @add-line="(l) => addLineHandler(l)"
         @update-point="updatePointHandler"
         @update-before="updateBeforeHandler"
         @update="updateHandler"
         @del-line="delLineHandler(item)"
+        @dragLineStart="dragstartLineHandler"
+        @dragLine="dragLineHandler"
+        @dragLineEnd="dragendLineHandler"
+        :get-shape-attrib="getShapeAttrib"
+      />
+    </v-group>
+    <v-group>
+      <singlePoint
+        v-for="(item, ndx) in data.lines"
+        :line-shape="lineShapes[ndx]"
+        :dragPointIds="dragPointIds"
+        :key="item.id"
+        :line="item"
+        :data="data"
+        :add-mode="addMode"
+        @del-point="delPointHandler"
+        @update-point="updatePointHandler"
+        @update-before="updateBeforeHandler"
+        @update="updateHandler"
       />
     </v-group>
-    <v-rect
-      v-if="operMode.mulSelection"
-      :config="{ fill: 'red', ...lineBox, opacity: 0.1 }"
-    />
   </v-group>
 </template>
 
 <script lang="ts" setup>
-import { onlyId } from "@/utils/shared.ts";
-import { delPoint, LineData } from "./index.ts";
+import { delPoint, LineData, renderer } from "./index.ts";
 import singleLine from "./single-line.vue";
-import { getInitCtx, NLineDataCtx, normalLineData, useInitData } from "./use-draw.ts";
+import singlePoint from "./single-point.vue";
+import { useInitData } from "./use-draw.ts";
+import {
+  genMoveLineHandler,
+  getInitCtx,
+  NLineDataCtx,
+  normalLineData,
+} from "./attach-server.ts";
 import { computed, ref } from "vue";
 import { useZIndex } from "@/core/hook/use-layer.ts";
 import { DC } from "@/deconstruction.js";
 import { Group } from "konva/lib/Group";
 import { useOperMode } from "@/core/hook/use-status.ts";
-import { useViewerInvertTransform } from "@/core/hook/use-viewer.ts";
 import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
+import { Pos } from "@/utils/math.ts";
+import { useSnapConfig, useSnapResultInfo } from "@/core/hook/use-snap.ts";
+import { Line } from "konva/lib/shapes/Line";
 
 const props = defineProps<{
   data: LineData;
@@ -49,38 +73,19 @@ const emit = defineEmits<{
   (e: "updateShape"): void;
 }>();
 
+const lineShapes = ref<DC<Line>[]>([]);
 const data = computed(() => {
   if (!initData.value || props.addMode) return props.data;
   return initData.value;
 });
-
-const invMat = useViewerInvertTransform();
-const lineBox = computed(() => {
-  const rect = shape.value?.getNode().getClientRect();
-  if (!rect) {
-    return {};
-  }
-  const start = invMat.value.point({ x: rect.x, y: rect.y });
-  const end = invMat.value.point({ x: rect.x + rect.width, y: rect.y + rect.height });
-  let width = end.x - start.x;
-  let height = end.y - start.y;
-
-  if (width < 0) {
-    start.x = end.x;
-    width = -width;
-  }
-  if (height < 0) {
-    start.y = end.y;
-    height = -height;
-  }
-  return { ...start, width, height };
-});
+const getShapeAttrib = computed(
+  () => renderer.value && renderer.value.genGetShapeAttrib(data)
+);
 
 const dragPointIds = ref<string[]>();
 let track = false;
 let ctx: NLineDataCtx;
 const updateBeforeHandler = (ids: string[]) => {
-  console.log("before");
   dragPointIds.value = ids;
   track = true;
   ctx = getInitCtx();
@@ -93,9 +98,6 @@ const delPointHandler = (p: LineData["points"][0]) => {
 const addPointHandler = (p: LineData["points"][0], l: LineData["lines"][0]) => {
   props.data.points.push(p);
   ctx.add.points[p.id] = p;
-  delLineHandler(l);
-  addLineHandler({ ...l, a: p.id, id: onlyId() });
-  addLineHandler({ ...l, b: p.id, id: onlyId() });
 };
 
 const updatePointHandler = (p: LineData["points"][0]) => {
@@ -122,6 +124,21 @@ const updateHandler = () => {
   dragPointIds.value = undefined;
 };
 
+const resultInfo = useSnapResultInfo();
+const snapConfig = useSnapConfig();
+let handler: ReturnType<typeof genMoveLineHandler>;
+const dragstartLineHandler = (line: LineData["lines"][0]) => {
+  updateBeforeHandler([line.a, line.b]);
+  handler = genMoveLineHandler(props.data, line.id, snapConfig, resultInfo, ctx);
+};
+const dragLineHandler = (_: any, ps: Pos[]) => {
+  handler.move(ps);
+};
+const dragendLineHandler = () => {
+  // handler.end();
+  updateHandler();
+};
+
 const shape = ref<DC<Group>>();
 useZIndex(shape, data);
 useMouseShapeStatus(shape);

+ 48 - 129
src/core/components/line/use-draw.ts

@@ -12,18 +12,20 @@ import { useViewerTransform } from "@/core/hook/use-viewer";
 import { useOperMode } from "@/core/hook/use-status";
 import { installGlobalVar, useCursor } from "@/core/hook/use-global-vars";
 import { useInteractiveDots } from "@/core/hook/use-interactive";
-import { computed, reactive, ref, watch } from "vue";
+import { computed, nextTick, reactive, ref, watch } from "vue";
 import { copy, mergeFuns } from "@/utils/shared";
-import { eqPoint, Pos } from "@/utils/math";
+import { lineVector, Pos } from "@/utils/math";
 import { getSnapInfos, type LineData } from "./";
 import { useCustomSnapInfos } from "@/core/hook/use-snap";
+import { getInitCtx, normalLineData } from "./attach-server";
+import { getKeywordInput } from "@/utils/dom";
+import { useProportion } from "@/core/hook/use-proportion";
 
 type PayData = Pos;
 
 export let initData: LineData | undefined;
 export const useInitData = installGlobalVar(() => ref<LineData>());
 
-// 单例钢笔添加
 export const useDraw = () => {
   const type = "line";
   const { quitDrawShape } = useInteractiveDrawShapeAPI();
@@ -36,6 +38,7 @@ export const useDraw = () => {
   const operMode = useOperMode();
   const hInitData = useInitData();
   const customSnapInfos = useCustomSnapInfos();
+  const { invTransform } = useProportion()
 
   // 可能历史空间会撤销 重做更改到正在绘制的组件
   const currentCursor = ref("./icons/m_add.png");
@@ -65,19 +68,18 @@ export const useDraw = () => {
       if (!drawItems[0]) return;
 
       if (isTempDraw) {
-        console.log(drawItems[0].points)
+        console.log(drawItems[0].points);
         drawItems[0].lines.pop();
         drawItems[0].points.pop();
         drawItems[0].polygon.pop();
       }
-      const lastP = drawItems[0].points[drawItems[0].points.length - 1]
+      const lastP = drawItems[0].points[drawItems[0].points.length - 1];
       if (lastP) {
-        console.log(lastP)
-        const ctx = getInitCtx()
-        ctx.add.points[lastP.id] = lastP
-        normalLineData(drawItems[0], ctx)
+        console.log(lastP);
+        const ctx = getInitCtx();
+        ctx.add.points[lastP.id] = lastP;
+        normalLineData(drawItems[0], ctx);
       }
-      
 
       snapInfos?.forEach(customSnapInfos.remove);
       drawSnapInfos?.forEach(customSnapInfos.remove);
@@ -235,6 +237,7 @@ export const useDraw = () => {
     }
 
     const update = () => {
+      needInputClear && keyInput.clear();
       const msg = setMessage(cur);
       drawItems[0] = obj.interactiveFixData({
         data: drawItems[0]!,
@@ -242,11 +245,44 @@ export const useDraw = () => {
         viewTransform: viewTransform.value,
         history,
         store,
-      });
+      })!;
       isTempDraw = true;
     };
 
+    let isInputChange = false;
+    let needInputClear = true;
+    const keyInput = getKeywordInput(
+      (inputData, prev) => {
+        const len = Number(inputData);
+        
+        if (len && drawItems[0].lines.length) {
+          const line = drawItems[0].lines[drawItems[0].lines.length - 1];
+          const points = [
+            drawItems[0].points.find((p) => p.id === line.a)!,
+            drawItems[0].points.find((p) => p.id === line.b)!,
+          ];
+          const vector = lineVector(points);
+          const position = vector.multiplyScalar(invTransform(len)).add(points[0]);
+          cur.x = position.x;
+          cur.y = position.y;
+          isInputChange = true;
+          needInputClear = false;
+          nextTick(() => {
+            needInputClear = true;
+          });
+        }
+        return len ? len.toString() : prev;
+      },
+      () => {
+        if (isInputChange) {
+          ia.singleDone.value = true;
+          keyInput.clear();
+        }
+      }
+    );
+
     stopWatch = mergeFuns(
+      keyInput.stop,
       watch(() => operMode.value.freeDraw, update),
       watch(cur, update, { immediate: true, deep: true }),
       watch(
@@ -256,6 +292,7 @@ export const useDraw = () => {
           if (messages.value.length === 0) {
             quitDrawShape();
           } else {
+            keyInput.clear();
             update();
           }
         },
@@ -288,121 +325,3 @@ export const useDraw = () => {
 
   return drawItems;
 };
-
-
-
-
-export type NLineDataCtx = {
-  del: {
-    points: Record<string, LineData["points"][0]>;
-    lines: Record<string, LineData["lines"][0]>;
-  };
-  add: {
-    points: Record<string, LineData["points"][0]>;
-    lines: Record<string, LineData["lines"][0]>;
-  };
-  update: {
-    points: Record<string, LineData["points"][0]>;
-    lines: Record<string, LineData["lines"][0]>;
-  };
-};
-export const getInitCtx = (): NLineDataCtx => ({
-  del: {
-    points: {},
-    lines: {},
-  },
-  add: {
-    points: {},
-    lines: {},
-  },
-  update: {
-    points: {},
-    lines: {},
-  },
-});
-
-export const repPointRef = (data: LineData, delId: string, repId: string) => {
-  for (let i = 0; i < data.lines.length; i++) {
-    const line = data.lines[i];
-    if (line.a === delId) {
-      data.lines[i] = { ...line, a: repId };
-    }
-    if (line.b === delId) {
-      data.lines[i] = { ...line, b: repId };
-    }
-  }
-  return data
-};
-
-export const deduplicateLines = (data: LineData) => {
-  const seen = new Map<string, LineData['lines'][0]>();
-  let isChange = false
-  for (const line of data.lines) {
-    if (line.a === line.b) continue;
-    // 生成标准化键:确保 (a,b) 和 (b,a) 被视为相同,并且 a === b 时也去重
-    const key1 = `${line.a},${line.b}`;
-    const key2 = `${line.b},${line.a}`;
-
-    // 检查是否已存在相同键
-    const existingKey = seen.has(key1) ? key1 : seen.has(key2) ? key2 : null;
-
-    if (existingKey) {
-      // 如果存在重复键,覆盖旧值(保留尾部元素)
-      seen.delete(existingKey);
-      seen.set(key1, line); // 统一存储为 key1 格式
-      isChange = true
-    } else {
-      // 新记录,直接存储
-      seen.set(key1, line);
-    }
-  }
-  if (isChange) {
-    data.lines = Array.from(seen.values())
-  }
-
-  return data
-};
-
-export const normalLineData = (data: LineData, ctx: NLineDataCtx) => {
-  const changePoints = [
-    ...Object.values(ctx.add.points),
-    ...Object.values(ctx.update.points),
-  ];
-
-
-  // 合并相同点
-  for (const p2 of changePoints) {
-    const ndx = data.points.findIndex((item) => item.id === p2.id);
-    if (!~ndx) continue;
-
-    for (let i = 0; i < data.points.length; i++) {
-      const p1 = data.points[i];
-      if (p1.id !== p2.id && eqPoint(p1, p2)) {
-        repPointRef(data, p1.id, p2.id);
-        data.points.splice(i, 1);
-        i--;
-      }
-    }
-  }
-
-  // 删除线a b 点一样的线段
-  for (let i = 0; i < data.lines.length; i++) {
-    const line = data.lines[i]
-    if (line.a === line.b) {
-      data.lines.splice(i--, 1)
-    }
-  }
-
-
-  // 删除游离点
-  const pointIds = Object.values(ctx.del.lines).flatMap(item => [item.a, item.b])
-  pointIds.push(...Object.keys(ctx.add.points))
-  const linePointIds = data.lines.flatMap(item => [item.a, item.b])
-  for (let id of pointIds) {
-    if (!linePointIds.includes(id)) {
-      const ndx = data.points.findIndex(p => p.id === id)
-      ~ndx && data.points.splice(ndx, 1)
-    }
-  }
-  return deduplicateLines(data);
-};

+ 1 - 0
src/core/components/polygon/temp-polygon.vue

@@ -4,6 +4,7 @@
       name="repShape"
       :config="{
         ...data,
+        id: void 0,
         closed: true,
         zIndex: undefined,
         points: flatPositions(data.points),

+ 1 - 0
src/core/components/sequent-line/temp-line.vue

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         zIndex: undefined,
+        id: void 0,
         points: flatPositions(data.points),
         opacity: addMode ? 0.3 : data.opacity,
         hitFunc,

+ 1 - 0
src/core/components/serial/temp-serial.vue

@@ -5,6 +5,7 @@
       :config="{
         ...data,
         ...matConfig,
+        id: void 0,
         zIndex: undefined,
         opacity: addMode ? 0.3 : data.opacity,
       }"

+ 4 - 2
src/core/components/share/edit-line.vue

@@ -6,6 +6,7 @@
       strokeWidth: data.strokeWidth,
       opacity: opacity || 0,
       stroke: data.stroke,
+      zIndex: zIndex,
       points: flatPositions(points),
       hitStrokeWidth: data.strokeWidth,
     }"
@@ -43,7 +44,7 @@ import { useViewer } from "@/core/hook/use-viewer";
 import { useMode } from "@/core/hook/use-status";
 import { Mode } from "@/constant/mode";
 
-type LData = Required<Pick<SLineData, "strokeWidth" | "stroke">>;
+type LData = Pick<SLineData, "strokeWidth" | "stroke">;
 const props = defineProps<{
   data: LData;
   points: Pos[];
@@ -52,6 +53,7 @@ const props = defineProps<{
   closed?: boolean;
   disablePoint?: boolean;
   opacity?: number;
+  zIndex?: number;
 }>();
 const emit = defineEmits<{
   (e: "update:line", data: Pos[]): void;
@@ -162,7 +164,7 @@ useShapeClick(point, () => {
 const center = computed(() => lineCenter(points.value));
 const pointStyle = computed(() => {
   const color = getMouseColors(props.data.stroke || themeColor);
-  const size = props.data.strokeWidth + 6 || 5;
+  const size = (props.data.strokeWidth || 1) + 6 || 5;
   return {
     radius: size / 2,
     fill: "#fff",

+ 1 - 0
src/core/components/table/temp-table.vue

@@ -15,6 +15,7 @@
       :config="{
         points: getBorderPoints(),
         ...data,
+        id: void 0,
         closed: true,
         zIndex: undefined,
       }"

+ 1 - 0
src/core/components/triangle/temp-triangle.vue

@@ -6,6 +6,7 @@
         ...data,
         zIndex: undefined,
         closed: true,
+        id: void 0,
         points: flatPositions(data.points),
         opacity: addMode ? 0.3 : data.opacity,
       }"

+ 18 - 10
src/core/helper/compass.vue

@@ -28,12 +28,14 @@ import {
 } from "../hook/use-viewer.ts";
 import { useStore } from "../store/index.ts";
 import { getSvgContent, parseSvgContent } from "@/utils/resource.ts";
+import { useComponentDescribes } from "../hook/use-component.ts";
 
 const config = useConfig();
 const store = useStore();
 
 const maxWidth = 60;
 const data = ref({
+  id: "__compass",
   coverOpcatiy: 0,
   strokeScaleEnabled: false,
   width: maxWidth,
@@ -81,17 +83,23 @@ const [style] = useAnimationMouseStyle({
 } as any);
 
 let currentRotation = store.config.compass.rotation;
-const describes = mergeDescribes(data, {}, ["rotate"]);
-describes.rotate = {
-  ...describes.rotate,
-  sort: 3,
-  get value() {
-    return store.config.compass.rotation;
-  },
-  set value(val) {
-    store.config.compass.rotation = val;
+const describes = useComponentDescribes(data, ["rotate"], {});
+watch(
+  describes,
+  (describes) => {
+    describes.rotate = {
+      ...describes.rotate,
+      sort: 3,
+      get value() {
+        return store.config.compass.rotation;
+      },
+      set value(val) {
+        store.config.compass.rotation = val;
+      },
+    };
   },
-};
+  { immediate: true }
+);
 const changeHandler = () => {
   if (currentRotation !== store.config.compass.rotation) {
     store.setConfig({});

+ 16 - 1
src/core/helper/debugger.vue

@@ -1,5 +1,20 @@
 <template>
-  <v-circle v-for="p in points" :config="{ ...p, radius: 8, fill: 'red' }" />
+  <v-group v-for="(p, ndx) in points" :x="p.x" :y="p.y">
+    <v-circle
+      :config="{
+        radius: testPoints[ndx].size || 8,
+        fill: testPoints[ndx].color || 'red',
+      }"
+    />
+    <v-text
+      v-if="testPoints[ndx].text"
+      :config="{
+        text: testPoints[ndx].text,
+        y: testPoints[ndx].size || 8,
+        fill: testPoints[ndx].color || 'red',
+      }"
+    />
+  </v-group>
 </template>
 
 <script lang="ts" setup>

+ 3 - 6
src/core/helper/split-line.vue

@@ -7,6 +7,7 @@
           ...config.labelLineConfig,
           strokeWidth: 0,
           ...textConfig,
+          fill: config.labelLineConfig.stroke,
           fontSize: config.labelLineConfig.fontSize,
           align: 'center',
         }"
@@ -96,13 +97,9 @@ const center = computed(() => {
 });
 
 const compsInfo = useComponentsAttach(
-  (type, item) => {
+  (type, item): Pos[] => {
     if (config.labelLineConfig.showShapeTypes.includes(type)) {
-      if (type === "icon" && (item as any).fixScreen) {
-        return [];
-      } else {
-        return components[type].getSnapPoints(item as any);
-      }
+      return components[type].getSnapPoints(item as any);
     } else {
       return [];
     }

+ 119 - 100
src/core/hook/use-animation.ts

@@ -1,149 +1,168 @@
-import { onUnmounted, ref, Ref, watch } from "vue"
-import { Tween, Easing } from '@tweenjs/tween.js'
-import { inRevise, startAnimation } from "@/utils/shared.ts"
-import { Color, RGB } from "three"
-import { DC } from "@/deconstruction"
-import { Shape } from "konva/lib/Shape"
+import { onUnmounted, ref, Ref, watch } from "vue";
+import { Tween, Easing } from "@tweenjs/tween.js";
+import { inRevise, startAnimation } from "@/utils/shared.ts";
+import { Color, RGB } from "three";
+import { DC } from "@/deconstruction";
+import { Shape } from "konva/lib/Shape";
 
-export const animation = <T extends object>(origin: T, target: T, update?: (data: T) => void) => {
-  let isStop = false
-  const stop = () => {
-    tw.stop()
-    isStop = true
-  }
+export const animation = <T extends object>(
+  origin: T,
+  target: T,
+  update?: (data: T) => void,
+  time = 300
+) => {
+  let stop: (() => void) & {promise: Promise<void>};
+  const promise = new Promise<void>((resolve, reject) => {
+    let isStop = false;
+    let isComp = false
+    stop = (() => {
+      tw.stop();
+      isStop = true;
+      isComp || reject('动画中断')
+    }) as any;
 
-  const tw = new Tween(origin)
-  .to(target, 300)
-  .easing(easing)
-  .start()
-  .onComplete(() => {
-    stop()
-  })
-  if (update) {
-    tw.onUpdate(() => {
-      update && update(origin)
-    })
-  }
+    const tw = new Tween(origin)
+      .to(target, time)
+      .easing(easing)
+      .start()
+      .onComplete(() => {
+        resolve()
+        isComp = true
+        stop();
+      });
+    if (update) {
+      tw.onUpdate(() => {
+        update && update(origin);
+      });
+    }
 
-  const start = () => {
-    requestAnimationFrame(() => {
-      tw.update()
-      isStop || start()
-    })
-  }
-  start()
-  return stop
-}
+    const start = () => {
+      requestAnimationFrame(() => {
+        tw.update();
+        isStop || start();
+      });
+    };
+    start();
+  });
+  stop!.promise = promise
+  return stop!;
+};
 
-const pickColors = <T extends object>(origin: T): Record<string, RGB>  => {
-  const originColors = {} as any
+const pickColors = <T extends object>(origin: T): Record<string, RGB> => {
+  const originColors = {} as any;
   for (const [key, val] of Object.entries(origin)) {
-    if (typeof val === 'string') {
-      if (val.startsWith('#') || val.startsWith('rgb')) {
-        originColors[key] = new Color(val).getRGB({} as any)
-      } 
+    if (typeof val === "string") {
+      if (val.startsWith("#") || val.startsWith("rgb")) {
+        originColors[key] = new Color(val).getRGB({} as any);
+      }
     }
   }
-  return originColors
-}
-const resumeColors = <T extends object>(origin: T, colors: Record<string, RGB>) => {
+  return originColors;
+};
+const resumeColors = <T extends object>(
+  origin: T,
+  colors: Record<string, RGB>
+) => {
   for (const [key, val] of Object.entries(colors)) {
-    (origin as any)[key] = `#${new Color(val.r, val.g, val.b).getHexString()}`
+    (origin as any)[key] = `#${new Color(val.r, val.g, val.b).getHexString()}`;
   }
-}
+};
 
-const easing = Easing.Quadratic.InOut
-const animationProperty = <T extends object>(origin: T, target: T, update?: (data: T) => void) => {
-  let isStop = false
+const easing = Easing.Quadratic.InOut;
+const animationProperty = <T extends object>(
+  origin: T,
+  target: T,
+  update?: (data: T) => void
+) => {
+  let isStop = false;
   const stop = () => {
-    numTw.stop()
-    colorTw.stop()
-    isStop = true
-  }
+    numTw.stop();
+    colorTw.stop();
+    isStop = true;
+  };
   const oColors = pickColors(origin);
-  const tColors = pickColors(target)
-  const tOrigin = {...origin}
-  const tTarget = {...target}
+  const tColors = pickColors(target);
+  const tOrigin = { ...origin };
+  const tTarget = { ...target };
 
   for (const key in oColors) {
     if (!(key in tColors)) {
-      ;(origin as any)[key] = null
-      delete oColors[key]
+      (origin as any)[key] = null;
+      delete oColors[key];
     }
   }
 
   for (const key in tOrigin) {
-    if (typeof tOrigin[key] === 'string') {
-      delete tOrigin[key]
+    if (typeof tOrigin[key] === "string") {
+      delete tOrigin[key];
     }
   }
   for (const key in tTarget) {
-    if (typeof tTarget[key] === 'string') {
-      delete tTarget[key]
+    if (typeof tTarget[key] === "string") {
+      delete tTarget[key];
     }
   }
 
-  let count = 0
+  let count = 0;
   const tw = (origin: any, target: any) => {
-    ++count
+    ++count;
     return new Tween(origin)
       .to(target, 300)
       .easing(easing)
       .start()
       .onComplete(() => {
-        if (--count === 0) stop()
-      })
-  }
+        if (--count === 0) stop();
+      });
+  };
+
+  const numTw = tw(tOrigin, tTarget).onUpdate(() => {
+    Object.assign(origin, tOrigin);
+    update && update(origin);
+  });
 
-  const numTw = tw(tOrigin, tTarget)
-    .onUpdate(() => {
-      Object.assign(origin, tOrigin)
-      update && update(origin)
-    })
-  
-  const colorTw = tw(oColors, tColors)
-    .onUpdate(() => {
-      resumeColors(origin, oColors)
-      update && update(origin)
-    })
+  const colorTw = tw(oColors, tColors).onUpdate(() => {
+    resumeColors(origin, oColors);
+    update && update(origin);
+  });
 
   const start = () => {
     requestAnimationFrame(() => {
-      numTw.update()
-      colorTw.update()
-      isStop || start()
-    })
-  }
-  start()
-  return stop
-}
+      numTw.update();
+      colorTw.update();
+      isStop || start();
+    });
+  };
+  start();
+  return stop;
+};
 
 export const useAniamtion = <T extends object>(data: Ref<T>) => {
-  const atData = ref(JSON.parse(JSON.stringify(data.value))) as Ref<T>
-  let animationStop: () => void
-  let isPause = false
+  const atData = ref(JSON.parse(JSON.stringify(data.value))) as Ref<T>;
+  let animationStop: () => void;
+  let isPause = false;
   watch(data, (newData) => {
     if (!inRevise(newData, atData.value)) return;
-    animationStop! && animationStop()
+    animationStop! && animationStop();
     if (isPause) {
-      atData.value = newData
+      atData.value = newData;
     } else {
-      animationStop = animationProperty(atData.value, newData)
+      animationStop = animationProperty(atData.value, newData);
     }
-  })
+  });
 
   onUnmounted(() => {
-    animationStop && animationStop()
-  })
-  return [
-    atData, () => isPause = true, () => isPause = false
-  ] as const
-}
+    animationStop && animationStop();
+  });
+  return [atData, () => (isPause = true), () => (isPause = false)] as const;
+};
 
-type DA = DC<Shape> | undefined
+type DA = DC<Shape> | undefined;
 export const useDashAnimation = (shapes: Ref<DA | DA[]>) => {
   watch(
-    () => (Array.isArray(shapes.value) ? shapes.value : [shapes.value]).map((i) => i),
+    () =>
+      (Array.isArray(shapes.value) ? shapes.value : [shapes.value]).map(
+        (i) => i
+      ),
     (shapes, _, onCleanup) => {
       for (const shape of shapes) {
         if (!shape) continue;
@@ -157,4 +176,4 @@ export const useDashAnimation = (shapes: Ref<DA | DA[]>) => {
       }
     }
   );
-}
+};

+ 96 - 16
src/core/hook/use-component.ts

@@ -7,6 +7,7 @@ import {
   nextTick,
   onUnmounted,
   reactive,
+  ref,
   Ref,
   shallowReactive,
   shallowRef,
@@ -25,12 +26,17 @@ import { useGetShapeCopyTransform } from "./use-copy";
 import { asyncTimeout, copy, mergeFuns, onlyId } from "@/utils/shared";
 import { Shape } from "konva/lib/Shape";
 import { Transform } from "konva/lib/Util";
-import { mergeDescribes, PropertyKeys } from "../html-mount/propertys";
+import {
+  mergeDescribes,
+  PropertyDescribes,
+  PropertyKeys,
+} from "../html-mount/propertys";
 import { useStore } from "../store";
-import { globalWatch, useStage } from "./use-global-vars";
+import { globalWatch, useMountMenusFilter, useStage } from "./use-global-vars";
 import { useAlignmentShape } from "./use-alignment";
 import { useViewerTransform } from "./use-viewer";
 import { usePause } from "./use-pause";
+import { useGlobalDescribes } from "./use-group";
 
 type Emit<T> = EmitFn<{
   updateShape: (value: T) => void;
@@ -172,6 +178,7 @@ export type UseComponentStatusProps<
   getMouseStyle: any;
   defaultStyle: any;
   propertys: PropertyKeys;
+  selfData?: boolean;
   debug?: boolean;
   noJoinZindex?: boolean;
   noOperateMenus?: boolean;
@@ -185,6 +192,59 @@ export type UseComponentStatusProps<
   copyHandler?: (transform: Transform, data: T) => T;
 };
 
+export const useComponentDescribes = <T extends { id: string }>(
+  data: Ref<T>,
+  propertys: PropertyKeys,
+  defaultStyle: any
+) => {
+  const store = useStore();
+  const id = computed(() => data.value.id);
+  const type = computed(() => id.value && store.getType(id.value));
+  const initDescs = mergeDescribes(data, defaultStyle, propertys || []);
+  const { getFilter } = useMountMenusFilter();
+  const gdescs = useGlobalDescribes();
+
+  let descs = ref(initDescs);
+  watchEffect(() => {
+    const iDescs =
+      type.value && id.value
+        ? getFilter(type.value, id.value)(initDescs)
+        : initDescs;
+    descs.value = Object.fromEntries(
+      Object.entries(iDescs).sort(
+        ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
+      )
+    ) as PropertyDescribes;
+  });
+  watchEffect((onCleanup) => {
+    gdescs.set(data.value, descs.value);
+    onCleanup(() => gdescs.del(data.value.id));
+  });
+  watch(
+    descs,
+    (descs) => {
+      for (const key in descs) {
+        const initProps = descs[key].props;
+        watchEffect(() => {
+          if (!type.value) return;
+          const getPredefine = components[type.value].getPredefine;
+          const predefine =
+            getPredefine && (getPredefine as any)(key as keyof DrawItem);
+          if (!predefine) return;
+
+          if (initProps) {
+            descs[key].props = { ...initProps, ...predefine };
+          } else {
+            descs[key].props = predefine;
+          }
+        });
+      }
+    },
+    { immediate: true }
+  );
+  return descs;
+};
+
 export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
   args: UseComponentStatusProps<T, S>
 ) => {
@@ -197,7 +257,10 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
     },
     { flush: "sync" }
   );
-  const data = useAutomaticData(() => args.props.data);
+  const data = useAutomaticData(
+    () => args.props.data,
+    (data) => args.selfData ? data : copy(data)
+  );
   const [style, pause, resume] = useAnimationMouseStyle({
     data: data,
     shape,
@@ -254,7 +317,11 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
           args.alignment,
           args.copyHandler
         ),
-    describes: mergeDescribes(data, args.defaultStyle, args.propertys || []),
+    describes: useComponentDescribes(
+      data,
+      args.propertys || [],
+      args.defaultStyle
+    ),
   };
 };
 
@@ -263,29 +330,42 @@ export const useGetShapeBelong = () => {
   return (shape: EntityShape) => {
     let curId = shape.id();
     let id: string;
-    let item: DrawItem | undefined;
-    let type: ShapeType | undefined;
+    let item: DrawItem;
+    let type: ShapeType;
     do {
       id = shape.id();
-      item = store.getItemById(id);
-      type = store.getType(id);
+      item = store.getItemById(id)!;
+      type = store.getType(id)!;
       if (item && type) {
         break;
       }
     } while ((shape = shape.parent as any));
 
-    return item ? [item, curId === id, type, id] as const : null
-  }
-}
+    return item ? { item, isSelf: curId === id, type, id, curId } : null;
+  };
+};
 
-export const useGetComponentData = <D extends DrawItem>() => {
+export const useGetComponentData = () => {
   const store = useStore();
+  const getShapeBelong = useGetShapeBelong();
 
   return (shape: Ref<EntityShape | undefined> | EntityShape | undefined) =>
     computed(() => {
       shape = isRef(shape) ? shape.value : shape;
       if (!shape?.id()) return;
-      return store.getItemById(shape.id()) as D;
+      let item: any = store.getItemById(shape.id());
+      if (!item) {
+        const belong = getShapeBelong(shape);
+        if (belong && !belong.isSelf) {
+          const getter = components[belong.type].childrenDataGetter;
+          let parent: any;
+          if (getter && (parent = store.getItemById(belong.id))) {
+            item = getter(parent, belong.curId);
+          }
+        }
+      }
+
+      return item;
     });
 };
 
@@ -360,10 +440,10 @@ export const useOnComponentBoundChange = () => {
       const repShape = (shape.repShape as T) || shape;
       const syncBd = () => update(repShape);
       repShape.on("transform", syncBd);
-      shape.on("bound-change", () => syncBd);
+      shape.on("bound-change", syncBd);
       return () => {
         repShape.off("transform", syncBd);
-        shape.off("bound-change", () => syncBd);
+        shape.off("bound-change", syncBd);
       };
     };
     const cleanups: (() => void)[] = [];
@@ -379,7 +459,7 @@ export const useOnComponentBoundChange = () => {
         $shapes,
         ($shapes, _, onCleanup) => {
           const cleanups = $shapes.flatMap(($shape) => {
-            const item = getComponentData($shape);
+            let item = getComponentData($shape);
             return [
               watch(item, () => nextTick(() => update($shape, "data")), {
                 deep: true,

+ 1 - 1
src/core/hook/use-debugger.ts

@@ -4,7 +4,7 @@ import { Pos } from "@/utils/math";
 import { DrawItem } from "../components";
 
 export const useTestPoints = installGlobalVar(() => {
-  return ref<Pos[]>([]);
+  return ref<(Pos & {color?: string, size?: number, text?: string})[]>([]);
 });
 
 

+ 34 - 18
src/core/hook/use-draw.ts

@@ -1,5 +1,5 @@
 import { computed, h, nextTick, reactive, ref, watch, watchEffect } from "vue";
-import { installGlobalVar, useCursor, useStage } from "./use-global-vars";
+import { globalWatch, installGlobalVar, useCursor, useRunHook, useStage } from "./use-global-vars";
 import { useCan, useMode, useOperMode } from "./use-status";
 import {
   Area,
@@ -16,6 +16,7 @@ import {
   components,
   ComponentSnapInfo,
   ComponentValue,
+  DrawData,
   DrawItem,
   ShapeType,
   SnapPoint,
@@ -85,9 +86,10 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
       data?: PayData<T>,
-      pixel = false
+      pixel = false,
+      force = false
     ) => {
-      if (!can.drawMode) {
+      if (!force && !can.drawMode) {
         throw "当前状态不允许添加";
       }
       enter();
@@ -111,13 +113,14 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
     enterDrawShape: async <T extends ShapeType>(
       shapeType: T,
       preset: InteractivePreset<T>["preset"] = {},
-      single = false
+      single = false,
+      force = false
     ) => {
       if (isEnter) {
         leave();
         await new Promise((resolve) => setTimeout(resolve, 16));
       }
-      if (!can.drawMode || mode.include(Mode.draw)) {
+      if (!force && (!can.drawMode || mode.include(Mode.draw))) {
         throw "当前状态不允许添加";
       }
       if (!preset.zIndex) {
@@ -139,8 +142,11 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
     },
     drawing: computed(() => mode.include(Mode.draw)),
     drawType: computed(() => {
-      return interactiveProps.value?.type && components[interactiveProps.value?.type].addMode
-    })
+      return (
+        interactiveProps.value?.type &&
+        components[interactiveProps.value?.type].addMode
+      );
+    }),
   };
 });
 
@@ -171,7 +177,10 @@ export const useDrawRunning = (shapeType?: ShapeType) => {
   return isRunning;
 };
 
-export const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
+export const usePointBeforeHandler = (
+  enableTransform = false,
+  enableSnap = false
+) => {
   const operMode = useOperMode();
   const conversionPosition = useConversionPosition(enableTransform);
   const snap = enableSnap && useSnap();
@@ -242,7 +251,6 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
   const clear = () => {
     beforeHandler.clear();
     beforeHandler.clearRef();
-    
   };
 
   const ia = useIA({
@@ -290,10 +298,12 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
 
     const storeAddItem = (cItem: any) => {
       const items = store.getTypeItems(type);
-      if (items.some((item) => item.id === cItem.id)) {
-        store.setItem(type, { id: cItem.id, value: cItem });
-      } else {
-        store.addItem(type, cItem);
+      if (!obj.checkItemData || obj.checkItemData(cItem)) {
+        if (items.some((item) => item.id === cItem.id)) {
+          store.setItem(type, { id: cItem.id, value: cItem });
+        } else {
+          store.addItem(type, cItem);
+        }
       }
     };
 
@@ -576,10 +586,12 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
 
     const storeAddItem = (cItem: any) => {
       const items = store.getTypeItems(type);
-      if (items.some((item) => item.id === cItem.id)) {
-        store.setItem(type, { id: cItem.id, value: cItem });
-      } else {
-        store.addItem(type, cItem);
+      if (!obj.checkItemData || obj.checkItemData(cItem)) {
+        if (items.some((item) => item.id === cItem.id)) {
+          store.setItem(type, { id: cItem.id, value: cItem });
+        } else {
+          store.addItem(type, cItem);
+        }
       }
     };
 
@@ -648,7 +660,6 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   return items;
 };
 
-
 export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
   const obj = components[type];
   if (obj.addMode === "dots") {
@@ -659,3 +670,8 @@ export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
     return useInteractiveDrawDots(type);
   }
 };
+
+export const useDrawIngData = installGlobalVar(() => {
+  const drawStore: DrawData = reactive({});
+  return drawStore;
+});

+ 14 - 0
src/core/hook/use-dxf.ts

@@ -33,6 +33,7 @@ import {
 import { nextTick } from "vue";
 import { SLineData } from "../components/sequent-line";
 import { IRect } from "konva/lib/types";
+import { LineIconData } from "../components/line-icon";
 
 export const useGetDXF = () => {
   const store = useStore();
@@ -375,6 +376,19 @@ export const useGetDXF = () => {
             }
           });
           break;
+        case 'lineIcon':
+          const lineIconItem = _item as LineIconData;
+          const linePathGroup = $stage
+            .findOne<Group>(`#${lineIconItem.id}`)!
+            .findOne<Group>(".rep-position")!;
+
+          await writeImage(linePathGroup, () => {
+            lineIconItem.strokeScaleEnabled = true
+            return () => {
+              lineIconItem.strokeScaleEnabled = false
+            }
+          });
+          break;
       }
     }
 

+ 0 - 3
src/core/hook/use-event.ts

@@ -4,8 +4,6 @@ import { globalWatch, installGlobalVar, useStage } from "./use-global-vars.ts";
 import { nextTick, reactive, ref, watch, watchEffect } from "vue";
 import { KonvaEventObject } from "konva/lib/Node";
 import { debounce } from "@/utils/shared.ts";
-import { Shape } from "konva/lib/Shape";
-import { shapeTreeContain } from "@/utils/shape.ts";
 import { EntityShape } from "@/deconstruction.js";
 
 export const useListener = <
@@ -203,7 +201,6 @@ export const useGlobalOnlyRightClickShape = installGlobalVar(() => {
       let clickShape: any
       let ndx = -1
       if (ev.button === 2) {
-        console.log(clickShape)
         const pos = stage.value?.getNode().pointerPos;
         if (!pos) return false;
         clickShape = stage.value?.getNode().getIntersection(pos);

+ 42 - 55
src/core/hook/use-expose.ts

@@ -32,11 +32,11 @@ import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
 import { useExcludeSelection, useSelectionRevise } from "./use-selection.ts";
 import { useFormalLayer, useGetFormalChildren } from "./use-layer.ts";
-import { components, DrawItem } from "../components/index.ts";
+import { components } from "../components/index.ts";
 import { useProportion } from "./use-proportion.ts";
-import { ShapeType } from "@/index.ts";
 import { useGetDXF } from "./use-dxf.ts";
 import { getIconStyle } from "../components/icon/index.ts";
+import { useGetShapeBelong } from "./use-component.ts";
 
 // 自动粘贴服务
 export const useAutoPaste = () => {
@@ -48,10 +48,16 @@ export const useAutoPaste = () => {
       async handler(pos, val) {
         if (isSvgString(val)) {
           const url = await resourceHandler(val, "svg");
-          const style = await getIconStyle(url, 100, 100)
-          drawAPI.addShape("icon", { ...style, fill: undefined, stroke: undefined }, pos, true);
+          const style = await getIconStyle(url, 100, 100);
+          drawAPI.addShape(
+            "icon",
+            { ...style, fill: undefined, stroke: undefined },
+            pos,
+            true,
+            true
+          );
         } else {
-          drawAPI.addShape("text", { content: val }, pos, true);
+          drawAPI.addShape("text", { content: val }, pos, true, true);
         }
       },
       type: "string",
@@ -60,14 +66,21 @@ export const useAutoPaste = () => {
       async handler(pos, val, type) {
         const url = await resourceHandler(val, type);
         if (type.includes("svg")) {
-          const style = await getIconStyle(url, 100, 100)
-          drawAPI.addShape("icon", { ...style, fill: undefined, stroke: undefined }, pos, true);
+          const style = await getIconStyle(url, 100, 100);
+          drawAPI.addShape(
+            "icon",
+            { ...style, fill: undefined, stroke: undefined },
+            pos,
+            true,
+            true
+          );
         } else {
           const image = await getImage(url);
           drawAPI.addShape(
             "image",
             { url, width: image.width, height: image.height },
             pos,
+            true,
             true
           );
         }
@@ -106,68 +119,40 @@ export const useShortcutKey = () => {
   const status = useMouseShapesStatus();
   const getChildren = useGetFormalChildren();
   const operMode = useOperMode();
+  const getShapeBelong = useGetShapeBelong()
   const eSelection = useExcludeSelection()
   useListener(
     "keydown",
     (ev) => {
       if (ev.target !== document.body) return;
-      if (ev.key === "z" && ev.ctrlKey) {
+      const key = ev.key.toLowerCase()
+      if (key === "z" && ev.ctrlKey) {
         ev.preventDefault();
         history.hasUndo.value && history.undo();
-      } else if (ev.key === "y" && ev.ctrlKey) {
+      } else if (key === "y" && ev.ctrlKey) {
         ev.preventDefault();
         history.hasRedo.value && history.redo();
-      } else if (ev.key === "s" && ev.ctrlKey) {
+      } else if (key === "s" && ev.ctrlKey) {
         ev.preventDefault();
         // 保存
         // history.saveLocal();
-      } else if (ev.key === "Delete" || ev.key === "Backspace") {
+      } else if (key === "delete" || key === "backspace") {
         // 删除
         ev.preventDefault();
 
         const isSelect = status.selects.length;
         const shapes = isSelect ? status.selects : status.actives;
         const delItems = shapes
-          .map((shape) => {
-            // getShapeBelong(shape)
-
-            let curId = shape.id();
-            if (!curId) return;
-            let id: string;
-            let item: DrawItem | undefined;
-            let type: ShapeType | undefined;
-            do {
-              id = shape.id();
-              item = store.getItemById(id);
-              type = store.getType(id);
-              if (item && type) {
-                break;
-              }
-            } while ((shape = shape.parent as any));
-            if (!type) return;
-            if (id === curId) {
-              if (!item?.disableDelete && type) {
-                return [type, item, undefined] as const;
-              }
-            } else {
-              return [type, item, curId] as const;
-            }
-          })
+          .map(getShapeBelong)
           .filter((item) => !!item);
-        history.onceTrack(() => {
-          delItems.forEach(([type, item, childId]) => {
-            if (!childId) {
-              if (components[type as ShapeType].delItem) {
-                components[type as ShapeType].delItem!(store, item as any);
-              } else {
-                store.delItem(type as ShapeType, item!.id);
-              }
-            } else {
-              components[type as ShapeType].delItem!(
-                store,
-                item as any,
-                childId
-              );
+
+          history.onceTrack(() => {
+          delItems.forEach((belong) => {
+            const compDelItem = components[belong.type].delItem
+            if (compDelItem) {
+              compDelItem(store, belong.item as any, belong.isSelf ? undefined : belong.curId)
+            } else if (belong.isSelf) {
+              store.delItem(belong.type, belong.id);
             }
           });
         });
@@ -178,7 +163,7 @@ export const useShortcutKey = () => {
             status.actives = [];
           }
         }
-      } else if (operMode.value.mulSelection && ev.key === "a") {
+      } else if (operMode.value.mulSelection && key === "a") {
         ev.preventDefault();
         if (status.selects.length) {
           status.selects = [];
@@ -202,11 +187,13 @@ export const useAutoService = () => {
   const cursor = useCursor();
   const { set: setCursor } = cursor.push("initial");
 
-  watchEffect(() => {
-    let style: string | null = null;
+  watchEffect((onCleanup) => {
     if (operMode.value.freeView) {
-      style = "pointer";
-    } else if (mode.include(Mode.update)) {
+      onCleanup(cursor.push('pointer'))
+      return;
+    }
+    let style: string | null = null;
+    if (mode.include(Mode.update)) {
       style = "./icons/m_move.png";
     } else if (status.hovers.length) {
       style = "pointer";

+ 100 - 91
src/core/hook/use-global-vars.ts

@@ -21,17 +21,17 @@ import { Pos } from "@/utils/math.ts";
 import { listener } from "@/utils/event.ts";
 import { debounce, mergeFuns, onlyId } from "@/utils/shared.ts";
 import { StoreData } from "../store/store.ts";
-import { rendererMap, rendererName } from "@/constant/index.ts";
+import { DomMountId, rendererMap, rendererName } from "@/constant/index.ts";
 import { Shape, ShapeConfig } from "konva/lib/Shape";
 import { ElLoading } from "element-plus";
 import { PropertyDescribes } from "../html-mount/propertys/index.ts";
 import { ShapeType } from "@/index.ts";
 import { isEditableElement } from "@/utils/dom.ts";
 
-let getInstance = getCurrentInstance
-export const useRendererInstance = () => {
+let getInstance = getCurrentInstance;
+export const useRendererInstance = (name = rendererName) => {
   let instance = getInstance()!;
-  while (instance.type.name !== rendererName) {
+  while (instance.type.name !== name) {
     if (instance.parent) {
       instance = instance.parent;
     } else {
@@ -43,10 +43,11 @@ export const useRendererInstance = () => {
 
 export const installGlobalVar = <T>(
   create: () => { var: T; onDestroy: () => void } | T,
-  key = Symbol("globalVar")
+  key = Symbol("globalVar"),
+  instanceName = rendererName
 ) => {
   const useGlobalVar = (): T => {
-    const instance = useRendererInstance() as any;
+    const instance = useRendererInstance(instanceName) as any;
     const { unmounteds } = rendererMap.get(instance)!;
     if (!(key in instance)) {
       let val = create() as any;
@@ -68,16 +69,15 @@ export const installGlobalVar = <T>(
 };
 
 export const useRunHook = installGlobalVar(() => {
-  const instance = getCurrentInstance()
-  return <R, T extends () => R>(hook: T): R => {
-    const back = getInstance
-    getInstance = () => instance
-    const result = hook()
-    getInstance = back
-    return result
-  }
-})
-
+  const instance = getCurrentInstance();
+  return <T extends () => any>(hook: T): ReturnType<T> => {
+    const back = getInstance;
+    getInstance = () => instance;
+    const result = hook();
+    getInstance = back;
+    return result;
+  };
+});
 
 export type InstanceProps = {
   id?: string;
@@ -207,17 +207,23 @@ export const usePointerPos = installGlobalVar(() => {
     if (!$stage) return;
 
     const mount = $stage.container().parentElement!;
+    const domMount = document.querySelector(`#${DomMountId}`) as HTMLDivElement;
     pos.value = $stage.pointerPos;
+    const moveHandler = (ev: MouseEvent) => {
+      pos.value = $stage.pointerPos;
+      if (!replayIng) {
+        lastClient = {
+          clientX: ev.clientX,
+          clientY: ev.clientY,
+        };
+      }
+    };
+
     onCleanup(
-      listener(mount, "pointermove", (ev) => {
-        pos.value = $stage.pointerPos;
-        if (pos.value && !replayIng) {
-          lastClient = {
-            clientX: ev.clientX,
-            clientY: ev.clientY,
-          };
-        }
-      })
+      mergeFuns(
+        listener(mount, "pointermove", moveHandler),
+        listener(domMount, "pointermove", moveHandler)
+      )
     );
   });
 
@@ -237,7 +243,7 @@ export const usePointerIntersections = installGlobalVar(() => {
       return;
     }
     shapes.value = stage.value.getNode().getAllIntersections(pos.value) || [];
-  }, 300);
+  }, 100);
 
   const stopWatch = watch(pos, updateShapes);
 
@@ -271,29 +277,29 @@ export const usePointerIntersection = installGlobalVar(() => {
     },
   };
 });
-export const useDownKeys = installGlobalVar(() => {
+
+export const getDownKeys = () => {
   const keyKeys = reactive(new Set<string>());
   const mouseKeys = reactive(new Set<string>());
   const evHandler = (ev: KeyboardEvent | MouseEvent, keys: Set<string>) => {
-      ev.shiftKey ? keys.add("Shift") : keys.delete("Shift");
-      ev.altKey ? keys.add("Alt") : keys.delete("Alt");
-      ev.metaKey ? keys.add("Meta") : keys.delete("Meta");
-      ev.ctrlKey ? keys.add("Ctrl") : keys.delete("Ctrl");
-
-  }
+    ev.shiftKey ? keys.add("Shift") : keys.delete("Shift");
+    ev.altKey ? keys.add("Alt") : keys.delete("Alt");
+    ev.metaKey ? keys.add("Meta") : keys.delete("Meta");
+    ev.ctrlKey ? keys.add("Ctrl") : keys.delete("Ctrl");
+  };
   const cleanup = mergeFuns(
     listener(window, "keydown", (ev) => {
       if (!isEditableElement(ev.target as HTMLElement)) {
         keyKeys.add(ev.key);
-        evHandler(ev, keyKeys)
+        evHandler(ev, keyKeys);
       }
     }),
     listener(window, "keyup", (ev) => {
       keyKeys.delete(ev.key);
-      evHandler(ev, keyKeys)
+      evHandler(ev, keyKeys);
     }),
     listener(window, "mousemove", (ev) => {
-      evHandler(ev, mouseKeys)
+      evHandler(ev, keyKeys);
     })
   );
   const keys = reactive(new Set<string>());
@@ -310,7 +316,9 @@ export const useDownKeys = installGlobalVar(() => {
     var: keys,
     onDestroy: cleanup,
   };
-});
+}
+
+export const useDownKeys = installGlobalVar(getDownKeys);
 
 export const useLayers = () => {
   const stage = useStage();
@@ -361,69 +369,73 @@ export const useForciblyShowItemIds = installGlobalVar(() => {
   return set;
 }, Symbol("forciblyShowItemId"));
 
-export const useRendererDOM = installGlobalVar(() => ref<HTMLDivElement>())
+export const useRendererDOM = installGlobalVar(() => ref<HTMLDivElement>());
 
 export const useTempStatus = installGlobalVar(() => {
   const temp = ref(false);
   const enterTemp = <T>(fn: () => T): T => {
-    temp.value = !import.meta.env.DEV && true
-    const result = fn()
+    temp.value = !import.meta.env.DEV && true;
+    const result = fn();
     if (result instanceof Promise) {
-      return result.then(async (data) => {
-        temp.value = false
-        await nextTick()
-        return data;
-      }).catch(r => {
-        temp.value = false
-        throw r
-      }) as T
-    } else {
-      temp.value = false
       return result
+        .then(async (data) => {
+          temp.value = false;
+          await nextTick();
+          return data;
+        })
+        .catch((r) => {
+          temp.value = false;
+          throw r;
+        }) as T;
+    } else {
+      temp.value = false;
+      return result;
     }
-  }
-  const dom = useRendererDOM()
+  };
+  const dom = useRendererDOM();
   watch(temp, (_a, _b, onCleanup) => {
     if (temp.value && dom.value) {
-      const instance = ElLoading.service({ fullscreen: true, target: dom.value })
-      onCleanup(() => instance.close())
+      const instance = ElLoading.service({
+        fullscreen: true,
+        target: dom.value,
+      });
+      onCleanup(() => instance.close());
     }
-  })
-  
+  });
 
-  return { tempStatus: temp, enterTemp }
+  return { tempStatus: temp, enterTemp };
 });
 
 const getFilters = <T>() => {
-  type Val = (d: T) => T
-  const globalFilter = ref<{[key in ShapeType]?: Val[]}>({})
-  const shapeFilter = ref<Record<string, Val[]>>({})
+  type Val = (d: T) => T;
+  const globalFilter = ref<{ [key in ShapeType]?: Val[] }>({});
+  const shapeFilter = ref<Record<string, Val[]>>({});
   const setShapeFilter = (id: string, descs: Val) => {
     if (shapeFilter.value[id]) {
-      shapeFilter.value[id].push(descs)
+      shapeFilter.value[id].push(descs);
     } else {
-      shapeFilter.value[id] = [descs]
+      shapeFilter.value[id] = [descs];
     }
     return () => {
       if (shapeFilter.value[id]) {
-        const ndx = shapeFilter.value[id].indexOf(descs)
-        shapeFilter.value[id].splice(ndx, 1)
+        const ndx = shapeFilter.value[id].indexOf(descs);
+        shapeFilter.value[id].splice(ndx, 1);
       }
-    }
-  }
+    };
+  };
   const setFilter = (type: ShapeType, descs: Val) => {
     if (globalFilter.value[type]) {
-      globalFilter.value[type].push(descs)
+      globalFilter.value[type].push(descs);
     } else {
-      globalFilter.value[type] = [descs]
+      globalFilter.value[type] = [descs];
     }
     return () => {
       if (globalFilter.value[type]) {
-        const ndx = globalFilter.value[type].indexOf(descs)
-        globalFilter.value[type].splice(ndx, 1)
+        const ndx = globalFilter.value[type].indexOf(descs);
+        globalFilter.value[type].splice(ndx, 1);
       }
-    }
-  }
+    };
+  };
 
   return {
     setFilter,
@@ -432,38 +444,35 @@ const getFilters = <T>() => {
       return (menus: T) => {
         if (globalFilter.value[type]) {
           for (const filter of globalFilter.value[type]) {
-            menus = filter(menus)
+            menus = filter(menus);
           }
         }
         if (shapeFilter.value[id]) {
           for (const filter of shapeFilter.value[id]) {
-            menus = filter(menus)
+            menus = filter(menus);
           }
         }
-        return menus
-      }
-    }
-  }
-}
-
+        return menus;
+      };
+    },
+  };
+};
 
 export const useMountMenusFilter = installGlobalVar(() => {
-  const menusFilter = getFilters<PropertyDescribes>()
+  const menusFilter = getFilters<PropertyDescribes>();
   return {
     setMenusFilter: menusFilter.setFilter,
     setShapeMenusFilter: menusFilter.setShapeFilter,
-    getFilter: menusFilter.getFilter
-  }
-})
-
-
+    getFilter: menusFilter.getFilter,
+  };
+});
 
 export const useMouseMenusFilter = installGlobalVar(() => {
-  type Menu = { icon?: any; label?: string; handler: () => void }
-  const propsFilter = getFilters<Menu[]>()
+  type Menu = { icon?: any; label?: string; handler: () => void };
+  const propsFilter = getFilters<Menu[]>();
   return {
     setMenusFilter: propsFilter.setFilter,
     setShapeMenusFilter: propsFilter.setShapeFilter,
-    getFilter: propsFilter.getFilter
-  }
-})
+    getFilter: propsFilter.getFilter,
+  };
+});

+ 115 - 0
src/core/hook/use-group.ts

@@ -0,0 +1,115 @@
+import { computed, reactive, ref, Ref, watchEffect } from "vue";
+import { PropertyDescribes } from "../html-mount/propertys";
+import { installGlobalVar } from "./use-global-vars";
+import { inRevise, mergeFuns } from "@/utils/shared";
+
+export const useGlobalDescribes = installGlobalVar(() => {
+  const shapesDescribes: Record<string, PropertyDescribes> = reactive({});
+  const data: Record<string, { id: string }> = reactive({});
+  return {
+    set(item: { id: string }, descs: PropertyDescribes) {
+      shapesDescribes[item.id] = descs;
+      data[item.id] = item;
+    },
+    del(id: string) {
+      delete shapesDescribes[id];
+    },
+    get(id: string) {
+      return (
+        shapesDescribes[id] && { desc: shapesDescribes[id], data: data[id] }
+      );
+    },
+  };
+});
+
+export const useComponentsDescribes = (ids: Ref<string[]>) => {
+  const gdesc = useGlobalDescribes();
+  const excludeKeys = ["length", "name"];
+  const groups = computed(() => {
+    return ids.value.map((id) => gdesc.get(id)).filter((item) => !!item);
+  });
+  const shareDescribes = computed(() => {
+    if (groups.value.length === 0) {
+      return {};
+    }
+
+    const shareDescribes: Record<
+      string,
+      { desc: PropertyDescribes[string][]; data: { id: string }[] }
+    > = {};
+    const shareKeys: string[] = Object.keys(groups.value[0].desc);
+
+    for (const item of groups.value) {
+      const keys = Object.keys(item.desc);
+      for (const key of keys) {
+        if (shareKeys.includes(key)) {
+          if (!shareDescribes[key]) {
+            shareDescribes[key] = { desc: [], data: [] };
+          } else {
+            const temp = shareDescribes[key].desc[0];
+            if (inRevise(temp.props, item.desc[key].props)) {
+              delete shareDescribes[key];
+              shareKeys.splice(shareKeys.indexOf(key), 1);
+              continue;
+            }
+          }
+          shareDescribes[key].desc.push(item.desc[key]);
+          shareDescribes[key].data.push(item.data);
+        }
+      }
+      for (let i = 0; i < shareKeys.length; i++) {
+        if (!keys.includes(shareKeys[i])) {
+          delete shareDescribes[shareKeys[i]];
+          shareKeys.splice(i--, 1);
+        }
+      }
+    }
+    return shareDescribes;
+  });
+
+  const stopWatchs: (() => void)[] = []
+  const mergeDesc = computed(() => {
+    mergeFuns(stopWatchs)()
+    stopWatchs.length = 0
+    const mergeDesc: Record<
+      string,
+      PropertyDescribes[string] & { joins: { id: string }[] }
+    > = {};
+    for (const key in shareDescribes.value) {
+      if (excludeKeys.includes(key)) continue;
+
+      const { desc: descs, data } = shareDescribes.value[key];
+      mergeDesc[key] = {
+        ...descs[0],
+        joins: data,
+      };
+      let value = descs[0].value;
+      let i = 1;
+      for (; i < descs.length; i++) {
+        if (descs[i].value !== value) {
+          break;
+        }
+      }
+      if (i !== descs.length) {
+        mergeDesc[key].label = mergeDesc[key].label + '(多值)'
+        value = undefined
+      }
+
+      Object.defineProperty(mergeDesc[key], "value", {
+        get() {
+          return value
+        },
+        set(val) {
+          for (const desc of descs) {
+            desc.value = val;
+          }
+          return true;
+        },
+      });
+      delete mergeDesc[key].default;
+    }
+    return mergeDesc;
+  });
+
+  return mergeDesc;
+};

+ 25 - 42
src/core/hook/use-history.ts

@@ -2,7 +2,7 @@ import mitt from "mitt";
 import { SingleHistory } from "../history";
 import { installGlobalVar } from "./use-global-vars";
 import { Ref, ref, watch } from "vue";
-import { copy } from "@/utils/shared";
+import { copy, trackFlag } from "@/utils/shared";
 
 type HistoryItem = { attachs: string; data: string };
 export class DrawHistory {
@@ -19,8 +19,15 @@ export class DrawHistory {
     return this.list.find(item => item.id === this.currentId)?.data
   }
 
-  private preventFlag = 0;
-  private onceFlag = 0;
+  private once = trackFlag((flag) => {
+    if (flag === 0 && this.onceHistory) {
+      this.push(this.onceHistory);
+      this.onceHistory = null;
+    }
+  })
+  private prevent = trackFlag()
+  private enforce = trackFlag()
+
   private onceHistory: string | null = null;
   private rendererRaw?: (data: string) => void;
   initData: string | null = null;
@@ -47,6 +54,18 @@ export class DrawHistory {
     };
   }
 
+  onceTrack<T>(fn: () => T): T {
+    return this.once.track(fn)
+  }
+
+  preventTrack(fn: () => any) {
+    return this.prevent.track(fn)
+  }
+
+  enforceTrack(fn: () => any) {
+    return this.enforce.track(fn)
+  }
+
   setRenderer(renderer: (data: string) => void) {
     this.rendererRaw = renderer
   }
@@ -78,20 +97,6 @@ export class DrawHistory {
     this.push(data);
   }
 
-  private preventTrackCallback() {
-    this.preventFlag--
-  }
-
-  preventTrack(fn: () => any) {
-    this.preventFlag++
-    const result = fn();
-    if (result instanceof Promise) {
-      result.then(() => this.preventTrackCallback())
-    } else {
-      this.preventTrackCallback()
-    }
-  }
-
   private saveKeyPrev = '__history__'
   private saveKeyId: string | null = null
   get saveKey() {
@@ -141,10 +146,10 @@ export class DrawHistory {
   }
 
   push(data: string) {
-    if (this.preventFlag) return;
-    if (this.onceFlag) {
+    if (this.prevent.flag) return;
+    if (this.once.flag) {
       this.onceHistory = data;
-    } else if (data !== this.current?.data) {
+    } else if (data !== this.current?.data || this.enforce.flag) {
       console.log('push history')
       this.bus.emit("push", data);
       this.history.push({ attachs: JSON.stringify(this.pushAttachs), data });
@@ -167,28 +172,6 @@ export class DrawHistory {
     return data;
   }
 
-  private onceTrackCallback() {
-    this.onceFlag--
-    if (this.onceHistory) {
-      this.push(this.onceHistory);
-      this.onceHistory = null;
-    }
-  }
-
-  onceTrack<T>(fn: () => T): T {
-    this.onceFlag++
-    const result = fn();
-    if (result instanceof Promise) {
-      return result.then((data) => {
-        this.onceTrackCallback()
-        return data
-      }) as T
-    } else {
-      this.onceTrackCallback()
-      return result
-    }
-  }
-
   clearCurrent(): void {
     this.renderer({ data: this.clearData, attachs: "" });
     this.push(this.clearData);

+ 23 - 14
src/core/hook/use-interactive.ts

@@ -140,7 +140,6 @@ export const useInteractiveAreas = ({
     enter && enter();
 
     const upHandler = (ev: MouseEvent) => {
-      console.log('upHandler')
       if (downed) {
         mode.del(Mode.draging);
       }
@@ -154,7 +153,7 @@ export const useInteractiveAreas = ({
           : position;
       }
 
-      prevEv = null
+      prevEv = null;
       pushNdx = -1;
       pushed = false;
       downed = false;
@@ -162,7 +161,7 @@ export const useInteractiveAreas = ({
       singleDone.value = true;
     };
 
-    let prevEv: any
+    let prevEv: any;
     return mergeFuns(
       listener(dom, "pointerdown", (ev) => {
         if (!can.dragMode) return;
@@ -186,7 +185,7 @@ export const useInteractiveAreas = ({
         const end = getOffset(ev, dom);
         const point = beforeHandler ? beforeHandler(end) : end;
 
-        prevEv = ev
+        prevEv = ev;
         if (downed) {
           if (pushed) {
             messages.value[pushNdx]![1] = point;
@@ -235,10 +234,11 @@ export const useInteractiveDots = ({
     let pushed = false;
     const empty = { x: -9999, y: -9999 };
     const pointer = ref(empty);
+    const beforePointer = ref(empty);
     enter && enter();
     mode.add(Mode.draging);
 
-    const move = (ev: MouseEvent) => {
+    const posMove = (position: Pos) => {
       if (!can.dragMode) return;
       if (!pushed) {
         messages.value.push(pointer.value);
@@ -247,10 +247,13 @@ export const useInteractiveDots = ({
       }
 
       moveIng = true;
-      const position = getOffset(ev);
       const current = beforeHandler ? beforeHandler(position) : position;
       pointer.value.x = current.x;
       pointer.value.y = current.y;
+      beforePointer.value = { ...position };
+    };
+    const move = (ev: MouseEvent) => {
+      posMove(getOffset(ev));
     };
 
     let prevPoint: Pos = { ...empty };
@@ -258,17 +261,23 @@ export const useInteractiveDots = ({
       () => {
         mode.del(Mode.draging);
       },
-      clickListener(dom, (_, ev) => {
+      watch(singleDone, () => {
+        if (singleDone.value) {
+          prevPoint = pointer.value;
+          const prevBeforePoint = beforePointer.value
+          pointer.value = { ...empty };
+          beforePointer.value = {...empty}
+          singleDone.value = true;
+          moveIng = false;
+          pushed = false;
+          nextTick(() => posMove(prevBeforePoint));
+        }
+      }),
+      clickListener(dom, (_) => {
         if (!moveIng || !can.dragMode || eqPoint(prevPoint, pointer.value))
           return;
-        prevPoint = pointer.value;
-
-        pointer.value = { ...empty };
         singleDone.value = true;
-        moveIng = false;
-        pushed = false;
-
-        nextTick(() => move(ev));
+        // nextTick(() => move(ev));
       }),
       listener(dom, "pointermove", move)
     );

+ 11 - 0
src/core/hook/use-layer.ts

@@ -15,6 +15,7 @@ import { Layer } from "konva/lib/Layer";
 import { useMouseShapeStatus } from "./use-mouse-status";
 import { DataGroupId } from "@/constant";
 import { Group } from "konva/lib/Group";
+import { getFlatChildren } from "@/utils/shape";
 
 // const useRefreshCount = installGlobalVar(() => ref(0));
 // const useRefresh = () => {
@@ -39,6 +40,16 @@ export const useGetFormalChildren = () => {
   };
 };
 
+export const useGetFormalFlatChildren = () => {
+  const formal = useFormalLayer();
+  return () => {
+    const children = formal.value?.findOne<Group>(`#${DataGroupId}`)?.children
+    if (!children) return []
+    return children.flatMap(item => getFlatChildren(item))
+  };
+};
+
+
 
 export const useHelperLayer = () => {
   const stage = useStage();

+ 0 - 1
src/core/hook/use-mouse-status.ts

@@ -302,7 +302,6 @@ export const useMouseShapesStatus = installGlobalVar(() => {
             const ndx = selects.value.findIndex(
               (item) => item.id() === target?.id()
             );
-            
             if (~ndx) {
               selects.value.splice(ndx, 1);
             } else {

+ 0 - 320
src/core/hook/use-selection-n.ts

@@ -1,320 +0,0 @@
-import { Rect } from "konva/lib/shapes/Rect";
-import {
-  globalWatch,
-  installGlobalVar,
-  useForciblyShowItemIds,
-  useMountParts,
-  useStage,
-} from "./use-global-vars";
-import {
-  useGetFormalChildren,
-  useFormalLayer,
-  useHelperLayer,
-} from "./use-layer";
-import { themeColor } from "@/constant";
-import { dragListener } from "@/utils/event";
-import { Layer } from "konva/lib/Layer";
-import { useOperMode } from "./use-status";
-import {
-  computed,
-  markRaw,
-  nextTick,
-  reactive,
-  Ref,
-  ref,
-  toRaw,
-  watch,
-  watchEffect,
-} from "vue";
-import { EntityShape } from "@/deconstruction";
-import { Util } from "konva/lib/Util";
-import {
-  useViewerInvertTransform,
-  useViewerInvertTransformConfig,
-} from "./use-viewer";
-import { debounce, diffArrayChange, mergeFuns, onlyId } from "@/utils/shared";
-import { IRect } from "konva/lib/types";
-import { useMouseShapesStatus } from "./use-mouse-status";
-import Icon from "../components/icon/temp-icon.vue";
-import { Group } from "konva/lib/Group";
-import { Component as GroupComp, GroupData } from "../components/group/";
-import { useStore } from "../store";
-import { useOnComponentBoundChange } from "./use-component";
-import { useHistory } from "./use-history";
-import { isRectContained } from "@/utils/math";
-import { useTransformer } from "./use-transformer";
-import { IconData } from "../components/icon";
-import { usePause } from "./use-pause";
-import mitt, { Emitter } from "mitt";
-
-// 多选不包含分组, 只包含选中者
-export const useSelection = installGlobalVar(() => {
-  const layer = useHelperLayer();
-  const getChildren = useGetFormalChildren();
-  const box = new Rect({
-    stroke: themeColor,
-    strokeWidth: 1,
-    fill: "#fff",
-    listening: false,
-    opacity: 0.5,
-  });
-  const stage = useStage();
-  const operMode = useOperMode();
-  const selections = ref<EntityShape[]>();
-  const transformer = useTransformer();
-
-  let shapeBoxs: IRect[] = [];
-  let shapes: EntityShape[] = [];
-
-  const updateSelections = () => {
-    const boxRect = box.getClientRect();
-    selections.value = [];
-
-    for (let i = 0; i < shapeBoxs.length; i++) {
-      if (
-        Util.haveIntersection(boxRect, shapeBoxs[i]) &&
-        !isRectContained(shapeBoxs[i], boxRect) &&
-        shapes[i] !== toRaw(transformer)
-      ) {
-        if (!selections.value.includes(shapes[i])) {
-          selections.value.push(shapes[i]);
-        }
-      }
-    }
-  };
-
-  const init = (dom: HTMLDivElement, layer: Layer) => {
-    const stopListener = dragListener(dom, {
-      down(pos) {
-        layer.add(box);
-        box.x(pos.x);
-        box.y(pos.y);
-        box.width(0);
-        box.height(0);
-      },
-      move({ end }) {
-        box.width(end.x - box.x());
-        box.height(end.y - box.y());
-        updateSelections();
-      },
-      up() {
-        selections.value = undefined;
-        box.remove();
-      },
-    });
-    return () => {
-      stopListener();
-      box.remove();
-    };
-  };
-
-  const updateInitData = () => {
-    shapes = getChildren();
-    shapeBoxs = shapes.map((shape) => shape.getClientRect());
-  };
-
-  const stopWatch = globalWatch(
-    () => operMode.value.mulSelection,
-    (mulSelection, _, onCleanup) => {
-      if (!mulSelection) return;
-      const dom = stage.value?.getNode().container()!;
-      updateInitData();
-      onCleanup(init(dom, layer.value!));
-    }
-  );
-
-  return {
-    onDestroy: stopWatch,
-    var: { selections, box },
-  };
-});
-
-type ShapeIconArgs = Partial<
-  Pick<IconData, "width" | "height" | "url" | "fill" | "stroke">
->;
-export const useShapesIcon = (
-  shapes: Ref<EntityShape[] | undefined>,
-  args: ShapeIconArgs = {}
-) => {
-  const mParts = useMountParts();
-  const { on } = useOnComponentBoundChange();
-  const iconProps = {
-    width: 12,
-    height: 12,
-    url: "./icons/state_s.svg",
-    fill: themeColor,
-    stroke: "#fff",
-    ...args,
-    listening: false,
-  };
-  const invConfig = useViewerInvertTransformConfig();
-  const invMat = useViewerInvertTransform();
-  const getShapeMat = (shape: EntityShape) => {
-    const rect = shape.getClientRect();
-    const center = invMat.value.point({
-      x: rect.x + rect.width / 2,
-      y: rect.y + rect.height / 2,
-    });
-    return [1, 0, 0, 1, center.x, center.y];
-  };
-  const unMountMap = new WeakMap<EntityShape, () => void>();
-
-  const pause = usePause();
-  const stop = watch([shapes, () => pause.isPause], ([shapes], [oldShapes]) => {
-    if (pause.isPause) {
-      shapes = [];
-    }
-
-    const { added, deleted } = diffArrayChange(shapes || [], oldShapes || []);
-    for (const addShape of added) {
-      const mat = ref(getShapeMat(addShape));
-      const data = reactive({ ...iconProps, mat: mat });
-      const unHooks = [
-        on(addShape, () => (mat.value = getShapeMat(addShape))),
-        watch(
-          invConfig,
-          () => {
-            data.width = invConfig.value.scaleX * iconProps.width;
-            data.height = invConfig.value.scaleY * iconProps.height;
-          },
-          { immediate: true }
-        ),
-        mParts.add({
-          comp: markRaw(Icon),
-          props: { data },
-        }),
-      ];
-      unMountMap.set(addShape, mergeFuns(unHooks));
-    }
-    for (const delShape of deleted) {
-      const fn = unMountMap.get(delShape);
-      fn && fn();
-    }
-  });
-  return [stop, pause];
-};
-
-export const useStoreSelectionManage = installGlobalVar(() => {
-  const store = useStore();
-  const bus: Emitter<Record<"del" | "update", EntityShape>> = mitt();
-  const { on } = useOnComponentBoundChange();
-
-  const canSelect = (shape: EntityShape) => {
-    const id = shape.id();
-    return id && store.items.some((item) => item.id === id);
-  };
-  const listener = (shape: EntityShape) => {
-    return watch(
-      () => canSelect(shape),
-      (exixts, _, onCleanup) => {
-        if (!exixts) {
-          bus.emit("del", shape);
-        } else {
-          onCleanup(on(shape, () => bus.emit("update", shape)));
-        }
-      },
-      { immediate: true }
-    );
-  };
-
-  return { canSelect, listener };
-});
-
-export const useSelectionRevise = () => {
-  const storeManage = useStoreSelectionManage()
-  const mParts = useMountParts();
-  const status = useMouseShapesStatus();
-  const store = useStore();
-  const { selections: rectSelects } = useSelection();
-
-  let initSelections: EntityShape[] = [];
-  watch(
-    () => rectSelects.value && [...rectSelects.value],
-    (rectSelects, oldRectSelects) => {
-      if (!oldRectSelects) {
-        initSelections = [...status.selects];
-      } else if (!rectSelects) {
-        initSelections = [];
-      } else {
-        status.selects = initSelections.concat(rectSelects);
-        filterSelect()
-      }
-    }
-  );
-  useShapesIcon(computed(() => status.selects.concat(rectSelects.value || [])));
-
-  const filterSelect = debounce(() => {
-    const mouseSelects = status.selects.filter((shape) => storeManage.canSelect(shape));
-    status.selects = mouseSelects;
-  }, 16);
-  store.bus.on("delItemAfter", filterSelect);
-  store.bus.on("clearAfter", filterSelect);
-  store.bus.on("dataChangeAfter", filterSelect);
-  store.bus.on("setCurrentLayerAfter", filterSelect);
-
-  const ids = computed(() => [
-    ...new Set(status.selects.map((item) => item.id())),
-  ]);
-  const groupConfig = {
-    id: onlyId(),
-    createTime: Date.now(),
-    lock: false,
-    opacity: 1,
-    ref: false,
-    listening: false,
-    stroke: themeColor,
-  };
-  const operMode = useOperMode();
-  const layer = useFormalLayer();
-  watch(
-    () => [!!ids.value.length, operMode.value.mulSelection],
-    (_a, _b) => {
-      const groupShape = layer.value?.findOne<Group>(`#${groupConfig.id}`);
-      if (!groupShape) return;
-      if (ids.value.length && !operMode.value.mulSelection) {
-        status.actives = [groupShape];
-      } else if (status.actives.includes(groupShape)) {
-        status.actives = [];
-      }
-    }
-  );
-
-  const stage = useStage();
-  const history = useHistory();
-  const showItemId = useForciblyShowItemIds();
-  watchEffect((onCleanup) => {
-    if (!ids.value.length) return;
-    const props = {
-      data: { ...groupConfig, ids: ids.value },
-      key: groupConfig.id,
-      onUpdateShape(data: GroupData) {
-        // status.selects;
-        // data.ids;
-      },
-      onDelShape() {
-        status.selects = [];
-      },
-      onAddShape(data: GroupData) {
-        history.onceTrack(() => {
-          const ids = data.ids;
-          const groups = store.typeItems.group;
-          const exists = groups?.some((group) => {
-            if (group.ids.length !== ids.length) return false;
-            const diff = diffArrayChange(group.ids, ids);
-            return diff.added.length === 0 && diff.deleted.length == 0;
-          });
-          if (exists) return;
-
-          store.addItem("group", { ...data, ids });
-          showItemId.cycle(data.id, async () => {
-            await nextTick();
-            const $stage = stage.value!.getNode();
-            const addShape = $stage.findOne("#" + data.id) as EntityShape;
-            status.selects = [addShape];
-          });
-        });
-      },
-    };
-    onCleanup(mParts.add({ comp: markRaw(GroupComp), props }));
-  });
-};

+ 169 - 225
src/core/hook/use-selection.ts

@@ -7,9 +7,9 @@ import {
   useStage,
 } from "./use-global-vars";
 import {
-  useGetFormalChildren,
   useFormalLayer,
   useHelperLayer,
+  useGetFormalChildren,
 } from "./use-layer";
 import { themeColor } from "@/constant";
 import { dragListener } from "@/utils/event";
@@ -20,6 +20,7 @@ import {
   markRaw,
   nextTick,
   reactive,
+  Ref,
   ref,
   toRaw,
   watch,
@@ -27,91 +28,35 @@ import {
 } from "vue";
 import { EntityShape } from "@/deconstruction";
 import { Util } from "konva/lib/Util";
-import { useViewerInvertTransform, useViewerInvertTransformConfig } from "./use-viewer";
+import {
+  useViewerInvertTransform,
+  useViewerInvertTransformConfig,
+} from "./use-viewer";
 import { debounce, diffArrayChange, mergeFuns, onlyId } from "@/utils/shared";
 import { IRect } from "konva/lib/types";
 import { useMouseShapesStatus } from "./use-mouse-status";
 import Icon from "../components/icon/temp-icon.vue";
 import { Group } from "konva/lib/Group";
-import { Component as GroupComp, GroupData } from "../components/group/";
-import { DrawStore, useStore } from "../store";
-import { DrawItem } from "../components";
-import { Stage } from "konva/lib/Stage";
-import { useOnComponentBoundChange } from "./use-component";
+import { Component as GroupComp, GroupData } from "../components/group";
+import { useStore } from "../store";
+import { useGetShapeBelong, useOnComponentBoundChange } from "./use-component";
 import { useHistory } from "./use-history";
 import { isRectContained } from "@/utils/math";
 import { useTransformer } from "./use-transformer";
-
-const normalSelectIds = (
-  store: DrawStore,
-  ids: string[],
-  needChildren = false
-) => {
-  if (!store.typeItems.group) return ids;
-
-  const gChildrenIds = store.typeItems.group.map((item) => item.ids);
-  const findNdx = (id: string) =>
-    gChildrenIds.findIndex((cIds) => cIds.includes(id));
-  if (!needChildren) {
-    return ids.filter((id) => !~findNdx(id));
-  }
-
-  const groupIds = store.typeItems.group.map((item) => item.id);
-  const nIds: string[] = [];
-  for (let i = 0; i < ids.length; i++) {
-    let ndx = findNdx(ids[i]);
-    ~ndx || (ndx = groupIds.indexOf(ids[i]));
-
-    if (!~ndx) {
-      nIds.push(ids[i]);
-      continue;
-    }
-
-    const group = store.typeItems.group[ndx];
-    const addIds = [group.id, ...group.ids].filter(
-      (aid) => !nIds.includes(aid)
-    );
-    nIds.push(...addIds);
-  }
-  return nIds;
-};
-
-export const normalSelectShapes = (
-  stage: Stage,
-  store: DrawStore,
-  shapes: EntityShape[],
-  needChildren = false
-) => {
-  let ids: string[] = [];
-  for (let i = 0; i < shapes.length; i++) {
-    const shape = shapes[i];
-    const id = shape.id();
-    id && ids.push(id);
-  }
-  ids = normalSelectIds(store, ids, needChildren);
-  return ids.map((id) => stage.findOne(`#${id}`)!) as EntityShape[];
-};
-
-export const normalSelectItems = (
-  store: DrawStore,
-  items: DrawItem[],
-  needChildren = false
-) => {
-  return normalSelectIds(
-    store,
-    items.map((item) => item.id),
-    needChildren
-  ).map((id) => store.getItemById(id)!);
-};
-
-export const useExcludeSelection = installGlobalVar(() => ref<string[]>([]))
-
+import { IconData } from "../components/icon";
+import { usePause } from "./use-pause";
+import mitt, { Emitter } from "mitt";
+import { components, ShapeType, shapeTypes } from "../components";
+import { getFlatChildren } from "@/utils/shape";
+
+export const useExcludeSelection = installGlobalVar(() => ref<string[]>([]));
+// 多选不包含分组, 只包含选中者
 export const useSelection = installGlobalVar(() => {
   const layer = useHelperLayer();
-  const eSelection = useExcludeSelection()
+  const eSelection = useExcludeSelection();
   const getChildren = useGetFormalChildren();
   const box = new Rect({
-    stroke: themeColor, 
+    stroke: themeColor,
     strokeWidth: 1,
     fill: "#fff",
     listening: false,
@@ -121,29 +66,36 @@ export const useSelection = installGlobalVar(() => {
   const operMode = useOperMode();
   const selections = ref<EntityShape[]>();
   const transformer = useTransformer();
+  const getShapeSelectionManage = useGetShapeSelectionManage();
 
-  let shapeBoxs: IRect[] = [];
-  let shapes: EntityShape[] = [];
+  let itemShapeBoxs: IRect[][] = [];
+  let itemShapes: EntityShape[][] = [];
 
   const updateSelections = () => {
     const boxRect = box.getClientRect();
     selections.value = [];
-
-    for (let i = 0; i < shapeBoxs.length; i++) {
-      if (
-        
-        Util.haveIntersection(boxRect, shapeBoxs[i]) &&
-        !isRectContained(shapeBoxs[i], boxRect) &&
-        shapes[i] !== toRaw(transformer)
-      ) {
-        if (!selections.value.includes(shapes[i])) {
-          selections.value.push(shapes[i]);
+    for (let i = 0; i < itemShapeBoxs.length; i++) {
+      for (let j = 0; j < itemShapeBoxs[i].length; j++) {
+        const shape = itemShapes[i][j];
+        const box = itemShapeBoxs[i][j];
+        const itemSelects: EntityShape[] = [];
+        if (
+          Util.haveIntersection(boxRect, box) &&
+          !isRectContained(box, boxRect)
+        ) {
+          if (!selections.value.includes(shape)) {
+            selections.value.push(shape);
+            itemSelects.push(shape);
+          }
         }
       }
     }
   };
 
+  const store = useStore();
   const init = (dom: HTMLDivElement, layer: Layer) => {
+    store.bus.on("addItemAfter", updateInitData);
+    store.bus.on('dataChangeAfter', updateInitData);
     const stopListener = dragListener(dom, {
       down(pos) {
         layer.add(box);
@@ -163,14 +115,25 @@ export const useSelection = installGlobalVar(() => {
       },
     });
     return () => {
+      store.bus.off("addItemAfter", updateInitData);
+      store.bus.off('dataChangeAfter', updateInitData);
       stopListener();
       box.remove();
     };
   };
 
   const updateInitData = () => {
-    shapes = getChildren().filter(shape => !eSelection.value.includes(shape.id()));
-    shapeBoxs = shapes.map((shape) => shape.getClientRect());
+    itemShapes = getChildren().map((item) =>
+      getFlatChildren(item).filter(
+        (shape) =>
+          !eSelection.value.includes(shape.id()) &&
+          shape !== toRaw(transformer) &&
+          getShapeSelectionManage(shape)?.canSelect(shape)
+      )
+    );
+    itemShapeBoxs = itemShapes.map((shapes) =>
+      shapes.map((shape) => shape.getClientRect())
+    );
   };
 
   const stopWatch = globalWatch(
@@ -185,11 +148,17 @@ export const useSelection = installGlobalVar(() => {
 
   return {
     onDestroy: stopWatch,
-    var: {selections, box},
+    var: { selections, box },
   };
 });
 
-export const useSelectionShowIcons = installGlobalVar(() => {
+type ShapeIconArgs = Partial<
+  Pick<IconData, "width" | "height" | "url" | "fill" | "stroke">
+>;
+export const useShapesIcon = (
+  shapes: Ref<EntityShape[] | undefined>,
+  args: ShapeIconArgs = {}
+) => {
   const mParts = useMountParts();
   const { on } = useOnComponentBoundChange();
   const iconProps = {
@@ -198,13 +167,10 @@ export const useSelectionShowIcons = installGlobalVar(() => {
     url: "./icons/state_s.svg",
     fill: themeColor,
     stroke: "#fff",
+    ...args,
     listening: false,
   };
-  const invConfig = useViewerInvertTransformConfig()
-  const status = useMouseShapesStatus();
-  const getChildren = useGetFormalChildren()
-
-  const store = useStore();
+  const invConfig = useViewerInvertTransformConfig();
   const invMat = useViewerInvertTransform();
   const getShapeMat = (shape: EntityShape) => {
     const rect = shape.getClientRect();
@@ -214,22 +180,28 @@ export const useSelectionShowIcons = installGlobalVar(() => {
     });
     return [1, 0, 0, 1, center.x, center.y];
   };
-  const shapes = computed(() => {
-    const child = getChildren()
-    return status.selects.filter((shape) => store.getType(shape.id()) !== "group" && child.includes(shape))
-  });
   const unMountMap = new WeakMap<EntityShape, () => void>();
-  watch(shapes, (shapes, oldShapes) => {
-    const { added, deleted } = diffArrayChange(shapes, oldShapes);
+
+  const pause = usePause();
+  const stop = watch([shapes, () => pause.isPause], ([shapes], [oldShapes]) => {
+    if (pause.isPause) {
+      shapes = [];
+    }
+
+    const { added, deleted } = diffArrayChange(shapes || [], oldShapes || []);
     for (const addShape of added) {
       const mat = ref(getShapeMat(addShape));
-      const data = reactive({ ...iconProps, mat: mat })
+      const data = reactive({ ...iconProps, mat: mat });
       const unHooks = [
         on(addShape, () => (mat.value = getShapeMat(addShape))),
-        watch(invConfig, () => {
-          data.width = invConfig.value.scaleX * iconProps.width
-          data.height = invConfig.value.scaleY * iconProps.height
-        }, {immediate: true}),
+        watch(
+          invConfig,
+          () => {
+            data.width = invConfig.value.scaleX * iconProps.width;
+            data.height = invConfig.value.scaleY * iconProps.height;
+          },
+          { immediate: true }
+        ),
         mParts.add({
           comp: markRaw(Icon),
           props: { data },
@@ -242,134 +214,120 @@ export const useSelectionShowIcons = installGlobalVar(() => {
       fn && fn();
     }
   });
-});
-
-const useWatchSelection = () => {
-  const status = useMouseShapesStatus();
-  const addShapes = (allShapes: Set<EntityShape>, iShapes: EntityShape[]) => {
-    iShapes.forEach((shape) => allShapes.add(toRaw(shape)));
-    return allShapes;
-  };
-  const delShapes = (allShapes: Set<EntityShape>, dShapes: EntityShape[]) => {
-    dShapes.forEach((item) => allShapes.delete(toRaw(item)));
-    return allShapes;
-  };
-
-  // 分组管理
-  const watchSelection = () =>
-    watch(
-      () => status.selects,
-      (shapes) => {
-        const fShapes = Array.from(new Set(shapes));
-        const { added, deleted } = diffArrayChange(shapes, fShapes);
-        if (added.length || deleted.length) {
-          status.selects = fShapes;
-        }
-      },
-      { flush: "post" }
-    );
+  return [stop, pause];
+};
 
-  return {
-    addShapes,
-    delShapes,
-    watchSelection,
+export type SelectionManageBus = Emitter<Record<"del" | "update", EntityShape>>;
+export type SelectionManage = {
+  canSelect: (shape: EntityShape, selects?: EntityShape[]) => boolean;
+  listener: (shape: EntityShape) => {
+    stop: () => void;
+    bus: SelectionManageBus;
   };
 };
+export type UseGetSelectionManage = () => SelectionManage;
 
-const useWatchSelectionGroup = () => {
-  const stage = useStage();
+export const useStoreSelectionManage = installGlobalVar((): SelectionManage => {
   const store = useStore();
-  const status = useMouseShapesStatus();
-  const addShapes = (allShapes: Set<EntityShape>, iShapes: EntityShape[]) => {
-    const shapes = normalSelectShapes(
-      stage.value!.getNode(),
-      store,
-      iShapes,
-      true
-    );
-    shapes.forEach((shape) => allShapes.add(shape));
-    return allShapes;
-  };
-  const delShapes = (allShapes: Set<EntityShape>, dShapes: EntityShape[]) => {
-    const shapes = normalSelectShapes(
-      stage.value!.getNode(),
-      store,
-      dShapes,
-      true
-    );
-    shapes.forEach((item) => allShapes.delete(item));
-    return allShapes;
-  };
-
-  // 分组管理
-  const watchSelection = () =>
-    watch(
-      () => status.selects,
-      (shapes, oldShapes) => {
-        const { added, deleted } = diffArrayChange(shapes, oldShapes);
-        const filterShapes = new Set(shapes);
-        added.length && addShapes(filterShapes, added);
-        deleted.length && delShapes(filterShapes, deleted);
+  const { on } = useOnComponentBoundChange();
 
-        if (added.length || deleted.length) {
-          status.selects = Array.from(filterShapes);
+  const canSelect = (shape: EntityShape) => {
+    const id = shape.id();
+    return !!(id && store.items.some((item) => item.id === id));
+  };
+  const listener = (shape: EntityShape) => {
+    const bus: SelectionManageBus = mitt();
+    const stop = watch(
+      () => canSelect(shape),
+      (exixts, _, onCleanup) => {
+        if (!exixts) {
+          bus.emit("del", shape);
+        } else {
+          onCleanup(on(shape, () => bus.emit("update", shape)));
         }
       },
-      { flush: "post" }
+      { immediate: true }
     );
+    return { stop, bus };
+  };
 
-  return {
-    addShapes,
-    delShapes,
-    watchSelection,
+  return { canSelect, listener };
+});
+
+export const useGetShapeSelectionManage = installGlobalVar(() => {
+  const compManages: Partial<Record<ShapeType, SelectionManage>> = {};
+  for (const type of shapeTypes) {
+    compManages[type] =
+      components[type].useGetSelectionManage &&
+      components[type].useGetSelectionManage();
+  }
+  const storeManage = useStoreSelectionManage();
+  const getShapeBelong = useGetShapeBelong();
+  return (shape: EntityShape) => {
+    const bl = getShapeBelong(shape);
+    if (!bl) return;
+    if (compManages[bl.type]) {
+      return compManages[bl.type];
+    } else if (bl.isSelf) {
+      return storeManage;
+    }
   };
-};
+});
 
 export const useSelectionRevise = () => {
+  const getShapeSelectionManage = useGetShapeSelectionManage();
   const mParts = useMountParts();
   const status = useMouseShapesStatus();
   const store = useStore();
-  const eSelection = useExcludeSelection()
-
-  const { addShapes, delShapes, watchSelection } = useWatchSelection();
-
-  useSelectionShowIcons();
-
-  const getFormatChildren = useGetFormalChildren();
-  const filterSelect = debounce(() => {
-    const children = getFormatChildren();
-    const mouseSelects = status.selects.filter((shape) =>
-      !eSelection.value.includes(shape.id()) && children.includes(shape)
-    );
-    status.selects = mouseSelects;
-  }, 16);
-  store.bus.on("delItemAfter", filterSelect);
-  store.bus.on("clearAfter", filterSelect);
-  store.bus.on("dataChangeAfter", filterSelect);
-  store.bus.on("setCurrentLayerAfter", filterSelect);
-
   const { selections: rectSelects } = useSelection();
+  let selfSet = false;
+  const setSelectShapes = (shapes: EntityShape[]) => {
+    selfSet = true;
+    status.selects = shapes;
+    selfSet = false;
+  };
+
   let initSelections: EntityShape[] = [];
-  let stopWatchSelection = watchSelection();
   watch(
     () => rectSelects.value && [...rectSelects.value],
     (rectSelects, oldRectSelects) => {
       if (!oldRectSelects) {
         initSelections = [...status.selects];
-        stopWatchSelection();
       } else if (!rectSelects) {
         initSelections = [];
-        stopWatchSelection = watchSelection();
       } else {
-        status.selects = Array.from(
-          addShapes(new Set(initSelections), rectSelects)
-        );
-        // filterSelect()
+        setSelectShapes(initSelections.concat(rectSelects));
       }
     }
   );
+  useShapesIcon(computed(() => status.selects.concat(rectSelects.value || [])));
 
-  const ids = computed(() => [...new Set(status.selects.map((item) => item.id()))]);
+  const filterSelect = debounce(() => {
+    const selects = new Set<EntityShape>();
+    for (const shape of status.selects) {
+      const children = getFlatChildren(shape);
+      children.forEach((childShape) => {
+        const manage = getShapeSelectionManage(childShape);
+        if (manage?.canSelect(childShape)) {
+          selects.add(childShape);
+        }
+      });
+    }
+    setSelectShapes([...selects]);
+  }, 16);
+  store.bus.on("delItemAfter", filterSelect);
+  store.bus.on("clearAfter", filterSelect);
+  store.bus.on("dataChangeAfter", filterSelect);
+  store.bus.on("setCurrentLayerAfter", filterSelect);
+  watch(
+    () => status.selects,
+    () => selfSet || filterSelect(),
+    { flush: "sync" }
+  );
+
+  const ids = computed(() => [
+    ...new Set(status.selects.map((item) => item.id())),
+  ]);
   const groupConfig = {
     id: onlyId(),
     createTime: Date.now(),
@@ -403,43 +361,29 @@ export const useSelectionRevise = () => {
       data: { ...groupConfig, ids: ids.value },
       key: groupConfig.id,
       onUpdateShape(data: GroupData) {
-        status.selects;
-        data.ids;
+        // status.selects;
+        // data.ids;
       },
       onDelShape() {
-        status.selects = [];
+        setSelectShapes([]);
       },
       onAddShape(data: GroupData) {
         history.onceTrack(() => {
           const ids = data.ids;
-          const cIds = ids.filter((id) => store.getType(id) !== "group");
-
           const groups = store.typeItems.group;
           const exists = groups?.some((group) => {
-            if (group.ids.length !== cIds.length) return false;
-            const diff = diffArrayChange(group.ids, cIds);
+            if (group.ids.length !== ids.length) return false;
+            const diff = diffArrayChange(group.ids, ids);
             return diff.added.length === 0 && diff.deleted.length == 0;
           });
           if (exists) return;
 
-          let selects = new Set(status.selects);
-          for (let i = 0; i < ids.length; i++) {
-            if (store.getType(ids[i]) === "group") {
-              delShapes(
-                selects,
-                status.selects.filter((shape) => shape.id() === ids[i])
-              );
-              store.delItem("group", ids[i]);
-            }
-          }
-
-          store.addItem("group", { ...data, ids: cIds });
+          store.addItem("group", { ...data, ids });
           showItemId.cycle(data.id, async () => {
             await nextTick();
             const $stage = stage.value!.getNode();
             const addShape = $stage.findOne("#" + data.id) as EntityShape;
-            addShapes(selects, [addShape]);
-            status.selects = Array.from(selects);
+            setSelectShapes([addShape]);
           });
         });
       },

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

@@ -87,7 +87,8 @@ export const useOperMode = installGlobalVar(() => {
 
   return computed(() => ({
     // 多选模式
-    mulSelection: keys.has('Shift') && !keys.has(' ') && !keys.has('Alt'),
+    mulSelection: keys.has('Ctrl') && !keys.has(' ') && !keys.has('Alt'),
+    // mulSelection: keys.has('Meta') && !keys.has(' ') && !keys.has('Alt'),
     // mulSelection: false,
     // 自由移动视图
     freeView: keys.has(' '),

+ 1 - 1
src/core/hook/use-transformer.ts

@@ -185,7 +185,7 @@ export type TransformerVectorType = ScaleVectorType | "rotater";
 export const useGetTransformerOperType = () => {
   const transformer = useTransformer();
   return () => {
-    if (!transformer.nodes().length) return null;
+    if (!transformer.nodes().length) return undefined;
     return transformer.getActiveAnchor() as TransformerVectorType;
   };
 };

+ 0 - 1
src/core/html-mount/propertys/components/color.vue

@@ -28,7 +28,6 @@ const emit = defineEmits<{
   (e: "update:value", val: string | null): string;
   (e: "change"): void;
 }>();
-
 const predefineColors = ref<Array<string | null>>([
   "#000000",
   "#FFFFFF",

+ 2 - 2
src/core/html-mount/propertys/components/input-num.vue

@@ -2,7 +2,7 @@
   <div>
     <el-input-number
       :controls="false"
-      :modelValue="props.proportion ? transform(value) : value"
+      :modelValue="value ? (props.proportion ? transform(value) : value) : min"
       @update:model-value="(val: any) => props.proportion ? changeHandler(invTransform(val), val) : changeHandler(val, val)"
       @change="$emit('change')"
       style="width: 98px"
@@ -24,7 +24,7 @@ import { useProportion } from "@/core/hook/use-proportion";
 import { ElInputNumber } from "element-plus";
 
 const props = defineProps<{
-  value: number;
+  value?: number;
   min?: number;
   max?: number;
   proportion?: boolean;

+ 2 - 4
src/core/html-mount/propertys/components/num.vue

@@ -3,7 +3,7 @@
     <el-slider
       class="property-num-slider"
       :class="{ proportion: props.proportion }"
-      :modelValue="props.proportion ? transform(value) : value"
+      :modelValue="value ? (props.proportion ? transform(value) : value) : min"
       @update:model-value="(val: any) => props.proportion ? changeHandler(invTransform(val)) : changeHandler(val)"
       @change="$emit('change')"
       size="small"
@@ -28,15 +28,13 @@ import { ElSlider } from "element-plus";
 
 const props = defineProps<{
   data?: Record<string, any>;
-  value: number;
+  value?: number;
   min?: number;
   max?: number;
   step?: number;
   proportion?: boolean;
 }>();
-
 const { proportion, transform, invTransform } = useProportion();
-
 const emit = defineEmits<{
   (e: "update:value", val: number): void;
   (e: "change"): void;

+ 2 - 0
src/core/html-mount/propertys/index.ts

@@ -47,8 +47,10 @@ export type PropertyDescribes = Record<
     label: string;
     default?: PropertyValue<PropertyType>;
     'layout-type'?: string;
+    isChange?: boolean,
     props?: Partial<PropertyProps<PropertyType>>;
     value?: PropertyValue<PropertyType>;
+    onChange?: () => void,
     sort?: number
   }
 >;

+ 189 - 0
src/core/html-mount/propertys/mount-describes.vue

@@ -0,0 +1,189 @@
+<template>
+  <Teleport :to="`#${DomOutMountId}`">
+    <transition name="mount-fade">
+      <div class="mount-layout" v-if="show">
+        <div v-if="name" class="title">
+          <h4>设置{{ name }}</h4>
+          <icon name="close" size="20px" class="operate" @click="emit('close')" />
+        </div>
+        <div :size="8" class="mount-controller">
+          <div
+            v-for="[key, val] in describeItems"
+            :key="key"
+            class="mount-item"
+            :class="val['layout-type'] || 'column'"
+          >
+            <span class="label">{{ val.label }}</span>
+            <component
+              :data="data"
+              v-bind="describes[key].props"
+              :value="
+                'value' in describes[key]
+                  ? describes[key].value
+                  : props.data && props.data[key]
+              "
+              @update:value="(val: any) => {
+                updateValue(key, val)
+                describes[key].isChange = true
+              }"
+              @change="
+                () => {
+                  changeHandler();
+                  describes[key].onChange && describes[key].onChange();
+                  describes[key].isChange = false;
+                }
+              "
+              :is="propertyComponents[val.type]"
+              :key="key"
+            />
+          </div>
+        </div>
+        <div class="mount-bottom" v-if="calDelete">
+          <el-button type="danger" plain @click="emit('delete')" class="del-btn">
+            删除
+          </el-button>
+        </div>
+      </div>
+    </transition>
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from "vue";
+import { useMode } from "../../hook/use-status.ts";
+import { PropertyDescribes, propertyComponents } from "./index.ts";
+import { DomOutMountId } from "@/constant/index.ts";
+import { Mode } from "@/constant/mode.ts";
+import { debounce } from "@/utils/shared.ts";
+import { ElButton } from "element-plus";
+
+const props = defineProps<{
+  show?: boolean;
+  name?: string;
+  data?: Record<string, any>;
+  describes: PropertyDescribes;
+  calDelete?: boolean;
+}>();
+const emit = defineEmits<{
+  (e: "change"): void;
+  (e: "delete"): void;
+  (e: "close"): void;
+}>();
+
+const describeItems = computed(() => {
+  return Object.entries(props.describes).sort(
+    ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
+  );
+});
+
+const mode = useMode();
+const hidden = computed(
+  () => !props.show || mode.value.has(Mode.draw) || mode.value.has(Mode.draging)
+);
+const show = ref(false);
+watch(
+  hidden,
+  debounce(() => {
+    show.value = !hidden.value;
+  }, 32),
+  { immediate: true }
+);
+
+let isUpdate = false;
+const updateValue = (key: string, val: any) => {
+  if ("value" in props.describes[key]) {
+    props.describes[key].value = val;
+  } else {
+    props.data![key] = val;
+  }
+  isUpdate = true;
+};
+
+watch(hidden, (nHidden, oHidden) => {
+  if (nHidden && nHidden !== oHidden && isUpdate) {
+    isUpdate = false;
+    emit("change");
+    nHidden && emit("close");
+  }
+});
+
+const changeHandler = () => {
+  isUpdate = false;
+  emit("change");
+};
+</script>
+
+<style lang="scss" scoped>
+.mount-layout {
+  pointer-events: all;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  border-left: 1px solid #e6e6e6;
+  background-color: rgba(255, 255, 255, 1);
+  overflow-y: auto;
+  padding: 6px 16px;
+  color: #333333;
+  width: 280px;
+  font-size: 14px;
+  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
+  overflow: hidden;
+
+  .mount-controller {
+    border-bottom: 1px solid #f0f0f0;
+    margin-bottom: 24px;
+
+    .mount-item {
+      margin-bottom: 24px;
+
+      &.column {
+        .label {
+          display: block;
+          margin-bottom: 8px;
+        }
+      }
+
+      &.row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        justify-content: space-between;
+      }
+    }
+  }
+}
+
+.title {
+  display: flex;
+  font-size: 16px;
+  padding: 19px 0 25px;
+  align-items: center;
+  justify-content: space-between;
+
+  h3 {
+    font-size: inherit;
+    color: #333333;
+    align-items: center;
+  }
+}
+
+.mount-fade-enter-active,
+.mount-fade-leave-active {
+  transition: transform 0.3s ease, opacity 0.3s ease;
+}
+
+.mount-fade-enter-from,
+.mount-fade-leave-to {
+  transform: translateX(100%);
+  opacity: 0;
+}
+
+.del-btn {
+  width: 100%;
+  font-size: 14px;
+  height: 34px;
+  background: none;
+  color: var(--el-color-danger);
+}
+</style>

+ 20 - 181
src/core/html-mount/propertys/mount.vue

@@ -1,70 +1,35 @@
 <template>
-  <Teleport :to="`#${DomOutMountId}`" v-if="stage">
-    <transition name="mount-fade">
-      <div class="mount-layout" v-if="show">
-        <div
-          v-if="name || data?.itemName || (type && components[type].shapeName)"
-          class="title"
-        >
-          <h4>
-            设置{{ name || data?.itemName || (type && components[type].shapeName) }}
-          </h4>
-          <icon
-            name="close"
-            size="20px"
-            class="operate"
-            @click="shapesStatus.actives = []"
-          />
-        </div>
-        <div :size="8" class="mount-controller">
-          <div
-            v-for="[key, val] in describeItems"
-            :key="key"
-            class="mount-item"
-            :class="val['layout-type'] || 'column'"
-          >
-            <span class="label">{{ val.label }}</span>
-            <component
-              :data="data"
-              v-bind="{ ...(describes[key].props || {}), ...getPredefine(key) }"
-              :value="
-                'value' in describes[key]
-                  ? describes[key].value
-                  : props.data && props.data[key]
-              "
-              @update:value="(val: any) => updateValue(key, val)"
-              @change="changeHandler"
-              :is="propertyComponents[val.type]"
-              :key="key"
-            />
-          </div>
-        </div>
-        <div class="mount-bottom" v-if="!data?.disableDelete">
-          <el-button type="danger" plain @click="emit('delete')" class="del-btn">
-            删除
-          </el-button>
-        </div>
-      </div>
-    </transition>
-  </Teleport>
+  <MountDescribes
+    :name="name || data?.itemName || (type && components[type].shapeName)"
+    :show="!hidden"
+    :describes="describes"
+    :data="data"
+    :cal-delete="!data?.disableDelete"
+    @close="
+      () => {
+        shapesStatus.actives = [];
+        emit('close');
+      }
+    "
+    @change="emit('change')"
+    @delete="emit('delete')"
+  />
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watch } from "vue";
-import { useMountMenusFilter, useStage } from "../../hook/use-global-vars.ts";
+import { computed } from "vue";
+import { useStage } from "../../hook/use-global-vars.ts";
 import { useMode } from "../../hook/use-status.ts";
-import { PropertyDescribes, propertyComponents } from "./index.ts";
+import { PropertyDescribes } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import {
   useMouseShapesStatus,
   useMouseShapeStatus,
 } from "../../hook/use-mouse-status.ts";
-import { DomOutMountId } from "@/constant/index.ts";
 import { Mode } from "@/constant/mode.ts";
-import { debounce } from "@/utils/shared.ts";
 import { useStore } from "@/core/store/index.ts";
-import { components, DrawItem } from "@/core/components/index.ts";
-import { ElButton } from "element-plus";
+import { components } from "@/core/components/index.ts";
+import MountDescribes from "./mount-describes.vue";
 
 const props = defineProps<{
   show?: boolean;
@@ -84,27 +49,6 @@ const id = computed(() => props.target?.getNode().id());
 const type = computed(() => id.value && store.getType(id.value));
 const data = computed(() => (id.value ? store.getItemById(id.value) : props.data));
 
-const getPredefine = (key: string) => {
-  if (!type.value) return;
-  const getPredefine = components[type.value].getPredefine;
-  const predefine = getPredefine && (getPredefine as any)(key as keyof DrawItem);
-  return predefine || {};
-};
-const { getFilter } = useMountMenusFilter();
-const describes = computed(() => {
-  if (type.value && id.value) {
-    return getFilter(type.value, id.value)(props.describes);
-  } else {
-    return props.describes;
-  }
-});
-
-const describeItems = computed(() => {
-  return Object.entries(describes.value).sort(
-    ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
-  );
-});
-
 const stage = useStage();
 const status = useMouseShapeStatus(computed(() => props.target));
 const shapesStatus = useMouseShapesStatus();
@@ -118,109 +62,4 @@ const hidden = computed(
       mode.value.has(Mode.draw) ||
       mode.value.has(Mode.draging))
 );
-const show = ref(false);
-watch(
-  hidden,
-  debounce(() => {
-    show.value = !hidden.value;
-  }, 32)
-);
-
-let isUpdate = false;
-const updateValue = (key: string, val: any) => {
-  if ("value" in describes.value[key]) {
-    describes.value[key].value = val;
-  } else {
-    props.data![key] = val;
-  }
-  isUpdate = true;
-};
-
-watch(hidden, (nHidden, oHidden) => {
-  if (nHidden && nHidden !== oHidden && isUpdate) {
-    isUpdate = false;
-    emit("change");
-  }
-});
-
-const changeHandler = () => {
-  isUpdate = false;
-  emit("change");
-  console.log("change handler");
-};
 </script>
-
-<style lang="scss" scoped>
-.mount-layout {
-  pointer-events: all;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  position: absolute;
-  border-left: 1px solid #e6e6e6;
-  background-color: rgba(255, 255, 255, 1);
-  overflow-y: auto;
-  padding: 6px 16px;
-  color: #333333;
-  width: 280px;
-  font-size: 14px;
-  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
-  overflow: hidden;
-
-  .mount-controller {
-    border-bottom: 1px solid #f0f0f0;
-    margin-bottom: 24px;
-
-    .mount-item {
-      margin-bottom: 24px;
-
-      &.column {
-        .label {
-          display: block;
-          margin-bottom: 8px;
-        }
-      }
-
-      &.row {
-        display: flex;
-        flex-wrap: wrap;
-        align-items: center;
-        justify-content: space-between;
-      }
-    }
-  }
-}
-
-.title {
-  display: flex;
-  font-size: 16px;
-  padding: 19px 0 25px;
-  align-items: center;
-  justify-content: space-between;
-
-  h3 {
-    font-size: inherit;
-    color: #333333;
-    align-items: center;
-  }
-}
-
-.mount-fade-enter-active,
-.mount-fade-leave-active {
-  transition: transform 0.3s ease, opacity 0.3s ease;
-}
-
-.mount-fade-enter-from,
-.mount-fade-leave-to {
-  transform: translateX(100%);
-  opacity: 0;
-}
-
-.del-btn {
-  width: 100%;
-  font-size: 14px;
-  height: 34px;
-  background: none;
-  color: var(--el-color-danger);
-}
-</style>

+ 77 - 0
src/core/renderer-three/components/icon/index.vue

@@ -0,0 +1,77 @@
+<template></template>
+
+<script lang="ts" setup>
+import { useRender, useStageProps, useTree } from "../../hook/use-stage";
+import { getLevel, getModel } from "../resource";
+import { Box3, Group, MathUtils, Matrix4, Vector3 } from "three";
+import { computed, shallowRef, watch } from "vue";
+import { setMat } from "../../util";
+import { IconData } from "@/core/components/icon";
+import { Transform } from "konva/lib/Util";
+
+const props = defineProps<{ data: IconData }>();
+const render = useRender();
+
+const group = new Group();
+const size = shallowRef<Vector3>();
+watch(
+  () => props.data.url,
+  async (type, _, onCleanup) => {
+    let typeModel = await getModel(type);
+    if (typeModel && type === props.data.url) {
+      typeModel = typeModel.clone();
+      size.value = new Box3().setFromObject(typeModel).getSize(new Vector3());
+      group.add(typeModel);
+      render();
+      onCleanup(() => {
+        size.value = undefined;
+        group.remove(typeModel!);
+        render();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const sProps = useStageProps();
+const level = computed(() => getLevel(props.data.url, sProps.value.height));
+const mat = computed(() => {
+  let height = level.value.height;
+  let bottom = level.value.bottom || 0;
+
+  const data = props.data;
+  const dec = new Transform(data.mat).decompose();
+  if (!height) {
+    height = 30;
+    if (size.value) {
+      const widthScale = (data.width * dec.scaleX) / size.value.x;
+      const thicknessScale = (data.height * dec.scaleY) / size.value.z;
+      if (widthScale > thicknessScale) {
+        height = widthScale * size.value.y;
+      } else {
+        height = thicknessScale * size.value.y;
+      }
+      height = Math.min(height, 90);
+    }
+  }
+
+  const mat = new Matrix4()
+    .makeTranslation(dec.x, bottom, dec.y)
+    .multiply(new Matrix4().makeRotationY(-MathUtils.degToRad(dec.rotation)))
+    .multiply(new Matrix4().makeScale(dec.scaleX, 1, dec.scaleY))
+    .multiply(new Matrix4().makeScale(props.data.width, height, props.data.height))
+    .multiply(new Matrix4().makeTranslation(0, 0.5, 0));
+  return mat;
+});
+
+watch(
+  mat,
+  (mat) => {
+    setMat(group, mat);
+    render();
+  },
+  { immediate: true }
+);
+
+useTree().value = group;
+</script>

+ 14 - 0
src/core/renderer-three/components/index.ts

@@ -0,0 +1,14 @@
+import { ShapeType } from "@/core/components";
+import Line from './line/index.vue'
+import LineIcon from './line-icon/index.vue'
+import Icon from './icon/index.vue'
+import Text from './text/index.vue'
+
+export const components: {
+  [key in ShapeType]?: (data: {data: any}) => void
+} = {}
+
+components.line = Line as any
+components.lineIcon = LineIcon as any
+components.icon = Icon as any
+components.text = Text as any

+ 126 - 0
src/core/renderer-three/components/line-icon/index.vue

@@ -0,0 +1,126 @@
+<template></template>
+
+<script lang="ts" setup>
+import {
+  getLineIconEndpoints,
+  getSnapLine,
+  LineIconData,
+} from "@/core/components/line-icon";
+import { useRender, useStageProps, useTree } from "../../hook/use-stage";
+import { fullMesh, getLevel, getModel } from "../resource";
+import { Group, Matrix4 } from "three";
+import { computed, ref, watch, watchEffect } from "vue";
+import { lineCenter, lineVector, vector2IncludedAngle } from "@/utils/math";
+import { setMat } from "../../util";
+
+const props = defineProps<{ data: LineIconData }>();
+const render = useRender();
+
+const group = new Group();
+watch(
+  () => props.data.url,
+  async (type, _, onCleanup) => {
+    let typeModel = await getModel(type);
+    if (typeModel && type === props.data.url) {
+      typeModel = typeModel.clone();
+      group.add(typeModel);
+      render();
+      onCleanup(() => {
+        group.remove(typeModel!);
+        render();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const store = useStageProps().value.draw.store;
+const line = computed(
+  () => store.getTypeItems("line")[0].lines.find((item) => item.id === props.data.lineId)!
+);
+const fullThickness = ["men_l", "yimen", "shuangkaimen", "luodichuang"];
+
+const level = computed(() => getLevel(props.data.url, sProps.value.height));
+const height = computed(() =>
+  level.value.height ? level.value.height : sProps.value.height / 2
+);
+const btHeight = computed(() => {
+  let bot = 0;
+  if ("bottom" in level.value) {
+    bot = level.value.bottom!;
+  } else if ("top" in level.value) {
+    bot = sProps.value.height - height.value - level.value.top!;
+  } else {
+    bot = (sProps.value.height - height.value) / 2;
+  }
+  return {
+    bottom: bot,
+    top: sProps.value.height - height.value - bot,
+  };
+});
+const bottom = computed(() => height.value / 2 + btHeight.value.bottom);
+
+const thickness = computed(() => {
+  const isFullThickness = fullThickness.some((t) => props.data.url.includes(t));
+  return isFullThickness ? line.value.strokeWidth : props.data.height;
+});
+const sProps = useStageProps();
+const mat = computed(() => {
+  const data = props.data;
+  const snapLine = getSnapLine(store, data)!;
+  const points = getLineIconEndpoints(snapLine, data);
+  const lineRotate = vector2IncludedAngle(lineVector(points), { x: 1, y: 0 });
+  const center = lineCenter(points);
+
+  const width = Math.abs(props.data.endLen - props.data.startLen);
+  const mat = new Matrix4()
+    .makeTranslation(center.x, bottom.value, center.y)
+    .multiply(new Matrix4().makeRotationY(lineRotate));
+
+  if (props.data.openSide === "RIGHT") {
+    mat.multiply(new Matrix4().makeScale(1, 1, -1));
+  }
+  if (line.value.strokeWidth !== thickness.value) {
+    const outer = -thickness.value / 2 + line.value.strokeWidth / 2;
+    mat.multiply(new Matrix4().makeTranslation(0, 0, outer));
+  }
+
+  mat.multiply(new Matrix4().makeScale(width, height.value, thickness.value));
+  return mat;
+});
+
+watchEffect((onCleanup) => {
+  if (height.value === sProps.value.height) {
+    return;
+  }
+  const topMesh = fullMesh.clone();
+  const topScale = btHeight.value.top / height.value;
+  topMesh.scale.set(1, topScale, 1);
+  topMesh.position.add({ x: 0, y: 0.5 + topScale / 2, z: 0 });
+
+  const bottomMesh = fullMesh.clone();
+  const bottomScale = btHeight.value.bottom / height.value;
+  bottomMesh.scale.set(1, bottomScale, 1);
+  bottomMesh.position.add({ x: 0, y: -0.5 - bottomScale / 2, z: 0 });
+
+  group.add(topMesh);
+  group.add(bottomMesh);
+  render();
+  onCleanup(() => {
+    group.remove(topMesh);
+    group.remove(bottomMesh);
+    render();
+  });
+});
+
+watch(
+  mat,
+  (mat) => {
+    setMat(group, mat);
+    render();
+  },
+  { immediate: true }
+);
+
+useTree().value = group;
+</script>

+ 96 - 0
src/core/renderer-three/components/line.vue

@@ -0,0 +1,96 @@
+<template></template>
+
+<script lang="ts" setup>
+import { LineData } from "@/core/components/line";
+import { getLinePoints } from "@/core/components/line/attach-server";
+import {
+  BoxGeometry,
+  Color,
+  DoubleSide,
+  Group,
+  Matrix4,
+  Mesh,
+  MeshPhongMaterial,
+  Vector3,
+} from "three";
+import { computed, ref, watch, watchEffect } from "vue";
+import { useRender, useTree } from "../hook/use-stage";
+import { copy, diffArrayChange, mergeFuns } from "@/utils/shared";
+import { eqPoint, lineCenter, lineLen, lineVector, vectorAngle } from "@/utils/math";
+
+const props = defineProps<{ data: LineData }>();
+const render = useRender();
+const group = new Group();
+
+const destorys: Record<string, () => void> = {};
+const addLine = (id: string) => {
+  const line = computed(() => props.data.lines.find((item) => item.id === id)!);
+  const points = ref(copy(getLinePoints(props.data, line.value)));
+  const width = computed(() => lineLen(points.value[0], points.value[1]));
+
+  const geometry = computed(
+    () => new BoxGeometry(width.value, 100, line.value.strokeWidth)
+  );
+
+  const material = computed(() => {
+    const color = new Color(line.value.stroke);
+    return new MeshPhongMaterial({ color: color, side: DoubleSide });
+  });
+  const mesh = computed(() => {
+    const mesh = new Mesh(geometry.value, material.value);
+    mesh.castShadow = true;
+    mesh.receiveShadow = true;
+    return mesh;
+  });
+
+  const cleanups = [
+    watchEffect(() => {
+      const _points = getLinePoints(props.data, line.value);
+      if (
+        !eqPoint(points.value[0], _points[0]) ||
+        !eqPoint(points.value[1], _points[1])
+      ) {
+        points.value = copy(_points);
+      }
+    }),
+    watchEffect((onCleanup) => {
+      group.add(mesh.value);
+      watchEffect(() => {
+        const center = lineCenter(points.value);
+        const mat = new Matrix4().multiplyMatrices(
+          new Matrix4().makeTranslation(new Vector3(center.x, 50.5, center.y)),
+          new Matrix4().makeRotationY(vectorAngle(lineVector(points.value)))
+        );
+        mesh.value.matrixAutoUpdate = false;
+        mesh.value.matrix.copy(mat);
+        mesh.value.matrixWorldNeedsUpdate = true;
+        render();
+      });
+      onCleanup(() => {
+        group.remove(mesh.value);
+        render();
+      });
+    }),
+  ];
+
+  destorys[line.value.id] = () => {
+    mergeFuns(cleanups)();
+  };
+};
+
+const delLine = (id: string) => {
+  destorys[id]?.();
+};
+
+watch(
+  () => props.data.lines.map((item) => item.id),
+  (newLines, oldLines = []) => {
+    const { added, deleted } = diffArrayChange(newLines, oldLines);
+    deleted.forEach(delLine);
+    added.forEach(addLine);
+  },
+  { immediate: true }
+);
+
+useTree().value = group;
+</script>

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

@@ -0,0 +1,23 @@
+<template>
+  <Line
+    v-for="attr in data.lines"
+    :data="data"
+    :line="attr"
+    :key="attr.id"
+    :getExtendPolygon="getExtendPolygon"
+  />
+</template>
+
+<script lang="ts" setup>
+import { LineData } from "@/core/components/line";
+import Line from "./single-line.vue";
+import { computed } from "vue";
+import { useDrawHook } from "../../hook/use-stage";
+import { useGetExtendPolygon } from "@/core/components/line/renderer/wall/view";
+
+const props = defineProps<{ data: LineData }>();
+
+const getExtendPolygon = useDrawHook(() =>
+  useGetExtendPolygon(computed(() => props.data))
+);
+</script>

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

@@ -0,0 +1,25 @@
+import { MeshPhongMaterial, MeshPhongMaterialParameters, ShaderLib, UniformsUtils } from "three";
+
+export class StablePhongMaterial extends MeshPhongMaterial {
+  uniforms = {
+    ...UniformsUtils.clone(ShaderLib.phong.uniforms),
+    objectId: { value: 0 }
+  }
+  
+  constructor(options?: MeshPhongMaterialParameters) {
+    super(options)
+
+    // this.onBeforeCompile = shader => {
+    //   shader.uniforms.objectId = this.uniforms.objectId
+    //   shader.vertexShader = `
+    //     uniform float objectId;
+    //     ${shader.vertexShader}
+    //   `.replace(`#include <project_vertex>`, `
+    //       #include <project_vertex>
+    //       gl_Position.z += objectId * 0.0001;
+    //   `);
+
+    //   this.userData.shader = shader
+    // }
+  }
+}

+ 95 - 0
src/core/renderer-three/components/line/single-line.vue

@@ -0,0 +1,95 @@
+<template></template>
+
+<script lang="ts" setup>
+import { LineData, LineDataLine } from "@/core/components/line";
+import { getLinePoints } from "@/core/components/line/attach-server";
+import {
+  BufferGeometry,
+  Color,
+  DoubleSide,
+  ExtrudeGeometry,
+  Mesh,
+  MeshPhongMaterial,
+  Shape,
+} from "three";
+import { computed, onUnmounted, Ref, ref, watch, watchEffect } from "vue";
+import { useDrawHook, useRender, useStageProps, useTree } from "../../hook/use-stage";
+import { debounce } from "@/utils/shared";
+import {
+  useGetDiffLineIconPolygons,
+  useGetExtendPolygon,
+} from "@/core/components/line/renderer/wall/view";
+import { BufferGeometryUtils } from "three/examples/jsm/Addons.js";
+import { StablePhongMaterial } from "./material";
+
+const props = defineProps<{
+  line: LineDataLine;
+  data: LineData;
+  getExtendPolygon: ReturnType<typeof useGetExtendPolygon>;
+}>();
+
+const polygon = computed(() => props.getExtendPolygon(props.line));
+const points = computed(() => getLinePoints(props.data, props.line));
+const gd = useDrawHook(() => useGetDiffLineIconPolygons(props.line, points));
+const polygons = computed(() => gd.diff(polygon.value));
+const geometry = ref() as Ref<BufferGeometry>;
+const sProps = useStageProps();
+
+watch(
+  polygons,
+  debounce(() => {
+    if (geometry.value) {
+      geometry.value.dispose();
+    }
+    const polyGeos = polygons.value.map((poly) => {
+      const shape = new Shape();
+      shape.moveTo(poly[0].x, poly[0].y);
+      for (let i = 1; i < poly.length; i++) {
+        shape.lineTo(poly[i].x, poly[i].y);
+      }
+      shape.lineTo(poly[poly.length - 1].x, poly[poly.length - 1].y);
+      const geo = new ExtrudeGeometry(shape, {
+        depth: sProps.value.height,
+        bevelEnabled: false,
+        steps: 1,
+      });
+      return geo;
+    });
+
+    geometry.value = BufferGeometryUtils.mergeGeometries(polyGeos);
+    geometry.value.rotateX(Math.PI / 2).translate(0, sProps.value.height, 0);
+    polyGeos.forEach((geo) => geo.dispose());
+  }),
+  { immediate: true }
+);
+
+const material = new StablePhongMaterial({ side: DoubleSide });
+material.uniforms.objectId.value = props.data.lines.indexOf(props.line);
+
+const render = useRender();
+watchEffect(() => {
+  // material.color = new Color(props.line.stroke);
+  material.color = new Color(0xffffff);
+  render();
+});
+
+const mesh = new Mesh(undefined, material);
+mesh.castShadow = true;
+mesh.receiveShadow = true;
+watchEffect(() => {
+  mesh.geometry = geometry.value;
+  render();
+});
+
+onUnmounted(() => {
+  material.dispose();
+  mesh.geometry?.dispose();
+});
+
+const tree = useTree();
+watchEffect(() => {
+  if (geometry.value) {
+    tree.value = mesh;
+  }
+});
+</script>

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

@@ -0,0 +1,476 @@
+import {
+  Box3,
+  BoxGeometry,
+  Color,
+  DirectionalLight,
+  DoubleSide,
+  Mesh,
+  MeshPhongMaterial,
+  MeshPhysicalMaterial,
+  MeshStandardMaterial,
+  Object3D,
+  Vector3,
+} from "three";
+import { GLTFLoader } from "three/examples/jsm/Addons.js";
+
+const gltfLoader = new GLTFLoader().setPath("/static/models/");
+
+const normalized = async (model: Object3D, pub = true) => {
+  const parent = new Object3D();
+  parent.add(model);
+
+  const bbox = new Box3().setFromObject(parent);
+  const size = bbox.getSize(new Vector3());
+  if (pub) {
+    parent.scale.set(1 / size.x, 1 / size.y, 1 / size.z);
+  } else {
+    const min = Math.max(size.x, size.y, size.z);
+    parent.scale.set(1 / min, 1 / min, 1 / min);
+  }
+  model.traverse((child: any) => {
+    if (child.isMesh) {
+      child.receiveShadow = true;
+      child.castShadow = true;
+    }
+  });
+
+  const center = new Box3().setFromObject(parent).getCenter(new Vector3());
+  parent.position.sub({ x: center.x, y: center.y, z: center.z });
+
+  return parent;
+};
+
+const resources: Record<string, () => Promise<Object3D>> = {
+  "men_l.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("door_with_frame/scene.gltf");
+    gltf.scene.rotateY(Math.PI);
+    gltf.scene.scale.setX(-1);
+    return await normalized(gltf.scene);
+  },
+  "piaochuang.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("window_1/scene.gltf");
+    gltf.scene.rotateY(Math.PI);
+    gltf.scene.traverse((node: any) => {
+      if (!node.isMesh) return;
+      if (node.name.includes("Object")) {
+        node.material = new MeshPhysicalMaterial({
+          color: 0xffffff, // 浅灰色(可根据需求调整,如0xcccccc)
+          metalness: 0.1, // 轻微金属感(增强反射)
+          roughness: 0.01, // 表面光滑度(0-1,越小越光滑)
+          transmission: 1, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
+          opacity: 1, // 透明度(与transmission配合使用)
+          transparent: true, // 启用透明
+          side: DoubleSide, // 双面渲染(玻璃通常需要)
+          ior: 0, // 折射率(玻璃约为1.5)
+          clearcoat: 0.5, // 可选:表面清漆层(增强反光)
+        });
+      } else if (node.name.includes("_111111_white_plastic")) {
+        node.material = new MeshStandardMaterial({
+          color: 0xffffff, // 浅灰色
+          metalness: 0.9, // 高金属度
+          roughness: 0.3, // 中等粗糙度
+          side: DoubleSide,
+        });
+      } else if (
+        node.name.includes("_111111_seam_0") ||
+        node.name.includes("_111111__15_0") ||
+        node.name.includes("_111111_Aluminium_profile_0")
+      ) {
+        node.material = new MeshStandardMaterial({
+          color: 0xffffff,
+          metalness: 0.8,
+          roughness: 0.4,
+          aoMapIntensity: 1.0,
+          side: DoubleSide,
+        });
+      } else {
+        node.material = new MeshPhongMaterial({
+          side: DoubleSide,
+          color: 0xffffff,
+        });
+      }
+    });
+
+    const model = await normalized(gltf.scene);
+    model.scale.add({ x: 0.00015, y: 0.0001, z: 0 });
+    model.position.add({ x: -0.01, y: -0.005, z: 0.02 });
+    return model;
+  },
+  "chuang.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("window (3)/scene.gltf");
+
+    return await normalized(gltf.scene);
+  },
+  "yimen.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("sliding_door/scene.gltf");
+    return await normalized(gltf.scene);
+  },
+  "shuangkaimen.svg": async () => {
+    const gltf = await gltfLoader.loadAsync(
+      "white_double_windowed_door/scene.gltf"
+    );
+    return await normalized(gltf.scene);
+  },
+  "luodichuang.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("window2/scene.gltf");
+    gltf.scene.traverse((node: any) => {
+      if (node.name?.includes("glass_0")) {
+        node.material = new MeshPhysicalMaterial({
+          color: 0xffffff, // 浅灰色(可根据需求调整,如0xcccccc)
+          metalness: 0.1, // 轻微金属感(增强反射)
+          roughness: 0.01, // 表面光滑度(0-1,越小越光滑)
+          transmission: 1, // 透光率(模拟玻璃透光,需环境光遮蔽和光源支持)
+          opacity: 1, // 透明度(与transmission配合使用)
+          transparent: true, // 启用透明
+          side: DoubleSide, // 双面渲染(玻璃通常需要)
+          ior: 0, // 折射率(玻璃约为1.5)
+          clearcoat: 0.5, // 可选:表面清漆层(增强反光)
+        });
+      }
+    });
+    return await normalized(gltf.scene);
+  },
+  "DoubleBed.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("bed/scene.gltf");
+
+    const models: Object3D[] = [];
+    const delModelName = ["Pillow_2002", "Plane002"];
+    gltf.scene.traverse((child: any) => {
+      if (delModelName.some((n) => n === child.name)) {
+        models.push(child);
+      }
+    });
+    models.forEach((m) => m.parent?.remove(m));
+
+    const model = await normalized(gltf.scene);
+    model.position.setY(model.position.y - 0.131);
+    return model;
+  },
+  "SingleBed.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("woodbed/scene.gltf");
+    const model = await normalized(gltf.scene);
+    model.rotateY(Math.PI / 2);
+    return model;
+  },
+  sf: async () => {
+    const gltf = await gltfLoader.loadAsync(
+      "sofa_set_-_4_type_of_sofa_lowpoly./scene.gltf"
+    );
+    return gltf.scene;
+  },
+  "ThreeSofa.svg": async () => {
+    const gltf = await gltfLoader.loadAsync(
+      "sofa_-_game_ready_model/scene.gltf"
+    );
+    const model = await normalized(gltf.scene, undefined);
+
+    model.traverse((child: any) => {
+      if (child.isMesh) {
+        child.material.color = new Color(0x444444);
+      }
+    });
+
+    return model;
+  },
+  "SingleSofa.svg": async () => {
+    const scene = (await getModel("sf"))!;
+    const models: Object3D[] = [];
+    const pickModelName = ["Cube026"];
+    scene.traverse((child: any) => {
+      if (pickModelName.some((n) => n === child.name)) {
+        models.push(child);
+      }
+    });
+    const model = new Object3D().add(...models.map((item) => item.clone()));
+    model.rotateY(Math.PI / 2);
+    return await normalized(model);
+  },
+  "Desk.svg": async () => {
+    const scene = (await getModel("sf"))!;
+    const models: Object3D[] = [];
+    const pickModelName = ["Cube004"];
+    scene.traverse((child: any) => {
+      if (pickModelName.some((n) => n === child.name)) {
+        models.push(child);
+      }
+    });
+    const model = new Object3D().add(...models.map((item) => item.clone()));
+    model.rotateY(Math.PI / 2);
+    return await normalized(model);
+  },
+  "TeaTable.svg": async () => {
+    return (await getModel("Desk.svg"))!.clone();
+  },
+  "DiningTable.svg": async () => {
+    const desk = new Object3D().add((await getModel("Desk.svg"))!.clone());
+    const chair = (await getModel("Chair.svg"))!;
+    const model = new Object3D();
+
+    const lt = chair.clone();
+    lt.position.set(-0.14, -0.5, 0.25);
+    lt.scale.set(0.5, 1.2, 0.8);
+    lt.rotateY(Math.PI);
+    model.add(lt);
+
+    const rt = chair.clone();
+    rt.position.set(0.14, -0.5, 0.25);
+    rt.scale.set(0.5, 1.2, 0.8);
+    rt.rotateY(Math.PI);
+    model.add(rt);
+
+    const lb = chair.clone();
+    lb.position.set(-0.14, -0.5, -0.25);
+    lb.scale.set(0.5, 1.2, 0.8);
+    model.add(lb);
+
+    const rb = chair.clone();
+    rb.position.set(0.14, -0.5, -0.25);
+    rb.scale.set(0.5, 1.2, 0.8);
+    model.add(rb);
+
+    desk.scale.set(1.2, 1, 0.55);
+    model.add(desk);
+
+    const nModel = await normalized(model);
+    nModel.position.sub({ x: 0, y: 0.075, z: 0 });
+    return nModel;
+  },
+  "Chair.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("psx_chair/scene.gltf");
+    const model = await normalized(gltf.scene, undefined);
+    model.scale.add({ x: 0, y: 0.3, z: 0 });
+    return model;
+  },
+  "TV.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("tv_and_tv_stand/scene.gltf");
+    const model = await normalized(gltf.scene, undefined);
+    return model;
+  },
+  "Plant.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("pothos_plant/scene.gltf");
+    const model = await normalized(gltf.scene, undefined);
+    return model;
+  },
+  "Washstand.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("washbasin/scene.gltf");
+    gltf.scene.rotateY(Math.PI);
+    const model = await normalized(gltf.scene, undefined);
+    return model;
+  },
+  "Closestool.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("toilet/scene.gltf");
+    const model = await normalized(gltf.scene, undefined);
+    model.traverse((child: any) => {
+      if (child.isMesh) {
+        child.material.color = new Color(0xffffff);
+      }
+    });
+    return model;
+  },
+  "Wardrobe.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("wardrobe_14722-22/scene.gltf");
+    const model = await normalized(gltf.scene, undefined);
+    model.traverse((child: any) => {
+      if (child.isMesh) {
+        child.material.color = new Color(0xcbc3b3);
+      }
+    });
+    return model;
+  },
+  "BedsideCupboard.svg": async () => {
+    const gltf = await gltfLoader.loadAsync(
+      "low_poly_bedside_table/scene.gltf"
+    );
+    const model = await normalized(gltf.scene, undefined);
+    model.traverse((child: any) => {
+      if (child.isMesh) {
+        child.material.color = new Color(0xffffff);
+      }
+    });
+    return model;
+  },
+  "CombinationSofa.svg": async () => {
+    const tsofa = (await getModel("ThreeSofa.svg"))!.clone();
+    const ssofa = (await getModel("SingleSofa.svg"))!.clone();
+    const tea = (await getModel("TeaTable.svg"))!.clone();
+    const model = new Object3D();
+
+    // tsofa.rotateY(-Math.PI / 2)
+    tsofa.scale.multiply({ x: 0.8, y: 1, z: 0.4 });
+    tsofa.position.add({ x: -0, y: 0, z: -0.6 });
+    model.add(tsofa);
+
+    ssofa.rotateY(-Math.PI / 2);
+    ssofa.scale.multiply({ x: 0.4, y: 1, z: 0.4 });
+    ssofa.position.add({ x: -0.15, y: 0, z: -2.2 });
+    model.add(ssofa);
+
+    tea.scale.multiply({ x: 0.8, y: 0.5, z: 0.4 });
+    tea.position.add({ x: -0, y: -0.13, z: 0 });
+    model.add(tea);
+    return normalized(model);
+  },
+  kitchen: async () => {
+    const gltf = await gltfLoader.loadAsync(
+      "basic_kitchen_cabinets_and_counter/scene.gltf"
+    );
+    gltf.scene.rotateY(-Math.PI);
+    return gltf.scene;
+  },
+  "Cupboard.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("kitchen_cabinets (1)/scene.gltf");
+
+    gltf.scene.rotateY(Math.PI / 2);
+    const model = await normalized(gltf.scene);
+    model.traverse((child: any) => {
+      if (
+        child.isMesh &&
+        ["pCube1_cor_0", "pCube8_cor_0"].includes(child.name)
+      ) {
+        child.material.color = new Color(0xffffff);
+      }
+    });
+
+    return model;
+  },
+  "GasStove.svg": async () => {
+    const gltf = await gltfLoader.loadAsync("burner_gas_stove/scene.gltf");
+    const model = await normalized(gltf.scene);
+    model.traverse((child: any) => {
+      if (child.isMesh) {
+        child.material.emissive = new Color(0x222222)
+      }
+    });
+    return model;
+  },
+};
+
+export const levelResources: Record<
+  string,
+  {
+    bottom?: number | string;
+    height?: number | string | "full";
+    top?: number | string;
+  }
+> = {
+  "SingleBed.svg": {
+    height: 70,
+  },
+  "ThreeSofa.svg": {
+    height: 90,
+  },
+  "SingleSofa.svg": {
+    height: 90,
+  },
+  "CombinationSofa.svg": {
+    height: 90,
+  },
+  "Desk.svg": {
+    height: 80,
+  },
+  "TeaTable.svg": {
+    height: 50,
+  },
+  "DiningTable.svg": {
+    height: 100,
+  },
+  "Chair.svg": {
+    height: 80,
+  },
+  "TV.svg": {
+    height: 120,
+  },
+  "Washstand.svg": {
+    height: 100,
+  },
+  "Closestool.svg": {
+    height: 45,
+  },
+  "Wardrobe.svg": {
+    height: "full",
+  },
+  "BedsideCupboard.svg": {
+    height: 50,
+  },
+  "piaochuang.svg": {
+    top: 4,
+    bottom: 40,
+  },
+  "men_l.svg": {
+    height: "full",
+  },
+  "yimen.svg": {
+    height: "full",
+  },
+  "shuangkaimen.svg": {
+    height: "full",
+  },
+  "luodichuang.svg": {
+    height: "full",
+  },
+  "Cupboard.svg": {
+    height: "full",
+  },
+  "GasStove.svg": {
+    height: 10,
+    bottom: "0.335",
+  },
+};
+
+export const getLevel = (type: string, fullHeight: number) => {
+  const ndx = type.lastIndexOf("/");
+  if (~ndx) {
+    type = type.substring(ndx + 1);
+  }
+  const transform = (data: any): Record<string, number> => {
+    const tdata: Record<string, number> = {};
+    for (const key of Object.keys(data)) {
+      if (data[key] === "full") {
+        tdata[key] = fullHeight;
+      } else if (typeof data[key] === "string") {
+        tdata[key] = parseFloat(data[key]) * fullHeight;
+      } else {
+        tdata[key] = data[key];
+      }
+    }
+    return tdata;
+  };
+  if (!levelResources[type]) {
+    return {};
+  }
+
+  const data = transform(levelResources[type]);
+  if (!data.height && "top" in data && "bottom" in data) {
+    data.height = fullHeight - data.top - data.bottom;
+  }
+
+  return data;
+};
+
+export const getModel = (() => {
+  const typeModels: Record<string, Promise<Object3D | undefined>> = {};
+
+  return (type: string) => {
+    const ndx = type.lastIndexOf("/");
+    if (~ndx) {
+      type = type.substring(ndx + 1);
+    }
+
+    if (type in typeModels) {
+      return typeModels[type];
+    }
+    if (type in resources) {
+      typeModels[type] = resources[type]();
+      typeModels[type].catch(() => {
+        delete typeModels[type];
+      });
+      return typeModels[type];
+    }
+  };
+})();
+
+export const fullMesh = new Mesh(
+  new BoxGeometry(1, 1, 1),
+  new MeshPhongMaterial({ color: 0xffffff })
+);
+fullMesh.receiveShadow = fullMesh.castShadow = true;

+ 192 - 0
src/core/renderer-three/components/text/index.vue

@@ -0,0 +1,192 @@
+<template></template>
+
+<script lang="ts" setup>
+import {
+  BufferGeometry,
+  CanvasTexture,
+  DoubleSide,
+  Line,
+  LineBasicMaterial,
+  Mesh,
+  MeshBasicMaterial,
+  Object3D,
+  PlaneGeometry,
+  Vector3,
+} from "three";
+import {
+  useCamera,
+  useCursor,
+  useRender,
+  useStageProps,
+  useTree,
+} from "../../hook/use-stage";
+import { TextData } from "@/core/components/text";
+import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
+import { Transform } from "konva/lib/Util";
+import { Size } from "@/utils/math";
+import { useHoverEventRegister, useMouseEventRegister } from "../../hook/use-event";
+import { useFlyRoaming } from "../../hook/use-controls";
+
+const props = defineProps<{ data: TextData }>();
+
+const fontSize = computed(() => props.data.fontSize || 12);
+const size = ref<Size>({ width: 1, height: 1 });
+
+const getTextTexture = () => {
+  const pixelScale = 1;
+  let $canvas: HTMLCanvasElement;
+  $canvas = document.createElement("canvas");
+  const ctx = $canvas.getContext("2d")!;
+  const pixelFontSize = fontSize.value * pixelScale;
+  ctx.font = `${pixelFontSize}px ${props.data.fontFamily} ${props.data.fontStyle}`;
+
+  const contents = props.data.content.split("\n");
+  const textMetrics = contents.map((content, ndx) => {
+    const textMetric = ctx.measureText(content);
+    const width = textMetric.width;
+    let height = textMetric.fontBoundingBoxAscent;
+    if (ndx === contents.length - 1) {
+      height += textMetric.fontBoundingBoxDescent;
+    }
+    return {
+      width,
+      height,
+      fontBoundingBoxAscent: textMetric.fontBoundingBoxAscent,
+    };
+  });
+
+  const textSize = textMetrics.reduce(
+    (t, c) => ({
+      width: Math.max(t.width, c.width),
+      height: t.height + c.height,
+    }),
+    { width: 0, height: 0 }
+  );
+  const padding = 0.2 * pixelFontSize;
+  $canvas.width = textSize.width + 2 * padding;
+  $canvas.height = textSize.height + 2 * padding;
+
+  ctx.font = `${props.data.fontStyle} ${pixelFontSize}px "${props.data.fontFamily}"`;
+  ctx.fillStyle = props.data.fill || "#000000";
+  ctx.strokeStyle = props.data.stroke || "#ffffff";
+  ctx.lineWidth = padding;
+  ctx.textAlign = props.data.align as CanvasTextAlign;
+  let top = padding;
+  contents.forEach((content, ndx) => {
+    const met = textMetrics[ndx];
+    const b = top + met.fontBoundingBoxAscent;
+    if (ctx.textAlign === "center") {
+      ctx.strokeText(content, textSize.width / 2 + padding, b);
+    } else if (ctx.textAlign === "right") {
+      ctx.strokeText(content, textSize.width + padding, b);
+    } else if (ctx.textAlign === "left") {
+      ctx.strokeText(content, padding, b);
+    }
+    top += met.height;
+  });
+  top = padding;
+  contents.forEach((content, ndx) => {
+    const met = textMetrics[ndx];
+    const b = top + met.fontBoundingBoxAscent;
+    if (ctx.textAlign === "center") {
+      ctx.fillText(content, textSize.width / 2 + padding, b);
+    } else if (ctx.textAlign === "right") {
+      ctx.fillText(content, textSize.width + padding, b);
+    } else if (ctx.textAlign === "left") {
+      ctx.fillText(content, padding, b);
+    }
+    top += met.height;
+  });
+
+  size.value = {
+    width: textSize.width / pixelScale,
+    height: textSize.height / pixelScale,
+  };
+
+  const texture = new CanvasTexture($canvas);
+  texture.needsUpdate = true;
+  return texture;
+};
+
+const geo = new PlaneGeometry(1, 1);
+const material = new MeshBasicMaterial({
+  transparent: true,
+  side: DoubleSide,
+});
+const text = new Mesh(geo, material);
+
+const hoverRegister = useHoverEventRegister();
+const cursor = useCursor();
+let leave: () => void;
+hoverRegister(text, (hover) => {
+  if (hover) {
+    leave = cursor.push("pointer");
+  } else {
+    leave && leave();
+  }
+});
+
+const flyRoaming = useFlyRoaming();
+const mouseRegister = useMouseEventRegister();
+mouseRegister(text, "click", ({ point }) => {
+  flyRoaming(point.clone().setY(0));
+});
+
+const sProps = useStageProps();
+const dec = computed(() => new Transform(props.data.mat).decompose());
+const position = computed(() => {
+  const center = new Vector3(dec.value.x, sProps.value.height + 50, dec.value.y);
+
+  if (props.data.width && props.data.align !== "left") {
+    if (props.data.align === "center") {
+      center.x += (props.data.width - size.value.width) / 2;
+    } else {
+      center.x += props.data.width - size.value.width;
+    }
+  }
+  center.x += size.value.width / 2;
+  center.z += size.value.height / 2;
+
+  return center;
+});
+
+watchEffect(() => {
+  material.map = getTextTexture();
+});
+
+const lineGeo = new BufferGeometry();
+const line = new Line(lineGeo, new LineBasicMaterial({ color: 0xffffff }));
+
+const camera = useCamera();
+const updateTextMat = () => {
+  const distance = camera.position.distanceTo(position.value) / 400;
+  text.lookAt(camera.position);
+  const scaleX = (dec.value.scaleX * size.value.width * distance) / 1.5;
+  const scaleY = (size.value.height / size.value.width) * scaleX;
+
+  text.scale.set(scaleX, scaleY, 1);
+  text.position.copy(position.value);
+
+  const end = position.value.clone();
+  end.y -= scaleY / 2;
+  lineGeo.setFromPoints([end, new Vector3(end.x, 0, end.z)]);
+};
+
+camera.bus.on("change", updateTextMat);
+onUnmounted(() => camera.bus.off("change", updateTextMat));
+
+const render = useRender();
+watch(
+  [size, position],
+  () => {
+    updateTextMat();
+    render();
+  },
+  { immediate: true }
+);
+
+const group = new Object3D();
+group.add(text, line);
+
+useTree().value = group;
+</script>

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

@@ -0,0 +1 @@
+export const subgroupName = 'subgroup'

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

@@ -0,0 +1,18 @@
+<template></template>
+
+<script lang="ts" setup>
+import { DoubleSide, Mesh, MeshPhongMaterial, PlaneGeometry } from "three";
+import { useTree } from "../hook/use-stage";
+import { subgroupName } from "../container";
+
+const geometry = new PlaneGeometry(10000, 10000, 1, 1);
+const material = new MeshPhongMaterial({
+  color: 0xffffff,
+  side: DoubleSide,
+});
+const ground = new Mesh(geometry, material);
+ground.rotateX(-Math.PI / 2);
+ground.receiveShadow = true;
+ground.name = subgroupName;
+useTree().value = ground;
+</script>

+ 32 - 0
src/core/renderer-three/env/light.vue

@@ -0,0 +1,32 @@
+<template></template>
+
+<script lang="ts" setup>
+import { AmbientLight, DirectionalLight, Group, HemisphereLight, Vector3 } from "three";
+import { useTree } from "../hook/use-stage";
+
+const group = new Group();
+group.add(new AmbientLight(0x404040), new HemisphereLight(0xffffff, 0xcccccc, 2));
+
+const addDire = (position: Vector3) => {
+  const direLight = new DirectionalLight(0xffffff, 1);
+  direLight.position.copy(position);
+  const scale = 500;
+  direLight.position.multiplyScalar(scale);
+  direLight.lookAt(new Vector3(0, 0, 0));
+
+  direLight.castShadow = true;
+  direLight.shadow.camera.left = -scale * 1.5;
+  direLight.shadow.camera.right = scale * 1.5;
+  direLight.shadow.camera.top = -scale * 1.5;
+  direLight.shadow.camera.bottom = scale * 1.5;
+  direLight.shadow.camera.far = scale * 2;
+  direLight.shadow.mapSize.width = 2048 * 2;
+  direLight.shadow.mapSize.height = 2048 * 2;
+  direLight.shadow.camera.updateProjectionMatrix();
+  // group.add(new CameraHelper(direLight.shadow.camera));
+  group.add(direLight);
+};
+addDire(new Vector3(0.1, 1, -0.5));
+
+useTree().value = group;
+</script>

+ 84 - 0
src/core/renderer-three/hook/use-animation.ts

@@ -0,0 +1,84 @@
+import { animation } from "@/core/hook/use-animation";
+import { Matrix4, Quaternion, Vector3 } from "three";
+import { installThreeGlobalVar, useCamera, useRender } from "./use-stage";
+import { Tween, Easing } from "@tweenjs/tween.js";
+import { mergeFuns } from "@/utils/shared";
+
+const easing = Easing.Quadratic.InOut;
+export const quatAnimation = (
+  startQuat: Quaternion,
+  endQuat: Quaternion,
+  update?: (data: Quaternion) => void,
+  time = 300
+) => {
+  let stop: (() => void) & { promise: Promise<void> };
+  const promise = new Promise<void>((resolve, reject) => {
+    let isStop = false;
+    let isComp = false;
+    stop = (() => {
+      tw.stop();
+      isStop = true;
+      isComp || reject("动画中断");
+    }) as any;
+
+    const tw = new Tween({ t: 0 })
+      .to({ t: 1 }, time)
+      .easing(easing)
+      .start()
+      .onComplete(() => {
+        resolve();
+        isComp = true;
+        stop();
+      });
+    if (update) {
+      tw.onUpdate((data) => {
+        startQuat.slerp(endQuat, data.t);
+        update && update(startQuat);
+      });
+    }
+
+    const start = () => {
+      requestAnimationFrame(() => {
+        tw.update();
+        isStop || start();
+      });
+    };
+    start();
+  });
+  stop!.promise = promise;
+  return stop!;
+};
+
+export const useCameraAnimation = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    _render();
+    camera.bus.emit("change");
+  };
+
+  let prevStop: (() => void) | null = null;
+  return (position: Vector3, target: Vector3, time = 800) => {
+    if (prevStop) {
+      prevStop();
+      prevStop = null;
+    }
+
+    const startPosition = camera.position;
+    const startQuat = camera.quaternion;
+
+    const endQuat = new Quaternion().setFromRotationMatrix(
+      new Matrix4().lookAt(position, target, camera.up)
+    );
+
+    const stopQuat = quatAnimation(startQuat, endQuat, () => {}, time);
+    const stopPosition = animation(startPosition, position, render, time);
+    const promise = Promise.all([stopQuat.promise, stopPosition.promise]);
+
+    prevStop = mergeFuns(stopQuat, stopPosition);
+    promise.finally(() => {
+      prevStop = null;
+    });
+    return promise;
+  };
+});

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

@@ -0,0 +1,247 @@
+import { ref, watch, watchEffect } from "vue";
+import {
+  installThreeGlobalVar,
+  useCamera,
+  useContainer,
+  useRender,
+} from "./use-stage";
+import { OrbitControls } from "three/examples/jsm/Addons.js";
+import { listener } from "@/utils/event";
+import {  Vector3 } from "three";
+import { mergeFuns } from "@/utils/shared";
+import { useMouseEventRegister } from "./use-event";
+import { subgroupName } from "../container";
+import { useCameraAnimation } from "./use-animation";
+import {
+  getMoveDirectrionByKeys,
+  useFigureMoveCollision,
+} from "./use-move";
+
+const useModelControls = () => {
+  const container = useContainer();
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    camera.bus.emit("change");
+    _render();
+  };
+
+  const controls = new OrbitControls(camera);
+  controls.target.set(0, 5, 0);
+  controls.enabled = false;
+
+  const unListener = listener(controls as any, "change", render);
+  let prevOrigin: Vector3 | null = null;
+  let prevDire: Vector3 | null = null;
+
+  watch(
+    container,
+    (container, _, onCleanup) => {
+      if (container) {
+        controls.domElement = container;
+        controls.connect();
+        onCleanup(() => {
+          controls.disconnect();
+        });
+      }
+    },
+    { immediate: true }
+  );
+
+  return {
+    controls,
+    onDestory() {
+      controls.domElement && controls.dispose();
+      unListener();
+    },
+    syncCamera() {
+      controls.update();
+    },
+    disable() {
+      prevOrigin = camera.position.clone();
+      prevDire = camera.getWorldDirection(new Vector3());
+      controls.enabled = false;
+    },
+    enable() {
+      controls.enabled = true;
+    },
+    get current() {
+      return {
+        prevOrigin,
+        prevDire,
+      };
+    },
+  };
+};
+
+const roamingEysHeight = 120;
+const useRoamingControls = () => {
+  const container = useContainer();
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    camera.bus.emit("change");
+    _render();
+  };
+  const enabled = ref(false);
+  const syncCamera = (direction = camera.getWorldDirection(new Vector3())) => {
+    controls.target.copy(direction.add(camera.position));
+    controls.update();
+  };
+  
+  const { direction, onDestory: onDownDestory } = getMoveDirectrionByKeys();
+  const move = useFigureMoveCollision(camera, direction, syncCamera);
+  move.pause()
+
+  const controls = new OrbitControls(camera);
+  controls.rotateSpeed = -0.3;
+  controls.enableZoom = false;
+
+  let prevOrigin: Vector3 | null = null;
+  let prevDire: Vector3 | null = null;
+
+  return {
+    controls,
+    onDestory: mergeFuns(
+      watchEffect(() => (controls.enabled = enabled.value)),
+      watch(
+        container,
+        (container, _, onCleanup) => {
+          if (container) {
+            controls.domElement = container;
+            controls.connect();
+            onCleanup(() => {
+              controls.disconnect();
+            });
+          }
+        },
+        { immediate: true }
+      ),
+      move.destory,
+      () => {
+        controls.domElement && controls.dispose();
+      },
+      listener(controls as any, "change", render),
+      onDownDestory
+    ),
+    syncCamera,
+    disable() {
+      prevOrigin = camera.position.clone();
+      prevDire = camera.getWorldDirection(new Vector3());
+      controls.enabled = false;
+      move.pause()
+    },
+    enable() {
+      controls.enabled = true;
+      move.continue()
+      render();
+    },
+    get current() {
+      return {
+        prevOrigin,
+        prevDire,
+      };
+    },
+  };
+};
+
+const controlsFactory = {
+  model: useModelControls,
+  roaming: useRoamingControls,
+};
+export type ControlsType = keyof typeof controlsFactory;
+export type Controls = ReturnType<(typeof controlsFactory)[ControlsType]>;
+
+export const useControls = installThreeGlobalVar(() => {
+  const container = useContainer();
+  const type = ref<ControlsType>();
+  const controls = ref<Controls>();
+  const controlsMap = {} as Record<ControlsType, Controls>;
+
+  for (const [type, factory] of Object.entries(controlsFactory)) {
+    controlsMap[type as ControlsType] = factory();
+  }
+
+  const stopWatch = watch(
+    [container, type],
+    ([container, type], _, onCleanup) => {
+      if (!(type && container)) return;
+
+      const ct = controlsMap[type];
+      ct.enable();
+      controls.value = ct;
+      onCleanup(() => {
+        ct.disable();
+        controls.value = undefined;
+      });
+    },
+    { immediate: true, flush: "sync" }
+  );
+
+  return {
+    var: { type, value: controls },
+    onDestroy: () => {
+      stopWatch();
+      for (const controls of Object.values(controlsMap)) {
+        controls.onDestory();
+      }
+    },
+  };
+});
+
+export const useFlyRoaming = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const { type, value: controls } = useControls();
+  const cameraAnimation = useCameraAnimation();
+
+  return async (point: Vector3) => {
+    type.value = undefined;
+    const position = point.clone().add({ x: 0, y: roamingEysHeight, z: 0 });
+    const direction = camera.getWorldDirection(new Vector3());
+    const target = position.clone().add(direction);
+    await cameraAnimation(position, target);
+    type.value = "roaming";
+    controls.value!.syncCamera();
+  };
+});
+
+export const useFlyModel = installThreeGlobalVar(() => {
+  const camera = useCamera();
+  const { type, value: controls } = useControls();
+  const cameraAnimation = useCameraAnimation();
+
+  return async (set: { point?: Vector3; direction?: Vector3 } = {}) => {
+    type.value = "model";
+    const prev = controls.value!.current;
+    if (!set.point) {
+      set.point = prev.prevOrigin || camera.position.clone();
+    }
+    if (!set.direction) {
+      set.direction = prev.prevDire || camera.getWorldDirection(new Vector3());
+    }
+
+    const direction = set.direction;
+    const target = set.point.clone().add(direction);
+    await cameraAnimation(set.point, target);
+    controls.value?.syncCamera();
+  };
+});
+
+export const installAutoSwitchControls = installThreeGlobalVar(() => {
+  const { type } = useControls();
+  const mouseRegister = useMouseEventRegister();
+  const flyRoaming = useFlyRoaming();
+  const flyModel = useFlyModel();
+
+  const onDestroy = mergeFuns(
+    listener(document.documentElement, "keydown", (ev) => {
+      if (ev.key === "Escape" && type.value !== "model") {
+        flyModel();
+      }
+    }),
+    mouseRegister(subgroupName, "dblclick", ({ point }) => flyRoaming(point))
+  );
+  type.value = "model";
+
+  return { onDestroy };
+});

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

@@ -0,0 +1,204 @@
+import { Intersection, Object3D, Vector2 } from "three";
+import { installThreeGlobalVar, useContainer, useScene } from "./use-stage";
+import { listener } from "@/utils/event";
+import { markRaw, reactive, shallowRef, watch } from "vue";
+import { useGetIntersectObjectByPixel } from "./use-getter";
+import { diffArrayChange, mergeFuns } from "@/utils/shared";
+import { globalWatch } from "@/core/hook/use-global-vars";
+
+type ExtractMouseEventKeys<T, E> = {
+  [K in keyof T]: T[K] extends E ? K : never;
+}[keyof T];
+
+export const treeIncludes = (tree: Object3D | string, node: Object3D) => {
+  let current: Object3D | null = node;
+  while (current) {
+    if (current === tree || current.name === tree) {
+      return true;
+    }
+    current = current.parent;
+  }
+  return false;
+};
+
+const useRegisterFactory = <T extends UIEvent>(
+  hitInit: (container: HTMLDivElement) => {
+    destory: () => void;
+    mountRegister: (
+      name: ExtractMouseEventKeys<GlobalEventHandlersEventMap, T>,
+      cb: (ev: T) => void
+    ) => () => void;
+  }
+) => {
+  type Name = ExtractMouseEventKeys<GlobalEventHandlersEventMap, T>;
+  type CB<T extends Name> = (
+    object: Intersection,
+    event: GlobalEventHandlersEventMap[T],
+    objects: Intersection[]
+  ) => void;
+  type RegVal = [Object3D | string, CB<Name>, boolean];
+
+  const hitObjects = shallowRef<Intersection[]>();
+  const currentEvent = shallowRef<[Name, T]>();
+  const scene = useScene();
+  const registered = reactive({}) as {
+    [key in Name]?: RegVal[];
+  };
+  const container = useContainer();
+
+  const hasHit = (object: Intersection, regval: RegVal) => {
+    if (regval[0] === scene) {
+      return true;
+    } else {
+      return treeIncludes(regval[0], object.object);
+    }
+  };
+
+  const init = (container: HTMLDivElement) => {
+    const { destory, mountRegister } = hitInit(container);
+    const mounted: Partial<Record<Name, () => void>> = {};
+
+    const stopRegWatch = watch(
+      () => Object.keys(registered) as Name[],
+      (keys, oldKey = []) => {
+        const { added, deleted } = diffArrayChange(keys, oldKey);
+        added.forEach((key) => {
+          mounted[key] = mountRegister(
+            key,
+            (e) => (currentEvent.value = [key, e])
+          );
+        });
+        deleted.forEach((key) => {
+          if (key in mounted) {
+            mounted[key]!();
+            delete mounted[key];
+          }
+        });
+      },
+      { immediate: true }
+    );
+
+    const stopEmitWatch = watch(
+      [hitObjects, currentEvent, registered],
+      ([hitObjects, event]) => {
+        if (!hitObjects?.length || !event) return;
+        let regvals = registered[event[0]];
+        if (!regvals?.length) return;
+
+        regvals = [...regvals];
+        for (const hitObject of hitObjects) {
+          for (let i = 0; i < regvals!.length; i++) {
+            if (hasHit(hitObject, regvals[i])) {
+              regvals[i][1](hitObject, event[1] as any, hitObjects);
+              regvals.splice(i--, 1);
+            } else if (!regvals[i][2]) {
+              regvals.splice(i--, 1);
+            }
+          }
+        }
+        currentEvent.value = undefined;
+      }
+    );
+
+    return () => {
+      stopRegWatch();
+      stopEmitWatch();
+      for (const key in mounted) {
+        mounted[key as Name]!();
+      }
+      destory();
+    };
+  };
+
+  const register = <T extends Name>(
+    object: Object3D | string,
+    name: T,
+    cb: CB<T>,
+    penetrate = false
+  ) => {
+    if (!(name in registered)) {
+      registered[name] = [];
+    }
+    const regVal: RegVal = markRaw([object, cb as any, penetrate]);
+    registered[name]!.push(regVal);
+
+    return () => {
+      if (!registered[name]) return;
+      const ndx = registered[name].indexOf(regVal);
+      if (~ndx) {
+        registered[name].splice(ndx, 1);
+      }
+    };
+  };
+
+  return {
+    register,
+    registered,
+    hitObjects,
+    destory: globalWatch(
+      container,
+      (container, _, onCleanup) => {
+        container && onCleanup(init(container));
+      },
+      { immediate: true }
+    ),
+  };
+};
+
+export const useMouseEventRegister = installThreeGlobalVar(() => {
+  const getIntersectObjectByPixel = useGetIntersectObjectByPixel();
+  const rf = useRegisterFactory<MouseEvent>((container) => {
+    const unHitListener = listener(container, "mousemove", (ev) => {
+      if (!Object.keys(rf.registered).length) return;
+      rf.hitObjects.value = getIntersectObjectByPixel(
+        new Vector2(ev.offsetX, ev.offsetY)
+      );
+    });
+
+    return {
+      destory: mergeFuns(unHitListener),
+      mountRegister: (name, cb) => listener(container, name, cb),
+    };
+  });
+  return {
+    var: rf.register,
+    onDestroy: rf.destory,
+  };
+});
+
+export const useHoverEventRegister = installThreeGlobalVar(() => {
+  type Item =  [string | Object3D, (hover: boolean) => void]
+  const register = useMouseEventRegister();
+  const scene = useScene();
+  const checks = [] as Array<Item>
+  const prevs = [] as boolean[]
+
+  const stopListener = register(scene, "mousemove", ({ object }) => {
+    checks.forEach(([node, cb], ndx) => {
+      const hover = treeIncludes(node, object);
+      if (prevs[ndx] !== hover) {
+        cb(hover)
+        prevs[ndx] = hover
+      }
+    })
+  });
+
+  const hoverRegister = (tree: string | Object3D, cb: (hover: boolean) => void) => {
+    const item: Item = [tree, cb]
+    checks.push(item)
+    prevs.push(false)
+
+    return () => {
+      const ndx = checks.indexOf(item)
+      if (~ndx) {
+        checks.splice(ndx, 1)
+        prevs.splice(ndx, 1)
+      }
+    }
+  }
+
+  return {
+    var: hoverRegister,
+    onDestroy: stopListener
+  }
+});

+ 33 - 0
src/core/renderer-three/hook/use-getter.ts

@@ -0,0 +1,33 @@
+import { Raycaster, Vector2, Vector3 } from "three";
+import { installThreeGlobalVar, useCamera, useContainer, useScene } from "./use-stage";
+
+export const useRaycaster = installThreeGlobalVar(() => new Raycaster());
+
+export const useGetIntersectObject = () => {
+  const scene = useScene();
+  const raycaster = useRaycaster();
+
+  return (origin: Vector3, direction: Vector3, far = 10000, near = 0) => {
+    raycaster.set(origin, direction);
+    raycaster.far = far
+    raycaster.near = near
+    return raycaster.intersectObject(scene);
+  };
+};
+
+export const useGetIntersectObjectByPixel = () => {
+  const scene = useScene();
+  const raycaster = useRaycaster();
+  const camera = useCamera();
+  const container = useContainer()
+
+  return (pixel: Vector2) => {
+    if (container.value) {
+      pixel = pixel.clone()
+      pixel.setX((pixel.x / container.value.offsetWidth) * 2 - 1)
+      pixel.setY(-(pixel.y / container.value.offsetHeight) * 2 + 1)
+    }
+    raycaster.setFromCamera(pixel, camera);
+    return raycaster.intersectObject(scene);
+  };
+};

+ 200 - 0
src/core/renderer-three/hook/use-move.ts

@@ -0,0 +1,200 @@
+import { getDownKeys } from "@/core/hook/use-global-vars";
+import { frameInterval, mergeFuns } from "@/utils/shared";
+import { Object3D, Vector2, Vector3 } from "three";
+import { ref, Ref, ShallowRef, shallowRef, watch } from "vue";
+import { useGetIntersectObject, useRaycaster } from "./use-getter";
+
+export const getMoveDirectrionByKeys = () => {
+  const { var: keys, onDestroy: onDownDestory } = getDownKeys();
+  const direction = shallowRef<Vector3>();
+
+  const stopWatch = watch(
+    keys,
+    (keys) => {
+      let dire = new Vector3();
+      if (keys.has("a")) {
+        dire.setX(-1);
+      }
+      if (keys.has("d")) {
+        dire.setX(1);
+      }
+      if (keys.has("w")) {
+        dire.setZ(1);
+      }
+      if (keys.has("s")) {
+        dire.setZ(-1);
+      }
+      if (keys.has(" ")) {
+        dire.setY(1);
+      }
+      if (dire.x || dire.y || dire.z) {
+        direction.value = dire.normalize();
+      } else {
+        direction.value = undefined;
+      }
+    },
+    { deep: true }
+  );
+
+  return {
+    direction,
+    onDestory: mergeFuns(stopWatch, onDownDestory),
+  };
+};
+
+export const jumpFactory = (initY = 0, endY = initY, jumpForce = 5) => {
+  const gravity = -0.3; // 重力加速度
+  let velocityY = jumpForce; // 垂直速度
+  let y = initY;
+
+  const update = () => {
+    // 5. 应用重力
+    velocityY += gravity;
+    y += velocityY + Math.sin(Date.now() * 0.01) * 0.01;
+
+    if (y <= endY) {
+      y = endY;
+      velocityY = 0;
+      return { height: y, final: true, velocityY };
+    } else {
+      return { height: y, final: false, velocityY };
+    }
+  };
+
+  return update;
+};
+
+const up = new Vector3(0, 1, 0);
+export const useFigureMoveCollision = (
+  figure: Object3D,
+  dire: Ref<Vector3 | undefined>,
+  render: () => void,
+  scale = 2.5,
+  height = 120,
+  srang = 10
+) => {
+  const getIntersect = useGetIntersectObject();
+  const pause = ref(false);
+  const rangeCount = 10;
+  const offset = 10;
+  const rangeHeight: number[] = [];
+  for (let i = 0; i < rangeCount; i++) {
+    rangeHeight.push((i / (rangeCount - 1)) * (height - offset) + offset);
+  }
+
+  const getJumpPoint = (position = figure.position) => {
+    const objects = getIntersect(position, jumpRayDire, height);
+    return objects.length > 0 ? objects[0].point : 0;
+  };
+
+  const jumpRayDire = new Vector3(0, -1, 0);
+  const jump = (velocityY = 5) => {
+    const updateJump = jumpFactory(figure.position.y, height, velocityY);
+    return () => {
+      const j = updateJump();
+      figure.position.setY(j.height);
+      if (j.velocityY < 0) {
+        const p = getJumpPoint();
+        if (p) {
+          figure.position.setY(height + p.y);
+          return true;
+        }
+      }
+      return j.final;
+    };
+  };
+
+  const checkRange = (dire: Vector3) => {
+    const p = figure.position;
+    for (const offset of rangeHeight) {
+      const objects = getIntersect(
+        new Vector3(p.x, p.y - height + offset, p.z),
+        dire,
+        srang
+      );
+      if (objects.length) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  const move = (moveDire: Vector2) => {
+    const figureDire = figure.getWorldDirection(new Vector3());
+    figureDire.setY(0).normalize();
+
+    const rightDire = up.clone().cross(figureDire);
+    const finalDire = new Vector3();
+
+    if (moveDire.y) {
+      const xMoveDire = figureDire.clone().multiplyScalar(moveDire.y);
+      checkRange(xMoveDire) && finalDire.add(xMoveDire);
+    }
+
+    if (moveDire.x) {
+      const yMoveDire = rightDire.multiplyScalar(-moveDire.x);
+      checkRange(yMoveDire) && finalDire.add(yMoveDire);
+    }
+
+    if (figureDire.length() > 0) {
+      figure.position.addScaledVector(finalDire.normalize(), scale);
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+  let stopJump: (() => void) | null = null;
+  const startJump = (velocityY = 5) => {
+    const update = jump(velocityY);
+    stopJump = frameInterval(() => {
+      if (update()) {
+        stopJump && stopJump();
+        stopJump = null;
+      }
+      render();
+    });
+  };
+
+  let stopMove: (() => void) | null = null;
+  const stopFMove = watch([dire, pause], ([dire, pause]) => {
+    if (stopMove) {
+      stopMove();
+      stopMove = null;
+    }
+
+    if (!dire || pause) {
+      return;
+    }
+
+    if (dire.y && !stopJump) {
+      startJump();
+    }
+
+    stopMove = frameInterval(() => {
+      move(new Vector2(dire.x, dire.z));
+      render();
+      if (!stopJump) {
+        // console.log(getJumpPoint());
+      }
+    });
+  });
+
+  return {
+    destory: () => {
+      stopFMove();
+      stopJump && stopJump();
+      stopMove && stopMove();
+    },
+    pause: () => {
+      pause.value = true
+      stopJump && stopJump();
+      stopMove && stopMove();
+      stopJump = null
+      stopMove = null
+    },
+    continue() {
+      pause.value = false
+    }
+  };
+};

+ 224 - 0
src/core/renderer-three/hook/use-stage.ts

@@ -0,0 +1,224 @@
+import { DrawExpose } from "@/core/hook/use-expose";
+import {
+  globalWatch,
+  globalWatchEffect,
+  installGlobalVar,
+  stackVar,
+} from "@/core/hook/use-global-vars";
+import { listener } from "@/utils/event";
+import { frameThrottling } from "@/utils/shared";
+import mitt, { Emitter } from "mitt";
+import {
+  Color,
+  Fog,
+  Object3D,
+  PCFSoftShadowMap,
+  PerspectiveCamera,
+  Scene,
+  Vector2,
+  Vector3,
+  WebGLRenderer,
+} from "three";
+import {
+  computed,
+  getCurrentInstance,
+  onUnmounted,
+  Ref,
+  ref,
+  shallowRef,
+  watch,
+} from "vue";
+
+export const instanceName = "three-renderer";
+export const installThreeGlobalVar = <T>(
+  create: () => { var: T; onDestroy: () => void } | T,
+  key = Symbol("globalVar")
+) => installGlobalVar(create, key, instanceName);
+
+export const useContainer = installThreeGlobalVar(() => ref<HTMLDivElement>());
+
+export const useRenderer = installThreeGlobalVar(() => {
+  const container = useContainer();
+  const renderer = new WebGLRenderer({
+    antialias: true,
+    precision: "highp", // 强制高精度着色器计算(可选:'highp'/'mediump'/'lowp')
+    powerPreference: "high-performance", // 优先使用高性能GPU模式
+  }) as WebGLRenderer & {
+    bus: Emitter<{ sizeChange: void }>;
+  };
+
+  renderer.bus = mitt();
+  renderer.shadowMap.enabled = true;
+  renderer.shadowMap.type = PCFSoftShadowMap;
+
+  const init = (container: HTMLDivElement) => {
+    container.appendChild(renderer.domElement);
+    const resizeHandler = () => {
+      const w = container.offsetWidth;
+      const h = container.offsetHeight;
+      renderer.setSize(w, h);
+      renderer.bus.emit("sizeChange");
+    };
+
+    resizeHandler();
+    return listener(window, "resize", resizeHandler);
+  };
+
+  return {
+    var: renderer,
+    onDestroy: globalWatch(
+      container,
+      (dom, _, onCleanup) => {
+        dom && onCleanup(init(dom));
+      },
+      { immediate: true }
+    ),
+  };
+});
+
+export type StageProps = {
+  draw: DrawExpose;
+  height?: number;
+};
+export const useStageProps = installThreeGlobalVar(
+  () => ref() as Ref<Required<StageProps>>
+);
+
+export type Loop = () => void;
+export const useAnimationLoop = installThreeGlobalVar(() => {
+  const loops = ref<Loop[]>([]);
+  const trigger = () => {
+    for (const loop of loops.value) {
+      loop();
+    }
+  };
+
+  const remove = (fn: Loop) => {
+    const ndx = loops.value.indexOf(fn);
+    if (~ndx) {
+      loops.value.splice(ndx, 1);
+    }
+  };
+  const add = (fn: Loop) => {
+    loops.value.push(fn);
+    return () => remove(fn);
+  };
+
+  return { add, remove, trigger };
+});
+
+export const useCamera = installThreeGlobalVar(() => {
+  const renderer = useRenderer();
+  const camera = new PerspectiveCamera(
+    75,
+    1,
+    0.1,
+    500000
+  ) as PerspectiveCamera & { bus: Emitter<{ change: void }> };
+  camera.position.set(1, 1, 1).multiplyScalar(1000);
+  camera.lookAt(new Vector3(0, 1, 0));
+  camera.bus = mitt();
+
+  renderer.bus.on("sizeChange", () => {
+    const size = renderer.getSize(new Vector2());
+    camera.aspect = size.width / size.height;
+    camera.updateProjectionMatrix();
+    camera.bus.emit("change");
+  });
+  return camera;
+});
+
+export const useScene = installThreeGlobalVar(() => {
+  const scene = new Scene();
+  scene.background = new Color("white");
+  scene.fog = new Fog( 0xffffff, 0, 7500 );
+  return scene;
+});
+
+export const useRender = installThreeGlobalVar(() => {
+  const loop = useAnimationLoop();
+  const renderer = useRenderer();
+  const scene = useScene();
+  const camera = useCamera();
+  const render = frameThrottling(() => {
+    loop.trigger();
+    renderer.render(scene, camera);
+  });
+
+  renderer.bus.on("sizeChange", render);
+  return render;
+});
+
+export const useTree = () => {
+  const expose = shallowRef();
+  const current = getCurrentInstance() as any;
+  const render = useRender();
+
+  let parent = current.parent;
+  while (parent.type.name !== instanceName) {
+    if (parent.__three_instance) {
+      break;
+    } else if (parent.parent) {
+      parent = parent.parent;
+    }
+  }
+
+  const scene = useScene();
+  const threeParent = computed(
+    () =>
+      (parent.__three_instance
+        ? parent.__three_instance.value
+        : scene) as Object3D
+  );
+
+  watch([threeParent, expose], ([parent, expose], _, onCleanup) => {
+    if (current.props?.data?.id) {
+      expose.name = current.props.data.id;
+    }
+    if (parent && expose) {
+      parent.add(expose);
+      render();
+      onCleanup(() => {
+        parent.remove(expose);
+        render();
+      });
+    }
+  });
+
+  current.__three_instance = expose;
+  onUnmounted(() => {
+    expose.value = undefined;
+  });
+  return expose;
+};
+
+export const useDrawHook = <T extends () => any>(hook: T): ReturnType<T> => {
+  const draw = useStageProps().value.draw;
+  return draw.runHook(hook);
+};
+
+export const useCursor = installThreeGlobalVar(
+  () => stackVar("default"),
+  Symbol("cursor")
+);
+
+export const installCursorStyle = installThreeGlobalVar(() => {
+  const cursor = useCursor();
+  const container = useContainer()
+  const cursorStyle = computed(() => {
+    if (cursor.value.includes(".")) {
+      return `url(${cursor.value}) 12 12, auto`;
+    } else {
+      return cursor.value;
+    }
+  });
+
+  const stop = globalWatchEffect((onCleanup) => {
+    const dom = container.value
+    if (dom) {
+      dom.style.cursor = cursorStyle.value
+      onCleanup(() => dom.style.cursor = 'initial')
+    }
+  })
+  return { onDestroy: stop }
+});

+ 51 - 0
src/core/renderer-three/renderer.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="three-container" ref="container">
+    <Ground />
+    <Light />
+    <template v-for="(com, key) in components" :key="key">
+      <component
+        :is="com"
+        :data="item"
+        v-for="item in draw.store.getTypeItems(key)"
+        :key="item.id"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Ground from "./env/ground.vue";
+import Light from "./env/light.vue";
+import { getCurrentInstance, onUnmounted } from "vue";
+import {
+  installCursorStyle,
+  instanceName,
+  StageProps,
+  useContainer,
+  useStageProps,
+} from "./hook/use-stage";
+import { rendererMap } from "@/constant";
+import { mergeFuns } from "@/utils/shared";
+import { components } from "./components";
+import { installAutoSwitchControls } from "./hook/use-controls";
+
+const instance = getCurrentInstance();
+defineOptions({ name: instanceName });
+rendererMap.set(instance, { unmounteds: [] });
+onUnmounted(() => {
+  mergeFuns(rendererMap.get(instance)!.unmounteds)();
+});
+
+const props = defineProps<StageProps>();
+useStageProps().value = { ...props, height: props.height || 200 };
+installAutoSwitchControls();
+installCursorStyle();
+const container = useContainer();
+</script>
+
+<style lang="scss">
+.three-container {
+  width: 100%;
+  height: 100%;
+}
+</style>

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

@@ -0,0 +1,7 @@
+import { Matrix4, Object3D } from "three";
+
+export const setMat = (obj: Object3D, mat: Matrix4) => {
+  obj.matrixAutoUpdate = false;
+  obj.matrix.copy(mat);
+  obj.matrixWorldNeedsUpdate = true;
+};

+ 7 - 10
src/core/renderer/draw-group.vue

@@ -1,20 +1,17 @@
-<template>
-  <ShapeComponent
-    :data="(item as any)"
-    v-for="item in tempItems"
-    :key="item.id"
-    addMode
-  />
-</template>
+<template></template>
 
 <script setup lang="ts">
+import { onUnmounted } from "vue";
 import { ShapeType, components } from "../components";
-import { useInteractiveAdd } from "../hook/use-draw.ts";
+import { useDrawIngData, useInteractiveAdd } from "../hook/use-draw.ts";
 
 const props = defineProps<{ type: ShapeType }>();
 const type = props.type;
+const drawStore = useDrawIngData();
 const tempItems = components[type].useDraw
   ? components[type].useDraw()
   : useInteractiveAdd(props.type);
-const ShapeComponent = components[type].TempComponent || components[type].Component;
+drawStore[type] = (tempItems || []) as any;
+
+onUnmounted(() => delete drawStore[type]);
 </script>

+ 1 - 1
src/core/renderer/draw-shape.vue

@@ -22,7 +22,7 @@ watch(
   () => ({ canSnap: !props.temp && !props.item.ref, item: props.item }),
   ({ canSnap, item }) => {
     infos.forEach(customSnapInfos.remove);
-    if (!canSnap) return;
+    if (!canSnap || !component.value.getSnapInfos) return;
     infos = component.value.getSnapInfos(item as any);
     infos.forEach(customSnapInfos.add);
   },

+ 15 - 0
src/core/renderer/group-temp.vue

@@ -0,0 +1,15 @@
+<template>
+  <ShapeComponent :data="item" addMode :key="item.id" v-for="item in items" />
+</template>
+
+<script setup lang="ts">
+import { ShapeType, components } from "../components";
+import { computed } from "vue";
+import { useDrawIngData } from "../hook/use-draw";
+
+const props = defineProps<{ type: ShapeType }>();
+const store = useDrawIngData();
+const type = props.type as "arrow";
+const ShapeComponent = components[type].TempComponent || components[type].Component;
+const items = computed(() => store[type]);
+</script>

+ 4 - 2
src/core/renderer/renderer.vue

@@ -43,8 +43,9 @@
         <!--	临时组,提供临时绘画,以及高频率渲染	-->
         <v-layer :config="viewerConfig" id="temp">
           <template v-if="mode.include(Mode.draw)">
-            <TempShapeGroup v-for="type in types" :type="type" :key="type" />
+            <DrawShapeGroup v-for="type in types" :type="type" :key="type" />
           </template>
+          <TempShapeGroup v-for="type in types" :type="type" :key="type" />
         </v-layer>
         <v-layer id="helper">
           <!-- <ActiveBoxs /> -->
@@ -62,9 +63,10 @@
 
 <script lang="ts" setup>
 import ShapeGroup from "./group.vue";
+import TempShapeGroup from "./group-temp.vue";
 import Back from "../helper/back.vue";
 import Border from "../helper/facade.vue";
-import TempShapeGroup from "./draw-group.vue";
+import DrawShapeGroup from "./draw-group.vue";
 import SnapLines from "../helper/snap-lines.vue";
 import BackGrid from "../helper/back-grid.vue";
 import SplitLine from "../helper/split-line.vue";

+ 2 - 2
src/core/store/store.ts

@@ -2,7 +2,7 @@ import { defineStore } from "pinia";
 import { DrawData, DrawItem, ShapeType } from "../components";
 import { defaultLayer } from "@/constant";
 
-const sortFn = (
+export const sortFn = (
   a: Pick<DrawItem, "zIndex" | "createTime">,
   b: Pick<DrawItem, "zIndex" | "createTime">
 ) => a.zIndex - b.zIndex || a.createTime - b.createTime;
@@ -41,7 +41,7 @@ export const useStoreRaw = defineStore("draw-data", {
       if (layers.includes(layer)) {
         return layer
       } else {
-        return layers[0]
+        return layers[0] 
       }
     },
     layers() {

+ 0 - 0
src/deconstruction.d.ts


Some files were not shown because too many files changed in this diff