| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- <template>
- <div v-if="visible" class="ruler-container" :style="{ pointerEvents: 'none' }">
- <!-- 垂直标尺(左侧) -->
- <svg class="ruler-svg vertical" :width="rulerWidth" :height="containerHeight" :style="verticalStyle">
- <defs>
- <pattern id="verticalTicks" width="1" height="10" patternUnits="userSpaceOnUse">
- <line x1="0" y1="0" x2="0" y2="10" stroke="#ddd" stroke-width="1" />
- </pattern>
- </defs>
- <rect width="100%" height="100%" fill="url(#verticalTicks)" />
- <!-- 大刻度线和标签 -->
- <g v-for="(tick, index) in verticalTicks" :key="index">
- <line :x1="0" :y1="tick.pos" :x2="rulerWidth" :y2="tick.pos" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
- <text v-if="tick.isMajor" :x="rulerWidth - 5" :y="tick.pos + 4" font-size="10" fill="#333" text-anchor="end">{{ tick.label }}</text>
- </g>
- </svg>
- <!-- 水平标尺(顶部) -->
- <svg class="ruler-svg horizontal" :width="containerWidth" :height="rulerHeight" :style="horizontalStyle">
- <defs>
- <pattern id="horizontalTicks" width="10" height="1" patternUnits="userSpaceOnUse">
- <line x1="0" y1="0" x2="10" y2="0" stroke="#ddd" stroke-width="1" />
- </pattern>
- </defs>
- <rect width="100%" height="100%" fill="url(#horizontalTicks)" />
- <!-- 大刻度线和标签 -->
- <g v-for="(tick, index) in horizontalTicks" :key="index">
- <line :x1="tick.pos" :y1="0" :x2="tick.pos" :y2="rulerHeight" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
- <text v-if="tick.isMajor" :x="tick.pos - 5" :y="rulerHeight - 2" font-size="10" fill="#333" text-anchor="middle">{{ tick.label }}</text>
- </g>
- </svg>
- <!-- 角落重叠遮罩(可选,避免重叠视觉问题) -->
- <div class="ruler-corner" :style="{ width: rulerWidth + 'px', height: rulerHeight + 'px', background: 'white' }"></div>
- </div>
- </template>
- <script setup>
- import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue'
- import { useVueFlow } from '@vue-flow/core'
- const props = defineProps({
- visible: { type: Boolean, default: false },
- containerRef: { type: Object, required: true } // 传入 Vue Flow 容器的 ref,用于获取尺寸
- })
- const { getViewport } = useVueFlow()
- // 标尺尺寸(固定宽度/高度)
- const rulerWidth = 20 // 垂直标尺宽度
- const rulerHeight = 20 // 水平标尺高度
- // 获取容器尺寸
- const containerWidth = ref(0)
- const containerHeight = ref(0)
- const updateDimensions = () => {
- if (props.containerRef) {
- const rect = props.containerRef.getBoundingClientRect()
- containerWidth.value = rect.width
- containerHeight.value = rect.height
- console.log('Container dimensions updated:', containerWidth.value, containerHeight.value) // 调试日志
- }
- }
- let resizeObserver = null
- onMounted(async () => {
- await nextTick()
- updateDimensions() // 初始获取尺寸
- resizeObserver = new ResizeObserver(() => {
- updateDimensions()
- })
- if (props.containerRef) {
- resizeObserver.observe(props.containerRef)
- }
- })
- onUnmounted(() => {
- if (resizeObserver) {
- resizeObserver.disconnect()
- }
- })
- // 计算当前 transform(响应式)
- const currentViewport = computed(() => getViewport())
- const scale = computed(() => currentViewport.value.zoom || 1) // 当前缩放,默认1
- const translateX = computed(() => currentViewport.value.x || 0) // 平移 X,默认0
- const translateY = computed(() => currentViewport.value.y || 0) // 平移 Y,默认0
- // 动态刻度间隔(基于缩放调整,避免太密/太疏)——固定世界单位间隔,动态生成
- const worldTickInterval = computed(() => {
- // 根据缩放选择合适的间隔:小缩放时大间隔,避免太密
- if (scale.value >= 2) return 10; // 放大时小间隔
- if (scale.value >= 1) return 50;
- if (scale.value >= 0.5) return 100;
- return 200; // 缩小时大间隔
- })
- // 生成垂直刻度:基于世界坐标范围,确保覆盖整个视口
- const verticalTicks = computed(() => {
- const ticks = []
- // 计算视口的世界坐标范围
- const minWorldY = -translateY.value / scale.value
- const maxWorldY = (containerHeight.value - translateY.value) / scale.value
- // 从范围开始生成 ticks,确保有至少 5-10 个
- let startWorld = Math.floor(minWorldY / worldTickInterval.value) * worldTickInterval.value
- if (startWorld > minWorldY) startWorld -= worldTickInterval.value // 确保覆盖起始
- for (let worldY = startWorld; worldY <= maxWorldY; worldY += worldTickInterval.value) {
- // 转换回屏幕坐标
- const screenY = (worldY * scale.value + translateY.value)
- if (screenY >= 0 && screenY <= containerHeight.value) { // 只渲染可见的
- const isMajor = (worldY % (worldTickInterval.value * 5)) === 0 // 每5小刻一大刻
- ticks.push({
- pos: screenY,
- label: isMajor ? Math.round(worldY) : '',
- isMajor
- })
- }
- }
- console.log('Vertical ticks generated:', ticks.length, ticks) // 调试日志:检查生成数量和位置
- return ticks
- })
- // 生成水平刻度:类似
- const horizontalTicks = computed(() => {
- const ticks = []
- const minWorldX = -translateX.value / scale.value
- const maxWorldX = (containerWidth.value - translateX.value) / scale.value
- let startWorld = Math.floor(minWorldX / worldTickInterval.value) * worldTickInterval.value
- if (startWorld > minWorldX) startWorld -= worldTickInterval.value
- for (let worldX = startWorld; worldX <= maxWorldX; worldX += worldTickInterval.value) {
- const screenX = (worldX * scale.value + translateX.value)
- if (screenX >= 0 && screenX <= containerWidth.value) {
- const isMajor = (worldX % (worldTickInterval.value * 5)) === 0
- ticks.push({
- pos: screenX,
- label: isMajor ? Math.round(worldX) : '',
- isMajor
- })
- }
- }
- console.log('Horizontal ticks generated:', ticks.length) // 调试日志
- return ticks
- })
- // 垂直标尺样式(固定位置,不跟随平移)
- const verticalStyle = computed(() => ({
- position: 'absolute',
- left: '0px',
- top: '0px'
- }))
- // 水平标尺样式(避开垂直标尺)
- const horizontalStyle = computed(() => ({
- position: 'absolute',
- left: `${rulerWidth}px`,
- top: '0px'
- }))
- // 监听 visible 变化,强制更新尺寸(可选,防初始空)
- watch(() => props.visible, (newVal) => {
- if (newVal) {
- nextTick(() => updateDimensions())
- }
- })
- </script>
- <style scoped>
- .ruler-container {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 100;
- background: transparent;
- }
- .ruler-svg {
- position: absolute;
- background: #fdfdfd;
- border: 1px solid #ddd;
- overflow: visible;
- }
- .vertical {
- z-index: 102;
- }
- .horizontal {
- z-index: 101;
- }
- .ruler-corner {
- position: absolute;
- top: 0;
- left: 0;
- z-index: 103; /* 覆盖重叠部分 */
- background: white;
- border: 1px solid #ddd;
- }
- </style>
|