Module: duim-geometry-internals Synopsis: DUIM geometry Author: Scott McKay, Andy Armstrong Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Arithmetic on simple regions and region sets /// General region union define sealed class () sealed constant slot %regions :: , required-init-keyword: regions:; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define inline function make-region-union (#rest regions) => (region :: ) make(, regions: as(, regions)) end function make-region-union; define sealed method region-set-function (region :: ) => (function) union end method region-set-function; define sealed method region-set-regions (region :: , #key normalize?) => (regions :: ) ignore(normalize?); region.%regions end method region-set-regions; define sealed method transform-region (transform :: , region-set :: ) => (region :: ) let regions :: = make(); local method do-transform (region) => () add!(regions, transform-region(transform, region)) end method; do-regions(do-transform, region-set); make(, regions: regions) end method transform-region; define method region-union (region :: , nowhere :: ) => (region :: ) region end method region-union; define method region-union (nowhere :: , region :: ) => (region :: ) region end method region-union; define method region-union (everywhere :: , region :: ) => (region :: ) $everywhere end method region-union; define method region-union (region :: , everywhere :: ) => (region :: ) $everywhere end method region-union; // Take the region of maximum dimensionality define method region-union (point :: , path :: ) => (region :: ) path end method region-union; define method region-union (path :: , point :: ) => (region :: ) path end method region-union; define method region-union (point :: , area :: ) => (region :: ) area end method region-union; define method region-union (area :: , point :: ) => (region :: ) area end method region-union; define method region-union (path :: , area :: ) => (region :: ) area end method region-union; define method region-union (area :: , path :: ) => (region :: ) area end method region-union; define method region-union (point1 :: , point2 :: ) => (region :: ) if (region-equal(point1, point2)) point1 else make-region-union(point1, point2) end end method region-union; define method region-union (path1 :: , path2 :: ) => (region :: ) case region-contains-region?(path1, path2) => path1; region-contains-region?(path2, path1) => path2; otherwise => make-region-union(path1, path2) end end method region-union; define method region-union (area1 :: , area2 :: ) => (region :: ) case region-contains-region?(area1, area2) => area1; region-contains-region?(area2, area1) => area2; otherwise => make-region-union(area1, area2) end end method region-union; define method region-union (region1 :: , region2 :: ) => (region :: ) make-region-union(region1, region2) end method region-union; define method region-union (region :: , union :: ) => (region :: ) apply(make-region-union, region, union.%regions) end method region-union; define method region-union (union :: , region :: ) => (region :: ) apply(make-region-union, region, union.%regions) end method region-union; define method region-union (region1 :: , region2 :: ) => (region :: ) apply(make-region-union, concatenate(region1.%regions, region2.%regions)) end method region-union; /// General region intersection define sealed class () sealed constant slot %regions :: , required-init-keyword: regions:; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define inline function make-region-intersection (#rest regions) => (region :: ) make(, regions: as(, regions)) end function make-region-intersection; define sealed method region-set-function (region :: ) => (function) intersection end method region-set-function; define sealed method region-set-regions (region :: , #key normalize?) => (regions :: ) ignore(normalize?); region.%regions end method region-set-regions; define sealed method transform-region (transform :: , region-set :: ) => (region :: ) let regions :: = make(); local method do-transform (region) => () add!(regions, transform-region(transform, region)) end method; do-regions(do-transform, region-set); make(, regions: regions) end method transform-region; define method region-intersection (region :: , nowhere :: ) => (region :: ) $nowhere end method region-intersection; define method region-intersection (nowhere :: , region :: ) => (region :: ) $nowhere end method region-intersection; define method region-intersection (everywhere :: , region :: ) => (region :: ) region end method region-intersection; define method region-intersection (region :: , everywhere :: ) => (region :: ) region end method region-intersection; // Take the region of minumum dimensionality define method region-intersection (point :: , path :: ) => (region :: ) if (region-intersects-region?(point, path)) point else $nowhere end end method region-intersection; define method region-intersection (path :: , point :: ) => (region :: ) if (region-intersects-region?(point, path)) point else $nowhere end end method region-intersection; define method region-intersection (point :: , area :: ) => (region :: ) if (region-intersects-region?(point, area)) point else $nowhere end end method region-intersection; define method region-intersection (area :: , point :: ) => (region :: ) if (region-intersects-region?(point, area)) point else $nowhere end end method region-intersection; define method region-intersection (path :: , area :: ) => (region :: ) if (region-intersects-region?(path, area)) path else $nowhere end end method region-intersection; define method region-intersection (area :: , path :: ) => (region :: ) if (region-intersects-region?(path, area)) path else $nowhere end end method region-intersection; define method region-intersection (point1 :: , point2 :: ) => (region :: ) if (region-equal(point1, point2)) point1 else $nowhere end end method region-intersection; // This catches paths and areas, too define method region-intersection (region1 :: , region2 :: ) => (region :: ) if (region-intersects-region?(region1, region2)) make-region-intersection(region1, region2) else $nowhere end end method region-intersection; define method region-intersection (region :: , intersection :: ) => (region :: ) apply(make-region-intersection, region, intersection.%regions) end method region-intersection; define method region-intersection (intersection :: , region :: ) => (region :: ) apply(make-region-intersection, region, intersection.%regions) end method region-intersection; define method region-intersection (region1 :: , region2 :: ) => (region :: ) apply(make-region-intersection, concatenate(region1.%regions, region2.%regions)) end method region-intersection; /// General region difference define sealed class () sealed constant slot %region1 :: , required-init-keyword: region1:; sealed constant slot %region2 :: , required-init-keyword: region2:; sealed slot %regions :: false-or() = #f; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define inline function make-region-difference (region1, region2) => (region :: ) make(, region1: region1, region2: region2) end function make-region-difference; define sealed method region-set-function (region :: ) => (function) difference end method region-set-function; define sealed method region-set-regions (region :: , #key normalize?) => (regions :: ) ignore(normalize?); region.%regions | (region.%regions := vector(region.%region1, region.%region2)) end method region-set-regions; define sealed method do-regions (function :: , region :: , #key normalize?) => () ignore(normalize?); function(region.%region1); function(region.%region2) end method do-regions; define sealed method transform-region (transform :: , region-set :: ) => (region :: ) make-region-difference (transform-region(transform, region-set.%region1), transform-region(transform, region-set.%region2)) end method transform-region; define method region-difference (nowhere :: , region :: ) => (region :: ) $nowhere end method region-difference; define method region-difference (region :: , nowhere :: ) => (region :: ) region end method region-difference; define method region-difference (region :: , everywhere :: ) => (region :: ) $nowhere end method region-difference; // For the case where the first region has higher dimensionality, the // first region is the result. define method region-difference (path :: , point :: ) => (region :: ) path end method region-difference; define method region-difference (area :: , point :: ) => (region :: ) area end method region-difference; define method region-difference (area :: , path :: ) => (region :: ) area end method region-difference; define method region-difference (region1 :: , region2 :: ) => (region :: ) make-region-difference(region1, region2) end method region-difference; /// Simple box (LTRB) arithmetic /// These operate only on 's, so be careful! //--- Should we really allow zero-sized LTRBs? define inline function ltrb-well-formed? (left :: , top :: , right :: , bottom :: ) => (true? :: ) right >= left & bottom >= top end function ltrb-well-formed?; define inline function ltrb-equals-ltrb? (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (true? :: ) left1 = left2 & top1 = top2 & right1 = right2 & bottom1 = bottom2 end function ltrb-equals-ltrb?; define inline function ltrb-size-equal? (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (true? :: ) right1 - left1 = right2 - left2 & bottom1 - top1 = bottom2 - top2 end function ltrb-size-equal?; define inline function ltrb-contains-position? (left :: , top :: , right :: , bottom :: , x :: , y :: ) => (true? :: ) left <= x & top <= y & right >= x & bottom >= y end function ltrb-contains-position?; // Returns #t iff LTRB1 wholly contains LTRB2 define inline function ltrb-contains-ltrb? (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (true? :: ) left1 <= left2 & top1 <= top2 & right1 >= right2 & bottom1 >= bottom2 end function ltrb-contains-ltrb?; define function ltrb-intersects-ltrb? (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (valid? :: , left :: , top :: , right :: , bottom :: ) let left = max(left1, left2); let top = max(top1, top2); let right = min(right1, right2); let bottom = min(bottom1, bottom2); if (ltrb-well-formed?(left, top, right, bottom)) values(#t, left, top, right, bottom) else values(#f, 0, 0, 0, 0) end end function ltrb-intersects-ltrb?; // Returns a sequence of bounding boxes that represent the union define sealed method ltrb-union (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: , #key banding = #"x-banding") => (boxes :: ) case ltrb-contains-ltrb?(left1, top1, right1, bottom1, left2, top2, right2, bottom2) => vector(make-bounding-box(left1, top1, right1, bottom1)); ltrb-contains-ltrb?(left2, top2, right2, bottom2, left1, top1, right1, bottom1) => vector(make-bounding-box(left2, top2, right2, bottom2)); ~ltrb-intersects-ltrb?(left1, top1, right1, bottom1, left2, top2, right2, bottom2) => vector(make-bounding-box(left1, top1, right1, bottom1), make-bounding-box(left2, top2, right2, bottom2)); otherwise => select (banding) #"x-banding" => when (abs(left2) < abs(left1)) swap!(left1, left2); swap!(top1, top2); swap!(right1, right2); swap!(bottom1, bottom2) end; let result :: = make(); when (top1 < top2) add!(result, make-bounding-box(left1, top1, right1, top2)) end; when (bottom2 > bottom1) add!(result, make-bounding-box(left2, bottom2, right2, bottom1)) end; when (left1 < left2) let top = max(top1, top2); let bottom = min(bottom1, bottom2); when (bottom > top) add!(result, make-bounding-box(left1, top, right2, bottom)) end end; when (right1 > right2) let top = min(bottom1, bottom2); let bottom = max(top1, top2); when (bottom > top) add!(result, make-bounding-box(left2, top, right1, bottom)) end end; result; #"y-banding" => when (abs(top2) < abs(top1)) swap!(left1, left2); swap!(top1, top2); swap!(right1, right2); swap!(bottom1, bottom2) end; let result :: = make(); when (left1 < left2) add!(result, make-bounding-box(left1, top1, left2, bottom1)) end; when (right2 > right1) add!(result, make-bounding-box(right1, top2, right2, bottom2)) end; when (top1 < top2) let left = max(left1, left2); let right = min(right1, right2); when (right > left) add!(result, make-bounding-box(left, top1, right, bottom2)) end end; when (bottom1 > bottom2) let left = min(right1, right2); let right = max(left1, left2); when (right > left) add!(result, make-bounding-box(left, top2, right, bottom1)) end end; result; #f => vector(make-bounding-box(left1, top1, right1, bottom1), make-bounding-box(left2, top2, right2, bottom2)); end end end method ltrb-union; // Returns a single bounding box that represents the intersection, or #f. define sealed method ltrb-intersection (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (box :: false-or()) let (valid?, left, top, right, bottom) = ltrb-intersects-ltrb? (left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (valid?) make-bounding-box(left, top, right, bottom) end end method ltrb-intersection; // Returns a sequence of bounding boxes that represent the difference, or #f. // Diagrams of box differences: // // 111111111111111111 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 1cccccc222222222232222222222 // 1cccccc2 1 2 // 1cccccc2 1 2 // 1cccccc2 1 2 // 111111131111111111 2 // 2 2 // 2 2 // 222222222222222222222 // // // 111111111111111111 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 1aaaaaaaaaaaaaaaa1 // 2222322222222222222dd1 // 2 1 2dd1 // 2 1 2dd1 // 2 1 2dd1 // 2 1 2dd1 // 2 1 2dd1 // 2 1 2dd1 // 2222322222222222222dd1 // 1bbbbbbbbbbbbbbbb1 // 1bbbbbbbbbbbbbbbb1 // 111111111111111111 define sealed method ltrb-difference (left1 :: , top1 :: , right1 :: , bottom1 :: , left2 :: , top2 :: , right2 :: , bottom2 :: ) => (box :: false-or()) // If the second ltrb contains the first ltrb, the difference is #f unless (ltrb-contains-ltrb?(left2, top2, right2, bottom2, left1, top1, right1, bottom1)) if (~ltrb-intersects-ltrb?(left1, top1, right1, bottom1, left2, top2, right2, bottom2)) vector(make-bounding-box(left1, top1, right1, bottom1)) else let result :: = make(); when (top1 < top2) // Area A above add!(result, make-bounding-box(left1, top1, right1, top2)) end; when (bottom1 > bottom2) // Area B above add!(result, make-bounding-box(left1, bottom2, right1, bottom1)) end; when (left1 < left2) // Area C above let top = max(top1, top2); let bottom = min(bottom1, bottom2); when (bottom > top) add!(result, make-bounding-box(left1, top, left2, bottom)) end end; when (right1 > right2) // Area D above let top = max(top1, top2); let bottom = min(bottom1, bottom2); when (bottom > top) add!(result, make-bounding-box(right2, top, right1, bottom)) end end; if (empty?(result)) #f else result end end end end method ltrb-difference; /// Special cases for sets of boxes define sealed class () sealed constant slot %left :: , required-init-keyword: left:; sealed constant slot %top :: , required-init-keyword: top:; sealed constant slot %right :: , required-init-keyword: right:; sealed constant slot %bottom :: , required-init-keyword: bottom:; sealed slot box-set-boxes :: , required-init-keyword: boxes:; sealed slot %x-banded-boxes :: false-or() = #f; sealed slot %y-banded-boxes :: false-or() = #f; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define method make-box-set (#rest boxes) => (boxes :: ) let left :: = $largest-coordinate; let top :: = $largest-coordinate; let right :: = $smallest-coordinate; let bottom :: = $smallest-coordinate; for (box in boxes) let (rl, rt, rr, rb) = box-edges(box); min!(left, rl); min!(top, rt); max!(right, rr); max!(bottom, rb) end; make(, boxes: as(, boxes), left: left, top: top, right: right, bottom: bottom) end method make-box-set; define sealed inline method box-edges (box :: ) => (left :: , top :: , right :: , bottom :: ) values(box.%left, box.%top, box.%right, box.%bottom) end method box-edges; define sealed method transform-region (transform :: , set :: ) => (boxes :: ) local method do-transform (box) => () transform-region(transform, box) end method; apply(make-box-set, map-as(, do-transform, box-set-boxes(set))) end method transform-region; define sealed method region-set-function (region :: ) => (function) union end method region-set-function; define sealed method region-set-regions (region :: , #key normalize?) => (regions :: ) select (normalize?) #f => box-set-boxes(region); #"x-banding" => region-set-x-banded-boxes(region); #"y-banding" => region-set-y-banded-boxes(region) end end method region-set-regions; define sealed method region-set-x-banded-boxes (region :: ) => (regions :: ) region.%x-banded-boxes | begin let boxes = normalize-box-set(region, #"x-banding"); region.%x-banded-boxes := boxes; boxes end end method region-set-x-banded-boxes; define sealed method region-set-y-banded-boxes (region :: ) => (regions :: ) region.%y-banded-boxes | begin let boxes = normalize-box-set(region, #"y-banding"); region.%y-banded-boxes := boxes; boxes end end method region-set-y-banded-boxes; define sealed method region-union (box1 :: , box2 :: ) => (region :: ) let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); let new-boxes = ltrb-union(left1, top1, right1, bottom1, left2, top2, right2, bottom2); if (size(new-boxes) = 1) new-boxes[0] else apply(make-box-set, new-boxes) end end method region-union; define sealed method region-union (box :: , set :: ) => (region :: ) apply(make-box-set, box, box-set-boxes(set)) end method region-union; define sealed method region-union (set :: , box :: ) => (region :: ) apply(make-box-set, box, box-set-boxes(set)) end method region-union; define sealed method region-union (set1 :: , set2 :: ) => (region :: ) apply(make-box-set, concatenate(box-set-boxes(set1), box-set-boxes(set2))) end method region-union; define sealed method region-intersection (box1 :: , box2 :: ) => (region :: ) let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); let box = ltrb-intersection(left1, top1, right1, bottom1, left2, top2, right2, bottom2); box | $nowhere end method region-intersection; define sealed method region-intersection (box :: , set :: ) => (region :: ) let new-boxes :: = make(); let (left1, top1, right1, bottom1) = box-edges(box); local method do-intersection (b) => () let (left2, top2, right2, bottom2) = box-edges(b); let new = ltrb-intersection(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end method; do-regions(do-intersection, set); if (empty?(new-boxes)) $nowhere else apply(make-box-set, new-boxes) end end method region-intersection; define sealed method region-intersection (set :: , box :: ) => (region :: ) let new-boxes :: = make(); let (left2, top2, right2, bottom2) = box-edges(box); local method do-intersection (b) => () let (left1, top1, right1, bottom1) = box-edges(b); let new = ltrb-intersection(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end method; do-regions(do-intersection, set); if (empty?(new-boxes)) $nowhere else apply(make-box-set, new-boxes) end end method region-intersection; define sealed method region-intersection (set1 :: , set2 :: ) => (region :: ) let new-boxes :: = make(); do-regions (method (box1) do-regions (method (box2) let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); let new = ltrb-intersection(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end, set2) end, set1); if (empty?(new-boxes)) $nowhere else apply(make-box-set, new-boxes) end end method region-intersection; define sealed method region-difference (box1 :: , box2 :: ) => (region :: ) let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); let new-boxes = ltrb-difference(left1, top1, right1, bottom1, left2, top2, right2, bottom2); if (empty?(new-boxes)) $nowhere else if (size(new-boxes) = 1) new-boxes[0] else apply(make-box-set, new-boxes) end end end method region-difference; define sealed method region-difference (box :: , set :: ) => (region :: ) let new-boxes :: = make(); let (left1, top1, right1, bottom1) = box-edges(box); local method do-difference (b) => () let (left2, top2, right2, bottom2) = box-edges(b); let new = ltrb-difference(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end method; do-regions(do-difference, set); if (new-boxes) apply(make-box-set, new-boxes) else $nowhere end end method region-difference; define sealed method region-difference (set :: , box :: ) => (region :: ) let new-boxes :: = make(); let (left2, top2, right2, bottom2) = box-edges(box); local method do-difference (b) => () let (left1, top1, right1, bottom1) = box-edges(b); let new = ltrb-difference(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end method; do-regions(do-difference, set); if (empty?(new-boxes)) $nowhere else apply(make-box-set, new-boxes) end end method region-difference; define sealed method region-difference (set1 :: , set2 :: ) => (region :: ) let new-boxes :: = make(); do-regions (method (box1) do-regions (method (box2) let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); let new = ltrb-difference(left1, top1, right1, bottom1, left2, top2, right2, bottom2); when (new) add!(new-boxes, new) end end, set2) end, set1); if (empty?(new-boxes)) $nowhere else apply(make-box-set, new-boxes) end end method region-difference; define sealed method region-empty? (set :: ) => (true? :: ) every?(region-empty?, box-set-boxes(set)) end method region-empty?; define sealed method normalize-box-set (set :: , banding) => (boxes :: ) local method collect-boxes (region) => (boxes) select (region by instance?) => apply(concatenate-as, , map(collect-boxes, box-set-boxes(region))); => list(region); => list(region); end end method, method reduce-boxes (pending-boxes, processed-boxes) => (boxes) case empty?(pending-boxes) => processed-boxes; region-empty?(head(pending-boxes)) => reduce-boxes(tail(pending-boxes), processed-boxes); otherwise => let intersecting-region = begin local method intersects? (box) => (intersects :: ) region-intersects-region?(box, head(pending-boxes)) end method; find-value(tail(pending-boxes), intersects?) end; if (empty?(intersecting-region)) reduce-boxes (tail(pending-boxes), pair(head(pending-boxes), processed-boxes)) else reduce-boxes (concatenate! (reduce-box-pair(head(pending-boxes), intersecting-region), remove!(tail(pending-boxes), intersecting-region)), processed-boxes) end; end end method, method reduce-box-pair (box1, box2) => (boxes) // Don't use 'region-union', because we are only prepared // to deal with bounding boxes let (left1, top1, right1, bottom1) = box-edges(box1); let (left2, top2, right2, bottom2) = box-edges(box2); remove!(ltrb-union(left1, top1, right1, bottom1, left2, top2, right2, bottom2, banding: banding), #f, test: method (_x, _y) ignore(_y); region-empty?(_x) end) end method; as(, reduce-boxes(collect-boxes(set), #())) end method normalize-box-set;