Last active
February 15, 2023 08:02
-
-
Save PhosCity/9f76fea939937f91c0c09d70bcd0eb2e to your computer and use it in GitHub Desktop.
Improved img2ass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export script_name = "Phos-img2ass" | |
| export script_description = "Img2ass that is optimized to not be img2ass" | |
| export script_author = "PhosCity" | |
| export script_version = "0.1.5" | |
| export script_namespace = "phos.img2ass" | |
| DependencyControl = require "l0.DependencyControl" | |
| depctrl = DependencyControl{ | |
| feed: "https://raw.githubusercontent.com/PhosCity/Aegisub-Scripts/main/DependencyControl.json", | |
| { | |
| { "ZF.main", url: "https://github.com/TypesettingTools/zeref-Aegisub-Scripts", | |
| feed: "https://raw.githubusercontent.com/TypesettingTools/zeref-Aegisub-Scripts/main/DependencyControl.json"}, | |
| {"l0.Functional", version: "0.6.0", url: "https://github.com/TypesettingTools/Functional", | |
| feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}, | |
| {"Yutils"} | |
| }, | |
| } | |
| zf, Functional, Yutils = depctrl\requireModules! | |
| logger = depctrl\getLogger! | |
| {:list} = Functional | |
| ClipperLib = { | |
| Clear: -> | |
| return {}, | |
| ClipType: { | |
| ctIntersection: 0, | |
| ctUnion: 1, | |
| ctDifference: 2, | |
| ctXor: 3 | |
| }, | |
| PolyType: { | |
| ptSubject: 0, | |
| ptClip: 1 | |
| }, | |
| PolyFillType: { | |
| pftEvenOdd: 0, | |
| pftNonZero: 1, | |
| pftPositive: 2, | |
| pftNegative: 3 | |
| }, | |
| EdgeSide: { | |
| esLeft: 0, | |
| esRight: 1 | |
| }, | |
| Direction: { | |
| dRightToLeft: 0, | |
| dLeftToRight: 1 | |
| }, | |
| Point: {}, | |
| ClipperBase: { | |
| horizontal: -9007199254740992, | |
| Skip: -2, | |
| Unassigned: -1, | |
| tolerance: 1E-20, | |
| loRange: 47453132, | |
| hiRange: 4503599627370495 | |
| }, | |
| Clipper: { | |
| ioReverseSolution: 1, | |
| ioStrictlySimple: 2, | |
| ioPreserveCollinear: 4, | |
| NodeType: { | |
| ntAny: 0, | |
| ntOpen: 1, | |
| ntClosed: 2 | |
| } | |
| }, | |
| } | |
| ClipperLib.Error = (message) -> | |
| aegisub.log(message) | |
| aegisub.cancel! | |
| BitXOR = (a, b) -> | |
| p, c = 1, 0 | |
| while a>0 and b>0 | |
| ra, rb = a%2, b%2 | |
| if ra != rb then c = c+p | |
| a, b, p = (a-ra)/2, (b-rb)/2, p*2 | |
| if a<b then a = b | |
| while a>0 | |
| ra = a%2 | |
| if ra>0 then c = c+p | |
| a, p = (a-ra)/2, p*2 | |
| return c | |
| class Point | |
| new: (...) => | |
| a = {...} | |
| @X = 0 | |
| @Y = 0 | |
| if #a == 1 | |
| @X = a[1].X | |
| @Y = a[1].Y | |
| elseif #a == 2 | |
| @X = a[1] | |
| @Y = a[2] | |
| else | |
| @X = 0 | |
| @Y = 0 | |
| ClipperLib.Point.op_Equality = (a, b) -> | |
| return a.X == b.X and a.Y == b.Y | |
| ClipperLib.Point.op_Inequality = (a, b) -> | |
| return a.X != b.X or a.Y != b.Y | |
| class TEdge | |
| new: => | |
| @Bot = Point! | |
| @Curr = Point! --current (updated for every new scanbeam) | |
| @Top = Point! | |
| @Delta = Point! | |
| @Dx = 0 | |
| @PolyTyp = ClipperLib.PolyType.ptSubject | |
| @Side = ClipperLib.EdgeSide.esLeft -- side only refers to current side of solution poly | |
| @WindDelta = 0 -- 1 or -1 depending on winding direction | |
| @WindCnt = 0 | |
| @WindCnt2 = 0 -- winding count of the opposite polytype | |
| @OutIdx = 0 | |
| @Next = nil | |
| @Prev = nil | |
| @NextInLML = nil | |
| @NextInAEL = nil | |
| @PrevInAEL = nil | |
| @NextInSEL = nil | |
| @PrevInSEL = nil | |
| class LocalMinima | |
| new: => | |
| @Y = 0 | |
| @LeftBound = nil | |
| @RightBound = nil | |
| @Next = nil | |
| class Scanbeam | |
| new: => | |
| @Y = 0 | |
| @Next = nil | |
| class OutRec | |
| new: => | |
| @Idx = 0 | |
| @IsHole = false | |
| @IsOpen = false | |
| @FirstLeft = nil | |
| @Pts = nil | |
| @BottomPt = nil | |
| @PolyNode = nil | |
| class OutPt | |
| new: => | |
| @Idx = 0 | |
| @Pt = Point! | |
| @Next = nil | |
| @Prev = nil | |
| class Join | |
| new: => | |
| @OutPt1 = nil | |
| @OutPt2 = nil | |
| @OffPt = Point! | |
| ClipperLib.ClipperBase.SlopesEqual = (...) -> | |
| a = {...} | |
| if #a == 2 -- ClipperLib.ClipperBase.SlopesEqual3 = (e1, e2) -> | |
| e1, e2 = a[1], a[2] | |
| return ((e1.Delta.Y) * (e2.Delta.X)) == ((e1.Delta.X) * (e2.Delta.Y)) | |
| elseif #a == 3 -- ClipperLib.ClipperBase.SlopesEqual4 = (pt1, pt2, pt3) -> | |
| pt1, pt2, pt3 = a[1], a[2], a[3] | |
| return ((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) - ((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) == 0 | |
| elseif #a == 4 -- ClipperLib.ClipperBase.SlopesEqual5 = (pt1, pt2, pt3, pt4) -> | |
| pt1, pt2, pt3, pt4 = a[1], a[2], a[3], a[4] | |
| return ((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) - ((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) == 0 | |
| ClipperLib.ClipperBase.IsHorizontal = (e) -> | |
| return e.Delta.Y == 0 | |
| class ClipperBase | |
| m_MinimaList: nil | |
| m_CurrentLM: nil | |
| m_edges: {} | |
| m_HasOpenPaths: false | |
| PreserveCollinear: false | |
| m_Scanbeam: nil | |
| m_PolyOuts: nil | |
| m_ActiveEdges: nil | |
| InitEdge: (e, eNext, ePrev, pt) => | |
| e.Next = eNext | |
| e.Prev = ePrev | |
| e.Curr.X = pt.X | |
| e.Curr.Y = pt.Y | |
| e.OutIdx = -1 | |
| InitEdge2: (e, polyType) => | |
| if (e.Curr.Y >= e.Next.Curr.Y) | |
| e.Bot.X = e.Curr.X | |
| e.Bot.Y = e.Curr.Y | |
| e.Top.X = e.Next.Curr.X | |
| e.Top.Y = e.Next.Curr.Y | |
| else | |
| e.Top.X = e.Curr.X | |
| e.Top.Y = e.Curr.Y | |
| e.Bot.X = e.Next.Curr.X | |
| e.Bot.Y = e.Next.Curr.Y | |
| @SetDx(e) | |
| e.PolyTyp = polyType | |
| FindNextLocMin: (E) => | |
| E2 = nil | |
| while true do | |
| while (ClipperLib.Point.op_Inequality(E.Bot, E.Prev.Bot) or ClipperLib.Point.op_Equality(E.Curr, E.Top)) | |
| E = E.Next | |
| if (E.Dx != ClipperLib.ClipperBase.horizontal and E.Prev.Dx != ClipperLib.ClipperBase.horizontal) | |
| break | |
| while (E.Prev.Dx == ClipperLib.ClipperBase.horizontal) | |
| E = E.Prev | |
| E2 = E | |
| while (E.Dx == ClipperLib.ClipperBase.horizontal) | |
| E = E.Next | |
| if (E.Top.Y == E.Prev.Bot.Y) | |
| continue | |
| --ie just an intermediate horz. | |
| if (E2.Prev.Bot.X < E.Bot.X) | |
| E = E2 | |
| break | |
| return E | |
| ProcessBound: (E, LeftBoundIsForward) => | |
| EStart = nil | |
| Result = E | |
| Horz = nil | |
| if (Result.OutIdx == ClipperLib.ClipperBase.Skip) | |
| --check if there are edges beyond the skip edge in the bound and if so | |
| --create another LocMin and calling ProcessBound once more ... | |
| E = Result | |
| if (LeftBoundIsForward) | |
| while (E.Top.Y == E.Next.Bot.Y) | |
| E = E.Next | |
| while (E != Result and E.Dx == ClipperLib.ClipperBase.horizontal) | |
| E = E.Prev | |
| else | |
| while (E.Top.Y == E.Prev.Bot.Y) | |
| E = E.Prev | |
| while (E != Result and E.Dx == ClipperLib.ClipperBase.horizontal) | |
| E = E.Next | |
| if (E == Result) | |
| if (LeftBoundIsForward) | |
| Result = E.Next | |
| else | |
| Result = E.Prev | |
| else | |
| --there are more edges in the bound beyond result starting with E | |
| if (LeftBoundIsForward) | |
| E = Result.Next | |
| else | |
| E = Result.Prev | |
| locMin = LocalMinima! | |
| locMin.Next = nil | |
| locMin.Y = E.Bot.Y | |
| locMin.LeftBound = nil | |
| locMin.RightBound = E | |
| E.WindDelta = 0 | |
| Result = @ProcessBound(E, LeftBoundIsForward) | |
| @InsertLocalMinima(locMin) | |
| return Result | |
| if (E.Dx == ClipperLib.ClipperBase.horizontal) | |
| --We need to be careful with open paths because this may not be a | |
| --true local minima (ie E may be following a skip edge). | |
| --Also, consecutive horz. edges may start heading left before going right. | |
| if (LeftBoundIsForward) | |
| EStart = E.Prev | |
| else | |
| EStart = E.Next | |
| if (EStart.Dx == ClipperLib.ClipperBase.horizontal) --ie an adjoining horizontal skip edge | |
| if (EStart.Bot.X != E.Bot.X and EStart.Top.X != E.Bot.X) | |
| @ReverseHorizontal(E) | |
| elseif (EStart.Bot.X != E.Bot.X) | |
| @ReverseHorizontal(E) | |
| EStart = E | |
| if (LeftBoundIsForward) | |
| while (Result.Top.Y == Result.Next.Bot.Y and Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) | |
| Result = Result.Next | |
| if (Result.Dx == ClipperLib.ClipperBase.horizontal and Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) | |
| --nb: at the top of a bound, horizontals are added to the bound | |
| --only when the preceding edge attaches to the horizontal's left vertex | |
| --unless a Skip edge is encountered when that becomes the top divide | |
| Horz = Result | |
| while (Horz.Prev.Dx == ClipperLib.ClipperBase.horizontal) | |
| Horz = Horz.Prev | |
| if (Horz.Prev.Top.X > Result.Next.Top.X) | |
| Result = Horz.Prev | |
| while (E != Result) | |
| E.NextInLML = E.Next | |
| if (E.Dx == ClipperLib.ClipperBase.horizontal and E != EStart and E.Bot.X != E.Prev.Top.X) | |
| @ReverseHorizontal(E) | |
| E = E.Next | |
| if (E.Dx == ClipperLib.ClipperBase.horizontal and E != EStart and E.Bot.X != E.Prev.Top.X) | |
| @ReverseHorizontal(E) | |
| Result = Result.Next | |
| --move to the edge just beyond current bound | |
| else | |
| while (Result.Top.Y == Result.Prev.Bot.Y and Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) | |
| Result = Result.Prev | |
| if (Result.Dx == ClipperLib.ClipperBase.horizontal and Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) | |
| Horz = Result | |
| while (Horz.Next.Dx == ClipperLib.ClipperBase.horizontal) | |
| Horz = Horz.Next | |
| if (Horz.Next.Top.X == Result.Prev.Top.X or Horz.Next.Top.X > Result.Prev.Top.X) | |
| Result = Horz.Next | |
| while (E != Result) | |
| E.NextInLML = E.Prev | |
| if (E.Dx == ClipperLib.ClipperBase.horizontal and E != EStart and E.Bot.X != E.Next.Top.X) | |
| @ReverseHorizontal(E) | |
| E = E.Prev | |
| if (E.Dx == ClipperLib.ClipperBase.horizontal and E != EStart and E.Bot.X != E.Next.Top.X) | |
| @ReverseHorizontal(E) | |
| Result = Result.Prev | |
| --move to the edge just beyond current bound | |
| return Result | |
| AddPath: (pg, polyType, Closed) => | |
| if ClipperLib.use_lines | |
| if not Closed and polyType == ClipperLib.PolyType.ptClip | |
| ClipperLib.Error("AddPath: Open paths must be subject.") | |
| else | |
| if not Closed | |
| ClipperLib.Error("AddPath: Open paths have been disabled.") | |
| highI = #pg | |
| if Closed | |
| while highI > 1 and ClipperLib.Point.op_Equality(pg[highI], pg[1]) | |
| highI -= 1 | |
| while highI > 1 and ClipperLib.Point.op_Equality(pg[highI], pg[highI - 1]) | |
| highI -= 1 | |
| if (Closed and highI < 3) or (not Closed and highI < 2) | |
| return false | |
| edges = {} | |
| for i = 1, highI | |
| table.insert(edges, TEdge!) | |
| IsFlat = true | |
| --1. Basic (first) edge initialization ... | |
| edges[2].Curr.X = pg[2].X | |
| edges[2].Curr.Y = pg[2].Y | |
| @InitEdge(edges[1], edges[2], edges[highI], pg[1]) | |
| @InitEdge(edges[highI], edges[1], edges[highI - 1], pg[highI]) | |
| for i = highI - 1, 2, -1 | |
| @InitEdge(edges[i], edges[i + 1], edges[i - 1], pg[i]) | |
| --2. Remove duplicate vertices, and (when closed) collinear edges ... | |
| eStart = edges[1] | |
| E = eStart | |
| eLoopStop = eStart | |
| while true do | |
| if (E.Curr == E.Next.Curr and (Closed or E.Next != eStart)) | |
| if E == E.Next | |
| break | |
| if E == eStart | |
| eStart = E.Next | |
| E = @RemoveEdge(E) | |
| eLoopStop = E | |
| continue | |
| if E.Prev == E.Next | |
| break | |
| elseif (Closed and ClipperLib.ClipperBase.SlopesEqual(E.Prev.Curr, E.Curr, E.Next.Curr) and (not @PreserveCollinear or @Pt2IsBetweenPt1AndPt3(E.Prev.Curr, E.Curr, E.Next.Curr))) | |
| if E == eStart | |
| eStart = E.Next | |
| E = @RemoveEdge(E) | |
| E = E.Prev | |
| eLoopStop = E | |
| continue | |
| E = E.Next | |
| if (E == eLoopStop) or (not Closed and E.Next == eStart) | |
| break | |
| if ((not Closed and (E == E.Next)) or (Closed and (E.Prev == E.Next))) | |
| return false | |
| if (not Closed) | |
| @m_HasOpenPaths = true | |
| eStart.Prev.OutIdx = ClipperLib.ClipperBase.Skip | |
| --3. Do second stage of edge initialization ... | |
| E = eStart | |
| while true do | |
| @InitEdge2(E, polyType) | |
| E = E.Next | |
| if IsFlat and E.Curr.Y != eStart.Curr.Y | |
| IsFlat = false | |
| if E == eStart | |
| break | |
| --4. Finally, add edge bounds to LocalMinima list ... | |
| --Totally flat paths must be handled differently when adding them | |
| --to LocalMinima list to avoid endless loops etc ... | |
| if IsFlat | |
| if Closed | |
| return false | |
| E.Prev.OutIdx = ClipperLib.ClipperBase.Skip | |
| locMin = LocalMinima! | |
| locMin.Next = nil | |
| locMin.Y = E.Bot.Y | |
| locMin.LeftBound = nil | |
| locMin.RightBound = E | |
| locMin.RightBound.Side = ClipperLib.EdgeSide.esRight | |
| locMin.RightBound.WindDelta = 0 | |
| while true do | |
| if (E.Bot.X != E.Prev.Top.X) | |
| @ReverseHorizontal(E) | |
| if (E.Next.OutIdx == ClipperLib.ClipperBase.Skip) | |
| break | |
| E.NextInLML = E.Next | |
| E = E.Next | |
| @InsertLocalMinima(locMin) | |
| table.insert(@m_edges, edges) | |
| return true | |
| table.insert(@m_edges, edges) | |
| leftBoundIsForward = nil | |
| EMin = nil | |
| --workaround to avoid an endless loop in the while loop below when | |
| --open paths have matching start and end points ... | |
| if ClipperLib.Point.op_Equality(E.Prev.Bot, E.Prev.Top) | |
| E = E.Next | |
| while true do | |
| E = @FindNextLocMin(E) | |
| if E == EMin | |
| break | |
| elseif EMin == nil | |
| EMin = E | |
| --E and E.Prev now share a local minima (left aligned if horizontal). | |
| --Compare their slopes to find which starts which bound ... | |
| locMin = LocalMinima! | |
| locMin.Next = nil | |
| locMin.Y = E.Bot.Y | |
| if E.Dx < E.Prev.Dx | |
| locMin.LeftBound = E.Prev | |
| locMin.RightBound = E | |
| leftBoundIsForward = false | |
| else | |
| locMin.LeftBound = E | |
| locMin.RightBound = E.Prev | |
| leftBoundIsForward = true | |
| locMin.LeftBound.Side = ClipperLib.EdgeSide.esLeft | |
| locMin.RightBound.Side = ClipperLib.EdgeSide.esRight | |
| if not Closed | |
| locMin.LeftBound.WindDelta = 0 | |
| elseif locMin.LeftBound.Next == locMin.RightBound | |
| locMin.LeftBound.WindDelta = -1 | |
| else | |
| locMin.LeftBound.WindDelta = 1 | |
| locMin.RightBound.WindDelta = -locMin.LeftBound.WindDelta | |
| E = @ProcessBound(locMin.LeftBound, leftBoundIsForward) | |
| if (E.OutIdx == ClipperLib.ClipperBase.Skip) | |
| E = @ProcessBound(E, leftBoundIsForward) | |
| E2 = @ProcessBound(locMin.RightBound, not leftBoundIsForward) | |
| if (E2.OutIdx == ClipperLib.ClipperBase.Skip) | |
| E2 = @ProcessBound(E2, not leftBoundIsForward) | |
| if (locMin.LeftBound.OutIdx == ClipperLib.ClipperBase.Skip) | |
| locMin.LeftBound = nil | |
| elseif (locMin.RightBound.OutIdx == ClipperLib.ClipperBase.Skip) | |
| locMin.RightBound = nil | |
| @InsertLocalMinima(locMin) | |
| if (not leftBoundIsForward) | |
| E = E2 | |
| return true | |
| AddPaths: (ppg, polyType, closed) => | |
| result = false | |
| for i = 1, #ppg | |
| if @AddPath(ppg[i], polyType, closed) | |
| result = true | |
| return result | |
| SetDx: (e) => | |
| e.Delta.X = (e.Top.X - e.Bot.X) | |
| e.Delta.Y = (e.Top.Y - e.Bot.Y) | |
| if (e.Delta.Y == 0) | |
| e.Dx = ClipperLib.ClipperBase.horizontal | |
| else | |
| e.Dx = (e.Delta.X) / (e.Delta.Y) | |
| InsertLocalMinima: (newLm) => | |
| if (@m_MinimaList == nil) | |
| @m_MinimaList = newLm | |
| elseif (newLm.Y >= @m_MinimaList.Y) | |
| newLm.Next = @m_MinimaList | |
| @m_MinimaList = newLm | |
| else | |
| tmpLm = @m_MinimaList | |
| while (tmpLm.Next != nil and (newLm.Y < tmpLm.Next.Y)) | |
| tmpLm = tmpLm.Next | |
| newLm.Next = tmpLm.Next | |
| tmpLm.Next = newLm | |
| PopLocalMinima: (Y, current) => | |
| current.v = @m_CurrentLM | |
| if (@m_CurrentLM != nil and @m_CurrentLM.Y == Y) | |
| @m_CurrentLM = @m_CurrentLM.Next | |
| return true | |
| return false | |
| ReverseHorizontal: (e) => | |
| --swap horizontal edges' top and bottom x's so they follow the natural | |
| --progression of the bounds - ie so their xbots will align with the | |
| --adjoining lower edge. [Helpful in the ProcessHorizontal() method.] | |
| tmp = e.Top.X | |
| e.Top.X = e.Bot.X | |
| e.Bot.X = tmp | |
| Reset: => | |
| @m_CurrentLM = @m_MinimaList | |
| if (@m_CurrentLM == nil) -- ie nothing to process | |
| return | |
| -- reset all edges ... | |
| @m_Scanbeam = nil | |
| lm = @m_MinimaList | |
| while (lm != nil) | |
| @InsertScanbeam(lm.Y) | |
| e = lm.LeftBound | |
| if (e != nil) | |
| e.Curr.X = e.Bot.X | |
| e.Curr.Y = e.Bot.Y | |
| e.OutIdx = ClipperLib.ClipperBase.Unassigned | |
| e = lm.RightBound | |
| if (e != nil) | |
| e.Curr.X = e.Bot.X | |
| e.Curr.Y = e.Bot.Y | |
| e.OutIdx = ClipperLib.ClipperBase.Unassigned | |
| lm = lm.Next | |
| @m_ActiveEdges = nil | |
| InsertScanbeam: (Y) => | |
| --single-linked list: sorted descending, ignoring dups. | |
| if (@m_Scanbeam == nil) | |
| @m_Scanbeam = Scanbeam! | |
| @m_Scanbeam.Next = nil | |
| @m_Scanbeam.Y = Y | |
| elseif (Y > @m_Scanbeam.Y) | |
| newSb = Scanbeam! | |
| newSb.Y = Y | |
| newSb.Next = @m_Scanbeam | |
| @m_Scanbeam = newSb | |
| else | |
| sb2 = @m_Scanbeam | |
| while (sb2.Next != nil and Y <= sb2.Next.Y) | |
| sb2 = sb2.Next | |
| if (Y == sb2.Y) | |
| return | |
| -- ie ignores duplicates | |
| newSb1 = Scanbeam! | |
| newSb1.Y = Y | |
| newSb1.Next = sb2.Next | |
| sb2.Next = newSb1 | |
| PopScanbeam: (Y) => | |
| if (@m_Scanbeam == nil) | |
| Y.v = 0 | |
| return false | |
| Y.v = @m_Scanbeam.Y | |
| @m_Scanbeam = @m_Scanbeam.Next | |
| return true | |
| LocalMinimaPending: => | |
| return (@m_CurrentLM != nil) | |
| CreateOutRec: => | |
| result = OutRec! | |
| result.Idx = ClipperLib.ClipperBase.Unassigned | |
| result.IsHole = false | |
| result.IsOpen = false | |
| result.FirstLeft = nil | |
| result.Pts = nil | |
| result.BottomPt = nil | |
| result.PolyNode = nil | |
| table.insert(@m_PolyOuts, result) | |
| result.Idx = #@m_PolyOuts | |
| return result | |
| DisposeOutRec: (index) => | |
| outRec = @m_PolyOuts[index] | |
| outRec.Pts = nil | |
| outRec = nil | |
| @m_PolyOuts[index] = nil | |
| UpdateEdgeIntoAEL: (e) => | |
| if (e.NextInLML == nil) | |
| ClipperLib.Error("UpdateEdgeIntoAEL: invalid call") | |
| AelPrev = e.PrevInAEL | |
| AelNext = e.NextInAEL | |
| e.NextInLML.OutIdx = e.OutIdx | |
| if (AelPrev != nil) | |
| AelPrev.NextInAEL = e.NextInLML | |
| else | |
| @m_ActiveEdges = e.NextInLML | |
| if (AelNext != nil) | |
| AelNext.PrevInAEL = e.NextInLML | |
| e.NextInLML.Side = e.Side | |
| e.NextInLML.WindDelta = e.WindDelta | |
| e.NextInLML.WindCnt = e.WindCnt | |
| e.NextInLML.WindCnt2 = e.WindCnt2 | |
| e = e.NextInLML | |
| e.Curr.X = e.Bot.X | |
| e.Curr.Y = e.Bot.Y | |
| e.PrevInAEL = AelPrev | |
| e.NextInAEL = AelNext | |
| if (not ClipperLib.ClipperBase.IsHorizontal(e)) | |
| @InsertScanbeam(e.Top.Y) | |
| return e | |
| SwapPositionsInAEL: (edge1, edge2) => | |
| --check that one or other edge hasn't already been removed from AEL ... | |
| if (edge1.NextInAEL == edge1.PrevInAEL or edge2.NextInAEL == edge2.PrevInAEL) | |
| return | |
| if (edge1.NextInAEL == edge2) | |
| next = edge2.NextInAEL | |
| if (next != nil) | |
| next.PrevInAEL = edge1 | |
| prev = edge1.PrevInAEL | |
| if (prev != nil) | |
| prev.NextInAEL = edge2 | |
| edge2.PrevInAEL = prev | |
| edge2.NextInAEL = edge1 | |
| edge1.PrevInAEL = edge2 | |
| edge1.NextInAEL = next | |
| elseif (edge2.NextInAEL == edge1) | |
| next1 = edge1.NextInAEL | |
| if (next1 != nil) | |
| next1.PrevInAEL = edge2 | |
| prev1 = edge2.PrevInAEL | |
| if (prev1 != nil) | |
| prev1.NextInAEL = edge1 | |
| edge1.PrevInAEL = prev1 | |
| edge1.NextInAEL = edge2 | |
| edge2.PrevInAEL = edge1 | |
| edge2.NextInAEL = next1 | |
| else | |
| next2 = edge1.NextInAEL | |
| prev2 = edge1.PrevInAEL | |
| edge1.NextInAEL = edge2.NextInAEL | |
| if (edge1.NextInAEL != nil) | |
| edge1.NextInAEL.PrevInAEL = edge1 | |
| edge1.PrevInAEL = edge2.PrevInAEL | |
| if (edge1.PrevInAEL != nil) | |
| edge1.PrevInAEL.NextInAEL = edge1 | |
| edge2.NextInAEL = next2 | |
| if (edge2.NextInAEL != nil) | |
| edge2.NextInAEL.PrevInAEL = edge2 | |
| edge2.PrevInAEL = prev2 | |
| if (edge2.PrevInAEL != nil) | |
| edge2.PrevInAEL.NextInAEL = edge2 | |
| if (edge1.PrevInAEL == nil) | |
| @m_ActiveEdges = edge1 | |
| else | |
| if (edge2.PrevInAEL == nil) | |
| @m_ActiveEdges = edge2 | |
| DeleteFromAEL: (e) => | |
| AelPrev = e.PrevInAEL | |
| AelNext = e.NextInAEL | |
| if (AelPrev == nil and AelNext == nil and e != @m_ActiveEdges) | |
| return --already deleted | |
| if (AelPrev != nil) | |
| AelPrev.NextInAEL = AelNext | |
| else | |
| @m_ActiveEdges = AelNext | |
| if (AelNext != nil) | |
| AelNext.PrevInAEL = AelPrev | |
| e.NextInAEL = nil | |
| e.PrevInAEL = nil | |
| ClipperLib.Clipper.TopX = (edge, currentY) -> | |
| --if (edge.Bot == edge.Curr) alert ("edge.Bot = edge.Curr"); | |
| --if (edge.Bot == edge.Top) alert ("edge.Bot = edge.Top"); | |
| if (currentY == edge.Top.Y) | |
| return edge.Top.X | |
| return edge.Bot.X + edge.Dx * (currentY - edge.Bot.Y) | |
| class Clipper extends ClipperBase | |
| new: (InitOptions) => | |
| if InitOptions == nil then InitOptions = 0 | |
| @m_edges = {} --? | |
| @m_ClipType = ClipperLib.ClipType.ctIntersection | |
| @m_ClipFillType = ClipperLib.PolyFillType.pftEvenOdd | |
| @m_SubjFillType = ClipperLib.PolyFillType.pftEvenOdd | |
| @m_Scanbeam = nil | |
| @m_Maxima = nil | |
| @m_ActiveEdges = nil | |
| @m_SortedEdges = nil | |
| @m_IntersectList = {} | |
| @m_ExecuteLocked = false | |
| @m_PolyOuts = {} | |
| @m_Joins = {} | |
| @m_GhostJoins = {} | |
| @ReverseSolution = false | |
| @StrictlySimple = false | |
| --@PreserveCollinear = false | |
| @FinalSolution = nil | |
| Execute: (clipType, subjFillType, clipFillType) => | |
| @m_ExecuteLocked = true | |
| @m_SubjFillType = subjFillType | |
| @m_ClipFillType = clipFillType | |
| @m_ClipType = clipType | |
| succeeded = @ExecuteInternal! | |
| if (succeeded) | |
| @BuildResult! | |
| @DisposeAllPolyPts! | |
| @m_ExecuteLocked = false | |
| ExecuteInternal: => | |
| @Reset! | |
| @m_SortedEdges = nil | |
| @m_Maxima = nil | |
| botY = {} | |
| topY = {} | |
| if (not @PopScanbeam(botY)) | |
| return false | |
| @InsertLocalMinimaIntoAEL(botY.v) | |
| while (@PopScanbeam(topY) or @LocalMinimaPending!) | |
| @ProcessHorizontals! | |
| @m_GhostJoins = {} | |
| if (not @ProcessIntersections(topY.v)) | |
| return false | |
| @ProcessEdgesAtTopOfScanbeam(topY.v) | |
| botY.v = topY.v | |
| @InsertLocalMinimaIntoAEL(botY.v) | |
| --fix orientations ... | |
| outRec = nil | |
| --fix orientations ... | |
| for i = 1, #@m_PolyOuts | |
| outRec = @m_PolyOuts[i] | |
| if outRec.Pts == nil or outRec.IsOpen | |
| continue | |
| if (BitXOR(outRec.IsHole == true and 1 or 0, @ReverseSolution == true and 1 or 0)) == ((@AreaS1(outRec) > 0) == true and 1 or 0) | |
| @ReversePolyPtLinks(outRec.Pts) | |
| @JoinCommonEdges! | |
| for i = 1, #@m_PolyOuts | |
| outRec = @m_PolyOuts[i] | |
| if outRec.Pts == nil | |
| continue | |
| elseif outRec.IsOpen | |
| @FixupOutPolyline(outRec) | |
| else | |
| @FixupOutPolygon(outRec) | |
| if @StrictlySimple | |
| @DoSimplePolygons! | |
| @m_Joins = {} | |
| @m_GhostJoins = {} | |
| return true | |
| DisposeAllPolyPts: => | |
| for i = 1, #@m_PolyOuts | |
| @DisposeOutRec(i) | |
| @m_PolyOuts = ClipperLib.Clear! | |
| AddJoin: (Op1, Op2, OffPt) => | |
| j = Join! | |
| j.OutPt1 = Op1 | |
| j.OutPt2 = Op2 | |
| j.OffPt.X = OffPt.X | |
| j.OffPt.Y = OffPt.Y | |
| table.insert(@m_Joins, j) | |
| AddGhostJoin: (Op, OffPt) => | |
| j = Join! | |
| j.OutPt1 = Op | |
| j.OffPt.X = OffPt.X | |
| j.OffPt.Y = OffPt.Y | |
| table.insert(@m_GhostJoins, j) | |
| InsertLocalMinimaIntoAEL: (botY) => | |
| lm = {} | |
| lb = nil | |
| rb = nil | |
| while @PopLocalMinima(botY, lm) | |
| lb = lm.v.LeftBound | |
| rb = lm.v.RightBound | |
| Op1 = nil | |
| if (lb == nil) | |
| @InsertEdgeIntoAEL(rb, nil) | |
| @SetWindingCount(rb) | |
| if @IsContributing(rb) | |
| Op1 = @AddOutPt(rb, rb.Bot) | |
| elseif (rb == nil) | |
| @InsertEdgeIntoAEL(lb, nil) | |
| @SetWindingCount(lb) | |
| if @IsContributing(lb) | |
| Op1 = @AddOutPt(lb, lb.Bot) | |
| @InsertScanbeam(lb.Top.Y) | |
| else | |
| @InsertEdgeIntoAEL(lb, nil) | |
| @InsertEdgeIntoAEL(rb, lb) | |
| @SetWindingCount(lb) | |
| rb.WindCnt = lb.WindCnt | |
| rb.WindCnt2 = lb.WindCnt2 | |
| if @IsContributing(lb) | |
| Op1 = @AddLocalMinPoly(lb, rb, lb.Bot) | |
| @InsertScanbeam(lb.Top.Y) | |
| if (rb != nil) | |
| if ClipperLib.ClipperBase.IsHorizontal(rb) | |
| if rb.NextInLML != nil | |
| @InsertScanbeam(rb.NextInLML.Top.Y) | |
| @AddEdgeToSEL(rb) | |
| else | |
| @InsertScanbeam(rb.Top.Y) | |
| if (lb == nil or rb == nil) | |
| continue | |
| --if output polygons share an Edge with a horizontal rb, they'll need joining later ... | |
| if (Op1 != nil and ClipperLib.ClipperBase.IsHorizontal(rb) and #@m_GhostJoins > 0 and rb.WindDelta != 0) | |
| for i = 1, #@m_GhostJoins | |
| --if the horizontal Rb and a 'ghost' horizontal overlap, then convert | |
| --the 'ghost' join to a real join ready for later ... | |
| j = @m_GhostJoins[i] | |
| if (@HorzSegmentsOverlap(j.OutPt1.Pt.X, j.OffPt.X, rb.Bot.X, rb.Top.X)) | |
| @AddJoin(j.OutPt1, Op1, j.OffPt) | |
| if (lb.OutIdx >= 0 and lb.PrevInAEL != nil and lb.PrevInAEL.Curr.X == lb.Bot.X and lb.PrevInAEL.OutIdx >= 0 and ClipperLib.ClipperBase.SlopesEqual(lb.PrevInAEL.Curr, lb.PrevInAEL.Top, lb.Curr, lb.Top) and lb.WindDelta != 0 and lb.PrevInAEL.WindDelta != 0) | |
| Op2 = @AddOutPt(lb.PrevInAEL, lb.Bot) | |
| @AddJoin(Op1, Op2, lb.Top) | |
| if (lb.NextInAEL != rb) | |
| if (rb.OutIdx >= 0 and rb.PrevInAEL.OutIdx >= 0 and ClipperLib.ClipperBase.SlopesEqual(rb.PrevInAEL.Curr, rb.PrevInAEL.Top, rb.Curr, rb.Top) and rb.WindDelta != 0 and rb.PrevInAEL.WindDelta != 0) | |
| Op2 = @AddOutPt(rb.PrevInAEL, rb.Bot) | |
| @AddJoin(Op1, Op2, rb.Top) | |
| e = lb.NextInAEL | |
| if (e != nil) | |
| while (e != rb) | |
| --nb: For calculating winding counts etc, IntersectEdges() assumes | |
| --that param1 will be to the right of param2 ABOVE the intersection ... | |
| @IntersectEdges(rb, e, lb.Curr) | |
| --order important here | |
| e = e.NextInAEL | |
| InsertEdgeIntoAEL: (edge, startEdge) => | |
| if @m_ActiveEdges == nil | |
| edge.PrevInAEL = nil | |
| edge.NextInAEL = nil | |
| @m_ActiveEdges = edge | |
| elseif startEdge == nil and @E2InsertsBeforeE1(@m_ActiveEdges, edge) | |
| edge.PrevInAEL = nil | |
| edge.NextInAEL = @m_ActiveEdges | |
| @m_ActiveEdges.PrevInAEL = edge | |
| @m_ActiveEdges = edge | |
| else | |
| if startEdge == nil | |
| startEdge = @m_ActiveEdges | |
| while (startEdge.NextInAEL != nil and not @E2InsertsBeforeE1(startEdge.NextInAEL, edge)) | |
| startEdge = startEdge.NextInAEL | |
| edge.NextInAEL = startEdge.NextInAEL | |
| if startEdge.NextInAEL != nil | |
| startEdge.NextInAEL.PrevInAEL = edge | |
| edge.PrevInAEL = startEdge | |
| startEdge.NextInAEL = edge | |
| E2InsertsBeforeE1: (e1, e2) => | |
| if (e2.Curr.X == e1.Curr.X) | |
| if (e2.Top.Y > e1.Top.Y) | |
| return e2.Top.X < ClipperLib.Clipper.TopX(e1, e2.Top.Y) | |
| else | |
| return e1.Top.X > ClipperLib.Clipper.TopX(e2, e1.Top.Y) | |
| else | |
| return e2.Curr.X < e1.Curr.X | |
| IsEvenOddFillType: (edge) => | |
| if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| return @m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd | |
| else | |
| return @m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd | |
| IsEvenOddAltFillType: (edge) => | |
| if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| return @m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd | |
| else | |
| return @m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd | |
| IsContributing: (edge) => | |
| pft = nil | |
| pft2 = nil | |
| if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| pft = @m_SubjFillType | |
| pft2 = @m_ClipFillType | |
| else | |
| pft = @m_ClipFillType | |
| pft2 = @m_SubjFillType | |
| switch pft | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| if (edge.WindDelta == 0 and edge.WindCnt != 1) | |
| return false | |
| when ClipperLib.PolyFillType.pftNonZero | |
| if (math.abs(edge.WindCnt) != 1) | |
| return false | |
| when ClipperLib.PolyFillType.pftPositive | |
| if (edge.WindCnt != 1) | |
| return false | |
| else | |
| if (edge.WindCnt != -1) | |
| return false | |
| switch @m_ClipType | |
| when ClipperLib.ClipType.ctIntersection | |
| switch (pft2) | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| return (edge.WindCnt2 != 0) | |
| when ClipperLib.PolyFillType.pftNonZero | |
| return (edge.WindCnt2 != 0) | |
| when ClipperLib.PolyFillType.pftPositive | |
| return (edge.WindCnt2 > 0) | |
| else | |
| return (edge.WindCnt2 < 0) | |
| when ClipperLib.ClipType.ctUnion | |
| switch (pft2) | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftNonZero | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftPositive | |
| return (edge.WindCnt2 <= 0) | |
| else | |
| return (edge.WindCnt2 >= 0) | |
| when ClipperLib.ClipType.ctDifference | |
| if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| switch (pft2) | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftNonZero | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftPositive | |
| return (edge.WindCnt2 <= 0) | |
| else | |
| return (edge.WindCnt2 >= 0) | |
| else | |
| switch (pft2) | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| return (edge.WindCnt2 != 0) | |
| when ClipperLib.PolyFillType.pftNonZero | |
| return (edge.WindCnt2 != 0) | |
| when ClipperLib.PolyFillType.pftPositive | |
| return (edge.WindCnt2 > 0) | |
| else | |
| return (edge.WindCnt2 < 0) | |
| when ClipperLib.ClipType.ctXor | |
| if (edge.WindDelta == 0) | |
| switch (pft2) | |
| when ClipperLib.PolyFillType.pftEvenOdd | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftNonZero | |
| return (edge.WindCnt2 == 0) | |
| when ClipperLib.PolyFillType.pftPositive | |
| return (edge.WindCnt2 <= 0) | |
| else | |
| return (edge.WindCnt2 >= 0) | |
| else | |
| return true | |
| return true | |
| SetWindingCount: (edge) => | |
| e = edge.PrevInAEL | |
| --find the edge of the same polytype that immediately preceeds 'edge' in AEL | |
| while (e != nil and ((e.PolyTyp != edge.PolyTyp) or (e.WindDelta == 0))) | |
| e = e.PrevInAEL | |
| if (e == nil) | |
| pft = nil | |
| if edge.PolyTyp == ClipperLib.PolyType.ptSubject | |
| pft = @m_SubjFillType | |
| else | |
| pft = @m_ClipFillType | |
| if (edge.WindDelta == 0) | |
| edge.WindCnt = nil | |
| if pft == ClipperLib.PolyFillType.pftNegative | |
| edge.WindCnt = -1 | |
| else | |
| edge.WindCnt = 1 | |
| else | |
| edge.WindCnt = edge.WindDelta | |
| edge.WindCnt2 = 0 | |
| e = @m_ActiveEdges | |
| --ie get ready to calc WindCnt2 | |
| elseif (edge.WindDelta == 0 and @m_ClipType != ClipperLib.ClipType.ctUnion) | |
| edge.WindCnt = 1 | |
| edge.WindCnt2 = e.WindCnt2 | |
| e = e.NextInAEL | |
| --ie get ready to calc WindCnt2 | |
| elseif (@IsEvenOddFillType(edge)) | |
| --EvenOdd filling ... | |
| if (edge.WindDelta == 0) | |
| --are we inside a subj polygon ... | |
| Inside = true | |
| e2 = e.PrevInAEL | |
| while (e2 != nil) | |
| if (e2.PolyTyp == e.PolyTyp and e2.WindDelta != 0) | |
| Inside = not Inside | |
| e2 = e2.PrevInAEL | |
| edge.WindCnt = nil | |
| if Inside | |
| edge.WindCnt = 0 | |
| else | |
| edge.WindCnt = 1 | |
| else | |
| edge.WindCnt = edge.WindDelta | |
| edge.WindCnt2 = e.WindCnt2 | |
| e = e.NextInAEL | |
| --ie get ready to calc WindCnt2 | |
| else | |
| --nonZero, Positive or Negative filling ... | |
| if (e.WindCnt * e.WindDelta < 0) | |
| --prev edge is 'decreasing' WindCount (WC) toward zero | |
| --so we're outside the previous polygon ... | |
| if (math.abs(e.WindCnt) > 1) | |
| --outside prev poly but still inside another. | |
| --when reversing direction of prev poly use the same WC | |
| if (e.WindDelta * edge.WindDelta < 0) | |
| edge.WindCnt = e.WindCnt | |
| else | |
| edge.WindCnt = e.WindCnt + edge.WindDelta | |
| else | |
| edge.WindCnt = nil | |
| if edge.WindDelta == 0 | |
| edge.WindCnt = 1 | |
| else | |
| edge.WindCnt = edge.WindDelta | |
| else | |
| --prev edge is 'increasing' WindCount (WC) away from zero | |
| --so we're inside the previous polygon ... | |
| if (edge.WindDelta == 0) | |
| edge.WindCnt = nil | |
| if e.WindCnt < 0 | |
| edge.WindCnt = e.WindCnt - 1 | |
| else | |
| edge.WindCnt = e.WindCnt + 1 | |
| else if (e.WindDelta * edge.WindDelta < 0) | |
| edge.WindCnt = e.WindCnt | |
| else | |
| edge.WindCnt = e.WindCnt + edge.WindDelta | |
| edge.WindCnt2 = e.WindCnt2 | |
| e = e.NextInAEL | |
| --ie get ready to calc WindCnt2 | |
| --update WindCnt2 ... | |
| if (@IsEvenOddAltFillType(edge)) | |
| --EvenOdd filling ... | |
| while (e != edge) | |
| if (e.WindDelta != 0) | |
| edge.TempWindCnt2 = nil | |
| if edge.WindCnt2 == 0 | |
| edge.TempWindCnt2 = 1 | |
| else | |
| edge.TempWindCnt2 = 0 | |
| edge.WindCnt2 = edge.TempWindCnt2 | |
| e = e.NextInAEL | |
| else | |
| --nonZero, Positive or Negative filling ... | |
| while (e != edge) | |
| edge.WindCnt2 += e.WindDelta | |
| e = e.NextInAEL | |
| AddEdgeToSEL: (edge) => | |
| --SEL pointers in PEdge are use to build transient lists of horizontal edges. | |
| --However, since we don't need to worry about processing order, all additions | |
| --are made to the front of the list ... | |
| if (@m_SortedEdges == nil) | |
| @m_SortedEdges = edge | |
| edge.PrevInSEL = nil | |
| edge.NextInSEL = nil | |
| else | |
| edge.NextInSEL = @m_SortedEdges | |
| edge.PrevInSEL = nil | |
| @m_SortedEdges.PrevInSEL = edge | |
| @m_SortedEdges = edge | |
| PopEdgeFromSEL: (e) => | |
| --Pop edge from front of SEL (ie SEL is a FILO list) | |
| e.v = @m_SortedEdges | |
| if (e.v == nil) | |
| return false | |
| oldE = e.v | |
| @m_SortedEdges = e.v.NextInSEL | |
| if (@m_SortedEdges != nil) | |
| @m_SortedEdges.PrevInSEL = nil | |
| oldE.NextInSEL = nil | |
| oldE.PrevInSEL = nil | |
| return true | |
| AddLocalMaxPoly: (e1, e2, pt) => | |
| @AddOutPt(e1, pt) | |
| if (e2.WindDelta == 0) | |
| @AddOutPt(e2, pt) | |
| if (e1.OutIdx == e2.OutIdx) | |
| e1.OutIdx = -1 | |
| e2.OutIdx = -1 | |
| elseif (e1.OutIdx < e2.OutIdx) | |
| @AppendPolygon(e1, e2) | |
| else | |
| @AppendPolygon(e2, e1) | |
| AddLocalMinPoly: (e1, e2, pt) => | |
| result = nil | |
| e = nil | |
| prevE = nil | |
| if (ClipperLib.ClipperBase.IsHorizontal(e2) or (e1.Dx > e2.Dx)) | |
| result = @AddOutPt(e1, pt) | |
| e2.OutIdx = e1.OutIdx | |
| e1.Side = ClipperLib.EdgeSide.esLeft | |
| e2.Side = ClipperLib.EdgeSide.esRight | |
| e = e1 | |
| if (e.PrevInAEL == e2) | |
| prevE = e2.PrevInAEL | |
| else | |
| prevE = e.PrevInAEL | |
| else | |
| result = @AddOutPt(e2, pt) | |
| e1.OutIdx = e2.OutIdx | |
| e1.Side = ClipperLib.EdgeSide.esRight | |
| e2.Side = ClipperLib.EdgeSide.esLeft | |
| e = e2 | |
| if (e.PrevInAEL == e1) | |
| prevE = e1.PrevInAEL | |
| else | |
| prevE = e.PrevInAEL | |
| if (prevE != nil and prevE.OutIdx >= 0 and prevE.Top.Y < pt.Y and e.Top.Y < pt.Y) | |
| xPrev = ClipperLib.Clipper.TopX(prevE, pt.Y) | |
| xE = ClipperLib.Clipper.TopX(e, pt.Y) | |
| if ((xPrev == xE) and (e.WindDelta != 0) and (prevE.WindDelta != 0) and ClipperLib.ClipperBase.SlopesEqual(Point(xPrev, pt.Y), prevE.Top, Point(xE, pt.Y), e.Top)) | |
| outPt = @AddOutPt(prevE, pt) | |
| @AddJoin(result, outPt, e.Top) | |
| return result | |
| AddOutPt: (e, pt) => | |
| if (e.OutIdx < 0) | |
| outRec = @CreateOutRec! | |
| outRec.IsOpen = (e.WindDelta == 0) | |
| newOp = OutPt! | |
| outRec.Pts = newOp | |
| newOp.Idx = outRec.Idx | |
| newOp.Pt.X = pt.X | |
| newOp.Pt.Y = pt.Y | |
| newOp.Next = newOp | |
| newOp.Prev = newOp | |
| if (not outRec.IsOpen) | |
| @SetHoleState(e, outRec) | |
| e.OutIdx = outRec.Idx | |
| return newOp | |
| else | |
| outRec = @m_PolyOuts[e.OutIdx] | |
| --OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' | |
| op = outRec.Pts | |
| ToFront = (e.Side == ClipperLib.EdgeSide.esLeft) | |
| if (ToFront and ClipperLib.Point.op_Equality(pt, op.Pt)) | |
| return op | |
| elseif (not ToFront and ClipperLib.Point.op_Equality(pt, op.Prev.Pt)) | |
| return op.Prev | |
| newOp = OutPt! | |
| newOp.Idx = outRec.Idx | |
| newOp.Pt.X = pt.X | |
| newOp.Pt.Y = pt.Y | |
| newOp.Next = op | |
| newOp.Prev = op.Prev | |
| newOp.Prev.Next = newOp | |
| op.Prev = newOp | |
| if (ToFront) | |
| outRec.Pts = newOp | |
| return newOp | |
| GetLastOutPt: (e) => | |
| outRec = @m_PolyOuts[e.OutIdx] | |
| if (e.Side == ClipperLib.EdgeSide.esLeft) | |
| return outRec.Pts | |
| else | |
| return outRec.Pts.Prev | |
| HorzSegmentsOverlap: (seg1a, seg1b, seg2a, seg2b) => | |
| tmp = nil | |
| if (seg1a > seg1b) | |
| tmp = seg1a | |
| seg1a = seg1b | |
| seg1b = tmp | |
| if (seg2a > seg2b) | |
| tmp = seg2a | |
| seg2a = seg2b | |
| seg2b = tmp | |
| return (seg1a < seg2b) and (seg2a < seg1b) | |
| SetHoleState: (e, outRec) => | |
| e2 = e.PrevInAEL | |
| eTmp = nil | |
| while (e2 != nil) | |
| if (e2.OutIdx >= 0 and e2.WindDelta != 0) | |
| if (eTmp == nil) | |
| eTmp = e2 | |
| elseif (eTmp.OutIdx == e2.OutIdx) | |
| eTmp = nil --paired | |
| e2 = e2.PrevInAEL | |
| if (eTmp == nil) | |
| outRec.FirstLeft = nil | |
| outRec.IsHole = false | |
| else | |
| outRec.FirstLeft = @m_PolyOuts[eTmp.OutIdx] | |
| outRec.IsHole = not outRec.FirstLeft.IsHole | |
| GetDx: (pt1, pt2) => | |
| if (pt1.Y == pt2.Y) | |
| return ClipperLib.ClipperBase.horizontal | |
| else | |
| return (pt2.X - pt1.X) / (pt2.Y - pt1.Y) | |
| FirstIsBottomPt: (btmPt1, btmPt2) => | |
| p = btmPt1.Prev | |
| while ((ClipperLib.Point.op_Equality(p.Pt, btmPt1.Pt)) and (p != btmPt1)) | |
| p = p.Prev | |
| dx1p = math.abs(@GetDx(btmPt1.Pt, p.Pt)) | |
| p = btmPt1.Next | |
| while ((ClipperLib.Point.op_Equality(p.Pt, btmPt1.Pt)) and (p != btmPt1)) | |
| p = p.Next | |
| dx1n = math.abs(@GetDx(btmPt1.Pt, p.Pt)) | |
| p = btmPt2.Prev | |
| while ((ClipperLib.Point.op_Equality(p.Pt, btmPt2.Pt)) and (p != btmPt2)) | |
| p = p.Prev | |
| dx2p = math.abs(@GetDx(btmPt2.Pt, p.Pt)) | |
| p = btmPt2.Next | |
| while ((ClipperLib.Point.op_Equality(p.Pt, btmPt2.Pt)) and (p != btmPt2)) | |
| p = p.Next | |
| dx2n = math.abs(@GetDx(btmPt2.Pt, p.Pt)) | |
| if (math.max(dx1p, dx1n) == math.max(dx2p, dx2n) and math.min(dx1p, dx1n) == math.min(dx2p, dx2n)) | |
| return @Area(btmPt1) > 0 -- if otherwise identical use orientation | |
| else | |
| return (dx1p >= dx2p and dx1p >= dx2n) or (dx1n >= dx2p and dx1n >= dx2n) | |
| GetBottomPt: (pp) => | |
| dups = nil | |
| p = pp.Next | |
| while (p != pp) | |
| if (p.Pt.Y > pp.Pt.Y) | |
| pp = p | |
| dups = nil | |
| else if (p.Pt.Y == pp.Pt.Y and p.Pt.X <= pp.Pt.X) | |
| if (p.Pt.X < pp.Pt.X) | |
| dups = nil | |
| pp = p | |
| else | |
| if (p.Next != pp and p.Prev != pp) | |
| dups = p | |
| p = p.Next | |
| if (dups != nil) | |
| --there appears to be at least 2 vertices at bottomPt so ... | |
| while (dups != p) | |
| if (not @FirstIsBottomPt(p, dups)) | |
| pp = dups | |
| dups = dups.Next | |
| while (ClipperLib.Point.op_Inequality(dups.Pt, pp.Pt)) | |
| dups = dups.Next | |
| return pp | |
| GetLowermostRec: (outRec1, outRec2) => | |
| --work out which polygon fragment has the correct hole state ... | |
| if (outRec1.BottomPt == nil) | |
| outRec1.BottomPt = @GetBottomPt(outRec1.Pts) | |
| if (outRec2.BottomPt == nil) | |
| outRec2.BottomPt = @GetBottomPt(outRec2.Pts) | |
| bPt1 = outRec1.BottomPt | |
| bPt2 = outRec2.BottomPt | |
| if (bPt1.Pt.Y > bPt2.Pt.Y) | |
| return outRec1 | |
| elseif (bPt1.Pt.Y < bPt2.Pt.Y) | |
| return outRec2 | |
| elseif (bPt1.Pt.X < bPt2.Pt.X) | |
| return outRec1 | |
| elseif (bPt1.Pt.X > bPt2.Pt.X) | |
| return outRec2 | |
| elseif (bPt1.Next == bPt1) | |
| return outRec2 | |
| elseif (bPt2.Next == bPt2) | |
| return outRec1 | |
| elseif (@FirstIsBottomPt(bPt1, bPt2)) | |
| return outRec1 | |
| else | |
| return outRec2 | |
| OutRec1RightOfOutRec2: (outRec1, outRec2) => | |
| while true do | |
| outRec1 = outRec1.FirstLeft | |
| if (outRec1 == outRec2) | |
| return true | |
| if (outRec1 == nil) | |
| break | |
| return false | |
| GetOutRec: (idx) => | |
| outrec = @m_PolyOuts[idx] | |
| while (outrec != @m_PolyOuts[outrec.Idx]) | |
| outrec = @m_PolyOuts[outrec.Idx] | |
| return outrec | |
| AppendPolygon: (e1, e2) => | |
| --get the start and ends of both output polygons ... | |
| outRec1 = @m_PolyOuts[e1.OutIdx] | |
| outRec2 = @m_PolyOuts[e2.OutIdx] | |
| holeStateRec = nil | |
| if (@OutRec1RightOfOutRec2(outRec1, outRec2)) | |
| holeStateRec = outRec2 | |
| elseif (@OutRec1RightOfOutRec2(outRec2, outRec1)) | |
| holeStateRec = outRec1 | |
| else | |
| holeStateRec = @GetLowermostRec(outRec1, outRec2) | |
| --get the start and ends of both output polygons and | |
| --join E2 poly onto E1 poly and delete pointers to E2 ... | |
| p1_lft = outRec1.Pts | |
| p1_rt = p1_lft.Prev | |
| p2_lft = outRec2.Pts | |
| p2_rt = p2_lft.Prev | |
| --join e2 poly onto e1 poly and delete pointers to e2 ... | |
| if (e1.Side == ClipperLib.EdgeSide.esLeft) | |
| if (e2.Side == ClipperLib.EdgeSide.esLeft) | |
| --z y x a b c | |
| @ReversePolyPtLinks(p2_lft) | |
| p2_lft.Next = p1_lft | |
| p1_lft.Prev = p2_lft | |
| p1_rt.Next = p2_rt | |
| p2_rt.Prev = p1_rt | |
| outRec1.Pts = p2_rt | |
| else | |
| --x y z a b c | |
| p2_rt.Next = p1_lft | |
| p1_lft.Prev = p2_rt | |
| p2_lft.Prev = p1_rt | |
| p1_rt.Next = p2_lft | |
| outRec1.Pts = p2_lft | |
| else | |
| if (e2.Side == ClipperLib.EdgeSide.esRight) | |
| --a b c z y x | |
| @ReversePolyPtLinks(p2_lft) | |
| p1_rt.Next = p2_rt | |
| p2_rt.Prev = p1_rt | |
| p2_lft.Next = p1_lft | |
| p1_lft.Prev = p2_lft | |
| else | |
| --a b c x y z | |
| p1_rt.Next = p2_lft | |
| p2_lft.Prev = p1_rt | |
| p1_lft.Prev = p2_rt | |
| p2_rt.Next = p1_lft | |
| outRec1.BottomPt = nil | |
| if (holeStateRec == outRec2) | |
| if (outRec2.FirstLeft != outRec1) | |
| outRec1.FirstLeft = outRec2.FirstLeft | |
| outRec1.IsHole = outRec2.IsHole | |
| outRec2.Pts = nil | |
| outRec2.BottomPt = nil | |
| outRec2.FirstLeft = outRec1 | |
| OKIdx = e1.OutIdx | |
| ObsoleteIdx = e2.OutIdx | |
| e1.OutIdx = -1 | |
| --nb: safe because we only get here via AddLocalMaxPoly | |
| e2.OutIdx = -1 | |
| e = @m_ActiveEdges | |
| while (e != nil) | |
| if (e.OutIdx == ObsoleteIdx) | |
| e.OutIdx = OKIdx | |
| e.Side = e1.Side | |
| break | |
| e = e.NextInAEL | |
| outRec2.Idx = outRec1.Idx | |
| ReversePolyPtLinks: (pp) => | |
| if (pp == nil) | |
| return | |
| pp1 = nil | |
| pp2 = nil | |
| pp1 = pp | |
| while true do | |
| pp2 = pp1.Next | |
| pp1.Next = pp1.Prev | |
| pp1.Prev = pp2 | |
| pp1 = pp2 | |
| if pp1 == pp | |
| break | |
| IntersectEdges: (e1, e2, pt) => | |
| --e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before | |
| --e2 in AEL except when e1 is being inserted at the intersection point ... | |
| e1Contributing = (e1.OutIdx >= 0) | |
| e2Contributing = (e2.OutIdx >= 0) | |
| if (ClipperLib.use_lines) | |
| --if either edge is on an OPEN path ... | |
| if (e1.WindDelta == 0 or e2.WindDelta == 0) | |
| --ignore subject-subject open path intersections UNLESS they | |
| --are both open paths, AND they are both 'contributing maximas' ... | |
| if (e1.WindDelta == 0 and e2.WindDelta == 0) | |
| return | |
| --if intersecting a subj line with a subj poly ... | |
| elseif (e1.PolyTyp == e2.PolyTyp and e1.WindDelta != e2.WindDelta and @m_ClipType == ClipperLib.ClipType.ctUnion) | |
| if (e1.WindDelta == 0) | |
| if (e2Contributing) | |
| @AddOutPt(e1, pt) | |
| if (e1Contributing) | |
| e1.OutIdx = -1 | |
| else | |
| if (e1Contributing) | |
| @AddOutPt(e2, pt) | |
| if (e2Contributing) | |
| e2.OutIdx = -1 | |
| elseif (e1.PolyTyp != e2.PolyTyp) | |
| if ((e1.WindDelta == 0) and math.abs(e2.WindCnt) == 1 and (@m_ClipType != ClipperLib.ClipType.ctUnion or e2.WindCnt2 == 0)) | |
| @AddOutPt(e1, pt) | |
| if (e1Contributing) | |
| e1.OutIdx = -1 | |
| else if ((e2.WindDelta == 0) and (math.abs(e1.WindCnt) == 1) and (@m_ClipType != ClipperLib.ClipType.ctUnion or e1.WindCnt2 == 0)) | |
| @AddOutPt(e2, pt) | |
| if (e2Contributing) | |
| e2.OutIdx = -1 | |
| return | |
| --update winding counts... | |
| --assumes that e1 will be to the Right of e2 ABOVE the intersection | |
| if (e1.PolyTyp == e2.PolyTyp) | |
| if (@IsEvenOddFillType(e1)) | |
| oldE1WindCnt = e1.WindCnt | |
| e1.WindCnt = e2.WindCnt | |
| e2.WindCnt = oldE1WindCnt | |
| else | |
| if (e1.WindCnt + e2.WindDelta == 0) | |
| e1.WindCnt = -e1.WindCnt | |
| else | |
| e1.WindCnt += e2.WindDelta | |
| if (e2.WindCnt - e1.WindDelta == 0) | |
| e2.WindCnt = -e2.WindCnt | |
| else | |
| e2.WindCnt -= e1.WindDelta | |
| else | |
| if (not @IsEvenOddFillType(e2)) | |
| e1.WindCnt2 += e2.WindDelta | |
| else | |
| e1.WindCnt2 = e1.WindCnt2 == 0 and 1 or 0 | |
| if (not @IsEvenOddFillType(e1)) | |
| e2.WindCnt2 -= e1.WindDelta | |
| else | |
| e2.WindCnt2 = e2.WindCnt2 == 0 and 1 or 0 | |
| e1FillType, e2FillType, e1FillType2, e2FillType2 = nil | |
| if (e1.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| e1FillType = @m_SubjFillType | |
| e1FillType2 = @m_ClipFillType | |
| else | |
| e1FillType = @m_ClipFillType | |
| e1FillType2 = @m_SubjFillType | |
| if (e2.PolyTyp == ClipperLib.PolyType.ptSubject) | |
| e2FillType = @m_SubjFillType | |
| e2FillType2 = @m_ClipFillType | |
| else | |
| e2FillType = @m_ClipFillType | |
| e2FillType2 = @m_SubjFillType | |
| e1Wc, e2Wc = nil | |
| switch (e1FillType) | |
| when ClipperLib.PolyFillType.pftPositive | |
| e1Wc = e1.WindCnt | |
| when ClipperLib.PolyFillType.pftNegative | |
| e1Wc = -e1.WindCnt | |
| else | |
| e1Wc = math.abs(e1.WindCnt) | |
| switch (e2FillType) | |
| when ClipperLib.PolyFillType.pftPositive | |
| e2Wc = e2.WindCnt | |
| when ClipperLib.PolyFillType.pftNegative | |
| e2Wc = -e2.WindCnt | |
| else | |
| e2Wc = math.abs(e2.WindCnt) | |
| if (e1Contributing and e2Contributing) | |
| if ((e1Wc != 0 and e1Wc != 1) or (e2Wc != 0 and e2Wc != 1) or (e1.PolyTyp != e2.PolyTyp and @m_ClipType != ClipperLib.ClipType.ctXor)) | |
| @AddLocalMaxPoly(e1, e2, pt) | |
| else | |
| @AddOutPt(e1, pt) | |
| @AddOutPt(e2, pt) | |
| ClipperLib.Clipper.SwapSides(e1, e2) | |
| ClipperLib.Clipper.SwapPolyIndexes(e1, e2) | |
| elseif (e1Contributing) | |
| if (e2Wc == 0 or e2Wc == 1) | |
| @AddOutPt(e1, pt) | |
| ClipperLib.Clipper.SwapSides(e1, e2) | |
| ClipperLib.Clipper.SwapPolyIndexes(e1, e2) | |
| elseif (e2Contributing) | |
| if (e1Wc == 0 or e1Wc == 1) | |
| @AddOutPt(e2, pt) | |
| ClipperLib.Clipper.SwapSides(e1, e2) | |
| ClipperLib.Clipper.SwapPolyIndexes(e1, e2) | |
| elseif ((e1Wc == 0 or e1Wc == 1) and (e2Wc == 0 or e2Wc == 1)) | |
| --neither edge is currently contributing ... | |
| e1Wc2, e2Wc2 = nil | |
| switch (e1FillType2) | |
| when ClipperLib.PolyFillType.pftPositive | |
| e1Wc2 = e1.WindCnt2 | |
| when ClipperLib.PolyFillType.pftNegative | |
| e1Wc2 = -e1.WindCnt2 | |
| else | |
| e1Wc2 = math.abs(e1.WindCnt2) | |
| switch (e2FillType2) | |
| when ClipperLib.PolyFillType.pftPositive | |
| e2Wc2 = e2.WindCnt2 | |
| when ClipperLib.PolyFillType.pftNegative | |
| e2Wc2 = -e2.WindCnt2 | |
| else | |
| e2Wc2 = math.abs(e2.WindCnt2) | |
| if (e1.PolyTyp != e2.PolyTyp) | |
| @AddLocalMinPoly(e1, e2, pt) | |
| elseif (e1Wc == 1 and e2Wc == 1) | |
| switch (@m_ClipType) | |
| when ClipperLib.ClipType.ctIntersection | |
| if (e1Wc2 > 0 and e2Wc2 > 0) | |
| @AddLocalMinPoly(e1, e2, pt) | |
| when ClipperLib.ClipType.ctUnion | |
| if (e1Wc2 <= 0 and e2Wc2 <= 0) | |
| @AddLocalMinPoly(e1, e2, pt) | |
| when ClipperLib.ClipType.ctDifference | |
| if (((e1.PolyTyp == ClipperLib.PolyType.ptClip) and (e1Wc2 > 0) and (e2Wc2 > 0)) or ((e1.PolyTyp == ClipperLib.PolyType.ptSubject) and (e1Wc2 <= 0) and (e2Wc2 <= 0))) | |
| @AddLocalMinPoly(e1, e2, pt) | |
| when ClipperLib.ClipType.ctXor | |
| @AddLocalMinPoly(e1, e2, pt) | |
| else | |
| ClipperLib.Clipper.SwapSides(e1, e2) | |
| ProcessHorizontals: => | |
| horzEdge = {} | |
| while @PopEdgeFromSEL(horzEdge) | |
| @ProcessHorizontal(horzEdge.v) | |
| GetHorzDirection: (HorzEdge, Svar) => | |
| if (HorzEdge.Bot.X < HorzEdge.Top.X) | |
| Svar.Left = HorzEdge.Bot.X | |
| Svar.Right = HorzEdge.Top.X | |
| Svar.Dir = ClipperLib.Direction.dLeftToRight | |
| else | |
| Svar.Left = HorzEdge.Top.X | |
| Svar.Right = HorzEdge.Bot.X | |
| Svar.Dir = ClipperLib.Direction.dRightToLeft | |
| ProcessHorizontal: (horzEdge) => | |
| Svar = { | |
| Dir: nil, | |
| Left: nil, | |
| Right: nil | |
| } | |
| @GetHorzDirection(horzEdge, Svar) | |
| dir = Svar.Dir | |
| horzLeft = Svar.Left | |
| horzRight = Svar.Right | |
| IsOpen = horzEdge.WindDelta == 0 | |
| eLastHorz = horzEdge | |
| eMaxPair = nil | |
| while (eLastHorz.NextInLML != nil and ClipperLib.ClipperBase.IsHorizontal(eLastHorz.NextInLML)) | |
| eLastHorz = eLastHorz.NextInLML | |
| if (eLastHorz.NextInLML == nil) | |
| eMaxPair = @GetMaximaPair(eLastHorz) | |
| currMax = @m_Maxima | |
| if (currMax != nil) | |
| --get the first maxima in range (X) ... | |
| if (dir == ClipperLib.Direction.dLeftToRight) | |
| while (currMax != nil and currMax.X <= horzEdge.Bot.X) | |
| currMax = currMax.Next | |
| if (currMax != nil and currMax.X >= eLastHorz.Top.X) | |
| currMax = nil | |
| else | |
| while (currMax.Next != nil and currMax.Next.X < horzEdge.Bot.X) | |
| currMax = currMax.Next | |
| if (currMax.X <= eLastHorz.Top.X) | |
| currMax = nil | |
| op1 = nil | |
| while true do --loop through consec. horizontal edges | |
| IsLastHorz = (horzEdge == eLastHorz) | |
| e = @GetNextInAEL(horzEdge, dir) | |
| while (e != nil) | |
| --this code block inserts extra coords into horizontal edges (in output | |
| --polygons) whereever maxima touch these horizontal edges. This helps | |
| --'simplifying' polygons (ie if the Simplify property is set). | |
| if (currMax != nil) | |
| if (dir == ClipperLib.Direction.dLeftToRight) | |
| while (currMax != nil and currMax.X < e.Curr.X) | |
| if (horzEdge.OutIdx >= 0 and not IsOpen) | |
| @AddOutPt(horzEdge, Point(currMax.X, horzEdge.Bot.Y)) | |
| currMax = currMax.Next | |
| else | |
| while (currMax != nil and currMax.X > e.Curr.X) | |
| if (horzEdge.OutIdx >= 0 and not IsOpen) | |
| @AddOutPt(horzEdge, Point(currMax.X, horzEdge.Bot.Y)) | |
| currMax = currMax.Prev | |
| if ((dir == ClipperLib.Direction.dLeftToRight and e.Curr.X > horzRight) or (dir == ClipperLib.Direction.dRightToLeft and e.Curr.X < horzLeft)) | |
| break | |
| --Also break if we've got to the end of an intermediate horizontal edge ... | |
| --nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. | |
| if (e.Curr.X == horzEdge.Top.X and horzEdge.NextInLML != nil and e.Dx < horzEdge.NextInLML.Dx) | |
| break | |
| if (horzEdge.OutIdx >= 0 and not IsOpen) --note: may be done multiple times | |
| op1 = @AddOutPt(horzEdge, e.Curr) | |
| eNextHorz = @m_SortedEdges | |
| while (eNextHorz != nil) | |
| if (eNextHorz.OutIdx >= 0 and @HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, eNextHorz.Bot.X, eNextHorz.Top.X)) | |
| op2 = @GetLastOutPt(eNextHorz) | |
| @AddJoin(op2, op1, eNextHorz.Top) | |
| eNextHorz = eNextHorz.NextInSEL | |
| @AddGhostJoin(op1, horzEdge.Bot) | |
| --OK, so far we're still in range of the horizontal Edge but make sure | |
| --we're at the last of consec. horizontals when matching with eMaxPair | |
| if (e == eMaxPair and IsLastHorz) | |
| if (horzEdge.OutIdx >= 0) | |
| @AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge.Top) | |
| @DeleteFromAEL(horzEdge) | |
| @DeleteFromAEL(eMaxPair) | |
| return | |
| if (dir == ClipperLib.Direction.dLeftToRight) | |
| Pt = Point(e.Curr.X, horzEdge.Curr.Y) | |
| @IntersectEdges(horzEdge, e, Pt) | |
| else | |
| Pt = Point(e.Curr.X, horzEdge.Curr.Y) | |
| @IntersectEdges(e, horzEdge, Pt) | |
| eNext = @GetNextInAEL(e, dir) | |
| @SwapPositionsInAEL(horzEdge, e) | |
| e = eNext | |
| --Break out of loop if HorzEdge.NextInLML is not also horizontal ... | |
| if (horzEdge.NextInLML == nil or not ClipperLib.ClipperBase.IsHorizontal(horzEdge.NextInLML)) | |
| break | |
| horzEdge = @UpdateEdgeIntoAEL(horzEdge) | |
| if (horzEdge.OutIdx >= 0) | |
| @AddOutPt(horzEdge, horzEdge.Bot) | |
| Svar = { | |
| Dir: dir, | |
| Left: horzLeft, | |
| Right: horzRight | |
| } | |
| @GetHorzDirection(horzEdge, Svar) | |
| dir = Svar.Dir | |
| horzLeft = Svar.Left | |
| horzRight = Svar.Right | |
| if (horzEdge.OutIdx >= 0 and op1 == nil) | |
| op1 = @GetLastOutPt(horzEdge) | |
| eNextHorz = @m_SortedEdges | |
| while (eNextHorz != nil) | |
| if (eNextHorz.OutIdx >= 0 and @HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, eNextHorz.Bot.X, eNextHorz.Top.X)) | |
| op2 = @GetLastOutPt(eNextHorz) | |
| @AddJoin(op2, op1, eNextHorz.Top) | |
| eNextHorz = eNextHorz.NextInSEL | |
| @AddGhostJoin(op1, horzEdge.Top) | |
| if (horzEdge.NextInLML != nil) | |
| if (horzEdge.OutIdx >= 0) | |
| op1 = @AddOutPt(horzEdge, horzEdge.Top) | |
| horzEdge = @UpdateEdgeIntoAEL(horzEdge) | |
| if (horzEdge.WindDelta == 0) | |
| return | |
| --nb: HorzEdge is no longer horizontal here | |
| ePrev = horzEdge.PrevInAEL | |
| eNext = horzEdge.NextInAEL | |
| if (ePrev != nil and ePrev.Curr.X == horzEdge.Bot.X and ePrev.Curr.Y == horzEdge.Bot.Y and ePrev.WindDelta == 0 and (ePrev.OutIdx >= 0 and ePrev.Curr.Y > ePrev.Top.Y and ClipperLib.ClipperBase.SlopesEqual(horzEdge, ePrev))) | |
| op2 = @AddOutPt(ePrev, horzEdge.Bot) | |
| @AddJoin(op1, op2, horzEdge.Top) | |
| elseif (eNext != nil and eNext.Curr.X == horzEdge.Bot.X and eNext.Curr.Y == horzEdge.Bot.Y and eNext.WindDelta != 0 and eNext.OutIdx >= 0 and eNext.Curr.Y > eNext.Top.Y and ClipperLib.ClipperBase.SlopesEqual(horzEdge, eNext)) | |
| op2 = @AddOutPt(eNext, horzEdge.Bot) | |
| @AddJoin(op1, op2, horzEdge.Top) | |
| else | |
| horzEdge = @UpdateEdgeIntoAEL(horzEdge) | |
| else | |
| if (horzEdge.OutIdx >= 0) | |
| @AddOutPt(horzEdge, horzEdge.Top) | |
| @DeleteFromAEL(horzEdge) | |
| GetNextInAEL: (e, Direction) => | |
| r = nil | |
| if Direction == ClipperLib.Direction.dLeftToRight | |
| r = e.NextInAEL | |
| else | |
| r = e.PrevInAEL | |
| return r | |
| IsMinima: (e) => | |
| return e != nil and (e.Prev.NextInLML != e) and (e.Next.NextInLML != e) | |
| IsMaxima: (e, Y) => | |
| return (e != nil and e.Top.Y == Y and e.NextInLML == nil) | |
| IsIntermediate: (e, Y) => | |
| return (e.Top.Y == Y and e.NextInLML != nil) | |
| GetMaximaPair: (e) => | |
| if ((ClipperLib.Point.op_Equality(e.Next.Top, e.Top)) and e.Next.NextInLML == nil) | |
| return e.Next | |
| else | |
| if ((ClipperLib.Point.op_Equality(e.Prev.Top, e.Top)) and e.Prev.NextInLML == nil) | |
| return e.Prev | |
| else | |
| return nil | |
| GetMaximaPairEx: (e) => | |
| --as above but returns null if MaxPair isn't in AEL (unless it's horizontal) | |
| result = @GetMaximaPair(e) | |
| if (result == nil or result.OutIdx == ClipperLib.ClipperBase.Skip or ((result.NextInAEL == result.PrevInAEL) and not ClipperLib.ClipperBase.IsHorizontal(result))) | |
| return nil | |
| return result | |
| ProcessIntersections: (topY) => | |
| if (@m_ActiveEdges == nil) | |
| return true | |
| @BuildIntersectList(topY) | |
| if (#@m_IntersectList == 0) | |
| return true | |
| if (#@m_IntersectList == 1 or @FixupIntersectionOrder()) | |
| @ProcessIntersectList! | |
| else | |
| return false | |
| @m_SortedEdges = nil | |
| return true | |
| BuildIntersectList: (topY) => | |
| if (@m_ActiveEdges == nil) | |
| return | |
| --prepare for sorting ... | |
| e = @m_ActiveEdges | |
| @m_SortedEdges = e | |
| while (e != nil) | |
| e.PrevInSEL = e.PrevInAEL | |
| e.NextInSEL = e.NextInAEL | |
| e.Curr.X = ClipperLib.Clipper.TopX(e, topY) | |
| e = e.NextInAEL | |
| --bubblesort ... | |
| isModified = true | |
| while (isModified and @m_SortedEdges != nil) | |
| isModified = false | |
| e = @m_SortedEdges | |
| while (e.NextInSEL != nil) | |
| eNext = e.NextInSEL | |
| pt = Point! | |
| if (e.Curr.X > eNext.Curr.X) | |
| @IntersectPoint(e, eNext, pt) | |
| if (pt.Y < topY) | |
| pt = Point(ClipperLib.Clipper.TopX(e, topY), topY) | |
| newNode = IntersectNode! | |
| newNode.Edge1 = e | |
| newNode.Edge2 = eNext | |
| newNode.Pt.X = pt.X | |
| newNode.Pt.Y = pt.Y | |
| table.insert(@m_IntersectList, newNode) | |
| @SwapPositionsInSEL(e, eNext) | |
| isModified = true | |
| else | |
| e = eNext | |
| if (e.PrevInSEL != nil) | |
| e.PrevInSEL.NextInSEL = nil | |
| else | |
| break | |
| @m_SortedEdges = nil | |
| ProcessEdgesAtTopOfScanbeam: (topY) => | |
| e = @m_ActiveEdges | |
| while (e != nil) | |
| --1. process maxima, treating them as if they're 'bent' horizontal edges, | |
| --but exclude maxima with horizontal edges. nb: e can't be a horizontal. | |
| IsMaximaEdge = @IsMaxima(e, topY) | |
| if (IsMaximaEdge) | |
| eMaxPair = @GetMaximaPairEx(e) | |
| IsMaximaEdge = (eMaxPair == nil or not ClipperLib.ClipperBase.IsHorizontal(eMaxPair)) | |
| if (IsMaximaEdge) | |
| if (@StrictlySimple) | |
| @InsertMaxima(e.Top.X) | |
| ePrev = e.PrevInAEL | |
| @DoMaxima(e) | |
| if (ePrev == nil) | |
| e = @m_ActiveEdges | |
| else | |
| e = ePrev.NextInAEL | |
| else | |
| --2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... | |
| if (@IsIntermediate(e, topY) and ClipperLib.ClipperBase.IsHorizontal(e.NextInLML)) | |
| e = @UpdateEdgeIntoAEL(e) | |
| if (e.OutIdx >= 0) | |
| @AddOutPt(e, e.Bot) | |
| @AddEdgeToSEL(e) | |
| else | |
| e.Curr.X = ClipperLib.Clipper.TopX(e, topY) | |
| e.Curr.Y = topY | |
| --When StrictlySimple and 'e' is being touched by another edge, then | |
| --make sure both edges have a vertex here ... | |
| if (@StrictlySimple) | |
| ePrev = e.PrevInAEL | |
| if ((e.OutIdx >= 0) and (e.WindDelta != 0) and ePrev != nil and (ePrev.OutIdx >= 0) and (ePrev.Curr.X == e.Curr.X) and (ePrev.WindDelta != 0)) | |
| ip = Point(e.Curr) | |
| op = @AddOutPt(ePrev, ip) | |
| op2 = @AddOutPt(e, ip) | |
| @AddJoin(op, op2, ip) --StrictlySimple (type-3) join | |
| e = e.NextInAEL | |
| --3. Process horizontals at the Top of the scanbeam ... | |
| @ProcessHorizontals! | |
| @m_Maxima = nil | |
| --4. Promote intermediate vertices ... | |
| e = @m_ActiveEdges | |
| while (e != nil) | |
| if (@IsIntermediate(e, topY)) | |
| op = nil | |
| if (e.OutIdx >= 0) | |
| op = @AddOutPt(e, e.Top) | |
| e = @UpdateEdgeIntoAEL(e) | |
| --if output polygons share an edge, they'll need joining later ... | |
| ePrev = e.PrevInAEL | |
| eNext = e.NextInAEL | |
| if (ePrev != nil and ePrev.Curr.X == e.Bot.X and ePrev.Curr.Y == e.Bot.Y and op != nil and ePrev.OutIdx >= 0 and ePrev.Curr.Y == ePrev.Top.Y and ClipperLib.ClipperBase.SlopesEqual(e.Curr, e.Top, ePrev.Curr, ePrev.Top) and (e.WindDelta != 0) and (ePrev.WindDelta != 0)) | |
| op2 = @AddOutPt(ePrev2, e.Bot) | |
| @AddJoin(op, op2, e.Top) | |
| elseif (eNext != nil and eNext.Curr.X == e.Bot.X and eNext.Curr.Y == e.Bot.Y and op != nil and eNext.OutIdx >= 0 and eNext.Curr.Y == eNext.Top.Y and ClipperLib.ClipperBase.SlopesEqual(e.Curr, e.Top, eNext.Curr, eNext.Top) and (e.WindDelta != 0) and (eNext.WindDelta != 0)) | |
| op2 = @AddOutPt(eNext, e.Bot) | |
| @AddJoin(op, op2, e.Top) | |
| e = e.NextInAEL | |
| PointCount: (pts) => | |
| if (pts == nil) | |
| return 0 | |
| result = 0 | |
| p = pts | |
| while true do | |
| result += 1 | |
| p = p.Next | |
| if p == pts | |
| break | |
| return result | |
| BuildResult: => | |
| polyg = ClipperLib.Clear! | |
| for i = 1, #@m_PolyOuts | |
| outRec = @m_PolyOuts[i] | |
| if (outRec.Pts == nil) | |
| continue | |
| p = outRec.Pts.Prev | |
| cnt = @PointCount(p) | |
| if (cnt < 2) | |
| continue | |
| pg = {cnt} | |
| for j = 1, cnt | |
| pg[j] = p.Pt | |
| p = p.Prev | |
| table.insert(polyg, pg) | |
| @FinalSolution = polyg | |
| FixupOutPolygon: (outRec) => | |
| --FixupOutPolygon() - removes duplicate points and simplifies consecutive | |
| --parallel edges by removing the middle vertex. | |
| lastOK = nil | |
| outRec.BottomPt = nil | |
| pp = outRec.Pts | |
| preserveCol = @PreserveCollinear or @StrictlySimple | |
| while true do | |
| if (pp.Prev == pp or pp.Prev == pp.Next) | |
| outRec.Pts = nil | |
| return | |
| --test for duplicate points and collinear edges ... | |
| if ((ClipperLib.Point.op_Equality(pp.Pt, pp.Next.Pt)) or (ClipperLib.Point.op_Equality(pp.Pt, pp.Prev.Pt)) or (ClipperLib.ClipperBase.SlopesEqual(pp.Prev.Pt, pp.Pt, pp.Next.Pt) and (not preserveCol or not @Pt2IsBetweenPt1AndPt3(pp.Prev.Pt, pp.Pt, pp.Next.Pt)))) | |
| lastOK = nil | |
| pp.Prev.Next = pp.Next | |
| pp.Next.Prev = pp.Prev | |
| pp = pp.Prev | |
| elseif (pp == lastOK) | |
| break | |
| else | |
| if (lastOK == nil) | |
| lastOK = pp | |
| pp = pp.Next | |
| outRec.Pts = pp | |
| DupOutPt: (outPt, InsertAfter) => | |
| result = OutPt! | |
| result.Pt.X = outPt.Pt.X | |
| result.Pt.Y = outPt.Pt.Y | |
| result.Idx = outPt.Idx | |
| if (InsertAfter) | |
| result.Next = outPt.Next | |
| result.Prev = outPt | |
| outPt.Next.Prev = result | |
| outPt.Next = result | |
| else | |
| result.Prev = outPt.Prev | |
| result.Next = outPt | |
| outPt.Prev.Next = result | |
| outPt.Prev = result | |
| return result | |
| GetOverlap: (a1, a2, b1, b2, Sval) => | |
| if (a1 < a2) | |
| if (b1 < b2) | |
| Sval.Left = math.max(a1, b1) | |
| Sval.Right = math.min(a2, b2) | |
| else | |
| Sval.Left = math.max(a1, b2) | |
| Sval.Right = math.min(a2, b1) | |
| else | |
| if (b1 < b2) | |
| Sval.Left = math.max(a2, b1) | |
| Sval.Right = math.min(a1, b2) | |
| else | |
| Sval.Left = math.max(a2, b2) | |
| Sval.Right = math.min(a1, b1) | |
| return Sval.Left < Sval.Right | |
| JoinHorz: (op1, op1b, op2, op2b, Pt, DiscardLeft) => | |
| Dir1 = nil | |
| Dir2 = nil | |
| if op1.Pt.X > op1b.Pt.X | |
| Dir1 = ClipperLib.Direction.dRightToLeft | |
| else | |
| Dir1 = ClipperLib.Direction.dLeftToRight | |
| if op2.Pt.X > op2b.Pt.X | |
| Dir2 = ClipperLib.Direction.dRightToLeft | |
| else | |
| Dir2 = ClipperLib.Direction.dLeftToRight | |
| if (Dir1 == Dir2) | |
| return false | |
| --When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we | |
| --want Op1b to be on the Right. (And likewise with Op2 and Op2b.) | |
| --So, to facilitate this while inserting Op1b and Op2b ... | |
| --when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, | |
| --otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) | |
| if (Dir1 == ClipperLib.Direction.dLeftToRight) | |
| while (op1.Next.Pt.X <= Pt.X and op1.Next.Pt.X >= op1.Pt.X and op1.Next.Pt.Y == Pt.Y) | |
| op1 = op1.Next | |
| if (DiscardLeft and (op1.Pt.X != Pt.X)) | |
| op1 = op1.Next | |
| op1b = @DupOutPt(op1, not DiscardLeft) | |
| if (ClipperLib.Point.op_Inequality(op1b.Pt, Pt)) | |
| op1 = op1b | |
| op1.Pt.X = Pt.X | |
| op1.Pt.Y = Pt.Y | |
| op1b = @DupOutPt(op1, not DiscardLeft) | |
| else | |
| while (op1.Next.Pt.X >= Pt.X and op1.Next.Pt.X <= op1.Pt.X and op1.Next.Pt.Y == Pt.Y) | |
| op1 = op1.Next | |
| if (not DiscardLeft and (op1.Pt.X != Pt.X)) | |
| op1 = op1.Next | |
| op1b = @DupOutPt(op1, DiscardLeft) | |
| if (ClipperLib.Point.op_Inequality(op1b.Pt, Pt)) | |
| op1 = op1b | |
| op1.Pt.X = Pt.X | |
| op1.Pt.Y = Pt.Y | |
| op1b = @DupOutPt(op1, DiscardLeft) | |
| if (Dir2 == ClipperLib.Direction.dLeftToRight) | |
| while (op2.Next.Pt.X <= Pt.X and op2.Next.Pt.X >= op2.Pt.X and op2.Next.Pt.Y == Pt.Y) | |
| op2 = op2.Next | |
| if (DiscardLeft and (op2.Pt.X != Pt.X)) | |
| op2 = op2.Next | |
| op2b = @DupOutPt(op2, not DiscardLeft) | |
| if (ClipperLib.Point.op_Inequality(op2b.Pt, Pt)) | |
| op2 = op2b | |
| op2.Pt.X = Pt.X | |
| op2.Pt.Y = Pt.Y | |
| op2b = @DupOutPt(op2, not DiscardLeft) | |
| else | |
| while (op2.Next.Pt.X >= Pt.X and op2.Next.Pt.X <= op2.Pt.X and op2.Next.Pt.Y == Pt.Y) | |
| op2 = op2.Next | |
| if (not DiscardLeft and (op2.Pt.X != Pt.X)) | |
| op2 = op2.Next | |
| op2b = @DupOutPt(op2, DiscardLeft) | |
| if (ClipperLib.Point.op_Inequality(op2b.Pt, Pt)) | |
| op2 = op2b | |
| op2.Pt.X = Pt.X | |
| op2.Pt.Y = Pt.Y | |
| op2b = @DupOutPt(op2, DiscardLeft) | |
| if ((Dir1 == ClipperLib.Direction.dLeftToRight) == DiscardLeft) | |
| op1.Prev = op2 | |
| op2.Next = op1 | |
| op1b.Next = op2b | |
| op2b.Prev = op1b | |
| else | |
| op1.Next = op2 | |
| op2.Prev = op1 | |
| op1b.Prev = op2b | |
| op2b.Next = op1b | |
| return true | |
| JoinPoints: (j, outRec1, outRec2) => | |
| op1 = j.OutPt1 | |
| op1b = OutPt! | |
| op2 = j.OutPt2 | |
| op2b = OutPt! | |
| --There are 3 kinds of joins for output polygons ... | |
| --1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere | |
| --along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). | |
| --2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same | |
| --location at the Bottom of the overlapping segment (& Join.OffPt is above). | |
| --3. StrictlySimple joins where edges touch but are not collinear and where | |
| --Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. | |
| isHorizontal = (j.OutPt1.Pt.Y == j.OffPt.Y) | |
| if (isHorizontal and (ClipperLib.Point.op_Equality(j.OffPt, j.OutPt1.Pt)) and (ClipperLib.Point.op_Equality(j.OffPt, j.OutPt2.Pt))) | |
| --Strictly Simple join ... | |
| if (outRec1 != outRec2) | |
| return false | |
| op1b = j.OutPt1.Next | |
| while (op1b != op1 and (ClipperLib.Point.op_Equality(op1b.Pt, j.OffPt))) | |
| op1b = op1b.Next | |
| reverse1 = (op1b.Pt.Y > j.OffPt.Y) | |
| op2b = j.OutPt2.Next | |
| while (op2b != op2 and (ClipperLib.Point.op_Equality(op2b.Pt, j.OffPt))) | |
| op2b = op2b.Next | |
| reverse2 = (op2b.Pt.Y > j.OffPt.Y) | |
| if (reverse1 == reverse2) | |
| return false | |
| if (reverse1) | |
| op1b = @DupOutPt(op1, false) | |
| op2b = @DupOutPt(op2, true) | |
| op1.Prev = op2 | |
| op2.Next = op1 | |
| op1b.Next = op2b | |
| op2b.Prev = op1b | |
| j.OutPt1 = op1 | |
| j.OutPt2 = op1b | |
| return true | |
| else | |
| op1b = @DupOutPt(op1, true) | |
| op2b = @DupOutPt(op2, false) | |
| op1.Next = op2 | |
| op2.Prev = op1 | |
| op1b.Prev = op2b | |
| op2b.Next = op1b | |
| j.OutPt1 = op1 | |
| j.OutPt2 = op1b | |
| return true | |
| elseif (isHorizontal) | |
| --treat horizontal joins differently to non-horizontal joins since with | |
| --them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt | |
| --may be anywhere along the horizontal edge. | |
| op1b = op1 | |
| while (op1.Prev.Pt.Y == op1.Pt.Y and op1.Prev != op1b and op1.Prev != op2) | |
| op1 = op1.Prev | |
| while (op1b.Next.Pt.Y == op1b.Pt.Y and op1b.Next != op1 and op1b.Next != op2) | |
| op1b = op1b.Next | |
| if (op1b.Next == op1 or op1b.Next == op2) | |
| return false | |
| --a flat 'polygon' | |
| op2b = op2 | |
| while (op2.Prev.Pt.Y == op2.Pt.Y and op2.Prev != op2b and op2.Prev != op1b) | |
| op2 = op2.Prev | |
| while (op2b.Next.Pt.Y == op2b.Pt.Y and op2b.Next != op2 and op2b.Next != op1) | |
| op2b = op2b.Next | |
| if (op2b.Next == op2 or op2b.Next == op1) | |
| return false | |
| --a flat 'polygon' | |
| --Op1 -. Op1b & Op2 -. Op2b are the extremites of the horizontal edges | |
| Sval = { | |
| Left: nil, | |
| Right: nil | |
| } | |
| if (not @GetOverlap(op1.Pt.X, op1b.Pt.X, op2.Pt.X, op2b.Pt.X, Sval)) | |
| return false | |
| Left = Sval.Left | |
| Right = Sval.Right | |
| --DiscardLeftSide: when overlapping edges are joined, a spike will created | |
| --which needs to be cleaned up. However, we don't want Op1 or Op2 caught up | |
| --on the discard Side as either may still be needed for other joins ... | |
| Pt = Point! | |
| DiscardLeftSide = nil | |
| if (op1.Pt.X >= Left and op1.Pt.X <= Right) | |
| Pt.X = op1.Pt.X | |
| Pt.Y = op1.Pt.Y | |
| DiscardLeftSide = (op1.Pt.X > op1b.Pt.X) | |
| elseif (op2.Pt.X >= Left and op2.Pt.X <= Right) | |
| Pt.X = op2.Pt.X | |
| Pt.Y = op2.Pt.Y | |
| DiscardLeftSide = (op2.Pt.X > op2b.Pt.X) | |
| elseif (op1b.Pt.X >= Left and op1b.Pt.X <= Right) | |
| --Pt = op1b.Pt; | |
| Pt.X = op1b.Pt.X | |
| Pt.Y = op1b.Pt.Y | |
| DiscardLeftSide = op1b.Pt.X > op1.Pt.X | |
| else | |
| Pt.X = op2b.Pt.X | |
| Pt.Y = op2b.Pt.Y | |
| DiscardLeftSide = (op2b.Pt.X > op2.Pt.X) | |
| j.OutPt1 = op1 | |
| j.OutPt2 = op2 | |
| return @JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide) | |
| else | |
| --nb: For non-horizontal joins ... | |
| -- 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y | |
| -- 2. Jr.OutPt1.Pt > Jr.OffPt.Y | |
| --make sure the polygons are correctly oriented ... | |
| op1b = op1.Next | |
| while ((ClipperLib.Point.op_Equality(op1b.Pt, op1.Pt)) and (op1b != op1)) | |
| op1b = op1b.Next | |
| Reverse1 = op1b.Pt.Y > op1.Pt.Y or not ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt) | |
| if (Reverse1) | |
| op1b = op1.Prev | |
| while ((ClipperLib.Point.op_Equality(op1b.Pt, op1.Pt)) and (op1b != op1)) | |
| op1b = op1b.Prev | |
| if ((op1b.Pt.Y > op1.Pt.Y) or not ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt)) | |
| return false | |
| op2b = op2.Next | |
| while ((ClipperLib.Point.op_Equality(op2b.Pt, op2.Pt)) and (op2b != op2)) | |
| op2b = op2b.Next | |
| Reverse2 = op2b.Pt.Y > op2.Pt.Y or not ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt) | |
| if (Reverse2) | |
| op2b = op2.Prev | |
| while ((ClipperLib.Point.op_Equality(op2b.Pt, op2.Pt)) and (op2b != op2)) | |
| op2b = op2b.Prev | |
| if ((op2b.Pt.Y > op2.Pt.Y) or not ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt)) | |
| return false | |
| if ((op1b == op1) or (op2b == op2) or (op1b == op2b) or ((outRec1 == outRec2) and (Reverse1 == Reverse2))) | |
| return false | |
| if (Reverse1) | |
| op1b = @DupOutPt(op1, false) | |
| op2b = @DupOutPt(op2, true) | |
| op1.Prev = op2 | |
| op2.Next = op1 | |
| op1b.Next = op2b | |
| op2b.Prev = op1b | |
| j.OutPt1 = op1 | |
| j.OutPt2 = op1b | |
| return true | |
| else | |
| op1b = @DupOutPt(op1, true) | |
| op2b = @DupOutPt(op2, false) | |
| op1.Next = op2 | |
| op2.Prev = op1 | |
| op1b.Prev = op2b | |
| op2b.Next = op1b | |
| j.OutPt1 = op1 | |
| j.OutPt2 = op1b | |
| return true | |
| PointInPolygon: (pt, op) => | |
| --returns 0 if false, +1 if true, -1 if pt ON polygon boundary | |
| result = 0 | |
| startOp = op | |
| ptx = pt.X | |
| pty = pt.Y | |
| poly0x = op.Pt.X | |
| poly0y = op.Pt.Y | |
| while true do | |
| op = op.Next | |
| poly1x = op.Pt.X | |
| poly1y = op.Pt.Y | |
| if (poly1y == pty) | |
| if ((poly1x == ptx) or (poly0y == pty and ((poly1x > ptx) == (poly0x < ptx)))) | |
| return -1 | |
| if ((poly0y < pty) != (poly1y < pty)) | |
| if (poly0x >= ptx) | |
| if (poly1x > ptx) | |
| result = 1 - result | |
| else | |
| d = (poly0x - ptx) * (poly1y - pty) - (poly1x - ptx) * (poly0y - pty) | |
| if (d == 0) | |
| return -1 | |
| if ((d > 0) == (poly1y > poly0y)) | |
| result = 1 - result | |
| else | |
| if (poly1x > ptx) | |
| d = (poly0x - ptx) * (poly1y - pty) - (poly1x - ptx) * (poly0y - pty) | |
| if (d == 0) | |
| return -1 | |
| if ((d > 0) == (poly1y > poly0y)) | |
| result = 1 - result | |
| poly0x = poly1x | |
| poly0y = poly1y | |
| if (startOp == op) | |
| break | |
| return result | |
| Poly2ContainsPoly1: (outPt1, outPt2) => | |
| op = outPt1 | |
| while true do | |
| --nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon | |
| res = @PointInPolygon(op.Pt, outPt2) | |
| if (res >= 0) | |
| return res > 0 | |
| op = op.Next | |
| if (op == outPt1) | |
| break | |
| return true | |
| JoinCommonEdges: => | |
| for i = 1, #@m_Joins | |
| join = @m_Joins[i] | |
| outRec1 = @GetOutRec(join.OutPt1.Idx) | |
| outRec2 = @GetOutRec(join.OutPt2.Idx) | |
| if (outRec1.Pts == nil or outRec2.Pts == nil) | |
| continue | |
| if (outRec1.IsOpen or outRec2.IsOpen) | |
| continue | |
| --get the polygon fragment with the correct hole state (FirstLeft) | |
| --before calling JoinPoints() ... | |
| holeStateRec = nil | |
| if (outRec1 == outRec2) | |
| holeStateRec = outRec1 | |
| elseif (@OutRec1RightOfOutRec2(outRec1, outRec2)) | |
| holeStateRec = outRec2 | |
| elseif (@OutRec1RightOfOutRec2(outRec2, outRec1)) | |
| holeStateRec = outRec1 | |
| else | |
| holeStateRec = @GetLowermostRec(outRec1, outRec2) | |
| if (not @JoinPoints(join, outRec1, outRec2)) | |
| continue | |
| if (outRec1 == outRec2) | |
| --instead of joining two polygons, we've just created a new one by | |
| --splitting one polygon into two. | |
| outRec1.Pts = join.OutPt1 | |
| outRec1.BottomPt = nil | |
| outRec2 = @CreateOutRec! | |
| outRec2.Pts = join.OutPt2 | |
| --update all OutRec2.Pts Idx's ... | |
| @UpdateOutPtIdxs(outRec2) | |
| if (@Poly2ContainsPoly1(outRec2.Pts, outRec1.Pts)) | |
| --outRec1 contains outRec2 ... | |
| outRec2.IsHole = not outRec1.IsHole | |
| outRec2.FirstLeft = outRec1 | |
| if (BitXOR(outRec2.IsHole == true and 1 or 0, @ReverseSolution == true and 1 or 0)) == ((@AreaS1(outRec2) > 0) == true and 1 or 0) | |
| @ReversePolyPtLinks(outRec2.Pts) | |
| elseif (@Poly2ContainsPoly1(outRec1.Pts, outRec2.Pts)) | |
| --outRec2 contains outRec1 ... | |
| outRec2.IsHole = outRec1.IsHole | |
| outRec1.IsHole = not outRec2.IsHole | |
| outRec2.FirstLeft = outRec1.FirstLeft | |
| outRec1.FirstLeft = outRec2 | |
| if (BitXOR(outRec1.IsHole == true and 1 or 0, @ReverseSolution == true and 1 or 0)) == ((@AreaS1(outRec1) > 0) == true and 1 or 0) | |
| @ReversePolyPtLinks(outRec1.Pts) | |
| else | |
| --the 2 polygons are completely separate ... | |
| outRec2.IsHole = outRec1.IsHole | |
| outRec2.FirstLeft = outRec1.FirstLeft | |
| else | |
| --joined 2 polygons together ... | |
| outRec2.Pts = nil | |
| outRec2.BottomPt = nil | |
| outRec2.Idx = outRec1.Idx | |
| outRec1.IsHole = holeStateRec.IsHole | |
| if (holeStateRec == outRec2) | |
| outRec1.FirstLeft = outRec2.FirstLeft | |
| outRec2.FirstLeft = outRec1 | |
| UpdateOutPtIdxs: (outrec) => | |
| op = outrec.Pts | |
| while true do | |
| op.Idx = outrec.Idx | |
| op = op.Prev | |
| if (op == outrec.Pts) | |
| break | |
| Area: (op) => | |
| opFirst = op | |
| if (op == nil) | |
| return 0 | |
| a = 0 | |
| while true do | |
| a = a + (op.Prev.Pt.X + op.Pt.X) * (op.Prev.Pt.Y - op.Pt.Y) | |
| op = op.Next | |
| if(op == opFirst) -- and typeof op !== 'undefined') | |
| break | |
| return a * 0.5 | |
| AreaS1: (outRec) => | |
| return @Area(outRec.Pts) | |
| Aegihelp = {} | |
| Aegihelp.AegiToClipper = (clip) -> | |
| clip = Yutils.shape.flatten(clip) | |
| coord = {} | |
| part = {} | |
| for i in clip\gmatch("m ([^m]+)") | |
| table.insert(part, i) | |
| for i = 1, #part | |
| for x, y in part[i]\gmatch("([-%d.]+).([-%d.]+)") | |
| if coord[i] == nil then coord[i] = {} | |
| table.insert(coord[i], {X:tonumber(x), Y:tonumber(y)}) | |
| return coord | |
| Aegihelp.ClipperToAegi = (shapes) -> | |
| contours = {} | |
| for i, shape in ipairs shapes | |
| cmds = {} | |
| for j, point in ipairs shape | |
| fmt = switch j | |
| when 1 then "m #{point.X} #{point.Y}" | |
| when 2 then "l #{point.X} #{point.Y}" | |
| else "#{point.X} #{point.Y}" | |
| table.insert cmds, fmt | |
| table.insert contours, table.concat(cmds, ' ') | |
| return table.concat contours, ' ' | |
| pathsep = package.config\sub(1, 1) | |
| createGUI = -> | |
| dlg = { | |
| {x: 0, y: 0, width: 1, height: 1, class: "label", label: "Tolerance"}, | |
| {x: 1, y: 0, width: 1, height: 1, class: "intedit", name: "tolerance", value: 5, min: 0, max: 254}, | |
| {x: 0, y: 1, width: 1, height: 1, class: "label", label: "0 : Too Many Lines, Max Quality"}, | |
| {x: 0, y: 2, width: 1, height: 1, class: "label", label: "5 : Lesser Lines, Decent Quality"}, | |
| {x: 0, y: 3, width: 1, height: 1, class: "label", label: "10 : Mid Sweet Spot"}, | |
| {x: 0, y: 4, width: 1, height: 1, class: "label", label: ">20: Fewer Lines, Low Quality"}, | |
| {x: 0, y: 6, width: 1, height: 1, class: "label", label: "Warn if number of line exceeds"}, | |
| {x: 1, y: 6, width: 1, height: 1, class: "intedit", name: "linelimit", value: 700, min: 0}, | |
| } | |
| if pathsep != "\\" --Not Windows | |
| dlg[#dlg+1] = {x: 0, y: 8, width: 1, height: 1, class: "label", label: "Filepath"} | |
| dlg[#dlg+1] = {x: 0, y: 9, width: 2, height: 1, class: "edit", name: "filepath", value: ""} | |
| btn, res = aegisub.dialog.display dlg, {"OK", "Cancel"}, {"ok": "OK", "cancel": "Cancel"} | |
| aegisub.cancel! unless btn | |
| res | |
| is_file_readable = (file) -> | |
| handle = io.open(file, "r") | |
| if handle | |
| io.close() | |
| return true | |
| return false | |
| main = (sub, selected, act) -> | |
| res = createGUI! | |
| exts = "*.png;*.jpeg;*.jpe;*.jpg;*.jfif;*.jfi;*.bmp;*.dib" | |
| local filename | |
| if pathsep == "\\" -- Windows | |
| filename = aegisub.dialog.open "Open Image File", "", "", "Image extents (#{exts})|#{exts};", false, true | |
| else | |
| filename = res.filepath | |
| logger\assert is_file_readable(filename), "Could not open file for reading: #{filename}" | |
| dlg = zf.dialog sub, selected, active | |
| sel = selected[#selected] | |
| img = zf.potrace filename | |
| pixels = img\toAss nil | |
| linesOfSameColor = {} | |
| lineCount = #pixels | |
| logger\log "Make Image found #{lineCount} pixels in your image." | |
| for index, pixel in ipairs pixels | |
| aegisub.progress.task "Processing pixel %d out of %d..."\format index, lineCount | |
| aegisub.cancel! if aegisub.progress.is_cancelled! | |
| color = pixel\match "\\cH(%x%x%x%x%x%x)" | |
| position = pixel\match "\\pos%(([^%)]+)%)" | |
| posx, posy = position\match "([^,]+),([^,]+)" | |
| shape = "m #{posx} #{posy} l #{posx+1} #{posy} #{posx+1} #{posy+1} #{posx} #{posy+1}" | |
| linesOfSameColor[color] or= {} | |
| table.insert linesOfSameColor[color], shape | |
| lineCount = 0 | |
| for _, _ in pairs linesOfSameColor | |
| lineCount += 1 | |
| logger\log "But I reduced them to #{lineCount} chunks of same colors." | |
| img = nil | |
| pixels = nil | |
| pathsep = nil | |
| exts = nil | |
| filename = nil | |
| finalTable = {} | |
| if res.tolerance == 0 | |
| finalTable = linesOfSameColor | |
| else | |
| extractRGB = (color) -> | |
| b, g, r = color\match "(..)(..)(..)" | |
| tonumber(b, 16), tonumber(g, 16), tonumber(r, 16) | |
| logger\log "Now, I'll try to group them into chunks of similar colors to reduce the line count even more." | |
| count = 1 | |
| for color, lines in pairs linesOfSameColor | |
| aegisub.cancel! if aegisub.progress.is_cancelled! | |
| aegisub.progress.task "Merging line %d out of %d..."\format count, lineCount | |
| count += 1 | |
| b, g, r = extractRGB color | |
| minDiff = math.huge | |
| for col, _ in pairs finalTable | |
| b1, g1, r1 = extractRGB col | |
| bdiff, gdiff, rdiff = math.abs(b-b1), math.abs(g-g1), math.abs(r-r1) | |
| sumDiff = bdiff + gdiff + rdiff | |
| if bdiff < res.tolerance and gdiff < res.tolerance and rdiff < res.tolerance and minDiff > sumDiff | |
| minDiff = sumDiff | |
| color = col | |
| finalTable[color] or= {} | |
| finalTable[color] = list.join finalTable[color], lines | |
| lineCount = 0 | |
| for _,_ in pairs finalTable | |
| lineCount += 1 | |
| logger\log "I reduced it to #{lineCount} lines." | |
| if res.linelimit < lineCount | |
| btn = aegisub.dialog.display { | |
| {x: 0, y: 0, width: 1, height: 1, class: "label", label: "The number of lines to be inserted exceeds the limit you set."}, | |
| {x: 0, y: 1, width: 1, height: 1, class: "label", label: "Number of Lines: #{lineCount}"}, | |
| {x: 0, y: 2, width: 1, height: 1, class: "label", label: "Limit: #{res.linelimit}"}, | |
| {x: 0, y: 3, width: 1, height: 1, class: "label", label: "Do you want to proceed to insert those lines?"}, | |
| }, {"Yes", "No"} | |
| if btn == "No" | |
| logger\log "CANCELLED!" | |
| aegisub.cancel! | |
| logger\log "Now inserting those #{lineCount} lines." | |
| logger\log "This may take some time." | |
| linesOfSameColor = nil | |
| res = nil | |
| count = 1 | |
| ft = ClipperLib.PolyFillType.pftNonZero | |
| for color, shapeTable in pairs finalTable | |
| aegisub.cancel! if aegisub.progress.is_cancelled! | |
| aegisub.progress.task "Inserting line %d out of %d..."\format count, lineCount | |
| count += 1 | |
| cpr = Clipper! | |
| shape = table.concat shapeTable, " " | |
| cpr\AddPaths(Aegihelp.AegiToClipper(shape), ClipperLib.PolyType.ptSubject, true) | |
| cpr\Execute(ClipperLib.ClipType.ctUnion, ft, ft) | |
| line = sub[act] | |
| line.text = "{\\an7\\pos(0,0)\\fscx100\\fscy100\\bord0\\shad0\\frz0\\c&H#{color}&\\alpha&H00&\\p1}" .. Aegihelp.ClipperToAegi(cpr.FinalSolution) | |
| dlg\insertLine line, sel | |
| logger\log "FINISHED!" | |
| depctrl\registerMacro main |
Author
Author
So there are some caveats that comes with this script. In some sense, this script is better than traditional img2ass. In others, it's worse. And some of the limitation of img2ass still exists.
Let's talk about bad things first:
- Combining pixels of same color does reduce the number of shapes overall. However, it may increase the bounding box size and in extension, the bitmap size. So in worst case, the performace inefficieny remains similar.
- This is in no way a replacement of ai2ass or svg2ass. They do a lot of smart color quantization that this script does not.
- In some cases, this script still has the issue with gaps being visible. (Although it depends on the type of image as well.)
- In comparision to ai2ass and svg2ass, this script still makes a lot of shapes. Don't use this in big images or motion track it.
Now good stuff:
- The most obvious is that the number of shapes is very less when compared to the img2ass scripts.
- The result is more detailed than ai2ass or svg2ass in almost all the cases I've tested.
- If there are many pixels of same color, then the number of shapes/lines are actually less with better results.
So at the end, whether you want to use this or ai2ass/svg2ass really depends on the type of image you're tracing.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Requirements:
Recommended to install from Dependency Control.