Skip to content

Commit 24d88e4

Browse files
committed
add octree and i3 packages
1 parent 593aa06 commit 24d88e4

File tree

4 files changed

+545
-0
lines changed

4 files changed

+545
-0
lines changed

i3/icube.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package i3
2+
3+
// Cube implements a tree cube for Octree algorithms.
4+
type Cube struct {
5+
Vec
6+
// Lvl keeps track of the level in the tree.
7+
// - Lvl==1 means the cube is the smallest possible cube.
8+
// - Lvl==0 is an invalid level. May be used as a flag to signal the cube has been discarded or processed and ready for discard.
9+
Lvl int
10+
}
11+
12+
// IsSmallest returns true if Lvl==1. This means the cube cannot be decomposed further with [Cube.Octree].
13+
func (c Cube) IsSmallest() bool { return c.Lvl == 1 }
14+
15+
// IsSecondSmallest returns true if Lvl==2. This means the cube can be decomposed once more with [Cube.Octree].
16+
func (c Cube) IsSecondSmallest() bool { return c.Lvl == 2 }
17+
18+
// DecomposesTo returns the amount of cubes generated from decomposing the cube down to cubes of the argument target level.
19+
func (c Cube) DecomposesTo(targetLvl int) uint64 {
20+
if targetLvl > c.Lvl {
21+
panic("invalid targetLvl to icube.decomposesTo")
22+
}
23+
return Pow8(c.Lvl - targetLvl)
24+
}
25+
26+
// Size returns the length of one of the icube's sides.
27+
func (c Cube) Size() (resUnits int) {
28+
return 1 << (c.Lvl - 1)
29+
}
30+
31+
// Supercube returns the ICube3's parent octree ICube3.
32+
func (c Cube) Supercube() Cube {
33+
upLvl := c.Lvl + 1
34+
bitmask := (1 << upLvl) - 1
35+
return Cube{
36+
Vec: c.Vec.AndnotScalar(bitmask),
37+
Lvl: upLvl,
38+
}
39+
}
40+
41+
// Index returns the indices corresponding to the ICube3 in the root cube.
42+
// By multiplying the resulting indices by the smallest cube size one can obtain the origin of the ICube in space.
43+
func (c Cube) Index() Vec {
44+
return c.Vec.ShiftRight(c.Lvl) // icube indices per level in the octree.
45+
}
46+
47+
// Octree returns the 8 sub-cubes of the receiver.
48+
func (c Cube) Octree() [8]Cube {
49+
lvl := c.Lvl - 1
50+
if lvl <= 0 {
51+
panic("invalid operation: octree for level<=1")
52+
}
53+
s := 1 << lvl
54+
return [8]Cube{
55+
{Vec: c.Add(Vec{0, 0, 0}), Lvl: lvl},
56+
{Vec: c.Add(Vec{s, 0, 0}), Lvl: lvl},
57+
{Vec: c.Add(Vec{s, s, 0}), Lvl: lvl},
58+
{Vec: c.Add(Vec{0, s, 0}), Lvl: lvl},
59+
{Vec: c.Add(Vec{0, 0, s}), Lvl: lvl},
60+
{Vec: c.Add(Vec{s, 0, s}), Lvl: lvl},
61+
{Vec: c.Add(Vec{s, s, s}), Lvl: lvl},
62+
{Vec: c.Add(Vec{0, s, s}), Lvl: lvl},
63+
}
64+
}
65+
66+
// Pow8 returns 8**y.
67+
func Pow8(y int) uint64 {
68+
if y < len(_pow8) {
69+
return _pow8[y]
70+
}
71+
panic("overflow Pow8")
72+
}
73+
74+
// Pow4 returns 4**y.
75+
func Pow4(y int) uint64 {
76+
if y < len(_pow4) {
77+
return _pow4[y]
78+
}
79+
panic("overflow Pow4")
80+
}
81+
82+
var _pow8 = [...]uint64{
83+
0: 1,
84+
1: 8,
85+
2: 8 * 8,
86+
3: 8 * 8 * 8,
87+
4: 8 * 8 * 8 * 8,
88+
5: 8 * 8 * 8 * 8 * 8,
89+
6: 8 * 8 * 8 * 8 * 8 * 8,
90+
7: 8 * 8 * 8 * 8 * 8 * 8 * 8,
91+
8: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
92+
9: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
93+
10: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
94+
11: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
95+
12: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
96+
13: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
97+
14: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
98+
15: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
99+
16: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
100+
17: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
101+
18: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
102+
19: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
103+
20: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
104+
21: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8,
105+
// 22: 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8, // overflows
106+
}
107+
108+
var _pow4 = [...]uint64{
109+
0: 1,
110+
1: 4,
111+
2: 4 * 4,
112+
3: 4 * 4 * 4,
113+
4: 4 * 4 * 4 * 4,
114+
5: 4 * 4 * 4 * 4 * 4,
115+
6: 4 * 4 * 4 * 4 * 4 * 4,
116+
7: 4 * 4 * 4 * 4 * 4 * 4 * 4,
117+
8: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
118+
9: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
119+
10: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
120+
11: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
121+
12: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
122+
13: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
123+
14: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
124+
15: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
125+
16: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
126+
17: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
127+
18: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
128+
19: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
129+
20: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
130+
21: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
131+
22: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
132+
23: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
133+
24: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
134+
25: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
135+
26: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
136+
27: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
137+
28: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
138+
29: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
139+
30: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4,
140+
// 31: 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4 * 4, // overflows
141+
}

i3/ivec.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package i3
2+
3+
type Vec struct {
4+
X int
5+
Y int
6+
Z int
7+
}
8+
9+
func (a Vec) Add(b Vec) Vec { return Vec{X: a.X + b.X, Y: a.Y + b.Y, Z: a.Z + b.Z} }
10+
func (a Vec) Sub(v Vec) Vec { return Vec{X: a.X - v.X, Y: a.Y - v.Y, Z: a.Z - v.Z} }
11+
12+
func (a Vec) AddScalar(v int) Vec { return Vec{X: a.X + v, Y: a.Y + v, Z: a.Z + v} }
13+
func (a Vec) MulScalar(v int) Vec { return Vec{X: a.X * v, Y: a.Y * v, Z: a.Z * v} }
14+
func (a Vec) DivScalar(v int) Vec { return Vec{X: a.X / v, Y: a.Y / v, Z: a.Z / v} }
15+
16+
func (a Vec) ShiftRight(lo int) Vec { return Vec{X: a.X >> lo, Y: a.Y >> lo, Z: a.Z >> lo} }
17+
func (a Vec) ShiftLeft(hi int) Vec { return Vec{X: a.X << hi, Y: a.Y << hi, Z: a.Z << hi} }
18+
19+
func (a Vec) AndScalar(b int) Vec { return Vec{X: a.X & b, Y: a.Y & b, Z: a.Z & b} }
20+
func (a Vec) OrScalar(b int) Vec { return Vec{X: a.X | b, Y: a.Y | b, Z: a.Z | b} }
21+
func (a Vec) XorScalar(b int) Vec { return Vec{X: a.X ^ b, Y: a.Y ^ b, Z: a.Z ^ b} }
22+
func (a Vec) AndnotScalar(b int) Vec { return Vec{X: a.X &^ b, Y: a.Y &^ b, Z: a.Z &^ b} }

md3/octree.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// DO NOT EDIT.
2+
// This file was generated automatically
3+
// from gen.go. Please do not edit this file.
4+
5+
package md3
6+
7+
import (
8+
"github.com/soypat/geometry/i3"
9+
)
10+
11+
// Octree implements heapless 3D spatial Octree algorithms.
12+
type Octree struct {
13+
// Resolution is the size of the smallest cube in the Octree. [i3.Cube].Lvl==0.
14+
Resolution float64
15+
// Origin represents the position of the first cube's first corner (most negative/smallest corner).
16+
Origin Vec
17+
}
18+
19+
// DecomposeDFS decomposes icubes from the end of cubes into their octree sub-icubes
20+
// and appends them to the cubes buffer, resulting in a depth-first traversal (DFS) of the octree.
21+
// This way cubes will contain the largest cubes at the start (low index) and the smallest cubes at the end (high index).
22+
// Cubes that reach the smallest size will be consumed and their 3D corners appended to dst. Smallest size cubes do not decompose into more icubes.
23+
// cubes with level of zero are discarded and no action is taken.
24+
//
25+
// The icube decomposition continues until one or more of the following conditions are met:
26+
// - Smallest cube size is reached and the capacity in 3D dst can't store a resolution sized icube corners, calculated as cap(dst)-len(dst) < 64.
27+
// - Need to decompose a icube to more icubes but capacity of cubes buffer not enough to store an octree decomposition, calculated as cap(cubes)-len(cubes) < 8.
28+
// - cubes buffer has been fully consumed and is empty, calculated as len(cubes) == 0.
29+
//
30+
// This algorithm is HEAPLESS: this means dst and cubes buffer capacities are not modified.
31+
func (oct Octree) DecomposeDFS(dst []Vec, cubes []i3.Cube) ([]Vec, []i3.Cube) {
32+
for len(cubes) > 0 {
33+
lastIdx := len(cubes) - 1
34+
cube := cubes[lastIdx]
35+
if cube.Lvl == 0 {
36+
// Cube has been moved to prune queue. Discard and keep going.
37+
cubes = cubes[:lastIdx]
38+
continue
39+
}
40+
if cube.IsSecondSmallest() {
41+
// Is base-level cube.
42+
if cap(dst)-len(dst) < 8*8 {
43+
break // No space for position buffering.
44+
}
45+
subcubes := cube.Octree()
46+
for _, scube := range subcubes {
47+
corners := oct.CubeCorners(scube, oct.Resolution)
48+
// corners := scube.(origin, res)
49+
dst = append(dst, corners[:]...)
50+
}
51+
cubes = cubes[:lastIdx] // Trim cube used.
52+
53+
} else {
54+
// Is cube with sub-cubes.
55+
if cap(cubes)-len(cubes) < 8 {
56+
break // No more space for cube buffering.
57+
}
58+
subcubes := cube.Octree()
59+
// We trim off the last cube which we just processed in append.
60+
cubes = append(cubes[:lastIdx], subcubes[:]...)
61+
}
62+
}
63+
return dst, cubes
64+
}
65+
66+
// DecomposeBFS decomposes start into octree cubes and appends them to dst without surpassing dst's slice capacity
67+
// and continues to decompose the resulting cubes until all cubes are minimumDecomposedLvl or dst capacity reached.
68+
// Smallest cubes will remain at the highest index of dst. The boolean value returned indicates whether the
69+
// argument start icube was able to be decomposed and its children added to dst.
70+
func (oct Octree) DecomposeBFS(dst []i3.Cube, start i3.Cube, minimumDecomposedLvl int) ([]i3.Cube, bool) {
71+
if minimumDecomposedLvl < 1 {
72+
panic("bad minimumDecomposedLvl")
73+
}
74+
if cap(dst) < 8 {
75+
return dst, false // No space to decompose new cubes.
76+
} else if start.Lvl <= minimumDecomposedLvl {
77+
return dst, false // Cube already fully decomposed.
78+
}
79+
80+
subCubes := start.Octree()
81+
startIdx := len(dst)
82+
firstIdx := len(dst)
83+
dst = append(dst, subCubes[:]...) // Cubes will be of at minimum minLvl-1
84+
for cap(dst)-len(dst) >= 8 {
85+
// Decompose and append cubes.
86+
cube := dst[firstIdx]
87+
if cube.Lvl <= minimumDecomposedLvl {
88+
// Reached cube of minimum prunable level.
89+
break
90+
}
91+
subCubes := cube.Octree()
92+
// Is cube with sub-cubes.
93+
// We trim off the last cube which we just processed in append.
94+
dst = append(dst, subCubes[:]...)
95+
firstIdx++
96+
}
97+
// Move cubes to start of buffer from where we started consuming them.
98+
n := copy(dst[startIdx:], dst[firstIdx:])
99+
dst = dst[:startIdx+n]
100+
return dst, true
101+
}
102+
103+
// SafeMove appends cubes from the end of src to dst while taking care
104+
// not to leave dst without space to decompose to smallest cube level using DFS.
105+
// Cubes appended to dst from src are removed from src.
106+
func (oct Octree) SafeMove(dst, src []i3.Cube) (newDst, newSrc []i3.Cube) {
107+
if len(src) == 0 {
108+
return dst, src
109+
}
110+
// Calculate amount of cubes that would be generated in DFS
111+
srcGenCubes := 8 * (src[0].Lvl + 1) // TODO(soypat): Checking the first cube is very (read as "too") conservative.
112+
neededSpace := 1 + srcGenCubes // plus one for appended cube.
113+
// Calculate free space in dst after cubes generated by 1 decomposition+append.
114+
free := cap(dst) - neededSpace
115+
trimIdx := max(0, len(src)-free)
116+
prevCap := cap(dst)
117+
dst = append(dst, src[trimIdx:]...)
118+
if cap(dst) != prevCap {
119+
panic("heapless assumption broken")
120+
}
121+
src = src[:trimIdx]
122+
return dst, src
123+
}
124+
125+
// SafeSpread takes cube swith Lvl>0 from end of src and "spreads" them over dstWithLvl0 cube buffer taking special care so that the buffer can still be decomposed to smallest cubes.
126+
// The buffer dstWithLvl0 is considered to have exactly numLvl0 cubes with Lvl==0 anywhere within. These Lvl==0 cubes will be replaced
127+
// with src cubes first.
128+
func (oct Octree) SafeSpread(dstWithLvl0, src []i3.Cube, numLvl0 int) (newDst, newSrc []i3.Cube, newNumLvl0 int) {
129+
if len(src) == 0 || numLvl0 == 0 || len(dstWithLvl0) == 0 {
130+
return dstWithLvl0, src, numLvl0 // No work to do.
131+
}
132+
srcIdx := len(src) - 1 // Start appending from end of src.
133+
cube := src[srcIdx]
134+
neededSpace := 8*cube.Lvl + 1
135+
for i := 0; numLvl0 > 0 && i < len(dstWithLvl0); i++ {
136+
free := cap(dstWithLvl0) - i
137+
if free < neededSpace {
138+
break // If we add this cube we'd overflow the target buffer upon DFS decomposition, so don't.
139+
}
140+
// Look for zero level cubes (invalid/empty/discarded).
141+
if dstWithLvl0[i].Lvl != 0 {
142+
continue
143+
} else if cube.Lvl == 0 {
144+
panic("bad src cube in octreeSafeSpread")
145+
}
146+
// Calculate free space.
147+
dstWithLvl0[i] = cube
148+
numLvl0--
149+
srcIdx--
150+
if srcIdx < 0 {
151+
break // Done processing cubes.
152+
}
153+
cube = src[srcIdx]
154+
neededSpace = 8*cube.Lvl + 1
155+
}
156+
return dstWithLvl0, src[:srcIdx+1], numLvl0
157+
}
158+
159+
// CubeCorners returns the corners of the cube starting with the z=0 xy plane corners.
160+
// cubeSize should be the result of [Octree.CubeSize] called on c. It is left to the user to do for performance reasons.
161+
func (oct Octree) CubeCorners(c i3.Cube, cubeSize float64) [8]Vec {
162+
origin := oct.CubeOrigin(c, cubeSize)
163+
return [8]Vec{
164+
Add(origin, Vec{X: 0, Y: 0, Z: 0}),
165+
Add(origin, Vec{X: cubeSize, Y: 0, Z: 0}),
166+
Add(origin, Vec{X: cubeSize, Y: cubeSize, Z: 0}),
167+
Add(origin, Vec{X: 0, Y: cubeSize, Z: 0}),
168+
Add(origin, Vec{X: 0, Y: 0, Z: cubeSize}),
169+
Add(origin, Vec{X: cubeSize, Y: 0, Z: cubeSize}),
170+
Add(origin, Vec{X: cubeSize, Y: cubeSize, Z: cubeSize}),
171+
Add(origin, Vec{X: 0, Y: cubeSize, Z: cubeSize}),
172+
}
173+
}
174+
175+
// CubeOrigin returns the Cube argument origin (lowest index corner position) in the octree.
176+
// cubeSize should be the result of [Octree.CubeSize] called on c. It is left to the user to do for performance reasons.
177+
func (oct Octree) CubeOrigin(c i3.Cube, cubeSize float64) Vec {
178+
idx := c.Index()
179+
return Add(oct.Origin, Scale(cubeSize, Vec{X: float64(idx.X), Y: float64(idx.Y), Z: float64(idx.Z)}))
180+
}
181+
182+
// CubeSize returns the length of the sides of the cube.
183+
func (oct Octree) CubeSize(c i3.Cube) float64 {
184+
dim := 1 << (c.Lvl - 1)
185+
return float64(dim) * oct.Resolution
186+
}
187+
188+
func max(a, b int) int {
189+
if a > b {
190+
return a
191+
}
192+
return b
193+
}

0 commit comments

Comments
 (0)