You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FabricJs+Angular实现类Acrobat长度测量工具的技术问询

实现Acrobat式长度测量的具体建议

一、交互流程的状态管理

在Angular组件里维护测量状态机,通过FabricJS画布事件驱动三步流程:

  • 定义状态枚举:Idle(未开始)、SelectingStart(选起点后等终点)、SelectingEnd(选终点后等确认)
  • 监听mouse:down事件切换状态:
    1. 首次点击记录起点,生成半透明临时引导线,切换到SelectingStart
    2. 二次点击记录终点,固定临时线,切换到SelectingEnd
    3. 三次点击生成正式测量对象,重置状态
  • 监听mouse:move事件,在SelectingStart状态下实时更新临时线位置

核心代码示例:

enum MeasureState { Idle, SelectingStart, SelectingEnd }

private currentState = MeasureState.Idle;
private startPoint: fabric.Point | null = null;
private endPoint: fabric.Point | null = null;
private tempLine: fabric.Line | null = null;

private setupCanvasEvents() {
  const canvas = this.getCanvas();
  canvas.on('mouse:down', (e) => {
    const pointer = canvas.getPointer(e.e);
    switch(this.currentState) {
      case MeasureState.Idle:
        this.startPoint = new fabric.Point(pointer.x, pointer.y);
        this.currentState = MeasureState.SelectingStart;
        this.tempLine = new fabric.Line([this.startPoint.x, this.startPoint.y, pointer.x, pointer.y], {
          stroke: '#0078d4', strokeDashArray: [5,3], selectable: false
        });
        canvas.add(this.tempLine);
        break;
      case MeasureState.SelectingStart:
        this.endPoint = new fabric.Point(pointer.x, pointer.y);
        this.currentState = MeasureState.SelectingEnd;
        this.tempLine?.set({x2: pointer.x, y2: pointer.y});
        canvas.renderAll();
        break;
      case MeasureState.SelectingEnd:
        this.createMeasureObject(this.startPoint!, this.endPoint!);
        canvas.remove(this.tempLine!);
        this.resetMeasureState();
        break;
    }
  });

  canvas.on('mouse:move', (e) => {
    if(this.currentState === MeasureState.SelectingStart && this.tempLine && this.startPoint) {
      const pointer = canvas.getPointer(e.e);
      this.tempLine.set({x2: pointer.x, y2: pointer.y});
      canvas.renderAll();
    }
  });
}

private resetMeasureState() {
  this.tempLine = null;
  this.startPoint = null;
  this.endPoint = null;
  this.currentState = MeasureState.Idle;
}

二、短距离箭头外侧逻辑实现

基于线条长度与箭头尺寸的比例判断短距离,动态计算箭头顶点坐标:

  1. 先计算线条的长度、方向向量和垂直向量
  2. 定义箭头尺寸(如宽度10、长度15),当线条长度小于箭头宽度2倍时判定为短距离
  3. 非短距离时箭头指向线条中心;短距离时箭头指向线条外侧,顶点向线条延伸方向偏移

核心代码示例:

const ARROW_WIDTH = 10;
const ARROW_LENGTH = 15;

private calculateLineProps(start: fabric.Point, end: fabric.Point) {
  const dx = end.x - start.x;
  const dy = end.y - start.y;
  const length = Math.sqrt(dx*dx + dy*dy);
  const dirX = dx / length;
  const dirY = dy / length;
  const perpX = -dirY;
  const perpY = dirX;
  return { length, dirX, dirY, perpX, perpY };
}

private createArrowPoints(basePoint: fabric.Point, dirX: number, dirY: number, isStartArrow: boolean, isShort: boolean) {
  const arrowDirX = isShort ? (isStartArrow ? -dirX : dirX) : (isStartArrow ? dirX : -dirX);
  const arrowDirY = isShort ? (isStartArrow ? -dirY : dirY) : (isStartArrow ? dirY : -dirY);

  const tipPoint = new fabric.Point(
    basePoint.x + arrowDirX * ARROW_LENGTH,
    basePoint.y + arrowDirY * ARROW_LENGTH
  );
  const perpX = -arrowDirY;
  const perpY = arrowDirX;

  return [
    tipPoint.x, tipPoint.y,
    basePoint.x + perpX * ARROW_WIDTH/2, basePoint.y + perpY * ARROW_WIDTH/2,
    basePoint.x - perpX * ARROW_WIDTH/2, basePoint.y - perpY * ARROW_WIDTH/2
  ];
}

三、长度文本与组合对象处理

  1. 计算线条中点作为文本放置位置,可选择让文本与线条平行或保持水平
  2. 将主线条、两个箭头、长度文本组合为fabric.Group,支持整体选择、编辑
  3. 格式化长度值(如保留两位小数、添加单位)

核心代码示例:

private createMeasureText(start: fabric.Point, end: fabric.Point, length: number) {
  const midPoint = new fabric.Point((start.x+end.x)/2, (start.y+end.y)/2);
  const angle = Math.atan2(end.y - start.y, end.x - start.x) * 180 / Math.PI;
  return new fabric.Text(`${length.toFixed(2)} mm`, {
    left: midPoint.x, top: midPoint.y, fill: '#000', fontSize:12,
    originX: 'center', originY: 'center', angle: angle
  });
}

private createMeasureObject(start: fabric.Point, end: fabric.Point) {
  const props = this.calculateLineProps(start, end);
  const isShort = props.length < ARROW_WIDTH * 2;

  const mainLine = new fabric.Line([start.x, start.y, end.x, end.y], {stroke: '#0078d4', strokeWidth:1});
  const startArrow = new fabric.Polygon(this.createArrowPoints(start, props.dirX, props.dirY, true, isShort), {fill: '#0078d4', selectable: false});
  const endArrow = new fabric.Polygon(this.createArrowPoints(end, props.dirX, props.dirY, false, isShort), {fill: '#0078d4', selectable: false});
  const measureText = this.createMeasureText(start, end, props.length);

  const measureGroup = new fabric.Group([mainLine, startArrow, endArrow, measureText], {
    selectable: true, hasControls: true
  });
  this.getCanvas().add(measureGroup);
}

四、Angular环境适配要点

  • 在ngAfterViewInit钩子中初始化FabricJS画布和事件监听,确保DOM加载完成
  • 封装FabricService统一管理画布实例,减少组件冗余代码
  • 在ngOnDestroy钩子中解绑画布事件,避免内存泄漏:
ngOnDestroy() {
  const canvas = this.getCanvas();
  canvas.off('mouse:down');
  canvas.off('mouse:move');
}

内容的提问来源于stack exchange,提问作者muton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:52:39