├── Baseplate.rbxl ├── LICENSE ├── README.md ├── default.project.json └── src ├── ReplicatedStorage ├── PolyBool │ ├── Epsilon.lua │ ├── Intersector.lua │ ├── LinkedList.lua │ ├── SegmentChainer.lua │ ├── SegmentSelector.lua │ └── init.lua └── Test │ ├── Dragger.lua │ ├── Draw.lua │ ├── Maid.lua │ └── init.lua └── StarterPlayerScripts └── RunTest.client.lua /Baseplate.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EgoMoose/PolyBool-Lua/8a9504f52cd1575097f626c5756a82cb15313615/Baseplate.rbxl -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EgoMoose 4 | Copyright (c) 2016 Sean Connelly (@voidqk, web: syntheti.cc) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolyBool-Lua 2 | Boolean operations on polygons (union, intersection, difference, xor). 3 | 4 | This is a Lua port of this repository: 5 | https://github.com/velipso/polybooljs 6 | 7 | Check it out as it has all documentation on how to use it that you'd need. -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PolyBool-Lua", 3 | "tree": { 4 | "$className": "DataModel", 5 | "HttpService": { 6 | "$className": "HttpService", 7 | "$properties": { 8 | "HttpEnabled": true 9 | } 10 | }, 11 | "ReplicatedStorage": { 12 | "$className": "ReplicatedStorage", 13 | "$path": "src/ReplicatedStorage" 14 | }, 15 | "StarterPlayer": { 16 | "$className": "StarterPlayer", 17 | "StarterPlayerScripts": { 18 | "$className": "StarterPlayerScripts", 19 | "$path": "src/StarterPlayerScripts" 20 | } 21 | }, 22 | "SoundService": { 23 | "$className": "SoundService", 24 | "$properties": { 25 | "RespectFilteringEnabled": true 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/Epsilon.lua: -------------------------------------------------------------------------------- 1 | -- (c) Copyright 2016, Sean Connelly (@voidqk), http:--syntheti.cc 2 | -- MIT License 3 | -- Project Home: https:--github.com/voidqk/polybooljs 4 | -- Converted to Lua by EgoMoose 5 | 6 | -- 7 | -- provides the raw computation functions that takes epsilon into account 8 | -- 9 | -- zero is defined to be between (-epsilon, epsilon) exclusive 10 | -- 11 | 12 | local function iif(bool, a, b) 13 | if (bool) then return a end 14 | return b 15 | end 16 | 17 | function Epsilon(eps) 18 | if (type(eps) ~= 'number') then 19 | eps = 0.0000000001; -- sane default? sure why not 20 | end 21 | 22 | local my 23 | my = { 24 | epsilon = function(v) 25 | if (type(v) == 'number') then 26 | eps = v 27 | end 28 | return eps 29 | end, 30 | pointAboveOrOnLine = function(pt, left, right) 31 | local Ax = left[1] 32 | local Ay = left[2] 33 | local Bx = right[1] 34 | local By = right[2] 35 | local Cx = pt[1] 36 | local Cy = pt[2] 37 | return (Bx - Ax) * (Cy - Ay) - (By - Ay) * (Cx - Ax) >= -eps 38 | end, 39 | pointBetween = function(p, left, right) 40 | -- p must be collinear with left->right 41 | -- returns false if p == left, p == right, or left == right 42 | local d_py_ly = p[2] - left[2] 43 | local d_rx_lx = right[1] - left[1] 44 | local d_px_lx = p[1] - left[1] 45 | local d_ry_ly = right[2] - left[2] 46 | 47 | local dot = d_px_lx * d_rx_lx + d_py_ly * d_ry_ly 48 | -- if `dot` is 0, then `p` == `left` or `left` == `right` (reject) 49 | -- if `dot` is less than 0, then `p` is to the left of `left` (reject) 50 | if (dot < eps) then 51 | return false 52 | end 53 | 54 | local sqlen = d_rx_lx * d_rx_lx + d_ry_ly * d_ry_ly 55 | -- if `dot` > `sqlen`, then `p` is to the right of `right` (reject) 56 | -- therefore, if `dot - sqlen` is greater than 0, then `p` is to the right of `right` (reject) 57 | if (dot - sqlen > -eps) then 58 | return false 59 | end 60 | 61 | return true 62 | end, 63 | pointsSameX = function(p1, p2) 64 | return math.abs(p1[1] - p2[1]) < eps 65 | end, 66 | pointsSameY = function(p1, p2) 67 | return math.abs(p1[2] - p2[2]) < eps 68 | end, 69 | pointsSame = function(p1, p2) 70 | return my.pointsSameX(p1, p2) and my.pointsSameY(p1, p2) 71 | end, 72 | pointsCompare = function(p1, p2) 73 | -- returns -1 if p1 is smaller, 1 if p2 is smaller, 0 if equal 74 | if (my.pointsSameX(p1, p2)) then 75 | return iif(my.pointsSameY(p1, p2), 0, iif(p1[2] < p2[2], -1, 1)) 76 | end 77 | return iif(p1[1] < p2[1], -1, 1) 78 | end, 79 | pointsCollinear = function(pt1, pt2, pt3) 80 | -- does pt1->pt2->pt3 make a straight line? 81 | -- essentially this is just checking to see if the slope(pt1->pt2) === slope(pt2->pt3) 82 | -- if slopes are equal, then they must be collinear, because they share pt2 83 | local dx1 = pt1[1] - pt2[1] 84 | local dy1 = pt1[2] - pt2[2] 85 | local dx2 = pt2[1] - pt3[1] 86 | local dy2 = pt2[2] - pt3[2] 87 | return math.abs(dx1 * dy2 - dx2 * dy1) < eps 88 | end, 89 | linesIntersect = function(a0, a1, b0, b1) 90 | -- returns false if the lines are coincident (e.g., parallel or on top of each other) 91 | -- 92 | -- returns an object if the lines intersect: 93 | -- { 94 | -- pt: [x, y], where the intersection point is at 95 | -- alongA: where intersection point is along A, 96 | -- alongB: where intersection point is along B 97 | -- } 98 | -- 99 | -- alongA and alongB will each be one of: -2, -1, 0, 1, 2 100 | -- 101 | -- with the following meaning: 102 | -- 103 | -- -2 intersection point is before segment's first point 104 | -- -1 intersection point is directly on segment's first point 105 | -- 0 intersection point is between segment's first and second points (exclusive) 106 | -- 1 intersection point is directly on segment's second point 107 | -- 2 intersection point is after segment's second point 108 | local adx = a1[1] - a0[1] 109 | local ady = a1[2] - a0[2] 110 | local bdx = b1[1] - b0[1] 111 | local bdy = b1[2] - b0[2] 112 | 113 | local axb = adx * bdy - ady * bdx 114 | if (math.abs(axb) < eps) then 115 | return false; -- lines are coincident 116 | end 117 | 118 | local dx = a0[1] - b0[1] 119 | local dy = a0[2] - b0[2] 120 | 121 | local A = (bdx * dy - bdy * dx) / axb 122 | local B = (adx * dy - ady * dx) / axb 123 | 124 | local ret = { 125 | alongA = 0, 126 | alongB = 0, 127 | pt = { 128 | a0[1] + A * adx, 129 | a0[2] + A * ady 130 | } 131 | }; 132 | 133 | -- categorize where intersection point is along A and B 134 | 135 | if (A <= -eps) then 136 | ret.alongA = -2 137 | elseif (A < eps) then 138 | ret.alongA = -1 139 | elseif (A - 1 <= -eps) then 140 | ret.alongA = 0 141 | elseif (A - 1 < eps) then 142 | ret.alongA = 1 143 | else 144 | ret.alongA = 2 145 | end 146 | 147 | if (B <= -eps) then 148 | ret.alongB = -2 149 | elseif (B < eps) then 150 | ret.alongB = -1 151 | elseif (B - 1 <= -eps) then 152 | ret.alongB = 0 153 | elseif (B - 1 < eps) then 154 | ret.alongB = 1 155 | else 156 | ret.alongB = 2 157 | end 158 | 159 | return ret 160 | end, 161 | pointInsideRegion = function(pt, region) 162 | local x = pt[1] 163 | local y = pt[2] 164 | local last_x = region[#region][1] 165 | local last_y = region[#region][2] 166 | local inside = false 167 | for i = 1, #region do 168 | local curr_x = region[i][1] 169 | local curr_y = region[i][2] 170 | 171 | -- if y is between curr_y and last_y, and 172 | -- x is to the right of the boundary created by the line 173 | if ((curr_y - y > eps) ~= (last_y - y > eps) and (last_x - curr_x) * (y - curr_y) / (last_y - curr_y) + curr_x - x > eps) then 174 | inside = not inside 175 | end 176 | 177 | last_x = curr_x 178 | last_y = curr_y 179 | end 180 | return inside 181 | end 182 | } 183 | return my; 184 | end 185 | 186 | return Epsilon -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/Intersector.lua: -------------------------------------------------------------------------------- 1 | -- (c) Copyright 2016, Sean Connelly (@voidqk), http:--syntheti.cc 2 | -- MIT License 3 | -- Project Home: https:--github.com/voidqk/polybooljs 4 | -- Converted to Lua by EgoMoose 5 | 6 | -- 7 | -- this is the core work-horse 8 | -- 9 | 10 | local function iif(bool, a, b) 11 | if (bool) then return a end 12 | return b 13 | end 14 | 15 | local LinkedList = require(script.Parent:WaitForChild("LinkedList")) 16 | 17 | local function Intersecter(selfIntersection, eps, buildLog) 18 | -- selfIntersection is true/false depending on the phase of the overall algorithm 19 | 20 | -- 21 | -- segment creation 22 | -- 23 | 24 | local function segmentNew(start, finish) 25 | return { 26 | id = buildLog and buildLog.segmentId() or -1, 27 | start = start, 28 | finish = finish, 29 | myFill = { 30 | above = nil, -- is there fill above us? 31 | below = nil -- is there fill below us? 32 | }, 33 | otherFill = nil 34 | } 35 | end 36 | 37 | local function segmentCopy(start, finish, seg) 38 | return { 39 | id = buildLog and buildLog.segmentId() or -1, 40 | start = start, 41 | finish = finish, 42 | myFill = { 43 | above = seg.myFill.above, 44 | below = seg.myFill.below 45 | }, 46 | otherFill = nil 47 | } 48 | end 49 | 50 | -- 51 | -- event logic 52 | -- 53 | 54 | local event_root = LinkedList.create() 55 | 56 | local function eventCompare(p1_isStart, p1_1, p1_2, p2_isStart, p2_1, p2_2) 57 | -- compare the selected points first 58 | local comp = eps.pointsCompare(p1_1, p2_1) 59 | if (comp ~= 0) then 60 | return comp 61 | end 62 | -- the selected points are the same 63 | 64 | if (eps.pointsSame(p1_2, p2_2)) then -- if the non-selected points are the same too... 65 | return 0 -- then the segments are equal 66 | end 67 | 68 | if (p1_isStart ~= p2_isStart) then -- if one is a start and the other isn't... 69 | return iif(p1_isStart, 1, -1) -- favor the one that isn't the start 70 | end 71 | 72 | -- otherwise, we'll have to calculate which one is below the other manually 73 | return iif(eps.pointAboveOrOnLine(p1_2, 74 | iif(p2_isStart, p2_1, p2_2), -- order matters 75 | iif(p2_isStart, p2_2, p2_1) 76 | ), 1, -1) 77 | end 78 | 79 | local function eventAdd(ev, other_pt) 80 | event_root.insertBefore(ev, function(here) 81 | -- should ev be inserted before here? 82 | local comp = eventCompare( 83 | ev.isStart, ev.pt, other_pt, 84 | here.isStart, here.pt, here.other.pt 85 | ) 86 | return comp < 0 87 | end) 88 | end 89 | 90 | local function eventAddSegmentStart(seg, primary) 91 | local ev_start = LinkedList.node({ 92 | isStart = true, 93 | pt = seg.start, 94 | seg = seg, 95 | primary = primary, 96 | other = nil, 97 | status = nil 98 | }) 99 | eventAdd(ev_start, seg.finish) 100 | return ev_start 101 | end 102 | 103 | local function eventAddSegmentEnd(ev_start, seg, primary) 104 | local ev_end = LinkedList.node({ 105 | isStart = false, 106 | pt = seg.finish, 107 | seg = seg, 108 | primary = primary, 109 | other = ev_start, 110 | status = nil 111 | }) 112 | ev_start.other = ev_end 113 | eventAdd(ev_end, ev_start.pt) 114 | end 115 | 116 | local function eventAddSegment(seg, primary) 117 | local ev_start = eventAddSegmentStart(seg, primary) 118 | eventAddSegmentEnd(ev_start, seg, primary) 119 | return ev_start 120 | end 121 | 122 | local function eventUpdateEnd(ev, finish) 123 | -- slides an finish backwards 124 | -- (start)------------(finish) to: 125 | -- (start)---(finish) 126 | 127 | if (buildLog) then 128 | buildLog.segmentChop(ev.seg, finish) 129 | end 130 | 131 | ev.other.remove() 132 | ev.seg.finish = finish 133 | ev.other.pt = finish 134 | eventAdd(ev.other, ev.pt) 135 | end 136 | 137 | local function eventDivide(ev, pt) 138 | local ns = segmentCopy(pt, ev.seg.finish, ev.seg) 139 | eventUpdateEnd(ev, pt) 140 | return eventAddSegment(ns, ev.primary) 141 | end 142 | 143 | local function calculate(primaryPolyInverted, secondaryPolyInverted) 144 | -- if selfIntersection is true then there is no secondary polygon, so that isn't used 145 | 146 | -- 147 | -- status logic 148 | -- 149 | 150 | local status_root = LinkedList.create() 151 | 152 | local function statusCompare(ev1, ev2) 153 | local a1 = ev1.seg.start 154 | local a2 = ev1.seg.finish 155 | local b1 = ev2.seg.start 156 | local b2 = ev2.seg.finish 157 | 158 | if (eps.pointsCollinear(a1, b1, b2)) then 159 | if (eps.pointsCollinear(a2, b1, b2)) then 160 | return 1;--eventCompare(true, a1, a2, true, b1, b2); 161 | end 162 | return iif(eps.pointAboveOrOnLine(a2, b1, b2), 1, -1) 163 | end 164 | return iif(eps.pointAboveOrOnLine(a1, b1, b2), 1, -1) 165 | end 166 | 167 | local function statusFindSurrounding(ev) 168 | return status_root.findTransition(function(here) 169 | local comp = statusCompare(ev, here.ev) 170 | return comp > 0 171 | end) 172 | end 173 | 174 | local function checkIntersection(ev1, ev2) 175 | -- returns the segment equal to ev1, or false if nothing equal 176 | 177 | local seg1 = ev1.seg 178 | local seg2 = ev2.seg 179 | local a1 = seg1.start 180 | local a2 = seg1.finish 181 | local b1 = seg2.start 182 | local b2 = seg2.finish 183 | 184 | if (buildLog) then 185 | buildLog.checkIntersection(seg1, seg2) 186 | end 187 | 188 | local i = eps.linesIntersect(a1, a2, b1, b2) 189 | 190 | if (i == false) then 191 | -- segments are parallel or coincident 192 | 193 | -- if points aren't collinear, then the segments are parallel, so no intersections 194 | if (not eps.pointsCollinear(a1, a2, b1)) then 195 | return false 196 | end 197 | -- otherwise, segments are on top of each other somehow (aka coincident) 198 | 199 | if (eps.pointsSame(a1, b2) or eps.pointsSame(a2, b1)) then 200 | return false -- segments touch at endpoints... no intersection 201 | end 202 | 203 | local a1_equ_b1 = eps.pointsSame(a1, b1) 204 | local a2_equ_b2 = eps.pointsSame(a2, b2) 205 | 206 | if (a1_equ_b1 and a2_equ_b2) then 207 | return ev2 -- segments are exactly equal 208 | end 209 | 210 | local a1_between = not a1_equ_b1 and eps.pointBetween(a1, b1, b2) 211 | local a2_between = not a2_equ_b2 and eps.pointBetween(a2, b1, b2) 212 | 213 | -- handy for debugging: 214 | -- buildLog.log({ 215 | -- a1_equ_b1: a1_equ_b1, 216 | -- a2_equ_b2: a2_equ_b2, 217 | -- a1_between: a1_between, 218 | -- a2_between: a2_between 219 | -- }); 220 | 221 | if (a1_equ_b1) then 222 | if (a2_between) then 223 | -- (a1)---(a2) 224 | -- (b1)----------(b2) 225 | eventDivide(ev2, a2) 226 | else 227 | -- (a1)----------(a2) 228 | -- (b1)---(b2) 229 | eventDivide(ev1, b2) 230 | end 231 | return ev2 232 | elseif (a1_between) then 233 | if (not a2_equ_b2) then 234 | -- make a2 equal to b2 235 | if (a2_between) then 236 | -- (a1)---(a2) 237 | -- (b1)-----------------(b2) 238 | eventDivide(ev2, a2) 239 | else 240 | -- (a1)----------(a2) 241 | -- (b1)----------(b2) 242 | eventDivide(ev1, b2) 243 | end 244 | end 245 | 246 | -- (a1)---(a2) 247 | -- (b1)----------(b2) 248 | eventDivide(ev2, a1) 249 | end 250 | else 251 | -- otherwise, lines intersect at i.pt, which may or may not be between the endpoints 252 | 253 | -- is A divided between its endpoints? (exclusive) 254 | if (i.alongA == 0) then 255 | if (i.alongB == -1) then -- yes, at exactly b1 256 | eventDivide(ev1, b1) 257 | elseif (i.alongB == 0) then -- yes, somewhere between B's endpoints 258 | eventDivide(ev1, i.pt) 259 | elseif (i.alongB == 1) then -- yes, at exactly b2 260 | eventDivide(ev1, b2) 261 | end 262 | end 263 | 264 | -- is B divided between its endpoints? (exclusive) 265 | if (i.alongB == 0) then 266 | if (i.alongA == -1) then -- yes, at exactly a1 267 | eventDivide(ev2, a1) 268 | elseif (i.alongA == 0) then -- yes, somewhere between A's endpoints (exclusive) 269 | eventDivide(ev2, i.pt) 270 | elseif (i.alongA == 1) then -- yes, at exactly a2 271 | eventDivide(ev2, a2) 272 | end 273 | end 274 | end 275 | return false 276 | end 277 | 278 | -- 279 | -- main event loop 280 | -- 281 | local segments = {} 282 | while (not event_root.isEmpty()) do 283 | local ev = event_root.getHead() 284 | 285 | if (buildLog) then 286 | buildLog.vert(ev.pt[1]) 287 | end 288 | 289 | if (ev.isStart) then 290 | 291 | if (buildLog) then 292 | buildLog.segmentNew(ev.seg, ev.primary) 293 | end 294 | 295 | local surrounding = statusFindSurrounding(ev) 296 | local above = surrounding.before and surrounding.before.ev or nil 297 | local below = surrounding.after and surrounding.after.ev or nil 298 | 299 | if (buildLog) then 300 | buildLog.tempStatus( 301 | ev.seg, 302 | above and above.seg or false, 303 | below and below.seg or false 304 | ) 305 | end 306 | 307 | local function checkBothIntersections() 308 | if (above) then 309 | local eve = checkIntersection(ev, above) 310 | if (eve) then 311 | return eve 312 | end 313 | end 314 | if (below) then 315 | return checkIntersection(ev, below) 316 | end 317 | return false 318 | end 319 | 320 | local eve = checkBothIntersections() 321 | if (eve) then 322 | -- ev and eve are equal 323 | -- we'll keep eve and throw away ev 324 | 325 | -- merge ev.seg's fill information into eve.seg 326 | 327 | if (selfIntersection) then 328 | local toggle -- are we a toggling edge? 329 | if (ev.seg.myFill.below == nil) then 330 | toggle = true 331 | else 332 | toggle = ev.seg.myFill.above ~= ev.seg.myFill.below 333 | end 334 | 335 | -- merge two segments that belong to the same polygon 336 | -- think of this as sandwiching two segments together, where `eve.seg` is 337 | -- the bottom -- this will cause the above fill flag to toggle 338 | if (toggle) then 339 | eve.seg.myFill.above = not eve.seg.myFill.above 340 | end 341 | else 342 | -- merge two segments that belong to different polygons 343 | -- each segment has distinct knowledge, so no special logic is needed 344 | -- note that this can only happen once per segment in this phase, because we 345 | -- are guaranteed that all self-intersections are gone 346 | eve.seg.otherFill = ev.seg.myFill 347 | end 348 | 349 | if (buildLog) then 350 | buildLog.segmentUpdate(eve.seg) 351 | end 352 | 353 | ev.other.remove() 354 | ev.remove() 355 | end 356 | 357 | if (event_root.getHead() ~= ev) then 358 | -- something was inserted before us in the event queue, so loop back around and 359 | -- process it before continuing 360 | if (buildLog) then 361 | buildLog.rewind(ev.seg) 362 | end 363 | continue; 364 | end 365 | 366 | -- 367 | -- calculate fill flags 368 | -- 369 | if (selfIntersection) then 370 | local toggle -- are we a toggling edge? 371 | if (ev.seg.myFill.below == nil) then -- if we are a new segment... 372 | toggle = true -- then we toggle 373 | else -- we are a segment that has previous knowledge from a division 374 | toggle = ev.seg.myFill.above ~= ev.seg.myFill.below -- calculate toggle 375 | end 376 | 377 | -- next, calculate whether we are filled below us 378 | if (not below) then -- if nothing is below us... 379 | -- we are filled below us if the polygon is inverted 380 | ev.seg.myFill.below = primaryPolyInverted 381 | else 382 | -- otherwise, we know the answer -- it's the same if whatever is below 383 | -- us is filled above it 384 | ev.seg.myFill.below = below.seg.myFill.above 385 | end 386 | 387 | -- since now we know if we're filled below us, we can calculate whether 388 | -- we're filled above us by applying toggle to whatever is below us 389 | if (toggle) then 390 | ev.seg.myFill.above = not ev.seg.myFill.below; 391 | else 392 | ev.seg.myFill.above = ev.seg.myFill.below; 393 | end 394 | else 395 | -- now we fill in any missing transition information, since we are all-knowing 396 | -- at this point 397 | 398 | if (ev.seg.otherFill == nil) then 399 | -- if we don't have other information, then we need to figure out if we're 400 | -- inside the other polygon 401 | local inside 402 | if (not below) then 403 | -- if nothing is below us, then we're inside if the other polygon is 404 | -- inverted 405 | inside = iif(ev.primary, secondaryPolyInverted, primaryPolyInverted) 406 | else -- otherwise, something is below us 407 | -- so copy the below segment's other polygon's above 408 | if (ev.primary == below.primary) then 409 | inside = below.seg.otherFill.above 410 | else 411 | inside = below.seg.myFill.above 412 | end 413 | end 414 | ev.seg.otherFill = { 415 | above = inside, 416 | below = inside 417 | } 418 | end 419 | end 420 | 421 | if (buildLog) then 422 | buildLog.status( 423 | ev.seg, 424 | above and above.seg or false, 425 | below and below.seg or false 426 | ) 427 | end 428 | 429 | -- insert the status and remember it for later removal 430 | ev.other.status = surrounding.insert(LinkedList.node({ ev = ev })) 431 | else 432 | local st = ev.status 433 | 434 | if (st == nil) then 435 | error('PolyBool: Zero-length segment detected; your epsilon is probably too small or too large') 436 | end 437 | 438 | -- removing the status will create two new adjacent edges, so we'll need to check 439 | -- for those 440 | if (status_root.exists(st.prev) and status_root.exists(st.next)) then 441 | checkIntersection(st.prev.ev, st.next.ev) 442 | end 443 | 444 | if (buildLog) then 445 | buildLog.statusRemove(st.ev.seg) 446 | end 447 | 448 | -- remove the status 449 | st.remove() 450 | 451 | -- if we've reached this point, we've calculated everything there is to know, so 452 | -- save the segment for reporting 453 | if (not ev.primary) then 454 | -- make sure `seg.myFill` actually points to the primary polygon though 455 | local s = ev.seg.myFill 456 | ev.seg.myFill = ev.seg.otherFill 457 | ev.seg.otherFill = s 458 | end 459 | table.insert(segments, ev.seg) 460 | end 461 | 462 | -- remove the event and continue 463 | event_root.getHead().remove() 464 | end 465 | 466 | if (buildLog) then 467 | buildLog.done() 468 | end 469 | 470 | return segments 471 | end 472 | 473 | -- return the appropriate API depending on what we're doing 474 | if (not selfIntersection) then 475 | -- performing combination of polygons, so only deal with already-processed segments 476 | return { 477 | calculate = function(segments1, inverted1, segments2, inverted2) 478 | -- segmentsX come from the self-intersection API, or this API 479 | -- invertedX is whether we treat that list of segments as an inverted polygon or not 480 | -- returns segments that can be used for further operations 481 | for _, seg in next, segments1 do 482 | eventAddSegment(segmentCopy(seg.start, seg.finish, seg), true) 483 | end 484 | for _, seg in next, segments2 do 485 | eventAddSegment(segmentCopy(seg.start, seg.finish, seg), false) 486 | end 487 | return calculate(inverted1, inverted2) 488 | end 489 | } 490 | end 491 | 492 | -- otherwise, performing self-intersection, so deal with regions 493 | return { 494 | addRegion = function(region) 495 | -- regions are a list of points: 496 | -- [ [0, 0], [100, 0], [50, 100] ] 497 | -- you can add multiple regions before running calculate 498 | local pt1 499 | local pt2 = region[#region] 500 | for i = 1, #region do 501 | pt1 = pt2 502 | pt2 = region[i] 503 | 504 | local forward = eps.pointsCompare(pt1, pt2) 505 | if (forward == 0) then -- points are equal, so we have a zero-length segment 506 | continue -- just skip it 507 | end 508 | 509 | eventAddSegment( 510 | segmentNew( 511 | iif(forward < 0, pt1, pt2), 512 | iif(forward < 0, pt2, pt1) 513 | ), 514 | true 515 | ) 516 | end 517 | end, 518 | calculate = function(inverted) 519 | -- is the polygon inverted? 520 | -- returns segments 521 | return calculate(inverted, false); 522 | end 523 | } 524 | end 525 | 526 | return Intersecter -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/LinkedList.lua: -------------------------------------------------------------------------------- 1 | -- (c) Copyright 2016, Sean Connelly (@voidqk), http:--syntheti.cc 2 | -- MIT License 3 | -- Project Home: https:--github.com/voidqk/polybooljs 4 | -- Converted to Lua by EgoMoose 5 | 6 | -- 7 | -- simple linked list implementation that allows you to traverse down nodes and save positions 8 | -- 9 | 10 | local function iif(bool, a, b) 11 | if (bool) then return a end 12 | return b 13 | end 14 | 15 | local LinkedList = { 16 | create = function() 17 | local my 18 | my = { 19 | root = { root = true, next = nil }, 20 | exists = function(node) 21 | if (node == nil or node == my.root) then 22 | return false 23 | end 24 | return true 25 | end, 26 | isEmpty = function() 27 | return my.root.next == nil 28 | end, 29 | getHead = function() 30 | return my.root.next 31 | end, 32 | insertBefore = function(node, check) 33 | local last = my.root 34 | local here = my.root.next 35 | while (here ~= nil) do 36 | if (check(here)) then 37 | node.prev = here.prev 38 | node.next = here 39 | here.prev.next = node 40 | here.prev = node 41 | return 42 | end 43 | last = here 44 | here = here.next 45 | end 46 | last.next = node 47 | node.prev = last 48 | node.next = nil 49 | end, 50 | findTransition = function(check) 51 | local prev = my.root 52 | local here = my.root.next 53 | while (here ~= nil) do 54 | if (check(here)) then 55 | break 56 | end 57 | prev = here 58 | here = here.next 59 | end 60 | return { 61 | before = iif(prev == my.root, nil, prev), 62 | after = here, 63 | insert = function(node) 64 | node.prev = prev 65 | node.next = here 66 | prev.next = node 67 | if (here ~= nil) then 68 | here.prev = node 69 | end 70 | return node 71 | end 72 | } 73 | end 74 | } 75 | return my 76 | end, 77 | node = function(data) 78 | data.prev = nil 79 | data.next = nil 80 | data.remove = function() 81 | data.prev.next = data.next 82 | if (data.next) then 83 | data.next.prev = data.prev 84 | end 85 | data.prev = nil 86 | data.next = nil 87 | end 88 | return data 89 | end 90 | } 91 | 92 | return LinkedList -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/SegmentChainer.lua: -------------------------------------------------------------------------------- 1 | -- (c) Copyright 2016, Sean Connelly (@voidqk), http:--syntheti.cc 2 | -- MIT License 3 | -- Project Home: https:--github.com/voidqk/polybooljs 4 | -- Converted to Lua by EgoMoose 5 | 6 | -- 7 | -- converts a list of segments into a list of regions, while also removing unnecessary verticies 8 | -- 9 | 10 | local function iif(bool, a, b) 11 | if (bool) then return a end 12 | return b 13 | end 14 | 15 | local function reverseArray(t) 16 | local n = #t 17 | local i = 1 18 | while i < n do 19 | t[i],t[n] = t[n],t[i] 20 | i = i + 1 21 | n = n - 1 22 | end 23 | end 24 | 25 | local function concatArray(arr1, arr2) 26 | local c, arr = 0, {} 27 | for i = 1, #arr1 do c = c + 1 arr[c] = arr1[i] end 28 | for i = 1, #arr2 do c = c + 1 arr[c] = arr2[i] end 29 | return arr 30 | end 31 | 32 | local function SegmentChainer(segments, eps, buildLog) 33 | local chains = {} 34 | local regions = {} 35 | 36 | for _, seg in next, segments do 37 | local pt1 = seg.start 38 | local pt2 = seg.finish 39 | if (eps.pointsSame(pt1, pt2)) then 40 | warn('PolyBool: Warning: Zero-length segment detected; your epsilon is probably too small or too large'); 41 | return 42 | end 43 | 44 | if (buildLog) then 45 | buildLog.chainStart(seg) 46 | end 47 | 48 | -- search for two chains that this segment matches 49 | local first_match = { 50 | index = 1, -- zero? 51 | matches_head = false, 52 | matches_pt1 = false 53 | } 54 | local second_match = { 55 | index = 1, -- zero? 56 | matches_head = false, 57 | matches_pt1 = false 58 | } 59 | local next_match = first_match 60 | local function setMatch(index, matches_head, matches_pt1) 61 | -- return true if we've matched twice 62 | next_match.index = index 63 | next_match.matches_head = matches_head 64 | next_match.matches_pt1 = matches_pt1 65 | if (next_match == first_match) then 66 | next_match = second_match 67 | return false 68 | end 69 | next_match = nil 70 | return true -- we've matched twice, we're done here 71 | end 72 | for i = 1, #chains do 73 | local chain = chains[i]; 74 | local head = chain[1]; 75 | local head2 = chain[2]; 76 | local tail = chain[#chain]; 77 | local tail2 = chain[#chain - 1]; 78 | if (eps.pointsSame(head, pt1)) then 79 | if (setMatch(i, true, true)) then 80 | break 81 | end 82 | elseif (eps.pointsSame(head, pt2)) then 83 | if (setMatch(i, true, false)) then 84 | break 85 | end 86 | elseif (eps.pointsSame(tail, pt1)) then 87 | if (setMatch(i, false, true)) then 88 | break 89 | end 90 | elseif (eps.pointsSame(tail, pt2)) then 91 | if (setMatch(i, false, false)) then 92 | break 93 | end 94 | end 95 | end 96 | 97 | if (next_match == first_match) then 98 | -- we didn't match anything, so create a new chain 99 | table.insert(chains, {pt1, pt2}) 100 | if (buildLog) then 101 | buildLog.chainNew(pt1, pt2) 102 | end 103 | continue 104 | end 105 | 106 | if (next_match == second_match) then 107 | -- we matched a single chain 108 | 109 | if (buildLog) then 110 | buildLog.chainMatch(first_match.index) 111 | end 112 | 113 | -- add the other point to the apporpriate finish, and check to see if we've closed the 114 | -- chain into a loop 115 | 116 | local index = first_match.index 117 | local pt = iif(first_match.matches_pt1, pt2, pt1) -- if we matched pt1, then we add pt2, etc 118 | local addToHead = first_match.matches_head -- if we matched at head, then add to the head 119 | 120 | local chain = chains[index] 121 | local grow = iif(addToHead, chain[1], chain[#chain]) 122 | local grow2 = iif(addToHead, chain[2], chain[#chain - 1]) 123 | local oppo = iif(addToHead, chain[#chain], chain[1]) 124 | local oppo2 = iif(addToHead, chain[#chain - 1], chain[2]) 125 | 126 | if (eps.pointsCollinear(grow2, grow, pt)) then 127 | -- grow isn't needed because it's directly between grow2 and pt: 128 | -- grow2 ---grow---> pt 129 | if (addToHead) then 130 | if (buildLog) then 131 | buildLog.chainRemoveHead(first_match.index, pt) 132 | end 133 | table.remove(chain, 1) 134 | else 135 | if (buildLog) then 136 | buildLog.chainRemoveTail(first_match.index, pt) 137 | end 138 | table.remove(chain) 139 | end 140 | grow = grow2 -- old grow is gone... new grow is what grow2 was 141 | end 142 | 143 | if (eps.pointsSame(oppo, pt)) then 144 | -- we're closing the loop, so remove chain from chains 145 | table.remove(chains, index) 146 | 147 | if (eps.pointsCollinear(oppo2, oppo, grow)) then 148 | -- oppo isn't needed because it's directly between oppo2 and grow: 149 | -- oppo2 ---oppo--->grow 150 | if (addToHead) then 151 | if (buildLog) then 152 | buildLog.chainRemoveTail(first_match.index, grow) 153 | end 154 | table.remove(chain) 155 | else 156 | if (buildLog) then 157 | buildLog.chainRemoveHead(first_match.index, grow) 158 | end 159 | table.remove(chain, 1) 160 | end 161 | end 162 | 163 | if (buildLog) then 164 | buildLog.chainClose(first_match.index) 165 | end 166 | 167 | -- we have a closed chain! 168 | table.insert(regions, chain) 169 | continue 170 | end 171 | 172 | -- not closing a loop, so just add it to the apporpriate side 173 | if (addToHead) then 174 | if (buildLog) then 175 | buildLog.chainAddHead(first_match.index, pt) 176 | end 177 | table.insert(chain, 1, pt) 178 | else 179 | if (buildLog) then 180 | buildLog.chainAddTail(first_match.index, pt) 181 | end 182 | table.insert(chain, pt) 183 | end 184 | continue 185 | end 186 | 187 | -- otherwise, we matched two chains, so we need to combine those chains together 188 | 189 | local function reverseChain(index) 190 | if (buildLog) then 191 | buildLog.chainReverse(index) 192 | end 193 | reverseArray(chains[index]) -- gee, that's easy 194 | end 195 | 196 | local function appendChain(index1, index2) 197 | -- index1 gets index2 appended to it, and index2 is removed 198 | local chain1 = chains[index1] 199 | local chain2 = chains[index2] 200 | local tail = chain1[#chain1] 201 | local tail2 = chain1[#chain1 - 1] 202 | local head = chain2[1] 203 | local head2 = chain2[2] 204 | 205 | if (eps.pointsCollinear(tail2, tail, head)) then 206 | -- tail isn't needed because it's directly between tail2 and head 207 | -- tail2 ---tail---> head 208 | if (buildLog) then 209 | buildLog.chainRemoveTail(index1, tail) 210 | end 211 | table.remove(chain1) 212 | tail = tail2 -- old tail is gone... new tail is what tail2 was 213 | end 214 | 215 | if (eps.pointsCollinear(tail, head, head2)) then 216 | -- head isn't needed because it's directly between tail and head2 217 | -- tail ---head---> head2 218 | if (buildLog) then 219 | buildLog.chainRemoveHead(index2, head) 220 | end 221 | table.remove(chain2, 1) 222 | end 223 | 224 | if (buildLog) then 225 | buildLog.chainJoin(index1, index2) 226 | end 227 | chains[index1] = concatArray(chain1, chain2) 228 | table.remove(chains, index2) 229 | end 230 | 231 | local F = first_match.index 232 | local S = second_match.index 233 | 234 | if (buildLog) then 235 | buildLog.chainConnect(F, S) 236 | end 237 | 238 | local reverseF = #chains[F] < #chains[S] -- reverse the shorter chain, if needed 239 | if (first_match.matches_head) then 240 | if (second_match.matches_head) then 241 | if (reverseF) then 242 | -- <<<< F <<<< --- >>>> S >>>> 243 | reverseChain(F) 244 | -- >>>> F >>>> --- >>>> S >>>> 245 | appendChain(F, S) 246 | else 247 | -- <<<< F <<<< --- >>>> S >>>> 248 | reverseChain(S); 249 | -- <<<< F <<<< --- <<<< S <<<< logically same as: 250 | -- >>>> S >>>> --- >>>> F >>>> 251 | appendChain(S, F); 252 | end 253 | else 254 | -- <<<< F <<<< --- <<<< S <<<< logically same as: 255 | -- >>>> S >>>> --- >>>> F >>>> 256 | appendChain(S, F); 257 | end 258 | else 259 | if (second_match.matches_head) then 260 | -- >>>> F >>>> --- >>>> S >>>> 261 | appendChain(F, S) 262 | else 263 | if (reverseF) then 264 | -- >>>> F >>>> --- <<<< S <<<< 265 | reverseChain(F) 266 | -- <<<< F <<<< --- <<<< S <<<< logically same as: 267 | -- >>>> S >>>> --- >>>> F >>>> 268 | appendChain(S, F) 269 | else 270 | -- >>>> F >>>> --- <<<< S <<<< 271 | reverseChain(S) 272 | -- >>>> F >>>> --- >>>> S >>>> 273 | appendChain(F, S) 274 | end 275 | end 276 | end 277 | end 278 | 279 | return regions 280 | end 281 | 282 | return SegmentChainer -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/SegmentSelector.lua: -------------------------------------------------------------------------------- 1 | -- (c) Copyright 2016, Sean Connelly (@voidqk), http:--syntheti.cc 2 | -- MIT License 3 | -- Project Home: https:--github.com/voidqk/polybooljs 4 | -- Converted to Lua by EgoMoose 5 | 6 | -- 7 | -- filter a list of segments based on boolean operations 8 | -- 9 | 10 | local function iif(bool, a, b) 11 | if (bool) then return a end 12 | return b 13 | end 14 | 15 | local function select(segments, selection, buildLog) 16 | local result = {} 17 | for _, seg in next, segments do 18 | local index = 19 | iif(seg.myFill.above, 8, 0) + 20 | iif(seg.myFill.below, 4, 0) + 21 | iif((seg.otherFill and seg.otherFill.above), 2, 0) + 22 | iif((seg.otherFill and seg.otherFill.below), 1, 0) + 1 23 | if (selection[index] ~= 0) then 24 | -- copy the segment to the results, while also calculating the fill status 25 | table.insert(result, { 26 | id = buildLog and buildLog.segmentId() or -1, 27 | start = seg.start, 28 | finish = seg.finish, 29 | myFill = { 30 | above = selection[index] == 1, -- 1 if filled above 31 | below = selection[index] == 2 -- 2 if filled below 32 | }, 33 | otherFill = nil 34 | }) 35 | end 36 | end 37 | 38 | if (buildLog) then 39 | buildLog.selected(result) 40 | end 41 | 42 | return result 43 | end 44 | 45 | local SegmentSelector = { 46 | union = function(segments, buildLog) -- primary | secondary 47 | -- above1 below1 above2 below2 Keep? Value 48 | -- 0 0 0 0 => no 0 49 | -- 0 0 0 1 => yes filled below 2 50 | -- 0 0 1 0 => yes filled above 1 51 | -- 0 0 1 1 => no 0 52 | -- 0 1 0 0 => yes filled below 2 53 | -- 0 1 0 1 => yes filled below 2 54 | -- 0 1 1 0 => no 0 55 | -- 0 1 1 1 => no 0 56 | -- 1 0 0 0 => yes filled above 1 57 | -- 1 0 0 1 => no 0 58 | -- 1 0 1 0 => yes filled above 1 59 | -- 1 0 1 1 => no 0 60 | -- 1 1 0 0 => no 0 61 | -- 1 1 0 1 => no 0 62 | -- 1 1 1 0 => no 0 63 | -- 1 1 1 1 => no 0 64 | return select(segments, { 65 | 0, 2, 1, 0, 66 | 2, 2, 0, 0, 67 | 1, 0, 1, 0, 68 | 0, 0, 0, 0 69 | }, buildLog) 70 | end, 71 | intersect = function(segments, buildLog) -- primary & secondary 72 | -- above1 below1 above2 below2 Keep? Value 73 | -- 0 0 0 0 => no 0 74 | -- 0 0 0 1 => no 0 75 | -- 0 0 1 0 => no 0 76 | -- 0 0 1 1 => no 0 77 | -- 0 1 0 0 => no 0 78 | -- 0 1 0 1 => yes filled below 2 79 | -- 0 1 1 0 => no 0 80 | -- 0 1 1 1 => yes filled below 2 81 | -- 1 0 0 0 => no 0 82 | -- 1 0 0 1 => no 0 83 | -- 1 0 1 0 => yes filled above 1 84 | -- 1 0 1 1 => yes filled above 1 85 | -- 1 1 0 0 => no 0 86 | -- 1 1 0 1 => yes filled below 2 87 | -- 1 1 1 0 => yes filled above 1 88 | -- 1 1 1 1 => no 0 89 | return select(segments, { 90 | 0, 0, 0, 0, 91 | 0, 2, 0, 2, 92 | 0, 0, 1, 1, 93 | 0, 2, 1, 0 94 | }, buildLog) 95 | end, 96 | difference = function(segments, buildLog) -- primary - secondary 97 | -- above1 below1 above2 below2 Keep? Value 98 | -- 0 0 0 0 => no 0 99 | -- 0 0 0 1 => no 0 100 | -- 0 0 1 0 => no 0 101 | -- 0 0 1 1 => no 0 102 | -- 0 1 0 0 => yes filled below 2 103 | -- 0 1 0 1 => no 0 104 | -- 0 1 1 0 => yes filled below 2 105 | -- 0 1 1 1 => no 0 106 | -- 1 0 0 0 => yes filled above 1 107 | -- 1 0 0 1 => yes filled above 1 108 | -- 1 0 1 0 => no 0 109 | -- 1 0 1 1 => no 0 110 | -- 1 1 0 0 => no 0 111 | -- 1 1 0 1 => yes filled above 1 112 | -- 1 1 1 0 => yes filled below 2 113 | -- 1 1 1 1 => no 0 114 | return select(segments, { 115 | 0, 0, 0, 0, 116 | 2, 0, 2, 0, 117 | 1, 1, 0, 0, 118 | 0, 1, 2, 0 119 | }, buildLog) 120 | end, 121 | differenceRev = function(segments, buildLog) -- secondary - primary 122 | -- above1 below1 above2 below2 Keep? Value 123 | -- 0 0 0 0 => no 0 124 | -- 0 0 0 1 => yes filled below 2 125 | -- 0 0 1 0 => yes filled above 1 126 | -- 0 0 1 1 => no 0 127 | -- 0 1 0 0 => no 0 128 | -- 0 1 0 1 => no 0 129 | -- 0 1 1 0 => yes filled above 1 130 | -- 0 1 1 1 => yes filled above 1 131 | -- 1 0 0 0 => no 0 132 | -- 1 0 0 1 => yes filled below 2 133 | -- 1 0 1 0 => no 0 134 | -- 1 0 1 1 => yes filled below 2 135 | -- 1 1 0 0 => no 0 136 | -- 1 1 0 1 => no 0 137 | -- 1 1 1 0 => no 0 138 | -- 1 1 1 1 => no 0 139 | return select(segments, { 140 | 0, 2, 1, 0, 141 | 0, 0, 1, 1, 142 | 0, 2, 0, 2, 143 | 0, 0, 0, 0 144 | }, buildLog) 145 | end, 146 | xor = function(segments, buildLog) -- primary ^ secondary 147 | -- above1 below1 above2 below2 Keep? Value 148 | -- 0 0 0 0 => no 0 149 | -- 0 0 0 1 => yes filled below 2 150 | -- 0 0 1 0 => yes filled above 1 151 | -- 0 0 1 1 => no 0 152 | -- 0 1 0 0 => yes filled below 2 153 | -- 0 1 0 1 => no 0 154 | -- 0 1 1 0 => no 0 155 | -- 0 1 1 1 => yes filled above 1 156 | -- 1 0 0 0 => yes filled above 1 157 | -- 1 0 0 1 => no 0 158 | -- 1 0 1 0 => no 0 159 | -- 1 0 1 1 => yes filled below 2 160 | -- 1 1 0 0 => no 0 161 | -- 1 1 0 1 => yes filled above 1 162 | -- 1 1 1 0 => yes filled below 2 163 | -- 1 1 1 1 => no 0 164 | return select(segments, { 165 | 0, 2, 1, 0, 166 | 2, 0, 0, 1, 167 | 1, 0, 0, 2, 168 | 0, 1, 2, 0 169 | }, buildLog) 170 | end 171 | } 172 | 173 | return SegmentSelector -------------------------------------------------------------------------------- /src/ReplicatedStorage/PolyBool/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * @copyright 2016 Sean Connelly (@voidqk), http://syntheti.cc 3 | * @license MIT 4 | * @preserve Project Home: https://github.com/voidqk/polybooljs 5 | 6 | Converted to Lua by EgoMoose 7 | --]] 8 | 9 | local Epsilon = require(script:WaitForChild("Epsilon")) 10 | local Intersecter = require(script:WaitForChild("Intersector")) 11 | local SegmentChainer = require(script:WaitForChild("SegmentChainer")) 12 | local SegmentSelector = require(script:WaitForChild("SegmentSelector")) 13 | 14 | local epsilon = Epsilon() 15 | local buildLog = false 16 | 17 | local PolyBool 18 | PolyBool = { 19 | -- getter/setter for epsilon 20 | epsilon = function(v) 21 | return epsilon.epsilon(v) 22 | end, 23 | 24 | -- core API 25 | segments = function(poly) 26 | local i = Intersecter(true, epsilon, buildLog) 27 | for _, reg in next, poly.regions do 28 | i.addRegion(reg) 29 | end 30 | return { 31 | segments = i.calculate(poly.inverted), 32 | inverted = poly.inverted 33 | } 34 | end, 35 | combine = function(segments1, segments2) 36 | local i3 = Intersecter(false, epsilon, buildLog) 37 | return { 38 | combined = i3.calculate( 39 | segments1.segments, segments1.inverted, 40 | segments2.segments, segments2.inverted 41 | ), 42 | inverted1 = segments1.inverted, 43 | inverted2 = segments2.inverted 44 | } 45 | end, 46 | selectUnion = function(combined) 47 | return { 48 | segments = SegmentSelector.union(combined.combined, buildLog), 49 | inverted = combined.inverted1 or combined.inverted2 50 | } 51 | end, 52 | selectIntersect = function(combined) 53 | return { 54 | segments = SegmentSelector.intersect(combined.combined, buildLog), 55 | inverted = combined.inverted1 and combined.inverted2 56 | } 57 | end, 58 | selectDifference = function(combined) 59 | return { 60 | segments = SegmentSelector.difference(combined.combined, buildLog), 61 | inverted = combined.inverted1 and not combined.inverted2 62 | } 63 | end, 64 | selectDifferenceRev = function(combined) 65 | return { 66 | segments = SegmentSelector.differenceRev(combined.combined, buildLog), 67 | inverted = not combined.inverted1 and combined.inverted2 68 | } 69 | end, 70 | selectXor = function(combined) 71 | return { 72 | segments = SegmentSelector.xor(combined.combined, buildLog), 73 | inverted = combined.inverted1 ~= combined.inverted2 74 | } 75 | end, 76 | polygon = function(segments) 77 | return { 78 | regions = SegmentChainer(segments.segments, epsilon, buildLog), 79 | inverted = segments.inverted 80 | } 81 | end, 82 | 83 | -- helper functions for common operations 84 | union = function(poly1, poly2) 85 | return operate(poly1, poly2, PolyBool.selectUnion) 86 | end, 87 | intersect = function(poly1, poly2) 88 | return operate(poly1, poly2, PolyBool.selectIntersect) 89 | end, 90 | difference = function(poly1, poly2) 91 | return operate(poly1, poly2, PolyBool.selectDifference) 92 | end, 93 | differenceRev = function(poly1, poly2) 94 | return operate(poly1, poly2, PolyBool.selectDifferenceRev) 95 | end, 96 | xor = function(poly1, poly2) 97 | return operate(poly1, poly2, PolyBool.selectXor) 98 | end 99 | } 100 | 101 | function operate(poly1, poly2, selector) 102 | local seg1 = PolyBool.segments(poly1) 103 | local seg2 = PolyBool.segments(poly2) 104 | local comb = PolyBool.combine(seg1, seg2) 105 | local seg3 = selector(comb) 106 | return PolyBool.polygon(seg3) 107 | end 108 | 109 | return PolyBool -------------------------------------------------------------------------------- /src/ReplicatedStorage/Test/Dragger.lua: -------------------------------------------------------------------------------- 1 | local Maid = require(script.Parent:WaitForChild("Maid")) 2 | 3 | -- CONSTANTS 4 | 5 | local UIS = game:GetService("UserInputService") 6 | local RUNSERVICE = game:GetService("RunService") 7 | 8 | local DEADZONE2 = 0.15^2 9 | local FLIP_THUMB = Vector3.new(1, -1, 1) 10 | 11 | local VALID_PRESS = { 12 | [Enum.UserInputType.MouseButton1] = true; 13 | [Enum.UserInputType.Touch] = true; 14 | } 15 | 16 | local VALID_MOVEMENT = { 17 | [Enum.UserInputType.MouseMovement] = true; 18 | [Enum.UserInputType.Touch] = true; 19 | } 20 | 21 | -- Class 22 | 23 | local DraggerClass = {} 24 | DraggerClass.__index = DraggerClass 25 | DraggerClass.ClassName = "Dragger" 26 | 27 | -- Public Constructors 28 | 29 | function DraggerClass.new(element) 30 | local self = setmetatable({}, DraggerClass) 31 | 32 | self._Maid = Maid.new() 33 | self._DragBind = Instance.new("BindableEvent") 34 | self._StartBind = Instance.new("BindableEvent") 35 | self._StopBind = Instance.new("BindableEvent") 36 | 37 | self.Element = element 38 | self.IsDragging = false 39 | self.DragChanged = self._DragBind.Event 40 | self.DragStart = self._StartBind.Event 41 | self.DragStop = self._StopBind.Event 42 | 43 | init(self) 44 | 45 | return self 46 | end 47 | 48 | -- Private Methods 49 | 50 | function init(self) 51 | local element = self.Element 52 | local maid = self._Maid 53 | local dragBind = self._DragBind 54 | local lastMousePosition = Vector3.new() 55 | 56 | maid:Mark(self._DragBind) 57 | maid:Mark(self._StartBind) 58 | maid:Mark(self._StopBind) 59 | 60 | maid:Mark(element.InputBegan:Connect(function(input) 61 | if (VALID_PRESS[input.UserInputType]) then 62 | lastMousePosition = input.Position 63 | self.IsDragging = true 64 | self._StartBind:Fire() 65 | end 66 | end)) 67 | 68 | maid:Mark(UIS.InputEnded:Connect(function(input) 69 | if (VALID_PRESS[input.UserInputType]) then 70 | self.IsDragging = false 71 | self._StopBind:Fire() 72 | end 73 | end)) 74 | 75 | maid:Mark(UIS.InputChanged:Connect(function(input, process) 76 | if (self.IsDragging) then 77 | if (VALID_MOVEMENT[input.UserInputType]) then 78 | local delta = input.Position - lastMousePosition 79 | lastMousePosition = input.Position 80 | dragBind:Fire(element, input, delta) 81 | end 82 | end 83 | end)) 84 | end 85 | 86 | -- Public Methods 87 | 88 | function DraggerClass:Destroy() 89 | self._Maid:Sweep() 90 | self.DragChanged = nil 91 | self.DragStart = nil 92 | self.DragStop = nil 93 | self.Element = nil 94 | end 95 | 96 | -- 97 | 98 | return DraggerClass -------------------------------------------------------------------------------- /src/ReplicatedStorage/Test/Draw.lua: -------------------------------------------------------------------------------- 1 | -- CONSTANTS 2 | local DEPTH = 10 3 | 4 | local WEDGE = Instance.new("WedgePart") 5 | WEDGE.Anchored = true 6 | WEDGE.TopSurface = Enum.SurfaceType.Smooth 7 | WEDGE.BottomSurface = Enum.SurfaceType.Smooth 8 | 9 | local FRAME = Instance.new("Frame") 10 | FRAME.BorderSizePixel = 0 11 | FRAME.Size = UDim2.new(0, 0, 0, 0) 12 | FRAME.BackgroundColor3 = Color3.new(1, 1, 1) 13 | 14 | -- Functions 15 | 16 | local function draw(properties) 17 | local frame = FRAME:Clone() 18 | for k, v in next, properties do 19 | frame[k] = v 20 | end 21 | return frame 22 | end 23 | 24 | local function rayPlane(p, v, o, n) 25 | local r = p - o 26 | local t = -r:Dot(n) / v:Dot(n) 27 | return p + t*v, t 28 | end 29 | 30 | local function point(p) 31 | return draw({ 32 | AnchorPoint = Vector2.new(0.5, 0.5); 33 | Position = UDim2.new(0, p.x, 0, p.y); 34 | Size = UDim2.new(0, 4, 0, 4); 35 | BackgroundColor3 = Color3.new(0, 1, 0); 36 | }) 37 | end 38 | 39 | local function line(a, b) 40 | local v = (b - a) 41 | local m = (a + b) / 2 42 | 43 | return draw({ 44 | AnchorPoint = Vector2.new(0.5, 0.5); 45 | Position = UDim2.new(0, m.x, 0, m.y); 46 | Size = UDim2.new(0, 2, 0, v.Magnitude); 47 | Rotation = math.deg(math.atan2(v.y, v.x)) - 90; 48 | BackgroundColor3 = Color3.new(1, 1, 0); 49 | }) 50 | end 51 | 52 | local function triangle(parent, a, b, c, color) 53 | WEDGE.Color = color 54 | local w1 = WEDGE:Clone() 55 | local w2 = WEDGE:Clone() 56 | 57 | local points = {a, b, c} 58 | 59 | local myCam = workspace.CurrentCamera 60 | local myCamCF = myCam.CFrame 61 | 62 | for i, p in next, points do 63 | local r = myCam:ViewportPointToRay(p.x, p.y, 0) 64 | local p = rayPlane(r.Origin, r.Direction, Vector3.new(0, 0, -DEPTH), Vector3.new(0, 0, 1)) 65 | points[i] = myCamCF:PointToObjectSpace(p) 66 | end 67 | 68 | a, b, c = unpack(points) 69 | 70 | -- Render the 3D triangle 71 | local ab, ac, bc = b - a, c - a, c - b 72 | local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc) 73 | 74 | if (abd > acd and abd > bcd) then 75 | c, a = a, c 76 | elseif (acd > bcd and acd > abd) then 77 | a, b = b, a 78 | end 79 | 80 | ab, ac, bc = b - a, c - a, c - b 81 | 82 | local right = ac:Cross(ab).Unit 83 | local up = bc:Cross(right).Unit 84 | local back = bc.Unit 85 | 86 | local height = math.abs(ab:Dot(up)) 87 | 88 | w1.Size = Vector3.new(0, height, math.abs(ab:Dot(back))) 89 | w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back) 90 | w1.Parent = parent 91 | 92 | w2.Size = Vector3.new(0, height, math.abs(ac:Dot(back))) 93 | w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back) 94 | w2.Parent = parent 95 | 96 | return function() 97 | w1:Destroy() 98 | w2:Destroy() 99 | end 100 | end 101 | 102 | local function makeVPF(color3) 103 | local cam = Instance.new("Camera") 104 | cam.CameraType = Enum.CameraType.Scriptable 105 | cam.CFrame = CFrame.new() 106 | cam.Focus = CFrame.new(0, 0, -DEPTH) 107 | 108 | local vpf = Instance.new("ViewportFrame") 109 | vpf.Size = UDim2.new(1, 0, 1, 0) 110 | vpf.BackgroundTransparency = 1 111 | vpf.BackgroundColor3 = color3 112 | vpf.CurrentCamera = cam 113 | cam.Parent = vpf 114 | 115 | return vpf 116 | end 117 | 118 | 119 | return { 120 | Point = point; 121 | Line = line; 122 | Triangle = triangle; 123 | MakeVPF = makeVPF; 124 | } -------------------------------------------------------------------------------- /src/ReplicatedStorage/Test/Maid.lua: -------------------------------------------------------------------------------- 1 | -- CONSTANTS 2 | 3 | local FORMAT_STR = "Maid does not support type \"%s\"" 4 | 5 | local DESTRUCTORS = { 6 | ["function"] = function(item) 7 | item() 8 | end; 9 | ["RBXScriptConnection"] = function(item) 10 | item:Disconnect() 11 | end; 12 | ["Instance"] = function(item) 13 | item:Destroy() 14 | end; 15 | ["table"] = function(item) 16 | item:Destroy() 17 | end; 18 | } 19 | 20 | -- Class 21 | 22 | local MaidClass = {} 23 | MaidClass.__index = MaidClass 24 | MaidClass.ClassName = "Maid" 25 | 26 | -- Public Constructors 27 | 28 | function MaidClass.new() 29 | local self = setmetatable({}, MaidClass) 30 | 31 | self.Trash = {} 32 | 33 | return self 34 | end 35 | 36 | -- Public Methods 37 | 38 | function MaidClass:Mark(item) 39 | local tof = typeof(item) 40 | 41 | if (DESTRUCTORS[tof]) then 42 | self.Trash[item] = tof 43 | else 44 | error(FORMAT_STR:format(tof), 2) 45 | end 46 | end 47 | 48 | function MaidClass:Unmark(item) 49 | if (item) then 50 | self.Trash[item] = nil 51 | else 52 | self.Trash = {} 53 | end 54 | end 55 | 56 | function MaidClass:Sweep() 57 | for item, tof in next, self.Trash do 58 | DESTRUCTORS[tof](item) 59 | end 60 | self.Trash = {} 61 | end 62 | 63 | -- 64 | 65 | return MaidClass -------------------------------------------------------------------------------- /src/ReplicatedStorage/Test/init.lua: -------------------------------------------------------------------------------- 1 | local Mouse = game.Players.LocalPlayer:GetMouse() 2 | local PolyBool = require(game.ReplicatedStorage.PolyBool) 3 | local Screen = game.Players.LocalPlayer.PlayerGui:WaitForChild("ScreenGui") 4 | 5 | local Maid = require(script:WaitForChild("Maid")).new() 6 | local Dragger = require(script:WaitForChild("Dragger")) 7 | local Draw = require(script:WaitForChild("Draw")) 8 | 9 | local HEIGHT = Vector2.new(0, 36) 10 | local VPF = Draw.MakeVPF(Screen.Background.BackgroundColor3) 11 | VPF.Parent = Screen.Render 12 | 13 | local funcName = nil 14 | local lastButton = nil 15 | local polygonFrames = {Screen.Container.Polygon1, Screen.Container.Polygon2} 16 | 17 | local function toV2(p) 18 | return Vector2.new(p[1], p[2]) 19 | end 20 | 21 | local function selectButton(button) 22 | if lastButton then 23 | funcName = nil 24 | lastButton.BackgroundColor3 = lastButton.BorderColor3 25 | end 26 | funcName = "select" .. button.Name 27 | button.BackgroundColor3 = Color3.fromRGB(0, 170, 255) 28 | lastButton = button 29 | end 30 | 31 | local function render() 32 | Maid:Sweep() 33 | 34 | local polygons = {} 35 | 36 | for k, polyFrame in pairs(polygonFrames) do 37 | local n = #polyFrame:GetChildren() 38 | local vertices = {} 39 | for i = 1, n do 40 | local framei = polyFrame[i] 41 | local framej = polyFrame[i % n + 1] 42 | local posi = framei.AbsolutePosition + framei.AbsoluteSize / 2 + HEIGHT 43 | local posj = framej.AbsolutePosition + framej.AbsoluteSize / 2 + HEIGHT 44 | 45 | vertices[i] = {posi.x, posi.y} 46 | 47 | local line = Draw.Line(posi, posj) 48 | line.BackgroundColor3 = polyFrame.BackgroundColor3 49 | line.ZIndex = 2 50 | line.Parent = Screen.Render 51 | Maid:Mark(line) 52 | end 53 | 54 | polygons[k] = { 55 | regions = {vertices}; 56 | inverted = false; 57 | } 58 | end 59 | 60 | if funcName ~= "selectNone" then 61 | local segments = PolyBool.segments(polygons[1]) 62 | for i = 2, #polygons do 63 | local seg2 = PolyBool.segments(polygons[i]) 64 | local comb = PolyBool.combine(segments, seg2) 65 | segments = PolyBool[funcName](comb) 66 | end 67 | 68 | local polygon = PolyBool.polygon(segments) 69 | 70 | for k, region in pairs(polygon.regions) do 71 | local color = (funcName == "selectXor" and BrickColor.new(k).Color) 72 | for i = 1, #region do 73 | local pi = toV2(region[i]) 74 | local pj = toV2(region[i % #region + 1]) 75 | local line = Draw.Line(pi, pj) 76 | line.BackgroundColor3 = color or line.BackgroundColor3 77 | line.Parent = Screen.Result 78 | Maid:Mark(line) 79 | end 80 | end 81 | end 82 | end 83 | 84 | for _, polygon in pairs(polygonFrames) do 85 | for _, handle in pairs(polygon:GetChildren()) do 86 | local drag = Dragger.new(handle) 87 | drag.DragChanged:Connect(function(element, input, delta) 88 | local size = polygon.Parent.AbsoluteSize 89 | local pos = Vector2.new(input.Position.x, input.Position.y) 90 | pos = pos - polygon.Parent.AbsolutePosition 91 | 92 | pos = Vector2.new( 93 | math.clamp(pos.x, 0, size.x), 94 | math.clamp(pos.y, 0, size.y) 95 | ) 96 | 97 | element.Position = UDim2.new(0, pos.x, 0, pos.y) 98 | render() 99 | end) 100 | end 101 | end 102 | 103 | for _, button in pairs(Screen.Options:GetChildren()) do 104 | if button:IsA("TextButton") then 105 | button.Activated:Connect(function() 106 | selectButton(button) 107 | render() 108 | end) 109 | end 110 | end 111 | 112 | selectButton(Screen.Options.None) 113 | render() 114 | 115 | return true -------------------------------------------------------------------------------- /src/StarterPlayerScripts/RunTest.client.lua: -------------------------------------------------------------------------------- 1 | require(game.ReplicatedStorage.Test) --------------------------------------------------------------------------------