// Package main stl.go - GOOS=js GOARCH=wasm go build -ldflags="-X main.stlFileName=STLFILE.stl" stl.go
package main
import (
"bytes"
"log"
"encoding/base64"
"encoding/binary"
m "math"
"crypto/rand"
r "reflect"
"runtime"
"strconv"
"syscall/js"
u "unsafe"
"github.com/go-gl/mathgl/mgl32"
"gitlab.com/russoj88/stl/stl"
)
var (
running = true
done chan struct{}
stlFileName, originalHTML string
rr Renderer
existingFooter, body, footer, sXV, sYV, sZV, sZoomV, cEl, gl js.Value
currentZoom float32 = 3
)
const gebi = "getElementById"
const ael = "addEventListener"
const ih = "innerHTML"
func main() {
ready := make(chan struct{})
// Check if the DOM is already loaded
document := js.Global().Get("document")
readyState := document.Get("readyState").String()
if readyState == "interactive" || readyState == "complete" {
log.Println("WASM: DOM already fully loaded")
close(ready) // Signal the channel immediately
} else {
// Define the callback function for DOMContentLoaded
cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
log.Println("WASM: DOM fully loaded and parsed")
close(ready) // Signal the channel to continue
return nil
})
defer cb.Release() // Release the callback when it's no longer needed
// Add an event listener for DOMContentLoaded
document.Call(ael, "DOMContentLoaded", cb)
log.Println("WASM: waiting for DOM to load")
}
// Wait until the DOM is ready
<-ready
mgl32.DisableMemoryPooling()
preElement := js.Global().Get("document").Call("getElementById", "package-type")
if !preElement.IsUndefined() && !preElement.IsNull() {
packageText := preElement.Get("textContent").String()
packageType := packageText[len("Package Type: "):]
stlFileName = packageType + ".stl"
}
//stlFileName = "TO-247.stl"
//log.Println("WASM: test")
tdata := struct {
XRange, ZMin, ZMax string
XStep, ZoomStep string
}{
XRange: "1", ZMin: "0", ZMax: "50",
XStep: "0.01", ZoomStep: "0.1",
}
if stlFileName != ".stl" && stlFileName != "" {
tdata.ZMin = "10"
tdata.ZMax = "1000"
}
var controlsHTML = `
<datalist id="speeds">
<option>-`+tdata.XRange+`</option><option>0</option><option>`+tdata.XRange+`</option></datalist>
<table class="🌐">
<tr><td><h2>3D Model</h2></td><td><p>X<input id="X" type="range" min="-`+tdata.XRange+`" max="`+tdata.XRange+`" step="`+tdata.XStep+`" list="speeds"><text id="XV">00.00</text></p></td>
<td><p>Y<input id="Y" type="range" min="-`+tdata.XRange+`" max="`+tdata.XRange+`" step="`+tdata.XStep+`" list="speeds"><text id="YV">00.00</text></p></td>
<td><p>Z<input id="Z" type="range" min="-`+tdata.XRange+`" max="`+tdata.XRange+`" step="`+tdata.XStep+`" list="speeds"><text id="ZV">00.00</text></p></td>
<td><p>Zoom<input id="Zoom" type="range" min="`+tdata.ZMin+`" max="`+tdata.ZMax+`" step="`+tdata.ZoomStep+`" list="speeds"><text id="ZoomV">0000.00</text></p></td>
<td><p><button type="button" id="stop">Stop Rendering</button></p></td></tr>
</table>
`
doc := js.Global().Get("document")
body = doc.Get("body")
existingFooter = doc.Call("getElementsByTagName", "footer").Index(0)
if existingFooter.Truthy() {
originalHTML = existingFooter.Get(ih).String()
footer = doc.Call("createElement", "footer")
footer.Set(ih, originalHTML+controlsHTML)
body.Call("replaceChild", footer, existingFooter)
} else {
footer = doc.Call("createElement", "footer")
footer.Set(ih, controlsHTML)
body.Call("appendChild", footer)
}
cEl = doc.Call(gebi, "gocanvas")
width := doc.Get("body").Get("clientWidth").Int()
height := doc.Get("body").Get("clientHeight").Int()
cEl.Set("width", width)
cEl.Set("height", height)
sXc := js.FuncOf(sCX)
sX := doc.Call(gebi, "X")
sX.Call(ael, "input", sXc)
sXV = doc.Call(gebi, "XV")
sYc := js.FuncOf(sCY)
sY := doc.Call(gebi, "Y")
sY.Call(ael, "input", sYc)
sYV = doc.Call(gebi, "YV")
sZc := js.FuncOf(sCZ)
sZ := doc.Call(gebi, "Z")
sZ.Call(ael, "input", sZc)
sZV = doc.Call(gebi, "ZV")
sZoomc := js.FuncOf(sCZoom)
sZoom := doc.Call(gebi, "Zoom")
sZoom.Call(ael, "input", sZoomc)
sZoomV = doc.Call(gebi, "ZoomV")
sBc := js.FuncOf(stopApplication)
sB := doc.Call(gebi, "stop")
sB.Call(ael, "click", sBc)
defer sBc.Release()
if stlFileName != ".stl" && stlFileName != "" {
response := js.Global().Call("fetch", "/stl/base64/"+stlFileName)
promise := response.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
if p[0].Get("ok").Bool() {
return p[0].Call("text")
}
return "Error fetching stereolithograph"
}))
promise.Call("then", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
result := p[0].String()
uploadedFile, _ := parseBase64File(result) // nolint
stlSolid, _ := NewSTL(uploadedFile) // nolint
vert, colors, indices := stlSolid.GetModel()
modelSize := getMaxScalar(vert)
currentZoom := modelSize * 3
rr.SetZoom(currentZoom)
rr.SetModel(colors, vert, indices)
return nil
}))
}
gl = cEl.Call("getContext", "webgl")
if gl.IsUndefined() {
gl = cEl.Call("getContext", "experimental-webgl")
}
if gl.IsUndefined() {
js.Global().Call("alert", "WASM: browser might not support webgl")
return
}
config := InitialConfig{
W: width,
H: height,
X: 0,
Y: 0,
Z: 0,
Vertices: verticesNative,
Indices: indicesNative,
Colors: colorsNative,
FSC: fragShaderCode,
VSC: vertShaderCode,
}
config.X = cryptoRandFloat32() / 20
config.Y = cryptoRandFloat32() / 20
config.Z = cryptoRandFloat32() / 20
config.Vertices, config.Indices = generateSphereVertices(float32(1.0),30,30)
if stlFileName == ".stl" || stlFileName == "" {
config.FSC, config.VSC = fragShaderCode1, vertShaderCode1
}
var jsErr js.Value
rr, jsErr = NewRenderer(gl, config)
if !jsErr.IsNull() {
js.Global().Call("alert", "WASM: Cannot load webgl ")
return
}
rr.SetZoom(currentZoom)
defer rr.Release()
x, y, z := rr.GetSpeed()
sX.Set("value", f32(x, 'f', -1, 64))
if x > 0 {sXV.Set(ih, "+"+f32(x, 'f', 2, 64))}
if x == 0 {sXV.Set(ih, " "+f32(x, 'f', 2, 64))}
if x < 0 {sXV.Set(ih, f32(x, 'f', 2, 64))}
sY.Set("value", f32(y, 'f', -1, 64))
if y > 0 {sYV.Set(ih, "+"+f32(y, 'f', 2, 64))}
if y == 0 {sYV.Set(ih, "0"+f32(y, 'f', 2, 64))}
if y < 0 {sYV.Set(ih, f32(y, 'f', 2, 64))}
sZ.Set("value", f32(z, 'f', -1, 64))
if z > 0 {sZV.Set(ih, "+"+f32(z, 'f', 2, 64))}
if z == 0 {sZV.Set(ih, "0"+f32(z, 'f', 2, 64))}
if z < 0 {sZV.Set(ih, f32(z, 'f', 2, 64))}
var renderFrame js.Func
renderFrame = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
rr.Render(this, args)
js.Global().Call("requestAnimationFrame", renderFrame)
return nil
})
js.Global().Call("requestAnimationFrame", renderFrame)
done = make(chan struct{})
<-done
}
func parseBase64File(input string) (output []byte, err js.Value) {
searchString := "base64,"
searchLength := len(searchString)
var index = -1
for i := 0; i <= len(input)-searchLength; i++ {
if input[i:i+searchLength] == searchString {
index = i
break
}
}
if index < 0 {
err = js.Global().Get("Error").New("Error opening file")
return
}
sBuffer := input[index+searchLength:]
output, decodeErr := base64.StdEncoding.DecodeString(sBuffer)
if decodeErr != nil {
err = js.Global().Get("Error").New(decodeErr.Error())
return
}
return output, js.Null()
}
func getMaxScalar(vertices []float32) float32 {
var max float32
for baseIndex := 0; baseIndex < len(vertices); baseIndex += 3 {
testScale := scalar(vertices[baseIndex], vertices[baseIndex], vertices[baseIndex])
if testScale > max {
max = testScale
}
}
return max
}
func scalar(x float32, y float32, z float32) float32 {
xy := m.Sqrt(float64(x*x + y*y))
return float32(m.Sqrt(xy*xy + float64(z*z)))
}
func cryptoRandFloat32() float32 {
b := make([]byte, 4)
_, err := rand.Read(b)
if err != nil {
panic("crypto/rand read failed: " + err.Error())
}
u := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
return float32(u) / float32(m.MaxUint32)
}
func stopApplication(_ js.Value, _ []js.Value) interface{} {
running = false
sZoomV.Set(ih, float32(10000))
currentZoom = float32(10000)
rr.SetZoom(float32(10000))
footer.Set(ih, originalHTML)
js.Global().Call("setTimeout", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
close(done)
// done <- struct{}{}
return nil
}), 5000)
return nil
}
func sCX(this js.Value, _ []js.Value) interface{} {
sSpeed := this.Get("value").String()
s, _ := strconv.ParseFloat(sSpeed, 64)
rr.SetX(float32(s))
if s > 0 { sXV.Set(ih, "+"+f64(s, 'f', 2, 32)) }
if s == 0 { sXV.Set(ih, "0"+f64(s, 'f', 2, 32)) }
if s < 0 { sXV.Set(ih, f64(s, 'f', 2, 32)) }
return nil
}
func sCY(this js.Value, _ []js.Value) interface{} {
sS := this.Get("value").String()
s, _ := strconv.ParseFloat(sS, 64)
rr.SetY(float32(s))
if s > 0 { sYV.Set(ih, "+"+f64(s, 'f', 2, 32)) }
if s == 0 { sYV.Set(ih, "0"+f64(s, 'f', 2, 32)) }
if s < 0 { sYV.Set(ih, f64(s, 'f', 2, 32)) }
return nil
}
func sCZ(this js.Value, _ []js.Value) interface{} {
sS := this.Get("value").String()
s, _ := strconv.ParseFloat(sS, 64)
rr.SetZ(float32(s))
if s > 0 { sZV.Set(ih, "+"+f64(s, 'f', 2, 32)) }
if s == 0 { sZV.Set(ih, "0"+f64(s, 'f', 2, 32)) }
if s < 0 { sZV.Set(ih, f64(s, 'f', 2, 32)) }
return nil
}
func sCZoom(this js.Value, _ []js.Value) interface{} {
sS := this.Get("value").String()
s, _ := strconv.ParseFloat(sS, 64)
if s < 10 {
sZoomV.Set(ih, "000"+f64(s, 'f', 2, 32))
} else if s < 100 {
sZoomV.Set(ih, "00"+f64(s, 'f', 2, 32))
} else if s < 1000 {
sZoomV.Set(ih, "0"+f64(s, 'f', 2, 32))
} else {
sZoomV.Set(ih, f64(s, 'f', 2, 32))
}
currentZoom = float32(s)
rr.SetZoom(currentZoom)
return nil
}
// Model is an interface for a model
type Model interface {
GetModel() ([]float32, []float32, []uint16)
}
func cryptoRandIntn(max int) (int, js.Value) {
if max <= 0 { return 0, js.Global().Get("Error").New("max must be a positive integer") }
numBytes := (max + 7) / 8
maxBytes := 1 << (numBytes * 8)
randBytes := make([]byte, numBytes)
randNum := 0
for {
_, err := rand.Read(randBytes)
if err != nil { return 0, js.Global().Get("Error").New("error generating random number") }
for _, b := range randBytes { randNum = (randNum << 8) | int(b) }
if randNum < maxBytes-maxBytes%max { break }
}
return randNum % max, js.Null()
}
// NewSTL returns a new STL & error
func NewSTL(buffer []byte) (o STL, err error) {
bufferReader := bytes.NewReader(buffer)
solid, err := stl.From(bufferReader)
if err != nil {
return
}
// Generate random rotation matrix
rotationMatrix := randomRotationMatrix()
// Generate colors
numColors, _ := cryptoRandIntn(5)
numColors += 2 // Random number between 2 and 6
colors := GenerateGradient(numColors, int(solid.TriangleCount))
var index uint32
for i, triangle := range solid.Triangles {
colorR := colors[i].Red
colorG := colors[i].Green
colorB := colors[i].Blue
// Convert each triangle's vertices to custom Vertex type and apply rotation
v0 := Vertex{X: float32(triangle.Vertices[0].X), Y: float32(triangle.Vertices[0].Y), Z: float32(triangle.Vertices[0].Z)}
v1 := Vertex{X: float32(triangle.Vertices[1].X), Y: float32(triangle.Vertices[1].Y), Z: float32(triangle.Vertices[1].Z)}
v2 := Vertex{X: float32(triangle.Vertices[2].X), Y: float32(triangle.Vertices[2].Y), Z: float32(triangle.Vertices[2].Z)}
// Rotate and add vertices
o.addRotatedVertex(&index, v0, rotationMatrix, colorR, colorG, colorB)
o.addRotatedVertex(&index, v1, rotationMatrix, colorR, colorG, colorB)
o.addRotatedVertex(&index, v2, rotationMatrix, colorR, colorG, colorB)
}
return o, err
}
// Add a rotated vertex to the STL structure
func (s *STL) addRotatedVertex(index *uint32, vertex Vertex, rotation mgl32.Mat4, r, g, b float32) {
// Apply rotation
rotatedVertex := rotation.Mul4x1(mgl32.Vec3{vertex.X, vertex.Y, vertex.Z}.Vec4(1.0))
// Add rotated vertex to the STL struct
s.v = append(s.v, rotatedVertex[0], rotatedVertex[1], rotatedVertex[2])
s.i = append(s.i, *index)
s.c = append(s.c, r, g, b)
(*index)++
}
// STL is a stereolithograph
type STL struct {
v []float32
c []float32
i []uint32
}
// Define a custom Vertex type for storing the vertex data.
type Vertex struct {
X, Y, Z float32
}
// GetModel gets the model
func (s STL) GetModel() ([]float32, []float32, []uint32) {
return s.v, s.c, s.i
}
// InitialConfig is the initial config
type InitialConfig struct {
W int
H int
X float32
Y float32
Z float32
Colors []float32
Vertices []float32
Indices []uint32
FSC string
VSC string
}
// Renderer is the renderer
type Renderer struct {
glContext js.Value
glTypes GLTypes
colors js.Value
v js.Value
i js.Value
colorBuffer js.Value
vertexBuffer js.Value
indexBuffer js.Value
numIndices int
numVertices int
fragShader js.Value
vertShader js.Value
shaderProgram js.Value
tmark float32
rX float32 //rotation X
rY float32
rZ float32
movMatrix mgl32.Mat4
PositionMatrix js.Value
ViewMatrix js.Value
ModelMatrix js.Value
height int
width int
sX float32
sY float32
sZ float32
}
// NewRenderer returns a new renderer & error
func NewRenderer(gl js.Value, config InitialConfig) (r Renderer, err js.Value) {
// Get some WebGL bindings
r.glContext = gl
err = r.glTypes.New(r.glContext)
r.numIndices = len(config.Indices)
r.numVertices = len(config.Vertices)
r.movMatrix = mgl32.Ident4()
r.width = config.W
r.height = config.H
r.sX = config.X
r.sY = config.Y
r.sZ = config.Z
// Convert buffers to JS TypedArrays
r.UpdateColorBuffer(config.Colors)
r.UpdateVerticesBuffer(config.Vertices)
r.UpdateIndicesBuffer(config.Indices)
r.UpdateFragmentShader(config.FSC)
r.UpdateVertexShader(config.VSC)
r.updateShaderProgram()
r.attachShaderProgram()
r.setContextFlags()
r.createMatrixes()
r.EnableObject()
return
}
// SetModel sets a new model
func (r *Renderer) SetModel(Colors []float32, Vertices []float32, Indices []uint32) {
r.numIndices = len(Indices)
r.UpdateColorBuffer(Colors)
r.UpdateVerticesBuffer(Vertices)
r.UpdateIndicesBuffer(Indices)
r.EnableObject()
}
// Release releases the renderer
func (r *Renderer) Release() {
return
}
// EnableObject enables the object
func (r *Renderer) EnableObject() {
r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
}
// SetX set rotation x axis speed
func (r *Renderer) SetX(x float32) {
r.sX = x
}
// SetY set rotation y axis speed
func (r *Renderer) SetY(y float32) {
r.sY = y
}
// SetZ set rotation z axis speed
func (r *Renderer) SetZ(z float32) {
r.sZ = z
}
// GetSpeed returns the rotation speeds
func (r *Renderer) GetSpeed() (x, y, z float32) {
return r.sX, r.sY, r.sZ
}
// SetSize sets the size of the rendering
func (r *Renderer) SetSize(height, width int) {
r.height = height
r.width = width
}
func (r *Renderer) createMatrixes() {
ratio := float32(r.width) / float32(r.height)
// fmt.Println("Renderer.createMatrixes")
projMatrix := mgl32.Perspective(mgl32.DegToRad(45.0), ratio, 1, 100000.0)
projMatrixBuffer := (*[16]float32)(u.Pointer(&projMatrix)) // nolint
typedProjMatrixBuffer := S2TA([]float32((*projMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.PositionMatrix, false, typedProjMatrixBuffer)
viewMatrix := mgl32.LookAtV(mgl32.Vec3{3.0, 3.0, 3.0}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
viewMatrixBuffer := (*[16]float32)(u.Pointer(&viewMatrix)) // nolint
typedViewMatrixBuffer := S2TA([]float32((*viewMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}
func (r *Renderer) setContextFlags() {
r.glContext.Call("clearColor", 0.0, 0.0, 0.0, 0.0) // Color the screen is cleared to
r.glContext.Call("viewport", 0, 0, r.width, r.height) // Viewport size
r.glContext.Call("depthFunc", r.glTypes.LEqual)
}
// UpdateFragmentShader Updates the Fragment Shader
func (r *Renderer) UpdateFragmentShader(shaderCode string) {
r.fragShader = r.glContext.Call("createShader", r.glTypes.FragmentShader)
r.glContext.Call("shaderSource", r.fragShader, shaderCode)
r.glContext.Call("compileShader", r.fragShader)
}
// UpdateVertexShader updates the vertex shader
func (r *Renderer) UpdateVertexShader(shaderCode string) {
r.vertShader = r.glContext.Call("createShader", r.glTypes.VertexShader)
r.glContext.Call("shaderSource", r.vertShader, shaderCode)
r.glContext.Call("compileShader", r.vertShader)
}
func (r *Renderer) updateShaderProgram() {
if r.fragShader.IsUndefined() || r.vertShader.IsUndefined() {
return
}
r.shaderProgram = r.glContext.Call("createProgram")
r.glContext.Call("attachShader", r.shaderProgram, r.vertShader)
r.glContext.Call("attachShader", r.shaderProgram, r.fragShader)
r.glContext.Call("linkProgram", r.shaderProgram)
}
const gul = "getUniformLocation"
func (r *Renderer) attachShaderProgram() {
r.PositionMatrix = r.glContext.Call(gul, r.shaderProgram, "Pmatrix")
r.ViewMatrix = r.glContext.Call(gul, r.shaderProgram, "Vmatrix")
r.ModelMatrix = r.glContext.Call(gul, r.shaderProgram, "Mmatrix")
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
position := r.glContext.Call("getAttribLocation", r.shaderProgram, "position")
r.glContext.Call("vertexAttribPointer", position, 3, r.glTypes.Float, false, 0, 0)
r.glContext.Call("enableVertexAttribArray", position)
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
color := r.glContext.Call("getAttribLocation", r.shaderProgram, "color")
r.glContext.Call("vertexAttribPointer", color, 3, r.glTypes.Float, false, 0, 0)
r.glContext.Call("enableVertexAttribArray", color)
r.glContext.Call("useProgram", r.shaderProgram)
if stlFileName == ".stl" || stlFileName == "" {
uBaseColor := r.glContext.Call(gul, r.shaderProgram, "uBaseColor")
uTopColor := r.glContext.Call(gul, r.shaderProgram, "uTopColor")
uColor := r.glContext.Call(gul, r.shaderProgram, "uColor")
r.glContext.Call("uniform3f", uBaseColor, 1.0, 0.0, 0.0)
r.glContext.Call("uniform3f", uTopColor, 0.0, 0.0, 1.0)
r.glContext.Call("uniform3f", uColor, 1.0, 1.0, 1.0)
}
}
// UpdateColorBuffer Updates the ColorBuffer
func (r *Renderer) UpdateColorBuffer(buffer []float32) {
r.colors = S2TA(buffer)
if r.colorBuffer.IsUndefined() {
r.colorBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.colorBuffer)
r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.colors, r.glTypes.StaticDraw)
}
// UpdateVerticesBuffer Updates the VerticesBuffer
func (r *Renderer) UpdateVerticesBuffer(buffer []float32) {
r.v = S2TA(buffer)
if r.vertexBuffer.IsUndefined() {
r.vertexBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ArrayBuffer, r.vertexBuffer)
r.glContext.Call("bufferData", r.glTypes.ArrayBuffer, r.v, r.glTypes.StaticDraw)
}
// UpdateIndicesBuffer Updates the IndicesBuffer
func (r *Renderer) UpdateIndicesBuffer(buffer []uint32) {
r.i = S2TA(buffer)
if r.indexBuffer.IsUndefined() {
r.indexBuffer = r.glContext.Call("createBuffer")
}
r.glContext.Call("bindBuffer", r.glTypes.ElementArrayBuffer, r.indexBuffer)
r.glContext.Call("bufferData", r.glTypes.ElementArrayBuffer, r.i, r.glTypes.StaticDraw)
}
// Render renders
func (r *Renderer) Render(_ js.Value, args []js.Value) interface{} { // nolint
now := float32(args[0].Float())
tdiff := now - r.tmark
r.tmark = now
r.rX = r.rX + r.sX*float32(tdiff)/500
r.rY = r.rY + r.sY*float32(tdiff)/500
r.rZ = r.rZ + r.sZ*float32(tdiff)/500
r.movMatrix = mgl32.HomogRotate3DX(r.rX)
r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DY(r.rY))
r.movMatrix = r.movMatrix.Mul4(mgl32.HomogRotate3DZ(r.rZ))
modelMatrixBuffer := (*[16]float32)(u.Pointer(&r.movMatrix)) // nolint
typedModelMatrixBuffer := S2TA([]float32((*modelMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ModelMatrix, false, typedModelMatrixBuffer)
r.glContext.Call("enable", r.glTypes.DepthTest)
r.glContext.Call("clear", r.glTypes.ColorBufferBit)
r.glContext.Call("clear", r.glTypes.DepthBufferBit)
usegltype := r.glTypes.Triangles
if stlFileName == ".stl" || stlFileName == "" {
usegltype = r.glTypes.Line
r.glContext.Call("drawArrays", r.glTypes.LineLoop, 0, r.numVertices/3)
}
r.glContext.Call("drawElements", usegltype, r.numIndices, r.glTypes.UnsignedInt, 0)
return nil
}
// SetZoom Sets the Zoom
func (r *Renderer) SetZoom(currentZoom float32) {
viewMatrix := mgl32.LookAtV(mgl32.Vec3{currentZoom, currentZoom, currentZoom}, mgl32.Vec3{0.0, 0.0, 0.0}, mgl32.Vec3{0.0, 1.0, 0.0})
viewMatrixBuffer := (*[16]float32)(u.Pointer(&viewMatrix)) // nolint
typedViewMatrixBuffer := S2TA([]float32((*viewMatrixBuffer)[:]))
r.glContext.Call("uniformMatrix4fv", r.ViewMatrix, false, typedViewMatrixBuffer)
}
// NewColorInterpolation generates color interpolation
func NewColorInterpolation(a Color, b Color) ColorInterpolation {
return ColorInterpolation{
a,
b,
a.Subtract(b),
}
}
// ColorInterpolation is interpolated color
type ColorInterpolation struct {
startColor Color
endColor Color
deltaColor Color
}
// Interpolate interpolates
func (c ColorInterpolation) Interpolate(percent float32) Color {
scaled := c.deltaColor.MultiplyFloat(percent)
return c.startColor.Add(scaled)
}
// Color represents a color
type Color struct {
Red float32
Green float32
Blue float32
}
// NewRandomColor returns a New RandomColor
func NewRandomColor() Color {
const maxRGB = 255
var r, g, b float64
buf := make([]byte, 3)
rand.Read(buf)
r = float64(buf[0]) / 256
g = float64(buf[1]) / 256
b = float64(buf[2]) / 256
r = r * maxRGB
g = g * maxRGB
b = b * maxRGB
return Color{float32(r), float32(g), float32(b)}
}
// Subtract Subtracts color
func (c Color) Subtract(d Color) Color {
return Color{
c.Red - d.Red,
c.Green - d.Green,
c.Blue - d.Blue,
}
}
// Add Adds color
func (c Color) Add(d Color) Color {
return Color{
c.Red + d.Red,
c.Green + d.Green,
c.Blue + d.Blue,
}
}
// MultiplyFloat Multiplies Float
func (c Color) MultiplyFloat(x float32) Color {
return Color{
c.Red * x,
c.Green * x,
c.Blue * x,
}
}
// GenerateGradient Generates Gradient
func GenerateGradient(numColors int, steps int) []Color {
distribution := distributeColors(numColors, steps)
colors := make([]Color, numColors)
for i := 0; i < numColors; i++ {
colors[i] = NewRandomColor()
}
outputBuffer := make([]Color, 0, steps)
for index := 0; index < numColors; index++ {
if index >= numColors-1 {
size := steps - distribution[index]
interpolation := NewColorInterpolation(colors[index-1], colors[index])
buffer := generateSingleGradient(interpolation, size)
outputBuffer = append(outputBuffer, buffer...)
break
}
currentStep := distribution[index]
nextStep := distribution[index+1]
size := nextStep - currentStep
interpolation := NewColorInterpolation(colors[index], colors[index+1])
buffer := generateSingleGradient(interpolation, size)
outputBuffer = append(outputBuffer, buffer...)
}
return outputBuffer
}
func distributeColors(numColors int, steps int) []int {
diff := int(m.Ceil(float64(steps) / float64(numColors)))
output := make([]int, numColors)
for i := 0; i < numColors; i++ {
output[i] = diff * i
}
return output
}
func generateSingleGradient(c ColorInterpolation, numSteps int) []Color {
output := make([]Color, numSteps)
for i := 0; i < numSteps; i++ {
percent := float32(i) / float32(numSteps)
output[i] = c.Interpolate(percent)
}
return output
}
func f32(f float32, g byte, prec, bitSize int) string {
return strconv.FormatFloat(float64(f), g, prec, bitSize)
}
func f64(f float64, g byte, prec, bitSize int) string {
return strconv.FormatFloat(f, g, prec, bitSize)
}
func generateSphereVertices(radius float32, stacks, slices int) ([]float32, []uint32) {
var vertices []float32
var indices []uint32
// Generate random initial rotation
rotationMatrix := randomRotationMatrix()
// Generate sphere vertices
for i := 0; i <= stacks; i++ {
phi := float32(i) * float32(m.Pi) / float32(stacks)
for j := 0; j <= slices; j++ {
theta := float32(j) * 2.0 * float32(m.Pi) / float32(slices)
x := radius * float32(m.Sin(float64(phi))) * float32(m.Cos(float64(theta)))
y := radius * float32(m.Sin(float64(phi))) * float32(m.Sin(float64(theta)))
z := radius * float32(m.Cos(float64(phi)))
// Apply rotation to the vertex
vertex := mgl32.Vec3{z, y, x}
rotatedVertex := rotationMatrix.Mul4x1(vertex.Vec4(1.0))
// Append rotated vertex
vertices = append(vertices, rotatedVertex[0], rotatedVertex[1], rotatedVertex[2])
}
}
// Generate sphere indices
for i := 0; i < stacks; i++ {
for j := 0; j <= slices; j++ {
indices = append(indices, uint32(i*(slices+1)+j), uint32((i+1)*(slices+1)+j))
}
}
return vertices, indices
}
func randomRotationMatrix() mgl32.Mat4 {
// Generate random rotation angles (in radians) using crypto/rand
rotX := randomFloat32() * 2 * float32(m.Pi)
rotY := randomFloat32() * 2 * float32(m.Pi)
rotZ := randomFloat32() * 2 * float32(m.Pi)
// Create rotation matrices for each axis
rotMatrixX := mgl32.HomogRotate3DX(rotX)
rotMatrixY := mgl32.HomogRotate3DY(rotY)
rotMatrixZ := mgl32.HomogRotate3DZ(rotZ)
// Combine rotations (Z * Y * X)
return rotMatrixZ.Mul4(rotMatrixY).Mul4(rotMatrixX)
}
func randomFloat32() float32 {
var randomValue uint32
err := binary.Read(rand.Reader, binary.BigEndian, &randomValue)
if err != nil {
log.Fatalf("Failed to read random value: %v", err)
}
return float32(randomValue) / float32(0xFFFFFFFF)
}
var verticesNative = []float32{
-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
-1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1,
-1, -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1,
1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1,
-1, -1, -1, -1, -1, 1, 1, -1, 1, 1, -1, -1,
-1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1,
}
var colorsNative = []float32{
5, 3, 7, 5, 3, 7, 5, 3, 7, 5, 3, 7,
1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 3,
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
}
var indicesNative = []uint32{
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7,
8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15,
16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23,
}
const vertShaderCode = `
attribute vec3 position;
uniform mat4 Pmatrix;
uniform mat4 Vmatrix;
uniform mat4 Mmatrix;
attribute vec3 color;
varying vec3 vColor;
void main(void) {
gl_Position = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);
vColor = color;
}
`
const fragShaderCode = `
precision mediump float;
varying vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.);
}
`
const fragShaderCode1 = `
precision mediump float;
uniform vec3 uBaseColor; // Color value at the base
uniform vec3 uTopColor; // Color value at the top
varying vec3 vPosition; // Interpolated vertex position
void main(void) {
float t = (vPosition.y + 1.0) * 0.5; // Normalize the y-coordinate to [0, 1]
vec3 rainbowColor = mix(uBaseColor, uTopColor, t);
gl_FragColor = vec4(rainbowColor, 1.0);
}
`
const vertShaderCode1 = `
attribute vec3 position;
uniform mat4 Pmatrix;
uniform mat4 Vmatrix;
uniform mat4 Mmatrix;
varying vec3 vPosition; // Pass vertex position to fragment shader
void main(void) {
gl_Position = Pmatrix * Vmatrix * Mmatrix * vec4(position, 1.0);
vPosition = position; // Pass vertex position to fragment shader
}
`
// GLTypes provides WebGL bindings.
type GLTypes struct {
StaticDraw js.Value
ArrayBuffer js.Value
ElementArrayBuffer js.Value
VertexShader js.Value
FragmentShader js.Value
Float js.Value
DepthTest js.Value
ColorBufferBit js.Value
DepthBufferBit js.Value
Triangles js.Value
UnsignedShort js.Value
UnsignedInt js.Value
LEqual js.Value
LineLoop js.Value
Line js.Value
}
// New grabs the WebGL bindings from a GL context.
func (types *GLTypes) New(gl js.Value) js.Value {
types.StaticDraw = gl.Get("STATIC_DRAW")
types.ArrayBuffer = gl.Get("ARRAY_BUFFER")
types.ElementArrayBuffer = gl.Get("ELEMENT_ARRAY_BUFFER")
types.VertexShader = gl.Get("VERTEX_SHADER")
types.FragmentShader = gl.Get("FRAGMENT_SHADER")
types.Float = gl.Get("FLOAT")
types.DepthTest = gl.Get("DEPTH_TEST")
types.ColorBufferBit = gl.Get("COLOR_BUFFER_BIT")
types.Triangles = gl.Get("TRIANGLES")
types.UnsignedShort = gl.Get("UNSIGNED_SHORT")
types.LEqual = gl.Get("LEQUAL")
types.DepthBufferBit = gl.Get("DEPTH_BUFFER_BIT")
types.LineLoop = gl.Get("LINE_LOOP")
types.Line = gl.Get("LINES")
enabled := gl.Call("getExtension", "OES_element_index_uint")
if !enabled.Truthy() {
return js.Global().Get("Error").New("missing extension: OES_element_index_uint")
}
types.UnsignedInt = gl.Get("UNSIGNED_INT")
return js.Null()
}
func sliceToByteSlice(s interface{}) []byte {
switch s := s.(type) {
case []int8:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
return *(*[]byte)(u.Pointer(h)) // nolint
case []int16:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 2
h.Cap *= 2
return *(*[]byte)(u.Pointer(h)) // nolint
case []int32:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(u.Pointer(h)) // nolint
case []int64:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(u.Pointer(h)) // nolint
case []uint8:
return s
case []uint16:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 2
h.Cap *= 2
return *(*[]byte)(u.Pointer(h)) // nolint
case []uint32:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(u.Pointer(h)) // nolint
case []uint64:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(u.Pointer(h)) // nolint
case []float32:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 4
h.Cap *= 4
return *(*[]byte)(u.Pointer(h)) // nolint
case []float64:
h := (*r.SliceHeader)(u.Pointer(&s)) // nolint
h.Len *= 8
h.Cap *= 8
return *(*[]byte)(u.Pointer(h)) // nolint
default:
// panic("jsutil: unexpected value at sliceToBytesSlice: " + r.TypeOf(s).String())
panic("jsutil: unexpected value at sliceToBytesSlice: ")
}
}
const bo = "byteOffset"
const bl = "byteLength"
const b = "buffer"
const u8a = "Uint8Array"
// S2TA converts Slice To TypedArray
func S2TA(s interface{}) js.Value {
switch s := s.(type) {
case []int8:
a := js.Global().Get(u8a).New(len(s))
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Int8Array").New(buf, a.Get(bo), a.Get(bl))
case []int16:
a := js.Global().Get(u8a).New(len(s) * 2)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Int16Array").New(buf, a.Get(bo), a.Get(bl).Int()/2)
case []int32:
a := js.Global().Get(u8a).New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Int32Array").New(buf, a.Get(bo), a.Get(bl).Int()/4)
case []uint8:
a := js.Global().Get(u8a).New(len(s))
js.CopyBytesToJS(a, s)
runtime.KeepAlive(s)
return a
case []uint16:
a := js.Global().Get(u8a).New(len(s) * 2)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Uint16Array").New(buf, a.Get(bo), a.Get(bl).Int()/2)
case []uint32:
a := js.Global().Get(u8a).New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Uint32Array").New(buf, a.Get(bo), a.Get(bl).Int()/4)
case []float32:
a := js.Global().Get(u8a).New(len(s) * 4)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Float32Array").New(buf, a.Get(bo), a.Get(bl).Int()/4)
case []float64:
a := js.Global().Get(u8a).New(len(s) * 8)
js.CopyBytesToJS(a, sliceToByteSlice(s))
runtime.KeepAlive(s)
buf := a.Get(b)
return js.Global().Get("Float64Array").New(buf, a.Get(bo), a.Get(bl).Int()/8)
default:
// panic("jsutil: unexpected value at S2TA: " + r.TypeOf(s).String())
panic("jsutil: unexpected value at S2TA: ")
}
}