Skip to content

Commit dbcdf62

Browse files
committed
Convert csg.js from JavaScript to Go
1 parent 5be14a7 commit dbcdf62

File tree

13 files changed

+761
-2
lines changed

13 files changed

+761
-2
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
MIT License
22

3+
Copyright (c) 2021 René Post (https://github.com/reactivego)
34
Copyright (c) 2011 Evan Wallace (http://madebyevan.com/)
4-
Copyright (c) 2021 René Post
55

66
Permission is hereby granted, free of charge, to any person obtaining a copy
77
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,86 @@
11
# csg
22

3-
Constructive solid geometry on meshes using BSP trees in Go
3+
import "github.com/reactivego/csg"
4+
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/reactivego/csg.svg)](https://pkg.go.dev/github.com/reactivego/csg#section-documentation)
6+
7+
![Example](../assets/example.svg)
8+
9+
Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean operations like union and intersection to combine 3D solids. This library implements CSG operations on meshes elegantly and concisely using BSP trees, and is meant to serve as an easily understandable implementation of the algorithm. All edge cases involving overlapping coplanar polygons in both solids are correctly handled.
10+
11+
Example usage:
12+
``` go
13+
import "github.com/reactivego/csg"
14+
15+
cube := csg.Cube()
16+
sphere := csg.Sphere(csg.Radius(1.3))
17+
polygons := cube.Subtract(sphere).Polygons
18+
```
19+
20+
## Operations
21+
22+
This library provides three CSG operations: union, subtract, and intersect.
23+
The operations are rendered below.
24+
25+
| ![Cube](../assets/cube.svg) | ![Sphere](../assets/sphere.svg) |
26+
|:---:|:---:|
27+
| `a` | `b` |
28+
29+
30+
The solids `a` and `b` were generated with the following code:
31+
32+
```go
33+
import . "github.com/reactivego/csg"
34+
35+
a := Cube(Center(-0.25, -0.25, -0.25))
36+
b := Sphere(Center(0.25, 0.25, 0.25), Radius(1.3))
37+
```
38+
39+
| ![Union](../assets/union.svg) | ![Subtract](../assets/subtract.svg) | ![Intersect](../assets/intersect.svg) |
40+
|:---:|:---:|:---:|
41+
| `a.Union(b)`| `a.Subtract(b)` | `a.Intersect(b)` |
42+
43+
44+
45+
## Implementation Details
46+
47+
All CSG operations are implemented in terms of two functions, `ClipTo()` and
48+
`Invert()`, which remove parts of a BSP tree inside another BSP tree and swap
49+
solid and empty space, respectively. Tob find the union of `a` and `b`, we
50+
want to remove everything in `a` inside `b` and everything in `b` inside `a`,
51+
then combine polygons from `a` and `b` into one solid:
52+
53+
``` go
54+
a.ClipTo(b)
55+
b.ClipTo(a)
56+
a.AddPolygons(b.AllPolygons())
57+
```
58+
59+
The only tricky part is handling overlapping coplanar polygons in both trees.
60+
The code above keeps both copies, but we need to keep them in one tree and
61+
remove them in the other tree. To remove them from `b` we can clip the
62+
inverse of `b` against `a`. The code for union now looks like this:
63+
64+
``` go
65+
a.ClipTo(b)
66+
b.ClipTo(a)
67+
b.Invert()
68+
b.ClipTo(a)
69+
b.Invert()
70+
a.AddPolygons(b.AllPolygons())
71+
```
72+
73+
Subtraction and intersection naturally follow from set operations. If
74+
union is `A | B`, subtraction is `A - B = ~(~A | B)` and intersection is
75+
`A & B = ~(~A | ~B)` where `~` is the complement operator.
76+
77+
## Acknowledgments
78+
79+
This package is a direct conversion of [cgs.js](http://evanw.github.io/csg.js/) to Go.
80+
The original JavaScript code was written by Evan Wallace and committed to git on November 30, 2011.
81+
As an odd coincidence, I am writing this a full 10 years later on Nov 30, 2021.
82+
83+
## License
84+
85+
This library is licensed under the terms of the MIT License.
86+
See [LICENSE](LICENSE) file for copyright notice and exact wording.

bsp.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package csg
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// BSP holds a node in a BSP tree. A BSP tree is built from a collection of
9+
// polygons by picking a polygon to split along. That polygon (and all other
10+
// coplanar polygons) are added directly to that node and the other polygons
11+
// are added to the front and/or back subtrees. This is not a leafy BSP tree
12+
// since there is no distinction between internal and leaf nodes.
13+
type BSP struct {
14+
Plane *Plane
15+
Polygons Polygons
16+
Front *BSP
17+
Back *BSP
18+
}
19+
20+
// Invert converts solid space to empty space and empty space to solid space.
21+
func (n *BSP) Invert() {
22+
for i := range n.Polygons {
23+
n.Polygons[i].Flip()
24+
}
25+
n.Plane.Flip()
26+
if n.Front != nil {
27+
n.Front.Invert()
28+
}
29+
if n.Back != nil {
30+
n.Back.Invert()
31+
}
32+
n.Front, n.Back = n.Back, n.Front
33+
}
34+
35+
// ClipPolygons recursively removes all polygons in `polygons` that are inside
36+
// this BSP tree.
37+
func (n BSP) ClipPolygons(polygons Polygons) Polygons {
38+
if n.Plane == nil {
39+
return append(Polygons(nil), polygons...)
40+
}
41+
var front, back Polygons
42+
for _, p := range polygons {
43+
n.Plane.SplitPolygon(p, &front, &back, &front, &back)
44+
}
45+
if n.Front != nil {
46+
front = n.Front.ClipPolygons(front)
47+
}
48+
if n.Back != nil {
49+
back = n.Back.ClipPolygons(back)
50+
} else {
51+
back = nil
52+
}
53+
return append(front, back...)
54+
}
55+
56+
// ClipTo removes all polygons in this BSP tree that are inside the other BSP
57+
// tree `bsp`.
58+
func (n *BSP) ClipTo(bsp *BSP) {
59+
n.Polygons = bsp.ClipPolygons(n.Polygons)
60+
if n.Front != nil {
61+
n.Front.ClipTo(bsp)
62+
}
63+
if n.Back != nil {
64+
n.Back.ClipTo(bsp)
65+
}
66+
}
67+
68+
// AllPolygons returns a list of all polygons in this BSP tree.
69+
func (n BSP) AllPolygons() Polygons {
70+
polygons := append(Polygons(nil), n.Polygons...)
71+
if n.Front != nil {
72+
polygons = append(polygons, n.Front.AllPolygons()...)
73+
}
74+
if n.Back != nil {
75+
polygons = append(polygons, n.Back.AllPolygons()...)
76+
}
77+
return polygons
78+
}
79+
80+
// AddPolygons builds a BSP tree out of `polygons`. When called on an existing
81+
// tree, the new polygons are filtered down to the bottom of the tree and become
82+
// new nodes there. Each set of polygons is partitioned using the first polygon
83+
// (no heuristic is used to pick a good split).
84+
func (n *BSP) AddPolygons(polygons Polygons) {
85+
if len(polygons) == 0 {
86+
return
87+
}
88+
if n.Plane == nil {
89+
p := polygons[0].Plane
90+
n.Plane = &p
91+
}
92+
var front, back Polygons
93+
for _, p := range polygons {
94+
n.Plane.SplitPolygon(p, &n.Polygons, &n.Polygons, &front, &back)
95+
}
96+
if len(front) > 0 {
97+
if n.Front == nil {
98+
n.Front = &BSP{}
99+
}
100+
n.Front.AddPolygons(front)
101+
}
102+
if len(back) > 0 {
103+
if n.Back == nil {
104+
n.Back = &BSP{}
105+
}
106+
n.Back.AddPolygons(back)
107+
}
108+
}
109+
110+
func (n *BSP) print(level int, sb *strings.Builder) {
111+
sb.WriteString(fmt.Sprintf("%*s%s:%+v\n", level*2, "", "plane", n.Plane.Normal))
112+
if n.Front != nil {
113+
n.Front.print(level+1, sb)
114+
}
115+
if n.Back != nil {
116+
n.Back.print(level+1, sb)
117+
}
118+
}
119+
120+
func (n *BSP) String() string {
121+
var sb strings.Builder
122+
n.print(0, &sb)
123+
return sb.String()
124+
}

cube.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package csg
2+
3+
// Cube constructs an axis-aligned solid cuboid. Optional parameters are
4+
// `Center` and `Size`, which default to `Center(0, 0, 0)` and
5+
// `Size(2, 2, 2)`. The Size is specified a list of three numbers, one for
6+
// each axis.
7+
//
8+
// Example code:
9+
//
10+
// cube := csg.Cube(
11+
// csg.Center(0, 0, 0),
12+
// csg.Size(2, 2, 2))
13+
//
14+
func Cube(options ...Option) *Solid {
15+
o := OptionsFrom(options)
16+
faces := [][]int{
17+
{0, 4, 6, 2},
18+
{1, 3, 7, 5},
19+
{0, 1, 5, 4},
20+
{2, 6, 7, 3},
21+
{0, 2, 3, 1},
22+
{4, 5, 7, 6}}
23+
normals := []Vector{
24+
{X: -1, Y: 0, Z: 0},
25+
{X: +1, Y: 0, Z: 0},
26+
{X: 0, Y: -1, Z: 0},
27+
{X: 0, Y: +1, Z: 0},
28+
{X: 0, Y: 0, Z: -1},
29+
{X: 0, Y: 0, Z: +1}}
30+
polygons := []Polygon{}
31+
c := o.Center
32+
r := o.Size.DividedBy(2)
33+
for i, corners := range faces {
34+
vertices := make([]Vertex, 0, len(corners))
35+
for _, corner := range corners {
36+
pos := Vector{
37+
X: c.X + r.X*normals[0+corner&1/1].X,
38+
Y: c.Y + r.Y*normals[2+corner&2/2].Y,
39+
Z: c.Z + r.Z*normals[4+corner&4/4].Z,
40+
}
41+
vertices = append(vertices, Vertex{Pos: pos, Normal: normals[i]})
42+
}
43+
polygons = append(polygons, PolygonFromVertices(vertices...))
44+
}
45+
return SolidFromPolygons(polygons)
46+
}

cylinder.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package csg
2+
3+
import (
4+
"math"
5+
)
6+
7+
// Cylinder constructs a solid cylinder. Optional parameters are `Start`,
8+
// `End`, `Radius`, and `Slices`, which default to `Start(0, -1, 0)`,
9+
// `End(0, 1, 0)`, `Radius(1)`, and `Slices(16)`. The `Slices` parameter
10+
// controls the tessellation.
11+
//
12+
// Example usage:
13+
//
14+
// cylinder := csg.Cylinder(
15+
// Start(0, -1, 0),
16+
// End(0, 1, 0),
17+
// Radius(1),
18+
// Slices(16))
19+
//
20+
func Cylinder(options ...Option) *Solid {
21+
o := OptionsFrom(options)
22+
s := o.Start
23+
e := o.End
24+
ray := e.Minus(s)
25+
r := o.Radius
26+
slices := float64(o.Slices)
27+
axisZ := ray.Unit()
28+
axisX := Vector{0, 1, 0}
29+
if math.Abs(axisZ.Y) > 0.5 {
30+
axisX = Vector{1, 0, 0}
31+
}
32+
axisX = axisX.Cross(axisZ).Unit()
33+
axisY := axisX.Cross(axisZ).Unit()
34+
start := Vertex{Pos: s, Normal: axisZ.Negated()}
35+
end := Vertex{Pos: e, Normal: axisZ.Unit()}
36+
point := func(stack, slice, normalBlend float64) Vertex {
37+
angle := slice * math.Pi * 2
38+
out := axisX.Times(math.Cos(angle)).Plus(axisY.Times(math.Sin(angle)))
39+
pos := s.Plus(ray.Times(stack)).Plus(out.Times(r))
40+
normal := out.Times(1 - math.Abs(normalBlend)).Plus(axisZ.Times(normalBlend))
41+
return Vertex{Pos: pos, Normal: normal}
42+
}
43+
var polygons Polygons
44+
for i := 0.0; i < slices; i++ {
45+
t0, t1 := i/slices, (i+1)/slices
46+
polygons = append(polygons, PolygonFromVertices(start, point(0, t0, -1), point(0, t1, -1)))
47+
polygons = append(polygons, PolygonFromVertices(point(0, t1, 0), point(0, t0, 0), point(1, t0, 0), point(1, t1, 0)))
48+
polygons = append(polygons, PolygonFromVertices(end, point(1, t1, 1), point(1, t0, 1)))
49+
}
50+
return SolidFromPolygons(polygons)
51+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/reactivego/csg
2+
3+
go 1.17

0 commit comments

Comments
 (0)