Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
DrawString 86ba04e Jun 14, 2017
1 contributor

Users who have contributed to this file

679 lines (637 sloc) 15.3 KB
package pinhole
import (
"fmt"
"image"
"image/color"
"image/png"
"io"
"io/ioutil"
"math"
"os"
"sort"
"strconv"
"strings"
"golang.org/x/image/font/gofont/goregular"
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"github.com/google/btree"
)
const circleSteps = 45
var gof = func() *truetype.Font {
gof, err := truetype.Parse(goregular.TTF)
if err != nil {
panic(err)
}
return gof
}()
type line struct {
x1, y1, z1 float64
x2, y2, z2 float64
nocaps bool
color color.Color
str string
scale float64
circle bool
cfirst *line
cprev *line
cnext *line
drawcoords *fourcorners
}
func (l *line) Rect() (min, max [3]float64) {
if l.x1 < l.x2 {
min[0], max[0] = l.x1, l.x2
} else {
min[0], max[0] = l.x2, l.x1
}
if l.y1 < l.y2 {
min[1], max[1] = l.y1, l.y2
} else {
min[1], max[1] = l.y2, l.y1
}
if l.z1 < l.z2 {
min[2], max[2] = l.z1, l.z2
} else {
min[2], max[2] = l.z2, l.z1
}
return
}
func (l *line) Center() []float64 {
min, max := l.Rect()
return []float64{
(max[0] + min[0]) / 2,
(max[1] + min[1]) / 2,
(max[2] + min[2]) / 2,
}
}
type Pinhole struct {
lines []*line
stack []int
}
func New() *Pinhole {
return &Pinhole{}
}
func (p *Pinhole) Begin() {
p.stack = append(p.stack, len(p.lines))
}
func (p *Pinhole) End() {
if len(p.stack) > 0 {
p.stack = p.stack[:len(p.stack)-1]
}
}
func (p *Pinhole) Rotate(x, y, z float64) {
var i int
if len(p.stack) > 0 {
i = p.stack[len(p.stack)-1]
}
for ; i < len(p.lines); i++ {
l := p.lines[i]
if x != 0 {
l.x1, l.y1, l.z1 = rotate(l.x1, l.y1, l.z1, x, 0)
l.x2, l.y2, l.z2 = rotate(l.x2, l.y2, l.z2, x, 0)
}
if y != 0 {
l.x1, l.y1, l.z1 = rotate(l.x1, l.y1, l.z1, y, 1)
l.x2, l.y2, l.z2 = rotate(l.x2, l.y2, l.z2, y, 1)
}
if z != 0 {
l.x1, l.y1, l.z1 = rotate(l.x1, l.y1, l.z1, z, 2)
l.x2, l.y2, l.z2 = rotate(l.x2, l.y2, l.z2, z, 2)
}
p.lines[i] = l
}
}
func (p *Pinhole) Translate(x, y, z float64) {
var i int
if len(p.stack) > 0 {
i = p.stack[len(p.stack)-1]
}
for ; i < len(p.lines); i++ {
p.lines[i].x1 += x
p.lines[i].y1 += y
p.lines[i].z1 += z
p.lines[i].x2 += x
p.lines[i].y2 += y
p.lines[i].z2 += z
}
}
func (p *Pinhole) Scale(x, y, z float64) {
var i int
if len(p.stack) > 0 {
i = p.stack[len(p.stack)-1]
}
for ; i < len(p.lines); i++ {
p.lines[i].x1 *= x
p.lines[i].y1 *= y
p.lines[i].z1 *= z
p.lines[i].x2 *= x
p.lines[i].y2 *= y
p.lines[i].z2 *= z
if len(p.lines[i].str) > 0 {
p.lines[i].scale *= math.Min(x, y)
}
}
}
func (p *Pinhole) Colorize(color color.Color) {
var i int
if len(p.stack) > 0 {
i = p.stack[len(p.stack)-1]
}
for ; i < len(p.lines); i++ {
p.lines[i].color = color
}
}
func (p *Pinhole) Center() {
var i int
if len(p.stack) > 0 {
i = p.stack[len(p.stack)-1]
}
minx, miny, minz := math.Inf(+1), math.Inf(+1), math.Inf(+1)
maxx, maxy, maxz := math.Inf(-1), math.Inf(-1), math.Inf(-1)
for ; i < len(p.lines); i++ {
if p.lines[i].x1 < minx {
minx = p.lines[i].x1
}
if p.lines[i].x1 > maxx {
maxx = p.lines[i].x1
}
if p.lines[i].y1 < miny {
miny = p.lines[i].y1
}
if p.lines[i].y1 > maxy {
maxy = p.lines[i].y1
}
if p.lines[i].z1 < minz {
minz = p.lines[i].z1
}
if p.lines[i].z2 > maxz {
maxz = p.lines[i].z2
}
if p.lines[i].x2 < minx {
minx = p.lines[i].x2
}
if p.lines[i].x2 > maxx {
maxx = p.lines[i].x2
}
if p.lines[i].y2 < miny {
miny = p.lines[i].y2
}
if p.lines[i].y2 > maxy {
maxy = p.lines[i].y2
}
if p.lines[i].z2 < minz {
minz = p.lines[i].z2
}
if p.lines[i].z2 > maxz {
maxz = p.lines[i].z2
}
}
x := (maxx + minx) / 2
y := (maxy + miny) / 2
z := (maxz + minz) / 2
p.Translate(-x, -y, -z)
}
func (p *Pinhole) DrawString(x, y, z float64, s string) {
if s != "" {
p.DrawLine(x, y, z, x, y, z)
//p.lines[len(p.lines)-1].scale = 10 / 0.1 * radius
p.lines[len(p.lines)-1].str = s
}
}
func (p *Pinhole) DrawRect(minx, miny, maxx, maxy, z float64) {
p.DrawLine(minx, maxy, z, maxx, maxy, z)
p.DrawLine(maxx, maxy, z, maxx, miny, z)
p.DrawLine(maxx, miny, z, minx, miny, z)
p.DrawLine(minx, miny, z, minx, maxy, z)
}
func (p *Pinhole) DrawCube(minx, miny, minz, maxx, maxy, maxz float64) {
p.DrawLine(minx, maxy, minz, maxx, maxy, minz)
p.DrawLine(maxx, maxy, minz, maxx, miny, minz)
p.DrawLine(maxx, miny, minz, minx, miny, minz)
p.DrawLine(minx, miny, minz, minx, maxy, minz)
p.DrawLine(minx, maxy, maxz, maxx, maxy, maxz)
p.DrawLine(maxx, maxy, maxz, maxx, miny, maxz)
p.DrawLine(maxx, miny, maxz, minx, miny, maxz)
p.DrawLine(minx, miny, maxz, minx, maxy, maxz)
p.DrawLine(minx, maxy, minz, minx, maxy, maxz)
p.DrawLine(maxx, maxy, minz, maxx, maxy, maxz)
p.DrawLine(maxx, miny, minz, maxx, miny, maxz)
p.DrawLine(minx, miny, minz, minx, miny, maxz)
}
func (p *Pinhole) DrawDot(x, y, z float64, radius float64) {
p.DrawLine(x, y, z, x, y, z)
p.lines[len(p.lines)-1].scale = 10 / 0.1 * radius
}
func (p *Pinhole) DrawLine(x1, y1, z1, x2, y2, z2 float64) {
l := &line{
x1: x1, y1: y1, z1: z1,
x2: x2, y2: y2, z2: z2,
color: color.Black,
scale: 1,
}
p.lines = append(p.lines, l)
}
func (p *Pinhole) DrawCircle(x, y, z float64, radius float64) {
var fx, fy, fz float64
var lx, ly, lz float64
var first, prev *line
// we go one beyond the steps because we need to join at the end
for i := float64(0); i <= circleSteps; i++ {
var dx, dy, dz float64
dx, dy = destination(x, y, (math.Pi*2)/circleSteps*i, radius)
dz = z
if i > 0 {
if i == circleSteps {
p.DrawLine(lx, ly, lz, fx, fy, fz)
} else {
p.DrawLine(lx, ly, lz, dx, dy, dz)
}
line := p.lines[len(p.lines)-1]
line.nocaps = true
line.circle = true
if first == nil {
first = line
}
line.cfirst = first
line.cprev = prev
if prev != nil {
prev.cnext = line
}
prev = line
} else {
fx, fy, fz = dx, dy, dz
}
lx, ly, lz = dx, dy, dz
}
}
type ImageOptions struct {
BGColor color.Color
LineWidth float64
Scale float64
}
var DefaultImageOptions = &ImageOptions{
BGColor: color.White,
LineWidth: 1,
Scale: 1,
}
type byDistance []*line
func (a byDistance) Len() int {
return len(a)
}
func (a byDistance) Less(i, j int) bool {
imin, imax := a[i].Rect()
jmin, jmax := a[j].Rect()
for i := 2; i >= 0; i-- {
if imax[i] > jmax[i] {
return i == 2
}
if imax[i] < jmax[i] {
return i != 2
}
if imin[i] > jmin[i] {
return i == 2
}
if imin[i] < jmin[i] {
return i != 2
}
}
return false
}
func (a byDistance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (p *Pinhole) Image(width, height int, opts *ImageOptions) *image.RGBA {
if opts == nil {
opts = DefaultImageOptions
}
sort.Sort(byDistance(p.lines))
for _, line := range p.lines {
line.drawcoords = nil
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
c := gg.NewContextForRGBA(img)
if opts.BGColor != nil {
c.SetColor(opts.BGColor)
c.DrawRectangle(0, 0, float64(width), float64(height))
c.Fill()
}
capsMap := make(map[color.Color]*capTree)
var ccolor color.Color
var caps *capTree
fwidth, fheight := float64(width), float64(height)
focal := math.Min(fwidth, fheight) / 2
maybeDraw := func(line *line) *fourcorners {
x1, y1, z1 := line.x1, line.y1, line.z1
x2, y2, z2 := line.x2, line.y2, line.z2
px1, py1 := projectPoint(x1, y1, z1, fwidth, fheight, focal, opts.Scale)
px2, py2 := projectPoint(x2, y2, z2, fwidth, fheight, focal, opts.Scale)
if !onscreen(fwidth, fheight, px1, py1, px2, py2) && !line.circle && line.str == "" {
return nil
}
t1 := lineWidthAtZ(z1, focal) * opts.LineWidth * line.scale
t2 := lineWidthAtZ(z2, focal) * opts.LineWidth * line.scale
if line.str != "" {
sz := 10 * t1
c.SetFontFace(truetype.NewFace(gof, &truetype.Options{Size: sz}))
w, h := c.MeasureString(line.str)
c.DrawString(line.str, px1-w/2, py1+h*.4)
return nil
}
var cap1, cap2 bool
if !line.nocaps {
cap1 = caps.insert(x1, y1, z1)
cap2 = caps.insert(x2, y2, z2)
}
return drawUnbalancedLineSegment(c,
px1, py1, px2, py2,
t1, t2,
cap1, cap2,
line.circle,
)
}
for _, line := range p.lines {
if line.color != ccolor {
ccolor = line.color
caps = capsMap[ccolor]
if caps == nil {
caps = newCapTree()
capsMap[ccolor] = caps
}
c.SetColor(ccolor)
}
if line.circle {
if line.drawcoords == nil {
// need to process the coords for all segments belonging to
// the current circle segment.
// first get the basic estimates
var coords []*fourcorners
seg := line.cfirst
for seg != nil {
seg.drawcoords = maybeDraw(seg)
if seg.drawcoords == nil {
panic("nil!")
}
coords = append(coords, seg.drawcoords)
seg = seg.cnext
}
// next reprocess to join the midpoints
for i := 0; i < len(coords); i++ {
var line1, line2 *fourcorners
if i == 0 {
line1 = coords[len(coords)-1]
} else {
line1 = coords[i-1]
}
line2 = coords[i]
midx1 := (line2.x1 + line1.x4) / 2
midy1 := (line2.y1 + line1.y4) / 2
midx2 := (line2.x2 + line1.x3) / 2
midy2 := (line2.y2 + line1.y3) / 2
line2.x1 = midx1
line2.y1 = midy1
line1.x4 = midx1
line1.y4 = midy1
line2.x2 = midx2
line2.y2 = midy2
line1.x3 = midx2
line1.y3 = midy2
}
}
// draw the cached coords
c.MoveTo(line.drawcoords.x1-math.SmallestNonzeroFloat64, line.drawcoords.y1-math.SmallestNonzeroFloat64)
c.LineTo(line.drawcoords.x2-math.SmallestNonzeroFloat64, line.drawcoords.y2-math.SmallestNonzeroFloat64)
c.LineTo(line.drawcoords.x3+math.SmallestNonzeroFloat64, line.drawcoords.y3+math.SmallestNonzeroFloat64)
c.LineTo(line.drawcoords.x4+math.SmallestNonzeroFloat64, line.drawcoords.y4+math.SmallestNonzeroFloat64)
c.LineTo(line.drawcoords.x1-math.SmallestNonzeroFloat64, line.drawcoords.y1-math.SmallestNonzeroFloat64)
c.ClosePath()
} else {
maybeDraw(line)
}
c.Fill()
}
return img
}
type fourcorners struct {
x1, y1, x2, y2, x3, y3, x4, y4 float64
}
func drawUnbalancedLineSegment(c *gg.Context,
x1, y1, x2, y2 float64,
t1, t2 float64,
cap1, cap2 bool,
circleSegment bool,
) *fourcorners {
if x1 == x2 && y1 == y2 {
c.DrawCircle(x1, y1, t1/2)
return nil
}
a := lineAngle(x1, y1, x2, y2)
dx1, dy1 := destination(x1, y1, a-math.Pi/2, t1/2)
dx2, dy2 := destination(x1, y1, a+math.Pi/2, t1/2)
dx3, dy3 := destination(x2, y2, a+math.Pi/2, t2/2)
dx4, dy4 := destination(x2, y2, a-math.Pi/2, t2/2)
if circleSegment {
return &fourcorners{dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4}
}
const cubicCorner = 1.0 / 3 * 2 //0.552284749831
if cap1 && t1 < 2 {
cap1 = false
}
if cap2 && t2 < 2 {
cap2 = false
}
c.MoveTo(dx1, dy1)
if cap1 {
ax1, ay1 := destination(dx1, dy1, a-math.Pi*2, t1*cubicCorner)
ax2, ay2 := destination(dx2, dy2, a-math.Pi*2, t1*cubicCorner)
c.CubicTo(ax1, ay1, ax2, ay2, dx2, dy2)
} else {
c.LineTo(dx2, dy2)
}
c.LineTo(dx3, dy3)
if cap2 {
ax1, ay1 := destination(dx3, dy3, a-math.Pi*2, -t2*cubicCorner)
ax2, ay2 := destination(dx4, dy4, a-math.Pi*2, -t2*cubicCorner)
c.CubicTo(ax1, ay1, ax2, ay2, dx4, dy4)
} else {
c.LineTo(dx4, dy4)
}
c.LineTo(dx1, dy1)
c.ClosePath()
return nil
}
func onscreen(w, h float64, x1, y1, x2, y2 float64) bool {
amin := [2]float64{0, 0}
amax := [2]float64{w, h}
var bmin [2]float64
var bmax [2]float64
if x1 < x2 {
bmin[0], bmax[0] = x1, x2
} else {
bmin[0], bmax[0] = x2, x1
}
if y1 < y2 {
bmin[1], bmax[1] = y1, y2
} else {
bmin[1], bmax[1] = y2, y1
}
for i := 0; i < len(amin); i++ {
if !(bmin[i] <= amax[i] && bmax[i] >= amin[i]) {
return false
}
}
return true
}
func (p *Pinhole) LoadObj(r io.Reader) error {
var faces [][][3]float64
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
var verts [][3]float64
for ln, line := range strings.Split(string(data), "\n") {
for {
nline := strings.Replace(line, " ", " ", -1)
if len(nline) < len(line) {
line = nline
continue
}
break
}
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "v ") {
parts := strings.Split(line[2:], " ")
if len(parts) >= 3 {
verts = append(verts, [3]float64{})
for j := 0; j < 3; j++ {
if verts[len(verts)-1][j], err = strconv.ParseFloat(parts[j], 64); err != nil {
return fmt.Errorf("line %d: %s", ln+1, err.Error())
}
}
}
} else if strings.HasPrefix(line, "f ") {
parts := strings.Split(line[2:], " ")
if len(parts) >= 3 {
faces = append(faces, [][3]float64{})
for _, part := range parts {
part = strings.Split(part, "/")[0]
idx, err := strconv.ParseUint(part, 10, 64)
if err != nil {
return fmt.Errorf("line %d: %s", ln+1, err.Error())
}
if int(idx) > len(verts) {
return fmt.Errorf("line %d: invalid vert index: %d", ln+1, idx)
}
faces[len(faces)-1] = append(faces[len(faces)-1], verts[idx-1])
}
}
}
}
for _, faces := range faces {
var fx, fy, fz float64
var lx, ly, lz float64
var i int
for _, face := range faces {
if i == 0 {
fx, fy, fz = face[0], face[1], face[2]
} else {
p.DrawLine(lx, ly, lz, face[0], face[1], face[2])
}
lx, ly, lz = face[0], face[1], face[2]
i++
}
if i > 1 {
p.DrawLine(lx, ly, lz, fx, fy, fz)
}
}
return nil
}
func (p *Pinhole) SavePNG(path string, width, height int, opts *ImageOptions) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return png.Encode(file, p.Image(width, height, opts))
}
// projectPoint projects a 3d point cartesian point to 2d screen coords.
// Origin is the center
// X is left/right
// Y is down/up
// Z is near/far, the 0 position is focal distance away from lens.
func projectPoint(
x, y, z float64, // 3d point to project
w, h, f float64, // width, height, focal
scale float64, // scale
) (px, py float64) { // projected point
x, y, z = x*scale*f, y*scale*f, z*scale*f
zz := z + f
if zz == 0 {
zz = math.SmallestNonzeroFloat64
}
px = x*(f/zz) + w/2
py = y*(f/zz) - h/2
py *= -1
return
}
func lineWidthAtZ(z float64, f float64) float64 {
return ((z*-1 + 1) / 2) * f * 0.04
}
func lineAngle(x1, y1, x2, y2 float64) float64 {
return math.Atan2(y1-y2, x1-x2)
}
func destination(x, y, angle, distance float64) (dx, dy float64) {
dx = x + math.Cos(angle)*distance
dy = y + math.Sin(angle)*distance
return
}
// https://www.siggraph.org/education/materials/HyperGraph/modeling/mod_tran/3drota.htm
func rotate(x, y, z float64, q float64, which int) (dx, dy, dz float64) {
switch which {
case 0: // x
dy = y*math.Cos(q) - z*math.Sin(q)
dz = y*math.Sin(q) + z*math.Cos(q)
dx = x
case 1: // y
dz = z*math.Cos(q) - x*math.Sin(q)
dx = z*math.Sin(q) + x*math.Cos(q)
dy = y
case 2: // z
dx = x*math.Cos(q) - y*math.Sin(q)
dy = x*math.Sin(q) + y*math.Cos(q)
dz = z
}
return
}
type capItem struct {
point [3]float64
}
func (a *capItem) Less(v btree.Item) bool {
b := v.(*capItem)
for i := 2; i >= 0; i-- {
if a.point[i] < b.point[i] {
return true
}
if a.point[i] > b.point[i] {
return false
}
}
return false
}
// really lazy structure.
type capTree struct {
tr *btree.BTree
}
func newCapTree() *capTree {
return &capTree{
tr: btree.New(9),
}
}
func (tr *capTree) insert(x, y, z float64) bool {
if tr.has(x, y, z) {
return false
}
tr.tr.ReplaceOrInsert(&capItem{point: [3]float64{x, y, z}})
return true
}
func (tr *capTree) has(x, y, z float64) bool {
return tr.tr.Has(&capItem{point: [3]float64{x, y, z}})
}
You can’t perform that action at this time.