GR32_VectorUtils.Angus.pas 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. unit GR32_VectorUtils.Angus;
  2. (* ***** BEGIN LICENSE BLOCK *****
  3. * Version: MPL 1.1 or LGPL 2.1 with linking exception
  4. *
  5. * The contents of this file are subject to the Mozilla Public License Version
  6. * 1.1 (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. * http://www.mozilla.org/MPL/
  9. *
  10. * Software distributed under the License is distributed on an "AS IS" basis,
  11. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12. * for the specific language governing rights and limitations under the
  13. * License.
  14. *
  15. * Alternatively, the contents of this file may be used under the terms of the
  16. * Free Pascal modified version of the GNU Lesser General Public License
  17. * Version 2.1 (the "FPC modified LGPL License"), in which case the provisions
  18. * of this license are applicable instead of those above.
  19. * Please see the file LICENSE.txt for additional information concerning this
  20. * license.
  21. *
  22. * The Original Code is Vector drawing for TImage32
  23. *
  24. * The Initial Developer of the Original Code is
  25. * Angus Johnson (http://www.angusj.com)
  26. *
  27. * Portions created by the Initial Developer are Copyright (C) 2019-2024
  28. * the Initial Developer. All Rights Reserved.
  29. *
  30. * ***** END LICENSE BLOCK ***** *)
  31. interface
  32. {$include GR32.inc}
  33. {$BOOLEVAL OFF}
  34. uses
  35. Types,
  36. GR32,
  37. {$if defined(UseInlining)}
  38. // In interface section for inlining
  39. GR32_Geometry,
  40. {$ifend}
  41. GR32_Polygons,
  42. GR32_VectorUtils;
  43. //------------------------------------------------------------------------------
  44. //
  45. // Grow and BuildPoly*line replacements adapted from Image32
  46. //
  47. //------------------------------------------------------------------------------
  48. // Note: Does not currently support JoinStyle=jsSquare; jsBevel is used instead.
  49. //------------------------------------------------------------------------------
  50. // The CalcRoundingSteps function has been rewritten to use the same algorithm
  51. // as the Arc function.
  52. //------------------------------------------------------------------------------
  53. type
  54. PolyLineBuilderAngus = class(TPolyLineBuilder)
  55. protected
  56. // Float
  57. class function Grow(const Points: TArrayOfFloatPoint; const Normals: TArrayOfFloatPoint; const Delta: TFloat; JoinStyle: TJoinStyle = jsMiter; Closed: Boolean = True; MiterLimit: TFloat = DEFAULT_MITER_LIMIT): TArrayOfFloatPoint; overload; override;
  58. public
  59. class function SupportedJoinStyles: TJoinStyles; override;
  60. class function SupportedEndStyles: TEndStyles; override;
  61. // Float
  62. class function BuildPolyLine(const Points: TArrayOfFloatPoint; StrokeWidth: TFloat; JoinStyle: TJoinStyle = jsMiter; EndStyle: TEndStyle = esButt; MiterLimit: TFloat = DEFAULT_MITER_LIMIT): TArrayOfFloatPoint; overload; override;
  63. class function BuildPolyPolyLine(const Points: TArrayOfArrayOfFloatPoint; Closed: Boolean; StrokeWidth: TFloat; JoinStyle: TJoinStyle = jsMiter; EndStyle: TEndStyle = esButt; MiterLimit: TFloat = DEFAULT_MITER_LIMIT): TArrayOfArrayOfFloatPoint; overload; override;
  64. end;
  65. //------------------------------------------------------------------------------
  66. //------------------------------------------------------------------------------
  67. //------------------------------------------------------------------------------
  68. implementation
  69. uses
  70. Math,
  71. {$if not defined(UseInlining)}
  72. GR32_Geometry,
  73. {$ifend}
  74. GR32_Math;
  75. const
  76. GrowScale = 1.0;
  77. //------------------------------------------------------------------------------
  78. class function PolyLineBuilderAngus.SupportedEndStyles: TEndStyles;
  79. begin
  80. Result := [esButt, esSquare, esRound];
  81. end;
  82. class function PolyLineBuilderAngus.SupportedJoinStyles: TJoinStyles;
  83. begin
  84. Result := [jsMiter, jsBevel, jsRound];
  85. end;
  86. //------------------------------------------------------------------------------
  87. //
  88. // Type and value mappings
  89. //
  90. //------------------------------------------------------------------------------
  91. type
  92. TJoinStyle = (jsAuto, jsSquare, jsMiter, jsRound);
  93. TEndStyle = (esPolygon = 0, esClosed = 0, esButt, esSquare, esRound);
  94. const
  95. // Note: We map Graphics32's jsBevel to jsSquare, and jsRoundEx to jsRound
  96. JoinStyleMap: array[GR32_Polygons.TJoinStyle] of TJoinStyle = (jsMiter, jsSquare, jsRound, jsRound, jsSquare);
  97. EndStyleMap: array[GR32_Polygons.TEndStyle] of TEndStyle = (esButt, esSquare, esRound);
  98. type
  99. TPathD = TArrayOfFloatPoint;
  100. TPathsD = TArrayOfArrayOfFloatPoint;
  101. TPointD = TFloatPoint;
  102. TRectD = TFloatRect;
  103. //------------------------------------------------------------------------------
  104. //
  105. // Grow types
  106. //
  107. //------------------------------------------------------------------------------
  108. const
  109. InvalidPointD : TPointD = (X: -Infinity; Y: -Infinity);
  110. //------------------------------------------------------------------------------
  111. //
  112. // Grow global config
  113. //
  114. //------------------------------------------------------------------------------
  115. var
  116. //AutoWidthThreshold: When JoinStyle = jsAuto, this is the threshold at
  117. //which line joins will be rounded instead of squared. With wider strokes,
  118. //rounded joins generally look better, but as rounding is more complex it
  119. //also requries more processing and hence is slower to execute.
  120. AutoWidthThreshold: double = 5.0;
  121. //When lines are too narrow, they become too faint to sensibly draw
  122. MinStrokeWidth: double = 0.5;
  123. //Miter limit avoids excessive spikes when line offsetting
  124. DefaultMiterLimit: double = 4.0;
  125. //------------------------------------------------------------------------------
  126. //
  127. // Function redirects to Graphics32 equivalents
  128. //
  129. //------------------------------------------------------------------------------
  130. function PointD(const X, Y: Double): TPointD; inline;
  131. begin
  132. Result := FloatPoint(X, Y);
  133. end;
  134. function RectD(const L, T, R, B: TFloat): TRectD; inline;
  135. begin
  136. Result := FloatRect(L, T, R, B);
  137. end;
  138. function GetBoundsD(const path: TPathD): TRectD; inline;
  139. begin
  140. Result := PolygonBounds(path);
  141. end;
  142. function ReversePath(const path: TPathD): TPathD; inline;
  143. begin
  144. Result := ReversePolygon(path);
  145. end;
  146. function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD; out ip: TPointD): Boolean; overload;
  147. var
  148. m1,b1,m2,b2: double;
  149. begin
  150. // Note: Returns the intersection between the two lines.
  151. // Unlike the Graphics32 Intersect function it does not test for
  152. // intersection between the line segments.
  153. result := False;
  154. //see http://paulbourke.net/geometry/pointlineplane/
  155. if (ln1B.X = ln1A.X) then
  156. begin
  157. if (ln2B.X = ln2A.X) then exit; //parallel lines
  158. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  159. b2 := ln2A.Y - m2 * ln2A.X;
  160. ip.X := ln1A.X;
  161. ip.Y := m2*ln1A.X + b2;
  162. Result := True;
  163. end
  164. else if (ln2B.X = ln2A.X) then
  165. begin
  166. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  167. b1 := ln1A.Y - m1 * ln1A.X;
  168. ip.X := ln2A.X;
  169. ip.Y := m1*ln2A.X + b1;
  170. Result := True;
  171. end else
  172. begin
  173. m1 := (ln1B.Y - ln1A.Y)/(ln1B.X - ln1A.X);
  174. b1 := ln1A.Y - m1 * ln1A.X;
  175. m2 := (ln2B.Y - ln2A.Y)/(ln2B.X - ln2A.X);
  176. b2 := ln2A.Y - m2 * ln2A.X;
  177. if m1 = m2 then exit; //parallel lines
  178. ip.X := (b2 - b1)/(m1 - m2);
  179. ip.Y := m1 * ip.X + b1;
  180. Result := True;
  181. end;
  182. end;
  183. function IntersectPoint(const ln1a, ln1b, ln2a, ln2b: TPointD): TPointD; overload; {$IFDEF USEINLINING} inline; {$ENDIF}
  184. begin
  185. if (not IntersectPoint(ln1a, ln1b, ln2a, ln2b, Result)) then
  186. Result := InvalidPointD;
  187. end;
  188. function PointsNearEqual(const pt1, pt2: TPointD; distSqrd: double): Boolean; inline;
  189. begin
  190. Result := SamePoint(pt1, pt2, distSqrd);
  191. end;
  192. function PointsEqual(const pt1, pt2: TPointD): Boolean; inline;
  193. begin
  194. result := (pt1 = pt2);
  195. end;
  196. function DotProduct(const vector1, vector2: TPointD): double; inline;
  197. begin
  198. Result := Dot(vector1, vector2);
  199. end;
  200. //------------------------------------------------------------------------------
  201. //
  202. // Utilities
  203. //
  204. //------------------------------------------------------------------------------
  205. function NormalizeVector(const vec: TPointD): TPointD; inline;
  206. var
  207. h, inverseHypot: Double;
  208. begin
  209. h := GR32_Math.Hypot(vec.X, vec.Y);
  210. if IsZero(h, 0.001) then
  211. begin
  212. Result := Default(TPointD);
  213. Exit;
  214. end;
  215. inverseHypot := 1 / h;
  216. Result.X := vec.X * inverseHypot;
  217. Result.Y := vec.Y * inverseHypot;
  218. end;
  219. //------------------------------------------------------------------------------
  220. function ReflectPoint(const pt, pivot: TPointD): TPointD; inline;
  221. begin
  222. Result.X := pivot.X + (pivot.X - pt.X);
  223. Result.Y := pivot.Y + (pivot.Y - pt.Y);
  224. end;
  225. //------------------------------------------------------------------------------
  226. function GetNormals(const path: TPathD): TPathD;
  227. var
  228. i, highI: integer;
  229. last: TPointD;
  230. begin
  231. highI := High(path);
  232. setLength(result, highI+1);
  233. if highI < 0 then Exit;
  234. last := Default(TPointD);
  235. for i := 0 to highI -1 do
  236. begin
  237. if (not SamePoint(path[i], path[i+1], 0.001)) then
  238. GetUnitNormal(path[i], path[i+1], last);
  239. result[i] := last;
  240. (*
  241. if GetUnitNormal(path[i], path[i+1], result[i]) then
  242. last := result[i] else
  243. result[i] := last;
  244. *)
  245. end;
  246. if (not SamePoint(path[highI], path[0], 0.001)) then
  247. begin
  248. GetUnitNormal(path[highI], path[0], result[highI]);
  249. last := result[highI];
  250. end;
  251. for i := 0 to highI do
  252. begin
  253. if (result[i].X <> 0) or (result[i].Y <> 0) then Break;
  254. result[i] := last;
  255. end;
  256. end;
  257. //------------------------------------------------------------------------------
  258. function ApplyNormal(const pt, norm: TPointD; delta: double): TPointD; inline;
  259. begin
  260. result := PointD(pt.X + norm.X * delta, pt.Y + norm.Y * delta);
  261. end;
  262. //------------------------------------------------------------------------------
  263. function StripNearDuplicates(const path: TPathD;
  264. minDist: double; isClosedPath: Boolean): TPathD;
  265. var
  266. i,j, len: integer;
  267. begin
  268. len := length(path);
  269. SetLength(Result, len);
  270. if len = 0 then Exit;
  271. Result[0] := path[0];
  272. j := 0;
  273. minDist := minDist * minDist;
  274. for i := 1 to len -1 do
  275. if not PointsNearEqual(Result[j], path[i], minDist) then
  276. begin
  277. inc(j);
  278. Result[j] := path[i];
  279. end;
  280. if isClosedPath and
  281. PointsNearEqual(Result[j], Result[0], minDist) then dec(j);
  282. SetLength(Result, j +1);
  283. end;
  284. //------------------------------------------------------------------------------
  285. procedure AppendToPath(var path: TPathD; const pt: TPointD);
  286. var
  287. len: integer;
  288. begin
  289. len := length(path);
  290. if (len > 0) and PointsEqual(pt, path[len -1]) then Exit;
  291. setLength(path, len + 1);
  292. path[len] := pt;
  293. end;
  294. //------------------------------------------------------------------------------
  295. procedure AppendPath(var path1: TPathD; const path2: TPathD); overload;
  296. var
  297. len1, len2: integer;
  298. begin
  299. len1 := length(path1);
  300. len2 := length(path2);
  301. if len2 = 0 then Exit;
  302. if (len1 > 0) and (path2[0] = path1[len1 -1]) then dec(len1);
  303. setLength(path1, len1 + len2);
  304. Move(path2[0], path1[len1], len2 * SizeOf(TPointD));
  305. end;
  306. procedure AppendPath(var paths: TPathsD; const extra: TPathD); overload;
  307. var
  308. len1, len2: integer;
  309. begin
  310. len2 := length(extra);
  311. if len2 = 0 then Exit;
  312. len1 := length(paths);
  313. setLength(paths, len1 + 1);
  314. paths[len1] := Copy(extra, 0, len2);
  315. end;
  316. procedure AppendPath(var paths: TPathsD; const extra: TPathsD); overload;
  317. var
  318. i, len1, len2: integer;
  319. begin
  320. len2 := length(extra);
  321. if len2 = 0 then Exit;
  322. len1 := length(paths);
  323. setLength(paths, len1 + len2);
  324. for i := 0 to len2 -1 do
  325. paths[len1+i] := Copy(extra[i], 0, length(extra[i]));
  326. end;
  327. //------------------------------------------------------------------------------
  328. //
  329. // Grow internals
  330. //
  331. //------------------------------------------------------------------------------
  332. function CalcRoundingStepsOld(radius: double): double;
  333. begin
  334. //the results of this function have been derived empirically
  335. //and may need further adjustment
  336. if radius < 0.55 then result := 4
  337. else result := Pi * Sqrt(radius);
  338. end;
  339. function CalcRoundingSteps(radius: double): double;
  340. const
  341. MINSTEPS = 6;
  342. SQUAREDMINSTEPS = Sqr(MINSTEPS);
  343. var
  344. Temp: TFloat;
  345. begin
  346. Temp := Abs(Radius) * Sqr(TWOPI);
  347. if Temp < SQUAREDMINSTEPS then
  348. Result := 6
  349. else
  350. Result := Round(Sqrt(Temp));
  351. end;
  352. //------------------------------------------------------------------------------
  353. //
  354. // Grow
  355. //
  356. //------------------------------------------------------------------------------
  357. function Grow(const path, normals: TPathD; delta: double;
  358. joinStyle: TJoinStyle; miterLim: double; isOpen: Boolean): TPathD;
  359. var
  360. resCnt, resCap : integer;
  361. norms : TPathD;
  362. stepsPerRadian : double;
  363. stepSin, stepCos : double;
  364. asin, acos : double;
  365. procedure AddPoint(const pt: TPointD);
  366. begin
  367. if resCnt >= resCap then
  368. begin
  369. inc(resCap, 64);
  370. setLength(result, resCap);
  371. end;
  372. result[resCnt] := pt;
  373. inc(resCnt);
  374. end;
  375. procedure DoMiter(j, k: Integer; cosA: Double);
  376. var
  377. q: Double;
  378. begin
  379. q := delta / (cosA +1);
  380. AddPoint(PointD(
  381. path[j].X + (norms[k].X + norms[j].X) *q,
  382. path[j].Y + (norms[k].Y + norms[j].Y) *q));
  383. end;
  384. procedure DoBevel(j, k: Integer);
  385. var
  386. absDelta: double;
  387. begin
  388. if k = j then
  389. begin
  390. absDelta := Abs(delta);
  391. AddPoint(PointD(
  392. path[j].x - absDelta * norms[j].x,
  393. path[j].y - absDelta * norms[j].y));
  394. AddPoint(PointD(
  395. path[j].x + absDelta * norms[j].x,
  396. path[j].y + absDelta * norms[j].y));
  397. end else
  398. begin
  399. AddPoint(PointD(
  400. path[j].x + delta * norms[k].x,
  401. path[j].y + delta * norms[k].y));
  402. AddPoint(PointD(
  403. path[j].x + delta * norms[j].x,
  404. path[j].y + delta * norms[j].y));
  405. end;
  406. end;
  407. procedure DoRound(j, k: Integer);
  408. var
  409. i, steps: Integer;
  410. pt: TPointD;
  411. dx, dy, oldDx: double;
  412. angle: double;
  413. begin
  414. // nb: angles may be negative but this will always be a convex join
  415. pt := path[j];
  416. if j = k then
  417. begin
  418. dx := -norms[k].X * delta;
  419. dy := -norms[k].Y * delta;
  420. end else
  421. begin
  422. dx := norms[k].X * delta;
  423. dy := norms[k].Y * delta;
  424. end;
  425. AddPoint(PointD(pt.X + dx, pt.Y + dy));
  426. angle := ArcTan2(asin, acos);
  427. steps := Ceil(stepsPerRadian * abs(angle));
  428. for i := 2 to steps do
  429. begin
  430. oldDx := dx;
  431. dx := oldDx * stepCos - stepSin * dy;
  432. dy := oldDx * stepSin + stepCos * dy;
  433. AddPoint(PointD(pt.X + dx, pt.Y + dy));
  434. end;
  435. AddPoint(PointD(
  436. pt.X + norms[j].X * delta,
  437. pt.Y + norms[j].Y * delta));
  438. end;
  439. var
  440. j, k : cardinal;
  441. len : cardinal;
  442. steps : double;
  443. highI : cardinal;
  444. iLo,iHi : cardinal;
  445. absDelta : double;
  446. begin
  447. Result := nil;
  448. if not Assigned(path) then exit;
  449. len := Length(path);
  450. if not isOpen then
  451. while (len > 2) and
  452. PointsNearEqual(path[len -1], path[0], 0.001) do
  453. dec(len);
  454. if len < 2 then Exit;
  455. absDelta := Abs(delta);
  456. if absDelta < MinStrokeWidth/2 then
  457. begin
  458. if delta < 0 then
  459. delta := -MinStrokeWidth/2 else
  460. delta := MinStrokeWidth/2;
  461. end;
  462. if absDelta < 1 then
  463. joinStyle := jsSquare
  464. else if joinStyle = jsAuto then
  465. begin
  466. if delta < AutoWidthThreshold / 2 then
  467. joinStyle := jsSquare else
  468. joinStyle := jsRound;
  469. end;
  470. if assigned(normals) then
  471. norms := normals else
  472. norms := GetNormals(path);
  473. highI := len -1;
  474. stepsPerRadian := 0;
  475. if joinStyle = jsRound then
  476. begin
  477. steps := CalcRoundingSteps(delta);
  478. // // avoid excessive precision // todo - recheck if needed
  479. // if (steps > absDelta * Pi) then
  480. // steps := absDelta * Pi;
  481. stepSin := sin(TwoPi/steps);
  482. stepCos := cos(TwoPi/steps);
  483. if (delta < 0) then stepSin := -stepSin;
  484. stepsPerRadian := steps / TwoPi;
  485. end;
  486. if miterLim <= 0 then miterLim := DefaultMiterLimit
  487. else if miterLim < 2 then miterLim := 2;
  488. miterLim := 2 /(sqr(miterLim));
  489. resCnt := 0;
  490. resCap := 0;
  491. if isOpen then
  492. begin
  493. iLo := 1; iHi := highI -1;
  494. k := 0;
  495. AddPoint(PointD(
  496. path[0].X + norms[0].X * delta,
  497. path[0].Y + norms[0].Y * delta));
  498. end else
  499. begin
  500. iLo := 0; iHi := highI;
  501. k := highI;
  502. end;
  503. for j := iLo to iHi do
  504. begin
  505. if PointsNearEqual(path[j], path[k], 0.01) then
  506. begin
  507. k := j; // todo - check if needed
  508. Continue;
  509. end;
  510. asin := CrossProduct(norms[k], norms[j]);
  511. if (asin > 1.0) then asin := 1.0
  512. else if (asin < -1.0) then asin := -1.0;
  513. acos := DotProduct(norms[k], norms[j]);
  514. if (acos > -0.999) and (asin * delta < 0) then
  515. begin
  516. // is concave
  517. AddPoint(PointD(
  518. path[j].X + norms[k].X * delta, path[j].Y + norms[k].Y * delta));
  519. AddPoint(path[j]);
  520. AddPoint(PointD(
  521. path[j].X + norms[j].X * delta, path[j].Y + norms[j].Y * delta));
  522. end
  523. else if (acos > 0.999) and (joinStyle <> jsRound) then
  524. begin
  525. // almost straight - less than 2.5 degree, so miter
  526. DoMiter(j, k, acos);
  527. end
  528. else if (joinStyle = jsMiter) then
  529. begin
  530. if (1 + acos > miterLim) then
  531. DoMiter(j, k, acos) else
  532. DoBevel(j, k);
  533. end
  534. else if (joinStyle = jsRound) then
  535. begin
  536. DoRound(j, k);
  537. end
  538. else
  539. DoBevel(j, k);
  540. k := j;
  541. end;
  542. if isOpen then
  543. AddPoint(PointD(
  544. path[highI].X + norms[highI].X * delta, //todo - check this !!!
  545. path[highI].Y + norms[highI].Y * delta));
  546. SetLength(Result, resCnt);
  547. end;
  548. //------------------------------------------------------------------------------
  549. //
  550. // RoughOutline internals
  551. //
  552. //------------------------------------------------------------------------------
  553. function GetAvgUnitVector(const vec1, vec2: TPointD): TPointD; inline;
  554. begin
  555. Result := NormalizeVector(PointD(vec1.X + vec2.X, vec1.Y + vec2.Y));
  556. end;
  557. //------------------------------------------------------------------------------
  558. //
  559. // RoughOutline
  560. //
  561. //------------------------------------------------------------------------------
  562. function GrowOpenLine(const line: TPathD; delta: double;
  563. joinStyle: TJoinStyle; endStyle: TEndStyle;
  564. miterLim: double): TPathD;
  565. var
  566. len : integer;
  567. resCnt, resCap : integer;
  568. asin, acos : double;
  569. stepSin, stepCos : double;
  570. stepsPerRadian : double;
  571. path, norms : TPathD;
  572. procedure AddPoint(const pt: TPointD);
  573. begin
  574. if resCnt >= resCap then
  575. begin
  576. inc(resCap, 64);
  577. setLength(result, resCap);
  578. end;
  579. result[resCnt] := pt;
  580. inc(resCnt);
  581. end;
  582. procedure DoMiter(j, k: Integer; cosA: Double);
  583. var
  584. q: Double;
  585. begin
  586. q := delta / (cosA +1);
  587. AddPoint(PointD(
  588. path[j].X + (norms[k].X + norms[j].X) *q,
  589. path[j].Y + (norms[k].Y + norms[j].Y) *q));
  590. end;
  591. procedure DoBevel(j, k: Integer);
  592. var
  593. absDelta: double;
  594. begin
  595. if k = j then
  596. begin
  597. absDelta := Abs(delta);
  598. AddPoint(PointD(
  599. path[j].x - absDelta * norms[j].x,
  600. path[j].y - absDelta * norms[j].y));
  601. AddPoint(PointD(
  602. path[j].x + absDelta * norms[j].x,
  603. path[j].y + absDelta * norms[j].y));
  604. end else
  605. begin
  606. AddPoint(PointD(
  607. path[j].x + delta * norms[k].x,
  608. path[j].y + delta * norms[k].y));
  609. AddPoint(PointD(
  610. path[j].x + delta * norms[j].x,
  611. path[j].y + delta * norms[j].y));
  612. end;
  613. end;
  614. procedure DoSquare(j, k: Integer);
  615. var
  616. vec, ptQ, ptR, ptS, ptT, ptU, ip: TPointD;
  617. absDelta: double;
  618. begin
  619. if k = j then
  620. begin
  621. vec.X := norms[j].Y; //squaring a line end
  622. vec.Y := -norms[j].X;
  623. end else
  624. begin
  625. // using the reciprocal of unit normals (as unit vectors)
  626. // get the average unit vector ...
  627. vec := GetAvgUnitVector(
  628. PointD(-norms[k].Y, norms[k].X),
  629. PointD(norms[j].Y, -norms[j].X));
  630. end;
  631. absDelta := Abs(delta);
  632. ptQ := PointD(path[j].X + absDelta * vec.X, path[j].Y + absDelta * vec.Y);
  633. ptR := PointD(ptQ.X + delta * vec.Y, ptQ.Y + delta * -vec.X);
  634. ptS := ReflectPoint(ptR, ptQ);
  635. // get 2 vertices along one edge offset
  636. ptT := PointD(
  637. path[k].X + norms[k].X * delta,
  638. path[k].Y + norms[k].Y * delta);
  639. if (j = k) then
  640. begin
  641. ptU.X := ptT.X + vec.X * delta;
  642. ptU.Y := ptT.Y + vec.Y * delta;
  643. ip := IntersectPoint(ptR, ptS, ptT, ptU);
  644. AddPoint(ReflectPoint(ip, ptQ));
  645. AddPoint(ip);
  646. end else
  647. begin
  648. ptU := PointD(
  649. path[j].X + norms[k].X * delta,
  650. path[j].Y + norms[k].Y * delta);
  651. ip := IntersectPoint(ptR, ptS, ptT, ptU);
  652. AddPoint(ip);
  653. AddPoint(ReflectPoint(ip, ptQ));
  654. end;
  655. end;
  656. procedure DoRound(j, k: Integer);
  657. var
  658. i, steps: Integer;
  659. pt: TPointD;
  660. dx, dy, oldDx: double;
  661. angle: double;
  662. begin
  663. // nb: angles may be negative but this will always be a convex join
  664. pt := path[j];
  665. if j = k then
  666. begin
  667. dx := -norms[k].X * delta;
  668. dy := -norms[k].Y * delta;
  669. angle := PI;
  670. end else
  671. begin
  672. dx := norms[k].X * delta;
  673. dy := norms[k].Y * delta;
  674. angle := ArcTan2(asin, acos);
  675. end;
  676. AddPoint(PointD(pt.X + dx, pt.Y + dy));
  677. steps := Ceil(stepsPerRadian * abs(angle));
  678. for i := 2 to steps do
  679. begin
  680. oldDx := dx;
  681. dx := oldDx * stepCos - stepSin * dy;
  682. dy := oldDx * stepSin + stepCos * dy;
  683. AddPoint(PointD(pt.X + dx, pt.Y + dy));
  684. end;
  685. AddPoint(PointD(
  686. pt.X + norms[j].X * delta,
  687. pt.Y + norms[j].Y * delta));
  688. end;
  689. procedure DoPoint(j: Cardinal; var k: Cardinal);
  690. begin
  691. asin := CrossProduct(norms[k], norms[j]);
  692. if (asin > 1.0) then asin := 1.0
  693. else if (asin < -1.0) then asin := -1.0;
  694. acos := DotProduct(norms[k], norms[j]);
  695. if (acos > -0.999) and (asin * delta < 0) then
  696. begin
  697. // is concave
  698. AddPoint(PointD(
  699. path[j].X + norms[k].X * delta, path[j].Y + norms[k].Y * delta));
  700. AddPoint(path[j]);
  701. AddPoint(PointD(
  702. path[j].X + norms[j].X * delta, path[j].Y + norms[j].Y * delta));
  703. end
  704. else if (acos > 0.999) and (joinStyle <> jsRound) then
  705. // almost straight - less than 2.5 degree, so miter
  706. DoMiter(j, k, acos)
  707. else if (joinStyle = jsMiter) then
  708. begin
  709. if (1 + acos > miterLim) then
  710. DoMiter(j, k, acos) else
  711. DoBevel(j, k);
  712. end
  713. else if (joinStyle = jsRound) then
  714. DoRound(j, k)
  715. else
  716. DoBevel(j, k);
  717. k := j;
  718. end;
  719. var
  720. highJ : cardinal;
  721. j, k : cardinal;
  722. steps : double;
  723. begin
  724. Result := nil;
  725. path := StripNearDuplicates(line, 0.5, false);
  726. len := length(path);
  727. if len = 0 then Exit;
  728. if delta < MinStrokeWidth then
  729. delta := MinStrokeWidth;
  730. delta := delta * 0.5;
  731. if len = 1 then
  732. begin
  733. with path[0] do
  734. result := Ellipse(RectD(x-delta, y-delta, x+delta, y+delta));
  735. Exit;
  736. end;
  737. Assert(endStyle <> esClosed);
  738. //with very narrow lines, don't get fancy with joins and line ends
  739. if (delta <= 1) then
  740. begin
  741. joinStyle := jsSquare;
  742. if endStyle = esRound then endStyle := esSquare;
  743. end
  744. else if joinStyle = jsAuto then
  745. begin
  746. if (endStyle = esRound) and
  747. (delta >= AutoWidthThreshold) then
  748. joinStyle := jsRound
  749. else
  750. joinStyle := jsSquare;
  751. end;
  752. stepsPerRadian := 0;
  753. if (joinStyle = jsRound) or (endStyle = esRound) then
  754. begin
  755. steps := CalcRoundingSteps(delta);
  756. // if (steps > absDelta * Pi) then // todo - recheck if needed
  757. // steps := absDelta * Pi;
  758. stepSin := sin(TwoPi/steps);
  759. stepCos := cos(TwoPi/steps);
  760. if (delta < 0) then stepSin := -stepSin;
  761. stepsPerRadian := steps / TwoPi;
  762. end;
  763. if miterLim <= 0 then miterLim := DefaultMiterLimit
  764. else if miterLim < 2 then miterLim := 2;
  765. miterLim := 2 /(sqr(miterLim));
  766. norms := GetNormals(path);
  767. resCnt := 0; resCap := 0;
  768. case endStyle of
  769. esButt: DoBevel(0,0);
  770. esRound: DoRound(0,0);
  771. else DoSquare(0, 0);
  772. end;
  773. // offset the left side going **forward**
  774. k := 0;
  775. highJ := len -1;
  776. for j := 1 to highJ -1 do DoPoint(j,k);
  777. // reverse the normals ...
  778. for j := highJ downto 1 do
  779. begin
  780. norms[j].X := -norms[j-1].X;
  781. norms[j].Y := -norms[j-1].Y;
  782. end;
  783. norms[0] := norms[len -1];
  784. case endStyle of
  785. esButt: DoBevel(highJ,highJ);
  786. esRound: DoRound(highJ,highJ);
  787. else DoSquare(highJ,highJ);
  788. end;
  789. // offset the left side going **backward**
  790. k := highJ;
  791. for j := highJ -1 downto 1 do
  792. DoPoint(j, k);
  793. SetLength(Result, resCnt);
  794. end;
  795. //------------------------------------------------------------------------------
  796. function GrowClosedLine(const line: TPathD; width: double;
  797. joinStyle: TJoinStyle; miterLimOrRndScale: double): TPathsD;
  798. var
  799. norms: TPathD;
  800. rec: TRectD;
  801. skipHole: Boolean;
  802. begin
  803. rec := GetBoundsD(line);
  804. skipHole := (rec.Width <= width) or (rec.Height <= width);
  805. if skipHole then
  806. begin
  807. SetLength(Result, 1);
  808. norms := GetNormals(line);
  809. Result[0] := Grow(line, norms, width/2, joinStyle, miterLimOrRndScale, false);
  810. end else
  811. begin
  812. SetLength(Result, 2);
  813. norms := GetNormals(line);
  814. Result[0] := Grow(line, norms, width/2, joinStyle, miterLimOrRndScale, false);
  815. Result[1] := ReversePath(
  816. Grow(line, norms, -width/2, joinStyle, miterLimOrRndScale, false));
  817. end;
  818. end;
  819. //------------------------------------------------------------------------------
  820. function RoughOutline(const line: TPathD; lineWidth: double;
  821. joinStyle: TJoinStyle; endStyle: TEndStyle;
  822. miterLimOrRndScale: double): TPathsD; overload;
  823. begin
  824. if not assigned(line) then
  825. Result := nil
  826. else if endStyle = esClosed then
  827. result := GrowClosedLine(line,
  828. lineWidth, joinStyle, miterLimOrRndScale)
  829. else
  830. begin
  831. SetLength(Result,1);
  832. result[0] := GrowOpenLine(line, lineWidth,
  833. joinStyle, endStyle, miterLimOrRndScale);
  834. end;
  835. end;
  836. //------------------------------------------------------------------------------
  837. function RoughOutline(const lines: TPathsD; lineWidth: double;
  838. joinStyle: TJoinStyle; endStyle: TEndStyle;
  839. miterLimOrRndScale: double): TPathsD; overload;
  840. var
  841. i: integer;
  842. lwDiv2: double;
  843. p: TPathD;
  844. begin
  845. result := nil;
  846. if not assigned(lines) then exit;
  847. if joinStyle = jsAuto then
  848. begin
  849. if endStyle in [esPolygon, esRound] then
  850. joinStyle := jsRound else
  851. joinStyle := jsSquare;
  852. end;
  853. if endStyle = esPolygon then
  854. begin
  855. for i := 0 to high(lines) do
  856. begin
  857. if Length(lines[i]) = 1 then
  858. begin
  859. lwDiv2 := lineWidth/2;
  860. with lines[i][0] do
  861. AppendPath(Result,
  862. Ellipse(RectD(x-lwDiv2, y-lwDiv2, x+lwDiv2, y+lwDiv2)));
  863. end else
  864. begin
  865. p := StripNearDuplicates(lines[i], 0.25, true);
  866. if Length(p) = 2 then AppendToPath(p, p[0]);
  867. AppendPath(Result,
  868. GrowClosedLine(p, lineWidth, joinStyle, miterLimOrRndScale));
  869. end;
  870. end;
  871. end
  872. else
  873. for i := 0 to high(lines) do
  874. AppendPath(Result, GrowOpenLine(lines[i], lineWidth,
  875. joinStyle, endStyle, miterLimOrRndScale));
  876. end;
  877. //------------------------------------------------------------------------------
  878. //
  879. // PolyLineBuilderAngus
  880. //
  881. //------------------------------------------------------------------------------
  882. //------------------------------------------------------------------------------
  883. // Grow
  884. //------------------------------------------------------------------------------
  885. class function PolyLineBuilderAngus.Grow(const Points, Normals: TArrayOfFloatPoint; const Delta: TFloat; JoinStyle: GR32_Polygons.TJoinStyle; Closed: Boolean; MiterLimit: TFloat): TArrayOfFloatPoint;
  886. begin
  887. Result := GR32_VectorUtils.Angus.Grow(Points, Normals, Delta * GrowScale, JoinStyleMap[JoinStyle], MiterLimit, not Closed);
  888. end;
  889. //------------------------------------------------------------------------------
  890. // BuildPoly*line
  891. //------------------------------------------------------------------------------
  892. class function PolyLineBuilderAngus.BuildPolyline(const Points: TArrayOfFloatPoint; StrokeWidth: TFloat; JoinStyle: GR32_Polygons.TJoinStyle; EndStyle: GR32_Polygons.TEndStyle; MiterLimit: TFloat): TArrayOfFloatPoint;
  893. var
  894. Res: TArrayOfArrayOfFloatPoint;
  895. begin
  896. Res := RoughOutline(Points, StrokeWidth * GrowScale, JoinStyleMap[JoinStyle], EndStyleMap[EndStyle], MiterLimit);
  897. if (Length(Res) > 0) then
  898. Result := Res[0]
  899. else
  900. SetLength(Result, 0);
  901. end;
  902. class function PolyLineBuilderAngus.BuildPolyPolyLine(const Points: TArrayOfArrayOfFloatPoint; Closed: Boolean; StrokeWidth: TFloat; JoinStyle: GR32_Polygons.TJoinStyle; EndStyle: GR32_Polygons.TEndStyle; MiterLimit: TFloat): TArrayOfArrayOfFloatPoint;
  903. var
  904. OutlineEndStyle: TEndStyle;
  905. begin
  906. if (Closed) then
  907. OutlineEndStyle := esPolygon
  908. else
  909. OutlineEndStyle := EndStyleMap[EndStyle];
  910. Result := RoughOutline(Points, StrokeWidth * GrowScale, JoinStyleMap[JoinStyle], OutlineEndStyle, MiterLimit);
  911. end;
  912. //------------------------------------------------------------------------------
  913. end.