FabricJs+Angular实现类Acrobat长度测量工具的技术问询
实现Acrobat式长度测量的具体建议
一、交互流程的状态管理
在Angular组件里维护测量状态机,通过FabricJS画布事件驱动三步流程:
- 定义状态枚举:
Idle(未开始)、SelectingStart(选起点后等终点)、SelectingEnd(选终点后等确认) - 监听
mouse:down事件切换状态:- 首次点击记录起点,生成半透明临时引导线,切换到
SelectingStart - 二次点击记录终点,固定临时线,切换到
SelectingEnd - 三次点击生成正式测量对象,重置状态
- 首次点击记录起点,生成半透明临时引导线,切换到
- 监听
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; }
二、短距离箭头外侧逻辑实现
基于线条长度与箭头尺寸的比例判断短距离,动态计算箭头顶点坐标:
- 先计算线条的长度、方向向量和垂直向量
- 定义箭头尺寸(如宽度10、长度15),当线条长度小于箭头宽度2倍时判定为短距离
- 非短距离时箭头指向线条中心;短距离时箭头指向线条外侧,顶点向线条延伸方向偏移
核心代码示例:
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 ]; }
三、长度文本与组合对象处理
- 计算线条中点作为文本放置位置,可选择让文本与线条平行或保持水平
- 将主线条、两个箭头、长度文本组合为
fabric.Group,支持整体选择、编辑 - 格式化长度值(如保留两位小数、添加单位)
核心代码示例:
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
相关产品推荐
相关产品推荐

