Quadrilateral (an extension of rect)

Status
Not open for further replies.
Level 23
Joined
Feb 6, 2014
Messages
2,466
Have you ever wanted to create diagonal rects or irregularly shaped rects? Then this is the resource for you.

It functions like a rect, you can detect when a unit enters/leaves the quad. You can enumerate items/destructables/units inside the quad (with filter option) all in struct format.

JASS:
library Quadrilateral /* v0.1
            
            by Flux
    [url]http://www.hiveworkshop.com/forums/members/flux/[/url]

    This system allows you to create Quadrilaterals which is almost 
    the same as rects, except that quadrilaterals are not limited 
    to only having horizontal and vertical sides. This system provides
    the same extensive API of rect but in struct format.
    
    ALGORITHM:
    The system uses a quick ray casting algorithm to determine if
    the point is inside the quadrilateral. The system periodically
    monitors all Quads which have units within its bounding box.
    When no units is present within its bounding box, its monitoring
    is stop and will only resume its monitoring when a unit enters 
    its bounding box.
    
    
    LIMITATION:
        Event Responses has a maximum delay of TIMEOUT (0.03125 second)
        Sample:
        [url]http://www.hiveworkshop.com/forums/pastebin_data/scr8y9/delay.jpg[/url]
        
 
    optionally requires:
        - Table by Bribe
        
    Credits:
        Bribe - Table
    */

    //! novjass
    **************************  API:  *********************************
    
        function GetTriggerUnitInQuad takes nothing returns unit
            - Event Response, the unit that causes the trigger to run
    
    ----------------------------------
    struct Quad
    ----------------------------------
    
    //Quadrilateral Geometric Properties
    Vertices
        readonly real array x[0,1,2,3]  
        readonly real array y[0,1,2,3]
    Bounding Box
        readonly real xMin
        readonly real xMax
        readonly real yMin
        readonly real yMax
    Centroid
        readonly real centerX   <- not yet implemented
        readonly real centerY   <- not yet implemented
    Area
        readonly real area      <- not yet implemented
        
    
        
        static method create takes real ax, real ay, real bx, real by, real cx, real cy, real dx, real dy returns thistype
            - Create the Quadrilateral
        
        method triggerRegisterEnter takes trigger trig returns nothing
            - Register a trigger when a unit enters the Quad. 
              The Quad equivalent of TriggerRegisterEnterRectSimple.
              
        method triggerRegisterLeave takes trigger trig returns nothing
            - Register a trigger when a unit leaves the Quad. 
              The Quad equivalent of TriggerRegisterLeaveRectSimple.
        
        method enumUnits takes group whichGroup, boolexpr filter returns nothing
            - Pick all units inside the Quad and add them to whichGroup
              The Quad equivalent of GroupEnumUnitsInRect
            
        method enumDestructables takes boolexpr filter, code actionFunc returns nothing
            - Picks all the destructable inside the Quad.
              Quad equivalent of EnumDestructablesInRect.
            
        method enumItems takes boolexpr filter, code actionFunc returns nothing
            - Picks all the item inside the Quad. 
              The Quad equivalent to EnumItemsInRect.
        
        method isPointInside takes real x, real y returns boolean
            - Returns true if x and y is inside the Quad
        
        method isUnitInside takes real x, real y returns boolean
            - Returns true if x and y is inside the Quad
        
        method destroy takes nothing returns nothing
            - Destroy a Quad
    
    ********  Plan to add:  *******
        
        method move takes real x, real y returns nothing
            - Move the centroid of the Quad to a new position.
              Quad equivalent of MoveRectTo.
        
        suggest more here...
        
    //! endnovjass
 
    globals
        //Monitoring Timeout
        private constant real TIMEOUT = 0.03125000
        private constant real MARGIN = 30.0
    endglobals
    
    private keyword TrgList
    
    private struct TrgList
        
        readonly trigger trg
        readonly boolean enter  //trigger runs when a unit enters?
        readonly boolean leave  //trigger runs when a unit leaves?
        private Quad q
        
        thistype next
        thistype prev
        
        method destroy takes nothing returns nothing
            if .trg != null then
                call DestroyTrigger(.trg)
                set .trg = null
            endif
           //Remove from the list
            set .next.prev = .prev
            set .prev.next = .next
            set .enter = false
            set .leave = false
            call .deallocate()
        endmethod
        
        static method allocateHead takes nothing returns thistype
            local thistype this = .allocate()
            set .next = 0
            set .prev = 0
            return this
        endmethod
        
        static method addToList takes trigger t, Quad q, boolean enter returns thistype
            local thistype this = .allocate()
            if enter then
                set .enter = true
            else
                set .leave = true
            endif
            set .trg = t
            set .next = q.head.next
            set .prev = q.head
            return this
        endmethod
        
    endstruct
    
    
    globals
        private unit triggeringUnit
        private constant integer ENUM_UNIT = 1
        private constant integer ENUM_ITEM = 2
        private constant integer ENUM_DESTRUCTABLE = 3
    endglobals
    
    struct Quad
        
        //Quadrilateral Geometric Properties
        readonly real array x[4]   //x of the vertices
        readonly real array y[4]   //y of the vertices
        readonly real xMin
        readonly real xMax
        readonly real yMin
        readonly real yMax
        readonly real centerX
        readonly real centerY
        readonly real area
        
        
        private boolean monitored
        private region reg  //region for TriggerRegisterEnterRegion
        private rect r      //rect for RegionAddRect, when a unit enters this rect, start monitoring
        private rect rMargin  //rect for GroupEnumUnitsInRect since 'rect r' can't detect it all
        private trigger trg //trigger that will run when a unit enters the Quad's rect
        private integer count //number of units inside the quad
        private group g  //Units periodically monitored inside rect r
        readonly TrgList head  //group of triggers that will run when unit enters/leaves the Quad
        
        private static integer array next
        private static integer array prev
        private static timer t = CreateTimer()
        private static group tempGroup = CreateGroup()
        private static group triggeringGroup = CreateGroup()
        private static thistype globalThis
        private static integer enumType
        
        static if LIBRARY_Table then
            private static Table tb
        else
            private static hashtable hash = InitHashtable()
        endif
        
        method destroy takes nothing returns nothing
            local TrgList node
            local TrgList nextNode
            if .monitored then
                call .stopMonitor()
            endif
            call DestroyTrigger(.trg)
            set .trg = null
            //Destroy List of triggers that will execute
            set node = .head
            loop
                exitwhen node == 0
                set nextNode = node.next
                call node.destroy()
                set node = nextNode
            endloop
            call .head.destroy()
            call DestroyGroup(.g)
            set .g = null
            call RemoveRect(.r)
            call RemoveRect(.rMargin)
            call RemoveRegion(.reg)
            set .r = null
            set .rMargin = null
            set .reg = null
        endmethod
        
        private method minAndMax takes nothing returns nothing
            local integer i = 1
            set .xMin = .x[0]
            set .xMax = .x[0]
            set .yMin = .y[0]
            set .yMax = .y[0]
            loop
                if .x[i] < .xMin then
                    set .xMin = .x[i]
                elseif .x[i] > .xMax then
                    set .xMax = .x[i]
                endif
                if .y[i] < .yMin then
                    set .yMin = .y[i]
                elseif .y[i] > .yMax then
                    set .yMax = .y[i]
                endif
                exitwhen i == 3
                set i = i + 1
            endloop
        endmethod
        
        method isPointInside takes real x, real y returns boolean   
            local integer i
            local integer j
            local boolean b
            //Check if the point is within the bounding box first
            if x >= .xMin and x <= .xMax and y >= .yMin and y <= .yMax then
                //loop through all the edges
                set i = 0
                set j = 3   //number of vertices - 1
                set b = false
                loop
                    //if the y is within the y's of the edge and if the x is on the left side of the edge
                    if (y >= .y[i]) != (y >= .y[j]) and ( x < (.x[j] - .x[i])*(y - .y[i])/(.y[j] - .y[i]) + .x[i]) then
                        set b = not b
                    endif
                    exitwhen i == 3 //number of vertices - 1
                    set j = i
                    set i = i + 1
                endloop
                return b
            endif
            return false
        endmethod
        
        method isWidgetInside takes widget w returns boolean
            return isPointInside(GetWidgetX(w), GetWidgetY(w))
        endmethod
        
        //Core of the system, it checks if the number of units inside the
        //Quad changes, if it does, apply appropriate actions such as
        //executing triggers and setting the correct GetTriggerUnitInQuad
        private method check takes nothing returns nothing
            local integer rectCount = 0
            local integer quadCount = 0
            local unit u
            local integer diff
            local TrgList node
            call GroupEnumUnitsInRect(tempGroup, .rMargin, null)
            loop
                set u = FirstOfGroup(tempGroup)
                exitwhen u == null
                call GroupRemoveUnit(tempGroup, u)
                set rectCount = rectCount + 1
                if .isPointInside(GetUnitX(u), GetUnitY(u)) then
                    set quadCount = quadCount + 1
                    call GroupAddUnit(triggeringGroup, u)
                endif
            endloop
            //quadCount = newCount, .count = previousCount
            if quadCount > .count then
                set diff = quadCount - .count
                loop
                    set u = FirstOfGroup(triggeringGroup)
                    exitwhen diff == 0 or u == null
                    call GroupRemoveUnit(triggeringGroup, u)
                    set triggeringUnit = u
                    set node = .head
                    //Run all the triggers in the list
                    loop
                        exitwhen node == 0
                        if node.enter then
                            if TriggerEvaluate(node.trg) then
                                call TriggerExecute(node.trg)
                            endif
                        endif
                        set node = node.next    
                    endloop
                    set diff = diff - 1
                endloop
            elseif quadCount < .count then
                set diff = .count - quadCount
                loop
                    set u = FirstOfGroup(triggeringGroup)
                    exitwhen diff == 0 or u == null
                    call GroupRemoveUnit(triggeringGroup, u)
                    set triggeringUnit = u
                    set node = .head
                    //Run all the triggers in the list
                    loop
                        exitwhen node == 0
                        if node.leave then
                            if TriggerEvaluate(node.trg) then
                                call TriggerExecute(node.trg)
                            endif
                        endif
                        set node = node.next    
                    endloop
                    set diff = diff - 1
                endloop
            endif
            set .count = quadCount
            if rectCount == 0 then
                call .stopMonitor()
            endif
            set u = null
        endmethod

        //pick all Quads being monitored
        private static method pickAll takes nothing returns nothing
            local thistype this = next[0]
            loop
                exitwhen this == 0
                call .check()
                set this = next[this]
            endloop
        endmethod
        
        private method stopMonitor takes nothing returns nothing
            set .monitored = false
            //Remove from the Monitor List
            set next[prev[this]] = next[this]
            set prev[next[this]] = prev[this]
            if next[0] == 0 then
                call PauseTimer(t)
            endif
        endmethod
        
        private method startMonitor takes nothing returns nothing
            set .monitored = true
            //Insert in the monitor list
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[this]] = this
            set prev[0] = this
            if prev[this] == 0 then
                call TimerStart(t, TIMEOUT, true, function thistype.pickAll)
            endif
        endmethod
        
        private static method onRectEnter takes nothing returns boolean
            static if LIBRARY_Table then
                local thistype this = tb[GetHandleId(GetTriggeringTrigger())]
            else
                local thistype this = LoadInteger(hash, GetHandleId(GetTriggeringTrigger()), 0)
            endif
            //If the Quad is currently not being monitored, monitor it
            if not .monitored then
                call .startMonitor()
            endif
            return false
        endmethod
        
        method triggerRegisterEnter takes trigger trig returns nothing
            local TrgList tl = TrgList.addToList(trig, this, true)
            set .head.next.prev = tl
            set .head.next = tl
            if not .monitored then
                call .startMonitor()
            endif
        endmethod
        
        method triggerRegisterLeave takes trigger trig returns nothing
            local TrgList tl = TrgList.addToList(trig, this, false)
            set .head.next.prev = tl
            set .head.next = tl
            if not .monitored then
                call .startMonitor()
            endif
        endmethod
        
        method move takes real x, real y returns nothing
            
        endmethod
        
        
        private static method alwaysTrue takes nothing returns boolean
            return true
        endmethod
        
        private static method enumFilter takes nothing returns boolean
            local thistype this = globalThis
            if enumType == ENUM_ITEM then
                return .isPointInside(GetItemX(GetFilterItem()), GetItemY(GetFilterItem()))
            elseif enumType == ENUM_DESTRUCTABLE then
                return .isPointInside(GetDestructableX(GetFilterDestructable()), GetDestructableY(GetFilterDestructable()))
            else
                return .isPointInside(GetUnitX(GetFilterUnit()), GetUnitY(GetFilterUnit()))
            endif
        endmethod
        
        method enumItems takes boolexpr filter, code actionFunc returns nothing
            local boolexpr tempFilter = filter
            if filter == null then
                set tempFilter = Filter(function thistype.alwaysTrue)
            endif
            set globalThis = this
            set enumType = ENUM_ITEM
            call EnumItemsInRect(.rMargin, And(tempFilter, Filter(function thistype.enumFilter)), actionFunc)
        endmethod
        
        method enumDestructables takes boolexpr filter, code actionFunc returns nothing
            local boolexpr tempFilter = filter
            if filter == null then
                set tempFilter = Filter(function thistype.alwaysTrue)
            endif
            set globalThis = this
            set enumType = ENUM_DESTRUCTABLE
            call EnumDestructablesInRect(.rMargin, And(tempFilter, Filter(function thistype.enumFilter)), actionFunc)
        endmethod
        
        method enumUnits takes group whichGroup, boolexpr filter returns nothing
            local boolexpr tempFilter = filter
            if filter == null then
                set tempFilter = Filter(function thistype.alwaysTrue)
            endif
            set globalThis = this
            set enumType = ENUM_UNIT
            call GroupEnumUnitsInRect(whichGroup, .rMargin, And(tempFilter, Filter(function thistype.enumFilter)))
        endmethod
        
        static method create takes real ax, real ay, real bx, real by, real cx, real cy, real dx, real dy returns thistype
            local thistype this = allocate()
            set .x[0] = ax
            set .x[1] = bx
            set .x[2] = cx
            set .x[3] = dx
            set .y[0] = ay
            set .y[1] = by
            set .y[2] = cy
            set .y[3] = dy
            //Calculate the Min and Max of x and y
            call .minAndMax()
            call .head.destroy()
            set .r = Rect(.xMin, .yMin, .xMax, .yMax)
            set .rMargin = Rect(.xMin - MARGIN, .yMin - MARGIN, .xMax + MARGIN, .yMax + MARGIN)
            set .head = TrgList.allocateHead()
            set .trg = CreateTrigger()
            call TriggerAddCondition(.trg, Condition(function thistype.onRectEnter))
            static if LIBRARY_Table then
                set tb[GetHandleId(.trg)] = this
            else
                call SaveInteger(hash, GetHandleId(.trg), 0, this)
            endif
            set .reg = CreateRegion()
            call RegionAddRect(.reg, .r)
            call TriggerRegisterEnterRegion(.trg, reg, null)
            return this
        endmethod
        
        private static method onInit takes nothing returns nothing
            static if LIBRARY_Table then
                set tb = Table.create()
            endif
        endmethod
    
    endstruct
    
    //-------------- WRAPPER FUNCTIONS ------------------
        //JASSHelper says this won't be a problem
    function GetTriggerUnitInQuad takes nothing returns unit
        return triggeringUnit
    endfunction
    
endlibrary

Big thanks to Aniki for helping with the algorithm in determining if a point is inside the quadrilateral.
 

Attachments

  • Quadrilateral v0.10.w3x
    30.9 KB · Views: 77
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
Maybe try another implementation of the IsPointInRectangle function?


JASS:
    private function IsPointInRectangle takes real ax, real ay, real bx, real by, real cx, real cy, real dx, real dy, real px, real py returns boolean
        /*
        local real cross0 = (py-ay)*(bx-ax)-(px-ax)*(by-ay)
        local real cross1 = (py-cy)*(ax-cx)-(px-cx)*(ay-cy)
        local real cross4 = (py-dy)*(ax-dx)-(px-dx)*(ay-dy)
        return ((cross0*cross1 >= 0) and (((py-by)*(cx-bx)-(px-bx)*(cy-by))*cross1 >= 0)) or ((cross0*cross4 >= 0) and (((py-by)*(dx-bx)-(px-bx)*(dy-by))*cross4 >= 0))
        */

        // Test whether the point lies on the right side of each rectangle edge.
        // Source: [url]https://www.gamedev.net/topic/142526-checking-if-a-point-is-inside-a-rotated-rectangle/?forum_id=20[/url]

        local real ex = bx - ax
        local real ey = by - ay
        local real fx = dx - ax
        local real fy = dy - ay

        if ((px - ax) * ex + (py - ay) * ey < 0.0) then
            return false
        endif

        if ((px - bx) * ex + (py - by) * ey > 0.0) then
            return false
        endif

        if ((px - ax) * fx + (py - ay) * fy < 0.0) then
            return false
        endif

        if ((px - dx) * fx + (py - dy) * fy > 0.0) then
            return false
        endif

        return true
    endfunction
 
Level 13
Joined
Nov 7, 2014
Messages
571
Flux, the function I posted earlier seems incorrect (it didn't detect the Mask of Death item in the example map you've uploaded).

I think this version should be correct (and hopefully fast enough):
JASS:
function IsPointInRectangle takes real ax, real ay, real bx, real by, real cx, real cy, real dx, real dy, real px, real py returns boolean
        local real x_axis_x = bx - ax
        local real x_axis_y = by - ay

        local real y_axis_x = dx - ax
        local real y_axis_y = dy - ay
        
        local real edge

        // edge 0, counting in a counter clockwise direction
        set edge = (px - ax) * (-y_axis_x) + (py - ay) * (-y_axis_y)
        if edge > 0 then
            return false
        endif
        
        // edge 1
        set edge = (px - bx) * (x_axis_x) + (py - by) * (x_axis_y)
        if edge > 0 then
            return false
        endif
        
        // edge 2
        set edge = (px - cx) * (y_axis_x) + (py - cy) * (y_axis_y)
        if edge > 0 then
            return false
        endif
        
        // edge 3
        set edge = (px - dx) * (-x_axis_x) + (py - dy) * (-x_axis_y)
        if edge > 0 then 
            return false
        endif

        // all edges must be < 0

        return true
endfunction

The math behind it is explained in this HMH episode.
 
Level 13
Joined
Nov 7, 2014
Messages
571
blah... quadrilateral != rectangle =).

JASS:
   function IsPointInQuadrilateral takes real ax, real ay, real bx, real by, real cx, real cy, real dx, real dy, real px, real py returns boolean
        local real nx
        local real ny
        local real edge
        
        set nx = bx - ax
        set ny = by - ay
        set edge = (px - ax) * (ny) + (py - ay) * (-nx)
        if edge > 0 then
            return false
        endif
        
        set nx = cx - bx
        set ny = cy - by
        set edge = (px - bx) * (ny) + (py - by) * (-nx)
        if edge > 0 then
            return false
        endif
        
        set nx = dx - cx
        set ny = dy - cy
        set edge = (px - cx) * (ny) + (py - cy) * (-nx)
        if edge > 0 then
            return false
        endif        
       
        set nx = ax - dx
        set ny = ay - dy
        set edge = (px - dx) * (ny) + (py - dy) * (-nx)
        if edge > 0 then
            return false
        endif              
        
        return true
    endfunction
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
JASS:
method move takes real x, real y returns nothing
            - Move the centroid of the Quad to a new position.
              Quad equivalent of MoveRectTo.
Shouldnt that be "setPosition"?
And make another function that moves the quad in an xOffset and yOffset... called "move" ;)

Also maybe "scale" and "rotate".
 
Level 23
Joined
Feb 6, 2014
Messages
2,466
JASS:
method move takes real x, real y returns nothing
            - Move the centroid of the Quad to a new position.
              Quad equivalent of MoveRectTo.
Shouldnt that be "setPosition"?
And make another function that moves the quad in an xOffset and yOffset... called "move" ;)

Also maybe "scale" and "rotate".

Right, thanks for the suggestions.
 
Status
Not open for further replies.
Top