import { Util } from '../misc'; import { Line } from './line'; import { Rect } from './rect'; import { Vector2 } from './vector2'; const MAX_SIZE = 500; export class ArbitrarySelection { cover: Rect[] = []; outlinesDirty = true; oldOutlines: Line[][] = []; constructor(cover: Rect[] = []) { this.cover = cover; } public get x(): number { if (this.cover.length === 0) { return 0; } return Util.MinBy(this.cover, r => r.x)!.x; } public get y(): number { if (this.cover.length === 0) { return 0; } return Util.MinBy(this.cover, r => r.y)!.y; } public get w(): number { if (this.cover.length === 0) { return 0; } return Util.MaxBy(this.cover, r => r.right)!.right - Util.MinBy(this.cover, r => r.x)!.x; } public get h(): number { if (this.cover.length === 0) { return 0; } return Util.MaxBy(this.cover, r => r.bottom)!.bottom - Util.MinBy(this.cover, r => r.y)!.y; } public get pos(): Vector2 { return new Vector2({ x: this.x, y: this.y }); } public get bounds(): Rect { return new Rect({ x: this.x, y: this.y, width: this.w, height: this.h, }); } public get isEmpty(): boolean { return this.cover.length === 0; } reset(): void { this.cover = []; this.oldOutlines = []; } addRect(rectToAdd: Rect): void { const subsumingRects = this.cover.filter(r => r.completelyContains(rectToAdd)); const intersectingRects = this.cover.filter(r => r.intersects(rectToAdd, { edgesOnlyIsAnIntersection: false })); if (subsumingRects.length > 0) { return; } for (const rect of intersectingRects) { this.subtractRect(rect.getIntersection(rectToAdd)!); } this.cover.push(rectToAdd); this.outlinesDirty = true; } subtractRect(rectToSubtract: Rect): void { const intersectingRects = this.cover.filter(r => r.intersects(rectToSubtract, { edgesOnlyIsAnIntersection: false })); for (const rect of intersectingRects) { // rectToSubtract completely contains rect if (rectToSubtract.completelyContains(rect)) { continue; } // rectToSubtract partially contains rect const subrectToRemove = rectToSubtract.getIntersection(rect)!; // rect completely contains subtractedRect // ------------------------- // | A | // | | // |-----------------------| // | B | hole | C | // |-----------------------| // | | // | D | // ------------------------- const newRects = [ { x: rect.x , y: rect.y , width: rect.width , height: subrectToRemove.y - rect.y }, // A { x: rect.x , y: subrectToRemove.y , width: subrectToRemove.x - rect.x , height: subrectToRemove.height }, // B { x: subrectToRemove.x + subrectToRemove.width, y: subrectToRemove.y , width: rect.x + rect.width - (subrectToRemove.width + subrectToRemove.x), height: subrectToRemove.height }, // C { x: rect.x , y: subrectToRemove.y + subrectToRemove.height, width: rect.width , height: rect.y + rect.height - (subrectToRemove.y + subrectToRemove.height) }, // D ].filter(r => r.width > 0 && r.height > 0) .map(r => new Rect(r)); this.cover = this.cover.concat(newRects); } for (const rect of intersectingRects) { this.cover.splice(this.cover.indexOf(rect), 1); } this.outlinesDirty = true; if (this.isEmpty) { this.reset(); } } // O(n^2) scc algorithm until someone convinces me I need a faster one getConnectedComponents(): Rect[][] { const components: Rect[][] = []; const seenRects: { [key: string]: boolean } = {} for (const rect of this.cover) { if (seenRects[rect.serialize()]) { continue; } const component = this.getConnectedComponentFrom(rect); components.push(component); for (const seen of component) { seenRects[seen.serialize()] = true; } } return components; } private getConnectedComponentFrom(start: Rect): Rect[] { const component: { [key: string]: boolean } = { }; let edge = [start]; while (edge.length > 0) { let newEdge: Rect[] = []; for (const rect of edge) { if (component[rect.serialize()]) { continue; } const intersectingRects = this.cover.filter(r => r.intersects(rect, { edgesOnlyIsAnIntersection: true })); component[rect.serialize()] = true; newEdge = newEdge.concat(intersectingRects); } edge = newEdge; } return Object.keys(component).map(r => Rect.DeserializeRect(r)); } getOutlines(): Line[][] { if (!this.outlinesDirty) { return this.oldOutlines; } let result: Line[][] = []; const components = this.getConnectedComponents(); for (const c of components) { const outline = this.getOutlineFor(c); const outlineComponents = this.getComponentsOfOutline(outline); result = result.concat(outlineComponents) } this.oldOutlines = result; this.outlinesDirty = false; return result; } private getOutlineFor(comp: Rect[]): Line[] { let allLines: (Line | undefined)[] = []; for (const rect of comp) { allLines.push.apply(allLines, rect.getLinesFromRect()); } // Alternate solution if this proves too hard: // Subdivide all lines on intersection points, then remove all // duplicates. // Actually that might even be better heh // The strategy here is to basically remove all overlapping segments. it's // hard because a single line could be overlapping with multiple other // lines. for (let i = 0; i < allLines.length; i++) { const line1 = allLines[i]; if (!line1) { continue; } for (let j = 0; j < allLines.length; j++) { const line2 = allLines[j]; if (!line2) { continue; } if (line1 === line2) { continue; } const intersection = line1.getOverlap(line2); if (intersection) { allLines[i] = undefined; allLines[j] = undefined; const newLines = line1.getNonOverlappingSections(line2); allLines = allLines.concat(newLines); break; } } } return allLines.filter(l => l !== undefined) as Line[]; } private getComponentsOfOutline(outline: Line[]): Line[][] { // Store lookup table by start and end vertex let lookupTable: { [key: number]: Line[] } = []; for (const line of outline) { const idx1 = line.x1 * MAX_SIZE + line.y1; const idx2 = line.x2 * MAX_SIZE + line.y2; if (!lookupTable[idx1]) { lookupTable[idx1] = []; } if (!lookupTable[idx2]) { lookupTable[idx2] = []; } lookupTable[idx1].push(line); lookupTable[idx2].push(line); } let result: Line[][] = []; let visited: { [key: string]: boolean } = {}; for (const line of outline) { if (visited[line.serialized]) { continue; } visited[line.serialized] = true; const sequence = [line]; while (true) { const current = sequence[sequence.length - 1]; const candidates = lookupTable[current.x1 * MAX_SIZE + current.y1].concat(lookupTable[current.x2 * MAX_SIZE + current.y2]); const next = candidates.filter(l => l !== current && !visited[l.serialized])[0]; if (!next) { break; } visited[next.serialized] = true; sequence.push(next); } result.push(sequence); } return result; } addArbitraryShape(pixels: Vector2[], canvasSize: Vector2): void { this.outlinesDirty = true; const covered: boolean[] = new Array(MAX_SIZE * MAX_SIZE); const rects: Rect[] = []; const ll = pixels.length; for (let i = 0; i < ll; i++) { const p = pixels[i]; covered[p.x * MAX_SIZE + p.y] = false; } for (let x = 0; x < canvasSize.x; x++) { for (let y = 0; y < canvasSize.y; y++) { if (covered[x * MAX_SIZE + y] !== false) { continue; } let squareSize = 2; outer: for (; squareSize < MAX_SIZE; squareSize++) { const endSquareX = x + squareSize; const endSquareY = y + squareSize; for (let bottomLineX = x; bottomLineX < endSquareX; bottomLineX++) { if (covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === undefined || covered[bottomLineX * MAX_SIZE + (y + squareSize - 1)] === true) { squareSize--; break outer; } } for (let bottomLineY = y; bottomLineY < endSquareY; bottomLineY++) { if (covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === undefined || covered[(x + squareSize - 1) * MAX_SIZE + bottomLineY] === true) { squareSize--; break outer; } } } for (let sx = x; sx < x + squareSize; sx++) { for (let sy = y; sy < y + squareSize; sy++) { covered[sx * MAX_SIZE + sy] = true; } } rects.push(new Rect({ x: x, y: y, width: squareSize, height: squareSize, })); } } for (const r of rects) { this.addRect(r); } } clone(): ArbitrarySelection { const result = new ArbitrarySelection(this.cover.slice(0)); result.outlinesDirty = this.outlinesDirty; result.oldOutlines = this.oldOutlines; return result; } translate(p: Vector2): void { this.cover = this.cover.map(x => x.translate(p)); this.oldOutlines = this.oldOutlines.map(l => l.map(ll => ll.translate(p))); } contains(p: Vector2): boolean { if (this.cover.length === 0) { return true; } for (const r of this.cover) { if (r.contains(p)) { return true; } } return false; } }