2
0

Clipper.Offset.pas 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. unit Clipper.Offset;
  2. (*******************************************************************************
  3. * Author : Angus Johnson *
  4. * Date : 6 July 2024 *
  5. * Website : http://www.angusj.com *
  6. * Copyright : Angus Johnson 2010-2024 *
  7. * Purpose : Path Offset (Inflate/Shrink) *
  8. * License : http://www.boost.org/LICENSE_1_0.txt *
  9. *******************************************************************************)
  10. {$I Clipper.inc}
  11. interface
  12. uses
  13. Classes, Clipper.Core, Clipper.Engine;
  14. type
  15. TJoinType = (jtMiter, jtSquare, jtBevel, jtRound);
  16. //jtSquare: Joins are 'squared' at exactly the offset distance (complex code)
  17. //jtBevel : offset distances vary depending on the angle (simple code, faster)
  18. TEndType = (etPolygon, etJoined, etButt, etSquare, etRound);
  19. // etButt : offsets both sides of a path, with square blunt ends
  20. // etSquare : offsets both sides of a path, with square extended ends
  21. // etRound : offsets both sides of a path, with round extended ends
  22. // etJoined : offsets both sides of a path, with joined ends
  23. // etPolygon: offsets only one side of a closed path
  24. TDeltaCallback64 = function (const path: TPath64;
  25. const path_norms: TPathD; currIdx, prevIdx: integer): double of object;
  26. TDoubleArray = array of double;
  27. BooleanArray = array of Boolean;
  28. TGroup = class
  29. paths : TPaths64;
  30. joinType : TJoinType;
  31. endType : TEndType;
  32. reversed : Boolean;
  33. lowestPathIdx : integer;
  34. constructor Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType);
  35. end;
  36. TClipperOffset = class
  37. private
  38. fDelta : Double;
  39. fGroupDelta : Double; //*0.5 for open paths; *-1.0 for neg areas
  40. fMinLenSqrd : double;
  41. fJoinType : TJoinType;
  42. fEndType : TEndType;
  43. fTmpLimit : Double;
  44. fMiterLimit : Double;
  45. fArcTolerance : Double;
  46. fStepsPerRad : Double;
  47. fStepSin : Double;
  48. fStepCos : Double;
  49. fNorms : TPathD;
  50. fGroupList : TListEx;
  51. fInPath : TPath64;
  52. fOutPath : TPath64;
  53. fOutPathLen : Integer;
  54. fSolution : TPaths64;
  55. fSolutionLen : Integer;
  56. fSolutionTree : TPolyTree64;
  57. fPreserveCollinear : Boolean;
  58. fReverseSolution : Boolean;
  59. fDeltaCallback64 : TDeltaCallback64;
  60. {$IFDEF USINGZ}
  61. fZCallback64 : TZCallback64;
  62. procedure ZCB(const bot1, top1, bot2, top2: TPoint64;
  63. var intersectPt: TPoint64);
  64. procedure AddPoint(x,y: double; z: Int64); overload;
  65. procedure AddPoint(const pt: TPoint64); overload;
  66. {$IFDEF INLINING} inline; {$ENDIF}
  67. procedure AddPoint(const pt: TPoint64; newZ: Int64); overload;
  68. {$IFDEF INLINING} inline; {$ENDIF}
  69. {$ELSE}
  70. procedure AddPoint(x,y: double); overload;
  71. procedure AddPoint(const pt: TPoint64); overload;
  72. {$IFDEF INLINING} inline; {$ENDIF}
  73. {$ENDIF}
  74. procedure DoSquare(j, k: Integer);
  75. procedure DoBevel(j, k: Integer);
  76. procedure DoMiter(j, k: Integer; cosA: Double);
  77. procedure DoRound(j, k: integer; angle: double);
  78. procedure OffsetPoint(j: Integer; var k: integer);
  79. procedure BuildNormals;
  80. procedure DoGroupOffset(group: TGroup);
  81. procedure OffsetPolygon;
  82. procedure OffsetOpenJoined;
  83. procedure OffsetOpenPath;
  84. function CalcSolutionCapacity: integer;
  85. procedure UpdateSolution; {$IFDEF INLINING} inline; {$ENDIF}
  86. function CheckReverseOrientation: Boolean;
  87. procedure ExecuteInternal(delta: Double);
  88. public
  89. constructor Create(miterLimit: double = 2.0;
  90. arcTolerance: double = 0.0;
  91. PreserveCollinear: Boolean = False;
  92. ReverseSolution: Boolean = False);
  93. destructor Destroy; override;
  94. procedure AddPath(const path: TPath64;
  95. joinType: TJoinType; endType: TEndType);
  96. procedure AddPaths(const paths: TPaths64;
  97. joinType: TJoinType; endType: TEndType);
  98. procedure Clear;
  99. procedure Execute(delta: Double; out solution: TPaths64); overload;
  100. procedure Execute(delta: Double; polytree: TPolyTree64); overload;
  101. procedure Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64); overload;
  102. // MiterLimit: needed for mitered offsets (see offset_triginometry3.svg)
  103. property MiterLimit: Double read fMiterLimit write fMiterLimit;
  104. // ArcTolerance: needed for rounded offsets (See offset_triginometry2.svg)
  105. property ArcTolerance: Double read fArcTolerance write fArcTolerance;
  106. property PreserveCollinear: Boolean
  107. read fPreserveCollinear write fPreserveCollinear;
  108. property ReverseSolution: Boolean
  109. read fReverseSolution write fReverseSolution;
  110. property DeltaCallback: TDeltaCallback64 read
  111. fDeltaCallback64 write fDeltaCallback64;
  112. {$IFDEF USINGZ}
  113. property ZCallback: TZCallback64 read fZCallback64 write fZCallback64;
  114. {$ENDIF}
  115. end;
  116. implementation
  117. uses
  118. Math;
  119. resourcestring
  120. rsClipper_CoordRangeError =
  121. 'Offsetting will exceed the valid coordinate range';
  122. const
  123. TwoPi : Double = 2 * PI;
  124. InvTwoPi : Double = 1/(2 * PI);
  125. //------------------------------------------------------------------------------
  126. // Miscellaneous offset support functions
  127. //------------------------------------------------------------------------------
  128. function DotProduct(const vec1, vec2: TPointD): double;
  129. {$IFDEF INLINING} inline; {$ENDIF}
  130. begin
  131. result := vec1.X * vec2.X + vec1.Y * vec2.Y;
  132. end;
  133. //------------------------------------------------------------------------------
  134. function ValueAlmostZero(val: double; epsilon: double = 0.001): Boolean;
  135. {$IFDEF INLINE} inline; {$ENDIF}
  136. begin
  137. Result := Abs(val) < epsilon;
  138. end;
  139. //------------------------------------------------------------------------------
  140. function NormalizeVector(const vec: TPointD): TPointD;
  141. {$IFDEF INLINE} inline; {$ENDIF}
  142. var
  143. h, inverseHypot: Double;
  144. begin
  145. h := Hypot(vec.X, vec.Y);
  146. if ValueAlmostZero(h) then
  147. begin
  148. Result := NullPointD;
  149. Exit;
  150. end;
  151. inverseHypot := 1 / h;
  152. Result.X := vec.X * inverseHypot;
  153. Result.Y := vec.Y * inverseHypot;
  154. end;
  155. //------------------------------------------------------------------------------
  156. function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD;
  157. begin
  158. Result := NormalizeVector(PointD(vec1.X + vec2.X, vec1.Y + vec2.Y));
  159. end;
  160. //------------------------------------------------------------------------------
  161. function GetUnitNormal(const pt1, pt2: TPoint64): TPointD;
  162. var
  163. dx, dy, inverseHypot: Double;
  164. begin
  165. dx := (pt2.X - pt1.X);
  166. dy := (pt2.Y - pt1.Y);
  167. if (dx = 0) and (dy = 0) then
  168. begin
  169. Result.X := 0;
  170. Result.Y := 0;
  171. end else
  172. begin
  173. inverseHypot := 1 / Hypot(dx, dy);
  174. Result.X := dy * inverseHypot;
  175. Result.Y := -dx * inverseHypot; //ie left side of vector
  176. end;
  177. end;
  178. //------------------------------------------------------------------------------
  179. function GetLowestPolygonIdx(const paths: TPaths64): integer;
  180. var
  181. i,j: integer;
  182. botPt: TPoint64;
  183. begin
  184. Result := -1;
  185. botPt := Point64(MaxInt64, MinInt64);
  186. for i := 0 to High(paths) do
  187. begin
  188. for j := 0 to High(paths[i]) do
  189. with paths[i][j] do
  190. begin
  191. if (Y < botPt.Y) or
  192. ((Y = botPt.Y) and (X >= botPt.X)) then Continue;
  193. result := i;
  194. botPt.X := X;
  195. botPt.Y := Y;
  196. end;
  197. end;
  198. end;
  199. //------------------------------------------------------------------------------
  200. function UnsafeGet(List: TList; Index: Integer): Pointer;
  201. {$IFDEF INLINING} inline; {$ENDIF}
  202. begin
  203. Result := List.List[Index];
  204. end;
  205. //------------------------------------------------------------------------------
  206. // TGroup methods
  207. //------------------------------------------------------------------------------
  208. constructor TGroup.Create(const pathsIn: TPaths64; jt: TJoinType; et: TEndType);
  209. var
  210. i, len: integer;
  211. isJoined: boolean;
  212. begin
  213. Self.joinType := jt;
  214. Self.endType := et;
  215. isJoined := et in [etPolygon, etJoined];
  216. len := Length(pathsIn);
  217. SetLength(paths, len);
  218. for i := 0 to len -1 do
  219. paths[i] := StripDuplicates(pathsIn[i], isJoined);
  220. reversed := false;
  221. if (et = etPolygon) then
  222. begin
  223. // the lowermost path must be an outer path, so if its orientation is
  224. // negative, then flag that the whole group is 'reversed' (so negate
  225. // delta etc.) as this is much more efficient than reversing every path.
  226. lowestPathIdx := GetLowestPolygonIdx(pathsIn);
  227. reversed := (lowestPathIdx >= 0) and (Area(pathsIn[lowestPathIdx]) < 0);
  228. end else
  229. lowestPathIdx := -1;
  230. end;
  231. //------------------------------------------------------------------------------
  232. // TClipperOffset methods
  233. //------------------------------------------------------------------------------
  234. constructor TClipperOffset.Create(miterLimit: double;
  235. arcTolerance: double; PreserveCollinear: Boolean;
  236. ReverseSolution: Boolean);
  237. begin
  238. fMiterLimit := MiterLimit;
  239. fArcTolerance := ArcTolerance;
  240. fGroupList := TListEx.Create;
  241. fPreserveCollinear := preserveCollinear;
  242. fReverseSolution := ReverseSolution;
  243. end;
  244. //------------------------------------------------------------------------------
  245. destructor TClipperOffset.Destroy;
  246. begin
  247. Clear;
  248. fGroupList.Free;
  249. inherited;
  250. end;
  251. //------------------------------------------------------------------------------
  252. procedure TClipperOffset.Clear;
  253. var
  254. i: integer;
  255. begin
  256. for i := 0 to fGroupList.Count -1 do
  257. TGroup(fGroupList[i]).Free;
  258. fGroupList.Clear;
  259. fSolution := nil;
  260. fSolutionLen := 0;
  261. end;
  262. //------------------------------------------------------------------------------
  263. procedure TClipperOffset.AddPath(const path: TPath64;
  264. joinType: TJoinType; endType: TEndType);
  265. var
  266. paths: TPaths64;
  267. begin
  268. if not assigned(path) then Exit;
  269. SetLength(paths, 1);
  270. paths[0] := path;
  271. AddPaths(Paths, joinType, endType);
  272. end;
  273. //------------------------------------------------------------------------------
  274. procedure TClipperOffset.AddPaths(const paths: TPaths64;
  275. joinType: TJoinType; endType: TEndType);
  276. var
  277. group: TGroup;
  278. begin
  279. if Length(paths) = 0 then Exit;
  280. group := TGroup.Create(paths, joinType, endType);
  281. fGroupList.Add(group);
  282. end;
  283. //------------------------------------------------------------------------------
  284. function GetPerpendic(const pt: TPoint64; const norm: TPointD; delta: double): TPoint64; overload;
  285. {$IFDEF INLINING} inline; {$ENDIF}
  286. begin
  287. result := Point64(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
  288. {$IFDEF USINGZ}
  289. result.Z := pt.Z;
  290. {$ENDIF}
  291. end;
  292. //------------------------------------------------------------------------------
  293. function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): TPointD; overload;
  294. {$IFDEF INLINING} inline; {$ENDIF}
  295. begin
  296. result := PointD(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
  297. {$IFDEF USINGZ}
  298. result.Z := pt.Z;
  299. {$ENDIF}
  300. end;
  301. //------------------------------------------------------------------------------
  302. procedure TClipperOffset.DoGroupOffset(group: TGroup);
  303. var
  304. i,j, len, steps: Integer;
  305. r, stepsPer360, arcTol: Double;
  306. absDelta: double;
  307. rec: TRect64;
  308. pt0: TPoint64;
  309. begin
  310. if group.endType = etPolygon then
  311. begin
  312. if (group.lowestPathIdx < 0) then fDelta := Abs(fDelta);
  313. fGroupDelta := Iif(group.reversed, -fDelta, fDelta);
  314. end
  315. else
  316. fGroupDelta := Abs(fDelta);
  317. absDelta := Abs(fGroupDelta);
  318. fJoinType := group.joinType;
  319. fEndType := group.endType;
  320. if (group.joinType = jtRound) or (group.endType = etRound) then
  321. begin
  322. // calculate the number of steps required to approximate a circle
  323. // (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm)
  324. // arcTol - when arc_tolerance_ is undefined (0) then curve imprecision
  325. // will be relative to the size of the offset (delta). Obviously very
  326. //large offsets will almost always require much less precision.
  327. arcTol := Iif(fArcTolerance > 0.01,
  328. Min(absDelta, fArcTolerance),
  329. Log10(2 + absDelta) * 0.25); // empirically derived
  330. stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta);
  331. if (stepsPer360 > absDelta * Pi) then
  332. stepsPer360 := absDelta * Pi; // avoid excessive precision
  333. fStepSin := sin(TwoPi/stepsPer360);
  334. fStepCos := cos(TwoPi/stepsPer360);
  335. if (fGroupDelta < 0.0) then fStepSin := -fStepSin;
  336. fStepsPerRad := stepsPer360 / TwoPi;
  337. end;
  338. for i := 0 to High(group.paths) do
  339. begin
  340. fInPath := group.paths[i];
  341. fNorms := nil;
  342. len := Length(fInPath);
  343. //if a single vertex then build a circle or a square ...
  344. if len = 1 then
  345. begin
  346. if fGroupDelta < 1 then Continue;
  347. pt0 := fInPath[0];
  348. if Assigned(fDeltaCallback64) then
  349. begin
  350. fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0);
  351. if TGroup(fGroupList[0]).reversed then fGroupDelta := -fGroupDelta;
  352. absDelta := Abs(fGroupDelta);
  353. end;
  354. if (group.endType = etRound) then
  355. begin
  356. r := absDelta;
  357. steps := Ceil(fStepsPerRad * TwoPi); //#617
  358. fOutPath := Path64(Ellipse(
  359. RectD(pt0.X-r, pt0.Y-r, pt0.X+r, pt0.Y+r), steps));
  360. {$IFDEF USINGZ}
  361. for j := 0 to high(fOutPath) do
  362. fOutPath[j].Z := pt0.Z;
  363. {$ENDIF}
  364. end else
  365. begin
  366. j := Round(absDelta);
  367. rec := Rect64(pt0.X -j, pt0.Y -j, pt0.X+j, pt0.Y+j);
  368. fOutPath := rec.AsPath;
  369. {$IFDEF USINGZ}
  370. for j := 0 to high(fOutPath) do
  371. fOutPath[j].Z := pt0.Z;
  372. {$ENDIF}
  373. end;
  374. UpdateSolution;
  375. Continue;
  376. end; // end of offsetting a single point
  377. if (len = 2) and (group.endType = etJoined) then
  378. begin
  379. if fJoinType = jtRound then
  380. fEndType := etRound else
  381. fEndType := etSquare;
  382. end;
  383. BuildNormals;
  384. if fEndType = etPolygon then
  385. OffsetPolygon
  386. else if fEndType = etJoined then
  387. OffsetOpenJoined
  388. else
  389. OffsetOpenPath;
  390. end;
  391. end;
  392. //------------------------------------------------------------------------------
  393. procedure TClipperOffset.BuildNormals;
  394. var
  395. i, len: integer;
  396. begin
  397. len := Length(fInPath);
  398. SetLength(fNorms, len);
  399. if len = 0 then Exit;
  400. for i := 0 to len-2 do
  401. fNorms[i] := GetUnitNormal(fInPath[i], fInPath[i+1]);
  402. fNorms[len -1] := GetUnitNormal(fInPath[len -1], fInPath[0]);
  403. end;
  404. //------------------------------------------------------------------------------
  405. procedure TClipperOffset.UpdateSolution;
  406. begin
  407. if fOutPathLen = 0 then Exit;
  408. SetLength(fOutPath, fOutPathLen);
  409. fSolution[fSolutionLen] := fOutPath;
  410. inc(fSolutionLen);
  411. fOutPath := nil;
  412. fOutPathLen := 0;
  413. end;
  414. //------------------------------------------------------------------------------
  415. function TClipperOffset.CalcSolutionCapacity: integer;
  416. var
  417. i: integer;
  418. begin
  419. Result := 0;
  420. for i := 0 to fGroupList.Count -1 do
  421. with TGroup(fGroupList[i]) do
  422. if endType = etJoined then
  423. inc(Result, Length(paths) *2) else
  424. inc(Result, Length(paths));
  425. end;
  426. //------------------------------------------------------------------------------
  427. procedure TClipperOffset.OffsetPolygon;
  428. var
  429. i,j: integer;
  430. begin
  431. j := high(fInPath);
  432. for i := 0 to high(fInPath) do
  433. OffsetPoint(i, j);
  434. UpdateSolution;
  435. end;
  436. //------------------------------------------------------------------------------
  437. procedure TClipperOffset.OffsetOpenJoined;
  438. begin
  439. OffsetPolygon;
  440. fInPath := ReversePath(fInPath);
  441. // Rebuild normals // BuildNormals;
  442. fNorms := ReversePath(fNorms);
  443. fNorms := ShiftPath(fNorms, 1);
  444. fNorms := NegatePath(fNorms);
  445. OffsetPolygon;
  446. end;
  447. //------------------------------------------------------------------------------
  448. procedure TClipperOffset.OffsetOpenPath;
  449. var
  450. i, k, highI: integer;
  451. begin
  452. highI := high(fInPath);
  453. if Assigned(fDeltaCallback64) then
  454. fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0);
  455. if (Abs(fGroupDelta) < Tolerance) and
  456. not Assigned(fDeltaCallback64) then
  457. begin
  458. inc(highI);
  459. SetLength(fOutPath, highI);
  460. Move(fInPath[0], fOutPath, highI + SizeOf(TPointD));
  461. fOutPathLen := highI;
  462. Exit;
  463. end;
  464. // do the line start cap
  465. if Assigned(fDeltaCallback64) then
  466. fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0);
  467. if (Abs(fGroupDelta) < Tolerance) then
  468. AddPoint(fInPath[0])
  469. else
  470. case fEndType of
  471. etButt: DoBevel(0, 0);
  472. etRound: DoRound(0,0, PI);
  473. else DoSquare(0, 0);
  474. end;
  475. // offset the left side going forward
  476. k := 0;
  477. for i := 1 to highI -1 do //nb: -1 is important
  478. OffsetPoint(i, k);
  479. // reverse the normals ...
  480. for i := HighI downto 1 do
  481. begin
  482. fNorms[i].X := -fNorms[i-1].X;
  483. fNorms[i].Y := -fNorms[i-1].Y;
  484. end;
  485. fNorms[0] := fNorms[highI];
  486. // do the line end cap
  487. if Assigned(fDeltaCallback64) then
  488. fGroupDelta := fDeltaCallback64(fInPath, fNorms, highI, highI);
  489. if Abs(fGroupDelta) < Tolerance then
  490. begin
  491. AddPoint(fInPath[highI]);
  492. end else
  493. case fEndType of
  494. etButt: DoBevel(highI, highI);
  495. etRound: DoRound(highI,highI, PI);
  496. else DoSquare(highI, highI);
  497. end;
  498. // offset the left side going back
  499. k := highI;
  500. for i := highI -1 downto 1 do //and stop at 1!
  501. OffsetPoint(i, k);
  502. UpdateSolution;
  503. end;
  504. //------------------------------------------------------------------------------
  505. procedure TClipperOffset.ExecuteInternal(delta: Double);
  506. var
  507. i,j: integer;
  508. group: TGroup;
  509. pathsReversed: Boolean;
  510. fillRule: TFillRule;
  511. dummy: TPaths64;
  512. begin
  513. fSolution := nil;
  514. fSolutionLen := 0;
  515. if fGroupList.Count = 0 then Exit;
  516. SetLength(fSolution, CalcSolutionCapacity);
  517. fMinLenSqrd := 1;
  518. if abs(delta) < Tolerance then
  519. begin
  520. // if delta == 0, just copy paths to Result
  521. for i := 0 to fGroupList.Count -1 do
  522. begin
  523. group := TGroup(fGroupList[i]);
  524. for j := 0 to High(group.paths) do
  525. begin
  526. fSolution[fSolutionLen] := group.paths[i];
  527. inc(fSolutionLen);
  528. end;
  529. end;
  530. Exit;
  531. end;
  532. fDelta := delta;
  533. // Miter Limit: see offset_triginometry3.svg
  534. if fMiterLimit > 1 then
  535. fTmpLimit := 2 / Sqr(fMiterLimit) else
  536. fTmpLimit := 2.0;
  537. // nb: delta will depend on whether paths are polygons or open
  538. for i := 0 to fGroupList.Count -1 do
  539. begin
  540. group := TGroup(fGroupList[i]);
  541. DoGroupOffset(group);
  542. end;
  543. SetLength(fSolution, fSolutionLen);
  544. pathsReversed := CheckReverseOrientation();
  545. if pathsReversed then
  546. fillRule := frNegative else
  547. fillRule := frPositive;
  548. // clean up self-intersections ...
  549. with TClipper64.Create do
  550. try
  551. PreserveCollinear := fPreserveCollinear;
  552. // the solution should retain the orientation of the input
  553. ReverseSolution := fReverseSolution <> pathsReversed;
  554. {$IFDEF USINGZ}
  555. ZCallback := ZCB;
  556. {$ENDIF}
  557. AddSubject(fSolution);
  558. if assigned(fSolutionTree) then
  559. Execute(ctUnion, fillRule, fSolutionTree, dummy);
  560. Execute(ctUnion, fillRule, fSolution);
  561. finally
  562. free;
  563. end;
  564. end;
  565. //------------------------------------------------------------------------------
  566. function TClipperOffset.CheckReverseOrientation: Boolean;
  567. var
  568. i: integer;
  569. begin
  570. Result := false;
  571. // find the orientation of the first closed path
  572. for i := 0 to fGroupList.Count -1 do
  573. with TGroup(fGroupList[i]) do
  574. if endType = etPolygon then
  575. begin
  576. Result := reversed;
  577. break;
  578. end;
  579. end;
  580. //------------------------------------------------------------------------------
  581. procedure TClipperOffset.Execute(delta: Double; out solution: TPaths64);
  582. begin
  583. solution := nil;
  584. fSolutionTree := nil;
  585. if fGroupList.Count = 0 then Exit;
  586. ExecuteInternal(delta);
  587. solution := fSolution;
  588. end;
  589. //------------------------------------------------------------------------------
  590. procedure TClipperOffset.Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64);
  591. begin
  592. fDeltaCallback64 := DeltaCallback;
  593. Execute(1.0, solution);
  594. end;
  595. //------------------------------------------------------------------------------
  596. procedure TClipperOffset.Execute(delta: Double; polytree: TPolyTree64);
  597. begin
  598. if not Assigned(polytree) then
  599. Raise EClipper2LibException(rsClipper_PolyTreeErr);
  600. fSolutionTree := polytree;
  601. fSolutionTree.Clear;
  602. ExecuteInternal(delta);
  603. end;
  604. //------------------------------------------------------------------------------
  605. {$IFDEF USINGZ}
  606. procedure TClipperOffset.ZCB(const bot1, top1, bot2, top2: TPoint64;
  607. var intersectPt: TPoint64);
  608. begin
  609. if (bot1.Z <> 0) and
  610. ((bot1.Z = bot2.Z) or (bot1.Z = top2.Z)) then intersectPt.Z := bot1.Z
  611. else if (bot2.Z <> 0) and (bot2.Z = top1.Z) then intersectPt.Z := bot2.Z
  612. else if (top1.Z <> 0) and (top1.Z = top2.Z) then intersectPt.Z := top1.Z
  613. else if Assigned(ZCallback) then
  614. ZCallback(bot1, top1, bot2, top2, intersectPt);
  615. end;
  616. {$ENDIF}
  617. //------------------------------------------------------------------------------
  618. {$IFDEF USINGZ}
  619. procedure TClipperOffset.AddPoint(x,y: double; z: Int64);
  620. {$ELSE}
  621. procedure TClipperOffset.AddPoint(x,y: double);
  622. {$ENDIF}
  623. const
  624. BuffLength = 32;
  625. var
  626. pt: TPoint64;
  627. begin
  628. {$IFDEF USINGZ}
  629. pt := Point64(Round(x),Round(y), z);
  630. {$ELSE}
  631. pt := Point64(Round(x),Round(y));
  632. {$ENDIF}
  633. if fOutPathLen = length(fOutPath) then
  634. SetLength(fOutPath, fOutPathLen + BuffLength);
  635. if (fOutPathLen > 0) and
  636. PointsEqual(fOutPath[fOutPathLen-1], pt) then Exit;
  637. fOutPath[fOutPathLen] := pt;
  638. Inc(fOutPathLen);
  639. end;
  640. //------------------------------------------------------------------------------
  641. {$IFDEF USINGZ}
  642. procedure TClipperOffset.AddPoint(const pt: TPoint64; newZ: Int64);
  643. begin
  644. AddPoint(pt.X, pt.Y, newZ);
  645. end;
  646. //------------------------------------------------------------------------------
  647. procedure TClipperOffset.AddPoint(const pt: TPoint64);
  648. begin
  649. AddPoint(pt.X, pt.Y, pt.Z);
  650. end;
  651. //------------------------------------------------------------------------------
  652. {$ELSE}
  653. procedure TClipperOffset.AddPoint(const pt: TPoint64);
  654. begin
  655. AddPoint(pt.X, pt.Y);
  656. end;
  657. //------------------------------------------------------------------------------
  658. {$ENDIF}
  659. function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD;
  660. var
  661. m1,b1,m2,b2: double;
  662. begin
  663. result := NullPointD;
  664. //see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
  665. if (ln1B.X = ln1A.X) then
  666. begin
  667. if (ln2B.X = ln2A.X) then exit; //parallel lines
  668. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  669. b2 := ln2A.Y - m2 * ln2A.X;
  670. Result.X := ln1A.X;
  671. Result.Y := m2*ln1A.X + b2;
  672. end
  673. else if (ln2B.X = ln2A.X) then
  674. begin
  675. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  676. b1 := ln1A.Y - m1 * ln1A.X;
  677. Result.X := ln2A.X;
  678. Result.Y := m1*ln2A.X + b1;
  679. end else
  680. begin
  681. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  682. b1 := ln1A.Y - m1 * ln1A.X;
  683. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  684. b2 := ln2A.Y - m2 * ln2A.X;
  685. if m1 = m2 then exit; //parallel lines
  686. Result.X := (b2 - b1)/(m1 - m2);
  687. Result.Y := m1 * Result.X + b1;
  688. end;
  689. end;
  690. //------------------------------------------------------------------------------
  691. function ReflectPoint(const pt, pivot: TPointD): TPointD;
  692. begin
  693. Result.X := pivot.X + (pivot.X - pt.X);
  694. Result.Y := pivot.Y + (pivot.Y - pt.Y);
  695. {$IFDEF USINGZ}
  696. Result.Z := pt.Z;
  697. {$ENDIF}
  698. end;
  699. //------------------------------------------------------------------------------
  700. procedure TClipperOffset.DoBevel(j, k: Integer);
  701. var
  702. absDelta: double;
  703. begin
  704. if k = j then
  705. begin
  706. absDelta := abs(fGroupDelta);
  707. {$IFDEF USINGZ}
  708. AddPoint(
  709. fInPath[j].x - absDelta * fNorms[j].x,
  710. fInPath[j].y - absDelta * fNorms[j].y, fInPath[j].z);
  711. AddPoint(
  712. fInPath[j].x + absDelta * fNorms[j].x,
  713. fInPath[j].y + absDelta * fNorms[j].y, fInPath[j].z);
  714. {$ELSE}
  715. AddPoint(
  716. fInPath[j].x - absDelta * fNorms[j].x,
  717. fInPath[j].y - absDelta * fNorms[j].y);
  718. AddPoint(
  719. fInPath[j].x + absDelta * fNorms[j].x,
  720. fInPath[j].y + absDelta * fNorms[j].y);
  721. {$ENDIF}
  722. end else
  723. begin
  724. {$IFDEF USINGZ}
  725. AddPoint(
  726. fInPath[j].x + fGroupDelta * fNorms[k].x,
  727. fInPath[j].y + fGroupDelta * fNorms[k].y, fInPath[j].z);
  728. AddPoint(
  729. fInPath[j].x + fGroupDelta * fNorms[j].x,
  730. fInPath[j].y + fGroupDelta * fNorms[j].y, fInPath[j].z);
  731. {$ELSE}
  732. AddPoint(
  733. fInPath[j].x + fGroupDelta * fNorms[k].x,
  734. fInPath[j].y + fGroupDelta * fNorms[k].y);
  735. AddPoint(
  736. fInPath[j].x + fGroupDelta * fNorms[j].x,
  737. fInPath[j].y + fGroupDelta * fNorms[j].y);
  738. {$ENDIF}
  739. end;
  740. end;
  741. //------------------------------------------------------------------------------
  742. procedure TClipperOffset.DoSquare(j, k: Integer);
  743. var
  744. vec, pt1,pt2,pt3,pt4, pt,ptQ : TPointD;
  745. absDelta: double;
  746. begin
  747. if k = j then
  748. begin
  749. vec.X := fNorms[j].Y; //squaring a line end
  750. vec.Y := -fNorms[j].X;
  751. end else
  752. begin
  753. // using the reciprocal of unit normals (as unit vectors)
  754. // get the average unit vector ...
  755. vec := GetAvgUnitVector(
  756. PointD(-fNorms[k].Y, fNorms[k].X),
  757. PointD(fNorms[j].Y, -fNorms[j].X));
  758. end;
  759. absDelta := Abs(fGroupDelta);
  760. // now offset the original vertex delta units along unit vector
  761. ptQ := PointD(fInPath[j]);
  762. ptQ := TranslatePoint(ptQ, absDelta * vec.X, absDelta * vec.Y);
  763. // get perpendicular vertices
  764. pt1 := TranslatePoint(ptQ, fGroupDelta * vec.Y, fGroupDelta * -vec.X);
  765. pt2 := TranslatePoint(ptQ, fGroupDelta * -vec.Y, fGroupDelta * vec.X);
  766. // get 2 vertices along one edge offset
  767. pt3 := GetPerpendicD(fInPath[k], fNorms[k], fGroupDelta);
  768. if (j = k) then
  769. begin
  770. pt4.X := pt3.X + vec.X * fGroupDelta;
  771. pt4.Y := pt3.Y + vec.Y * fGroupDelta;
  772. // get the intersection point
  773. pt := IntersectPoint(pt1, pt2, pt3, pt4);
  774. {$IFDEF USINGZ}
  775. with ReflectPoint(pt, ptQ) do AddPoint(X, Y, Z);
  776. AddPoint(pt.X, pt.Y, pt.Z);
  777. {$ELSE}
  778. with ReflectPoint(pt, ptQ) do AddPoint(X, Y);
  779. AddPoint(pt.X, pt.Y);
  780. {$ENDIF}
  781. end else
  782. begin
  783. pt4 := GetPerpendicD(fInPath[j], fNorms[k], fGroupDelta);
  784. // get the intersection point
  785. pt := IntersectPoint(pt1, pt2, pt3, pt4);
  786. {$IFDEF USINGZ}
  787. AddPoint(pt.X, pt.Y, ptQ.Z);
  788. //get the second intersect point through reflecion
  789. with ReflectPoint(pt, ptQ) do AddPoint(X, Y, ptQ.Z);
  790. {$ELSE}
  791. AddPoint(pt.X, pt.Y);
  792. //get the second intersect point through reflecion
  793. with ReflectPoint(pt, ptQ) do AddPoint(X, Y);
  794. {$ENDIF}
  795. end;
  796. end;
  797. //------------------------------------------------------------------------------
  798. procedure TClipperOffset.DoMiter(j, k: Integer; cosA: Double);
  799. var
  800. q: Double;
  801. begin
  802. // see offset_triginometry4.svg
  803. q := fGroupDelta / (cosA +1);
  804. {$IFDEF USINGZ}
  805. AddPoint(fInPath[j].X + (fNorms[k].X + fNorms[j].X)*q,
  806. fInPath[j].Y + (fNorms[k].Y + fNorms[j].Y)*q,
  807. fInPath[j].Z);
  808. {$ELSE}
  809. AddPoint(fInPath[j].X + (fNorms[k].X + fNorms[j].X)*q,
  810. fInPath[j].Y + (fNorms[k].Y + fNorms[j].Y)*q);
  811. {$ENDIF}
  812. end;
  813. //------------------------------------------------------------------------------
  814. procedure TClipperOffset.DoRound(j, k: Integer; angle: double);
  815. var
  816. i, steps: Integer;
  817. absDelta, arcTol, stepsPer360: double;
  818. pt: TPoint64;
  819. offDist: TPointD;
  820. begin
  821. if Assigned(fDeltaCallback64) then
  822. begin
  823. // when fDeltaCallback64 is assigned, fGroupDelta won't be constant,
  824. // so we'll need to do the following calculations for *every* vertex.
  825. absDelta := Abs(fGroupDelta);
  826. arcTol := Iif(fArcTolerance > 0.01,
  827. Min(absDelta, fArcTolerance),
  828. Log10(2 + absDelta) * 0.25); // empirically derived
  829. //http://www.angusj.com/clipper2/Docs/Trigonometry.htm
  830. stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta);
  831. if (stepsPer360 > absDelta * Pi) then
  832. stepsPer360 := absDelta * Pi; // avoid excessive precision
  833. fStepSin := sin(TwoPi/stepsPer360);
  834. fStepCos := cos(TwoPi/stepsPer360);
  835. if (fGroupDelta < 0.0) then fStepSin := -fStepSin;
  836. fStepsPerRad := stepsPer360 / TwoPi;
  837. end;
  838. // nb: angles may be negative but this will always be a convex join
  839. pt := fInPath[j];
  840. offDist := ScalePoint(fNorms[k], fGroupDelta);
  841. if j = k then offDist := Negate(offDist);
  842. {$IFDEF USINGZ}
  843. AddPoint(pt.X + offDist.X, pt.Y + offDist.Y, pt.Z);
  844. {$ELSE}
  845. AddPoint(pt.X + offDist.X, pt.Y + offDist.Y);
  846. {$ENDIF}
  847. steps := Ceil(fStepsPerRad * abs(angle)); // #448, #456
  848. for i := 2 to steps do
  849. begin
  850. offDist := PointD(offDist.X * fStepCos - fStepSin * offDist.Y,
  851. offDist.X * fStepSin + offDist.Y * fStepCos);
  852. {$IFDEF USINGZ}
  853. AddPoint(pt.X + offDist.X, pt.Y + offDist.Y, pt.Z);
  854. {$ELSE}
  855. AddPoint(pt.X + offDist.X, pt.Y + offDist.Y);
  856. {$ENDIF}
  857. end;
  858. AddPoint(GetPerpendic(pt, fNorms[j], fGroupDelta));
  859. end;
  860. //------------------------------------------------------------------------------
  861. procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer);
  862. var
  863. sinA, cosA: Double;
  864. begin
  865. if PointsEqual(fInPath[j], fInPath[k]) then
  866. begin
  867. k := j;
  868. Exit;
  869. end;
  870. // Let A = change in angle where edges join
  871. // A == 0: ie no change in angle (flat join)
  872. // A == PI: edges 'spike'
  873. // sin(A) < 0: right turning
  874. // cos(A) < 0: change in angle is more than 90 degree
  875. sinA := CrossProduct(fNorms[k], fNorms[j]);
  876. cosA := DotProduct(fNorms[j], fNorms[k]);
  877. if (sinA > 1.0) then sinA := 1.0
  878. else if (sinA < -1.0) then sinA := -1.0;
  879. if Assigned(fDeltaCallback64) then
  880. begin
  881. fGroupDelta := fDeltaCallback64(fInPath, fNorms, j, k);
  882. if TGroup(fGroupList[0]).reversed then fGroupDelta := -fGroupDelta;
  883. end;
  884. if Abs(fGroupDelta) <= Tolerance then
  885. begin
  886. AddPoint(fInPath[j]);
  887. Exit;
  888. end;
  889. //test for concavity first (#593)
  890. if (cosA > -0.999) and (sinA * fGroupDelta < 0) then
  891. begin
  892. // is concave
  893. {$IFDEF USINGZ}
  894. AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta), fInPath[j].Z);
  895. {$ELSE}
  896. AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta));
  897. {$ENDIF}
  898. // this extra point is the only simple way to ensure that path reversals
  899. // (ie over-shrunk paths) are fully cleaned out with the trailing union op.
  900. // However it's probably safe to skip this whenever an angle is almost flat.
  901. if (cosA < 0.99) then
  902. AddPoint(fInPath[j]); // (#405)
  903. {$IFDEF USINGZ}
  904. AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta), fInPath[j].Z);
  905. {$ELSE}
  906. AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta));
  907. {$ENDIF}
  908. end
  909. else if (cosA > 0.999) and (fJoinType <> jtRound) then
  910. begin
  911. // almost straight - less than 2.5 degree (#424, #482, #526 & #724)
  912. DoMiter(j, k, cosA);
  913. end
  914. else if (fJoinType = jtMiter) then
  915. begin
  916. // miter unless the angle is sufficiently acute to exceed ML
  917. if (cosA > fTmpLimit -1) then DoMiter(j, k, cosA)
  918. else DoSquare(j, k);
  919. end
  920. else if (fJoinType = jtRound) then
  921. DoRound(j, k, ArcTan2(sinA, cosA))
  922. else if (fJoinType = jtBevel) then
  923. DoBevel(j, k)
  924. else
  925. DoSquare(j, k);
  926. k := j;
  927. end;
  928. //------------------------------------------------------------------------------
  929. //------------------------------------------------------------------------------
  930. end.