|
|
@@ -0,0 +1,203 @@
|
|
|
+<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>
|