From 55a228997b4353062d780eee33935e17fa251616 Mon Sep 17 00:00:00 2001 From: Zander Date: Fri, 15 Apr 2022 15:42:21 +0000 Subject: [PATCH] feat: octree and helper utils --- src/helper.ts | 337 +++++++++++++++++++++++++++++++++++++ src/index.ts | 6 +- src/octree.ts | 448 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 789 insertions(+), 2 deletions(-) create mode 100644 src/helper.ts create mode 100644 src/octree.ts diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..76775f4 --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,337 @@ +import { Brick, Vector, WriteSaveObject } from './types'; + +export type BrickBounds = { + minBound: Vector; + maxBound: Vector; + center: Vector; +}; + +const rotationTable = [ + 16, 15, 22, 9, 18, 11, 20, 13, 17, 3, 21, 5, 19, 7, 23, 1, 0, 8, 4, 12, 6, 10, + 2, 14, 17, 12, 23, 10, 19, 8, 21, 14, 18, 0, 22, 6, 16, 4, 20, 2, 1, 9, 5, 13, + 7, 11, 3, 15, 18, 13, 20, 11, 16, 9, 22, 15, 19, 1, 23, 7, 17, 5, 21, 3, 2, + 10, 6, 14, 4, 8, 0, 12, 19, 14, 21, 8, 17, 10, 23, 12, 16, 2, 20, 4, 18, 6, + 22, 0, 3, 11, 7, 15, 5, 9, 1, 13, 22, 9, 16, 15, 20, 13, 18, 11, 21, 5, 17, 3, + 23, 1, 19, 7, 4, 12, 0, 8, 2, 14, 6, 10, 23, 10, 17, 12, 21, 14, 19, 8, 22, 6, + 18, 0, 20, 2, 16, 4, 5, 13, 1, 9, 3, 15, 7, 11, 20, 11, 18, 13, 22, 15, 16, 9, + 23, 7, 19, 1, 21, 3, 17, 5, 6, 14, 2, 10, 0, 12, 4, 8, 21, 8, 19, 14, 23, 12, + 17, 10, 20, 4, 16, 2, 22, 0, 18, 6, 7, 15, 3, 11, 1, 13, 5, 9, 15, 22, 9, 16, + 11, 20, 13, 18, 3, 21, 5, 17, 7, 23, 1, 19, 8, 4, 12, 0, 10, 2, 14, 6, 12, 23, + 10, 17, 8, 21, 14, 19, 0, 22, 6, 18, 4, 20, 2, 16, 9, 5, 13, 1, 11, 3, 15, 7, + 13, 20, 11, 18, 9, 22, 15, 16, 1, 23, 7, 19, 5, 21, 3, 17, 10, 6, 14, 2, 8, 0, + 12, 4, 14, 21, 8, 19, 10, 23, 12, 17, 2, 20, 4, 16, 6, 22, 0, 18, 11, 7, 15, + 3, 9, 1, 13, 5, 9, 16, 15, 22, 13, 18, 11, 20, 5, 17, 3, 21, 1, 19, 7, 23, 12, + 0, 8, 4, 14, 6, 10, 2, 10, 17, 12, 23, 14, 19, 8, 21, 6, 18, 0, 22, 2, 16, 4, + 20, 13, 1, 9, 5, 15, 7, 11, 3, 11, 18, 13, 20, 15, 16, 9, 22, 7, 19, 1, 23, 3, + 17, 5, 21, 14, 2, 10, 6, 12, 4, 8, 0, 8, 19, 14, 21, 12, 17, 10, 23, 4, 16, 2, + 20, 0, 18, 6, 22, 15, 3, 11, 7, 13, 5, 9, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1, 2, 3, 0, 5, 6, 7, 4, 9, + 10, 11, 8, 13, 14, 15, 12, 17, 18, 19, 16, 21, 22, 23, 20, 2, 3, 0, 1, 6, 7, + 4, 5, 10, 11, 8, 9, 14, 15, 12, 13, 18, 19, 16, 17, 22, 23, 20, 21, 3, 0, 1, + 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14, 19, 16, 17, 18, 23, 20, 21, 22, + 6, 5, 4, 7, 2, 1, 0, 3, 14, 13, 12, 15, 10, 9, 8, 11, 20, 23, 22, 21, 16, 19, + 18, 17, 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, 21, 20, 23, 22, + 17, 16, 19, 18, 4, 7, 6, 5, 0, 3, 2, 1, 12, 15, 14, 13, 8, 11, 10, 9, 22, 21, + 20, 23, 18, 17, 16, 19, 5, 4, 7, 6, 1, 0, 3, 2, 13, 12, 15, 14, 9, 8, 11, 10, + 23, 22, 21, 20, 19, 18, 17, 16, +]; + +const translationTable: (([x, y, z]: [number, number, number]) => [ + number, + number, + number +])[] = [ + ([x, y, z]) => [z, -y, x], + ([x, y, z]) => [z, -x, -y], + ([x, y, z]) => [z, y, -x], + ([x, y, z]) => [z, x, y], + + ([x, y, z]) => [-z, y, x], + ([x, y, z]) => [-z, x, -y], + ([x, y, z]) => [-z, -y, -x], + ([x, y, z]) => [-z, -x, y], + + ([x, y, z]) => [y, z, x], + ([x, y, z]) => [x, z, -y], + ([x, y, z]) => [-y, z, -x], + ([x, y, z]) => [-x, z, y], + + ([x, y, z]) => [-y, -z, x], + ([x, y, z]) => [-x, -z, -y], + ([x, y, z]) => [y, -z, -x], + ([x, y, z]) => [x, -z, y], + + ([x, y, z]) => [x, y, z], + ([x, y, z]) => [-y, x, z], + ([x, y, z]) => [-x, -y, z], + ([x, y, z]) => [y, -x, z], + + ([x, y, z]) => [-x, y, -z], + ([x, y, z]) => [y, x, -z], + ([x, y, z]) => [x, -y, -z], + ([x, y, z]) => [-y, -x, -z], +]; + +const orientationMap: Record = { + X_Positive_0: [0, 0], + X_Positive_90: [0, 1], + X_Positive_180: [0, 2], + X_Positive_270: [0, 3], + X_Negative_0: [1, 0], + X_Negative_90: [1, 1], + X_Negative_180: [1, 2], + X_Negative_270: [1, 3], + Y_Positive_0: [2, 0], + Y_Positive_90: [2, 1], + Y_Positive_180: [2, 2], + Y_Positive_270: [2, 3], + Y_Negative_0: [3, 0], + Y_Negative_90: [3, 1], + Y_Negative_180: [3, 2], + Y_Negative_270: [3, 3], + Z_Positive_0: [4, 0], + Z_Positive_90: [4, 1], + Z_Positive_180: [4, 2], + Z_Positive_270: [4, 3], + Z_Negative_0: [5, 0], + Z_Negative_90: [5, 1], + Z_Negative_180: [5, 2], + Z_Negative_270: [5, 3], +}; + +const DEFAULT_MATERIALS = [ + 'BMC_Hidden', + 'BMC_Ghost', + 'BMC_Ghost_Fail', + 'BMC_Plastic', + 'BMC_Glass', + 'BMC_Glow', + 'BMC_Metallic', + 'BMC_Hologram', +]; + +const brickSizeMap: Record = { + B_1x1_Brick_Side: [5, 5, 6], + B_1x1_Brick_Side_Lip: [5, 5, 6], + B_1x1_Cone: [5, 5, 6], + B_1x1_Round: [5, 5, 6], + B_1x1F_Octo: [5, 5, 2], + B_1x1F_Round: [5, 5, 2], + B_1x2_Overhang: [6, 10, 5], + B_1x2f_Plate_Center: [10, 5, 2], + B_1x2f_Plate_Center_Inv: [10, 5, 2], + B_1x4_Brick_Side: [20, 5, 6], + B_1x_Octo: [5, 5, 5], + B_1x_Octo_90Deg: [5, 5, 5], + B_1x_Octo_90Deg_Inv: [5, 5, 5], + B_1x_Octo_T: [5, 5, 5], + B_1x_Octo_T_Inv: [5, 5, 5], + B_2x1_Slipper: [10, 5, 6], + B_2x2_Cone: [10, 10, 12], + B_2x2_Corner: [10, 10, 6], + B_2x2_Overhang: [6, 10, 10], + B_2x2_Round: [10, 10, 6], + B_2x2_Slipper: [10, 10, 6], + B_2x2F_Octo: [10, 10, 2], + B_2x2F_Octo_Converter: [10, 10, 2], + B_2x2F_Octo_Converter_Inv: [10, 10, 2], + B_2x2f_Plate_Center: [10, 10, 2], + B_2x2f_Plate_Center_Inv: [10, 10, 2], + B_2x2F_Round: [10, 10, 2], + B_2x4_Door_Frame: [10, 20, 36], + B_2x_Octo: [10, 10, 10], + B_2x_Octo_90Deg: [10, 10, 10], + B_2x_Octo_90Deg_Inv: [10, 10, 10], + B_2x_Octo_Cone: [10, 10, 10], + B_2x_Octo_T: [10, 10, 10], + B_2x_Octo_T_Inv: [10, 10, 10], + B_4x4_Round: [20, 20, 6], + B_8x8_Lattice_Plate: [40, 40, 2], + B_Bishop: [5, 5, 10], + B_Bone: [10, 10, 2], + B_BoneStraight: [10, 5, 2], + B_Branch: [20, 11, 2], + B_Bush: [15, 15, 16], + B_Cauldron: [10, 10, 10], + B_Chalice: [5, 5, 10], + B_CheckPoint: [20, 20, 2], + B_Coffin: [5, 20, 30], + B_Coffin_Lid: [10, 20, 30], + B_Fern: [10, 10, 2], + B_Flame: [5, 5, 10], + B_Flower: [10, 10, 2], + B_Gravestone: [10, 20, 20], + B_Handle: [5, 10, 6], + B_Hedge_1x1: [5, 5, 6], + B_Hedge_1x1_Corner: [5, 5, 6], + B_Hedge_1x2: [5, 10, 6], + B_Hedge_1x4: [5, 20, 6], + B_Inverted_Cone: [5, 5, 8], + B_Jar: [9, 9, 12], + B_King: [5, 5, 12], + B_Knight: [5, 5, 8], + B_Ladder: [12, 15, 12], + B_Pawn: [5, 5, 6], + B_Picket_Fence: [20, 5, 12], + B_Pine_Tree: [20, 20, 38], + B_Pumpkin: [10, 10, 7], + B_Pumpkin_Carved: [10, 10, 7], + B_Queen: [5, 5, 12], + B_Rook: [5, 5, 6], + B_Sausage: [10, 5, 2], + B_Small_Flower: [5, 5, 2], + B_SpawnPoint: [20, 20, 2], + B_Swirl_Plate: [5, 5, 4], + B_Turkey_Body: [15, 10, 7], + B_Turkey_Leg: [10, 5, 3], + B_Leaf_Bush: [26, 26, 20], + B_GoalPoint: [20, 20, 2], +}; + +export { + rotationTable, + translationTable, + orientationMap, + DEFAULT_MATERIALS, + brickSizeMap, +}; + +export function checkBound( + brick: Brick, + brick_assets: string[], + bounds: BrickBounds, + axis: number +) { + const scaleAxis = getScaleAxis(brick, axis); + const size = getBrickSize(brick, brick_assets); + const upper = brick.position[axis] + size[scaleAxis]; + const lower = brick.position[axis] - size[scaleAxis]; + return upper <= bounds.maxBound[axis] && lower >= bounds.minBound[axis]; +} + +// check if the brick is in bounds +export function checkBounds( + brick: Brick, + brick_assets: string[], + bounds: BrickBounds +) { + return ( + checkBound(brick, brick_assets, bounds, 0) && + checkBound(brick, brick_assets, bounds, 1) && + checkBound(brick, brick_assets, bounds, 2) + ); +} + +// compare bound to see if it is new min or max, and then replace if it is +function minMaxBound( + brick: Brick, + brick_assets: string[], + bounds: BrickBounds, + axis: number +) { + const scaleAxis = getScaleAxis(brick, axis); + const size = getBrickSize(brick, brick_assets); + const upper = brick.position[axis] + size[scaleAxis]; + const lower = brick.position[axis] - size[scaleAxis]; + if (upper > bounds.maxBound[axis] || bounds.maxBound[axis] === undefined) + bounds.maxBound[axis] = upper; + if (lower < bounds.minBound[axis] || bounds.minBound[axis] === undefined) + bounds.minBound[axis] = lower; +} + +// returns bounds of the array of the brick +export function getBounds({ bricks, brick_assets }: WriteSaveObject) { + const bounds: BrickBounds = { + minBound: [], + maxBound: [], + center: [], + } as unknown as BrickBounds; + + bricks.forEach(brick => { + minMaxBound(brick, brick_assets, bounds, 0); + minMaxBound(brick, brick_assets, bounds, 1); + minMaxBound(brick, brick_assets, bounds, 2); + }); + + // calculate center from min and max bounds + bounds.center = bounds.minBound.map((min, index) => { + const max = bounds.maxBound[index]; + const avg = (max + min) / 2; + return Math.round(avg); + }) as [number, number, number]; + + return bounds; +} + +export function getBrickSize(brick: Brick, brick_assets: string[]) { + const asset = brick_assets[brick.asset_name_index]; + return brickSizeMap[asset] || brick.size; +} + +export function getScaleAxis(brick: Brick, axis: number) { + const { direction, rotation } = brick; + if ([0, 1].includes(direction)) { + if (axis === 0) { + axis = 2; + } else if (axis === 2) { + axis = 0; + } + } else if ([2, 3].includes(direction)) { + if (axis === 0) { + axis = 1; + } else if (axis === 1) { + axis = 2; + } else if (axis === 2) { + axis = 0; + } + } + + if ([1, 3].includes(rotation)) { + if (axis === 0) { + axis = 1; + } else if (axis === 1) { + axis = 0; + } + } + + return axis; +} + +const d2o = (direction: number, rotation: number) => + (direction << 2) | rotation; +const o2d = (orientation: number) => [(orientation >> 2) % 6, orientation & 3]; +const rotateDirection = (a: [number, number], b: [number, number]) => + o2d(rotationTable[d2o(...a) * 24 + d2o(...b)]); + +// Rotate a brick on its axis +const rotate = (brick: Brick, rotation: [number, number]) => { + // copy the brick + brick = { ...brick }; + // use default values if none exist + const { direction: brick_direction = 4, rotation: brick_rotation = 0 } = + brick; + const [d, r] = rotateDirection([brick_direction, brick_rotation], rotation); + brick.direction = d; + brick.rotation = r; + brick.position = translationTable[d2o(...rotation)](brick.position); + + return brick; +}; + +function repeat(times: number, fn: (o: T) => T) { + return (obj: T) => { + for (let i = 0; i < times; i++) { + obj = fn(obj); + } + return obj; + }; +} + +const rotate_x = (times: number) => + repeat(times, (brick: Brick) => rotate(brick, [3, 3])); +const rotate_y = (times: number) => + repeat(times, (brick: Brick) => rotate(brick, [1, 0])); +const rotate_z = (times: number) => + repeat(times, (brick: Brick) => rotate(brick, [4, 1])); + +export { rotate, rotate_x, rotate_y, rotate_z, d2o, o2d }; diff --git a/src/index.ts b/src/index.ts index a6db863..e8bda54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,13 @@ import read from './read'; import write from './write'; import * as utils from './utils'; import * as constants from './constants'; +import * as helper from './helper'; +export * from './octree'; export * from './types'; // https://i.imgur.com/cv1fDWs.png -const brs = { read, write, utils, constants }; -export { read, write, utils, constants }; +const brs = { read, write, utils, constants, helper }; +export { read, write, utils, constants, helper }; export default brs; declare global { diff --git a/src/octree.ts b/src/octree.ts new file mode 100644 index 0000000..2a84d92 --- /dev/null +++ b/src/octree.ts @@ -0,0 +1,448 @@ +import { getBrickSize, getScaleAxis } from './helper'; +import { Brick, Vector } from './types'; + +const CHUNK_SIZE = 1024; +const RIGHT = 1; +const BACK = 2; +const BOTTOM = 4; + +// a point in space +export class Point { + x: number; + y: number; + z: number; + + static fromVector(vec: Vector) { + return new Point(...vec); + } + + constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } + + static rect( + x: number, + y: number, + z: number, + w: number, + h: number, + d: number + ): [Point, Point] { + return [ + new Point(x - w / 2, y - h / 2, z - h / 2), + new Point(x + w / 2, y + h / 2, z + h / 2), + ]; + } + + // get a chunk from a point + getChunk() { + return new Point( + Math.floor(this.x / CHUNK_SIZE), + Math.floor(this.y / CHUNK_SIZE), + Math.floor(this.z / CHUNK_SIZE) + ); + } + + // returns true if this point is between the other two + in(min: Point, max: Point): boolean { + return ( + min.x <= this.x && + this.x <= max.x && + min.y <= this.y && + this.y <= max.y && + min.z <= this.z && + this.z <= max.z + ); + } + + // get the octant the point should be in for a node + getOctant(point: Point): number { + return ( + (point.x >= this.x ? RIGHT : 0) | + (point.y >= this.y ? BACK : 0) | + (point.z >= this.z ? BOTTOM : 0) + ); + } + + // get the middle of a chunk + getChunkMidpoint(): Point { + return new Point( + this.x * CHUNK_SIZE + CHUNK_SIZE / 2, + this.y * CHUNK_SIZE + CHUNK_SIZE / 2, + this.z * CHUNK_SIZE + CHUNK_SIZE / 2 + ); + } + + // compare points + eq(point: Point): boolean { + return this.x == point.x && this.y == point.y && this.z == point.z; + } + + // return a copy of this point shifted + shifted(x: number, y: number, z: number): Point { + return new Point(this.x + x, this.y + y, this.z + z); + } + + // stringified points are + toString(): string { + return `<${this.x}, ${this.y}, ${this.z}>`; + } +} + +// a node in the tree +export class Node { + point: Point; + depth: number; + value: T; + nodes: Node[]; + half: number; + chunk?: Point; + + constructor(point: Point, depth: number, value: T) { + this.point = point; + this.depth = depth; + this.value = value; + this.nodes = []; + this.half = Math.pow(2, this.depth - 1); + } + + // determine if provided bounds perfectly completely cover this node + wouldFillNode(min: Point, max: Point) { + const size = 1 << this.depth; + return ( + max.x - min.x == size && max.y - min.y == size && max.z - min.z == size + ); + } + + // true if this node is contained by the bounds + isInside(min: Point, max: Point) { + // check if this bounds are entirely within + return ( + this.point.x - this.half >= min.x && + this.point.x + this.half <= max.x && + this.point.y - this.half >= min.y && + this.point.y + this.half <= max.y && + this.point.z - this.half >= min.z && + this.point.z + this.half <= max.z + ); + } + + isOutside(min: Point, max: Point) { + return ( + this.point.x + this.half <= min.x || + this.point.x - this.half >= max.x || + this.point.y + this.half <= min.y || + this.point.y - this.half >= max.y || + this.point.z + this.half <= min.z || + this.point.z - this.half >= max.z + ); + } + + // if every child node has the same value, delete them + reduce() { + if (this.nodes.length === 0) return; + + let ok = true; + + // reduce all the nodes to values + // check if the other 7 nodes have the same value as the first one + for (let i = 0; i < 8; i++) { + // attempt to reduce this node + this.nodes[i].reduce(); + if ( + this.nodes[0].value !== this.nodes[i].value || + this.nodes[i].nodes.length > 0 + ) { + ok = false; + } + } + + if (ok) { + // delete the old nodes + this.value = this.nodes[0].value; + this.nodes = []; + } + } + + // insert an area into the tree + insert(value: T, minBound: Point, maxBound: Point) { + if (this.isInside(minBound, maxBound)) { + this.value = value; + return; + } + + if (this.depth === 0) return; + + // populate nodes if it's empty + if (this.nodes.length === 0) { + // decrease depth + const childDepth = this.depth - 1; + // the shift is half of the child's size + const childShift = this.half / 2; + // create new nodes in each 8 child octants of this node + this.nodes = [ + new Node( + this.point.shifted(-childShift, -childShift, -childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(childShift, -childShift, -childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(-childShift, childShift, -childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(childShift, childShift, -childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(-childShift, -childShift, childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(childShift, -childShift, childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(-childShift, childShift, childShift), + childDepth, + this.value + ), + new Node( + this.point.shifted(childShift, childShift, childShift), + childDepth, + this.value + ), + ]; + } + + for (const n of this.nodes) { + if (!n.isOutside(minBound, maxBound)) n.insert(value, minBound, maxBound); + } + } + + // search an area + search(minBound: Point, maxBound: Point, set: Set) { + if (!set) set = new Set(); + // if there's no nodes... + if (this.nodes.length === 0) { + // add this value, this node would only have come up if it was within bounds + set.add(this.value); + return; + } + + // search children + // // const halfSize = Math.pow(2, this.depth - 2); + for (const n of this.nodes) { + // if the bounds are not outside of this node, search + if (!n.isOutside(minBound, maxBound)) { + n.search(minBound, maxBound, set); + } + } + } + + // get the value at this point + get(point: Point): T { + if (this.nodes.length === 0) return this.value; + return this.nodes[this.point.getOctant(point)].get(point); + } +} + +export class ChunkTree { + chunks: Node[]; + fill: T; + + constructor(fill: T = undefined) { + this.chunks = []; + // empty nodes have this value + this.fill = fill; + } + + // reduce all chunks + reduce() { + for (const chunk of this.chunks) { + chunk.reduce(); + } + } + + // iterate across chunks with bounds and run the fn with those bounds + iterChunksFromBounds( + minBound: Point, + maxBound: Point, + fn: (min: Point, max: Point) => void + ) { + // if the boundaries are in different chunks, split the area up by chunk + const minChunk = minBound.getChunk(); + const maxChunk = maxBound.shifted(-1, -1, -1).getChunk(); + if ( + minChunk.x > maxChunk.x || + minChunk.y > maxChunk.y || + minChunk.z > maxChunk.z + ) + throw 'max chunk too small'; + + if (!minChunk.eq(maxChunk)) { + /*// insert in all chunks that overlap + for (let x = minChunk.x; x <= maxChunk.x; ++x) { + for (let y = minChunk.y; y <= maxChunk.y; ++y) { + for (let z = minChunk.z; z <= maxChunk.z; ++z) { + fn(minBound, maxBound); + } + } + }*/ + for (let x = minChunk.x; x <= maxChunk.x; ++x) { + // determine the min and max bounds for this chunk + // these should always be within the same chunk + // and should cap out at the max bound's position + const minX = x === minChunk.x ? minBound.x : x * CHUNK_SIZE; + const maxX = Math.min( + Math.floor(minX / CHUNK_SIZE + 1) * CHUNK_SIZE, + maxBound.x + ); + + for (let y = minChunk.y; y <= maxChunk.y; ++y) { + // same thing as above but for y + const minY = y === minChunk.y ? minBound.y : y * CHUNK_SIZE; + const maxY = Math.min( + Math.floor(minY / CHUNK_SIZE + 1) * CHUNK_SIZE, + maxBound.y + ); + + for (let z = minChunk.z; z <= maxChunk.z; ++z) { + // same thing as above but for z + const minZ = z === minChunk.z ? minBound.z : z * CHUNK_SIZE; + const maxZ = Math.min( + Math.floor(minZ / CHUNK_SIZE + 1) * CHUNK_SIZE, + maxBound.z + ); + + // run the fn on the new single-chunk bounds + fn(new Point(minX, minY, minZ), new Point(maxX, maxY, maxZ)); + } + } + } + return; + } // otherwise the boundaries are in the same chunk so it's okay to run the function + + fn(minBound, maxBound); + } + + // get a chunk at a point + getChunkAt(point: Point, create = false) { + const chunkPos = point.getChunk(); + + // find the the corresponding chunk octtree + let chunk = this.chunks.find(c => c.chunk.eq(chunkPos)); + if (!chunk && create) { + // create a new chunk because one does not exist + chunk = new Node(chunkPos.getChunkMidpoint(), 10, this.fill); + chunk.chunk = chunkPos; + this.chunks.push(chunk); + } + + return chunk; + } + + // search all values in an area + search(minBound: Point, maxBound: Point) { + // the set of all result values + const results = new Set(); + + // run the following code in the chunks covered by these boundaries: + this.iterChunksFromBounds(minBound, maxBound, (min, max) => { + const chunk = this.getChunkAt(min); + if (chunk) { + // search the chunk if it exists + chunk.search(min, max, results); + } + }); + + // remove the empty item1 + results.delete(this.fill); + + return results; + } + + // get the value at a point + get(point: Point) { + const chunk = this.getChunkAt(point); + if (chunk) { + // search the chunk if it exists + return chunk.get(point); + } else { + return this.fill; + } + } + + // insert an area into chunks + insert(value: T, minBound: Point, maxBound: Point) { + // run the following code in the chunks covered by these boundaries: + this.iterChunksFromBounds(minBound, maxBound, (min, max) => { + // insert the value into the oct tree + this.getChunkAt(min, true).insert(value, min, max); + }); + } +} + +export class SaveOctree { + brick_assets: string[]; + bricks: Brick[]; + tree: ChunkTree; + + constructor(bricks: Brick[], brick_assets: string[]) { + this.brick_assets = brick_assets; + this.bricks = bricks; + this.tree = new ChunkTree(-1); + + for (let i = 0; i < this.bricks.length; i++) { + const brick = this.bricks[i]; + // get normalized sizes for every brick + const normal_size = getBrickSize(brick as Brick, this.brick_assets); + const size = [ + normal_size[getScaleAxis(brick as Brick, 0)], + normal_size[getScaleAxis(brick as Brick, 1)], + normal_size[getScaleAxis(brick as Brick, 2)], + ]; + + // build boundaries from the normalized size + const bounds = { + min: new Point( + brick.position[0] - size[0], + brick.position[1] - size[1], + brick.position[2] - size[2] + ), + max: new Point( + brick.position[0] + size[0], + brick.position[1] + size[1], + brick.position[2] + size[2] + ), + }; + // const normal_size = size; + + // add it into the tree + this.tree.insert(i, bounds.min, bounds.max); + } + + this.tree.reduce(); + } + + get(vec: Vector): Brick | null { + return this.bricks[this.tree.get(Point.fromVector(vec))] ?? null; + } + + search(min: Vector, max: Vector): Brick[] { + return Array.from( + this.tree.search(Point.fromVector(min), Point.fromVector(max)) + ).map(i => this.bricks[i]); + } +}