Skip to content

Instantly share code, notes, and snippets.

@PhosCity
Last active February 15, 2023 08:02
Show Gist options
  • Select an option

  • Save PhosCity/9f76fea939937f91c0c09d70bcd0eb2e to your computer and use it in GitHub Desktop.

Select an option

Save PhosCity/9f76fea939937f91c0c09d70bcd0eb2e to your computer and use it in GitHub Desktop.
Improved img2ass
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
@PhosCity
Copy link
Author

PhosCity commented Feb 6, 2023

Requirements:

  • Zeref's modules.
  • Functional module
  • Yutils

Recommended to install from Dependency Control.

@PhosCity
Copy link
Author

PhosCity commented Feb 13, 2023

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