如何在FMX.Objects.TPath中实现混合绘制顺序下的正确填充?
解决FMX TPath导入DXF后无法自动填充的方案
核心思路
针对TPath自动填充的三个规则(链式连续、方向正确、无多余MoveTo),对DXF导出的分散图元进行预处理,转换成符合要求的路径数据。
具体步骤
1. 提取DXF中的闭合轮廓
- 遍历DXF文件中的图元(LINE、LWPOLYLINE、POLYLINE等),筛选出闭合轮廓:
- 对于
LWPOLYLINE,检查其Closed标志(DXF中Group Code=70,值含1表示闭合),直接提取顶点序列。 - 对于独立的
LINE集合,判断是否能形成闭合环(所有线段端点首尾相连,最终回到起点)。 - 忽略非闭合的图元(TPath开放区域无法自动填充)。
- 对于
2. 将分散图元拼接为链式连续路径
对于由多个独立线段组成的闭合轮廓,消除多余的MoveTo,构建连续的顶点链:
- 收集所有线段的起点、终点,建立端点映射表(记录每个端点关联的线段)。
- 从任意一条未使用的线段开始,依次寻找与当前端点重合的下一条线段,将顶点按顺序拼接,直到回到轮廓起点。
- 拼接过程中仅保留第一个
MoveTo(定位轮廓起点),后续全部使用LineTo,避免插入多余的MoveTo。
示例代码(Delphi):
type TLineSeg = record StartPt: TPointF; EndPt: TPointF; Used: Boolean; end; // 判断两点是否重合(考虑浮点误差) function PointsEqual(const A, B: TPointF): Boolean; const Epsilon = 1e-6; begin Result := (Abs(A.X - B.X) < Epsilon) and (Abs(A.Y - B.Y) < Epsilon); end; // 从分散线段构建连续闭合轮廓的顶点序列 function BuildContour(var Lines: TArray<TLineSeg>): TArray<TPointF>; var CurrentSegIdx: Integer; CurrentPt: TPointF; I: Integer; begin SetLength(Result, 0); // 找到第一个未使用的线段 CurrentSegIdx := -1; for I := 0 to High(Lines) do if not Lines[I].Used then begin CurrentSegIdx := I; Break; end; if CurrentSegIdx = -1 then Exit; // 初始化轮廓起点 Lines[CurrentSegIdx].Used := True; CurrentPt := Lines[CurrentSegIdx].StartPt; Result := Result + [CurrentPt]; CurrentPt := Lines[CurrentSegIdx].EndPt; Result := Result + [CurrentPt]; // 循环拼接后续线段 while True do begin var Found := False; for I := 0 to High(Lines) do begin if not Lines[I].Used then begin // 匹配当前端点作为线段起点 if PointsEqual(Lines[I].StartPt, CurrentPt) then begin Lines[I].Used := True; CurrentPt := Lines[I].EndPt; Result := Result + [CurrentPt]; Found := True; Break; end // 匹配当前端点作为线段终点(反向拼接) else if PointsEqual(Lines[I].EndPt, CurrentPt) then begin Lines[I].Used := True; CurrentPt := Lines[I].StartPt; Result := Result + [CurrentPt]; Found := True; Break; end; end; end; if not Found then Break; // 检查是否回到轮廓起点,完成闭合 if PointsEqual(CurrentPt, Result[0]) then Break; end; end;
3. 统一路径绘制方向
TPath的填充依赖非零环绕规则,方向错误会导致填充区域异常:
- 使用鞋带公式计算轮廓的面积符号,判断方向:面积为正表示逆时针,为负表示顺时针。
- 统一外轮廓方向(比如全部设为逆时针),内轮廓(嵌套在其他轮廓内的)则设置为相反方向(顺时针),确保挖空区域正确填充。
示例代码(计算面积判断方向):
function GetContourArea(const Contour: TArray<TPointF>): Double; var I, N: Integer; begin Result := 0; N := Length(Contour); if N < 3 then Exit; for I := 0 to N-1 do begin var J := (I + 1) mod N; Result := Result + (Contour[I].X * Contour[J].Y) - (Contour[J].X * Contour[I].Y); end; Result := Result * 0.5; end; // 反转轮廓顶点顺序,改变绘制方向 function ReverseContour(const Contour: TArray<TPointF>): TArray<TPointF>; var I: Integer; begin SetLength(Result, Length(Contour)); for I := 0 to High(Contour) do Result[I] := Contour[High(Contour) - I]; end;
4. 生成TPath可识别的路径数据
将处理后的轮廓序列转换为TPath的Data属性:
- 每个闭合轮廓仅调用一次
MoveTo(定位起点),后续顶点全部用LineTo连接。 - 最后调用
ClosePath确保轮廓闭合(也可依赖最后一个顶点与起点重合)。 - 多个嵌套轮廓依次添加到同一
TPathData中,注意方向相反。
示例代码:
procedure LoadProcessedContoursToPath(Path: TPath; const Contours: TArray<TArray<TPointF>>); var PathData: TPathData; Contour: TArray<TPointF>; Pt: TPointF; begin PathData := TPathData.Create; try for Contour in Contours do begin if Length(Contour) < 3 then Continue; // 移动到轮廓起点 PathData.MoveTo(Contour[0]); // 绘制所有后续顶点 for Pt in Contour[1..High(Contour)] do PathData.LineTo(Pt); // 闭合路径 PathData.ClosePath; end; Path.Data := PathData; finally PathData.Free; end; end;
关键注意事项
- 处理DXF时要考虑浮点精度问题,判断端点重合时需加入误差阈值(如1e-6)。
- 嵌套轮廓的包含关系需要用点-in-多边形算法(射线法)判断,确保方向调整正确。
- 对于DXF中的复杂图元(如ARC、SPLINE),需要先转换为折线(多段LINE)再进行拼接处理。
内容的提问来源于stack exchange,提问作者engerex
相关产品推荐
相关产品推荐

