PreviousUpNext

15.4.1351  src/lib/x-kit/draw/region.pkg

## region.pkg

# Compiled by:
#     src/lib/x-kit/draw/xkit-draw.sublib

# Code for maintaining regions.
#
# The interface and algorithms are roughly based on those
# found in the sample X library.
#
# Regions correspond to sets of points. 
# Regions are implemented as YX banded lists of rectangles.
# Specifically, a region is a list of bands listed by increasing y 
# coordinates. A band is a list of rectangles listed by increasing x
# coordinates. Within a band, the rectangles are non-contiguous
# and all have the same upper and lower y coordinate. In addition,
# the vertical intervals determined by two bands are disjoint.
# (Note that if a band has upper and lower limits y1 and y2, this
# corresponds to the half-open interval [y1, y2).)
#
# Thus, in a region, the rectangles lie in non-overlapping
# bands. Within a band, the rectangles are as wide as possible.
# Some effort is also taken to coalesce compatible bands, i.e.,
# those that have the same x intervals and whose y intervals abut.


stipulate
    package g2d =  geometry2d;                  # geometry2d    is from   src/lib/std/2d/geometry2d.pkg
herein

    package   region
    : (weak)  Region                            # Region        is from   src/lib/x-kit/draw/region.api
    {
        min = int::min;
        max = int::max;

        fun impossible msg
            =
            raise exception lib_base::IMPOSSIBLE ("region" + msg);

        include package   box2;                 # box2          is from   src/lib/x-kit/draw/box2.pkg
        include package   band;                 # band          is from   src/lib/x-kit/draw/band.pkg
        include package   scan_convert;         # scan_convrt   is from   src/lib/x-kit/draw/scan-convert.pkg

        Region
            =
            REGION
              {
                num_boxes:  Int,
                bands:      List( Band ),
                extents:    Box
              };

        empty
            =
            REGION
              {
                num_boxes => 0,
                extents   => zero_box,
                bands     => []
              };


        fun boxes_of (REGION { bands, ... } )
            =
            fold_backward boxes_of_band [] bands;


        # Calculate the bounding box of a region:
        # 
        fun set_extents (REGION { num_boxes, bands => [], ... } )
                => 
                REGION { num_boxes => 0, bands => [], extents => zero_box };

            set_extents (REGION { num_boxes,
                           bands => bands as ((b as BAND { y1, y2, ... } ) ! rs), ... } )
                =>
                {   my (x1, x2)
                        =
                        band_extent b;

                    fun bnds ([b as BAND { y2, ... } ], l, r)
                            =>
                            {   my (x1, x2) =   band_extent b;

                                BOX { x1=>min (x1, l), y1, x2=>min (x2, r), y2 };
                            };

                        bnds (b ! rs, l, r)
                            => 
                            {   my (x1, x2) =   band_extent b;

                               bnds (rs, min (x1, l), max (x2, r));
                            };

                       bnds _ => impossible "set_extents";
                    end;

                    case rs    

                        [] => REGION { num_boxes, bands,
                                       extents=> BOX { x1, y1, x2, y2 }
                                     };

                        _  => REGION { num_boxes, bands,
                                       extents=> bnds (rs, x1, x2)
                                     };
                    esac;
                };
        end;


        fun clip_box (REGION { extents => BOX { x1, y1, x2, y2 }, ... } )
            =
            { col => x1, row => y2, wide => x2 - x1, high => y2 - y1 };


        fun poly_region arg
            =
            {   include package   geometry2d;           # geometry2d    is from   src/lib/std/2d/geometry2d.pkg

                fun coalesce (b' as BAND { y2, ... }, n', b as BAND { y1, ... } )
                    =
                    if (y1 == y2 and n' == size_of b) 
                         band::coalesce { upper=>b', lower=>b };
                    else
                         NULL;
                    fi;

                fun skip (ps as ({ col=>x, row=>y } ! { col=>x', ... } ! pts))
                        => 
                        if (x == x') skip pts;
                        else         ps;
                        fi;

                    skip ps => ps;
                end;

                # Assume at least two points and the first two satisfy x1 < x2.
                # This guarantees the band in non-empty.
                #
                fun get_band ({ col=>x2, row=>y2 } ! { col=>x1, row=>y1 } ! ps)
                        =>
                        loop (ps, x1,[(x1, x2)], 1)
                        where 
                            fun loop ([], x1, xs, n)
                                    =>
                                    ([], n, x1, x2, BAND { y1, y2=>y2+1, xs } );

                                loop (ps as ({ col=>x, row=>y } ! { col=>x', row=>y' } ! pts), x1, xs, n)
                                    =>
                                    if (y' == y1)
                                        #
                                        if (x == x')
                                            #
                                            loop (pts, x1, xs, n); 
                                        else
                                            case xs
                                                 (l, r) ! xs'
                                                     =>
                                                     if (x >= l)  loop (pts, x', (x', r) ! xs', n);
                                                     else         loop (pts, x', (x', x) ! xs, n+1);
                                                     fi;
                                                _ => impossible "polygonRegion::getBand::loop";
                                           esac;
                                        fi;
                                    else
                                        (skip ps, n, x1, x2, BAND { y1, y2=>y2+1, xs } );
                                    fi;

                               loop _
                                    =>
                                    impossible "polygonRegion: odd number of points";
                            end;
                        end;

                   get_band _
                        =>
                        impossible "polygonRegion::getBand";
                end;


                fun poly ([], n, x1, x2, bands)
                        =>
                        (n, x1, x2, bands);

                    poly (pts, _, _, _,[])
                        =>
                        {   my (pts', n, x1, x2, b)
                                =
                                get_band pts;

                            poly (pts', n, x1, x2,[b]);
                        };

                    poly (pts, n, x1, x2, bs as b ! rb)
                        =>
                        {   my (pts', dn, x1', x2', b')
                                =
                                get_band pts;

                            my (bs', n')
                                =
                                case (coalesce (b', dn, b))
                                    #
                                    THE b'' => (b'' ! rb, n);
                                    NULL    => (b' ! bs, n+dn); 
                                esac;

                            poly (pts', n', min (x1, x1'), max (x2, x2'), bs');
                        };
                end;

                my (num_boxes, x1, x2, bands)
                    =
                    poly (skip (scan_convert arg), 0, 0, 0,[]);

                if (num_boxes == 0)
                     empty;
                else
                     REGION
                       {
                         num_boxes,
                         bands,
                         extents => box2::BOX { x1, y1=> y1of (head       bands),
                                                x2, y2=> y2of (list::last bands)
                                              }
                       };
                fi;
            };                                  # fun poly_region

        # Create a rectangular region
        # given two opposing corners. 
        #
        fun box_r (ax, cx, ay, cy)
            =
            {   x1 = min (ax, cx);
                y1 = min (ay, cy);
                x2 = max (ax, cx);
                y2 = max (ay, cy);

                if (x1 == x2  or
                    y1 == y2
                )
                    empty;
                else
                    REGION {
                        num_boxes=>1,
                        extents =>   BOX { x1, y1, x2, y2 },
                        bands   => [ BAND { y1, y2, xs => [(x1, x2)] } ]
                    };
                fi;
            };

        # If the points correspond to a rectangle,
        # create the rectangular region.
        # Else return NULL.
        #
        fun box_region ( { col=>ax, row=>ay }, { col=>bx, row=>by },
                         { col=>cx, row=>cy }, { col=>dx, row=>dy }
                       )
            = 
            if   ((ay == by and bx == cx and cy == dy and dx == ax)  or
                  (ax == bx and by == cy and cx == dx and dy == ay)
                 ) 
                 THE (box_r (ax, cx, ay, cy));
            else NULL;
            fi;

        # Create a region from a rectangle. 
        # If the rectangle is degenerate, returns empty.
        # Canonicalizes the rectangle, so this works even
        # for negative width and height.
        #
        fun box ({ col=>x, row=>y, wide, high } )
            = 
            box_r (x, x+wide, y, y+high);

        # Create a region given a list of points describing a
        # polygon and a fill rule. Try to catch the simple case
        # of rectangles.
        #
        fun polygon (arg as ([_, _, _], _))
                =>
                poly_region arg;

           polygon (arg as ([a, b, c, d], _))
                =>
                case (box_region (a, b, c, d))
                    #
                    NULL  => poly_region arg;
                    THE r => r;
                esac;

           polygon (arg as ([a, b, c, d, e], _))
                =>
                if (a == e) 
                    #                
                    case (box_region (a, b, c, d))
                        #
                        NULL  => poly_region arg;
                        THE r => r;
                    esac;
                else
                    poly_region arg;
                fi;

           polygon (arg as ((_ ! _ ! _ ! _), _))
                =>
                poly_region arg;

           polygon _
                =>
                empty;
        end;


        fun offsetf (REGION { bands, extents, num_boxes }, offsetbox, offsetband)
            =
            REGION
              { num_boxes,
                extents   => offsetbox extents, 
                bands     => map offsetband bands
              };

        fun offset   (r, p) =   offsetf (r,   offset_box p,   offset_band p);
        fun x_offset (r, x) =   offsetf (r, x_offset_box x, x_offset_band x);
        fun y_offset (r, y) =   offsetf (r, y_offset_box y, y_offset_band y);

        #
        # region_op --
        #      Apply an operation to two regions.
        #      The function recurses down the two lists of bands.
        #      If the y intervals of two bands intersect in (y1, y2), a new band is
        #      created with bounds (y1, y2) and horizontal intervals determined
        #      by ofn applied to the two bands. For those y intervals in which
        #      one band is disjoint from all others, a new band is created by
        #      clipping the band to the y interval and applying nofn1 or nofn2. 
        #      The function attempts to coalesce each band with the previous.

        fun region_op (REGION { bands=>b1, extents=>e1, ... }, 
                     REGION { bands=>b2, extents=>e2, ... }, ofn,  nofn1, nofn2)
            =
            loop (b1, b2, min (miny e1, miny e2),[], 0)
            where
                fun coalesce (b' as BAND { y1, ... }, n', bs as ((b as BAND { y2, ... } ) ! rest), n)
                        =>
                        if (y1 == y2  and  n' == size_of b)
                            #
                            case (band::coalesce { lower=>b', upper=>b })

                                 NULL => (b' ! bs, n+n');
                                THE b'' => (b'' ! rest, n);
                            esac;
                        else
                            (b' ! bs, n+n');
                        fi;

                   coalesce (b', n',[], _)
                        =>
                        ([b'], n');
                end;

                fun wrapup (bs, n)
                    =
                    REGION { bands     =>  reverse bs,
                             extents   =>  zero_box,
                             num_boxes =>  n
                           };


                fun tail (b as BAND { y1, y2, ... }, rest, f, ybot, bs, n)
                    =
                    {   fun loop ([], a)
                                =>
                                wrapup a;

                            loop ((b as BAND { y1, y2, ... } ) ! rest, (bs, n))
                                =>
                                case (f (b, max (y1, ybot), y2))

                                       (_, 0) => loop (rest, (bs, n));

                                      (b', n') => loop (rest, (b' ! bs, n'+n));
                                esac;
                        end;

                        case (f (b, max (y1, ybot), y2))
                            #
                            (_,  0 ) => loop (rest, (bs, n));
                            (b', n') => loop (rest, coalesce (b', n', bs, n));
                        esac;
                    };


                fun inter (b, b', top, bot, bs, n)
                    =
                    case (ofn (b, b', top, bot))

                        (_, 0)   => (bs, n);
                        (b', n') => coalesce (b', n', bs, n);
                    esac;


                fun noninter (_, _, NULL, _, bs, n)
                        =>
                        (bs, n);

                   noninter (top, bot, THE f, b, bs, n)
                        =>
                        if (top == bot)
                            (bs, n);
                        else
                            case (f (b, top, bot))
                                #
                                (_, 0) => (bs, n);
                                (b', n') => coalesce (b', n', bs, n);
                            esac;
                        fi;
                end;

                fun loop ([],[], _, bs, n)
                        =>
                        wrapup (bs, n);

                    loop (b ! b1,[], ybot, bs, n)
                        => 
                        case nofn1
                            #
                            NULL  =>  wrapup (bs, n);
                            THE f =>  tail (b, b1, f, ybot, bs, n);
                        esac;

                    loop ([], b ! b2, ybot, bs, n)
                        =>
                        case nofn2
                            #
                            NULL  => wrapup (bs, n);
                            THE f => tail (b, b2, f, ybot, bs, n);
                        esac;

                    loop (bl as ((b as BAND { y1, y2, ... } ) ! next),
                          bl' as ((b' as BAND { y1=>y1', y2=>y2', ... } ) ! next'), ybot, bs, n)
                        =>
                        {   my (ytop, (bs', n'))
                                = 
                                if   (y1 < y1' ) (y1', noninter (max (y1,  ybot), min (y2,  y1'), nofn1, b, bs, n));
                                elif (y1' < y1 ) (y1,  noninter (max (y1', ybot), min (y2', y1), nofn2, b', bs, n));
                                else (y1, (bs, n));
                                fi;

                            ybot =   min (y2, y2');

                            my (bs'', n'')
                                =
                                if (ybot > ytop)   inter (b, b', ytop, ybot, bs', n');
                                else               (bs', n');
                                fi;

                            nb  =   if (y2 == ybot   ) next;   else bl;fi;
                            nb' =   if (y2' == ybot  ) next';  else bl';fi;

                            loop (nb, nb', ybot, bs'', n'');
                        };
                  end;
            end;                                        # fun region_op

        fun intersect (
                reg1 as REGION { num_boxes, extents, ... },
                reg2 as REGION { num_boxes=>num_boxes', extents=>extents', ... } )
            =
            if (num_boxes == 0 or num_boxes' == 0               #  Check for trivial cases 
                  or not (box2::overlap (extents, extents')))

                empty;
            else
                set_extents (region_op (reg1, reg2, band::intersect, NULL, NULL));
            fi;

        fun union (
                reg1 as REGION { num_boxes, extents, ... },
                reg2 as REGION { num_boxes=>num_boxes', extents=>extents', ... } )
            =
            if (num_boxes == 0 ) reg2;
            elif (num_boxes' == 0 ) reg1;
            elif (num_boxes == 1 and inside (extents', extents) ) reg1;
            elif (num_boxes' == 1 and inside (extents, extents') ) reg2;
            else
                my REGION { bands, num_boxes, ... }
                    = 
                    region_op (reg1, reg2, band::union, THE squeeze, THE squeeze);

                REGION { bands, num_boxes,
                    extents => bound_box (extents, extents') } ;
            fi;

        fun subtract (
                reg_m as REGION { num_boxes, extents, ... },
                reg_s as REGION { num_boxes=>num_boxes', extents=>extents', ... } )
            =
            if (num_boxes == 0 or num_boxes' == 0                #  Check for trivial reject 
                  or not (box2::overlap (extents, extents')))

                reg_m;
            else
                set_extents (region_op (reg_m, reg_s, band::subtract, THE squeeze, NULL));
            fi;


        # adjust:
        #   Return region r' where (x, y) is in r' iff:
        #     (x+m, y) in r for some m <= dx:    shiftfn=xOffset, opfn=union
        #     (x+m, y) in r for all m <= dx:     shiftfn=xOffset, opfn=intersection
        #     (x, y+m) in r for some m <= dx:    shiftfn=yOffset, opfn=union
        #     (x, y+m) in r for all m <= dx:     shiftfn=yOffset, opfn=intersection
        #
        # ** NOTE: this code should be checked XXX BUGGO CHECKME **
        #
        fun adjust (r, dx, shiftfn, opfn)
            =
            comp (dx, 1, r, r)
            where
                fun comp (0, _, _, r)
                        =>
                        r;

                   comp (arg as (dx, shift, s, r))
                        =>
                        if (unt::bitwise_and (unt::from_int dx, unt::from_int shift) != 0u0)

                            r' = opfn (shiftfn (r,-shift), s);
                            dx' = dx - shift;

                            if (dx' == 0)   r';
                            else            c (dx', shift, s, r');
                            fi;
                        else
                            c arg;
                        fi;
                end 

                also
                fun c (dx, shift, s, r)
                    =
                    comp (
                        dx,
                        unt::to_int_x (unt::(<<) (unt::from_int shift, 0u1)),
                        opfn (shiftfn (s,-shift), s),
                        r
                    );
            end;


        fun shrink (r, { col=>0, row=>0 } )
                =>
                r;

           shrink (r, p as { col=>dx, row=>dy } )
                =>
                {   xr =   if   (dx == 0)  r;
                           elif (dx <  0)  adjust (r, 2*(-dx), x_offset, union);
                           else            adjust (r, 2*dx, x_offset, intersect);
                           fi;

                    yr =   if   (dy == 0)   xr;
                           elif (dy < 0)    adjust (xr, 2*(-dy), y_offset, union);
                           else             adjust (xr, 2*dy, y_offset, intersect);
                           fi;

                    offset (yr, p);
                };
        end;


        fun xor (r1, r2)
            =
            union (subtract (r1, r2), subtract (r2, r1));


        fun is_empty (REGION { num_boxes, ... } )
            =
            num_boxes == 0;


        fun equal ( REGION { num_boxes, extents, bands, ... },
                    REGION { num_boxes=>num_boxes', extents=>extents', bands=>bands', ... }
                   )
            =
            (num_boxes == num_boxes') and
           ((num_boxes == 0) or (extents == extents' and bands == bands'));


        fun overlap ( REGION { num_boxes, extents, bands },
                      REGION { num_boxes=>num_boxes', extents=>extents', bands=>bands' }
                    )
            =
            {   fun overl ([], _) => FALSE;
                    overl (_,[]) => FALSE;

                    overl (bl as ((b as BAND { y1, y2, xs } ) ! bs),
                          bl' as ((b' as BAND { y1=>y1', y2=>y2', xs=>xs' } ) ! bs'))
                      =>
                      if   (y2 <= y1' )             overl (bs, bl');
                      elif (y2' <= y1 )             overl (bl, bs');
                      elif (band::overlap (b, b') ) TRUE;
                      elif (y2 < y2' )              overl (bs, bl');
                      elif (y2' < y2 )              overl (bl, bs');
                      else                          overl (bs, bs');
                      fi;
                end;

                num_boxes != 0 and num_boxes' != 0 and 
                box2::overlap (extents, extents') and overl (bands, bands');
            };


        fun point_in (REGION { num_boxes=>0, ... }, _)
                =>
                FALSE;

            point_in (REGION { extents, bands, ... }, p)
                =>
                in_box (extents, p) and list::exists (\\ b => in_band (b, p); end ) bands;
        end;


        fun box_in (REGION { num_boxes => 0, ... }, _)
                =>
                BOX_OUT;

           box_in (REGION { num_boxes, extents, bands }, { col=>x, row=>y, wide, high } )
                =>
                {   my  b  as BOX { x2=>rx2, y2=>ry2, ... }
                        =
                        BOX { x1=>x, y1=>y, x2 => x+wide, y2 => y+high };


                    fun end_check (FALSE, _) =>   BOX_OUT;
                        end_check (_,    ry) =>   if (ry < ry2)  BOX_PART;
                                                  else           BOX_IN;
                                                  fi;
                    end;


                    fun check ([], ry, part_in, part_out)
                            =>
                            end_check (part_in, ry);

                        check ((b as BAND { y1, y2, ... } ) ! rest, ry, part_in, part_out)
                            =>
                            if   (y2 <= ry)

                                check (rest, ry, part_in, part_out);

                            elif (y1 >= ry2)

                                end_check (part_in, ry);

                            elif (y1 > ry)

                                if part_in
                                    #
                                    BOX_PART;
                                else
                                    case (box_in_band (b, x, rx2))
                                        #       
                                         BOX_OUT => check (rest, ry, FALSE, TRUE);
                                        _ => BOX_PART;
                                    esac;
                                fi;

                            else
                                case (box_in_band (b, x, rx2))

                                     BOX_PART => BOX_PART;

                                     BOX_OUT  => if part_in      BOX_PART;
                                                       else            check (rest, ry, FALSE, TRUE);
                                                       fi;

                                     BOX_IN   => if part_out     BOX_PART;
                                                       else            check (rest, y2, TRUE, FALSE);
                                                       fi;
                                esac;
                            fi;
                    end;

                    if (box2::overlap (extents, b))
                        #
                        check (bands, y, FALSE, FALSE);
                    else
                        BOX_OUT;
                    fi;
                };
        end;
    };
end;


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext