HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

IRect

09-05-2015, 11:57 AM#1
iNfraNe
Rects and regions are mostly used to create "Enter" or "Leave" events for units. The correct implementation of these events is, however, quite annoying. For this reason I wrote a simple script that to automate all the bothersome things one needs to do when working with rects.

For example, rects and regions don't share the same x&y coordinates (regions are on a 32 grid, rects are not) and the maxX and maxY of regions are rounded up to the nearest 32, which is very counter-intuitive. (for a good study on the subject, see http://www.wc3c.net/showthread.php?p=1134281).

This script addresses these issues and adds some useful functionality such as hysteresis between enter/leave events, counting occupants, a never-failing .containsUnit function and a very easy method to filter units by the Enter response function. It also comes with a debug method ".display()", which shows the boundaries of the rect.

The library requires Table

Collapse JASS:
library IRect initializer Init requires Table
   /*****************************************************************************************
    *
    *    rects are mostly used to do "on enter" and "on leave" events
    *    the IRect provides a straightforward way to use these events.
    *    
    *    besides having an easy syntax, it automatically fixes some bugs
    *    associated with rects/regions, such as the up-rounding of the 
    *    maxX / maxY, and regions being set to multiple of 32, while
    *    rects keep the original input leading to bugs when calling 
    *    RectContainsUnit. Another function is provided to check if a 
    *    unit is in the IRect.
    *    
    *    the last added benefit is the addition of hysteresis. A dynamic
    *    "outer" rect can be added, so that the leave event fires at a
    *    different position than the enter event. 
    *    
    *    syntax:
    *
    *******************************User variables/functions:*********************************
    *    IRect eventIRect
    *    
    *    this is the IRect from which an event was fired
    *****************************************************************************************
    *    unit eventUnit
    *    
    *    this is the entering/leaving/filter unit.
    *****************************************************************************************
    *    integer numberOfOccupants
    *    
    *    this is the number of units in the IRect
    *****************************************************************************************      
    *    method containsUnit(unit u) returns boolean
    *    
    *    returns if a unit is currently occupying the IRect
    *    it inlines to a simple native call
    *    
    ************************************Setup functions:*************************************
    *
    *    IRect.create(real minX, real minY, real maxX, real maxY) returns IRect
    *    
    *    this function creates an IRect. The IRect automatically creates 
    *    the rect, region and triggers required for the events
    *****************************************************************************************
    *    method setOnEnter(eventCondition) returns IRect
    *
    *    pass a function to this method that takes nothing and returns a boolean
    *    this function will be called when a unit enters the IRect
    *    if the function returns true, then the unit is added to the occupants
    *    if the function returns false, then the unit is ignored by the system
    *    As the entering/leaving unit, use IRect.eventUnit (a global variable)
    *    if you want to use the IRect variable itself, use the global
    *    IRect.eventIRect  
    *    
    *    this method also adds all units that are currently occupying the rect
    *    and calls their enter function.
    *****************************************************************************************
    *    method setOnLeave(eventCallBack) returns IRect
    *    
    *    pass a function to this method that takes and returns nothing
    *    this function will be called when a unit leaves the IRect
    *    for event unit/iRect, use the same as in setOnEnter
    *****************************************************************************************
    *    method setHysteresis(real left, real bottom, real right, real top) returns IRect
    *    
    *    sets the margins for hysteresis at the sides of the IRect.
    *    For example, for an IRect(0,0,64,64)
    *    with a left hysteresis of 32, an entering unit will fire an event
    *    coming from the left when it crosses x = 0. When exiting the
    *    IRect, a leave event is fired at -32.
    *****************************************************************************************
    *    method setHysteresisSimple(real margin) returns IRect
    *    
    *    sets the hysteresis margins to the same value for all
    *****************************************************************************************
    *    method display() returns IRect
    *    
    *    displays the rects boundaries (including hysteresis if set)
    *    useful for debugging, but nothing else
    *****************************************************************************************
    */
    
    globals
        HandleTable rTbl
        location l
        group g
    endglobals
    
    private function interface eventCallback takes nothing returns nothing
    private function interface eventCondition takes nothing returns boolean
    struct IRect
        readonly static unit eventUnit
        readonly static IRect eventIRect
        readonly integer numberOfOccupants = 0
        private rect mainRect
        private rect outerRect
        private region mainRegion
        private region outerRegion
        private group occupants
        private trigger enterTrig
        private trigger leaveTrig
        
        private eventCondition enterFunc = 0
        private eventCallback leaveFunc = 0
        method containsUnit takes unit u returns boolean
            return IsUnitInGroup(u, .occupants)
        endmethod
        // -------------------------------------------------------
        // setup functions (return the type to allow 1-line coding)
        // -------------------------------------------------------
        // set the function that is called upon entering, also adds all units that the function returns true for to the group
        method setOnEnter takes eventCallback enterFunction returns thistype
            set .enterFunc = enterFunction
            call GroupEnumUnitsInRect(g, .mainRect, null)
            loop
                set .eventUnit = FirstOfGroup(g)
                exitwhen .eventUnit == null
                if not IsUnitInGroup(.eventUnit, .occupants) then
                    if .enterFunc.evaluate() then
                        call GroupAddUnit(.occupants, .eventUnit)
                        set .numberOfOccupants = .numberOfOccupants + 1                    
                    endif
                endif
                call GroupRemoveUnit(g,.eventUnit)
            endloop
            return this
        endmethod
        // set the function that is called upon leaving
        method setOnLeave takes eventCallback leaveFunction returns thistype
            set .leaveFunc = leaveFunction
            return this
        endmethod
        // sets the hysteresis margins (different for all)
        method setHysteresis takes real left, real bottom, real right, real top returns thistype
            set left = RAbsBJ(left)
            set top = RAbsBJ(top)
            set right = RAbsBJ(right)
            set bottom = RAbsBJ(bottom)
            if .outerRect != null then
                call RegionClearRect(.outerRegion, .outerRect)
                call RemoveRect(.outerRect)
            else    
                call RegionClearRect(.outerRegion, .mainRect)
            endif
            set .outerRect = Rect(GetRectMinX(.mainRect)-left, GetRectMinY(.mainRect)-bottom, GetRectMaxX(.mainRect)+right, GetRectMaxY(.mainRect)+top)
            call RegionAddRect(.outerRegion, .outerRect)
            return this            
        endmethod
        // sets the hysteresis margins (same for all sides)
        method setHysteresisSimple takes real margin returns thistype
            set margin = RAbsBJ(margin)
            if .outerRect != null then
                call RegionClearRect(.outerRegion, .outerRect)
                call RemoveRect(.outerRect)
            else    
                call RegionClearRect(.outerRegion, .mainRect)
            endif
            set .outerRect = Rect(GetRectMinX(.mainRect)-margin, GetRectMinY(.mainRect)-margin, GetRectMaxX(.mainRect)+margin, GetRectMaxY(.mainRect)+margin)
            call RegionAddRect(.outerRegion, .outerRect)
            return this
        endmethod        
        // for debugging purposes
        method display takes nothing returns thistype
            local real x1 = GetRectMinX(.mainRect)
            local real x2 = GetRectMaxX(.mainRect)
            local real y1 = GetRectMinY(.mainRect)
            local real y2 = GetRectMaxY(.mainRect)            
            local real z1
            local real z2
            local real z3
            local real z4
            call MoveLocation(l, x1, y1)
            set z1 = GetLocationZ(l)
            call MoveLocation(l, x2, y1)
            set z2 = GetLocationZ(l)
            call MoveLocation(l, x2, y2)
            set z3 = GetLocationZ(l)            
            call MoveLocation(l, x1, y2)
            set z4 = GetLocationZ(l)                        
            call AddLightningEx("DRAL",true, x1, y1, z1, x2, y1, z2) // left, top to bottom
            call AddLightningEx("DRAL",true, x2, y1, z2, x2, y2, z3) // bottom, left to right
            call AddLightningEx("DRAL",true, x2, y2, z3, x1, y2, z4) // right, bottom to top
            call AddLightningEx("DRAL",true, x1, y2, z4, x1, y1, z1) // top, right to left
            if .outerRect != null then
                set x1 = GetRectMinX(.outerRect)
                set x2 = GetRectMaxX(.outerRect)
                set y1 = GetRectMinY(.outerRect)
                set y2 = GetRectMaxY(.outerRect) 
                call MoveLocation(l, x1, y1)
                set z1 = GetLocationZ(l)
                call MoveLocation(l, x2, y1)
                set z2 = GetLocationZ(l)
                call MoveLocation(l, x2, y2)
                set z3 = GetLocationZ(l)            
                call MoveLocation(l, x1, y2)
                set z4 = GetLocationZ(l)                        
                call AddLightningEx("DRAM",true, x1, y1, z1, x2, y1, z2) // left, top to bottom
                call AddLightningEx("DRAM",true, x2, y1, z2, x2, y2, z3) // bottom, left to right
                call AddLightningEx("DRAM",true, x2, y2, z3, x1, y2, z4) // right, bottom to top
                call AddLightningEx("DRAM",true, x1, y2, z4, x1, y1, z1) // top, right to left            
            endif
            return this
        endmethod
        // this method is called everytime a unit enters the rect
        private static method eventEnter takes nothing returns nothing
            local thistype this = rTbl[GetTriggeringTrigger()]
            set .eventIRect = this
            set .eventUnit = GetEnteringUnit()
            
            if not .containsUnit(.eventUnit) then //enter event, the unit enters for the first time
                // check if an enter event is configured, if so, evaluate it to see if the unit should be added to the occupants
                if .enterFunc == 0 or .enterFunc.evaluate() then
                    call GroupAddUnit(.occupants, .eventUnit)
                    set .numberOfOccupants = .numberOfOccupants + 1                
                endif
            endif            
        endmethod
        // this method is called everytime a unit leaves the rect
        private static method eventLeave takes nothing returns nothing
            local thistype this = rTbl[GetTriggeringTrigger()]
            set .eventIRect = this
            set .eventUnit = GetLeavingUnit()
            if .containsUnit(eventUnit) then //leave event should only fire when the unit is in the occupants group
                call GroupRemoveUnit(.occupants, .eventUnit)
                set .numberOfOccupants = .numberOfOccupants - 1
                // check if a leave event is configured, if so, fire it
                if .leaveFunc != 0 then
                    call .leaveFunc.execute()
                endif
            endif
        endmethod     
        // create method has identical parameters as Rect()
        static method create takes real minX, real minY, real maxX, real maxY returns thistype
            local thistype this = thistype.allocate()
            // setup the main rect
            // The rect must be put on a grid, because blizzard only allows multiples of 32 for region, but still stores a value that can be anything on the rect, leading to bugs.
            // Also, the maxX and maxY are bugged, they are rounded to +32. Therefore, a correction is in order. The Rect is 0.01 smaller than the region.
            set minX = I2R(R2I((minX)/32))*32
            set minY = I2R(R2I((minY)/32))*32
            set maxX = I2R(R2I((maxX)/32))*32-0.01
            set maxY = I2R(R2I((maxY)/32))*32-0.01
            set .mainRect = Rect(minX, minY, maxX, maxY) // correction for stupid blizzard bug
            set .outerRect = null
            set .mainRegion = CreateRegion()
            call RegionAddRect(.mainRegion, .mainRect)
            set .outerRegion = CreateRegion()
            call RegionAddRect(.outerRegion, .mainRect)
            // create the group of occupants
            set .occupants = CreateGroup()
            // setup the triggers
            set .enterTrig = CreateTrigger()
            set .leaveTrig = CreateTrigger()
            set rTbl[.enterTrig] = this
            set rTbl[.leaveTrig] = this
            call TriggerRegisterEnterRegion(.enterTrig, .mainRegion, null)
            call TriggerRegisterLeaveRegion(.leaveTrig, .outerRegion, null)
            call TriggerAddAction(.enterTrig, function thistype.eventEnter)
            call TriggerAddAction(.leaveTrig, function thistype.eventLeave)
            return this
        endmethod
    endstruct
    private function Init takes nothing returns nothing
        set rTbl = HandleTable.create()
        set l = Location(0,0)
        set g = CreateGroup()
    endfunction
endlibrary