Clipper.Offset.pas 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. unit Clipper.Offset;
  2. (*******************************************************************************
  3. * Author : Angus Johnson *
  4. * Date : 15 October 2022 *
  5. * Website : http://www.angusj.com *
  6. * Copyright : Angus Johnson 2010-2022 *
  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;
  14. type
  15. TJoinType = (jtSquare, jtRound, jtMiter);
  16. TEndType = (etPolygon, etJoined, etButt, etSquare, etRound);
  17. // etButt : offsets both sides of a path, with square blunt ends
  18. // etSquare : offsets both sides of a path, with square extended ends
  19. // etRound : offsets both sides of a path, with round extended ends
  20. // etJoined : offsets both sides of a path, with joined ends
  21. // etPolygon: offsets only one side of a closed path
  22. TGroup = class
  23. paths : TPaths64;
  24. reversed : Boolean;
  25. joinType : TJoinType;
  26. endType : TEndType;
  27. constructor Create(jt: TJoinType; et: TEndType);
  28. end;
  29. TClipperOffset = class
  30. private
  31. fGrpDelta : Double;
  32. fAbsGrpDelta : Double;
  33. fMinLenSqrd : double;
  34. fJoinType : TJoinType;
  35. fTmpLimit : Double;
  36. fMiterLimit : Double;
  37. fArcTolerance: Double;
  38. fStepsPerRad : Double;
  39. fNorms : TPathD;
  40. fInGroups : TList;
  41. fMergeGroups : Boolean;
  42. fInPath : TPath64;
  43. fOutPath : TPath64;
  44. fOutPaths : TPaths64;
  45. fOutPathLen : Integer;
  46. fSolution : TPaths64;
  47. fPreserveCollinear : Boolean;
  48. fReverseSolution : Boolean;
  49. procedure AddPoint(x,y: double); overload;
  50. procedure AddPoint(const pt: TPoint64); overload;
  51. {$IFDEF INLINING} inline; {$ENDIF}
  52. procedure DoSquare(j, k: Integer);
  53. procedure DoMiter(j, k: Integer; cosA: Double);
  54. procedure DoRound(j, k: integer; angle: double);
  55. procedure OffsetPoint(j: Integer; var k: integer);
  56. procedure BuildNormals;
  57. procedure DoGroupOffset(group: TGroup; groupDelta: double);
  58. procedure OffsetPolygon;
  59. procedure OffsetOpenJoined;
  60. procedure OffsetOpenPath(endType: TEndType);
  61. public
  62. constructor Create(miterLimit: double = 2.0;
  63. arcTolerance: double = 0.0;
  64. PreserveCollinear: Boolean = False;
  65. ReverseSolution: Boolean = False);
  66. destructor Destroy; override;
  67. procedure AddPath(const path: TPath64;
  68. joinType: TJoinType; endType: TEndType);
  69. procedure AddPaths(const paths: TPaths64;
  70. joinType: TJoinType; endType: TEndType);
  71. procedure Clear;
  72. function Execute(delta: Double): TPaths64;
  73. // MiterLimit: needed for mitered offsets (see offset_triginometry3.svg)
  74. property MiterLimit: Double read fMiterLimit write fMiterLimit;
  75. // ArcTolerance: needed for rounded offsets (See offset_triginometry2.svg)
  76. property ArcTolerance: Double read fArcTolerance write fArcTolerance;
  77. // MergeGroups: A path group is one or more paths added via the AddPath or
  78. // AddPaths methods. By default these path groups will be offset
  79. // independently of other groups and this may cause overlaps (intersections).
  80. // However, when MergeGroups is enabled, any overlapping offsets will be
  81. // merged (via a clipping union operation) to remove overlaps.
  82. property MergeGroups: Boolean read fMergeGroups write fMergeGroups;
  83. property PreserveCollinear: Boolean
  84. read fPreserveCollinear write fPreserveCollinear;
  85. property ReverseSolution: Boolean
  86. read fReverseSolution write fReverseSolution;
  87. end;
  88. implementation
  89. uses
  90. Math, Clipper.Engine;
  91. const
  92. TwoPi : Double = 2 * PI;
  93. InvTwoPi : Double = 1/(2 * PI);
  94. //------------------------------------------------------------------------------
  95. // Miscellaneous offset support functions
  96. //------------------------------------------------------------------------------
  97. function DotProduct(const vec1, vec2: TPointD): double;
  98. {$IFDEF INLINING} inline; {$ENDIF}
  99. begin
  100. result := vec1.X * vec2.X + vec1.Y * vec2.Y;
  101. end;
  102. //------------------------------------------------------------------------------
  103. function ValueAlmostZero(val: double; epsilon: double = 0.001): Boolean;
  104. {$IFDEF INLINE} inline; {$ENDIF}
  105. begin
  106. Result := Abs(val) < epsilon;
  107. end;
  108. //------------------------------------------------------------------------------
  109. function NormalizeVector(const vec: TPointD): TPointD;
  110. {$IFDEF INLINE} inline; {$ENDIF}
  111. var
  112. h, inverseHypot: Double;
  113. begin
  114. h := Hypot(vec.X, vec.Y);
  115. if ValueAlmostZero(h) then
  116. begin
  117. Result := NullPointD;
  118. Exit;
  119. end;
  120. inverseHypot := 1 / h;
  121. Result.X := vec.X * inverseHypot;
  122. Result.Y := vec.Y * inverseHypot;
  123. end;
  124. //------------------------------------------------------------------------------
  125. function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD;
  126. begin
  127. Result := NormalizeVector(PointD(vec1.X + vec2.X, vec1.Y + vec2.Y));
  128. end;
  129. //------------------------------------------------------------------------------
  130. function GetUnitNormal(const pt1, pt2: TPoint64): TPointD;
  131. var
  132. dx, dy, inverseHypot: Double;
  133. begin
  134. if (pt2.X = pt1.X) and (pt2.Y = pt1.Y) then
  135. begin
  136. Result.X := 0;
  137. Result.Y := 0;
  138. Exit;
  139. end;
  140. dx := (pt2.X - pt1.X);
  141. dy := (pt2.Y - pt1.Y);
  142. inverseHypot := 1 / Hypot(dx, dy);
  143. dx := dx * inverseHypot;
  144. dy := dy * inverseHypot;
  145. Result.X := dy;
  146. Result.Y := -dx
  147. end;
  148. //------------------------------------------------------------------------------
  149. function GetLowestPolygonIdx(const paths: TPaths64): integer;
  150. var
  151. i,j: integer;
  152. lp: TPoint64;
  153. p: TPath64;
  154. begin
  155. Result := -1;
  156. lp := Point64(0, -MaxInt64);
  157. for i := 0 to High(paths) do
  158. begin
  159. p := paths[i];
  160. for j := 0 to High(p) do
  161. begin
  162. if (p[j].Y < lp.Y) or
  163. ((p[j].Y = lp.Y) and (p[j].X >= lp.X)) then Continue;
  164. Result := i;
  165. lp := p[j];
  166. end;
  167. end;
  168. end;
  169. //------------------------------------------------------------------------------
  170. function UnsafeGet(List: TList; Index: Integer): Pointer;
  171. {$IFDEF INLINING} inline; {$ENDIF}
  172. begin
  173. Result := List.List[Index];
  174. end;
  175. //------------------------------------------------------------------------------
  176. // TGroup methods
  177. //------------------------------------------------------------------------------
  178. constructor TGroup.Create(jt: TJoinType; et: TEndType);
  179. begin
  180. Self.joinType := jt;
  181. Self.endType := et;
  182. end;
  183. //------------------------------------------------------------------------------
  184. // TClipperOffset methods
  185. //------------------------------------------------------------------------------
  186. constructor TClipperOffset.Create(miterLimit: double;
  187. arcTolerance: double; PreserveCollinear: Boolean;
  188. ReverseSolution: Boolean);
  189. begin
  190. fMergeGroups := true;
  191. fMiterLimit := MiterLimit;
  192. fArcTolerance := ArcTolerance;
  193. fInGroups := TList.Create;
  194. fPreserveCollinear := preserveCollinear;
  195. fReverseSolution := ReverseSolution;
  196. end;
  197. //------------------------------------------------------------------------------
  198. destructor TClipperOffset.Destroy;
  199. begin
  200. Clear;
  201. fInGroups.Free;
  202. inherited;
  203. end;
  204. //------------------------------------------------------------------------------
  205. procedure TClipperOffset.Clear;
  206. var
  207. i: integer;
  208. begin
  209. for i := 0 to fInGroups.Count -1 do
  210. TGroup(UnsafeGet(fInGroups, i)).Free;
  211. fInGroups.Clear;
  212. fSolution := nil;
  213. end;
  214. //------------------------------------------------------------------------------
  215. procedure TClipperOffset.AddPath(const path: TPath64;
  216. joinType: TJoinType; endType: TEndType);
  217. var
  218. paths: TPaths64;
  219. begin
  220. if not assigned(path) then Exit;
  221. SetLength(paths, 1);
  222. paths[0] := path;
  223. AddPaths(Paths, joinType, endType);
  224. end;
  225. //------------------------------------------------------------------------------
  226. procedure TClipperOffset.AddPaths(const paths: TPaths64;
  227. joinType: TJoinType; endType: TEndType);
  228. var
  229. group: TGroup;
  230. begin
  231. if Length(paths) = 0 then Exit;
  232. group := TGroup.Create(joinType, endType);
  233. AppendPaths(group.paths, paths);
  234. fInGroups.Add(group);
  235. end;
  236. //------------------------------------------------------------------------------
  237. function GetPerpendic(const pt: TPoint64; const norm: TPointD; delta: double): TPoint64; overload;
  238. {$IFDEF INLINING} inline; {$ENDIF}
  239. begin
  240. result := Point64(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
  241. end;
  242. //------------------------------------------------------------------------------
  243. function GetPerpendicD(const pt: TPoint64; const norm: TPointD; delta: double): TPointD; overload;
  244. {$IFDEF INLINING} inline; {$ENDIF}
  245. begin
  246. result := PointD(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
  247. end;
  248. //------------------------------------------------------------------------------
  249. procedure TClipperOffset.DoGroupOffset(group: TGroup; groupDelta: double);
  250. var
  251. i, len, lowestIdx: Integer;
  252. r, arcTol, area, steps: Double;
  253. IsClosedPaths: Boolean;
  254. begin
  255. if group.endType <> etPolygon then
  256. groupDelta := Abs(groupDelta) * 0.5;
  257. IsClosedPaths := (group.endType in [etPolygon, etJoined]);
  258. if IsClosedPaths then
  259. begin
  260. // the lowermost polygon must be an outer polygon. So we can use that as the
  261. // designated orientation for outer polygons (needed for tidy-up clipping)
  262. lowestIdx := GetLowestPolygonIdx(group.paths);
  263. if lowestIdx < 0 then Exit;
  264. // nb: don't use the default orientation here ...
  265. area := Clipper.Core.Area(group.paths[lowestIdx]);
  266. if area = 0 then Exit;
  267. group.reversed := (area < 0);
  268. if group.reversed then groupDelta := -groupDelta;
  269. end else
  270. group.reversed := false;
  271. fGrpDelta := groupDelta;
  272. fAbsGrpDelta := Abs(fGrpDelta);
  273. fJoinType := group.joinType;
  274. if fArcTolerance > 0 then
  275. arcTol := fArcTolerance else
  276. arcTol := Log10(2 + fAbsGrpDelta) * 0.25; // empirically derived
  277. // calculate a sensible number of steps (for 360 deg for the given offset
  278. if (group.joinType = jtRound) or (group.endType = etRound) then
  279. begin
  280. // get steps per 180 degrees (see offset_triginometry2.svg)
  281. steps := PI / ArcCos(1 - arcTol / fAbsGrpDelta);
  282. fStepsPerRad := steps * InvTwoPi;
  283. end;
  284. fOutPaths := nil;
  285. for i := 0 to High(group.paths) do
  286. begin
  287. fInPath := StripDuplicates(group.paths[i], IsClosedPaths);
  288. len := Length(fInPath);
  289. if (fInPath = nil) or
  290. ((group.endType in [etPolygon, etJoined]) and (len < 3)) then Continue;
  291. fNorms := nil;
  292. fOutPath := nil;
  293. fOutPathLen := 0;
  294. //if a single vertex then build a circle or a square ...
  295. if len = 1 then
  296. begin
  297. if (group.endType = etRound) then
  298. begin
  299. r := fAbsGrpDelta;
  300. if (group.endType = etPolygon) then
  301. r := r * 0.5;
  302. with fInPath[0] do
  303. fOutPath := Path64(Ellipse(RectD(X-r, Y-r, X+r, Y+r)));
  304. end else
  305. begin
  306. SetLength(fOutPath, 4);
  307. with fInPath[0] do
  308. begin
  309. fOutPath[0] := Point64(X-fGrpDelta,Y-fGrpDelta);
  310. fOutPath[1] := Point64(X+fGrpDelta,Y-fGrpDelta);
  311. fOutPath[2] := Point64(X+fGrpDelta,Y+fGrpDelta);
  312. fOutPath[3] := Point64(X-fGrpDelta,Y+fGrpDelta);
  313. end;
  314. end;
  315. AppendPath(fOutPaths, fOutPath);
  316. Continue;
  317. end else
  318. begin
  319. BuildNormals;
  320. if group.endType = etPolygon then
  321. OffsetPolygon
  322. else if group.endType = etJoined then
  323. OffsetOpenJoined
  324. else
  325. OffsetOpenPath(group.endType);
  326. end;
  327. if fOutPathLen = 0 then Continue;
  328. SetLength(fOutPath, fOutPathLen);
  329. AppendPath(fOutPaths, fOutPath);
  330. end;
  331. if not fMergeGroups then
  332. begin
  333. // clean up self-intersections ...
  334. with TClipper64.Create do
  335. try
  336. PreserveCollinear := fPreserveCollinear;
  337. // the solution should retain the orientation of the input
  338. ReverseSolution := fReverseSolution <> group.reversed;
  339. AddSubject(fOutPaths);
  340. if group.reversed then
  341. Execute(ctUnion, frNegative, fOutPaths) else
  342. Execute(ctUnion, frPositive, fOutPaths);
  343. finally
  344. free;
  345. end;
  346. end;
  347. // finally copy the working 'outPaths' to the solution
  348. AppendPaths(fSolution, fOutPaths);
  349. end;
  350. //------------------------------------------------------------------------------
  351. procedure TClipperOffset.BuildNormals;
  352. var
  353. i, len: integer;
  354. begin
  355. len := Length(fInPath);
  356. SetLength(fNorms, len);
  357. for i := 0 to len-2 do
  358. fNorms[i] := GetUnitNormal(fInPath[i], fInPath[i+1]);
  359. fNorms[len -1] := GetUnitNormal(fInPath[len -1], fInPath[0]);
  360. end;
  361. //------------------------------------------------------------------------------
  362. procedure TClipperOffset.OffsetPolygon;
  363. var
  364. i,j: integer;
  365. begin
  366. j := high(fInPath);
  367. for i := 0 to high(fInPath) do
  368. OffsetPoint(i, j);
  369. end;
  370. //------------------------------------------------------------------------------
  371. procedure TClipperOffset.OffsetOpenJoined;
  372. begin
  373. OffsetPolygon;
  374. SetLength(fOutPath, fOutPathLen);
  375. AppendPath(fOutPaths, fOutPath);
  376. fOutPath := nil;
  377. fOutPathLen := 0;
  378. fInPath := ReversePath(fInPath);
  379. BuildNormals;
  380. OffsetPolygon;
  381. end;
  382. //------------------------------------------------------------------------------
  383. procedure TClipperOffset.OffsetOpenPath(endType: TEndType);
  384. var
  385. i, k, highI: integer;
  386. begin
  387. highI := high(fInPath);
  388. // do the line start cap
  389. case endType of
  390. etButt:
  391. begin
  392. with fInPath[0] do AddPoint(Point64(
  393. X - fNorms[0].X * fGrpDelta,
  394. Y - fNorms[0].Y * fGrpDelta));
  395. AddPoint(GetPerpendic(fInPath[0], fNorms[0], fGrpDelta));
  396. end;
  397. etRound: DoRound(0,0, PI);
  398. else DoSquare(0, 0);
  399. end;
  400. // offset the left side going forward
  401. k := 0;
  402. for i := 1 to highI -1 do //nb: -1 is important
  403. OffsetPoint(i, k);
  404. // reverse the normals ...
  405. for i := HighI downto 1 do
  406. begin
  407. fNorms[i].X := -fNorms[i-1].X;
  408. fNorms[i].Y := -fNorms[i-1].Y;
  409. end;
  410. fNorms[0] := fNorms[highI];
  411. // do the line end cap
  412. case endType of
  413. etButt:
  414. begin
  415. with fInPath[highI] do AddPoint(Point64(
  416. X - fNorms[highI].X *fGrpDelta,
  417. Y - fNorms[highI].Y *fGrpDelta));
  418. AddPoint(GetPerpendic(fInPath[highI], fNorms[highI], fGrpDelta));
  419. end;
  420. etRound: DoRound(highI,highI, PI);
  421. else DoSquare(highI, highI);
  422. end;
  423. // offset the left side going back
  424. k := 0;
  425. for i := highI downto 1 do //and stop at 1!
  426. OffsetPoint(i, k);
  427. end;
  428. //------------------------------------------------------------------------------
  429. function TClipperOffset.Execute(delta: Double): TPaths64;
  430. var
  431. i: integer;
  432. group: TGroup;
  433. begin
  434. fSolution := nil;
  435. Result := nil;
  436. if fInGroups.Count = 0 then Exit;
  437. fMinLenSqrd := 1;
  438. if abs(delta) < Tolerance then
  439. begin
  440. // if delta == 0, just copy paths to Result
  441. for i := 0 to fInGroups.Count -1 do
  442. begin
  443. group := TGroup(UnsafeGet(fInGroups, i));
  444. AppendPaths(fSolution, group.paths);
  445. end;
  446. Result := fSolution;
  447. Exit;
  448. end;
  449. // Miter Limit: see offset_triginometry3.svg
  450. if fMiterLimit > 1 then
  451. fTmpLimit := 2 / Sqr(fMiterLimit) else
  452. fTmpLimit := 2.0;
  453. // nb: delta will depend on whether paths are polygons or open
  454. for i := 0 to fInGroups.Count -1 do
  455. begin
  456. group := TGroup(UnsafeGet(fInGroups, i));
  457. DoGroupOffset(group, delta);
  458. end;
  459. if fMergeGroups and (fInGroups.Count > 0) then
  460. begin
  461. // clean up self-intersections ...
  462. with TClipper64.Create do
  463. try
  464. PreserveCollinear := fPreserveCollinear;
  465. // the solution should retain the orientation of the input
  466. ReverseSolution :=
  467. fReverseSolution <> TGroup(fInGroups[0]).reversed;
  468. AddSubject(fSolution);
  469. if TGroup(UnsafeGet(fInGroups, 0)).reversed then
  470. Execute(ctUnion, frNegative, fSolution) else
  471. Execute(ctUnion, frPositive, fSolution);
  472. finally
  473. free;
  474. end;
  475. end;
  476. Result := fSolution;
  477. end;
  478. //------------------------------------------------------------------------------
  479. procedure TClipperOffset.AddPoint(x,y: double);
  480. const
  481. BuffLength = 32;
  482. var
  483. pt: TPoint64;
  484. begin
  485. pt := Point64(Round(x),Round(y));
  486. if fOutPathLen = length(fOutPath) then
  487. SetLength(fOutPath, fOutPathLen + BuffLength);
  488. if (fOutPathLen > 0) and
  489. PointsEqual(fOutPath[fOutPathLen-1], pt) then Exit;
  490. fOutPath[fOutPathLen] := pt;
  491. Inc(fOutPathLen);
  492. end;
  493. //------------------------------------------------------------------------------
  494. procedure TClipperOffset.AddPoint(const pt: TPoint64);
  495. begin
  496. AddPoint(pt.X, pt.Y);
  497. end;
  498. //------------------------------------------------------------------------------
  499. function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD;
  500. var
  501. m1,b1,m2,b2: double;
  502. begin
  503. result := NullPointD;
  504. //see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
  505. if (ln1B.X = ln1A.X) then
  506. begin
  507. if (ln2B.X = ln2A.X) then exit; //parallel lines
  508. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  509. b2 := ln2A.Y - m2 * ln2A.X;
  510. Result.X := ln1A.X;
  511. Result.Y := m2*ln1A.X + b2;
  512. end
  513. else if (ln2B.X = ln2A.X) then
  514. begin
  515. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  516. b1 := ln1A.Y - m1 * ln1A.X;
  517. Result.X := ln2A.X;
  518. Result.Y := m1*ln2A.X + b1;
  519. end else
  520. begin
  521. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  522. b1 := ln1A.Y - m1 * ln1A.X;
  523. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  524. b2 := ln2A.Y - m2 * ln2A.X;
  525. if m1 = m2 then exit; //parallel lines
  526. Result.X := (b2 - b1)/(m1 - m2);
  527. Result.Y := m1 * Result.X + b1;
  528. end;
  529. end;
  530. //------------------------------------------------------------------------------
  531. function ReflectPoint(const pt, pivot: TPointD): TPointD;
  532. begin
  533. Result.X := pivot.X + (pivot.X - pt.X);
  534. Result.Y := pivot.Y + (pivot.Y - pt.Y);
  535. end;
  536. //------------------------------------------------------------------------------
  537. procedure TClipperOffset.DoSquare(j, k: Integer);
  538. var
  539. vec, pt1,pt2,pt3,pt4, pt,ptQ : TPointD;
  540. begin
  541. if k = j then
  542. begin
  543. vec.X := fNorms[0].Y; //squaring a line end
  544. vec.Y := -fNorms[0].X;
  545. end else
  546. begin
  547. // using the reciprocal of unit normals (as unit vectors)
  548. // get the average unit vector ...
  549. vec := GetAvgUnitVector(
  550. PointD(-fNorms[k].Y, fNorms[k].X),
  551. PointD(fNorms[j].Y, -fNorms[j].X));
  552. end;
  553. // now offset the original vertex delta units along unit vector
  554. ptQ := PointD(fInPath[j]);
  555. ptQ := TranslatePoint(ptQ, fAbsGrpDelta * vec.X, fAbsGrpDelta * vec.Y);
  556. // get perpendicular vertices
  557. pt1 := TranslatePoint(ptQ, fGrpDelta * vec.Y, fGrpDelta * -vec.X);
  558. pt2 := TranslatePoint(ptQ, fGrpDelta * -vec.Y, fGrpDelta * vec.X);
  559. // get 2 vertices along one edge offset
  560. pt3 := GetPerpendicD(fInPath[k], fNorms[k], fGrpDelta);
  561. if (j = k) then
  562. begin
  563. pt4.X := pt3.X + vec.X * fGrpDelta;
  564. pt4.Y := pt3.Y + vec.Y * fGrpDelta;
  565. // get the intersection point
  566. pt := IntersectPoint(pt1, pt2, pt3, pt4);
  567. with ReflectPoint(pt, ptQ) do AddPoint(X, Y);
  568. AddPoint(pt.X, pt.Y);
  569. end else
  570. begin
  571. pt4 := GetPerpendicD(fInPath[j], fNorms[k], fGrpDelta);
  572. // get the intersection point
  573. pt := IntersectPoint(pt1, pt2, pt3, pt4);
  574. AddPoint(pt.X, pt.Y);
  575. //get the second intersect point through reflecion
  576. with ReflectPoint(pt, ptQ) do AddPoint(X, Y);
  577. end;
  578. end;
  579. //------------------------------------------------------------------------------
  580. procedure TClipperOffset.DoMiter(j, k: Integer; cosA: Double);
  581. var
  582. q: Double;
  583. begin
  584. // see offset_triginometry4.svg
  585. q := fGrpDelta / (cosA +1);
  586. AddPoint(fInPath[j].X + (fNorms[k].X + fNorms[j].X)*q,
  587. fInPath[j].Y + (fNorms[k].Y + fNorms[j].Y)*q);
  588. end;
  589. //------------------------------------------------------------------------------
  590. procedure TClipperOffset.DoRound(j, k: Integer; angle: double);
  591. var
  592. i, steps: Integer;
  593. stepSin, stepCos: double;
  594. pt: TPoint64;
  595. pt2: TPointD;
  596. begin
  597. // nb: even though angle may be negative this is a convex join
  598. pt := fInPath[j];
  599. pt2 := PointD(fNorms[k].X * fGrpDelta, fNorms[k].Y * fGrpDelta);
  600. if j = k then pt2 := Negate(pt2);
  601. steps := Ceil(fStepsPerRad * abs(angle));
  602. GetSinCos(angle / steps, stepSin, stepCos);
  603. AddPoint(pt.X + pt2.X, pt.Y + pt2.Y);
  604. for i := 0 to steps -1 do
  605. begin
  606. pt2 := PointD(pt2.X * stepCos - stepSin * pt2.Y,
  607. pt2.X * stepSin + pt2.Y * stepCos);
  608. AddPoint(pt.X + pt2.X, pt.Y + pt2.Y);
  609. end;
  610. AddPoint(GetPerpendic(pt, fNorms[j], fGrpDelta));
  611. end;
  612. //------------------------------------------------------------------------------
  613. procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer);
  614. var
  615. sinA, cosA: Double;
  616. almostNoAngle: Boolean;
  617. begin
  618. if PointsEqual(fInPath[j], fInPath[k]) then
  619. begin
  620. k := j;
  621. Exit;
  622. end;
  623. // Let A = change in angle where edges join
  624. // A == 0: ie no change in angle (flat join)
  625. // A == PI: edges 'spike'
  626. // sin(A) < 0: right turning
  627. // cos(A) < 0: change in angle is more than 90 degree
  628. sinA := CrossProduct(fNorms[k], fNorms[j]);
  629. cosA := DotProduct(fNorms[j], fNorms[k]);
  630. if (sinA > 1.0) then sinA := 1.0
  631. else if (sinA < -1.0) then sinA := -1.0;
  632. almostNoAngle := ValueAlmostZero(cosA - 1);
  633. // when there's almost no angle of deviation or it's concave
  634. if almostNoAngle or (sinA * fGrpDelta < 0) then
  635. begin
  636. //concave
  637. AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGrpDelta));
  638. // create a simple self-intersection that will be cleaned up later
  639. if not almostNoAngle then AddPoint(fInPath[j]);
  640. AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGrpDelta));
  641. end
  642. else // convex offset
  643. begin
  644. if (fJoinType = jtRound) then
  645. DoRound(j, k, ArcTan2(sinA, cosA))
  646. else if (fJoinType = jtMiter) then
  647. begin
  648. // miter unless the angle is so acute the miter would exceeds ML
  649. if (cosA > fTmpLimit -1) then DoMiter(j, k, cosA)
  650. else DoSquare(j, k);
  651. end
  652. // don't bother squaring angles that deviate < ~20 degrees because
  653. // squaring will be indistinguishable from mitering and just be a lot slower
  654. else if (cosA > 0.9) then
  655. DoMiter(j, k, cosA)
  656. else
  657. DoSquare(j, k);
  658. end;
  659. k := j;
  660. end;
  661. //------------------------------------------------------------------------------
  662. //------------------------------------------------------------------------------
  663. end.