IHitableShape

张开发
2026/4/3 17:58:15 15 分钟阅读
IHitableShape
IHitableShapenamespaceH.LabelImg.ShapeBox.Shapes.Base;publicinterfaceIHitableShape:IShape{boolHit(IViewview,Pointpoint);}IHitableShape 接口详解 - 可命中形状这是形状系统中最基础的交互接口定义了形状被击中点击/悬停的检测能力是所有交互功能悬停、选中、拖拽等的基石。 文件头部和命名空间namespaceH.LabelImg.ShapeBox.Shapes.Base;// 命名空间存放形状的基类和接口 IHitableShape 接口// 接口定义表示一个可以被击中命中检测的形状// 继承自 IShape所以它也是一个可以绘制的形状publicinterfaceIHitableShape:IShape{// 命中检测方法判断指定的点是否在形状内部// 参数// view - 视图对象提供缩放、变换等信息// point - 要检测的点通常是鼠标坐标// 返回值// true - 点在形状内命中// false - 点在形状外未命中boolHit(IViewview,Pointpoint);} 什么是命中检测Hit Testing命中检测就是判断一个点比如鼠标位置是否落在形状的区域内。命中Hit true: 未命中Hit false: 鼠标 ● 鼠标 ● │ │ ↓ ↓ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ 形状区域 │ │ 形状区域 │ │ │ │ │ │ ● │ ← 点在形状内 │ ● │ ← 点在形状外 └─────────────┘ └─────────────┘️ 接口继承关系IShape接口 ↓ IHitableShape接口← 当前接口 ↓ IMouseOverShape接口← 继承 IHitableShape ↓ ISelectableShape接口 ↓ IHandleShape接口为什么要这样设计// 基础形状只能绘制publicinterfaceIShape{voidDraw(...);}// 可命中形状可以绘制 可以检测点击publicinterfaceIHitableShape:IShape{boolHit(...);// 新增命中检测}// 可悬停形状继承命中检测增加悬停绘制publicinterfaceIMouseOverShape:IHitableShape{voidDrawMouseOver(...);// 悬停时用不同样式绘制}这样设计的好处功能可以逐层叠加不需要的接口就不实现。 不同形状的命中检测实现1. 矩形形状publicclassRectangleShape:IHitableShape{publicRectBounds{get;set;}publicboolHit(IViewview,Pointpoint){// 矩形命中检测判断点是否在矩形范围内returnBounds.Contains(point);}publicvoidDraw(...){/* 绘制矩形 */}}2. 圆形形状publicclassCircleShape:IHitableShape{publicPointCenter{get;set;}publicdoubleRadius{get;set;}publicboolHit(IViewview,Pointpoint){// 圆形命中检测计算点到圆心的距离doubledxpoint.X-Center.X;doubledypoint.Y-Center.Y;doubledistanceMath.Sqrt(dx*dxdy*dy);// 距离 半径 命中returndistanceRadius;}publicvoidDraw(...){/* 绘制圆形 */}}3. 椭圆形状publicclassEllipseShape:IHitableShape{publicPointCenter{get;set;}publicdoubleRadiusX{get;set;}publicdoubleRadiusY{get;set;}publicboolHit(IViewview,Pointpoint){// 椭圆命中检测椭圆方程doubledx(point.X-Center.X)/RadiusX;doubledy(point.Y-Center.Y)/RadiusY;// dx² dy² 1 表示点在椭圆内returndx*dxdy*dy1;}publicvoidDraw(...){/* 绘制椭圆 */}}4. 多边形形状publicclassPolygonShape:IHitableShape{publicListPointPoints{get;set;}publicboolHit(IViewview,Pointpoint){// 多边形命中检测射线法Ray Casting Algorithmboolinsidefalse;intcountPoints.Count;for(inti0,jcount-1;icount;ji){PointpiPoints[i];PointpjPoints[j];// 检查射线是否与边相交boolintersect((pi.Ypoint.Y)!(pj.Ypoint.Y))(point.X(pj.X-pi.X)*(point.Y-pi.Y)/(pj.Y-pi.Y)pi.X);if(intersect)inside!inside;}returninside;}publicvoidDraw(...){/* 绘制多边形 */}}射线法原理从点向右发射一条射线 与多边形边相交次数为奇数 内部 与多边形边相交次数为偶数 外部 ● 外部相交2次偶数 ┌───────┐ │ │ │ ● │ ← 内部相交1次奇数 │ │ └───────┘ ● 外部相交0次偶数5. 线条形状带容差publicclassLineShape:IHitableShape{publicPointStart{get;set;}publicPointEnd{get;set;}publicdoubleTolerance{get;set;}5;// 点击容差像素publicboolHit(IViewview,Pointpoint){// 线条命中检测计算点到线段的最短距离doubledistanceDistanceToSegment(point,Start,End);// 距离小于容差 命中returndistanceTolerance;}privatedoubleDistanceToSegment(Pointp,Pointa,Pointb){// 计算点到线段距离的数学公式doublevxb.X-a.X;doublevyb.Y-a.Y;doublewxp.X-a.X;doublewyp.Y-a.Y;doublelen2vx*vxvy*vy;if(len20)returnMath.Sqrt(wx*wxwy*wy);doublet(wx*vxwy*vy)/len2;if(t0)t0;if(t1)t1;doublecxa.Xt*vx;doublecya.Yt*vy;doubledxp.X-cx;doubledyp.Y-cy;returnMath.Sqrt(dx*dxdy*dy);}publicvoidDraw(...){/* 绘制线条 */}}线条容差的意义线条本身只有1像素宽很难精确点击 所以给一个容差比如5像素 容差区域5像素 ┌─────────────────┐ │ │ │ ● 鼠标点击 │ ← 虽然没点在线上 │ │ │ 但在容差范围内 │ ↓ │ 也算命中 │ ═══════ │ ← 实际线条 │ │ └─────────────────┘6. 文本形状使用格式化文本的命中检测publicclassTextShape:IHitableShape{publicPointPosition{get;set;}publicstringText{get;set;}publicdoubleFontSize{get;set;}12;publicboolHit(IViewview,Pointpoint){// 创建格式化文本对象varformattedTextnewFormattedText(Text,System.Globalization.CultureInfo.CurrentCulture,FlowDirection.LeftToRight,newTypeface(Microsoft YaHei),FontSize,Brushes.Black,1.2);// 计算文本占用的矩形区域RecttextRectnewRect(Position,newSize(formattedText.Width,formattedText.Height));// 判断点是否在文本区域内returntextRect.Contains(point);}publicvoidDraw(...){/* 绘制文本 */}} 在标注工具中的应用应用1点击选择形状// 在 SelectShapeBox 中protectedoverridevoidOnMouseDown(MouseButtonEventArgse){Pointpointe.GetPosition(this);// 从后往前查找优先选择上层的形状for(intiShapes.Count-1;i0;i--){varshapeShapes[i];// 只关心可命中的形状if(shapeisIHitableShapehitable){if(hitable.Hit(this,point)){// 选中这个形状this.SelectShapes(shapeasISelectableShape);break;}}}}应用2鼠标悬停高亮// 在 MouseOverShapeBox 中protectedoverridevoidOnMouseMove(MouseEventArgse){Pointpointe.GetPosition(this);// 查找所有鼠标下的形状varhitsShapes.OfTypeIMouseOverShape().Where(xx.Hit(this,point)).ToList();// 高亮这些形状this.MouseOverShapes(hits.ToArray());}应用3拖拽移动形状// 拖拽逻辑privateIShape_draggingShape;privatePoint_dragStartPoint;protectedoverridevoidOnMouseDown(MouseButtonEventArgse){Pointpointe.GetPosition(this);// 查找被点击的形状foreach(varshapeinShapes){if(shapeisIHitableShapehitablehitable.Hit(this,point)){_draggingShapeshape;_dragStartPointpoint;break;}}}protectedoverridevoidOnMouseMove(MouseEventArgse){if(_draggingShape!null){Pointcurrente.GetPosition(this);Vectoroffsetcurrent-_dragStartPoint;// 移动形状MoveShape(_draggingShape,offset);_dragStartPointcurrent;this.DrawShapes();// 重绘}} 与缩放Scale的关系publicinterfaceIHitableShape{boolHit(IViewview,Pointpoint);}// 在实现时需要考虑视图的缩放publicclassRectangleShape:IHitableShape{publicRectBounds{get;set;}// 逻辑坐标原始尺寸publicboolHit(IViewview,Pointpoint){// 将点转换到形状的坐标系// 或者将形状边界转换到视图坐标// 方法1转换点PointtransformednewPoint(point.X/view.Scale,point.Y/view.Scale);returnBounds.Contains(transformed);// 方法2转换边界// Rect viewBounds new Rect(// Bounds.X * view.Scale,// Bounds.Y * view.Scale,// Bounds.Width * view.Scale,// Bounds.Height * view.Scale// );// return viewBounds.Contains(point);}} 设计模式分析1. 接口隔离原则ISPIHitableShape 只包含命中检测 IMouseOverShape 增加悬停绘制 ISelectableShape 增加选中绘制每个接口只负责一个明确的功能。2. 策略模式不同的形状实现不同的Hit策略矩形矩形包含检测圆形距离检测多边形射线法线条点到线段距离3. 里氏替换原则LSP// 任何 IHitableShape 都可以替换使用voidProcessHit(IHitableShapeshape,Pointpoint){if(shape.Hit(view,point)){// 处理命中}}// 传入任何实现都可以正常工作ProcessHit(newRectangleShape(),point);ProcessHit(newCircleShape(),point);ProcessHit(newPolygonShape(),point);总结组件类型职责IHitableShape接口定义命中检测能力核心功能Hit()判断点是否在形状内为什么需要这个接口统一交互接口所有可交互的形状都实现这个接口解耦控件不需要知道具体形状类型只需要知道它可以被命中扩展性添加新形状时只需实现Hit()方法即可支持交互这个接口是整个形状交互系统的基石没有它鼠标悬停、点击选择、拖拽移动等功能都无法实现

更多文章