Skip to main content

模板编辑器:辅助线

About 3 minVueVue编辑器设计模式

辅助线听起来高大上,其实就那么回事儿😝

前言

这个功能的思路几乎没有经过深思熟虑,就自然地顺畅地出来了,思维像源泉一样不断涌现,是我最得意的功能。

需求分析

当控件移动的时候,监听其位置,跟其他控件的水平和垂直对应的边上的每一个控制点的x&y都做一个比较,也就是

left vs left

left vs right

right vs right

top vs top

top vs bottom

top vs center

bottom vs bottom

bottom vs center

bottom vs top

等等... (实际代码里我没有举例完)

跟其他控件的这些边都做一个比较,如果差值在5像素范围,那么就显示此辅助线,并将辅助线x,y更新。

设计

辅助线

辅助线只是一个展示,仅仅需要控制其 visible x,y 即可,我们可以根据控件定义6个辅助线分别是

yl => 纵轴左边

yc => 纵轴中间

yr => 纵轴右边

xt => 横轴上边

xc => 横轴中间

Xb => 横轴下边

当然其实定义2条线其实也是可以的,这里我为了更清楚的去显示辅助线所以使用了6条线

/**
   * @type {import('vue').Ref<Array<{type: 'yl'|'yc'|'yr'|'xt'|'xc'|'xb',direction: string, visible: boolean, position: {left: number, top: number}}>>}
   */
const markLine = ref([
  { type: 'yl', direction: 'vertical', visible: false, position: { left: 0, top: 0 } },
  { type: 'yc', direction: 'vertical', visible: false, position: { left: 0, top: 0 } },
  { type: 'yr', direction: 'vertical', visible: false, position: { left: 0, top: 0 } },
  { type: 'xt', direction: 'horizontal', visible: false, position: { left: 0, top: 0 } },
  { type: 'xc', direction: 'horizontal', visible: false, position: { left: 0, top: 0 } },
  { type: 'xb', direction: 'horizontal', visible: false, position: { left: 0, top: 0 } }
])

视图

组件

<template lang="pug">
svg.template-mark-line(
  xmlns="http://www.w3.org/2000/svg"
  :width="size.width"
  :height="size.height"
  :style="wrapperStyle"
  :class="{visible}")
  line(stroke="var(--color-primary)" x1="0" y1="0" :x2="size.width" :y2="size.height")
</template>

使用

<template>
TemplateMarkLine(v-for="item of markLine"
  :key="item.type"
  :position="item.position"
  :direction="item.direction"
  :visible="item.visible")
</template>

Props

{
  name: 'TemplateMarkLine',
  props: {
    visible: Boolean,
    direction: {
      type: String,
      default: 'horizontal'
    },
    position: {
      type: Object,
      required: true
    }
  }
}
NameType说明
direction`"horizontal""vertical"`
position{x: Number, y: Number}位置

实现

边界判定

/**
 * @param {number} dragValue
 * @param {number} targetValue
 */
function isNearly(dragValue, targetValue) {
  return Math.abs(dragValue - targetValue) <= 5
}

此函数 只是判断多少距离是到达边界值

对比条件

/**
 * @type {Record<string, (rect1: Template.Rect, rect2: Template.Rect) => {visible: boolean, position: {left: number, top: number}}}}>}
 */
const conditions = {
  yl(rect1, rect2) {
    const ll = {
      visible: isNearly(rect1.left, rect2.left),
      position: { left: rect2.left, top: 0 }
    }

    const lr = {
      visible: isNearly(rect1.left, rect2.right),
      position: { left: rect2.right, top: 0 }
    }

    return ll.visible ? ll : lr
  },
  yc(rect1, rect2) {
    return {
      visible: isNearly(rect1.right - rect1.width / 2, rect2.right - rect2.width / 2),
      position: { left: rect2.right - rect2.width / 2, top: 0 }
    }
  },
  yr(rect1, rect2) {
    const rr = {
      visible: isNearly(rect1.right, rect2.right),
      position: { left: rect2.right, top: 0 }
    }

    const rl = {
      visible: isNearly(rect1.right, rect2.left),
      position: { left: rect2.left, top: 0 }
    }

    return rr.visible ? rr : rl
  },
  xt(rect1, rect2) {
    const tt = {
      visible: isNearly(rect1.top, rect2.top),
      position: { left: 0, top: rect2.top }
    }

    const tb = {
      visible: isNearly(rect1.top, rect2.bottom),
      position: { left: 0, top: rect2.bottom }
    }

    return tt.visible ? tt : tb
  },
  xc(rect1, rect2) {
    return {
      visible: isNearly(rect1.bottom - rect1.height / 2, rect2.bottom - rect2.height / 2),
      position: { left: 0, top: rect2.bottom - rect2.height / 2 }
    }
  },
  xb(rect1, rect2) {
    const bb = {
      visible: isNearly(rect1.bottom, rect2.bottom),
      position: { left: 0, top: rect2.bottom }
    }

    const bt = {
      visible: isNearly(rect1.bottom, rect2.top),
      position: { left: 0, top: rect2.top }
    }

    return bb.visible ? bb : bt
  }
}

定义了各个边的对比,其实很简单,就是 两个点的距离计算比较

遍历对比

/**
  * @param {Template.Event<Template.BuiltinComponent>} e
  */
const moveHandle = function (e) {
  const currentComponent = e.detail
  // 获得当前控件 rect
  const currentRect = getRect(currentComponent)

  // 遍历所有控件
  for (const [key, item] of store.components) {
    // 如果当前控件是自己则跳过
    if (key === currentComponent.uid) continue

    // 获取控件 rect
    const rect = getRect(item)

    // 成立条件数量
    let condition = 0

    // 所有条件进行一次执行
    for (const line of markLine.value) {
      const { visible, position } = conditions[line.type].apply(null, [currentRect, rect])
      line.visible = visible
      line.position = position

      if (visible) {
        condition += 1

        /**
          * @type {[number, number]}
          */
        const [x, y] = [
          position.left || currentComponent.props.position.x,
          position.top || currentComponent.props.position.y
        ]

        // 这一步是为了 让组件有个吸附效果
        switch (line.type) {
          case 'xt':
            currentComponent.props.position.x = x
            currentComponent.props.position.y = y
            break
          case 'xc':
            currentComponent.props.position.x = x
            currentComponent.props.position.y = y - currentComponent.props.size.h / 2
            break
          case 'xb':
            currentComponent.props.position.x = x
            currentComponent.props.position.y = y - currentComponent.props.size.h
            break
          case 'yc':
            currentComponent.props.position.x = x - currentComponent.props.size.w / 2
            currentComponent.props.position.y = y
            break
          case 'yl':
            currentComponent.props.position.x = x
            currentComponent.props.position.y = y
            break
          case 'yr':
            currentComponent.props.position.x = x - currentComponent.props.size.w
            currentComponent.props.position.y = y
            break
        }
      }
    }

    // 如果有一个条件成立 则取消遍历
    if (condition) break
  }
}

最后

Last update:
Contributors: 机车靓仔