Ruler.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <template>
  2. <div v-if="visible" class="ruler-container" :style="{ pointerEvents: 'none' }">
  3. <!-- 垂直标尺(左侧) -->
  4. <svg class="ruler-svg vertical" :width="rulerWidth" :height="containerHeight" :style="verticalStyle">
  5. <defs>
  6. <pattern id="verticalTicks" width="1" height="10" patternUnits="userSpaceOnUse">
  7. <line x1="0" y1="0" x2="0" y2="10" stroke="#ddd" stroke-width="1" />
  8. </pattern>
  9. </defs>
  10. <rect width="100%" height="100%" fill="url(#verticalTicks)" />
  11. <!-- 大刻度线和标签 -->
  12. <g v-for="(tick, index) in verticalTicks" :key="index">
  13. <line :x1="0" :y1="tick.pos" :x2="rulerWidth" :y2="tick.pos" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
  14. <text v-if="tick.isMajor" :x="rulerWidth - 5" :y="tick.pos + 4" font-size="10" fill="#333" text-anchor="end">{{ tick.label }}</text>
  15. </g>
  16. </svg>
  17. <!-- 水平标尺(顶部) -->
  18. <svg class="ruler-svg horizontal" :width="containerWidth" :height="rulerHeight" :style="horizontalStyle">
  19. <defs>
  20. <pattern id="horizontalTicks" width="10" height="1" patternUnits="userSpaceOnUse">
  21. <line x1="0" y1="0" x2="10" y2="0" stroke="#ddd" stroke-width="1" />
  22. </pattern>
  23. </defs>
  24. <rect width="100%" height="100%" fill="url(#horizontalTicks)" />
  25. <!-- 大刻度线和标签 -->
  26. <g v-for="(tick, index) in horizontalTicks" :key="index">
  27. <line :x1="tick.pos" :y1="0" :x2="tick.pos" :y2="rulerHeight" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
  28. <text v-if="tick.isMajor" :x="tick.pos - 5" :y="rulerHeight - 2" font-size="10" fill="#333" text-anchor="middle">{{ tick.label }}</text>
  29. </g>
  30. </svg>
  31. <!-- 角落重叠遮罩(可选,避免重叠视觉问题) -->
  32. <div class="ruler-corner" :style="{ width: rulerWidth + 'px', height: rulerHeight + 'px', background: 'white' }"></div>
  33. </div>
  34. </template>
  35. <script setup>
  36. import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue'
  37. import { useVueFlow } from '@vue-flow/core'
  38. const props = defineProps({
  39. visible: { type: Boolean, default: false },
  40. containerRef: { type: Object, required: true } // 传入 Vue Flow 容器的 ref,用于获取尺寸
  41. })
  42. const { getViewport } = useVueFlow()
  43. // 标尺尺寸(固定宽度/高度)
  44. const rulerWidth = 20 // 垂直标尺宽度
  45. const rulerHeight = 20 // 水平标尺高度
  46. // 获取容器尺寸
  47. const containerWidth = ref(0)
  48. const containerHeight = ref(0)
  49. const updateDimensions = () => {
  50. if (props.containerRef) {
  51. const rect = props.containerRef.getBoundingClientRect()
  52. containerWidth.value = rect.width
  53. containerHeight.value = rect.height
  54. console.log('Container dimensions updated:', containerWidth.value, containerHeight.value) // 调试日志
  55. }
  56. }
  57. let resizeObserver = null
  58. onMounted(async () => {
  59. await nextTick()
  60. updateDimensions() // 初始获取尺寸
  61. resizeObserver = new ResizeObserver(() => {
  62. updateDimensions()
  63. })
  64. if (props.containerRef) {
  65. resizeObserver.observe(props.containerRef)
  66. }
  67. })
  68. onUnmounted(() => {
  69. if (resizeObserver) {
  70. resizeObserver.disconnect()
  71. }
  72. })
  73. // 计算当前 transform(响应式)
  74. const currentViewport = computed(() => getViewport())
  75. const scale = computed(() => currentViewport.value.zoom || 1) // 当前缩放,默认1
  76. const translateX = computed(() => currentViewport.value.x || 0) // 平移 X,默认0
  77. const translateY = computed(() => currentViewport.value.y || 0) // 平移 Y,默认0
  78. // 动态刻度间隔(基于缩放调整,避免太密/太疏)——固定世界单位间隔,动态生成
  79. const worldTickInterval = computed(() => {
  80. // 根据缩放选择合适的间隔:小缩放时大间隔,避免太密
  81. if (scale.value >= 2) return 10; // 放大时小间隔
  82. if (scale.value >= 1) return 50;
  83. if (scale.value >= 0.5) return 100;
  84. return 200; // 缩小时大间隔
  85. })
  86. // 生成垂直刻度:基于世界坐标范围,确保覆盖整个视口
  87. const verticalTicks = computed(() => {
  88. const ticks = []
  89. // 计算视口的世界坐标范围
  90. const minWorldY = -translateY.value / scale.value
  91. const maxWorldY = (containerHeight.value - translateY.value) / scale.value
  92. // 从范围开始生成 ticks,确保有至少 5-10 个
  93. let startWorld = Math.floor(minWorldY / worldTickInterval.value) * worldTickInterval.value
  94. if (startWorld > minWorldY) startWorld -= worldTickInterval.value // 确保覆盖起始
  95. for (let worldY = startWorld; worldY <= maxWorldY; worldY += worldTickInterval.value) {
  96. // 转换回屏幕坐标
  97. const screenY = (worldY * scale.value + translateY.value)
  98. if (screenY >= 0 && screenY <= containerHeight.value) { // 只渲染可见的
  99. const isMajor = (worldY % (worldTickInterval.value * 5)) === 0 // 每5小刻一大刻
  100. ticks.push({
  101. pos: screenY,
  102. label: isMajor ? Math.round(worldY) : '',
  103. isMajor
  104. })
  105. }
  106. }
  107. console.log('Vertical ticks generated:', ticks.length, ticks) // 调试日志:检查生成数量和位置
  108. return ticks
  109. })
  110. // 生成水平刻度:类似
  111. const horizontalTicks = computed(() => {
  112. const ticks = []
  113. const minWorldX = -translateX.value / scale.value
  114. const maxWorldX = (containerWidth.value - translateX.value) / scale.value
  115. let startWorld = Math.floor(minWorldX / worldTickInterval.value) * worldTickInterval.value
  116. if (startWorld > minWorldX) startWorld -= worldTickInterval.value
  117. for (let worldX = startWorld; worldX <= maxWorldX; worldX += worldTickInterval.value) {
  118. const screenX = (worldX * scale.value + translateX.value)
  119. if (screenX >= 0 && screenX <= containerWidth.value) {
  120. const isMajor = (worldX % (worldTickInterval.value * 5)) === 0
  121. ticks.push({
  122. pos: screenX,
  123. label: isMajor ? Math.round(worldX) : '',
  124. isMajor
  125. })
  126. }
  127. }
  128. console.log('Horizontal ticks generated:', ticks.length) // 调试日志
  129. return ticks
  130. })
  131. // 垂直标尺样式(固定位置,不跟随平移)
  132. const verticalStyle = computed(() => ({
  133. position: 'absolute',
  134. left: '0px',
  135. top: '0px'
  136. }))
  137. // 水平标尺样式(避开垂直标尺)
  138. const horizontalStyle = computed(() => ({
  139. position: 'absolute',
  140. left: `${rulerWidth}px`,
  141. top: '0px'
  142. }))
  143. // 监听 visible 变化,强制更新尺寸(可选,防初始空)
  144. watch(() => props.visible, (newVal) => {
  145. if (newVal) {
  146. nextTick(() => updateDimensions())
  147. }
  148. })
  149. </script>
  150. <style scoped>
  151. .ruler-container {
  152. position: absolute;
  153. top: 0;
  154. left: 0;
  155. width: 100%;
  156. height: 100%;
  157. z-index: 100;
  158. background: transparent;
  159. }
  160. .ruler-svg {
  161. position: absolute;
  162. background: #fdfdfd;
  163. border: 1px solid #ddd;
  164. overflow: visible;
  165. }
  166. .vertical {
  167. z-index: 102;
  168. }
  169. .horizontal {
  170. z-index: 101;
  171. }
  172. .ruler-corner {
  173. position: absolute;
  174. top: 0;
  175. left: 0;
  176. z-index: 103; /* 覆盖重叠部分 */
  177. background: white;
  178. border: 1px solid #ddd;
  179. }
  180. </style>